Add interactive graph editor connections: port-based edges, drag-connect, and detach

- Add visible typed ports on graph nodes (colored dots for each edge type)
- Route edges to specific port positions instead of node center
- Drag from output port to compatible input port to create/change connections
- Right-click edge context menu with Disconnect option
- Delete key detaches selected edge
- Mark nested edges (composite layers, zones) as non-editable with dotted style
- Add resolve_ref helper for empty-string sentinel to clear reference fields
- Apply resolve_ref across all storage stores for consistent detach support
- Add connection mapping module (graph-connections.js) with API field resolution
- Add i18n keys for connection operations (en/ru/zh)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:15:33 +03:00
parent ff24ec95e6
commit b370bb7d75
17 changed files with 661 additions and 60 deletions
@@ -336,32 +336,40 @@
/* ── Ports ── */
.graph-port {
stroke: var(--bg-color);
stroke-width: 2;
opacity: 0.85;
transition: r 0.15s, opacity 0.15s;
pointer-events: all;
}
.graph-node:hover .graph-port {
r: 5;
opacity: 1;
}
/* Port output cursor: draggable */
.graph-port-out {
cursor: crosshair;
}
.graph-port circle {
fill: var(--bg-secondary);
stroke: var(--text-muted);
stroke-width: 1.5;
r: 5;
transition: fill 0.15s, stroke 0.15s, r 0.15s;
/* Port interaction states during connection drag */
.graph-port-compatible {
r: 6 !important;
opacity: 1 !important;
cursor: pointer;
filter: drop-shadow(0 0 3px currentColor);
}
.graph-port:hover circle {
fill: var(--primary-color);
stroke: var(--primary-color);
r: 6;
.graph-port-incompatible {
opacity: 0.15 !important;
}
.graph-port.connected circle {
fill: var(--text-secondary);
stroke: var(--text-secondary);
}
.graph-port-label {
fill: var(--text-muted);
font-size: 9px;
font-family: 'DM Sans', sans-serif;
.graph-port-drop-target {
r: 7 !important;
stroke: var(--primary-color) !important;
stroke-width: 3 !important;
filter: drop-shadow(0 0 6px var(--primary-color));
}
/* ── Edges ── */
@@ -398,6 +406,12 @@
opacity: 0.12;
}
/* Nested edges (composite layers, zones) — not drag-editable */
.graph-edge-nested {
stroke-dasharray: 2 2;
opacity: 0.4;
}
/* Edge type colors */
.graph-edge-picture { stroke: #42A5F5; color: #42A5F5; }
.graph-edge-colorstrip { stroke: #66BB6A; color: #66BB6A; }
@@ -433,6 +447,47 @@
pointer-events: none;
}
/* ── Edge context menu ── */
.graph-edge-menu {
position: absolute;
z-index: 40;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 16px var(--shadow-color);
padding: 4px;
min-width: 120px;
}
.graph-edge-menu-item {
display: block;
width: 100%;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-color);
font-size: 0.85rem;
font-family: inherit;
text-align: left;
border-radius: 6px;
cursor: pointer;
transition: background 0.1s;
}
.graph-edge-menu-item:hover {
background: var(--bg-secondary);
}
.graph-edge-menu-item.danger {
color: var(--danger-color);
}
.graph-edge-menu-item.danger:hover {
background: var(--danger-color);
color: var(--primary-contrast);
}
/* ── Hover overlay (action buttons) ── */
.graph-node-overlay {