@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,121 @@
1
+ import { ArchimateModel, ArchimateView } from './ArchimateModel.js';
2
+ import { SKETCH_BOX_KINDS, DEFAULT_SKETCH_PALETTE } from './sketchBoxKinds.js';
3
+
4
+ /**
5
+ * SketchView — a sketch view as Archi has them (archimate:SketchModel):
6
+ * stickies (sketch-model-sticky), stick-figure actors (sketch-model-actor)
7
+ * and plain lines between them, drawn on a background paper or grid.
8
+ *
9
+ * Supports palette editing: custom sticky colours/styles, actor defaults,
10
+ * and importing/exporting palette configurations as JSON.
11
+ */
12
+ export class SketchView extends ArchimateView {
13
+ static kind = 'sketch';
14
+
15
+ static BOX_KINDS = SKETCH_BOX_KINDS;
16
+
17
+ static BACKGROUNDS = Object.freeze({
18
+ NONE: 0,
19
+ GRID: 1,
20
+ LINES: 2,
21
+ });
22
+
23
+ constructor(model, spec = {}) {
24
+ super(model, { archiViewType: 'sketch-model', elementIds: [], ...spec });
25
+ this._palette = spec.palette ? JSON.parse(JSON.stringify(spec.palette)) : [...DEFAULT_SKETCH_PALETTE];
26
+ if (spec.background !== undefined) this.background = spec.background;
27
+ }
28
+
29
+ /** The background kind (0 = none/plain, 1 = grid, 2 = lines). */
30
+ get background() {
31
+ const attr = this.archi?.attributes?.background;
32
+ return attr !== undefined ? Number(attr) : (this._background ?? 0);
33
+ }
34
+
35
+ set background(val) {
36
+ const num = Number(val) || 0;
37
+ this._background = num;
38
+ if (!this.archi) this.archi = {};
39
+ if (!this.archi.attributes) this.archi.attributes = {};
40
+ this.archi.attributes.background = String(num);
41
+ }
42
+
43
+ /** The sketch palette presets. */
44
+ get palette() {
45
+ return [...this._palette];
46
+ }
47
+
48
+ set palette(entries) {
49
+ if (!Array.isArray(entries)) throw new TypeError('Sketch palette must be an array of entries');
50
+ this._palette = entries.map(e => ({ ...e }));
51
+ }
52
+
53
+ /** Add or update a palette entry. */
54
+ setPaletteEntry(entry) {
55
+ if (!entry || !entry.id) throw new TypeError('Palette entry requires an id');
56
+ const idx = this._palette.findIndex(e => e.id === entry.id);
57
+ if (idx >= 0) this._palette[idx] = { ...this._palette[idx], ...entry };
58
+ else this._palette.push({ ...entry });
59
+ }
60
+
61
+ /** Remove a palette entry by id. */
62
+ removePaletteEntry(id) {
63
+ this._palette = this._palette.filter(e => e.id !== id);
64
+ }
65
+
66
+ /** Export the palette as a JSON string or object. */
67
+ exportPalette({ asString = true } = {}) {
68
+ const data = {
69
+ version: 1,
70
+ viewKind: 'sketch',
71
+ background: this.background,
72
+ entries: this.palette,
73
+ };
74
+ return asString ? JSON.stringify(data, null, 2) : data;
75
+ }
76
+
77
+ /** Import a palette from a JSON string or object. */
78
+ importPalette(source) {
79
+ const data = typeof source === 'string' ? JSON.parse(source) : source;
80
+ if (!data || !Array.isArray(data.entries)) {
81
+ throw new TypeError('Invalid sketch palette format: entries array expected');
82
+ }
83
+ if (data.background !== undefined) this.background = data.background;
84
+ this.palette = data.entries;
85
+ return this.palette;
86
+ }
87
+
88
+ /** CSS background styles matching this sketch's background setting. */
89
+ backgroundStyle() {
90
+ switch (this.background) {
91
+ case 1: // Grid
92
+ return {
93
+ backgroundImage: 'linear-gradient(#e5e7eb 1px, transparent 1px), linear-gradient(90deg, #e5e7eb 1px, transparent 1px)',
94
+ backgroundSize: '20px 20px',
95
+ backgroundColor: '#fafafa',
96
+ };
97
+ case 2: // Lines
98
+ return {
99
+ backgroundImage: 'linear-gradient(#e5e7eb 1px, transparent 1px)',
100
+ backgroundSize: '100% 24px',
101
+ backgroundColor: '#fffdfa',
102
+ };
103
+ case 0: // Plain
104
+ default:
105
+ return {
106
+ backgroundColor: '#ffffff',
107
+ };
108
+ }
109
+ }
110
+
111
+ toJSON() {
112
+ return {
113
+ ...super.toJSON(),
114
+ background: this.background,
115
+ palette: this.palette,
116
+ };
117
+ }
118
+ }
119
+
120
+ // Self-register with ArchimateModel
121
+ ArchimateModel.registerViewType('sketch-model', SketchView);
@@ -0,0 +1,30 @@
1
+ import { CanvasBoxKind } from '../canvas/CanvasBoxKind.js';
2
+
3
+ export const DEFAULT_SKETCH_PALETTE = Object.freeze([
4
+ { id: 'sticky-yellow', name: 'Yellow Sticky', color: '#fff59d', type: 'sticky' },
5
+ { id: 'sticky-green', name: 'Green Sticky', color: '#c8e6c9', type: 'sticky' },
6
+ { id: 'sticky-blue', name: 'Blue Sticky', color: '#bbdefb', type: 'sticky' },
7
+ { id: 'sticky-pink', name: 'Pink Sticky', color: '#f8bbd0', type: 'sticky' },
8
+ { id: 'sticky-orange', name: 'Orange Sticky', color: '#ffe0b2', type: 'sticky' },
9
+ { id: 'sticky-white', name: 'White Sticky', color: '#ffffff', type: 'sticky' },
10
+ { id: 'actor', name: 'Actor', color: '#000000', type: 'actor' },
11
+ ]);
12
+
13
+ /**
14
+ * Archi's sketch palette: Stickies and Actors (stick figures).
15
+ */
16
+ export const SKETCH_BOX_KINDS = Object.freeze([
17
+ new CanvasBoxKind({
18
+ key: 'sticky', label: 'Sticky', icon: '🗒', archiVisualType: 'sketch-model-sticky', width: 120, height: 80,
19
+ appearance: { fillColor: '#fff59d', strokeColor: 'none', textAlign: 'center', textPosition: 'middle' },
20
+ }),
21
+ new CanvasBoxKind({
22
+ key: 'actor', label: 'Actor', icon: '👤', archiVisualType: 'sketch-model-actor', width: 60, height: 100, shape: 'stick-figure',
23
+ appearance: { strokeColor: '#000000', textAlign: 'center', textPosition: 'top' },
24
+ }),
25
+ ]);
26
+
27
+ /** A sketch box kind by key, or undefined. */
28
+ export function sketchBoxKind(key) {
29
+ return SKETCH_BOX_KINDS.find(k => k.key === key);
30
+ }
@@ -1,10 +1,27 @@
1
1
  import { treeLayerOf } from './ModelTreePanel.js';
