@praxisui/page-builder 0.0.1

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.
@@ -0,0 +1,1129 @@
1
+ import * as i3 from '@angular/common';
2
+ import { CommonModule } from '@angular/common';
3
+ import * as i0 from '@angular/core';
4
+ import { EventEmitter, HostListener, Output, Input, Directive, Injectable, signal, Optional, Inject, ChangeDetectionStrategy, Component } from '@angular/core';
5
+ import * as i4 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
7
+ import * as i2$2 from '@angular/material/button';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import * as i3$1 from '@angular/material/icon';
10
+ import { MatIconModule } from '@angular/material/icon';
11
+ import * as i3$2 from '@angular/material/tooltip';
12
+ import { MatTooltipModule } from '@angular/material/tooltip';
13
+ import { BehaviorSubject } from 'rxjs';
14
+ import * as i2$1 from '@praxisui/settings-panel';
15
+ import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
16
+ import { ConnectionBuilderComponent } from './praxisui-page-builder.mjs';
17
+ import * as i2 from '@praxisui/core';
18
+
19
+ /**
20
+ * Simple elbow router (L-shape): horizontal then vertical.
21
+ * Produces an SVG path string.
22
+ */
23
+ function routeElbow(from, to) {
24
+ const midX = (from.x + to.x) / 2;
25
+ const p = [
26
+ `M ${from.x} ${from.y}`,
27
+ `L ${midX} ${from.y}`,
28
+ `L ${midX} ${to.y}`,
29
+ `L ${to.x} ${to.y}`,
30
+ ];
31
+ return p.join(' ');
32
+ }
33
+ /** Straight line path */
34
+ function routeStraight(from, to) {
35
+ return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
36
+ }
37
+ /** Simple cubic bezier: horizontal tangents from endpoints */
38
+ function routeBezier(from, to) {
39
+ const dx = (to.x - from.x) * 0.5;
40
+ const c1 = { x: from.x + dx, y: from.y };
41
+ const c2 = { x: to.x - dx, y: to.y };
42
+ return `M ${from.x} ${from.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${to.x} ${to.y}`;
43
+ }
44
+ /** Basic geometry helpers for auto-routing */
45
+ function rectContains(r, p) {
46
+ return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
47
+ }
48
+ function segmentsIntersect(p1, p2, p3, p4) {
49
+ // cross product approach
50
+ const d = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
51
+ if (d === 0)
52
+ return false;
53
+ const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / d;
54
+ const ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / d;
55
+ return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
56
+ }
57
+ function lineIntersectsRect(p1, p2, r) {
58
+ if (rectContains(r, p1) || rectContains(r, p2))
59
+ return true;
60
+ const tl = { x: r.x, y: r.y };
61
+ const tr = { x: r.x + r.width, y: r.y };
62
+ const bl = { x: r.x, y: r.y + r.height };
63
+ const br = { x: r.x + r.width, y: r.y + r.height };
64
+ return (segmentsIntersect(p1, p2, tl, tr) ||
65
+ segmentsIntersect(p1, p2, tr, br) ||
66
+ segmentsIntersect(p1, p2, br, bl) ||
67
+ segmentsIntersect(p1, p2, bl, tl));
68
+ }
69
+ /**
70
+ * Auto router: choose straight/bezier if clear, else elbow to avoid boxes.
71
+ * Obstacles are rectangles in world space. Optional padding inflates obstacles.
72
+ */
73
+ function routeAuto(from, to, obstacles = [], padding = 6) {
74
+ const inflated = obstacles.map(r => ({ x: r.x - padding, y: r.y - padding, width: r.width + padding * 2, height: r.height + padding * 2 }));
75
+ const intersects = inflated.some(r => lineIntersectsRect(from, to, r));
76
+ if (intersects)
77
+ return routeElbow(from, to);
78
+ // prefer bezier for aesthetics when unobstructed
79
+ return routeBezier(from, to);
80
+ }
81
+
82
+ class DragConnectDirective {
83
+ el;
84
+ from;
85
+ connectDragStart = new EventEmitter();
86
+ connectDragMove = new EventEmitter();
87
+ connectDragEnd = new EventEmitter();
88
+ active = false;
89
+ onMove = (e) => this.handleMove(e);
90
+ onUp = (e) => this.handleUp(e);
91
+ constructor(el) {
92
+ this.el = el;
93
+ }
94
+ onMouseDown(ev) {
95
+ if (!this.from)
96
+ return;
97
+ ev.preventDefault();
98
+ ev.stopPropagation();
99
+ this.active = true;
100
+ const rect = this.el.nativeElement.getBoundingClientRect();
101
+ const x = rect.left + rect.width / 2;
102
+ const y = rect.top + rect.height / 2;
103
+ try {
104
+ console.debug('[DragConnect] mousedown start', { from: this.from, x, y, target: ev.target?.tagName });
105
+ }
106
+ catch { }
107
+ this.connectDragStart.emit({ from: this.from, x, y });
108
+ document.addEventListener('mousemove', this.onMove);
109
+ document.addEventListener('mouseup', this.onUp, { once: true });
110
+ }
111
+ handleMove(e) {
112
+ if (!this.active)
113
+ return;
114
+ try {
115
+ console.debug('[DragConnect] mousemove', { x: e.clientX, y: e.clientY });
116
+ }
117
+ catch { }
118
+ this.connectDragMove.emit({ x: e.clientX, y: e.clientY });
119
+ }
120
+ handleUp(e) {
121
+ if (!this.active)
122
+ return;
123
+ this.active = false;
124
+ document.removeEventListener('mousemove', this.onMove);
125
+ try {
126
+ console.debug('[DragConnect] mouseup end', { x: e.clientX, y: e.clientY, target: e.target?.tagName, classes: e.target?.className });
127
+ }
128
+ catch { }
129
+ this.connectDragEnd.emit({ x: e.clientX, y: e.clientY, endEvent: e });
130
+ }
131
+ ngOnDestroy() {
132
+ document.removeEventListener('mousemove', this.onMove);
133
+ }
134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragConnectDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
135
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.4", type: DragConnectDirective, isStandalone: true, selector: "[dragConnect]", inputs: { from: ["dragConnect", "from"] }, outputs: { connectDragStart: "connectDragStart", connectDragMove: "connectDragMove", connectDragEnd: "connectDragEnd" }, host: { listeners: { "mousedown": "onMouseDown($event)" } }, ngImport: i0 });
136
+ }
137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragConnectDirective, decorators: [{
138
+ type: Directive,
139
+ args: [{
140
+ selector: '[dragConnect]'
141
+ }]
142
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { from: [{
143
+ type: Input,
144
+ args: ['dragConnect']
145
+ }], connectDragStart: [{
146
+ type: Output
147
+ }], connectDragMove: [{
148
+ type: Output
149
+ }], connectDragEnd: [{
150
+ type: Output
151
+ }], onMouseDown: [{
152
+ type: HostListener,
153
+ args: ['mousedown', ['$event']]
154
+ }] } });
155
+
156
+ class GraphMapperService {
157
+ registry;
158
+ constructor(registry) {
159
+ this.registry = registry;
160
+ }
161
+ /** Build nodes and edges for a given page definition and widgets. */
162
+ mapToGraph(page, widgets) {
163
+ const nodes = [];
164
+ const nodeById = new Map();
165
+ const tabsCfgByKey = new Map();
166
+ const edges = [];
167
+ // Layout: simple columns — sources left, targets right
168
+ const colWidth = 240;
169
+ const rowHeight = 96;
170
+ const gap = 28;
171
+ let leftY = 0;
172
+ let rightY = 0;
173
+ // First pass: create a node per widget
174
+ for (const w of widgets) {
175
+ const meta = this.registry.get(w.definition.id);
176
+ const metaOutputs = new Map();
177
+ for (const o of meta?.outputs || [])
178
+ metaOutputs.set(o.name, { label: o.label, description: o.description });
179
+ const outputs = new Set([(meta?.outputs || []).map((o) => o.name)].flat());
180
+ if (w.definition.id === 'praxis-tabs')
181
+ outputs.add('widgetEvent');
182
+ if (w.definition.id === 'praxis-stepper')
183
+ outputs.add('widgetEvent');
184
+ const metaInputs = new Map();
185
+ for (const i of meta?.inputs || [])
186
+ metaInputs.set(i.name, { label: i.label, description: i.description });
187
+ // Show both instance-defined inputs and metadata-declared inputs (e.g., resourceId)
188
+ const inputNames = Array.from(new Set([
189
+ ...Object.keys(w.definition.inputs || {}),
190
+ ...(meta?.inputs || []).map((m) => m.name),
191
+ ]));
192
+ const inputPorts = inputNames.map((name, i) => ({
193
+ id: `in:${name}`,
194
+ label: metaInputs.get(name)?.label || name,
195
+ description: metaInputs.get(name)?.description,
196
+ kind: 'input',
197
+ anchor: { x: colWidth, y: 24 + i * 22 },
198
+ }));
199
+ // Generic port for new inputs
200
+ inputPorts.push({ id: 'in:*', label: 'novo input…', kind: 'input', anchor: { x: colWidth, y: 24 + inputNames.length * 22 } });
201
+ const outputPorts = Array.from(outputs.values()).map((name, i) => ({
202
+ id: `out:${name}`,
203
+ label: metaOutputs.get(name)?.label || name,
204
+ description: metaOutputs.get(name)?.description,
205
+ kind: 'output',
206
+ anchor: { x: 0, y: 24 + i * 22 },
207
+ }));
208
+ const left = outputPorts.length > 0;
209
+ const pos = left ? { x: gap, y: gap + leftY } : { x: colWidth + gap * 3, y: gap + rightY };
210
+ const node = {
211
+ id: w.key,
212
+ label: meta?.friendlyName || w.definition.id || w.key,
213
+ icon: meta?.icon,
214
+ type: w.definition.id,
215
+ parentId: null,
216
+ collapsed: true,
217
+ bounds: { x: pos.x, y: pos.y, width: colWidth, height: rowHeight },
218
+ ports: [...outputPorts.map((p) => ({ ...p, anchor: { x: pos.x, y: pos.y + p.anchor.y } })), ...inputPorts.map((p) => ({ ...p, anchor: { x: pos.x + colWidth, y: pos.y + p.anchor.y } }))],
219
+ };
220
+ nodes.push(node);
221
+ nodeById.set(node.id, node);
222
+ if (left)
223
+ leftY += rowHeight + gap;
224
+ else
225
+ rightY += rowHeight + gap;
226
+ // Expand Tabs internals as child nodes (collapsed by default)
227
+ if (w.definition.id === 'praxis-tabs') {
228
+ const cfg = w.definition?.inputs?.config;
229
+ tabsCfgByKey.set(w.key, cfg);
230
+ // Group tabs
231
+ const tabs = (cfg?.tabs || []);
232
+ for (let ti = 0; ti < tabs.length; ti++) {
233
+ const inner = tabs[ti]?.widgets || [];
234
+ for (let wi = 0; wi < inner.length; wi++) {
235
+ const innerDef = inner[wi];
236
+ const innerMeta = this.registry.get(innerDef?.id);
237
+ const innerInputs = Object.keys(innerDef?.inputs || {});
238
+ const innerMetaInputs = new Map();
239
+ for (const i of innerMeta?.inputs || [])
240
+ innerMetaInputs.set(i.name, { label: i.label, description: i.description });
241
+ const sectionLabel = cfg?.tabs?.[ti]?.textLabel;
242
+ const containerTitle = sectionLabel ? `Aba: ${sectionLabel}` : `Aba ${ti + 1}`;
243
+ const widgetLabel = innerMeta?.friendlyName || innerDef?.id || `widget ${wi}`;
244
+ const inPorts = innerInputs.map((name, ii) => {
245
+ const path = `inputs.config.tabs[${ti}].widgets[${wi}].inputs.${name}`;
246
+ const base = innerMetaInputs.get(name);
247
+ const label = base?.label || name;
248
+ const description = (base?.description ? base.description + '\n' : '') + `${containerTitle} • ${widgetLabel} • ${name}\n${path}`;
249
+ return { id: `in:${name}`, label, description, kind: 'input', path, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (ti * 72) + ii * 18 } };
250
+ });
251
+ if (!inPorts.length)
252
+ inPorts.push({ id: 'in:*', label: 'novo input…', kind: 'input', path: `inputs.config.tabs[${ti}].widgets[${wi}].inputs.`, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (ti * 72) } });
253
+ const child = {
254
+ id: `${w.key}#group[${ti}]/${wi}`,
255
+ label: innerMeta?.friendlyName || innerDef?.id || `widget ${wi}`,
256
+ icon: innerMeta?.icon,
257
+ type: innerDef?.id || 'inner',
258
+ parentId: w.key,
259
+ collapsed: false,
260
+ bounds: { x: node.bounds.x + 24, y: node.bounds.y + rowHeight + (ti * 72) + wi * 32, width: colWidth - 48, height: 48 },
261
+ ports: inPorts,
262
+ };
263
+ nodes.push(child);
264
+ nodeById.set(child.id, child);
265
+ }
266
+ }
267
+ // Nav links
268
+ const links = (cfg?.nav?.links || []);
269
+ for (let li = 0; li < links.length; li++) {
270
+ const inner = links[li]?.widgets || [];
271
+ for (let wi = 0; wi < inner.length; wi++) {
272
+ const innerDef = inner[wi];
273
+ const innerMeta = this.registry.get(innerDef?.id);
274
+ const innerInputs = Object.keys(innerDef?.inputs || {});
275
+ const innerMetaInputs = new Map();
276
+ for (const i of innerMeta?.inputs || [])
277
+ innerMetaInputs.set(i.name, { label: i.label, description: i.description });
278
+ const linkLabel = cfg?.nav?.links?.[li]?.label;
279
+ const containerTitle = linkLabel ? `Link: ${linkLabel}` : `Link ${li + 1}`;
280
+ const widgetLabel = innerMeta?.friendlyName || innerDef?.id || `widget ${wi}`;
281
+ const inPorts = innerInputs.map((name, ii) => {
282
+ const path = `inputs.config.nav.links[${li}].widgets[${wi}].inputs.${name}`;
283
+ const base = innerMetaInputs.get(name);
284
+ const label = base?.label || name;
285
+ const description = (base?.description ? base.description + '\n' : '') + `${containerTitle} • ${widgetLabel} • ${name}\n${path}`;
286
+ return { id: `in:${name}`, label, description, kind: 'input', path, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (li * 72) + ii * 18 } };
287
+ });
288
+ if (!inPorts.length)
289
+ inPorts.push({ id: 'in:*', label: 'novo input…', kind: 'input', path: `inputs.config.nav.links[${li}].widgets[${wi}].inputs.`, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (li * 72) } });
290
+ const child = {
291
+ id: `${w.key}#nav[${li}]/${wi}`,
292
+ label: innerMeta?.friendlyName || innerDef?.id || `link ${wi}`,
293
+ icon: innerMeta?.icon,
294
+ type: innerDef?.id || 'inner',
295
+ parentId: w.key,
296
+ collapsed: false,
297
+ bounds: { x: node.bounds.x + 24, y: node.bounds.y + rowHeight + (li * 72) + wi * 32, width: colWidth - 48, height: 48 },
298
+ ports: inPorts,
299
+ };
300
+ nodes.push(child);
301
+ nodeById.set(child.id, child);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ // Second pass: map edges from page.connections
307
+ const conns = (page?.connections || []);
308
+ for (let i = 0; i < conns.length; i++) {
309
+ const c = conns[i];
310
+ const fromNodeId = c.from.widget;
311
+ const fromPortId = `out:${c.from.output}`;
312
+ let toNodeId = c.to.widget;
313
+ let toPortId = `in:${c.to.input}`;
314
+ const deep = this.parseTabsPath(c.to.input);
315
+ let friendlyPath;
316
+ if (deep) {
317
+ const childId = `${c.to.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
318
+ // If such child exists, route edge to it and its input name
319
+ if (nodes.some((n) => n.id === childId)) {
320
+ toNodeId = childId;
321
+ toPortId = `in:${deep.input}`;
322
+ }
323
+ else {
324
+ // Keep to widget but still expose label/path
325
+ toPortId = `in:${deep.input}`;
326
+ }
327
+ const cfg = tabsCfgByKey.get(c.to.widget);
328
+ const labelFromCfg = deep.kind === 'group' ? cfg?.tabs?.[deep.index]?.textLabel : cfg?.nav?.links?.[deep.index]?.label;
329
+ const containerLabel = labelFromCfg ? (deep.kind === 'group' ? `Aba: ${labelFromCfg}` : `Link: ${labelFromCfg}`) : (deep.kind === 'group' ? `Aba ${deep.index + 1}` : `Link ${deep.index + 1}`);
330
+ const child = nodes.find(n => n.id === childId);
331
+ friendlyPath = `${containerLabel} • ${(child?.label || 'Widget')} • ${deep.input}`;
332
+ }
333
+ // Ensure ports exist for visualization (create synthetic when missing)
334
+ this.ensurePort(nodeById, fromNodeId, fromPortId, 'output', colWidth);
335
+ this.ensurePort(nodeById, toNodeId, toPortId, 'input', colWidth);
336
+ const edge = {
337
+ id: `e${i}`,
338
+ from: { nodeId: fromNodeId, portId: fromPortId },
339
+ to: { nodeId: toNodeId, portId: toPortId },
340
+ label: c.map || undefined,
341
+ meta: { map: c.map, bindingOrder: c.to.bindingOrder, ...(friendlyPath ? { friendlyPath } : {}) },
342
+ };
343
+ edges.push(edge);
344
+ }
345
+ return { nodes, edges };
346
+ }
347
+ ensurePort(nodeById, nodeId, portId, kind, colWidth) {
348
+ const n = nodeById.get(nodeId);
349
+ if (!n)
350
+ return;
351
+ const exists = (n.ports || []).some(p => p.id === portId);
352
+ if (exists)
353
+ return;
354
+ const label = portId.replace(/^(in:|out:)/, '');
355
+ const portsOfKind = (n.ports || []).filter(p => p.kind === kind);
356
+ const idx = portsOfKind.length;
357
+ const y = n.bounds.y + 24 + idx * 22;
358
+ const x = kind === 'output' ? n.bounds.x : n.bounds.x + n.bounds.width;
359
+ const p = { id: portId, label, kind, anchor: { x, y } };
360
+ n.ports = [...(n.ports || []), p];
361
+ // Optionally grow node height to accommodate more ports
362
+ const approxBottom = y + 28 - n.bounds.y;
363
+ if (approxBottom > n.bounds.height)
364
+ n.bounds = { ...n.bounds, height: approxBottom };
365
+ }
366
+ /**
367
+ * Parse dot-path for Tabs internals. Supported:
368
+ * - inputs.config.tabs[<i>].widgets[<j>].inputs.<input>
369
+ * - inputs.config.nav.links[<i>].widgets[<j>].inputs.<input>
370
+ */
371
+ parseTabsPath(path) {
372
+ if (!path)
373
+ return null;
374
+ const groupRe = /^inputs\.config\.tabs\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/;
375
+ const navRe = /^inputs\.config\.nav\.links\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/;
376
+ const g = path.match(groupRe);
377
+ if (g)
378
+ return { kind: 'group', index: Number(g[1]), widgetIndex: Number(g[2]), input: g[3] };
379
+ const n = path.match(navRe);
380
+ if (n)
381
+ return { kind: 'nav', index: Number(n[1]), widgetIndex: Number(n[2]), input: n[3] };
382
+ return null;
383
+ }
384
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GraphMapperService, deps: [{ token: i2.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
385
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GraphMapperService, providedIn: 'root' });
386
+ }
387
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GraphMapperService, decorators: [{
388
+ type: Injectable,
389
+ args: [{ providedIn: 'root' }]
390
+ }], ctorParameters: () => [{ type: i2.ComponentMetadataRegistry }] });
391
+
392
+ class ConnectionGraphComponent {
393
+ mapper;
394
+ settingsPanel;
395
+ injectedData;
396
+ page;
397
+ widgets;
398
+ pageChange = new EventEmitter();
399
+ nodes = signal([], ...(ngDevMode ? [{ debugName: "nodes" }] : []));
400
+ edges = signal([], ...(ngDevMode ? [{ debugName: "edges" }] : []));
401
+ connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : []));
402
+ showPortLabels = true;
403
+ showEdgeLabels = false;
404
+ showUnconnectedPorts = true;
405
+ showDetails = true;
406
+ showFriendlyLabels = true;
407
+ selectedEdgeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "selectedEdgeIndex" }] : []));
408
+ // Panning/zoom
409
+ pan = { x: 0, y: 0 };
410
+ zoom = 1;
411
+ transform = signal('', ...(ngDevMode ? [{ debugName: "transform" }] : []));
412
+ // Hover state
413
+ hoverEdgeId = '';
414
+ // Drag state
415
+ dragFrom;
416
+ dragPath = signal('', ...(ngDevMode ? [{ debugName: "dragPath" }] : []));
417
+ // Pending popover
418
+ pendingPopover = signal(null, ...(ngDevMode ? [{ debugName: "pendingPopover" }] : []));
419
+ // Settings Panel integration
420
+ isDirty$ = new BehaviorSubject(false);
421
+ isValid$ = new BehaviorSubject(true);
422
+ isBusy$ = new BehaviorSubject(false);
423
+ initialSnapshot = '[]';
424
+ placeholderSample = 'payload | payload.id | ${payload.id}';
425
+ connectedPorts = new Map();
426
+ isPortConnected(nodeId, portId) {
427
+ const set = this.connectedPorts.get(nodeId);
428
+ return !!set && set.has(portId);
429
+ }
430
+ outputPorts(n) {
431
+ const ports = (n.ports || []).filter((p) => p.kind === 'output');
432
+ return this.showUnconnectedPorts ? ports : ports.filter(p => this.isPortConnected(n.id, p.id));
433
+ }
434
+ inputPorts(n) {
435
+ const ports = (n.ports || []).filter((p) => p.kind === 'input');
436
+ return this.showUnconnectedPorts ? ports : ports.filter(p => this.isPortConnected(n.id, p.id));
437
+ }
438
+ constructor(mapper, settingsPanel, injectedData) {
439
+ this.mapper = mapper;
440
+ this.settingsPanel = settingsPanel;
441
+ this.injectedData = injectedData;
442
+ }
443
+ ngOnInit() {
444
+ const data = this.injectedData || {};
445
+ const p = this.parsePage(data.page);
446
+ const ws = data.widgets || this.widgets || p?.widgets || [];
447
+ this.widgets = ws;
448
+ this.page = data.page || this.page;
449
+ const conns = [...(p?.connections || [])];
450
+ this.connections.set(conns);
451
+ this.initialSnapshot = JSON.stringify(conns);
452
+ this.rebuildGraph();
453
+ }
454
+ ngOnChanges(changes) {
455
+ if (changes['page'] || changes['widgets']) {
456
+ const p = this.parsePage(this.page);
457
+ this.connections.set([...(p?.connections || [])]);
458
+ this.rebuildGraph();
459
+ this.initialSnapshot = JSON.stringify(this.connections());
460
+ this.isDirty$.next(false);
461
+ }
462
+ }
463
+ parsePage(input) {
464
+ if (!input)
465
+ return undefined;
466
+ if (typeof input === 'string') {
467
+ try {
468
+ return JSON.parse(input);
469
+ }
470
+ catch {
471
+ return undefined;
472
+ }
473
+ }
474
+ return input;
475
+ }
476
+ rebuildGraph() {
477
+ const p = this.parsePage(this.page) || { widgets: this.widgets || [], connections: this.connections() };
478
+ const { nodes, edges } = this.mapper.mapToGraph({ ...p, connections: this.connections() }, this.widgets || []);
479
+ this.nodes.set(nodes);
480
+ this.edges.set(edges);
481
+ this.recomputeConnectedPorts();
482
+ this.updateTransform();
483
+ }
484
+ recomputeConnectedPorts() {
485
+ const map = new Map();
486
+ const add = (nodeId, portId) => {
487
+ const s = map.get(nodeId) || new Set();
488
+ s.add(portId);
489
+ map.set(nodeId, s);
490
+ };
491
+ for (const e of this.edges()) {
492
+ add(e.from.nodeId, e.from.portId);
493
+ add(e.to.nodeId, e.to.portId);
494
+ }
495
+ this.connectedPorts = map;
496
+ }
497
+ // External focus by connection (from Builder or other callers)
498
+ focusConnection(conn) {
499
+ const fromNodeId = conn.from.widget;
500
+ const fromPortId = `out:${conn.from.output}`;
501
+ let toNodeId = conn.to.widget;
502
+ let toPortId = `in:${conn.to.input}`;
503
+ const deep = this.parseTabsPath(conn.to.input);
504
+ if (deep) {
505
+ const childId = `${conn.to.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
506
+ if (this.nodes().some(n => n.id === childId)) {
507
+ toNodeId = childId;
508
+ toPortId = `in:${deep.input}`;
509
+ }
510
+ else {
511
+ toPortId = `in:${deep.input}`;
512
+ }
513
+ }
514
+ const idx = this.edges().findIndex(e => e.from.nodeId === fromNodeId && e.from.portId === fromPortId && e.to.nodeId === toNodeId && e.to.portId === toPortId);
515
+ if (idx >= 0)
516
+ this.selectEdge(idx);
517
+ }
518
+ parseTabsPath(path) {
519
+ if (!path)
520
+ return null;
521
+ const g = path.match(/^inputs\.config\.tabs\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/);
522
+ if (g)
523
+ return { kind: 'group', index: Number(g[1]), widgetIndex: Number(g[2]), input: g[3] };
524
+ const n = path.match(/^inputs\.config\.nav\.links\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/);
525
+ if (n)
526
+ return { kind: 'nav', index: Number(n[1]), widgetIndex: Number(n[2]), input: n[3] };
527
+ return null;
528
+ }
529
+ // Pan/Zoom handlers
530
+ onWheel(ev) {
531
+ ev.preventDefault();
532
+ const delta = Math.sign(ev.deltaY) * -0.05; // invert: wheel down zoom out
533
+ this.zoom = Math.min(2, Math.max(0.5, this.zoom + delta));
534
+ this.updateTransform();
535
+ }
536
+ panning = false;
537
+ panStart = { x: 0, y: 0 };
538
+ onPanStart(ev) {
539
+ if (ev.target.closest('.port-hit'))
540
+ return; // don't pan when starting on port
541
+ this.panning = true;
542
+ this.panStart = { x: ev.clientX - this.pan.x, y: ev.clientY - this.pan.y };
543
+ const onMove = (e) => this.onPanMove(e);
544
+ const onUp = () => { this.panning = false; document.removeEventListener('mousemove', onMove); };
545
+ document.addEventListener('mousemove', onMove);
546
+ document.addEventListener('mouseup', onUp, { once: true });
547
+ }
548
+ onPanMove(ev) {
549
+ if (!this.panning)
550
+ return;
551
+ this.pan = { x: ev.clientX - this.panStart.x, y: ev.clientY - this.panStart.y };
552
+ this.updateTransform();
553
+ }
554
+ updateTransform() {
555
+ this.transform.set(`translate(${this.pan.x},${this.pan.y}) scale(${this.zoom})`);
556
+ }
557
+ // Edge/path helpers
558
+ edgePath(e) {
559
+ const from = this.findPortAnchor(e.from.nodeId, e.from.portId);
560
+ const to = this.findPortAnchor(e.to.nodeId, e.to.portId);
561
+ return routeElbow(from, to);
562
+ }
563
+ edgeTooltip(e) {
564
+ const toNode = this.nodes().find(n => n.id === e.to.nodeId);
565
+ const fromNode = this.nodes().find(n => n.id === e.from.nodeId);
566
+ const parts = [
567
+ `De: ${fromNode?.label || e.from.nodeId}.${e.from.portId.replace('out:', '')}`,
568
+ `Para: ${toNode?.label || e.to.nodeId}.${e.to.portId.replace('in:', '')}`,
569
+ ];
570
+ if (e.meta && e.meta.friendlyPath)
571
+ parts.push(`Destino: ${e.meta.friendlyPath}`);
572
+ if (e.meta?.map)
573
+ parts.push(`map: ${e.meta.map}`);
574
+ return parts.join('\n');
575
+ }
576
+ // Port label/tooltip helpers
577
+ technicalName(p) {
578
+ const fromId = p.id?.replace(/^in:|^out:/, '') || '';
579
+ if (fromId)
580
+ return fromId;
581
+ if (p.path) {
582
+ const m = p.path.match(/\.inputs\.([^\.\[]+)$/);
583
+ if (m)
584
+ return m[1];
585
+ }
586
+ return p.label || '';
587
+ }
588
+ portText(p) { return this.showFriendlyLabels ? (p.label || this.technicalName(p)) : this.technicalName(p); }
589
+ portTooltip(p) {
590
+ const tech = this.technicalName(p);
591
+ const friendly = p.label || tech;
592
+ const desc = p.description ? `\n${p.description}` : '';
593
+ if (this.showFriendlyLabels)
594
+ return `${friendly}${desc}`;
595
+ // when showing technical, include friendly for contexto if different
596
+ return friendly !== tech ? `${tech} \n(${friendly})${desc}` : `${tech}${desc}`;
597
+ }
598
+ edgeMid(e) {
599
+ const from = this.findPortAnchor(e.from.nodeId, e.from.portId);
600
+ const to = this.findPortAnchor(e.to.nodeId, e.to.portId);
601
+ return { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
602
+ }
603
+ findPortAnchor(nodeId, portId) {
604
+ const n = this.nodes().find((x) => x.id === nodeId);
605
+ if (!n)
606
+ return { x: 0, y: 0 };
607
+ const p = n.ports.find((pp) => pp.id === portId);
608
+ return p?.anchor || { x: n.bounds.x, y: n.bounds.y };
609
+ }
610
+ // Drag connect flow
611
+ onDragStart(ev) {
612
+ this.dragFrom = ev.from;
613
+ this.dragPath.set(`M ${ev.x} ${ev.y} L ${ev.x} ${ev.y}`);
614
+ try {
615
+ console.debug('[ConnectionGraph] drag:start', { from: ev.from, at: { x: ev.x, y: ev.y } });
616
+ }
617
+ catch { }
618
+ }
619
+ onDragMove(ev) {
620
+ if (!this.dragFrom)
621
+ return;
622
+ const from = this.findPortAnchor(this.dragFrom.nodeId, this.dragFrom.portId);
623
+ this.dragPath.set(routeElbow(from, { x: ev.x, y: ev.y }));
624
+ try {
625
+ console.debug('[ConnectionGraph] drag:move', { to: { x: ev.x, y: ev.y } });
626
+ }
627
+ catch { }
628
+ }
629
+ onDragEnd(ev) {
630
+ if (!this.dragFrom)
631
+ return;
632
+ this.dragPath.set('');
633
+ // Find target input port element (robust: handle circle or its container)
634
+ const endEl = ev.endEvent.target;
635
+ const targetInfo = { tag: endEl?.tagName, class: endEl?.className };
636
+ let circleEl = endEl?.closest?.('.port.input circle');
637
+ if (!circleEl) {
638
+ const portGroup = endEl?.closest?.('.port.input');
639
+ if (portGroup)
640
+ circleEl = portGroup.querySelector('circle') || null;
641
+ }
642
+ if (!circleEl && endEl && endEl.getAttribute) {
643
+ const hasData = Boolean(endEl.getAttribute('data-port-id'));
644
+ if (hasData)
645
+ circleEl = endEl;
646
+ }
647
+ if (!circleEl) {
648
+ try {
649
+ console.warn('[ConnectionGraph] drag:end — no target port found', targetInfo);
650
+ }
651
+ catch { }
652
+ this.dragFrom = undefined;
653
+ return;
654
+ }
655
+ const toNodeId = circleEl.getAttribute('data-node-id') || '';
656
+ const toPortId = circleEl.getAttribute('data-port-id') || '';
657
+ const path = circleEl.getAttribute('data-port-path') || undefined;
658
+ try {
659
+ console.debug('[ConnectionGraph] drag:end found target', { toNodeId, toPortId, path, at: { x: ev.x, y: ev.y } });
660
+ }
661
+ catch { }
662
+ this.pendingPopover.set({ x: ev.x, y: ev.y, from: this.dragFrom, to: { nodeId: toNodeId, portId: toPortId, path }, map: 'payload' });
663
+ try {
664
+ console.debug('[ConnectionGraph] popover:open', this.pendingPopover());
665
+ }
666
+ catch { }
667
+ this.dragFrom = undefined;
668
+ }
669
+ cancelPending() { this.pendingPopover.set(null); }
670
+ confirmPending() {
671
+ const pop = this.pendingPopover();
672
+ if (!pop)
673
+ return;
674
+ const fromOutput = pop.from.portId.replace(/^out:/, '');
675
+ const toInputName = pop.to.portId.replace(/^in:/, '');
676
+ const toPath = pop.to.path || toInputName;
677
+ const newConn = {
678
+ from: { widget: pop.from.nodeId, output: fromOutput },
679
+ to: { widget: this.resolveToWidget(pop.to.nodeId), input: toPath },
680
+ map: pop.map,
681
+ meta: {
682
+ filterExpr: pop.filter || undefined,
683
+ debounceMs: pop.debounceMs || undefined,
684
+ distinct: pop.distinct || undefined,
685
+ distinctBy: pop.distinctBy || undefined,
686
+ },
687
+ };
688
+ const next = [...this.connections(), newConn];
689
+ try {
690
+ console.debug('[ConnectionGraph] connection:add', newConn);
691
+ }
692
+ catch { }
693
+ this.connections.set(next);
694
+ this.pendingPopover.set(null);
695
+ try {
696
+ console.debug('[ConnectionGraph] popover:close');
697
+ }
698
+ catch { }
699
+ this.rebuildGraph();
700
+ this.emitPageChange();
701
+ this.updateDirty(next);
702
+ }
703
+ applyPreset(map) {
704
+ const pop = this.pendingPopover();
705
+ if (!pop)
706
+ return;
707
+ this.pendingPopover.set({ ...pop, map });
708
+ }
709
+ computePreview(map) {
710
+ try {
711
+ const sample = { payload: { id: 123, data: { id: 123, name: 'Exemplo' }, row: { id: 123 } } };
712
+ const path = (map || 'payload').split('.');
713
+ let cur = sample;
714
+ for (const k of path) {
715
+ if (!k)
716
+ continue;
717
+ cur = cur?.[k];
718
+ }
719
+ return JSON.stringify(cur);
720
+ }
721
+ catch {
722
+ return '—';
723
+ }
724
+ }
725
+ computeFilterPreview(expr) {
726
+ try {
727
+ if (!expr)
728
+ return '—';
729
+ const sample = { payload: { id: 123, data: { id: 123, name: 'Exemplo' }, row: { id: 123 } } };
730
+ if (typeof expr === 'string' && expr.trim().startsWith('=')) {
731
+ const code = expr.trim().slice(1);
732
+ // Controlled eval limited to sample context
733
+ // eslint-disable-next-line no-new-func
734
+ const fn = new Function('payload', `"use strict"; return (${code});`);
735
+ const ok = !!fn(sample.payload);
736
+ return ok ? 'pass' : 'skip';
737
+ }
738
+ return '—';
739
+ }
740
+ catch {
741
+ return 'erro';
742
+ }
743
+ }
744
+ resolveToWidget(nodeId) {
745
+ // Child nodes are encoded as parent#kind[index]/widgetIndex
746
+ const hash = nodeId.indexOf('#');
747
+ if (hash > 0)
748
+ return nodeId.slice(0, hash);
749
+ return nodeId;
750
+ }
751
+ removeEdge(index) {
752
+ const next = this.connections().slice();
753
+ next.splice(index, 1);
754
+ this.connections.set(next);
755
+ this.rebuildGraph();
756
+ this.emitPageChange();
757
+ this.updateDirty(next);
758
+ }
759
+ // Select edge and open details
760
+ selectEdge(index) {
761
+ this.selectedEdgeIndex.set(index);
762
+ this.showDetails = true;
763
+ }
764
+ currentConn() { const i = this.selectedEdgeIndex(); return (i >= 0 ? this.connections()[i] : undefined); }
765
+ emitPageChange() {
766
+ const parsed = this.parsePage(this.page) || { widgets: this.widgets || [] };
767
+ const updated = { ...parsed, connections: this.connections() };
768
+ this.pageChange.emit(updated);
769
+ }
770
+ // Settings panel value provider methods
771
+ getSettingsValue() { return { page: { ...(this.parsePage(this.page) || {}), connections: this.connections() } }; }
772
+ reset() {
773
+ try {
774
+ this.connections.set(JSON.parse(this.initialSnapshot || '[]'));
775
+ }
776
+ catch {
777
+ this.connections.set([]);
778
+ }
779
+ this.rebuildGraph();
780
+ this.isDirty$.next(false);
781
+ this.isValid$.next(true);
782
+ }
783
+ // Node dragging
784
+ nodeDrag = { active: false, index: -1, startX: 0, startY: 0, origX: 0, origY: 0 };
785
+ onNodeDragStart(ev, index) {
786
+ ev.preventDefault();
787
+ ev.stopPropagation();
788
+ const n = this.nodes()[index];
789
+ if (!n)
790
+ return;
791
+ this.nodeDrag = { active: true, index, startX: ev.clientX, startY: ev.clientY, origX: n.bounds.x, origY: n.bounds.y };
792
+ const onMove = (e) => this.onNodeDragMove(e);
793
+ const onUp = () => { this.nodeDrag.active = false; document.removeEventListener('mousemove', onMove); };
794
+ document.addEventListener('mousemove', onMove);
795
+ document.addEventListener('mouseup', onUp, { once: true });
796
+ }
797
+ onNodeDragMove(ev) {
798
+ if (!this.nodeDrag.active)
799
+ return;
800
+ const dx = (ev.clientX - this.nodeDrag.startX) / this.zoom;
801
+ const dy = (ev.clientY - this.nodeDrag.startY) / this.zoom;
802
+ const nodes = this.nodes().slice();
803
+ const n = { ...nodes[this.nodeDrag.index] };
804
+ const newX = this.nodeDrag.origX + dx;
805
+ const newY = this.nodeDrag.origY + dy;
806
+ const ddx = newX - n.bounds.x;
807
+ const ddy = newY - n.bounds.y;
808
+ n.bounds = { ...n.bounds, x: newX, y: newY };
809
+ // Shift port anchors by delta
810
+ n.ports = (n.ports || []).map(p => ({ ...p, anchor: { x: p.anchor.x + ddx, y: p.anchor.y + ddy } }));
811
+ nodes[this.nodeDrag.index] = n;
812
+ this.nodes.set(nodes);
813
+ }
814
+ trackPort(_idx, p) { return p.id; }
815
+ updateDirty(next) {
816
+ this.isDirty$.next(JSON.stringify(next) !== this.initialSnapshot);
817
+ this.isValid$.next(true);
818
+ }
819
+ // Builder integration
820
+ openBuilder() {
821
+ const base = this.parsePage(this.page) || { widgets: this.widgets || [] };
822
+ const p = { ...base, connections: this.connections() };
823
+ try {
824
+ console.debug('[ConnectionGraph] openBuilder()', { baseHasConns: !!base.connections, graphConns: this.connections().length, widgets: this.widgets?.length });
825
+ }
826
+ catch { }
827
+ const ref = this.settingsPanel.open({ id: 'grid-connections', title: 'Conexões (Builder)', content: { component: ConnectionBuilderComponent, inputs: { page: p, widgets: this.widgets } } });
828
+ ref.applied$.subscribe((val) => { const next = (val && val.page) ? val.page : undefined; if (next)
829
+ this.applyConnections(next); });
830
+ ref.saved$.subscribe((val) => { const next = (val && val.page) ? val.page : undefined; if (next)
831
+ this.applyConnections(next); });
832
+ }
833
+ applyConnections(next) {
834
+ const conns = [...(next.connections || [])];
835
+ try {
836
+ console.debug('[ConnectionGraph] applyConnections()', { conns: conns.length });
837
+ }
838
+ catch { }
839
+ this.connections.set(conns);
840
+ this.rebuildGraph();
841
+ this.emitPageChange();
842
+ this.initialSnapshot = JSON.stringify(conns);
843
+ this.isDirty$.next(false);
844
+ }
845
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConnectionGraphComponent, deps: [{ token: GraphMapperService }, { token: i2$1.SettingsPanelService }, { token: SETTINGS_PANEL_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
846
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: ConnectionGraphComponent, isStandalone: true, selector: "praxis-connection-graph", inputs: { page: "page", widgets: "widgets" }, outputs: { pageChange: "pageChange" }, usesOnChanges: true, ngImport: i0, template: `
847
+ <div class="cg-root" (wheel)="onWheel($event)" (mousedown)="onPanStart($event)">
848
+ <div class="cg-head">
849
+ <div class="cg-title">Conexões</div>
850
+ <div class="spacer"></div>
851
+ <button mat-button [color]="showPortLabels ? 'primary' : undefined" (click)="showPortLabels = !showPortLabels" matTooltip="Mostrar/ocultar labels de portas" aria-label="Mostrar/ocultar labels de portas">
852
+ <mat-icon>label</mat-icon>
853
+ </button>
854
+ <button mat-button [color]="showFriendlyLabels ? 'primary' : undefined" (click)="showFriendlyLabels = !showFriendlyLabels" matTooltip="Alternar nome amigável/técnico" aria-label="Alternar nome amigável/técnico">
855
+ <mat-icon>translate</mat-icon>
856
+ </button>
857
+ <button mat-button [color]="showEdgeLabels ? 'primary' : undefined" (click)="showEdgeLabels = !showEdgeLabels" matTooltip="Mostrar/ocultar rótulos de arestas" aria-label="Mostrar/ocultar rótulos de arestas">
858
+ <mat-icon>text_fields</mat-icon>
859
+ </button>
860
+ <button mat-button [color]="showUnconnectedPorts ? 'primary' : undefined" (click)="showUnconnectedPorts = !showUnconnectedPorts" matTooltip="Mostrar/ocultar portas sem conexão" aria-label="Mostrar/ocultar portas sem conexão">
861
+ <mat-icon>tune</mat-icon>
862
+ </button>
863
+ <button mat-stroked-button color="primary" (click)="openBuilder()" aria-label="Editar no Builder"><mat-icon>tune</mat-icon><span>Editar no Builder</span></button>
864
+ <div class="cg-count" [attr.aria-label]="'Total de conexões: ' + connections().length">{{ connections().length }}</div>
865
+ </div>
866
+ <div class="cg-canvas">
867
+ <!-- Edge details panel -->
868
+ <div class="cg-details" *ngIf="selectedEdgeIndex() >= 0 && showDetails">
869
+ <div class="details-head">
870
+ <div class="details-title">Detalhes da conexão</div>
871
+ <button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
872
+ </div>
873
+ <div class="details-body" *ngIf="currentConn() as c">
874
+ <div><b>De:</b> {{ c.from.widget }}.{{ c.from.output }}</div>
875
+ <div><b>Para:</b> {{ c.to.widget }}.{{ c.to.input }}</div>
876
+ <div><b>map:</b> {{ c.map || 'payload' }}</div>
877
+ <div *ngIf="c.to.bindingOrder?.length"><b>bindingOrder:</b> {{ c.to.bindingOrder?.join(', ') }}</div>
878
+ <div class="details-actions">
879
+ <button mat-button color="primary" (click)="openBuilder()"><mat-icon>tune</mat-icon> Editar no Builder</button>
880
+ <button mat-button color="warn" (click)="removeEdge(selectedEdgeIndex())"><mat-icon>delete</mat-icon> Remover</button>
881
+ </div>
882
+ </div>
883
+ </div>
884
+
885
+ <svg [attr.width]="'100%'" [attr.height]="'100%'">
886
+ <defs>
887
+ <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
888
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"></path>
889
+ </marker>
890
+ </defs>
891
+ <g [attr.transform]="transform()">
892
+ <!-- Edges -->
893
+ <g class="edges">
894
+ <g *ngFor="let e of edges(); let i = index" class="edge-group" role="button" tabindex="0"
895
+ (mouseenter)="hoverEdgeId = e.id" (mouseleave)="hoverEdgeId = ''" (click)="selectEdge(i)">
896
+ <path class="edge" [attr.d]="edgePath(e)" marker-end="url(#arrow)" [class.hover]="hoverEdgeId===e.id" [attr.aria-label]="e.label || ''" [matTooltip]="edgeTooltip(e)"></path>
897
+ <ng-container *ngIf="showEdgeLabels && e.label">
898
+ <g [attr.transform]="'translate(' + edgeMid(e).x + ',' + edgeMid(e).y + ')'">
899
+ <text class="edge-label" x="0" y="0">{{ e.label }}</text>
900
+ </g>
901
+ </ng-container>
902
+ <g *ngIf="hoverEdgeId===e.id" class="edge-toolbar">
903
+ <rect [attr.x]="edgeMid(e).x-28" [attr.y]="edgeMid(e).y-14" width="56" height="28" rx="6" ry="6" class="edge-toolbar-bg"></rect>
904
+ <text [attr.x]="edgeMid(e).x-14" [attr.y]="edgeMid(e).y+4" class="icon" (click)="removeEdge(i)" aria-label="Remover">🗑</text>
905
+ <text [attr.x]="edgeMid(e).x+8" [attr.y]="edgeMid(e).y+4" class="icon" (click)="openBuilder()" aria-label="Editar">✎</text>
906
+ </g>
907
+ </g>
908
+ </g>
909
+
910
+ <!-- Temporary drag edge -->
911
+ <path *ngIf="dragPath()" class="edge temp" [attr.d]="dragPath()" marker-end="url(#arrow)"></path>
912
+
913
+ <!-- Nodes -->
914
+ <g class="nodes">
915
+ <g *ngFor="let n of nodes(); let ni = index" class="node" [attr.transform]="'translate(' + n.bounds.x + ',' + n.bounds.y + ')'" role="group" [attr.aria-label]="n.label">
916
+ <rect class="node-box" [attr.width]="n.bounds.width" [attr.height]="n.bounds.height" rx="8" ry="8"
917
+ (mousedown)="onNodeDragStart($event, ni)"></rect>
918
+ <text class="node-title" x="8" y="20">{{ n.label }}</text>
919
+
920
+ <!-- Output ports (left) -->
921
+ <g *ngFor="let p of outputPorts(n); trackBy: trackPort" class="port output"
922
+ [attr.transform]="'translate(' + (0) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
923
+ <circle r="6" cx="-6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" [matTooltip]="portTooltip(p)"></circle>
924
+ <rect class="port-hit" x="-16" y="0" width="20" height="16" [dragConnect]="{ nodeId: n.id, portId: p.id }" [matTooltip]="portTooltip(p)"
925
+ (connectDragStart)="onDragStart($event)" (connectDragMove)="onDragMove($event)" (connectDragEnd)="onDragEnd($event)"></rect>
926
+ <text *ngIf="showPortLabels" x="-4" y="12" class="port-label" text-anchor="end" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
927
+ </g>
928
+
929
+ <!-- Input ports (right) -->
930
+ <g *ngFor="let p of inputPorts(n); trackBy: trackPort" class="port input"
931
+ [attr.transform]="'translate(' + (n.bounds.width) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
932
+ <circle r="6" cx="6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" data-port-kind="input" [attr.data-port-path]="p.path || ''" [matTooltip]="portTooltip(p)"></circle>
933
+ <rect class="port-hit" x="-4" y="0" width="20" height="16" [matTooltip]="portTooltip(p)"></rect>
934
+ <text *ngIf="showPortLabels" x="8" y="12" class="port-label" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
935
+ </g>
936
+ </g>
937
+ </g>
938
+ </g>
939
+ </svg>
940
+ <!-- Pending connection popover -->
941
+ <div class="popover" *ngIf="pendingPopover() as pop" [ngStyle]="{ left: pop.x + 'px', top: pop.y + 'px' }">
942
+ <div class="popover-body">
943
+ <div class="popover-title">Nova conexão</div>
944
+ <div style="display:grid; gap:6px; min-width:260px;">
945
+ <label>map:
946
+ <input [(ngModel)]="pop.map" [attr.placeholder]="placeholderSample" />
947
+ </label>
948
+ <div style="display:flex; gap:6px; flex-wrap: wrap;">
949
+ <button mat-stroked-button (click)="applyPreset('payload')">payload</button>
950
+ <button mat-stroked-button (click)="applyPreset('payload.id')">payload.id</button>
951
+ <button mat-stroked-button (click)="applyPreset('payload.data')">payload.data</button>
952
+ </div>
953
+ <label>filter (opcional):
954
+ <input [(ngModel)]="pop.filter" placeholder="ex.: = payload != null" />
955
+ </label>
956
+ <div style="display:grid; gap:6px;">
957
+ <div style="display:flex; gap:8px; align-items:center;">
958
+ <label style="display:flex; align-items:center; gap:6px;">debounce (ms): <input type="number" min="0" style="width:90px" [(ngModel)]="pop.debounceMs" /></label>
959
+ <label style="display:flex; align-items:center; gap:6px;">
960
+ <input type="checkbox" [(ngModel)]="pop.distinct" /> distinct
961
+ </label>
962
+ </div>
963
+ <label>distinctBy (opcional):
964
+ <input [(ngModel)]="pop['distinctBy']" placeholder="ex.: payload.id" />
965
+ </label>
966
+ </div>
967
+ <div style="font-size:12px; opacity:.8;">Filtro: {{ computeFilterPreview(pop.filter) }}</div>
968
+ <div style="font-size:12px; opacity:.8;">Prévia: {{ computePreview(pop.map) }}</div>
969
+ </div>
970
+ <div class="actions">
971
+ <button mat-button (click)="cancelPending()">Cancelar</button>
972
+ <button mat-flat-button color="primary" (click)="confirmPending()">Confirmar</button>
973
+ </div>
974
+ </div>
975
+ </div>
976
+ </div>
977
+ </div>
978
+ `, isInline: true, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{opacity:.8}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest, #111);color:var(--md-sys-color-on-surface, #eee)}.node-box{fill:var(--md-sys-color-surface-container, #1b1b1b);stroke:var(--md-sys-color-outline, #4a4a4a)}.node-title{fill:var(--md-sys-color-on-surface, #eee);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant, #bbb);font-size:13px}.edge{stroke:var(--md-sys-color-primary, #56a0ff);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary, #ffa04d)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:#00000059}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface, #222);stroke:var(--md-sys-color-outline, #444)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;padding:8px;min-width:220px;box-shadow:0 8px 24px #0006}.popover-title{font-weight:600;margin-bottom:8px}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;box-shadow:0 8px 24px #00000059;z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: DragConnectDirective, selector: "[dragConnect]", inputs: ["dragConnect"], outputs: ["connectDragStart", "connectDragMove", "connectDragEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
979
+ }
980
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConnectionGraphComponent, decorators: [{
981
+ type: Component,
982
+ args: [{ selector: 'praxis-connection-graph', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatIconModule, MatTooltipModule, DragConnectDirective], template: `
983
+ <div class="cg-root" (wheel)="onWheel($event)" (mousedown)="onPanStart($event)">
984
+ <div class="cg-head">
985
+ <div class="cg-title">Conexões</div>
986
+ <div class="spacer"></div>
987
+ <button mat-button [color]="showPortLabels ? 'primary' : undefined" (click)="showPortLabels = !showPortLabels" matTooltip="Mostrar/ocultar labels de portas" aria-label="Mostrar/ocultar labels de portas">
988
+ <mat-icon>label</mat-icon>
989
+ </button>
990
+ <button mat-button [color]="showFriendlyLabels ? 'primary' : undefined" (click)="showFriendlyLabels = !showFriendlyLabels" matTooltip="Alternar nome amigável/técnico" aria-label="Alternar nome amigável/técnico">
991
+ <mat-icon>translate</mat-icon>
992
+ </button>
993
+ <button mat-button [color]="showEdgeLabels ? 'primary' : undefined" (click)="showEdgeLabels = !showEdgeLabels" matTooltip="Mostrar/ocultar rótulos de arestas" aria-label="Mostrar/ocultar rótulos de arestas">
994
+ <mat-icon>text_fields</mat-icon>
995
+ </button>
996
+ <button mat-button [color]="showUnconnectedPorts ? 'primary' : undefined" (click)="showUnconnectedPorts = !showUnconnectedPorts" matTooltip="Mostrar/ocultar portas sem conexão" aria-label="Mostrar/ocultar portas sem conexão">
997
+ <mat-icon>tune</mat-icon>
998
+ </button>
999
+ <button mat-stroked-button color="primary" (click)="openBuilder()" aria-label="Editar no Builder"><mat-icon>tune</mat-icon><span>Editar no Builder</span></button>
1000
+ <div class="cg-count" [attr.aria-label]="'Total de conexões: ' + connections().length">{{ connections().length }}</div>
1001
+ </div>
1002
+ <div class="cg-canvas">
1003
+ <!-- Edge details panel -->
1004
+ <div class="cg-details" *ngIf="selectedEdgeIndex() >= 0 && showDetails">
1005
+ <div class="details-head">
1006
+ <div class="details-title">Detalhes da conexão</div>
1007
+ <button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
1008
+ </div>
1009
+ <div class="details-body" *ngIf="currentConn() as c">
1010
+ <div><b>De:</b> {{ c.from.widget }}.{{ c.from.output }}</div>
1011
+ <div><b>Para:</b> {{ c.to.widget }}.{{ c.to.input }}</div>
1012
+ <div><b>map:</b> {{ c.map || 'payload' }}</div>
1013
+ <div *ngIf="c.to.bindingOrder?.length"><b>bindingOrder:</b> {{ c.to.bindingOrder?.join(', ') }}</div>
1014
+ <div class="details-actions">
1015
+ <button mat-button color="primary" (click)="openBuilder()"><mat-icon>tune</mat-icon> Editar no Builder</button>
1016
+ <button mat-button color="warn" (click)="removeEdge(selectedEdgeIndex())"><mat-icon>delete</mat-icon> Remover</button>
1017
+ </div>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <svg [attr.width]="'100%'" [attr.height]="'100%'">
1022
+ <defs>
1023
+ <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
1024
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"></path>
1025
+ </marker>
1026
+ </defs>
1027
+ <g [attr.transform]="transform()">
1028
+ <!-- Edges -->
1029
+ <g class="edges">
1030
+ <g *ngFor="let e of edges(); let i = index" class="edge-group" role="button" tabindex="0"
1031
+ (mouseenter)="hoverEdgeId = e.id" (mouseleave)="hoverEdgeId = ''" (click)="selectEdge(i)">
1032
+ <path class="edge" [attr.d]="edgePath(e)" marker-end="url(#arrow)" [class.hover]="hoverEdgeId===e.id" [attr.aria-label]="e.label || ''" [matTooltip]="edgeTooltip(e)"></path>
1033
+ <ng-container *ngIf="showEdgeLabels && e.label">
1034
+ <g [attr.transform]="'translate(' + edgeMid(e).x + ',' + edgeMid(e).y + ')'">
1035
+ <text class="edge-label" x="0" y="0">{{ e.label }}</text>
1036
+ </g>
1037
+ </ng-container>
1038
+ <g *ngIf="hoverEdgeId===e.id" class="edge-toolbar">
1039
+ <rect [attr.x]="edgeMid(e).x-28" [attr.y]="edgeMid(e).y-14" width="56" height="28" rx="6" ry="6" class="edge-toolbar-bg"></rect>
1040
+ <text [attr.x]="edgeMid(e).x-14" [attr.y]="edgeMid(e).y+4" class="icon" (click)="removeEdge(i)" aria-label="Remover">🗑</text>
1041
+ <text [attr.x]="edgeMid(e).x+8" [attr.y]="edgeMid(e).y+4" class="icon" (click)="openBuilder()" aria-label="Editar">✎</text>
1042
+ </g>
1043
+ </g>
1044
+ </g>
1045
+
1046
+ <!-- Temporary drag edge -->
1047
+ <path *ngIf="dragPath()" class="edge temp" [attr.d]="dragPath()" marker-end="url(#arrow)"></path>
1048
+
1049
+ <!-- Nodes -->
1050
+ <g class="nodes">
1051
+ <g *ngFor="let n of nodes(); let ni = index" class="node" [attr.transform]="'translate(' + n.bounds.x + ',' + n.bounds.y + ')'" role="group" [attr.aria-label]="n.label">
1052
+ <rect class="node-box" [attr.width]="n.bounds.width" [attr.height]="n.bounds.height" rx="8" ry="8"
1053
+ (mousedown)="onNodeDragStart($event, ni)"></rect>
1054
+ <text class="node-title" x="8" y="20">{{ n.label }}</text>
1055
+
1056
+ <!-- Output ports (left) -->
1057
+ <g *ngFor="let p of outputPorts(n); trackBy: trackPort" class="port output"
1058
+ [attr.transform]="'translate(' + (0) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
1059
+ <circle r="6" cx="-6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" [matTooltip]="portTooltip(p)"></circle>
1060
+ <rect class="port-hit" x="-16" y="0" width="20" height="16" [dragConnect]="{ nodeId: n.id, portId: p.id }" [matTooltip]="portTooltip(p)"
1061
+ (connectDragStart)="onDragStart($event)" (connectDragMove)="onDragMove($event)" (connectDragEnd)="onDragEnd($event)"></rect>
1062
+ <text *ngIf="showPortLabels" x="-4" y="12" class="port-label" text-anchor="end" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
1063
+ </g>
1064
+
1065
+ <!-- Input ports (right) -->
1066
+ <g *ngFor="let p of inputPorts(n); trackBy: trackPort" class="port input"
1067
+ [attr.transform]="'translate(' + (n.bounds.width) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
1068
+ <circle r="6" cx="6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" data-port-kind="input" [attr.data-port-path]="p.path || ''" [matTooltip]="portTooltip(p)"></circle>
1069
+ <rect class="port-hit" x="-4" y="0" width="20" height="16" [matTooltip]="portTooltip(p)"></rect>
1070
+ <text *ngIf="showPortLabels" x="8" y="12" class="port-label" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
1071
+ </g>
1072
+ </g>
1073
+ </g>
1074
+ </g>
1075
+ </svg>
1076
+ <!-- Pending connection popover -->
1077
+ <div class="popover" *ngIf="pendingPopover() as pop" [ngStyle]="{ left: pop.x + 'px', top: pop.y + 'px' }">
1078
+ <div class="popover-body">
1079
+ <div class="popover-title">Nova conexão</div>
1080
+ <div style="display:grid; gap:6px; min-width:260px;">
1081
+ <label>map:
1082
+ <input [(ngModel)]="pop.map" [attr.placeholder]="placeholderSample" />
1083
+ </label>
1084
+ <div style="display:flex; gap:6px; flex-wrap: wrap;">
1085
+ <button mat-stroked-button (click)="applyPreset('payload')">payload</button>
1086
+ <button mat-stroked-button (click)="applyPreset('payload.id')">payload.id</button>
1087
+ <button mat-stroked-button (click)="applyPreset('payload.data')">payload.data</button>
1088
+ </div>
1089
+ <label>filter (opcional):
1090
+ <input [(ngModel)]="pop.filter" placeholder="ex.: = payload != null" />
1091
+ </label>
1092
+ <div style="display:grid; gap:6px;">
1093
+ <div style="display:flex; gap:8px; align-items:center;">
1094
+ <label style="display:flex; align-items:center; gap:6px;">debounce (ms): <input type="number" min="0" style="width:90px" [(ngModel)]="pop.debounceMs" /></label>
1095
+ <label style="display:flex; align-items:center; gap:6px;">
1096
+ <input type="checkbox" [(ngModel)]="pop.distinct" /> distinct
1097
+ </label>
1098
+ </div>
1099
+ <label>distinctBy (opcional):
1100
+ <input [(ngModel)]="pop['distinctBy']" placeholder="ex.: payload.id" />
1101
+ </label>
1102
+ </div>
1103
+ <div style="font-size:12px; opacity:.8;">Filtro: {{ computeFilterPreview(pop.filter) }}</div>
1104
+ <div style="font-size:12px; opacity:.8;">Prévia: {{ computePreview(pop.map) }}</div>
1105
+ </div>
1106
+ <div class="actions">
1107
+ <button mat-button (click)="cancelPending()">Cancelar</button>
1108
+ <button mat-flat-button color="primary" (click)="confirmPending()">Confirmar</button>
1109
+ </div>
1110
+ </div>
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{opacity:.8}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest, #111);color:var(--md-sys-color-on-surface, #eee)}.node-box{fill:var(--md-sys-color-surface-container, #1b1b1b);stroke:var(--md-sys-color-outline, #4a4a4a)}.node-title{fill:var(--md-sys-color-on-surface, #eee);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant, #bbb);font-size:13px}.edge{stroke:var(--md-sys-color-primary, #56a0ff);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary, #ffa04d)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:#00000059}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface, #222);stroke:var(--md-sys-color-outline, #444)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;padding:8px;min-width:220px;box-shadow:0 8px 24px #0006}.popover-title{font-weight:600;margin-bottom:8px}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;box-shadow:0 8px 24px #00000059;z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\n"] }]
1115
+ }], ctorParameters: () => [{ type: GraphMapperService }, { type: i2$1.SettingsPanelService }, { type: undefined, decorators: [{
1116
+ type: Optional
1117
+ }, {
1118
+ type: Inject,
1119
+ args: [SETTINGS_PANEL_DATA]
1120
+ }] }], propDecorators: { page: [{
1121
+ type: Input
1122
+ }], widgets: [{
1123
+ type: Input
1124
+ }], pageChange: [{
1125
+ type: Output
1126
+ }] } });
1127
+
1128
+ export { ConnectionGraphComponent };
1129
+ //# sourceMappingURL=praxisui-page-builder-connection-graph.component-C6x--6--.mjs.map