@peter.prib/renderengine 0.13.0 → 0.15.0

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.
Files changed (124) hide show
  1. package/README.md +480 -302
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49217 -37484
  4. package/dist/renderengine.umd.js +24899 -13093
  5. package/documentation/REFERENCE.md +68 -1
  6. package/package.json +13 -3
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +269 -3
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +7 -2
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +142 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/ask/AskBar.js +388 -0
  34. package/src/charting/ask/AskView.js +400 -0
  35. package/src/charting/charts/AlluvialDiagram.js +311 -0
  36. package/src/charting/charts/AreaChart.js +69 -15
  37. package/src/charting/charts/BarChart.js +38 -3
  38. package/src/charting/charts/BarChart3D.js +6 -4
  39. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  40. package/src/charting/charts/BarChartBase.js +26 -6
  41. package/src/charting/charts/BeeswarmChart.js +164 -0
  42. package/src/charting/charts/BoxPlotChart.js +25 -3
  43. package/src/charting/charts/BubbleChart.js +4 -1
  44. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  45. package/src/charting/charts/CandlestickChart.js +172 -4
  46. package/src/charting/charts/ChordDiagram.js +107 -4
  47. package/src/charting/charts/CirclePackChart.js +81 -0
  48. package/src/charting/charts/ComboChart.js +233 -0
  49. package/src/charting/charts/ContourChart.js +310 -0
  50. package/src/charting/charts/DensityChart.js +1 -1
  51. package/src/charting/charts/DonutChart3D.js +13 -0
  52. package/src/charting/charts/DumbbellChart.js +217 -0
  53. package/src/charting/charts/EcdfChart.js +150 -0
  54. package/src/charting/charts/FunnelChart.js +5 -2
  55. package/src/charting/charts/GeoMapChart.js +132 -11
  56. package/src/charting/charts/GroupedBarChart.js +44 -3
  57. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  58. package/src/charting/charts/HeatmapChart.js +6 -3
  59. package/src/charting/charts/HexbinChart.js +137 -0
  60. package/src/charting/charts/HierarchyChart.js +77 -3
  61. package/src/charting/charts/HistogramChart.js +2 -2
  62. package/src/charting/charts/HorizonChart.js +105 -0
  63. package/src/charting/charts/IcicleChart.js +47 -0
  64. package/src/charting/charts/KagiChart.js +167 -0
  65. package/src/charting/charts/LineChart.js +74 -18
  66. package/src/charting/charts/LollipopChart.js +104 -0
  67. package/src/charting/charts/MarimekkoChart.js +75 -0
  68. package/src/charting/charts/OhlcChart.js +79 -0
  69. package/src/charting/charts/OutlineChart.js +10 -8
  70. package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
  71. package/src/charting/charts/ParetoChart.js +92 -0
  72. package/src/charting/charts/PieChart.js +195 -17
  73. package/src/charting/charts/PieChart3D.js +137 -0
  74. package/src/charting/charts/PipeChart.js +5 -2
  75. package/src/charting/charts/PointAndFigureChart.js +198 -0
  76. package/src/charting/charts/PolarAreaChart.js +189 -0
  77. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  78. package/src/charting/charts/RadarChart.js +12 -5
  79. package/src/charting/charts/RadialBarChart.js +6 -3
  80. package/src/charting/charts/RenkoChart.js +178 -0
  81. package/src/charting/charts/RidgelineChart.js +113 -0
  82. package/src/charting/charts/SankeyDiagram.js +129 -6
  83. package/src/charting/charts/ScatterChart.js +19 -3
  84. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  85. package/src/charting/charts/SlopegraphChart.js +205 -0
  86. package/src/charting/charts/SparklineTableChart.js +166 -0
  87. package/src/charting/charts/StackedBarChart.js +125 -11
  88. package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
  89. package/src/charting/charts/StreamGraphChart.js +18 -3
  90. package/src/charting/charts/SunburstChart.js +28 -8
  91. package/src/charting/charts/SurfaceChart3D.js +157 -0
  92. package/src/charting/charts/TreemapChart.js +31 -11
  93. package/src/charting/charts/ViolinChart.js +20 -2
  94. package/src/charting/charts/WaterfallChart.js +2 -2
  95. package/src/charting/charts/WordCloudChart.js +2 -2
  96. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  97. package/src/charting/datamodel/DataLineage.js +150 -0
  98. package/src/charting/datamodel/DataModel.js +83 -1
  99. package/src/charting/datamodel/ModelMapping.js +87 -0
  100. package/src/charting/datamodel/SqlDdl.js +110 -85
  101. package/src/charting/datamodel/index.js +3 -0
  102. package/src/charting/decision/DecisionWorkbench.js +295 -0
  103. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  104. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  105. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  106. package/src/charting/diagrams/SketchView.js +121 -0
  107. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  108. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  109. package/src/charting/modelTree/NavigatorModel.js +410 -0
  110. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  111. package/src/charting/pivot/PivotChart.js +310 -13
  112. package/src/charting/pivot/PivotGridChart.js +322 -34
  113. package/src/charting/pivot/PivotView.js +626 -21
  114. package/src/charting/report/TemplatedReport.js +547 -0
  115. package/src/charting/templates/BoxTemplate.js +28 -27
  116. package/src/charting/templates/PackageTemplate.js +26 -25
  117. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  118. package/src/charting/templates/Template.js +219 -212
  119. package/src/cli.js +146 -0
  120. package/src/index.js +3370 -2899
  121. package/src/workspace/Workspace.js +126 -7
  122. package/src/workspace/archimateServices.js +4 -0
  123. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  124. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,453 @@
