@utisha/graph-editor 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1666,11 +1666,12 @@ class GraphEditorComponent {
1666
1666
  if (!types || types.length === 0)
1667
1667
  return [];
1668
1668
  // Calculate available height for palette
1669
- // Top toolbar: 60px (12px top + 36px height + 12px gap)
1669
+ // Top toolbar: 72px (12px top + 36px height + 12px gap + 12px extra)
1670
1670
  // Bottom padding: 12px
1671
1671
  // Each item: 40px (36px height + 4px gap)
1672
1672
  const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 800;
1673
- const availableHeight = viewportHeight - 72 - 12 - 12; // toolbar + gaps + bottom padding
1673
+ const toolbarHeight = this.config.toolbar?.enabled !== false ? 72 : 0;
1674
+ const availableHeight = viewportHeight - toolbarHeight - 12 - 12; // toolbar + gaps + bottom padding
1674
1675
  const itemHeight = 40;
1675
1676
  const maxItemsPerColumn = Math.max(1, Math.floor(availableHeight / itemHeight));
1676
1677
  // Split into columns
@@ -1680,6 +1681,22 @@ class GraphEditorComponent {
1680
1681
  }
1681
1682
  return columns;
1682
1683
  }
1684
+ /**
1685
+ * Check whether a toolbar item should be shown.
1686
+ * If `config.toolbar.items` is not set, all items are visible.
1687
+ */
1688
+ showToolbarItem(item) {
1689
+ const items = this.config.toolbar?.items;
1690
+ return !items || items.includes(item);
1691
+ }
1692
+ /**
1693
+ * Check whether a divider should be shown between two toolbar groups.
1694
+ * A divider is shown when at least one item from the group before and
1695
+ * at least one item from the group after are visible.
1696
+ */
1697
+ showToolbarDivider(before, after) {
1698
+ return before.some(i => this.showToolbarItem(i)) && after.some(i => this.showToolbarItem(i));
1699
+ }
1683
1700
  /**
1684
1701
  * Get the position for the node image (top-left corner of image).
1685
1702
  * Uses same positioning logic as icon but accounts for image dimensions.
@@ -2042,7 +2059,7 @@ class GraphEditorComponent {
2042
2059
  };
2043
2060
  }
2044
2061
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: GraphEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2045
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: GraphEditorComponent, isStandalone: true, selector: "graph-editor", inputs: { config: "config", graph: "graph", readonly: "readonly", visualizationMode: "visualizationMode", overlayData: "overlayData" }, outputs: { graphChange: "graphChange", nodeAdded: "nodeAdded", nodeUpdated: "nodeUpdated", nodeRemoved: "nodeRemoved", edgeAdded: "edgeAdded", edgeUpdated: "edgeUpdated", edgeRemoved: "edgeRemoved", selectionChange: "selectionChange", validationChange: "validationChange", nodeClick: "nodeClick", nodeDoubleClick: "nodeDoubleClick", edgeClick: "edgeClick", edgeDoubleClick: "edgeDoubleClick", canvasClick: "canvasClick", contextMenu: "contextMenu" }, host: { attributes: { "tabindex": "0" }, listeners: { "keydown": "onKeyDown($event)" }, styleAttribute: "outline: none;" }, providers: [GraphHistoryService], queries: [{ propertyName: "nodeHtmlTemplate", first: true, predicate: NodeHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "nodeSvgTemplate", first: true, predicate: NodeSvgTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeTemplate", first: true, predicate: EdgeTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "canvasSvgRef", first: true, predicate: ["canvasSvg"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"graph-editor-container\">\n <!-- Canvas with overlaid palette -->\n <div class=\"graph-canvas-wrapper\">\n <!-- Top horizontal toolbar -->\n @if (config.palette?.enabled !== false) {\n <div class=\"graph-toolbar-top\">\n <!-- Tools -->\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'hand'\"\n title=\"Hand tool (move nodes)\"\n (click)=\"switchTool('hand')\"\n >\n <span class=\"icon\">\u270B</span>\n </button>\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'line'\"\n title=\"Line tool (draw connections)\"\n (click)=\"switchTool('line')\"\n >\n <span class=\"icon\">\u2215</span>\n </button>\n\n <div class=\"toolbar-divider\"></div>\n\n <!-- Zoom -->\n <button\n class=\"toolbar-btn\"\n title=\"Zoom in\"\n (click)=\"zoomIn()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M11 8v6M8 11h6\"/>\n </svg>\n </button>\n <button\n class=\"toolbar-btn\"\n title=\"Zoom out\"\n (click)=\"zoomOut()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M8 11h6\"/>\n </svg>\n </button>\n\n <div class=\"toolbar-divider\"></div>\n\n <!-- Actions -->\n <button\n class=\"toolbar-btn\"\n title=\"Auto layout\"\n (click)=\"applyLayout()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n </svg>\n </button>\n <button\n class=\"toolbar-btn\"\n title=\"Fit to screen\"\n (click)=\"fitToScreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\"/>\n </svg>\n </button>\n </div>\n\n <!-- Left vertical palette (node types) - supports multiple columns -->\n <div class=\"graph-palette-container\">\n @for (column of getPaletteColumns(); track $index) {\n <div class=\"graph-palette-column\">\n @for (nodeType of column; track nodeType.type) {\n <button\n class=\"palette-item\"\n [attr.data-node-type]=\"nodeType.type\"\n [attr.title]=\"nodeType.label || nodeType.type\"\n (click)=\"addNode(nodeType.type)\"\n >\n @if (nodeType.iconSvg) {\n <svg\n class=\"palette-icon-svg\"\n [attr.viewBox]=\"nodeType.iconSvg.viewBox || '0 0 24 24'\"\n [attr.fill]=\"nodeType.iconSvg.fill || 'none'\"\n [attr.stroke]=\"nodeType.iconSvg.stroke || '#1D6A96'\"\n [attr.stroke-width]=\"nodeType.iconSvg.strokeWidth || 2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n @for (pathData of splitIconPaths(nodeType.iconSvg.path); track $index) {\n <path [attr.d]=\"pathData\"/>\n }\n </svg>\n } @else if (nodeType.defaultData['imageUrl']) {\n <img\n class=\"palette-icon-img\"\n [src]=\"nodeType.defaultData['imageUrl']\"\n [alt]=\"nodeType.label || nodeType.type\"\n />\n } @else {\n <span class=\"icon\">{{ nodeType.icon || '\u25CF' }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <svg\n #canvasSvg\n [class.tool-line]=\"activeTool() === 'line'\"\n [attr.width]=\"'100%'\"\n [attr.height]=\"'100%'\"\n (mousedown)=\"onCanvasMouseDown($event)\"\n (mousemove)=\"onCanvasMouseMove($event)\"\n (mouseup)=\"onCanvasMouseUp($event)\"\n (wheel)=\"onWheel($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n >\n <!-- Arrow marker definitions -->\n <defs>\n <marker id=\"arrow-end\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-end-selected\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n <marker id=\"arrow-start\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-start-selected\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n </defs>\n\n <!-- Main transform group (pan + zoom) -->\n <g [attr.transform]=\"transform()\">\n <!-- Grid (if enabled) -->\n <!-- Grid (if enabled) - extended to cover viewport during pan -->\n @if (config.canvas?.grid?.enabled) {\n <defs>\n @if (resolvedTheme.canvas.gridType === 'dot') {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <circle [attr.cx]=\"config.canvas!.grid!.size / 2\" [attr.cy]=\"config.canvas!.grid!.size / 2\" r=\"1\" [attr.fill]=\"resolvedTheme.canvas.gridColor\" />\n </pattern>\n } @else {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <path [attr.d]=\"'M ' + config.canvas!.grid!.size + ' 0 L 0 0 0 ' + config.canvas!.grid!.size\" fill=\"none\" [attr.stroke]=\"resolvedTheme.canvas.gridColor\" stroke-width=\"1\" />\n </pattern>\n }\n </defs>\n <rect [attr.x]=\"gridBounds().x\" [attr.y]=\"gridBounds().y\" [attr.width]=\"gridBounds().width\" [attr.height]=\"gridBounds().height\" fill=\"url(#grid)\" />\n }\n\n <!-- Layer 0.5: Preview line for line tool (rubber-band) -->\n @if (previewLine()) {\n <line\n class=\"preview-line\"\n [attr.x1]=\"previewLine()!.source.x\"\n [attr.y1]=\"previewLine()!.source.y\"\n [attr.x2]=\"previewLine()!.target.x\"\n [attr.y2]=\"previewLine()!.target.y\"\n [attr.stroke]=\"resolvedTheme.selection.color\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,6\"\n stroke-linecap=\"round\"\n opacity=\"0.7\"\n />\n }\n\n <!-- Layer 1: Edge paths (behind everything) -->\n @for (edge of internalGraph().edges; track edge.id) {\n <!-- Edge shadow for depth (optional) -->\n @if (shadowsEnabled()) {\n <path\n class=\"edge-shadow\"\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"rgba(0,0,0,0.06)\"\n stroke-width=\"6\"\n fill=\"none\"\n stroke-linecap=\"round\"\n [attr.transform]=\"'translate(1, 2)'\"\n />\n }\n <!-- Invisible wide hit-area for easier clicking (hand tool only) -->\n <path\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"transparent\"\n [attr.stroke-width]=\"20\"\n fill=\"none\"\n class=\"edge-hit-area\"\n [attr.pointer-events]=\"activeTool() === 'hand' ? 'stroke' : 'none'\"\n (click)=\"onEdgeClick($event, edge)\"\n (dblclick)=\"onEdgeDoubleClick($event, edge)\"\n />\n <!-- Visible edge line -->\n @if (edgeTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"edgeTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getEdgeTemplateContext(edge)\" />\n } @else {\n <path class=\"edge-line\" [attr.d]=\"getEdgePath(edge)\"\n [attr.stroke]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStroke : resolvedTheme.edge.stroke\"\n [attr.stroke-width]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStrokeWidth : resolvedTheme.edge.strokeWidth\"\n fill=\"none\" stroke-linecap=\"round\" [class.selected]=\"selection().edges.includes(edge.id)\"\n [attr.marker-end]=\"getEdgeMarkerEnd(edge)\" [attr.marker-start]=\"getEdgeMarkerStart(edge)\"\n pointer-events=\"none\" />\n }\n }\n\n <!-- Layer 2: Nodes -->\n @for (node of internalGraph().nodes; track node.id) {\n <g\n [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\"\n class=\"graph-node\"\n [class.selected]=\"selection().nodes.includes(node.id)\"\n [attr.data-node-id]=\"node.id\"\n (mousedown)=\"onNodeMouseDown($event, node)\"\n (click)=\"onNodeClick($event, node)\"\n (dblclick)=\"nodeDoubleClick.emit(node)\"\n >\n <!-- Priority: nodeHtmlTemplate > nodeSvgTemplate > component > default -->\n @if (nodeHtmlTemplate()?.templateRef) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container [ngTemplateOutlet]=\"nodeHtmlTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n </foreignObject>\n } @else if (nodeSvgTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"nodeSvgTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n } @else if (getNodeComponent(node)) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container *ngComponentOutlet=\"getNodeComponent(node)!; inputs: getNodeComponentInputs(node)\" />\n </foreignObject>\n } @else {\n <!-- Default built-in rendering (same as before but with resolvedTheme) -->\n @if (shadowsEnabled()) {\n <rect class=\"node-shadow\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.shadowColor\" [attr.rx]=\"resolvedTheme.node.borderRadius\"\n transform=\"translate(2, 3)\" style=\"filter: blur(4px);\" />\n }\n <rect class=\"node-bg\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.background\"\n [attr.stroke]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderColor : resolvedTheme.node.borderColor\"\n [attr.stroke-width]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderWidth : resolvedTheme.node.borderWidth\"\n [attr.rx]=\"resolvedTheme.node.borderRadius\" />\n @if (getNodeImage(node)) {\n <image class=\"node-image\" [attr.href]=\"getNodeImage(node)\" [attr.xlink:href]=\"getNodeImage(node)\"\n [attr.x]=\"getImagePosition(node).x\" [attr.y]=\"getImagePosition(node).y\"\n [attr.width]=\"getImageSize(node)\" [attr.height]=\"getImageSize(node)\"\n preserveAspectRatio=\"xMidYMid meet\" />\n } @else {\n <text class=\"node-icon\" [attr.x]=\"getIconPosition(node).x\" [attr.y]=\"getIconPosition(node).y\"\n text-anchor=\"middle\" dominant-baseline=\"middle\" [attr.font-size]=\"getNodeSize(node).height * 0.28\">\n {{ getNodeTypeIcon(node) }}\n </text>\n }\n <text class=\"node-label\" [attr.x]=\"getLabelLineX(node)\" text-anchor=\"middle\"\n [attr.font-size]=\"getWrappedLabel(node).fontSize\" font-weight=\"500\" [attr.fill]=\"resolvedTheme.node.labelColor\">\n @for (line of getWrappedLabel(node).lines; track $index) {\n <tspan [attr.x]=\"getLabelLineX(node)\"\n [attr.y]=\"getLabelLineY(node, $index, getWrappedLabel(node).lines.length, getWrappedLabel(node).lineHeight)\">{{ line }}</tspan>\n }\n </text>\n @if (selection().nodes.includes(node.id) && activeTool() === 'hand' && selection().nodes.length === 1) {\n <rect class=\"resize-handle resize-handle-se\" [attr.x]=\"getNodeSize(node).width - 8\" [attr.y]=\"getNodeSize(node).height - 8\"\n width=\"10\" height=\"10\" [attr.fill]=\"resolvedTheme.selection.color\" rx=\"2\" cursor=\"se-resize\"\n (mousedown)=\"onResizeHandleMouseDown($event, node)\" />\n }\n }\n </g>\n }\n\n <!-- Layer 3: Attachment points (on top of nodes) -->\n @for (node of internalGraph().nodes; track node.id) {\n @if (showAttachmentPoints() === node.id) {\n <g [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\">\n @for (port of getNodePorts(node); track port.position) {\n <circle\n [attr.cx]=\"port.x\"\n [attr.cy]=\"port.y\"\n [attr.r]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverRadius : resolvedTheme.port.radius\"\n [attr.fill]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverFill : resolvedTheme.port.fill\"\n [attr.stroke]=\"resolvedTheme.port.stroke\"\n [attr.stroke-width]=\"resolvedTheme.port.strokeWidth\"\n class=\"attachment-point\"\n [class.hovered]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position\"\n (mousedown)=\"$event.stopPropagation()\"\n (click)=\"onAttachmentPointClick($event, node, port.position)\"\n />\n }\n </g>\n }\n }\n\n <!-- Layer 4: Edge endpoints (only visible when edge is selected) -->\n @for (edge of internalGraph().edges; track edge.id) {\n @if (selection().edges.includes(edge.id)) {\n <g>\n <!-- Source endpoint -->\n <circle\n [attr.cx]=\"getEdgeSourcePoint(edge).x\"\n [attr.cy]=\"getEdgeSourcePoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'source')\"\n />\n\n <!-- Target endpoint -->\n <circle\n [attr.cx]=\"getEdgeTargetPoint(edge).x\"\n [attr.cy]=\"getEdgeTargetPoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'target')\"\n />\n </g>\n }\n }\n\n <!-- Layer 5: Selection box (Shift+drag) -->\n @if (selectionBox()) {\n <rect\n class=\"selection-box\"\n [attr.x]=\"selectionBox()!.x\"\n [attr.y]=\"selectionBox()!.y\"\n [attr.width]=\"selectionBox()!.width\"\n [attr.height]=\"selectionBox()!.height\"\n [attr.fill]=\"resolvedTheme.selection.boxFill\"\n [attr.stroke]=\"resolvedTheme.selection.boxStroke\"\n stroke-width=\"1\"\n stroke-dasharray=\"4,2\"\n />\n }\n </g>\n </svg>\n </div>\n\n <!-- Edge direction selector overlay -->\n @if (selectedEdgeMidpoint()) {\n <div\n class=\"edge-direction-selector\"\n [style.left.px]=\"selectedEdgeMidpoint()!.x\"\n [style.top.px]=\"selectedEdgeMidpoint()!.y\"\n >\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'backward'\"\n title=\"Backward\"\n (click)=\"setEdgeDirection('backward')\"\n >\u2190</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'bidirectional'\"\n title=\"Bidirectional\"\n (click)=\"setEdgeDirection('bidirectional')\"\n >\u2194</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"!selectedEdgeMidpoint()!.edge.direction || selectedEdgeMidpoint()!.edge.direction === 'forward'\"\n title=\"Forward\"\n (click)=\"setEdgeDirection('forward')\"\n >\u2192</button>\n </div>\n }\n</div>\n", styles: [".graph-editor-container{display:flex;width:100%;height:100%;position:relative;background:var(--graph-editor-canvas-bg, #f8f9fa)}.graph-toolbar-top{position:absolute;top:12px;left:12px;display:flex;gap:4px;z-index:10;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.toolbar-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.toolbar-btn:focus-visible{outline:2px solid var(--indigo-400, #818cf8);outline-offset:2px}.toolbar-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f9fafb);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.toolbar-btn:active{transform:translateY(0);box-shadow:none}.toolbar-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-btn.active:hover{background:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);border-color:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-divider{width:1px;background:var(--ge-toolbar-divider, #e5e7eb);align-self:stretch;margin:4px 2px}.graph-palette-container{position:absolute;top:72px;left:12px;display:flex;flex-direction:row;gap:8px;z-index:10}.graph-palette-column{display:flex;flex-direction:column;gap:4px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.palette-item{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.graph-canvas-wrapper{flex:1;position:relative;overflow:hidden}.graph-canvas{width:100%;height:100%;cursor:grab}.graph-canvas:active{cursor:grabbing}.graph-canvas.tool-line,.graph-canvas.tool-line .graph-node{cursor:crosshair}.graph-node{cursor:move;user-select:none;-webkit-user-select:none;transition:transform .1s ease-out}.graph-node:hover .node-bg{stroke:var(--ge-node-border, #cbd5e1)}.graph-node text{pointer-events:none}.graph-node .node-label{font-family:var(--ge-font-family, system-ui, -apple-system, sans-serif)}.graph-node.selected .node-bg{stroke:var(--ge-selection-color, #3b82f6);filter:drop-shadow(0 4px 12px rgba(59,130,246,.25))}.edge-line{transition:stroke .15s,stroke-width .15s}.edge-line.selected{filter:drop-shadow(0 2px 4px rgba(59,130,246,.3))}.edge-hit-area{cursor:pointer}.edge-endpoint{cursor:pointer;transition:r .2s,fill .2s}.edge-endpoint:hover{r:8;fill:var(--ge-selection-color, #3b82f6)}.edge-endpoint.selected{fill:var(--ge-selection-color, #3b82f6)}.attachment-point{cursor:crosshair;transition:all .2s}.attachment-point.hovered{filter:drop-shadow(0 0 4px rgba(37,99,235,.6))}.validation-panel{position:absolute;bottom:0;left:0;right:0;max-height:200px;overflow-y:auto;background:#fff;border-top:1px solid #e5e7eb;padding:16px}.error-item{padding:8px 12px;margin-bottom:8px;background:#fee2e2;border-left:3px solid #ef4444;border-radius:4px;font-size:14px}.error-item.warning{background:#fef3c7;border-left-color:#f59e0b}.edge-direction-selector{position:absolute;transform:translate(-50%,-100%);margin-top:-12px;display:flex;gap:2px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:4px;border-radius:calc(var(--ge-toolbar-radius, 8px) * .75);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .15));backdrop-filter:blur(4px);z-index:20;pointer-events:auto}.direction-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:calc(var(--ge-toolbar-radius, 8px) * .5);background:var(--ge-toolbar-btn-bg, white);cursor:pointer;font-size:16px;transition:all .15s;color:var(--ge-toolbar-btn-color, #6b7280)}.direction-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f3f4f6);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.direction-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.resize-handle{cursor:se-resize;opacity:.8;transition:opacity .15s,fill .15s}.resize-handle:hover{opacity:1;fill:var(--ge-selection-color, #3b82f6)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2062
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: GraphEditorComponent, isStandalone: true, selector: "graph-editor", inputs: { config: "config", graph: "graph", readonly: "readonly", visualizationMode: "visualizationMode", overlayData: "overlayData" }, outputs: { graphChange: "graphChange", nodeAdded: "nodeAdded", nodeUpdated: "nodeUpdated", nodeRemoved: "nodeRemoved", edgeAdded: "edgeAdded", edgeUpdated: "edgeUpdated", edgeRemoved: "edgeRemoved", selectionChange: "selectionChange", validationChange: "validationChange", nodeClick: "nodeClick", nodeDoubleClick: "nodeDoubleClick", edgeClick: "edgeClick", edgeDoubleClick: "edgeDoubleClick", canvasClick: "canvasClick", contextMenu: "contextMenu" }, host: { attributes: { "tabindex": "0" }, listeners: { "keydown": "onKeyDown($event)" }, styleAttribute: "outline: none;" }, providers: [GraphHistoryService], queries: [{ propertyName: "nodeHtmlTemplate", first: true, predicate: NodeHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "nodeSvgTemplate", first: true, predicate: NodeSvgTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeTemplate", first: true, predicate: EdgeTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "canvasSvgRef", first: true, predicate: ["canvasSvg"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"graph-editor-container\">\n <!-- Canvas with overlaid palette -->\n <div class=\"graph-canvas-wrapper\">\n <!-- Top horizontal toolbar -->\n @if (config.toolbar?.enabled !== false) {\n <div class=\"graph-toolbar-top\">\n <!-- Tools -->\n @if (showToolbarItem('hand')) {\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'hand'\"\n title=\"Hand tool (move nodes)\"\n (click)=\"switchTool('hand')\"\n >\n <span class=\"icon\">\u270B</span>\n </button>\n }\n @if (showToolbarItem('line')) {\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'line'\"\n title=\"Line tool (draw connections)\"\n (click)=\"switchTool('line')\"\n >\n <span class=\"icon\">\u2215</span>\n </button>\n }\n\n @if (showToolbarDivider(['hand', 'line'], ['zoom-in', 'zoom-out'])) {\n <div class=\"toolbar-divider\"></div>\n }\n\n <!-- Zoom -->\n @if (showToolbarItem('zoom-in')) {\n <button\n class=\"toolbar-btn\"\n title=\"Zoom in\"\n (click)=\"zoomIn()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M11 8v6M8 11h6\"/>\n </svg>\n </button>\n }\n @if (showToolbarItem('zoom-out')) {\n <button\n class=\"toolbar-btn\"\n title=\"Zoom out\"\n (click)=\"zoomOut()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M8 11h6\"/>\n </svg>\n </button>\n }\n\n @if (showToolbarDivider(['hand', 'line', 'zoom-in', 'zoom-out'], ['layout', 'fit'])) {\n <div class=\"toolbar-divider\"></div>\n }\n\n <!-- Actions -->\n @if (showToolbarItem('layout')) {\n <button\n class=\"toolbar-btn\"\n title=\"Auto layout\"\n (click)=\"applyLayout()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n </svg>\n </button>\n }\n @if (showToolbarItem('fit')) {\n <button\n class=\"toolbar-btn\"\n title=\"Fit to screen\"\n (click)=\"fitToScreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- Left vertical palette (node types) - supports multiple columns -->\n @if (config.palette?.enabled !== false) {\n <div class=\"graph-palette-container\">\n @for (column of getPaletteColumns(); track $index) {\n <div class=\"graph-palette-column\">\n @for (nodeType of column; track nodeType.type) {\n <button\n class=\"palette-item\"\n [attr.data-node-type]=\"nodeType.type\"\n [attr.title]=\"nodeType.label || nodeType.type\"\n (click)=\"addNode(nodeType.type)\"\n >\n @if (nodeType.iconSvg) {\n <svg\n class=\"palette-icon-svg\"\n [attr.viewBox]=\"nodeType.iconSvg.viewBox || '0 0 24 24'\"\n [attr.fill]=\"nodeType.iconSvg.fill || 'none'\"\n [attr.stroke]=\"nodeType.iconSvg.stroke || '#1D6A96'\"\n [attr.stroke-width]=\"nodeType.iconSvg.strokeWidth || 2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n @for (pathData of splitIconPaths(nodeType.iconSvg.path); track $index) {\n <path [attr.d]=\"pathData\"/>\n }\n </svg>\n } @else if (nodeType.defaultData['imageUrl']) {\n <img\n class=\"palette-icon-img\"\n [src]=\"nodeType.defaultData['imageUrl']\"\n [alt]=\"nodeType.label || nodeType.type\"\n />\n } @else {\n <span class=\"icon\">{{ nodeType.icon || '\u25CF' }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <svg\n #canvasSvg\n [class.tool-line]=\"activeTool() === 'line'\"\n [attr.width]=\"'100%'\"\n [attr.height]=\"'100%'\"\n (mousedown)=\"onCanvasMouseDown($event)\"\n (mousemove)=\"onCanvasMouseMove($event)\"\n (mouseup)=\"onCanvasMouseUp($event)\"\n (wheel)=\"onWheel($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n >\n <!-- Arrow marker definitions -->\n <defs>\n <marker id=\"arrow-end\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-end-selected\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n <marker id=\"arrow-start\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-start-selected\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n </defs>\n\n <!-- Main transform group (pan + zoom) -->\n <g [attr.transform]=\"transform()\">\n <!-- Grid (if enabled) -->\n <!-- Grid (if enabled) - extended to cover viewport during pan -->\n @if (config.canvas?.grid?.enabled) {\n <defs>\n @if (resolvedTheme.canvas.gridType === 'dot') {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <circle [attr.cx]=\"config.canvas!.grid!.size / 2\" [attr.cy]=\"config.canvas!.grid!.size / 2\" r=\"1\" [attr.fill]=\"resolvedTheme.canvas.gridColor\" />\n </pattern>\n } @else {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <path [attr.d]=\"'M ' + config.canvas!.grid!.size + ' 0 L 0 0 0 ' + config.canvas!.grid!.size\" fill=\"none\" [attr.stroke]=\"resolvedTheme.canvas.gridColor\" stroke-width=\"1\" />\n </pattern>\n }\n </defs>\n <rect [attr.x]=\"gridBounds().x\" [attr.y]=\"gridBounds().y\" [attr.width]=\"gridBounds().width\" [attr.height]=\"gridBounds().height\" fill=\"url(#grid)\" />\n }\n\n <!-- Layer 0.5: Preview line for line tool (rubber-band) -->\n @if (previewLine()) {\n <line\n class=\"preview-line\"\n [attr.x1]=\"previewLine()!.source.x\"\n [attr.y1]=\"previewLine()!.source.y\"\n [attr.x2]=\"previewLine()!.target.x\"\n [attr.y2]=\"previewLine()!.target.y\"\n [attr.stroke]=\"resolvedTheme.selection.color\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,6\"\n stroke-linecap=\"round\"\n opacity=\"0.7\"\n />\n }\n\n <!-- Layer 1: Edge paths (behind everything) -->\n @for (edge of internalGraph().edges; track edge.id) {\n <!-- Edge shadow for depth (optional) -->\n @if (shadowsEnabled()) {\n <path\n class=\"edge-shadow\"\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"rgba(0,0,0,0.06)\"\n stroke-width=\"6\"\n fill=\"none\"\n stroke-linecap=\"round\"\n [attr.transform]=\"'translate(1, 2)'\"\n />\n }\n <!-- Invisible wide hit-area for easier clicking (hand tool only) -->\n <path\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"transparent\"\n [attr.stroke-width]=\"20\"\n fill=\"none\"\n class=\"edge-hit-area\"\n [attr.pointer-events]=\"activeTool() === 'hand' ? 'stroke' : 'none'\"\n (click)=\"onEdgeClick($event, edge)\"\n (dblclick)=\"onEdgeDoubleClick($event, edge)\"\n />\n <!-- Visible edge line -->\n @if (edgeTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"edgeTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getEdgeTemplateContext(edge)\" />\n } @else {\n <path class=\"edge-line\" [attr.d]=\"getEdgePath(edge)\"\n [attr.stroke]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStroke : resolvedTheme.edge.stroke\"\n [attr.stroke-width]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStrokeWidth : resolvedTheme.edge.strokeWidth\"\n fill=\"none\" stroke-linecap=\"round\" [class.selected]=\"selection().edges.includes(edge.id)\"\n [attr.marker-end]=\"getEdgeMarkerEnd(edge)\" [attr.marker-start]=\"getEdgeMarkerStart(edge)\"\n pointer-events=\"none\" />\n }\n }\n\n <!-- Layer 2: Nodes -->\n @for (node of internalGraph().nodes; track node.id) {\n <g\n [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\"\n class=\"graph-node\"\n [class.selected]=\"selection().nodes.includes(node.id)\"\n [attr.data-node-id]=\"node.id\"\n (mousedown)=\"onNodeMouseDown($event, node)\"\n (click)=\"onNodeClick($event, node)\"\n (dblclick)=\"nodeDoubleClick.emit(node)\"\n >\n <!-- Priority: nodeHtmlTemplate > nodeSvgTemplate > component > default -->\n @if (nodeHtmlTemplate()?.templateRef) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container [ngTemplateOutlet]=\"nodeHtmlTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n </foreignObject>\n } @else if (nodeSvgTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"nodeSvgTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n } @else if (getNodeComponent(node)) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container *ngComponentOutlet=\"getNodeComponent(node)!; inputs: getNodeComponentInputs(node)\" />\n </foreignObject>\n } @else {\n <!-- Default built-in rendering (same as before but with resolvedTheme) -->\n @if (shadowsEnabled()) {\n <rect class=\"node-shadow\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.shadowColor\" [attr.rx]=\"resolvedTheme.node.borderRadius\"\n transform=\"translate(2, 3)\" style=\"filter: blur(4px);\" />\n }\n <rect class=\"node-bg\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.background\"\n [attr.stroke]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderColor : resolvedTheme.node.borderColor\"\n [attr.stroke-width]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderWidth : resolvedTheme.node.borderWidth\"\n [attr.rx]=\"resolvedTheme.node.borderRadius\" />\n @if (getNodeImage(node)) {\n <image class=\"node-image\" [attr.href]=\"getNodeImage(node)\" [attr.xlink:href]=\"getNodeImage(node)\"\n [attr.x]=\"getImagePosition(node).x\" [attr.y]=\"getImagePosition(node).y\"\n [attr.width]=\"getImageSize(node)\" [attr.height]=\"getImageSize(node)\"\n preserveAspectRatio=\"xMidYMid meet\" />\n } @else {\n <text class=\"node-icon\" [attr.x]=\"getIconPosition(node).x\" [attr.y]=\"getIconPosition(node).y\"\n text-anchor=\"middle\" dominant-baseline=\"middle\" [attr.font-size]=\"getNodeSize(node).height * 0.28\">\n {{ getNodeTypeIcon(node) }}\n </text>\n }\n <text class=\"node-label\" [attr.x]=\"getLabelLineX(node)\" text-anchor=\"middle\"\n [attr.font-size]=\"getWrappedLabel(node).fontSize\" font-weight=\"500\" [attr.fill]=\"resolvedTheme.node.labelColor\">\n @for (line of getWrappedLabel(node).lines; track $index) {\n <tspan [attr.x]=\"getLabelLineX(node)\"\n [attr.y]=\"getLabelLineY(node, $index, getWrappedLabel(node).lines.length, getWrappedLabel(node).lineHeight)\">{{ line }}</tspan>\n }\n </text>\n @if (selection().nodes.includes(node.id) && activeTool() === 'hand' && selection().nodes.length === 1) {\n <rect class=\"resize-handle resize-handle-se\" [attr.x]=\"getNodeSize(node).width - 8\" [attr.y]=\"getNodeSize(node).height - 8\"\n width=\"10\" height=\"10\" [attr.fill]=\"resolvedTheme.selection.color\" rx=\"2\" cursor=\"se-resize\"\n (mousedown)=\"onResizeHandleMouseDown($event, node)\" />\n }\n }\n </g>\n }\n\n <!-- Layer 3: Attachment points (on top of nodes) -->\n @for (node of internalGraph().nodes; track node.id) {\n @if (showAttachmentPoints() === node.id) {\n <g [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\">\n @for (port of getNodePorts(node); track port.position) {\n <circle\n [attr.cx]=\"port.x\"\n [attr.cy]=\"port.y\"\n [attr.r]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverRadius : resolvedTheme.port.radius\"\n [attr.fill]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverFill : resolvedTheme.port.fill\"\n [attr.stroke]=\"resolvedTheme.port.stroke\"\n [attr.stroke-width]=\"resolvedTheme.port.strokeWidth\"\n class=\"attachment-point\"\n [class.hovered]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position\"\n (mousedown)=\"$event.stopPropagation()\"\n (click)=\"onAttachmentPointClick($event, node, port.position)\"\n />\n }\n </g>\n }\n }\n\n <!-- Layer 4: Edge endpoints (only visible when edge is selected) -->\n @for (edge of internalGraph().edges; track edge.id) {\n @if (selection().edges.includes(edge.id)) {\n <g>\n <!-- Source endpoint -->\n <circle\n [attr.cx]=\"getEdgeSourcePoint(edge).x\"\n [attr.cy]=\"getEdgeSourcePoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'source')\"\n />\n\n <!-- Target endpoint -->\n <circle\n [attr.cx]=\"getEdgeTargetPoint(edge).x\"\n [attr.cy]=\"getEdgeTargetPoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'target')\"\n />\n </g>\n }\n }\n\n <!-- Layer 5: Selection box (Shift+drag) -->\n @if (selectionBox()) {\n <rect\n class=\"selection-box\"\n [attr.x]=\"selectionBox()!.x\"\n [attr.y]=\"selectionBox()!.y\"\n [attr.width]=\"selectionBox()!.width\"\n [attr.height]=\"selectionBox()!.height\"\n [attr.fill]=\"resolvedTheme.selection.boxFill\"\n [attr.stroke]=\"resolvedTheme.selection.boxStroke\"\n stroke-width=\"1\"\n stroke-dasharray=\"4,2\"\n />\n }\n </g>\n </svg>\n </div>\n\n <!-- Edge direction selector overlay -->\n @if (selectedEdgeMidpoint()) {\n <div\n class=\"edge-direction-selector\"\n [style.left.px]=\"selectedEdgeMidpoint()!.x\"\n [style.top.px]=\"selectedEdgeMidpoint()!.y\"\n >\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'backward'\"\n title=\"Backward\"\n (click)=\"setEdgeDirection('backward')\"\n >\u2190</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'bidirectional'\"\n title=\"Bidirectional\"\n (click)=\"setEdgeDirection('bidirectional')\"\n >\u2194</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"!selectedEdgeMidpoint()!.edge.direction || selectedEdgeMidpoint()!.edge.direction === 'forward'\"\n title=\"Forward\"\n (click)=\"setEdgeDirection('forward')\"\n >\u2192</button>\n </div>\n }\n</div>\n", styles: [".graph-editor-container{display:flex;width:100%;height:100%;position:relative;background:var(--graph-editor-canvas-bg, #f8f9fa)}.graph-toolbar-top{position:absolute;top:12px;left:12px;display:flex;gap:4px;z-index:10;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.toolbar-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.toolbar-btn:focus-visible{outline:2px solid var(--indigo-400, #818cf8);outline-offset:2px}.toolbar-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f9fafb);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.toolbar-btn:active{transform:translateY(0);box-shadow:none}.toolbar-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-btn.active:hover{background:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);border-color:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-divider{width:1px;background:var(--ge-toolbar-divider, #e5e7eb);align-self:stretch;margin:4px 2px}.graph-palette-container{position:absolute;top:72px;left:12px;display:flex;flex-direction:row;gap:8px;z-index:10}.graph-palette-column{display:flex;flex-direction:column;gap:4px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.palette-item{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.graph-canvas-wrapper{flex:1;position:relative;overflow:hidden}.graph-canvas{width:100%;height:100%;cursor:grab}.graph-canvas:active{cursor:grabbing}.graph-canvas.tool-line,.graph-canvas.tool-line .graph-node{cursor:crosshair}.graph-node{cursor:move;user-select:none;-webkit-user-select:none;transition:transform .1s ease-out}.graph-node:hover .node-bg{stroke:var(--ge-node-border, #cbd5e1)}.graph-node text{pointer-events:none}.graph-node .node-label{font-family:var(--ge-font-family, system-ui, -apple-system, sans-serif)}.graph-node.selected .node-bg{stroke:var(--ge-selection-color, #3b82f6);filter:drop-shadow(0 4px 12px rgba(59,130,246,.25))}.edge-line{transition:stroke .15s,stroke-width .15s}.edge-line.selected{filter:drop-shadow(0 2px 4px rgba(59,130,246,.3))}.edge-hit-area{cursor:pointer}.edge-endpoint{cursor:pointer;transition:r .2s,fill .2s}.edge-endpoint:hover{r:8;fill:var(--ge-selection-color, #3b82f6)}.edge-endpoint.selected{fill:var(--ge-selection-color, #3b82f6)}.attachment-point{cursor:crosshair;transition:all .2s}.attachment-point.hovered{filter:drop-shadow(0 0 4px rgba(37,99,235,.6))}.validation-panel{position:absolute;bottom:0;left:0;right:0;max-height:200px;overflow-y:auto;background:#fff;border-top:1px solid #e5e7eb;padding:16px}.error-item{padding:8px 12px;margin-bottom:8px;background:#fee2e2;border-left:3px solid #ef4444;border-radius:4px;font-size:14px}.error-item.warning{background:#fef3c7;border-left-color:#f59e0b}.edge-direction-selector{position:absolute;transform:translate(-50%,-100%);margin-top:-12px;display:flex;gap:2px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:4px;border-radius:calc(var(--ge-toolbar-radius, 8px) * .75);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .15));backdrop-filter:blur(4px);z-index:20;pointer-events:auto}.direction-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:calc(var(--ge-toolbar-radius, 8px) * .5);background:var(--ge-toolbar-btn-bg, white);cursor:pointer;font-size:16px;transition:all .15s;color:var(--ge-toolbar-btn-color, #6b7280)}.direction-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f3f4f6);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.direction-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.resize-handle{cursor:se-resize;opacity:.8;transition:opacity .15s,fill .15s}.resize-handle:hover{opacity:1;fill:var(--ge-selection-color, #3b82f6)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2046
2063
  }
2047
2064
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: GraphEditorComponent, decorators: [{
2048
2065
  type: Component,
@@ -2050,7 +2067,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
2050
2067
  'tabindex': '0',
2051
2068
  'style': 'outline: none;',
2052
2069
  '(keydown)': 'onKeyDown($event)'
2053
- }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"graph-editor-container\">\n <!-- Canvas with overlaid palette -->\n <div class=\"graph-canvas-wrapper\">\n <!-- Top horizontal toolbar -->\n @if (config.palette?.enabled !== false) {\n <div class=\"graph-toolbar-top\">\n <!-- Tools -->\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'hand'\"\n title=\"Hand tool (move nodes)\"\n (click)=\"switchTool('hand')\"\n >\n <span class=\"icon\">\u270B</span>\n </button>\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'line'\"\n title=\"Line tool (draw connections)\"\n (click)=\"switchTool('line')\"\n >\n <span class=\"icon\">\u2215</span>\n </button>\n\n <div class=\"toolbar-divider\"></div>\n\n <!-- Zoom -->\n <button\n class=\"toolbar-btn\"\n title=\"Zoom in\"\n (click)=\"zoomIn()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M11 8v6M8 11h6\"/>\n </svg>\n </button>\n <button\n class=\"toolbar-btn\"\n title=\"Zoom out\"\n (click)=\"zoomOut()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M8 11h6\"/>\n </svg>\n </button>\n\n <div class=\"toolbar-divider\"></div>\n\n <!-- Actions -->\n <button\n class=\"toolbar-btn\"\n title=\"Auto layout\"\n (click)=\"applyLayout()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n </svg>\n </button>\n <button\n class=\"toolbar-btn\"\n title=\"Fit to screen\"\n (click)=\"fitToScreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\"/>\n </svg>\n </button>\n </div>\n\n <!-- Left vertical palette (node types) - supports multiple columns -->\n <div class=\"graph-palette-container\">\n @for (column of getPaletteColumns(); track $index) {\n <div class=\"graph-palette-column\">\n @for (nodeType of column; track nodeType.type) {\n <button\n class=\"palette-item\"\n [attr.data-node-type]=\"nodeType.type\"\n [attr.title]=\"nodeType.label || nodeType.type\"\n (click)=\"addNode(nodeType.type)\"\n >\n @if (nodeType.iconSvg) {\n <svg\n class=\"palette-icon-svg\"\n [attr.viewBox]=\"nodeType.iconSvg.viewBox || '0 0 24 24'\"\n [attr.fill]=\"nodeType.iconSvg.fill || 'none'\"\n [attr.stroke]=\"nodeType.iconSvg.stroke || '#1D6A96'\"\n [attr.stroke-width]=\"nodeType.iconSvg.strokeWidth || 2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n @for (pathData of splitIconPaths(nodeType.iconSvg.path); track $index) {\n <path [attr.d]=\"pathData\"/>\n }\n </svg>\n } @else if (nodeType.defaultData['imageUrl']) {\n <img\n class=\"palette-icon-img\"\n [src]=\"nodeType.defaultData['imageUrl']\"\n [alt]=\"nodeType.label || nodeType.type\"\n />\n } @else {\n <span class=\"icon\">{{ nodeType.icon || '\u25CF' }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <svg\n #canvasSvg\n [class.tool-line]=\"activeTool() === 'line'\"\n [attr.width]=\"'100%'\"\n [attr.height]=\"'100%'\"\n (mousedown)=\"onCanvasMouseDown($event)\"\n (mousemove)=\"onCanvasMouseMove($event)\"\n (mouseup)=\"onCanvasMouseUp($event)\"\n (wheel)=\"onWheel($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n >\n <!-- Arrow marker definitions -->\n <defs>\n <marker id=\"arrow-end\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-end-selected\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n <marker id=\"arrow-start\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-start-selected\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n </defs>\n\n <!-- Main transform group (pan + zoom) -->\n <g [attr.transform]=\"transform()\">\n <!-- Grid (if enabled) -->\n <!-- Grid (if enabled) - extended to cover viewport during pan -->\n @if (config.canvas?.grid?.enabled) {\n <defs>\n @if (resolvedTheme.canvas.gridType === 'dot') {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <circle [attr.cx]=\"config.canvas!.grid!.size / 2\" [attr.cy]=\"config.canvas!.grid!.size / 2\" r=\"1\" [attr.fill]=\"resolvedTheme.canvas.gridColor\" />\n </pattern>\n } @else {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <path [attr.d]=\"'M ' + config.canvas!.grid!.size + ' 0 L 0 0 0 ' + config.canvas!.grid!.size\" fill=\"none\" [attr.stroke]=\"resolvedTheme.canvas.gridColor\" stroke-width=\"1\" />\n </pattern>\n }\n </defs>\n <rect [attr.x]=\"gridBounds().x\" [attr.y]=\"gridBounds().y\" [attr.width]=\"gridBounds().width\" [attr.height]=\"gridBounds().height\" fill=\"url(#grid)\" />\n }\n\n <!-- Layer 0.5: Preview line for line tool (rubber-band) -->\n @if (previewLine()) {\n <line\n class=\"preview-line\"\n [attr.x1]=\"previewLine()!.source.x\"\n [attr.y1]=\"previewLine()!.source.y\"\n [attr.x2]=\"previewLine()!.target.x\"\n [attr.y2]=\"previewLine()!.target.y\"\n [attr.stroke]=\"resolvedTheme.selection.color\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,6\"\n stroke-linecap=\"round\"\n opacity=\"0.7\"\n />\n }\n\n <!-- Layer 1: Edge paths (behind everything) -->\n @for (edge of internalGraph().edges; track edge.id) {\n <!-- Edge shadow for depth (optional) -->\n @if (shadowsEnabled()) {\n <path\n class=\"edge-shadow\"\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"rgba(0,0,0,0.06)\"\n stroke-width=\"6\"\n fill=\"none\"\n stroke-linecap=\"round\"\n [attr.transform]=\"'translate(1, 2)'\"\n />\n }\n <!-- Invisible wide hit-area for easier clicking (hand tool only) -->\n <path\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"transparent\"\n [attr.stroke-width]=\"20\"\n fill=\"none\"\n class=\"edge-hit-area\"\n [attr.pointer-events]=\"activeTool() === 'hand' ? 'stroke' : 'none'\"\n (click)=\"onEdgeClick($event, edge)\"\n (dblclick)=\"onEdgeDoubleClick($event, edge)\"\n />\n <!-- Visible edge line -->\n @if (edgeTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"edgeTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getEdgeTemplateContext(edge)\" />\n } @else {\n <path class=\"edge-line\" [attr.d]=\"getEdgePath(edge)\"\n [attr.stroke]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStroke : resolvedTheme.edge.stroke\"\n [attr.stroke-width]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStrokeWidth : resolvedTheme.edge.strokeWidth\"\n fill=\"none\" stroke-linecap=\"round\" [class.selected]=\"selection().edges.includes(edge.id)\"\n [attr.marker-end]=\"getEdgeMarkerEnd(edge)\" [attr.marker-start]=\"getEdgeMarkerStart(edge)\"\n pointer-events=\"none\" />\n }\n }\n\n <!-- Layer 2: Nodes -->\n @for (node of internalGraph().nodes; track node.id) {\n <g\n [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\"\n class=\"graph-node\"\n [class.selected]=\"selection().nodes.includes(node.id)\"\n [attr.data-node-id]=\"node.id\"\n (mousedown)=\"onNodeMouseDown($event, node)\"\n (click)=\"onNodeClick($event, node)\"\n (dblclick)=\"nodeDoubleClick.emit(node)\"\n >\n <!-- Priority: nodeHtmlTemplate > nodeSvgTemplate > component > default -->\n @if (nodeHtmlTemplate()?.templateRef) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container [ngTemplateOutlet]=\"nodeHtmlTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n </foreignObject>\n } @else if (nodeSvgTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"nodeSvgTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n } @else if (getNodeComponent(node)) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container *ngComponentOutlet=\"getNodeComponent(node)!; inputs: getNodeComponentInputs(node)\" />\n </foreignObject>\n } @else {\n <!-- Default built-in rendering (same as before but with resolvedTheme) -->\n @if (shadowsEnabled()) {\n <rect class=\"node-shadow\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.shadowColor\" [attr.rx]=\"resolvedTheme.node.borderRadius\"\n transform=\"translate(2, 3)\" style=\"filter: blur(4px);\" />\n }\n <rect class=\"node-bg\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.background\"\n [attr.stroke]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderColor : resolvedTheme.node.borderColor\"\n [attr.stroke-width]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderWidth : resolvedTheme.node.borderWidth\"\n [attr.rx]=\"resolvedTheme.node.borderRadius\" />\n @if (getNodeImage(node)) {\n <image class=\"node-image\" [attr.href]=\"getNodeImage(node)\" [attr.xlink:href]=\"getNodeImage(node)\"\n [attr.x]=\"getImagePosition(node).x\" [attr.y]=\"getImagePosition(node).y\"\n [attr.width]=\"getImageSize(node)\" [attr.height]=\"getImageSize(node)\"\n preserveAspectRatio=\"xMidYMid meet\" />\n } @else {\n <text class=\"node-icon\" [attr.x]=\"getIconPosition(node).x\" [attr.y]=\"getIconPosition(node).y\"\n text-anchor=\"middle\" dominant-baseline=\"middle\" [attr.font-size]=\"getNodeSize(node).height * 0.28\">\n {{ getNodeTypeIcon(node) }}\n </text>\n }\n <text class=\"node-label\" [attr.x]=\"getLabelLineX(node)\" text-anchor=\"middle\"\n [attr.font-size]=\"getWrappedLabel(node).fontSize\" font-weight=\"500\" [attr.fill]=\"resolvedTheme.node.labelColor\">\n @for (line of getWrappedLabel(node).lines; track $index) {\n <tspan [attr.x]=\"getLabelLineX(node)\"\n [attr.y]=\"getLabelLineY(node, $index, getWrappedLabel(node).lines.length, getWrappedLabel(node).lineHeight)\">{{ line }}</tspan>\n }\n </text>\n @if (selection().nodes.includes(node.id) && activeTool() === 'hand' && selection().nodes.length === 1) {\n <rect class=\"resize-handle resize-handle-se\" [attr.x]=\"getNodeSize(node).width - 8\" [attr.y]=\"getNodeSize(node).height - 8\"\n width=\"10\" height=\"10\" [attr.fill]=\"resolvedTheme.selection.color\" rx=\"2\" cursor=\"se-resize\"\n (mousedown)=\"onResizeHandleMouseDown($event, node)\" />\n }\n }\n </g>\n }\n\n <!-- Layer 3: Attachment points (on top of nodes) -->\n @for (node of internalGraph().nodes; track node.id) {\n @if (showAttachmentPoints() === node.id) {\n <g [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\">\n @for (port of getNodePorts(node); track port.position) {\n <circle\n [attr.cx]=\"port.x\"\n [attr.cy]=\"port.y\"\n [attr.r]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverRadius : resolvedTheme.port.radius\"\n [attr.fill]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverFill : resolvedTheme.port.fill\"\n [attr.stroke]=\"resolvedTheme.port.stroke\"\n [attr.stroke-width]=\"resolvedTheme.port.strokeWidth\"\n class=\"attachment-point\"\n [class.hovered]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position\"\n (mousedown)=\"$event.stopPropagation()\"\n (click)=\"onAttachmentPointClick($event, node, port.position)\"\n />\n }\n </g>\n }\n }\n\n <!-- Layer 4: Edge endpoints (only visible when edge is selected) -->\n @for (edge of internalGraph().edges; track edge.id) {\n @if (selection().edges.includes(edge.id)) {\n <g>\n <!-- Source endpoint -->\n <circle\n [attr.cx]=\"getEdgeSourcePoint(edge).x\"\n [attr.cy]=\"getEdgeSourcePoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'source')\"\n />\n\n <!-- Target endpoint -->\n <circle\n [attr.cx]=\"getEdgeTargetPoint(edge).x\"\n [attr.cy]=\"getEdgeTargetPoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'target')\"\n />\n </g>\n }\n }\n\n <!-- Layer 5: Selection box (Shift+drag) -->\n @if (selectionBox()) {\n <rect\n class=\"selection-box\"\n [attr.x]=\"selectionBox()!.x\"\n [attr.y]=\"selectionBox()!.y\"\n [attr.width]=\"selectionBox()!.width\"\n [attr.height]=\"selectionBox()!.height\"\n [attr.fill]=\"resolvedTheme.selection.boxFill\"\n [attr.stroke]=\"resolvedTheme.selection.boxStroke\"\n stroke-width=\"1\"\n stroke-dasharray=\"4,2\"\n />\n }\n </g>\n </svg>\n </div>\n\n <!-- Edge direction selector overlay -->\n @if (selectedEdgeMidpoint()) {\n <div\n class=\"edge-direction-selector\"\n [style.left.px]=\"selectedEdgeMidpoint()!.x\"\n [style.top.px]=\"selectedEdgeMidpoint()!.y\"\n >\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'backward'\"\n title=\"Backward\"\n (click)=\"setEdgeDirection('backward')\"\n >\u2190</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'bidirectional'\"\n title=\"Bidirectional\"\n (click)=\"setEdgeDirection('bidirectional')\"\n >\u2194</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"!selectedEdgeMidpoint()!.edge.direction || selectedEdgeMidpoint()!.edge.direction === 'forward'\"\n title=\"Forward\"\n (click)=\"setEdgeDirection('forward')\"\n >\u2192</button>\n </div>\n }\n</div>\n", styles: [".graph-editor-container{display:flex;width:100%;height:100%;position:relative;background:var(--graph-editor-canvas-bg, #f8f9fa)}.graph-toolbar-top{position:absolute;top:12px;left:12px;display:flex;gap:4px;z-index:10;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.toolbar-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.toolbar-btn:focus-visible{outline:2px solid var(--indigo-400, #818cf8);outline-offset:2px}.toolbar-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f9fafb);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.toolbar-btn:active{transform:translateY(0);box-shadow:none}.toolbar-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-btn.active:hover{background:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);border-color:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-divider{width:1px;background:var(--ge-toolbar-divider, #e5e7eb);align-self:stretch;margin:4px 2px}.graph-palette-container{position:absolute;top:72px;left:12px;display:flex;flex-direction:row;gap:8px;z-index:10}.graph-palette-column{display:flex;flex-direction:column;gap:4px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.palette-item{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.graph-canvas-wrapper{flex:1;position:relative;overflow:hidden}.graph-canvas{width:100%;height:100%;cursor:grab}.graph-canvas:active{cursor:grabbing}.graph-canvas.tool-line,.graph-canvas.tool-line .graph-node{cursor:crosshair}.graph-node{cursor:move;user-select:none;-webkit-user-select:none;transition:transform .1s ease-out}.graph-node:hover .node-bg{stroke:var(--ge-node-border, #cbd5e1)}.graph-node text{pointer-events:none}.graph-node .node-label{font-family:var(--ge-font-family, system-ui, -apple-system, sans-serif)}.graph-node.selected .node-bg{stroke:var(--ge-selection-color, #3b82f6);filter:drop-shadow(0 4px 12px rgba(59,130,246,.25))}.edge-line{transition:stroke .15s,stroke-width .15s}.edge-line.selected{filter:drop-shadow(0 2px 4px rgba(59,130,246,.3))}.edge-hit-area{cursor:pointer}.edge-endpoint{cursor:pointer;transition:r .2s,fill .2s}.edge-endpoint:hover{r:8;fill:var(--ge-selection-color, #3b82f6)}.edge-endpoint.selected{fill:var(--ge-selection-color, #3b82f6)}.attachment-point{cursor:crosshair;transition:all .2s}.attachment-point.hovered{filter:drop-shadow(0 0 4px rgba(37,99,235,.6))}.validation-panel{position:absolute;bottom:0;left:0;right:0;max-height:200px;overflow-y:auto;background:#fff;border-top:1px solid #e5e7eb;padding:16px}.error-item{padding:8px 12px;margin-bottom:8px;background:#fee2e2;border-left:3px solid #ef4444;border-radius:4px;font-size:14px}.error-item.warning{background:#fef3c7;border-left-color:#f59e0b}.edge-direction-selector{position:absolute;transform:translate(-50%,-100%);margin-top:-12px;display:flex;gap:2px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:4px;border-radius:calc(var(--ge-toolbar-radius, 8px) * .75);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .15));backdrop-filter:blur(4px);z-index:20;pointer-events:auto}.direction-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:calc(var(--ge-toolbar-radius, 8px) * .5);background:var(--ge-toolbar-btn-bg, white);cursor:pointer;font-size:16px;transition:all .15s;color:var(--ge-toolbar-btn-color, #6b7280)}.direction-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f3f4f6);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.direction-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.resize-handle{cursor:se-resize;opacity:.8;transition:opacity .15s,fill .15s}.resize-handle:hover{opacity:1;fill:var(--ge-selection-color, #3b82f6)}\n"] }]
2070
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"graph-editor-container\">\n <!-- Canvas with overlaid palette -->\n <div class=\"graph-canvas-wrapper\">\n <!-- Top horizontal toolbar -->\n @if (config.toolbar?.enabled !== false) {\n <div class=\"graph-toolbar-top\">\n <!-- Tools -->\n @if (showToolbarItem('hand')) {\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'hand'\"\n title=\"Hand tool (move nodes)\"\n (click)=\"switchTool('hand')\"\n >\n <span class=\"icon\">\u270B</span>\n </button>\n }\n @if (showToolbarItem('line')) {\n <button\n class=\"toolbar-btn\"\n [class.active]=\"activeTool() === 'line'\"\n title=\"Line tool (draw connections)\"\n (click)=\"switchTool('line')\"\n >\n <span class=\"icon\">\u2215</span>\n </button>\n }\n\n @if (showToolbarDivider(['hand', 'line'], ['zoom-in', 'zoom-out'])) {\n <div class=\"toolbar-divider\"></div>\n }\n\n <!-- Zoom -->\n @if (showToolbarItem('zoom-in')) {\n <button\n class=\"toolbar-btn\"\n title=\"Zoom in\"\n (click)=\"zoomIn()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M11 8v6M8 11h6\"/>\n </svg>\n </button>\n }\n @if (showToolbarItem('zoom-out')) {\n <button\n class=\"toolbar-btn\"\n title=\"Zoom out\"\n (click)=\"zoomOut()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"M21 21l-4.35-4.35M8 11h6\"/>\n </svg>\n </button>\n }\n\n @if (showToolbarDivider(['hand', 'line', 'zoom-in', 'zoom-out'], ['layout', 'fit'])) {\n <div class=\"toolbar-divider\"></div>\n }\n\n <!-- Actions -->\n @if (showToolbarItem('layout')) {\n <button\n class=\"toolbar-btn\"\n title=\"Auto layout\"\n (click)=\"applyLayout()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>\n </svg>\n </button>\n }\n @if (showToolbarItem('fit')) {\n <button\n class=\"toolbar-btn\"\n title=\"Fit to screen\"\n (click)=\"fitToScreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- Left vertical palette (node types) - supports multiple columns -->\n @if (config.palette?.enabled !== false) {\n <div class=\"graph-palette-container\">\n @for (column of getPaletteColumns(); track $index) {\n <div class=\"graph-palette-column\">\n @for (nodeType of column; track nodeType.type) {\n <button\n class=\"palette-item\"\n [attr.data-node-type]=\"nodeType.type\"\n [attr.title]=\"nodeType.label || nodeType.type\"\n (click)=\"addNode(nodeType.type)\"\n >\n @if (nodeType.iconSvg) {\n <svg\n class=\"palette-icon-svg\"\n [attr.viewBox]=\"nodeType.iconSvg.viewBox || '0 0 24 24'\"\n [attr.fill]=\"nodeType.iconSvg.fill || 'none'\"\n [attr.stroke]=\"nodeType.iconSvg.stroke || '#1D6A96'\"\n [attr.stroke-width]=\"nodeType.iconSvg.strokeWidth || 2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n @for (pathData of splitIconPaths(nodeType.iconSvg.path); track $index) {\n <path [attr.d]=\"pathData\"/>\n }\n </svg>\n } @else if (nodeType.defaultData['imageUrl']) {\n <img\n class=\"palette-icon-img\"\n [src]=\"nodeType.defaultData['imageUrl']\"\n [alt]=\"nodeType.label || nodeType.type\"\n />\n } @else {\n <span class=\"icon\">{{ nodeType.icon || '\u25CF' }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <svg\n #canvasSvg\n [class.tool-line]=\"activeTool() === 'line'\"\n [attr.width]=\"'100%'\"\n [attr.height]=\"'100%'\"\n (mousedown)=\"onCanvasMouseDown($event)\"\n (mousemove)=\"onCanvasMouseMove($event)\"\n (mouseup)=\"onCanvasMouseUp($event)\"\n (wheel)=\"onWheel($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n >\n <!-- Arrow marker definitions -->\n <defs>\n <marker id=\"arrow-end\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-end-selected\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n <marker id=\"arrow-start\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.markerColor\"/>\n </marker>\n <marker id=\"arrow-start-selected\" viewBox=\"0 0 10 10\" refX=\"2\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" [attr.fill]=\"resolvedTheme.edge.selectedMarkerColor\"/>\n </marker>\n </defs>\n\n <!-- Main transform group (pan + zoom) -->\n <g [attr.transform]=\"transform()\">\n <!-- Grid (if enabled) -->\n <!-- Grid (if enabled) - extended to cover viewport during pan -->\n @if (config.canvas?.grid?.enabled) {\n <defs>\n @if (resolvedTheme.canvas.gridType === 'dot') {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <circle [attr.cx]=\"config.canvas!.grid!.size / 2\" [attr.cy]=\"config.canvas!.grid!.size / 2\" r=\"1\" [attr.fill]=\"resolvedTheme.canvas.gridColor\" />\n </pattern>\n } @else {\n <pattern id=\"grid\" [attr.width]=\"config.canvas!.grid!.size\" [attr.height]=\"config.canvas!.grid!.size\" patternUnits=\"userSpaceOnUse\">\n <path [attr.d]=\"'M ' + config.canvas!.grid!.size + ' 0 L 0 0 0 ' + config.canvas!.grid!.size\" fill=\"none\" [attr.stroke]=\"resolvedTheme.canvas.gridColor\" stroke-width=\"1\" />\n </pattern>\n }\n </defs>\n <rect [attr.x]=\"gridBounds().x\" [attr.y]=\"gridBounds().y\" [attr.width]=\"gridBounds().width\" [attr.height]=\"gridBounds().height\" fill=\"url(#grid)\" />\n }\n\n <!-- Layer 0.5: Preview line for line tool (rubber-band) -->\n @if (previewLine()) {\n <line\n class=\"preview-line\"\n [attr.x1]=\"previewLine()!.source.x\"\n [attr.y1]=\"previewLine()!.source.y\"\n [attr.x2]=\"previewLine()!.target.x\"\n [attr.y2]=\"previewLine()!.target.y\"\n [attr.stroke]=\"resolvedTheme.selection.color\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,6\"\n stroke-linecap=\"round\"\n opacity=\"0.7\"\n />\n }\n\n <!-- Layer 1: Edge paths (behind everything) -->\n @for (edge of internalGraph().edges; track edge.id) {\n <!-- Edge shadow for depth (optional) -->\n @if (shadowsEnabled()) {\n <path\n class=\"edge-shadow\"\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"rgba(0,0,0,0.06)\"\n stroke-width=\"6\"\n fill=\"none\"\n stroke-linecap=\"round\"\n [attr.transform]=\"'translate(1, 2)'\"\n />\n }\n <!-- Invisible wide hit-area for easier clicking (hand tool only) -->\n <path\n [attr.d]=\"getEdgePath(edge)\"\n stroke=\"transparent\"\n [attr.stroke-width]=\"20\"\n fill=\"none\"\n class=\"edge-hit-area\"\n [attr.pointer-events]=\"activeTool() === 'hand' ? 'stroke' : 'none'\"\n (click)=\"onEdgeClick($event, edge)\"\n (dblclick)=\"onEdgeDoubleClick($event, edge)\"\n />\n <!-- Visible edge line -->\n @if (edgeTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"edgeTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getEdgeTemplateContext(edge)\" />\n } @else {\n <path class=\"edge-line\" [attr.d]=\"getEdgePath(edge)\"\n [attr.stroke]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStroke : resolvedTheme.edge.stroke\"\n [attr.stroke-width]=\"selection().edges.includes(edge.id) ? resolvedTheme.edge.selectedStrokeWidth : resolvedTheme.edge.strokeWidth\"\n fill=\"none\" stroke-linecap=\"round\" [class.selected]=\"selection().edges.includes(edge.id)\"\n [attr.marker-end]=\"getEdgeMarkerEnd(edge)\" [attr.marker-start]=\"getEdgeMarkerStart(edge)\"\n pointer-events=\"none\" />\n }\n }\n\n <!-- Layer 2: Nodes -->\n @for (node of internalGraph().nodes; track node.id) {\n <g\n [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\"\n class=\"graph-node\"\n [class.selected]=\"selection().nodes.includes(node.id)\"\n [attr.data-node-id]=\"node.id\"\n (mousedown)=\"onNodeMouseDown($event, node)\"\n (click)=\"onNodeClick($event, node)\"\n (dblclick)=\"nodeDoubleClick.emit(node)\"\n >\n <!-- Priority: nodeHtmlTemplate > nodeSvgTemplate > component > default -->\n @if (nodeHtmlTemplate()?.templateRef) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container [ngTemplateOutlet]=\"nodeHtmlTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n </foreignObject>\n } @else if (nodeSvgTemplate()?.templateRef) {\n <ng-container [ngTemplateOutlet]=\"nodeSvgTemplate()!.templateRef\" [ngTemplateOutletContext]=\"getNodeTemplateContext(node)\" />\n } @else if (getNodeComponent(node)) {\n <foreignObject [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\">\n <ng-container *ngComponentOutlet=\"getNodeComponent(node)!; inputs: getNodeComponentInputs(node)\" />\n </foreignObject>\n } @else {\n <!-- Default built-in rendering (same as before but with resolvedTheme) -->\n @if (shadowsEnabled()) {\n <rect class=\"node-shadow\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.shadowColor\" [attr.rx]=\"resolvedTheme.node.borderRadius\"\n transform=\"translate(2, 3)\" style=\"filter: blur(4px);\" />\n }\n <rect class=\"node-bg\" [attr.width]=\"getNodeSize(node).width\" [attr.height]=\"getNodeSize(node).height\"\n [attr.fill]=\"resolvedTheme.node.background\"\n [attr.stroke]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderColor : resolvedTheme.node.borderColor\"\n [attr.stroke-width]=\"selection().nodes.includes(node.id) ? resolvedTheme.node.selectedBorderWidth : resolvedTheme.node.borderWidth\"\n [attr.rx]=\"resolvedTheme.node.borderRadius\" />\n @if (getNodeImage(node)) {\n <image class=\"node-image\" [attr.href]=\"getNodeImage(node)\" [attr.xlink:href]=\"getNodeImage(node)\"\n [attr.x]=\"getImagePosition(node).x\" [attr.y]=\"getImagePosition(node).y\"\n [attr.width]=\"getImageSize(node)\" [attr.height]=\"getImageSize(node)\"\n preserveAspectRatio=\"xMidYMid meet\" />\n } @else {\n <text class=\"node-icon\" [attr.x]=\"getIconPosition(node).x\" [attr.y]=\"getIconPosition(node).y\"\n text-anchor=\"middle\" dominant-baseline=\"middle\" [attr.font-size]=\"getNodeSize(node).height * 0.28\">\n {{ getNodeTypeIcon(node) }}\n </text>\n }\n <text class=\"node-label\" [attr.x]=\"getLabelLineX(node)\" text-anchor=\"middle\"\n [attr.font-size]=\"getWrappedLabel(node).fontSize\" font-weight=\"500\" [attr.fill]=\"resolvedTheme.node.labelColor\">\n @for (line of getWrappedLabel(node).lines; track $index) {\n <tspan [attr.x]=\"getLabelLineX(node)\"\n [attr.y]=\"getLabelLineY(node, $index, getWrappedLabel(node).lines.length, getWrappedLabel(node).lineHeight)\">{{ line }}</tspan>\n }\n </text>\n @if (selection().nodes.includes(node.id) && activeTool() === 'hand' && selection().nodes.length === 1) {\n <rect class=\"resize-handle resize-handle-se\" [attr.x]=\"getNodeSize(node).width - 8\" [attr.y]=\"getNodeSize(node).height - 8\"\n width=\"10\" height=\"10\" [attr.fill]=\"resolvedTheme.selection.color\" rx=\"2\" cursor=\"se-resize\"\n (mousedown)=\"onResizeHandleMouseDown($event, node)\" />\n }\n }\n </g>\n }\n\n <!-- Layer 3: Attachment points (on top of nodes) -->\n @for (node of internalGraph().nodes; track node.id) {\n @if (showAttachmentPoints() === node.id) {\n <g [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\">\n @for (port of getNodePorts(node); track port.position) {\n <circle\n [attr.cx]=\"port.x\"\n [attr.cy]=\"port.y\"\n [attr.r]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverRadius : resolvedTheme.port.radius\"\n [attr.fill]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? resolvedTheme.port.hoverFill : resolvedTheme.port.fill\"\n [attr.stroke]=\"resolvedTheme.port.stroke\"\n [attr.stroke-width]=\"resolvedTheme.port.strokeWidth\"\n class=\"attachment-point\"\n [class.hovered]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position\"\n (mousedown)=\"$event.stopPropagation()\"\n (click)=\"onAttachmentPointClick($event, node, port.position)\"\n />\n }\n </g>\n }\n }\n\n <!-- Layer 4: Edge endpoints (only visible when edge is selected) -->\n @for (edge of internalGraph().edges; track edge.id) {\n @if (selection().edges.includes(edge.id)) {\n <g>\n <!-- Source endpoint -->\n <circle\n [attr.cx]=\"getEdgeSourcePoint(edge).x\"\n [attr.cy]=\"getEdgeSourcePoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'source')\"\n />\n\n <!-- Target endpoint -->\n <circle\n [attr.cx]=\"getEdgeTargetPoint(edge).x\"\n [attr.cy]=\"getEdgeTargetPoint(edge).y\"\n r=\"6\"\n [attr.fill]=\"resolvedTheme.selection.color\"\n stroke=\"white\"\n stroke-width=\"2\"\n class=\"edge-endpoint selected\"\n (mousedown)=\"onEdgeEndpointMouseDown($event, edge, 'target')\"\n />\n </g>\n }\n }\n\n <!-- Layer 5: Selection box (Shift+drag) -->\n @if (selectionBox()) {\n <rect\n class=\"selection-box\"\n [attr.x]=\"selectionBox()!.x\"\n [attr.y]=\"selectionBox()!.y\"\n [attr.width]=\"selectionBox()!.width\"\n [attr.height]=\"selectionBox()!.height\"\n [attr.fill]=\"resolvedTheme.selection.boxFill\"\n [attr.stroke]=\"resolvedTheme.selection.boxStroke\"\n stroke-width=\"1\"\n stroke-dasharray=\"4,2\"\n />\n }\n </g>\n </svg>\n </div>\n\n <!-- Edge direction selector overlay -->\n @if (selectedEdgeMidpoint()) {\n <div\n class=\"edge-direction-selector\"\n [style.left.px]=\"selectedEdgeMidpoint()!.x\"\n [style.top.px]=\"selectedEdgeMidpoint()!.y\"\n >\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'backward'\"\n title=\"Backward\"\n (click)=\"setEdgeDirection('backward')\"\n >\u2190</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"selectedEdgeMidpoint()!.edge.direction === 'bidirectional'\"\n title=\"Bidirectional\"\n (click)=\"setEdgeDirection('bidirectional')\"\n >\u2194</button>\n <button\n class=\"direction-btn\"\n [class.active]=\"!selectedEdgeMidpoint()!.edge.direction || selectedEdgeMidpoint()!.edge.direction === 'forward'\"\n title=\"Forward\"\n (click)=\"setEdgeDirection('forward')\"\n >\u2192</button>\n </div>\n }\n</div>\n", styles: [".graph-editor-container{display:flex;width:100%;height:100%;position:relative;background:var(--graph-editor-canvas-bg, #f8f9fa)}.graph-toolbar-top{position:absolute;top:12px;left:12px;display:flex;gap:4px;z-index:10;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.toolbar-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.toolbar-btn:focus-visible{outline:2px solid var(--indigo-400, #818cf8);outline-offset:2px}.toolbar-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f9fafb);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.toolbar-btn:active{transform:translateY(0);box-shadow:none}.toolbar-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-btn.active:hover{background:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);border-color:color-mix(in srgb,var(--ge-toolbar-btn-active-bg, #3b82f6) 85%,black);color:var(--ge-toolbar-btn-active-color, white)}.toolbar-divider{width:1px;background:var(--ge-toolbar-divider, #e5e7eb);align-self:stretch;margin:4px 2px}.graph-palette-container{position:absolute;top:72px;left:12px;display:flex;flex-direction:row;gap:8px;z-index:10}.graph-palette-column{display:flex;flex-direction:column;gap:4px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:6px;border-radius:var(--ge-toolbar-radius, 8px);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .1));backdrop-filter:blur(4px)}.palette-item{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1.5px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:var(--ge-toolbar-radius, 8px);background:var(--ge-toolbar-btn-bg, #fff);color:var(--ge-toolbar-btn-color, #4b5563);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all .15s ease;font-size:18px}.graph-canvas-wrapper{flex:1;position:relative;overflow:hidden}.graph-canvas{width:100%;height:100%;cursor:grab}.graph-canvas:active{cursor:grabbing}.graph-canvas.tool-line,.graph-canvas.tool-line .graph-node{cursor:crosshair}.graph-node{cursor:move;user-select:none;-webkit-user-select:none;transition:transform .1s ease-out}.graph-node:hover .node-bg{stroke:var(--ge-node-border, #cbd5e1)}.graph-node text{pointer-events:none}.graph-node .node-label{font-family:var(--ge-font-family, system-ui, -apple-system, sans-serif)}.graph-node.selected .node-bg{stroke:var(--ge-selection-color, #3b82f6);filter:drop-shadow(0 4px 12px rgba(59,130,246,.25))}.edge-line{transition:stroke .15s,stroke-width .15s}.edge-line.selected{filter:drop-shadow(0 2px 4px rgba(59,130,246,.3))}.edge-hit-area{cursor:pointer}.edge-endpoint{cursor:pointer;transition:r .2s,fill .2s}.edge-endpoint:hover{r:8;fill:var(--ge-selection-color, #3b82f6)}.edge-endpoint.selected{fill:var(--ge-selection-color, #3b82f6)}.attachment-point{cursor:crosshair;transition:all .2s}.attachment-point.hovered{filter:drop-shadow(0 0 4px rgba(37,99,235,.6))}.validation-panel{position:absolute;bottom:0;left:0;right:0;max-height:200px;overflow-y:auto;background:#fff;border-top:1px solid #e5e7eb;padding:16px}.error-item{padding:8px 12px;margin-bottom:8px;background:#fee2e2;border-left:3px solid #ef4444;border-radius:4px;font-size:14px}.error-item.warning{background:#fef3c7;border-left-color:#f59e0b}.edge-direction-selector{position:absolute;transform:translate(-50%,-100%);margin-top:-12px;display:flex;gap:2px;background:var(--ge-toolbar-bg, rgba(255, 255, 255, .95));padding:4px;border-radius:calc(var(--ge-toolbar-radius, 8px) * .75);box-shadow:var(--ge-toolbar-shadow, 0 2px 8px rgba(0, 0, 0, .15));backdrop-filter:blur(4px);z-index:20;pointer-events:auto}.direction-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid var(--ge-toolbar-btn-border, #e5e7eb);border-radius:calc(var(--ge-toolbar-radius, 8px) * .5);background:var(--ge-toolbar-btn-bg, white);cursor:pointer;font-size:16px;transition:all .15s;color:var(--ge-toolbar-btn-color, #6b7280)}.direction-btn:hover{background:var(--ge-toolbar-btn-hover-bg, #f3f4f6);border-color:var(--ge-toolbar-btn-hover-accent, #3b82f6);color:var(--ge-toolbar-btn-hover-accent, #3b82f6)}.direction-btn.active{background:var(--ge-toolbar-btn-active-bg, #3b82f6);border-color:var(--ge-toolbar-btn-active-bg, #3b82f6);color:var(--ge-toolbar-btn-active-color, white)}.resize-handle{cursor:se-resize;opacity:.8;transition:opacity .15s,fill .15s}.resize-handle:hover{opacity:1;fill:var(--ge-selection-color, #3b82f6)}\n"] }]
2054
2071
  }], ctorParameters: () => [], propDecorators: { config: [{
2055
2072
  type: Input,
2056
2073
  args: [{ required: true }]