@utisha/graph-editor 1.0.5 → 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.
@@ -1,5 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, Injectable, EventEmitter, viewChild, inject, computed, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { signal, Injectable, inject, TemplateRef, Directive, EventEmitter, viewChild, computed, contentChild, ElementRef, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
3
4
 
4
5
  /**
5
6
  * Service for managing undo/redo history of graph state.
@@ -122,6 +123,226 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
122
123
  type: Injectable
123
124
  }] });
124
125
 
126
+ // ============================================================
127
+ // Template directives
128
+ // ============================================================
129
+ /**
130
+ * Marks an `<ng-template>` as a custom HTML node renderer.
131
+ * Content is rendered inside an `<svg:foreignObject>` — write standard HTML/CSS.
132
+ *
133
+ * @example
134
+ * ```html
135
+ * <graph-editor [config]="config" [graph]="graph">
136
+ * <ng-template geNodeHtml let-ctx>
137
+ * <div class="my-node" [class.selected]="ctx.selected">
138
+ * <div class="header">{{ ctx.type.label }}</div>
139
+ * <div class="body">{{ ctx.node.data.name }}</div>
140
+ * </div>
141
+ * </ng-template>
142
+ * </graph-editor>
143
+ * ```
144
+ */
145
+ class NodeHtmlTemplateDirective {
146
+ templateRef = inject(TemplateRef);
147
+ static ngTemplateContextGuard(_dir, _ctx) {
148
+ return true;
149
+ }
150
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: NodeHtmlTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
151
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.19", type: NodeHtmlTemplateDirective, isStandalone: true, selector: "ng-template[geNodeHtml]", ngImport: i0 });
152
+ }
153
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: NodeHtmlTemplateDirective, decorators: [{
154
+ type: Directive,
155
+ args: [{
156
+ standalone: true,
157
+ selector: 'ng-template[geNodeHtml]',
158
+ }]
159
+ }] });
160
+ /**
161
+ * Marks an `<ng-template>` as a custom SVG node renderer.
162
+ * Content is rendered inside an `<svg:g>` — use `svg:` prefixed elements.
163
+ *
164
+ * @example
165
+ * ```html
166
+ * <graph-editor [config]="config" [graph]="graph">
167
+ * <ng-template geNodeSvg let-ctx>
168
+ * <svg:rect [attr.width]="ctx.width" [attr.height]="ctx.height"
169
+ * rx="8" fill="white" stroke="#ccc" />
170
+ * <svg:text x="10" y="24">{{ ctx.node.data.name }}</svg:text>
171
+ * </ng-template>
172
+ * </graph-editor>
173
+ * ```
174
+ *
175
+ * **Important:** All SVG elements inside the template MUST use the `svg:` prefix
176
+ * (e.g. `<svg:rect>`, `<svg:text>`, `<svg:g>`).
177
+ */
178
+ class NodeSvgTemplateDirective {
179
+ templateRef = inject(TemplateRef);
180
+ static ngTemplateContextGuard(_dir, _ctx) {
181
+ return true;
182
+ }
183
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: NodeSvgTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
184
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.19", type: NodeSvgTemplateDirective, isStandalone: true, selector: "ng-template[geNodeSvg]", ngImport: i0 });
185
+ }
186
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: NodeSvgTemplateDirective, decorators: [{
187
+ type: Directive,
188
+ args: [{
189
+ standalone: true,
190
+ selector: 'ng-template[geNodeSvg]',
191
+ }]
192
+ }] });
193
+ /**
194
+ * Marks an `<ng-template>` as a custom edge renderer.
195
+ * Content is rendered inside an `<svg:g>` — use `svg:` prefixed elements.
196
+ * The library still handles the invisible hit-area and endpoint circles.
197
+ *
198
+ * @example
199
+ * ```html
200
+ * <graph-editor [config]="config" [graph]="graph">
201
+ * <ng-template geEdge let-ctx>
202
+ * <svg:path [attr.d]="ctx.path"
203
+ * [attr.stroke]="ctx.selected ? 'blue' : 'gray'"
204
+ * stroke-width="2" fill="none" />
205
+ * </ng-template>
206
+ * </graph-editor>
207
+ * ```
208
+ */
209
+ class EdgeTemplateDirective {
210
+ templateRef = inject(TemplateRef);
211
+ static ngTemplateContextGuard(_dir, _ctx) {
212
+ return true;
213
+ }
214
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: EdgeTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
215
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.19", type: EdgeTemplateDirective, isStandalone: true, selector: "ng-template[geEdge]", ngImport: i0 });
216
+ }
217
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: EdgeTemplateDirective, decorators: [{
218
+ type: Directive,
219
+ args: [{
220
+ standalone: true,
221
+ selector: 'ng-template[geEdge]',
222
+ }]
223
+ }] });
224
+
225
+ /**
226
+ * Resolves a partial ThemeConfig into a complete ResolvedTheme with all defaults filled.
227
+ */
228
+ function resolveTheme(theme) {
229
+ const selectionColor = theme?.selection?.color ?? '#3b82f6';
230
+ return {
231
+ shadows: theme?.shadows !== false,
232
+ canvas: {
233
+ background: theme?.canvas?.background ?? '#f8f9fa',
234
+ gridType: theme?.canvas?.gridType ?? 'line',
235
+ gridColor: theme?.canvas?.gridColor ?? '#e0e0e0',
236
+ },
237
+ node: {
238
+ background: theme?.node?.background ?? 'white',
239
+ borderColor: theme?.node?.borderColor ?? '#e2e8f0',
240
+ borderWidth: theme?.node?.borderWidth ?? 1.5,
241
+ borderRadius: theme?.node?.borderRadius ?? 12,
242
+ selectedBorderColor: theme?.node?.selectedBorderColor ?? selectionColor,
243
+ selectedBorderWidth: theme?.node?.selectedBorderWidth ?? 2.5,
244
+ shadowColor: theme?.node?.shadowColor ?? 'rgba(0,0,0,0.08)',
245
+ labelColor: theme?.node?.labelColor ?? '#1e293b',
246
+ labelFont: theme?.node?.labelFont ?? 'system-ui, -apple-system, sans-serif',
247
+ typeStyles: theme?.node?.typeStyles ?? {},
248
+ },
249
+ edge: {
250
+ stroke: theme?.edge?.stroke ?? '#94a3b8',
251
+ strokeWidth: theme?.edge?.strokeWidth ?? 2,
252
+ selectedStroke: theme?.edge?.selectedStroke ?? selectionColor,
253
+ selectedStrokeWidth: theme?.edge?.selectedStrokeWidth ?? 2.5,
254
+ markerColor: theme?.edge?.markerColor ?? '#94a3b8',
255
+ selectedMarkerColor: theme?.edge?.selectedMarkerColor ?? selectionColor,
256
+ pathType: theme?.edge?.pathType ?? 'straight',
257
+ },
258
+ port: {
259
+ fill: theme?.port?.fill ?? '#94a3b8',
260
+ stroke: theme?.port?.stroke ?? 'white',
261
+ strokeWidth: theme?.port?.strokeWidth ?? 2,
262
+ radius: theme?.port?.radius ?? 6,
263
+ hoverFill: theme?.port?.hoverFill ?? '#2563eb',
264
+ hoverRadius: theme?.port?.hoverRadius ?? 8,
265
+ },
266
+ selection: {
267
+ color: selectionColor,
268
+ boxFill: theme?.selection?.boxFill ?? `rgba(59, 130, 246, 0.1)`,
269
+ boxStroke: theme?.selection?.boxStroke ?? selectionColor,
270
+ },
271
+ font: {
272
+ family: theme?.font?.family ?? 'system-ui, -apple-system, sans-serif',
273
+ monoFamily: theme?.font?.monoFamily ?? 'monospace',
274
+ },
275
+ toolbar: resolveToolbar(theme?.toolbar, selectionColor),
276
+ };
277
+ }
278
+ function resolveToolbar(toolbar, selectionColor) {
279
+ return {
280
+ background: toolbar?.background ?? 'rgba(255, 255, 255, 0.95)',
281
+ borderRadius: toolbar?.borderRadius ?? 8,
282
+ shadow: toolbar?.shadow ?? '0 2px 8px rgba(0, 0, 0, 0.1)',
283
+ buttonBackground: toolbar?.buttonBackground ?? '#ffffff',
284
+ buttonBorderColor: toolbar?.buttonBorderColor ?? '#e5e7eb',
285
+ buttonTextColor: toolbar?.buttonTextColor ?? '#4b5563',
286
+ buttonHoverBackground: toolbar?.buttonHoverBackground ?? '#f9fafb',
287
+ buttonHoverAccent: toolbar?.buttonHoverAccent ?? selectionColor,
288
+ buttonActiveBackground: toolbar?.buttonActiveBackground ?? selectionColor,
289
+ buttonActiveTextColor: toolbar?.buttonActiveTextColor ?? '#ffffff',
290
+ dividerColor: toolbar?.dividerColor ?? '#e5e7eb',
291
+ };
292
+ }
293
+ /**
294
+ * Applies resolved theme values as CSS custom properties on a host element.
295
+ * This enables consumer templates to use `var(--ge-*)` in their CSS.
296
+ */
297
+ function applyThemeCssProperties(host, t, userVars) {
298
+ const style = host.style;
299
+ // Canvas
300
+ style.setProperty('--ge-canvas-bg', t.canvas.background);
301
+ style.setProperty('--ge-grid-color', t.canvas.gridColor);
302
+ // Node
303
+ style.setProperty('--ge-node-bg', t.node.background);
304
+ style.setProperty('--ge-node-border', t.node.borderColor);
305
+ style.setProperty('--ge-node-border-width', `${t.node.borderWidth}px`);
306
+ style.setProperty('--ge-node-border-radius', `${t.node.borderRadius}px`);
307
+ style.setProperty('--ge-node-selected-border', t.node.selectedBorderColor);
308
+ style.setProperty('--ge-node-selected-border-width', `${t.node.selectedBorderWidth}px`);
309
+ style.setProperty('--ge-node-shadow', t.node.shadowColor);
310
+ style.setProperty('--ge-node-label-color', t.node.labelColor);
311
+ style.setProperty('--ge-node-label-font', t.node.labelFont);
312
+ // Edge
313
+ style.setProperty('--ge-edge-stroke', t.edge.stroke);
314
+ style.setProperty('--ge-edge-stroke-width', `${t.edge.strokeWidth}px`);
315
+ style.setProperty('--ge-edge-selected-stroke', t.edge.selectedStroke);
316
+ // Port
317
+ style.setProperty('--ge-port-fill', t.port.fill);
318
+ style.setProperty('--ge-port-hover-fill', t.port.hoverFill);
319
+ // Selection
320
+ style.setProperty('--ge-selection-color', t.selection.color);
321
+ // Font
322
+ style.setProperty('--ge-font-family', t.font.family);
323
+ style.setProperty('--ge-font-mono', t.font.monoFamily);
324
+ // Toolbar
325
+ style.setProperty('--ge-toolbar-bg', t.toolbar.background);
326
+ style.setProperty('--ge-toolbar-radius', `${t.toolbar.borderRadius}px`);
327
+ style.setProperty('--ge-toolbar-shadow', t.toolbar.shadow);
328
+ style.setProperty('--ge-toolbar-btn-bg', t.toolbar.buttonBackground);
329
+ style.setProperty('--ge-toolbar-btn-border', t.toolbar.buttonBorderColor);
330
+ style.setProperty('--ge-toolbar-btn-color', t.toolbar.buttonTextColor);
331
+ style.setProperty('--ge-toolbar-btn-hover-bg', t.toolbar.buttonHoverBackground);
332
+ style.setProperty('--ge-toolbar-btn-hover-accent', t.toolbar.buttonHoverAccent);
333
+ style.setProperty('--ge-toolbar-btn-active-bg', t.toolbar.buttonActiveBackground);
334
+ style.setProperty('--ge-toolbar-btn-active-color', t.toolbar.buttonActiveTextColor);
335
+ style.setProperty('--ge-toolbar-divider', t.toolbar.dividerColor);
336
+ // Backward compat: --graph-editor-canvas-bg
337
+ style.setProperty('--graph-editor-canvas-bg', t.canvas.background);
338
+ // User-provided custom variables
339
+ if (userVars) {
340
+ for (const [key, value] of Object.entries(userVars)) {
341
+ style.setProperty(key.startsWith('--') ? key : `--${key}`, value);
342
+ }
343
+ }
344
+ }
345
+
125
346
  /**
126
347
  * Main graph editor component.
127
348
  *
@@ -208,8 +429,14 @@ class GraphEditorComponent {
208
429
  height: viewportHeight * 2
209
430
  };
210
431
  });
211
- // Shadow configuration (defaults to true)
212
- shadowsEnabled = computed(() => this.config.theme?.shadows !== false);
432
+ // Resolved theme (filled defaults)
433
+ resolvedTheme;
434
+ // Shadow configuration (derived from resolved theme)
435
+ shadowsEnabled = computed(() => this.resolvedTheme?.shadows ?? true);
436
+ // Template queries (signal-based contentChild)
437
+ nodeHtmlTemplate = contentChild(NodeHtmlTemplateDirective);
438
+ nodeSvgTemplate = contentChild(NodeSvgTemplateDirective);
439
+ edgeTemplate = contentChild(EdgeTemplateDirective);
213
440
  // Selected edge info for direction selector positioning
214
441
  selectedEdgeMidpoint = computed(() => {
215
442
  const sel = this.selection();
@@ -228,14 +455,25 @@ class GraphEditorComponent {
228
455
  y: midY * this.scale() + this.panY()
229
456
  };
230
457
  });
458
+ hostEl = inject(ElementRef);
231
459
  constructor() { }
232
460
  ngOnChanges(changes) {
233
461
  // Sync graph input to internal signal
234
462
  if (changes['graph'] && changes['graph'].currentValue) {
235
463
  this.internalGraph.set(structuredClone(changes['graph'].currentValue));
236
464
  }
465
+ // Re-resolve theme when config changes
466
+ if (changes['config']) {
467
+ this.resolvedTheme = resolveTheme(this.config.theme);
468
+ applyThemeCssProperties(this.hostEl.nativeElement, this.resolvedTheme, this.config.theme?.variables);
469
+ }
237
470
  }
238
471
  ngOnInit() {
472
+ // Resolve theme (first time, in case ngOnChanges didn't fire for config)
473
+ if (!this.resolvedTheme) {
474
+ this.resolvedTheme = resolveTheme(this.config.theme);
475
+ applyThemeCssProperties(this.hostEl.nativeElement, this.resolvedTheme, this.config.theme?.variables);
476
+ }
239
477
  // Initialize with current graph value
240
478
  if (this.graph) {
241
479
  this.internalGraph.set(structuredClone(this.graph));
@@ -1252,16 +1490,57 @@ class GraphEditorComponent {
1252
1490
  const targetNode = this.internalGraph().nodes.find(n => n.id === edge.target);
1253
1491
  if (!sourceNode || !targetNode)
1254
1492
  return '';
1255
- // Get port positions from edge or calculate closest
1256
1493
  const sourcePort = edge.sourcePort || this.findClosestPortForEdge(sourceNode, targetNode, 'source');
1257
1494
  const targetPort = edge.targetPort || this.findClosestPortForEdge(targetNode, sourceNode, 'target');
1258
- const sourcePoint = this.getPortWorldPosition(sourceNode, sourcePort);
1259
- const targetPoint = this.getPortWorldPosition(targetNode, targetPort);
1260
- // Simple straight line
1261
- return `M ${sourcePoint.x},${sourcePoint.y} L ${targetPoint.x},${targetPoint.y}`;
1495
+ const s = this.getPortWorldPosition(sourceNode, sourcePort);
1496
+ const t = this.getPortWorldPosition(targetNode, targetPort);
1497
+ const pathType = this.resolvedTheme.edge.pathType;
1498
+ if (pathType === 'bezier') {
1499
+ const offset = Math.max(40, Math.abs(t.x - s.x) * 0.3, Math.abs(t.y - s.y) * 0.3);
1500
+ const sc = this.getPortControlOffset(sourcePort, offset);
1501
+ const tc = this.getPortControlOffset(targetPort, offset);
1502
+ // Blend a small cross-axis component so the bezier tangent at endpoints
1503
+ // isn't purely axis-aligned — this makes arrowheads follow the curve naturally.
1504
+ const crossBias = 0.15;
1505
+ const dx = t.x - s.x;
1506
+ const dy = t.y - s.y;
1507
+ const sc1x = s.x + sc.dx + (sc.dx !== 0 ? 0 : dx * crossBias);
1508
+ const sc1y = s.y + sc.dy + (sc.dy !== 0 ? 0 : dy * crossBias);
1509
+ const tc1x = t.x + tc.dx + (tc.dx !== 0 ? 0 : dx * -crossBias);
1510
+ const tc1y = t.y + tc.dy + (tc.dy !== 0 ? 0 : dy * -crossBias);
1511
+ return `M ${s.x},${s.y} C ${sc1x},${sc1y} ${tc1x},${tc1y} ${t.x},${t.y}`;
1512
+ }
1513
+ if (pathType === 'step') {
1514
+ const midX = (s.x + t.x) / 2;
1515
+ const midY = (s.y + t.y) / 2;
1516
+ const isSourceVertical = sourcePort === 'top' || sourcePort === 'bottom';
1517
+ const isTargetVertical = targetPort === 'top' || targetPort === 'bottom';
1518
+ if (isSourceVertical && isTargetVertical) {
1519
+ return `M ${s.x},${s.y} L ${s.x},${midY} L ${t.x},${midY} L ${t.x},${t.y}`;
1520
+ }
1521
+ else if (!isSourceVertical && !isTargetVertical) {
1522
+ return `M ${s.x},${s.y} L ${midX},${s.y} L ${midX},${t.y} L ${t.x},${t.y}`;
1523
+ }
1524
+ else if (isSourceVertical) {
1525
+ return `M ${s.x},${s.y} L ${s.x},${t.y} L ${t.x},${t.y}`;
1526
+ }
1527
+ else {
1528
+ return `M ${s.x},${s.y} L ${t.x},${s.y} L ${t.x},${t.y}`;
1529
+ }
1530
+ }
1531
+ return `M ${s.x},${s.y} L ${t.x},${t.y}`;
1532
+ }
1533
+ /** Get the control point offset direction for a port (used by bezier path). */
1534
+ getPortControlOffset(port, offset) {
1535
+ switch (port) {
1536
+ case 'top': return { dx: 0, dy: -offset };
1537
+ case 'bottom': return { dx: 0, dy: offset };
1538
+ case 'left': return { dx: -offset, dy: 0 };
1539
+ case 'right': return { dx: offset, dy: 0 };
1540
+ }
1262
1541
  }