2
2
  import { typeTitle } from '../archimate/archimateNames.js';
3
+ import { NavigatorPanel } from './NavigatorPanel.js';
4
+
5
+ export { NavigatorPanel, buildNavigatorPanel } from './NavigatorPanel.js';
6
+ export { NavigatorModel } from './NavigatorModel.js';
7
+
8
+ let currentAnalysisMode = 'analysis';
9
+
10
+ export function setAnalysisMode(mode) {
11
+ if (mode === 'analysis' || mode === 'navigator') {
12
+ currentAnalysisMode = mode;
13
+ }
14
+ }
15
+
16
+ export function getAnalysisMode() {
17
+ return currentAnalysisMode;
18
+ }
19
+
3
20
  /**
4
21
  * Builds analysis data for a selected element: "Used in Views", incoming/outgoing relationships.
5
22
  */
6
23
  export function buildAnalysisData(model, elementId) {
7
- const element = model.getElement(elementId);
24
+ const element = model?.getElement(elementId);
8
25
  if (!element) return null;
9
26
 
10
27
  const rels = model.relationshipsFor(elementId);
@@ -18,35 +35,88 @@ export function buildAnalysisData(model, elementId) {
18
35
  }
19
36
 
20
37
  /**
21
- * DOM shell for rendering analysis panel.
38
+ * DOM shell for rendering analysis and visual navigator panel.
22
39
  * Builds via doc.createElement/Object.assign, never innerHTML.
23
40
  */
24
- export function buildAnalysisPanel(doc, { model, elementId, onSelectItem = () => {} } = {}) {
41
+ export function buildAnalysisPanel(doc, { model, elementId, onSelectItem = () => {}, onSelectView = () => {} } = {}) {
25
42
  const panel = doc.createElement('div');
26
43
  panel.style.cssText = `
27
44
  display: flex;
28
45
  flex-direction: column;
29
46
  height: 100%;
30
- gap: 8px;
31
- padding: 8px;
32
- font-family: sans-serif;
47
+ padding: 6px 8px;
48
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
33
49
  font-size: 13px;
34
50
  overflow: hidden;
51
+ background: #fff;
35
52
  `;
36
53
 
54
+ // Mode tabs: [ Analysis | Navigator ]
55
+ const header = doc.createElement('div');
56
+ header.style.cssText = 'display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e1e4e8; padding-bottom: 4px; margin-bottom: 6px; flex-shrink: 0;';
57
+
58
+ const tabs = doc.createElement('div');
59
+ tabs.style.cssText = 'display: flex; gap: 4px;';
60
+
61
+ const makeTab = (key, label) => {
62
+ const t = doc.createElement('button');
63
+ t.type = 'button';
64
+ t.textContent = label;
65
+ const active = (currentAnalysisMode === key);
66
+ t.style.cssText = `
67
+ font-size: 11px;
68
+ padding: 2px 8px;
69
+ border: 1px solid ${active ? '#0366d6' : '#d1d5da'};
70
+ border-radius: 3px;
71
+ background: ${active ? '#0366d6' : '#fff'};
72
+ color: ${active ? '#fff' : '#333'};
73
+ cursor: pointer;
74
+ font-weight: ${active ? '600' : 'normal'};
75
+ `;
76
+ t.addEventListener('click', () => {
77
+ currentAnalysisMode = key;
78
+ const updated = buildAnalysisPanel(doc, { model, elementId, onSelectItem, onSelectView });
79
+ panel.replaceWith(updated);
80
+ });
81
+ return t;
82
+ };
83
+
84
+ tabs.appendChild(makeTab('analysis', 'Analysis'));
85
+ tabs.appendChild(makeTab('navigator', 'Navigator'));
86
+ header.appendChild(tabs);
87
+ panel.appendChild(header);
88
+
89
+ // If Navigator mode is active, render the step-by-step NavigatorPanel
90
+ if (currentAnalysisMode === 'navigator') {
91
+ const navPanel = new NavigatorPanel(doc, {
92
+ model,
93
+ elementId,
94
+ onSelectItem,
95
+ onSelectView,
96
+ });
97
+ const navWrap = doc.createElement('div');
98
+ navWrap.style.cssText = 'flex: 1; overflow: hidden; display: flex; flex-direction: column; min-height: 0;';
99
+ navWrap.appendChild(navPanel.dom);
100
+ panel.appendChild(navWrap);
101
+ return panel;
102
+ }
103
+
104
+ // Default Analysis summary mode
37
105
  const data = buildAnalysisData(model, elementId);
38
106
  if (!data) {
39
- panel.textContent = '(No element selected)';
107
+ const empty = doc.createElement('div');
108
+ empty.style.cssText = 'color: #888; font-style: italic; padding: 12px; text-align: center;';
109
+ empty.textContent = '(No element selected)';
110
+ panel.appendChild(empty);
40
111
  return panel;
41
112
  }
42
113
 
43
114
  // Element details
44
115
  const detailsDiv = doc.createElement('div');
45
- detailsDiv.style.cssText = 'padding: 8px; background: #f5f5f5; border-radius: 4px;';
116
+ detailsDiv.style.cssText = 'padding: 8px; background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 4px; margin-bottom: 8px; flex-shrink: 0;';
46
117
 
47
118
  const nameRow = Object.assign(doc.createElement('div'), { textContent: `Name: ${data.element.name || '(unnamed)'}` });
48
- const typeRow = Object.assign(doc.createElement('div'), { textContent: `Type: ${data.element.type}` });
49
- // Its layer as the diagram draws it (a stored one, else its type's), as the tree files it.
119
+ const typeRow = Object.assign(doc.createElement('div'), { textContent: `Type: ${typeTitle(data.element.type)}` });
50
120
  const layerRow = Object.assign(doc.createElement('div'), { textContent: `Layer: ${typeTitle(treeLayerOf(data.element))}` });
51
121
 
52
122
  detailsDiv.appendChild(nameRow);
@@ -54,28 +124,34 @@ export function buildAnalysisPanel(doc, { model, elementId, onSelectItem = () =>
54
124
  detailsDiv.appendChild(layerRow);
55
125
  panel.appendChild(detailsDiv);
56
126
 
127
+ const bodyDiv = doc.createElement('div');
128
+ bodyDiv.style.cssText = 'flex: 1; overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 8px;';
129
+
57
130
  // Used in Views
58
131
  const usedDiv = doc.createElement('div');
59
132
  usedDiv.style.cssText = 'flex-shrink: 0;';
60
133
 
61
134
  const usedLabel = Object.assign(doc.createElement('div'), {
62
135
  textContent: `Used in Views (${data.usedInViews.length})`,
63
- style: 'font-weight: bold; margin-bottom: 4px;',
136
+ style: 'font-weight: 600; font-size: 11px; color: #586069; text-transform: uppercase; margin-bottom: 4px;',
64
137
  });
65
138
  usedDiv.appendChild(usedLabel);
66
139
 
67
140
  if (data.usedInViews.length === 0) {
68
141
  usedDiv.appendChild(Object.assign(doc.createElement('div'), {
69
142
  textContent: '(no views)',
70
- style: 'font-size: 12px; color: #666;',
143
+ style: 'font-size: 12px; color: #888;',
71
144
  }));
72
145
  } else {
73
146
  for (const view of data.usedInViews) {
74
- const row = buildAnalysisRow(doc, view.name, () => onSelectItem({ kind: 'view', id: view.id }));
147
+ const row = buildAnalysisRow(doc, view.name || 'View', () => {
148
+ onSelectView(view.id);
149
+ onSelectItem({ kind: 'view', id: view.id });
150
+ });
75
151
  usedDiv.appendChild(row);
76
152
  }
77
153
  }
78
- panel.appendChild(usedDiv);
154
+ bodyDiv.appendChild(usedDiv);
79
155
 
80
156
  // Incoming relationships
81
157
  const incomingDiv = doc.createElement('div');
@@ -83,24 +159,24 @@ export function buildAnalysisPanel(doc, { model, elementId, onSelectItem = () =>
83
159
 
84
160
  const incomingLabel = Object.assign(doc.createElement('div'), {
85
161
  textContent: `Incoming (${data.incoming.length})`,
86
- style: 'font-weight: bold; margin-bottom: 4px; margin-top: 8px;',
162
+ style: 'font-weight: 600; font-size: 11px; color: #586069; text-transform: uppercase; margin-bottom: 4px; margin-top: 4px;',
87
163
  });
88
164
  incomingDiv.appendChild(incomingLabel);
89
165
 
90
166
  if (data.incoming.length === 0) {
91
167
  incomingDiv.appendChild(Object.assign(doc.createElement('div'), {
92
168
  textContent: '(none)',
93
- style: 'font-size: 12px; color: #666;',
169
+ style: 'font-size: 12px; color: #888;',
94
170
  }));
95
171
  } else {
96
172
  for (const rel of data.incoming) {
97
173
  const source = model.getElement(rel.source);
98
- const label = `${source?.name || rel.source} → ${rel.kind}`;
174
+ const label = `${source?.name || rel.source} → ${typeTitle(rel.kind)}`;
99
175
  const row = buildAnalysisRow(doc, label, () => onSelectItem({ kind: 'relationship', id: rel.id }));
100
176
  incomingDiv.appendChild(row);
101
177
  }
102
178
  }
103
- panel.appendChild(incomingDiv);
179
+ bodyDiv.appendChild(incomingDiv);
104
180
 
105
181
  // Outgoing relationships
106
182
  const outgoingDiv = doc.createElement('div');
@@ -108,25 +184,26 @@ export function buildAnalysisPanel(doc, { model, elementId, onSelectItem = () =>
108
184
 
109
185
  const outgoingLabel = Object.assign(doc.createElement('div'), {
110
186
  textContent: `Outgoing (${data.outgoing.length})`,
111
- style: 'font-weight: bold; margin-bottom: 4px; margin-top: 8px; flex-shrink: 0;',
187
+ style: 'font-weight: 600; font-size: 11px; color: #586069; text-transform: uppercase; margin-bottom: 4px; margin-top: 4px; flex-shrink: 0;',
112
188
  });
113
189
  outgoingDiv.appendChild(outgoingLabel);
114
190
 
115
191
  if (data.outgoing.length === 0) {
116
192
  outgoingDiv.appendChild(Object.assign(doc.createElement('div'), {
117
193
  textContent: '(none)',
118
- style: 'font-size: 12px; color: #666;',
194
+ style: 'font-size: 12px; color: #888;',
119
195
  }));
120
196
  } else {
121
197
  for (const rel of data.outgoing) {
122
198
  const target = model.getElement(rel.target);
123
- const label = `${rel.kind} → ${target?.name || rel.target}`;
199
+ const label = `${typeTitle(rel.kind)} → ${target?.name || rel.target}`;
124
200
  const row = buildAnalysisRow(doc, label, () => onSelectItem({ kind: 'relationship', id: rel.id }));
125
201
  outgoingDiv.appendChild(row);
126
202
  }
127
203
  }
128
- panel.appendChild(outgoingDiv);
204
+ bodyDiv.appendChild(outgoingDiv);
129
205
 
206
+ panel.appendChild(bodyDiv);
130
207
  return panel;
131
208
  }
132
209
 
@@ -143,13 +220,14 @@ function buildAnalysisRow(doc, label, onClick) {
143
220
  cursor: pointer;
144
221
  user-select: none;
145
222
  font-size: 12px;
223
+ color: #24292e;
146
224
  transition: background-color 0.1s;
147
225
  `;
148
226
  row.textContent = label;
149
227
 
150
228
  row.addEventListener('click', onClick);
151
229
  row.addEventListener('mouseenter', () => {
152
- row.style.background = '#e0e0e0';
230
+ row.style.background = '#f1f3f5';
153
231
  });
154
232
  row.addEventListener('mouseleave', () => {
155
233
  row.style.background = '';