1
+ import { NavigatorModel } from './NavigatorModel.js';
2
+ import { typeTitle } from '../archimate/archimateNames.js';
3
+ import { treeLayerOf } from './ModelTreePanel.js';
4
+ import { ArchimateNotation } from '../../archimate.js';
5
+
6
+ /**
7
+ * NavigatorPanel — interactive, multi-step relationship visual explorer panel.
8
+ *
9
+ * Provides step-by-step traversal along incoming and outgoing relationships,
10
+ * breadcrumbs / history navigation (back/forward), pinning, direction filters,
11
+ * and view navigation.
12
+ */
13
+ export class NavigatorPanel {
14
+ /**
15
+ * @param {Document} doc
16
+ * @param {Object} options
17
+ * @param {ArchimateModel} options.model
18
+ * @param {NavigatorModel} [options.navigatorModel]
19
+ * @param {string} [options.elementId]
20
+ * @param {(item: { kind: string, id: string }) => void} [options.onSelectItem]
21
+ * @param {(viewId: string) => void} [options.onSelectView]
22
+ */
23
+ constructor(doc, { model, navigatorModel = null, elementId = null, onSelectItem = () => {}, onSelectView = () => {} } = {}) {
24
+ this.doc = doc;
25
+ this.model = model;
26
+ this.nav = navigatorModel || new NavigatorModel(model, { rootId: elementId });
27
+ if (elementId && !this.nav.currentId) {
28
+ this.nav.focus(elementId);
29
+ }
30
+ this.onSelectItem = onSelectItem;
31
+ this.onSelectView = onSelectView;
32
+
33
+ this.element = this._createDOM();
34
+ this._unsubscribe = this.nav.onChange(() => this.render());
35
+ }
36
+
37
+ destroy() {
38
+ this._unsubscribe?.();
39
+ }
40
+
41
+ /** Set or update the focused element (respects navigator pinning). */
42
+ setElementId(elementId) {
43
+ if (!elementId) return;
44
+ this.nav.focus(elementId);
45
+ }
46
+
47
+ /** The container DOM element. */
48
+ get dom() {
49
+ return this.element;
50
+ }
51
+
52
+ _createDOM() {
53
+ const root = this.doc.createElement('div');
54
+ root.className = 're-navigator-panel';
55
+ root.style.cssText = `
56
+ display: flex;
57
+ flex-direction: column;
58
+ height: 100%;
59
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
60
+ font-size: 12px;
61
+ color: #333;
62
+ background: #fff;
63
+ overflow: hidden;
64
+ user-select: none;
65
+ `;
66
+
67
+ this.toolbar = this._buildToolbar();
68
+ root.appendChild(this.toolbar);
69
+
70
+ this.content = this.doc.createElement('div');
71
+ this.content.style.cssText = `
72
+ flex: 1;
73
+ overflow-y: auto;
74
+ min-height: 0;
75
+ padding: 8px;
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 8px;
79
+ `;
80
+ root.appendChild(this.content);
81
+
82
+ this.render();
83
+ return root;
84
+ }
85
+
86
+ _buildToolbar() {
87
+ const bar = this.doc.createElement('div');
88
+ bar.style.cssText = `
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 4px;
92
+ padding: 6px 8px;
93
+ background: #f7f7f8;
94
+ border-bottom: 1px solid #e1e4e8;
95
+ flex-shrink: 0;
96
+ `;
97
+
98
+ const btn = (icon, title, onClick) => {
99
+ const b = this.doc.createElement('button');
100
+ b.type = 'button';
101
+ b.title = title;
102
+ b.textContent = icon;
103
+ b.style.cssText = `
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ width: 24px;
108
+ height: 24px;
109
+ padding: 0;
110
+ border: 1px solid #d1d5da;
111
+ border-radius: 3px;
112
+ background: #fff;
113
+ cursor: pointer;
114
+ font-size: 12px;
115
+ color: #444;
116
+ `;
117
+ b.addEventListener('click', onClick);
118
+ b.addEventListener('mouseenter', () => { if (!b.disabled) b.style.background = '#eef1f5'; });
119
+ b.addEventListener('mouseleave', () => { if (!b.disabled) b.style.background = b.dataset.active ? '#e1e4e8' : '#fff'; });
120
+ return b;
121
+ };
122
+
123
+ this.btnBack = btn('◀', 'Navigate back', () => this.nav.back());
124
+ this.btnForward = btn('▶', 'Navigate forward', () => this.nav.forward());
125
+ this.btnPin = btn('📌', 'Pin element (prevents auto-changing root)', () => {
126
+ this.nav.togglePin();
127
+ this._updateToolbarState();
128
+ });
129
+
130
+ // Direction selector
131
+ const dirSelect = this.doc.createElement('select');
132
+ dirSelect.style.cssText = `
133
+ font-size: 11px;
134
+ padding: 2px 4px;
135
+ border: 1px solid #d1d5da;
136
+ border-radius: 3px;
137
+ background: #fff;
138
+ color: #333;
139
+ margin-left: auto;
140
+ `;
141
+ const dirs = [
142
+ ['both', '⇄ Both'],
143
+ ['outgoing', '→ Outgoing'],
144
+ ['incoming', '← Incoming'],
145
+ ];
146
+ for (const [val, label] of dirs) {
147
+ const opt = this.doc.createElement('option');
148
+ opt.value = val;
149
+ opt.textContent = label;
150
+ dirSelect.appendChild(opt);
151
+ }
152
+ dirSelect.value = this.nav.direction;
153
+ dirSelect.addEventListener('change', () => this.nav.setDirection(dirSelect.value));
154
+ this.dirSelect = dirSelect;
155
+
156
+ bar.appendChild(this.btnBack);
157
+ bar.appendChild(this.btnForward);
158
+ bar.appendChild(this.btnPin);
159
+ bar.appendChild(this.dirSelect);
160
+
161
+ this._updateToolbarState();
162
+ return bar;
163
+ }
164
+
165
+ _updateToolbarState() {
166
+ if (!this.btnBack) return;
167
+ this.btnBack.disabled = !this.nav.canBack;
168
+ this.btnBack.style.opacity = this.nav.canBack ? '1' : '0.4';
169
+ this.btnBack.style.cursor = this.nav.canBack ? 'pointer' : 'default';
170
+
171
+ this.btnForward.disabled = !this.nav.canForward;
172
+ this.btnForward.style.opacity = this.nav.canForward ? '1' : '0.4';
173
+ this.btnForward.style.cursor = this.nav.canForward ? 'pointer' : 'default';
174
+
175
+ this.btnPin.dataset.active = this.nav.pinned ? 'true' : '';
176
+ this.btnPin.style.background = this.nav.pinned ? '#e1e4e8' : '#fff';
177
+ this.btnPin.style.borderColor = this.nav.pinned ? '#888' : '#d1d5da';
178
+ }
179
+
180
+ render() {
181
+ this._updateToolbarState();
182
+ if (!this.content) return;
183
+ this.content.replaceChildren();
184
+
185
+ const tree = this.nav.buildTree();
186
+ if (!tree) {
187
+ const empty = this.doc.createElement('div');
188
+ empty.style.cssText = 'color: #888; font-style: italic; padding: 12px; text-align: center;';
189
+ empty.textContent = '(Select an element to explore connections)';
190
+ this.content.appendChild(empty);
191
+ return;
192
+ }
193
+
194
+ // Focused element card
195
+ this.content.appendChild(this._renderRootCard(tree));
196
+
197
+ // Navigation tree container
198
+ const treeSection = this.doc.createElement('div');
199
+ treeSection.style.cssText = 'display: flex; flex-direction: column; gap: 4px;';
200
+
201
+ const hasConnections = tree.outgoing.length > 0 || tree.incoming.length > 0;
202
+ if (!hasConnections) {
203
+ const noRel = this.doc.createElement('div');
204
+ noRel.style.cssText = 'color: #888; font-size: 11px; padding: 4px 6px;';
205
+ noRel.textContent = '(No relationships found in selected direction)';
206
+ treeSection.appendChild(noRel);
207
+ } else {
208
+ if (tree.outgoing.length > 0) {
209
+ treeSection.appendChild(this._renderSectionHeader(`Outgoing (${tree.outgoing.length})`, '→'));
210
+ for (const branch of tree.outgoing) {
211
+ treeSection.appendChild(this._renderBranch(branch));
212
+ }
213
+ }
214
+
215
+ if (tree.incoming.length > 0) {
216
+ treeSection.appendChild(this._renderSectionHeader(`Incoming (${tree.incoming.length})`, '←'));
217
+ for (const branch of tree.incoming) {
218
+ treeSection.appendChild(this._renderBranch(branch));
219
+ }
220
+ }
221
+ }
222
+
223
+ this.content.appendChild(treeSection);
224
+ }
225
+
226
+ _renderSectionHeader(title, arrow) {
227
+ const h = this.doc.createElement('div');
228
+ h.style.cssText = `
229
+ font-weight: 600;
230
+ font-size: 11px;
231
+ color: #586069;
232
+ text-transform: uppercase;
233
+ letter-spacing: 0.5px;
234
+ padding: 4px 0 2px 2px;
235
+ margin-top: 6px;
236
+ `;
237
+ h.textContent = `${arrow} ${title}`;
238
+ return h;
239
+ }
240
+
241
+ _renderRootCard(tree) {
242
+ const card = this.doc.createElement('div');
243
+ card.style.cssText = `
244
+ background: #f6f8fa;
245
+ border: 1px solid #e1e4e8;
246
+ border-radius: 4px;
247
+ padding: 8px;
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 4px;
251
+ `;
252
+
253
+ const titleRow = this.doc.createElement('div');
254
+ titleRow.style.cssText = 'display: flex; align-items: center; justify-content: space-between;';
255
+
256
+ const nameSpan = this.doc.createElement('span');
257
+ nameSpan.style.cssText = 'font-weight: 600; font-size: 13px; color: #24292e;';
258
+ nameSpan.textContent = tree.element.name || '(unnamed)';
259
+ titleRow.appendChild(nameSpan);
260
+
261
+ card.appendChild(titleRow);
262
+
263
+ const metaRow = this.doc.createElement('div');
264
+ metaRow.style.cssText = 'font-size: 11px; color: #586069; display: flex; gap: 8px;';
265
+ const typeSpan = this.doc.createElement('span');
266
+ typeSpan.textContent = typeTitle(tree.element.type);
267
+ const layerSpan = this.doc.createElement('span');
268
+ layerSpan.textContent = `• ${typeTitle(tree.layer)}`;
269
+ metaRow.appendChild(typeSpan);
270
+ metaRow.appendChild(layerSpan);
271
+ card.appendChild(metaRow);
272
+
273
+ // Views list
274
+ if (tree.views && tree.views.length > 0) {
275
+ const viewsDiv = this.doc.createElement('div');
276
+ viewsDiv.style.cssText = 'margin-top: 4px; padding-top: 4px; border-top: 1px dashed #e1e4e8; font-size: 11px;';
277
+ const vHeader = this.doc.createElement('div');
278
+ vHeader.style.cssText = 'color: #586069; margin-bottom: 2px;';
279
+ vHeader.textContent = `In views (${tree.views.length}):`;
280
+ viewsDiv.appendChild(vHeader);
281
+
282
+ for (const view of tree.views) {
283
+ const vLink = this.doc.createElement('span');
284
+ vLink.style.cssText = `
285
+ display: inline-block;
286
+ margin: 2px 4px 2px 0;
287
+ padding: 1px 6px;
288
+ background: #eef1f5;
289
+ border-radius: 3px;
290
+ color: #0366d6;
291
+ cursor: pointer;
292
+ `;
293
+ vLink.textContent = view.name || 'View';
294
+ vLink.addEventListener('click', () => {
295
+ this.onSelectView(view.id);
296
+ this.onSelectItem({ kind: 'element', id: tree.element.id });
297
+ });
298
+ viewsDiv.appendChild(vLink);
299
+ }
300
+ card.appendChild(viewsDiv);
301
+ }
302
+
303
+ return card;
304
+ }
305
+
306
+ _renderBranch(branch, level = 0) {
307
+ const wrap = this.doc.createElement('div');
308
+ wrap.style.cssText = `
309
+ display: flex;
310
+ flex-direction: column;
311
+ margin-left: ${level * 14}px;
312
+ border-left: ${level > 0 ? '1px dashed #d1d5da' : 'none'};
313
+ padding-left: ${level > 0 ? '6px' : '0'};
314
+ margin-bottom: 2px;
315
+ `;
316
+
317
+ const row = this.doc.createElement('div');
318
+ row.style.cssText = `
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 4px;
322
+ padding: 3px 6px;
323
+ border-radius: 3px;
324
+ cursor: pointer;
325
+ transition: background 0.1s;
326
+ `;
327
+
328
+ // Expand / collapse chevron (if branch has potential further connections and not cycle)
329
+ const toggle = this.doc.createElement('span');
330
+ toggle.style.cssText = `
331
+ width: 14px;
332
+ height: 14px;
333
+ display: inline-flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ font-size: 9px;
337
+ color: #6a737d;
338
+ cursor: pointer;
339
+ flex-shrink: 0;
340
+ `;
341
+
342
+ if (branch.cycle) {
343
+ toggle.textContent = '↻';
344
+ toggle.title = 'Cycle detected (element already in path)';
345
+ toggle.style.color = '#e36209';
346
+ } else {
347
+ toggle.textContent = branch.expanded ? '▼' : '▶';
348
+ toggle.addEventListener('click', (e) => {
349
+ e.stopPropagation();
350
+ this.nav.toggleExpand(branch.pathKey);
351
+ });
352
+ }
353
+ row.appendChild(toggle);
354
+
355
+ // Relationship badge
356
+ const relBadge = this.doc.createElement('span');
357
+ relBadge.style.cssText = `
358
+ padding: 1px 4px;
359
+ background: #f1f3f5;
360
+ border: 1px solid #e1e4e8;
361
+ border-radius: 3px;
362
+ font-size: 10px;
363
+ color: #495057;
364
+ flex-shrink: 0;
365
+ `;
366
+ const dirSymbol = branch.direction === 'outgoing' ? '→' : '←';
367
+ relBadge.textContent = `${dirSymbol} ${typeTitle(branch.relationship.kind)}`;
368
+ relBadge.addEventListener('click', (e) => {
369
+ e.stopPropagation();
370
+ this.onSelectItem({ kind: 'relationship', id: branch.relationship.id });
371
+ });
372
+ row.appendChild(relBadge);
373
+
374
+ // Element name and layer
375
+ const elSpan = this.doc.createElement('span');
376
+ elSpan.style.cssText = `
377
+ font-size: 11px;
378
+ color: #24292e;
379
+ white-space: nowrap;
380
+ overflow: hidden;
381
+ text-overflow: ellipsis;
382
+ flex: 1;
383
+ `;
384
+ elSpan.textContent = branch.element.name || '(unnamed)';
385
+ elSpan.title = `${branch.element.name} (${typeTitle(branch.element.type)} - ${typeTitle(branch.layer)})`;
386
+ row.appendChild(elSpan);
387
+
388
+ // Focus jump button (⊙)
389
+ const focusBtn = this.doc.createElement('span');
390
+ focusBtn.textContent = '⊙';
391
+ focusBtn.title = 'Re-center navigator on this element';
392
+ focusBtn.style.cssText = `
393
+ color: #0366d6;
394
+ font-size: 12px;
395
+ opacity: 0.6;
396
+ padding: 0 4px;
397
+ cursor: pointer;
398
+ flex-shrink: 0;
399
+ `;
400
+ focusBtn.addEventListener('click', (e) => {
401
+ e.stopPropagation();
402
+ this.nav.focus(branch.element.id, { force: true });
403
+ });
404
+ row.appendChild(focusBtn);
405
+
406
+ row.addEventListener('click', () => {
407
+ this.onSelectItem({ kind: 'element', id: branch.element.id });
408
+ });
409
+
410
+ row.addEventListener('mouseenter', () => {
411
+ row.style.background = '#f1f3f5';
412
+ focusBtn.style.opacity = '1';
413
+ });
414
+ row.addEventListener('mouseleave', () => {
415
+ row.style.background = '';
416
+ focusBtn.style.opacity = '0.6';
417
+ });
418
+
419
+ wrap.appendChild(row);
420
+
421
+ // Render recursive children if expanded
422
+ if (branch.expanded && branch.children) {
423
+ const childWrap = this.doc.createElement('div');
424
+ childWrap.style.cssText = 'display: flex; flex-direction: column; gap: 2px;';
425
+
426
+ const sub = branch.children;
427
+ const hasKids = sub.outgoing.length > 0 || sub.incoming.length > 0;
428
+ if (!hasKids) {
429
+ const noMore = this.doc.createElement('div');
430
+ noMore.style.cssText = `margin-left: ${(level + 1) * 14}px; font-size: 10px; color: #888; font-style: italic;`;
431
+ noMore.textContent = '(end of path)';
432
+ childWrap.appendChild(noMore);
433
+ } else {
434
+ for (const outK of sub.outgoing) {
435
+ childWrap.appendChild(this._renderBranch(outK, level + 1));
436
+ }
437
+ for (const inK of sub.incoming) {
438
+ childWrap.appendChild(this._renderBranch(inK, level + 1));
439
+ }
440
+ }
441
+ wrap.appendChild(childWrap);
442
+ }
443
+
444
+ return wrap;
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Functional helper to build a NavigatorPanel DOM element.
450
+ */
451
+ export function buildNavigatorPanel(doc, options) {
452
+ return new NavigatorPanel(doc, options).dom;
453
+ }