1263
1542
  getEdgeColor(edge) {
1264
- return edge.metadata?.style?.stroke || this.config.edges.style?.stroke || '#94a3b8';
1543
+ return edge.metadata?.style?.stroke || this.resolvedTheme.edge.stroke;
1265
1544
  }
1266
1545
  getEdgeMarkerEnd(edge) {
1267
1546
  const dir = edge.direction || 'forward';
@@ -1387,11 +1666,12 @@ class GraphEditorComponent {
1387
1666
  if (!types || types.length === 0)
1388
1667
  return [];
1389
1668
  // Calculate available height for palette
1390
- // Top toolbar: 60px (12px top + 36px height + 12px gap)
1669
+ // Top toolbar: 72px (12px top + 36px height + 12px gap + 12px extra)
1391
1670
  // Bottom padding: 12px
1392
1671
  // Each item: 40px (36px height + 4px gap)
1393
1672
  const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 800;
1394
- 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
1395
1675
  const itemHeight = 40;
1396
1676
  const maxItemsPerColumn = Math.max(1, Math.floor(availableHeight / itemHeight));
1397
1677
  // Split into columns
@@ -1401,6 +1681,22 @@ class GraphEditorComponent {
1401
1681
  }
1402
1682
  return columns;
1403
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
+ }
1404
1700
  /**
1405
1701
  * Get the position for the node image (top-left corner of image).
1406
1702
  * Uses same positioning logic as icon but accounts for image dimensions.
@@ -1722,16 +2018,56 @@ class GraphEditorComponent {
1722
2018
  return dy > 0 ? 'bottom' : 'top';
1723
2019
  }
1724
2020
  }
2021
+ /** Get the component type for a node (from NodeTypeDefinition.component, if set). */
2022
+ getNodeComponent(node) {
2023
+ const nodeConfig = this.config.nodes.types.find(t => t.type === node.type);
2024
+ return nodeConfig?.component ?? null;
2025
+ }
2026
+ /** Build inputs map for ngComponentOutlet when rendering a node's custom component. */
2027
+ getNodeComponentInputs(node) {
2028
+ const size = this.getNodeSize(node);
2029
+ return {
2030
+ node,
2031
+ selected: this.selection().nodes.includes(node.id),
2032
+ width: size.width,
2033
+ height: size.height,
2034
+ config: this.config,
2035
+ };
2036
+ }
2037
+ /** Build the template context for custom node templates. */
2038
+ getNodeTemplateContext(node) {
2039
+ const nodeConfig = this.config.nodes.types.find(t => t.type === node.type);
2040
+ const size = this.getNodeSize(node);
2041
+ return {
2042
+ $implicit: {
2043
+ node,
2044
+ type: nodeConfig,
2045
+ selected: this.selection().nodes.includes(node.id),
2046
+ width: size.width,
2047
+ height: size.height,
2048
+ },
2049
+ };
2050
+ }
2051
+ /** Build the template context for custom edge templates. */
2052
+ getEdgeTemplateContext(edge) {
2053
+ return {
2054
+ $implicit: {
2055
+ edge,
2056
+ path: this.getEdgePath(edge),
2057
+ selected: this.selection().edges.includes(edge.id),
2058
+ },
2059
+ };
2060
+ }
1725
2061
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: GraphEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1726
- 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], 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=\"9\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" fill=\"#94a3b8\"/>\n </marker>\n <marker id=\"arrow-end-selected\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" fill=\"#3b82f6\"/>\n </marker>\n <marker id=\"arrow-start\" viewBox=\"0 0 10 10\" refX=\"1\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" fill=\"#94a3b8\"/>\n </marker>\n <marker id=\"arrow-start-selected\" viewBox=\"0 0 10 10\" refX=\"1\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" fill=\"#3b82f6\"/>\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 <pattern\n id=\"grid\"\n [attr.width]=\"config.canvas!.grid!.size\"\n [attr.height]=\"config.canvas!.grid!.size\"\n patternUnits=\"userSpaceOnUse\"\n >\n <path\n [attr.d]=\"'M ' + config.canvas!.grid!.size + ' 0 L 0 0 0 ' + config.canvas!.grid!.size\"\n fill=\"none\"\n [attr.stroke]=\"config.canvas!.grid!.color || '#e0e0e0'\"\n stroke-width=\"1\"\n />\n </pattern>\n </defs>\n <!-- Extended grid background covering viewport + pan offset -->\n <rect\n [attr.x]=\"gridBounds().x\"\n [attr.y]=\"gridBounds().y\"\n [attr.width]=\"gridBounds().width\"\n [attr.height]=\"gridBounds().height\"\n fill=\"url(#grid)\"\n />\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 stroke=\"#3b82f6\"\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 <path\n class=\"edge-line\"\n [attr.d]=\"getEdgePath(edge)\"\n [attr.stroke]=\"selection().edges.includes(edge.id) ? '#3b82f6' : '#94a3b8'\"\n [attr.stroke-width]=\"selection().edges.includes(edge.id) ? 2.5 : 2\"\n fill=\"none\"\n stroke-linecap=\"round\"\n [class.selected]=\"selection().edges.includes(edge.id)\"\n [attr.marker-end]=\"getEdgeMarkerEnd(edge)\"\n [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 <!-- Node shadow (optional) -->\n @if (shadowsEnabled()) {\n <rect\n class=\"node-shadow\"\n [attr.width]=\"getNodeSize(node).width\"\n [attr.height]=\"getNodeSize(node).height\"\n fill=\"rgba(0,0,0,0.08)\"\n rx=\"12\"\n transform=\"translate(2, 3)\"\n style=\"filter: blur(4px);\"\n />\n }\n <!-- Node background -->\n <rect\n class=\"node-bg\"\n [attr.width]=\"getNodeSize(node).width\"\n [attr.height]=\"getNodeSize(node).height\"\n fill=\"white\"\n [attr.stroke]=\"selection().nodes.includes(node.id) ? '#3b82f6' : '#e2e8f0'\"\n [attr.stroke-width]=\"selection().nodes.includes(node.id) ? 2.5 : 1.5\"\n rx=\"12\"\n />\n\n <!-- Node type icon (text/emoji) or custom image -->\n @if (getNodeImage(node)) {\n <image\n class=\"node-image\"\n [attr.href]=\"getNodeImage(node)\"\n [attr.xlink:href]=\"getNodeImage(node)\"\n [attr.x]=\"getImagePosition(node).x\"\n [attr.y]=\"getImagePosition(node).y\"\n [attr.width]=\"getImageSize(node)\"\n [attr.height]=\"getImageSize(node)\"\n preserveAspectRatio=\"xMidYMid meet\"\n />\n } @else {\n <text\n class=\"node-icon\"\n [attr.x]=\"getIconPosition(node).x\"\n [attr.y]=\"getIconPosition(node).y\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n [attr.font-size]=\"getNodeSize(node).height * 0.28\"\n >\n {{ getNodeTypeIcon(node) }}\n </text>\n }\n\n <!-- Node label (wrapped and auto-sized) -->\n <text\n class=\"node-label\"\n [attr.x]=\"getLabelLineX(node)\"\n text-anchor=\"middle\"\n [attr.font-size]=\"getWrappedLabel(node).fontSize\"\n font-weight=\"500\"\n fill=\"#1e293b\"\n >\n @for (line of getWrappedLabel(node).lines; track $index) {\n <tspan\n [attr.x]=\"getLabelLineX(node)\"\n [attr.y]=\"getLabelLineY(node, $index, getWrappedLabel(node).lines.length, getWrappedLabel(node).lineHeight)\"\n >{{ line }}</tspan>\n }\n </text>\n\n <!-- Resize handle (SE corner, visible when selected + hand tool) -->\n @if (selection().nodes.includes(node.id) && activeTool() === 'hand' && selection().nodes.length === 1) {\n <rect\n class=\"resize-handle resize-handle-se\"\n [attr.x]=\"getNodeSize(node).width - 8\"\n [attr.y]=\"getNodeSize(node).height - 8\"\n width=\"10\"\n height=\"10\"\n fill=\"#3b82f6\"\n rx=\"2\"\n 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 ? 8 : 6\"\n [attr.fill]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? '#2563eb' : '#94a3b8'\"\n stroke=\"white\"\n stroke-width=\"2\"\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 fill=\"#3b82f6\"\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 fill=\"#3b82f6\"\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 fill=\"rgba(59, 130, 246, 0.1)\"\n stroke=\"#3b82f6\"\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:#fffffff2;padding:6px;border-radius:var(--radius-md, 8px);box-shadow:0 2px 8px #0000001a;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(--neutral-200, #e5e7eb);border-radius:var(--radius-md, 8px);background:var(--white, #fff);color:var(--neutral-600, #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(--neutral-50, #f9fafb);border-color:var(--interactive, #3b82f6);color:var(--interactive, #3b82f6)}.toolbar-btn:active{transform:translateY(0);box-shadow:none}.toolbar-btn.active{background:var(--interactive, #3b82f6);border-color:var(--interactive, #3b82f6);color:#fff}.toolbar-btn.active:hover{background:var(--interactive-hover, #2563eb);border-color:var(--interactive-hover, #2563eb);color:#fff}.toolbar-divider{width:1px;background:var(--neutral-200, #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:#fffffff2;padding:6px;border-radius:var(--radius-md, 8px);box-shadow:0 2px 8px #0000001a;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(--neutral-200, #e5e7eb);border-radius:var(--radius-md, 8px);background:var(--white, #fff);color:var(--neutral-600, #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:#cbd5e1}.graph-node text{pointer-events:none}.graph-node .node-label{font-family:system-ui,-apple-system,sans-serif}.graph-node.selected .node-bg{stroke:#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:#2563eb}.edge-endpoint.selected{fill:#2563eb}.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:#fffffff2;padding:4px;border-radius:6px;box-shadow:0 2px 8px #00000026;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 #e5e7eb;border-radius:4px;background:#fff;cursor:pointer;font-size:16px;transition:all .15s;color:#6b7280}.direction-btn:hover{background:#f3f4f6;border-color:#3b82f6;color:#3b82f6}.direction-btn.active{background:#3b82f6;border-color:#3b82f6;color:#fff}.resize-handle{cursor:se-resize;opacity:.8;transition:opacity .15s,fill .15s}.resize-handle:hover{opacity:1;fill:#2563eb}\n"], 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 });
1727
2063
  }
1728
2064
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: GraphEditorComponent, decorators: [{
1729
2065
  type: Component,
1730
- args: [{ selector: 'graph-editor', standalone: true, imports: [], providers: [GraphHistoryService], host: {
2066
+ args: [{ selector: 'graph-editor', standalone: true, imports: [NgTemplateOutlet, NgComponentOutlet], providers: [GraphHistoryService], host: {
1731
2067
  'tabindex': '0',
1732
2068
  'style': 'outline: none;',
1733
2069
  '(keydown)': 'onKeyDown($event)'
1734
- }, 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=\"9\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" fill=\"#94a3b8\"/>\n </marker>\n <marker id=\"arrow-end-selected\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 0 1 L 8 5 L 0 9 z\" fill=\"#3b82f6\"/>\n </marker>\n <marker id=\"arrow-start\" viewBox=\"0 0 10 10\" refX=\"1\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" fill=\"#94a3b8\"/>\n </marker>\n <marker id=\"arrow-start-selected\" viewBox=\"0 0 10 10\" refX=\"1\" refY=\"5\"\n markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 10 1 L 2 5 L 10 9 z\" fill=\"#3b82f6\"/>\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 <pattern\n id=\"grid\"\n [attr.width]=\"config.canvas!.grid!.size\"\n [attr.height]=\"config.canvas!.grid!.size\"\n patternUnits=\"userSpaceOnUse\"\n >\n <path\n [attr.d]=\"'M ' + config.canvas!.grid!.size + ' 0 L 0 0 0 ' + config.canvas!.grid!.size\"\n fill=\"none\"\n [attr.stroke]=\"config.canvas!.grid!.color || '#e0e0e0'\"\n stroke-width=\"1\"\n />\n </pattern>\n </defs>\n <!-- Extended grid background covering viewport + pan offset -->\n <rect\n [attr.x]=\"gridBounds().x\"\n [attr.y]=\"gridBounds().y\"\n [attr.width]=\"gridBounds().width\"\n [attr.height]=\"gridBounds().height\"\n fill=\"url(#grid)\"\n />\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 stroke=\"#3b82f6\"\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 <path\n class=\"edge-line\"\n [attr.d]=\"getEdgePath(edge)\"\n [attr.stroke]=\"selection().edges.includes(edge.id) ? '#3b82f6' : '#94a3b8'\"\n [attr.stroke-width]=\"selection().edges.includes(edge.id) ? 2.5 : 2\"\n fill=\"none\"\n stroke-linecap=\"round\"\n [class.selected]=\"selection().edges.includes(edge.id)\"\n [attr.marker-end]=\"getEdgeMarkerEnd(edge)\"\n [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 <!-- Node shadow (optional) -->\n @if (shadowsEnabled()) {\n <rect\n class=\"node-shadow\"\n [attr.width]=\"getNodeSize(node).width\"\n [attr.height]=\"getNodeSize(node).height\"\n fill=\"rgba(0,0,0,0.08)\"\n rx=\"12\"\n transform=\"translate(2, 3)\"\n style=\"filter: blur(4px);\"\n />\n }\n <!-- Node background -->\n <rect\n class=\"node-bg\"\n [attr.width]=\"getNodeSize(node).width\"\n [attr.height]=\"getNodeSize(node).height\"\n fill=\"white\"\n [attr.stroke]=\"selection().nodes.includes(node.id) ? '#3b82f6' : '#e2e8f0'\"\n [attr.stroke-width]=\"selection().nodes.includes(node.id) ? 2.5 : 1.5\"\n rx=\"12\"\n />\n\n <!-- Node type icon (text/emoji) or custom image -->\n @if (getNodeImage(node)) {\n <image\n class=\"node-image\"\n [attr.href]=\"getNodeImage(node)\"\n [attr.xlink:href]=\"getNodeImage(node)\"\n [attr.x]=\"getImagePosition(node).x\"\n [attr.y]=\"getImagePosition(node).y\"\n [attr.width]=\"getImageSize(node)\"\n [attr.height]=\"getImageSize(node)\"\n preserveAspectRatio=\"xMidYMid meet\"\n />\n } @else {\n <text\n class=\"node-icon\"\n [attr.x]=\"getIconPosition(node).x\"\n [attr.y]=\"getIconPosition(node).y\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n [attr.font-size]=\"getNodeSize(node).height * 0.28\"\n >\n {{ getNodeTypeIcon(node) }}\n </text>\n }\n\n <!-- Node label (wrapped and auto-sized) -->\n <text\n class=\"node-label\"\n [attr.x]=\"getLabelLineX(node)\"\n text-anchor=\"middle\"\n [attr.font-size]=\"getWrappedLabel(node).fontSize\"\n font-weight=\"500\"\n fill=\"#1e293b\"\n >\n @for (line of getWrappedLabel(node).lines; track $index) {\n <tspan\n [attr.x]=\"getLabelLineX(node)\"\n [attr.y]=\"getLabelLineY(node, $index, getWrappedLabel(node).lines.length, getWrappedLabel(node).lineHeight)\"\n >{{ line }}</tspan>\n }\n </text>\n\n <!-- Resize handle (SE corner, visible when selected + hand tool) -->\n @if (selection().nodes.includes(node.id) && activeTool() === 'hand' && selection().nodes.length === 1) {\n <rect\n class=\"resize-handle resize-handle-se\"\n [attr.x]=\"getNodeSize(node).width - 8\"\n [attr.y]=\"getNodeSize(node).height - 8\"\n width=\"10\"\n height=\"10\"\n fill=\"#3b82f6\"\n rx=\"2\"\n 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 ? 8 : 6\"\n [attr.fill]=\"hoveredPort?.nodeId === node.id && hoveredPort?.port === port.position ? '#2563eb' : '#94a3b8'\"\n stroke=\"white\"\n stroke-width=\"2\"\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 fill=\"#3b82f6\"\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 fill=\"#3b82f6\"\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 fill=\"rgba(59, 130, 246, 0.1)\"\n stroke=\"#3b82f6\"\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:#fffffff2;padding:6px;border-radius:var(--radius-md, 8px);box-shadow:0 2px 8px #0000001a;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(--neutral-200, #e5e7eb);border-radius:var(--radius-md, 8px);background:var(--white, #fff);color:var(--neutral-600, #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(--neutral-50, #f9fafb);border-color:var(--interactive, #3b82f6);color:var(--interactive, #3b82f6)}.toolbar-btn:active{transform:translateY(0);box-shadow:none}.toolbar-btn.active{background:var(--interactive, #3b82f6);border-color:var(--interactive, #3b82f6);color:#fff}.toolbar-btn.active:hover{background:var(--interactive-hover, #2563eb);border-color:var(--interactive-hover, #2563eb);color:#fff}.toolbar-divider{width:1px;background:var(--neutral-200, #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:#fffffff2;padding:6px;border-radius:var(--radius-md, 8px);box-shadow:0 2px 8px #0000001a;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(--neutral-200, #e5e7eb);border-radius:var(--radius-md, 8px);background:var(--white, #fff);color:var(--neutral-600, #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:#cbd5e1}.graph-node text{pointer-events:none}.graph-node .node-label{font-family:system-ui,-apple-system,sans-serif}.graph-node.selected .node-bg{stroke:#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:#2563eb}.edge-endpoint.selected{fill:#2563eb}.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:#fffffff2;padding:4px;border-radius:6px;box-shadow:0 2px 8px #00000026;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 #e5e7eb;border-radius:4px;background:#fff;cursor:pointer;font-size:16px;transition:all .15s;color:#6b7280}.direction-btn:hover{background:#f3f4f6;border-color:#3b82f6;color:#3b82f6}.direction-btn.active{background:#3b82f6;border-color:#3b82f6;color:#fff}.resize-handle{cursor:se-resize;opacity:.8;transition:opacity .15s,fill .15s}.resize-handle:hover{opacity:1;fill:#2563eb}\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"] }]
1735
2071
  }], ctorParameters: () => [], propDecorators: { config: [{
1736
2072
  type: Input,
1737
2073
  args: [{ required: true }]
@@ -1799,5 +2135,5 @@ function iconToDataUrl(icon, size = 48) {
1799
2135
  * Generated bundle index. Do not edit.
1800
2136
  */
1801
2137
 
1802
- export { GraphEditorComponent, iconToDataUrl, renderIconSvg };
2138
+ export { EdgeTemplateDirective, GraphEditorComponent, NodeHtmlTemplateDirective, NodeSvgTemplateDirective, iconToDataUrl, renderIconSvg };
1803
2139
  //# sourceMappingURL=utisha-graph-editor.mjs.map