@liminis/editor 0.2.2 → 0.4.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.
- package/LICENSE +0 -13
- package/README.md +158 -103
- package/dist/app/editor/CorrectionPanelPlugin.js +10 -11
- package/dist/app/editor/DragHandlePlugin.js +1 -1
- package/dist/app/editor/Editor.js +17 -5
- package/dist/app/editor/SelectionContextMenuPlugin.js +4 -4
- package/dist/app/editor/nodes/C4Component.js +8 -10
- package/dist/app/editor/nodes/C4Node.d.ts +1 -1
- package/dist/app/editor/nodes/DiagramContextMenu.js +5 -5
- package/dist/headless.d.ts +3 -5
- package/dist/headless.js +2 -4
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +428 -325
- package/docs/decisions/adr-92-lexical-peer-range-policy.md +152 -0
- package/docs/decisions/adr-93-liminis-editor-defined-aliases.md +315 -0
- package/docs/decisions/adr-98-invert-token-direction.md +309 -0
- package/package.json +28 -27
- package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +0 -35
- package/dist/app/editor/c4/C4InteractiveRenderer.js +0 -299
- package/dist/app/editor/c4/edge-clipping.d.ts +0 -24
- package/dist/app/editor/c4/edge-clipping.js +0 -139
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +0 -38
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +0 -112
- package/dist/app/editor/c4/layout.d.ts +0 -25
- package/dist/app/editor/c4/layout.js +0 -839
- package/dist/app/editor/c4/parser.d.ts +0 -19
- package/dist/app/editor/c4/parser.js +0 -410
- package/dist/app/editor/c4/render-to-string.d.ts +0 -24
- package/dist/app/editor/c4/render-to-string.js +0 -34
- package/dist/app/editor/c4/renderer.d.ts +0 -64
- package/dist/app/editor/c4/renderer.js +0 -569
- package/dist/app/editor/c4/types.d.ts +0 -203
- package/dist/app/editor/c4/types.js +0 -43
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { buildClippedEdgePaths } from './edge-clipping.js';
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// CONSTANTS
|
|
5
|
-
// =============================================================================
|
|
6
|
-
/** Colors for light mode (matches imperative renderer — white fills, colored strokes) */
|
|
7
|
-
const LIGHT_COLORS = {
|
|
8
|
-
containerFill: '#ffffff', // oklch(1.00 0.00 0)
|
|
9
|
-
componentFill: '#f8f8f8', // oklch(0.98 0.00 0)
|
|
10
|
-
externalFill: '#f0f4f8', // oklch(0.96 0.01 240)
|
|
11
|
-
personFill: '#006b2d', // oklch(0.45 0.16 152)
|
|
12
|
-
boundaryFill: '#f4f7fb', // oklch(0.98 0.01 240)
|
|
13
|
-
containerStroke: '#005998', // oklch(0.45 0.15 240)
|
|
14
|
-
componentStroke: '#0068a0', // oklch(0.50 0.12 240)
|
|
15
|
-
externalStroke: '#667280', // oklch(0.55 0.02 240)
|
|
16
|
-
personStroke: '#004b1e', // oklch(0.35 0.16 152)
|
|
17
|
-
boundaryStroke: '#63869b', // oklch(0.60 0.05 240)
|
|
18
|
-
edgeStroke: '#565656', // oklch(0.35 0.00 0)
|
|
19
|
-
textPrimary: '#303030', // oklch(0.20 0.00 0)
|
|
20
|
-
textSecondary: '#626262', // oklch(0.40 0.00 0)
|
|
21
|
-
textOnBoundary: '#3d3d3d', // oklch(0.25 0.00 0)
|
|
22
|
-
edgeLabel: '#4a4a4a', // oklch(0.30 0.00 0)
|
|
23
|
-
edgeLabelBackground: '#ffffff', // oklch(1.00 0.00 0)
|
|
24
|
-
};
|
|
25
|
-
/** Colors for dark mode */
|
|
26
|
-
const DARK_COLORS = {
|
|
27
|
-
containerFill: '#16191c', // oklch(0.20 0.01 240)
|
|
28
|
-
componentFill: '#16191c', // oklch(0.20 0.01 240)
|
|
29
|
-
externalFill: '#2d2d2d', // oklch(0.18 0.00 0)
|
|
30
|
-
personFill: '#006b2d', // oklch(0.45 0.16 152)
|
|
31
|
-
boundaryFill: '#131619', // oklch(0.15 0.01 240)
|
|
32
|
-
containerStroke: '#0098e0', // oklch(0.65 0.15 240)
|
|
33
|
-
componentStroke: '#2b89bf', // oklch(0.60 0.12 240)
|
|
34
|
-
externalStroke: '#667280', // oklch(0.55 0.02 240)
|
|
35
|
-
personStroke: '#009848', // oklch(0.60 0.16 152)
|
|
36
|
-
boundaryStroke: '#47657d', // oklch(0.50 0.05 240)
|
|
37
|
-
edgeStroke: '#a7a7a7', // oklch(0.70 0.00 0)
|
|
38
|
-
textPrimary: '#d6d6d6', // oklch(0.90 0.00 0)
|
|
39
|
-
textSecondary: '#b2b2b2', // oklch(0.75 0.00 0)
|
|
40
|
-
textOnBoundary: '#d6d6d6', // oklch(0.90 0.00 0)
|
|
41
|
-
edgeLabel: '#bebebe', // oklch(0.80 0.00 0)
|
|
42
|
-
edgeLabelBackground: '#16191c', // oklch(0.20 0.01 240)
|
|
43
|
-
};
|
|
44
|
-
const BORDER_RADIUS = 8;
|
|
45
|
-
const PERSON_HEAD_RADIUS = 14;
|
|
46
|
-
const PERSON_BODY_WIDTH = 40;
|
|
47
|
-
const PERSON_BODY_HEIGHT = 30;
|
|
48
|
-
const NAME_FONT_SIZE = 14;
|
|
49
|
-
const TECH_FONT_SIZE = 11;
|
|
50
|
-
const DESCRIPTION_FONT_SIZE = 11;
|
|
51
|
-
const EDGE_LABEL_FONT_SIZE = 11;
|
|
52
|
-
const ARROW_SIZE = 8;
|
|
53
|
-
const LEGEND_ROW_HEIGHT = 24;
|
|
54
|
-
const LEGEND_MARGIN = 20;
|
|
55
|
-
const STEP_NUMBER_PATTERN = /^(\d+)\s*\[(.+)\]$/;
|
|
56
|
-
const LEGEND_THRESHOLD = 50;
|
|
57
|
-
// =============================================================================
|
|
58
|
-
// HELPER FUNCTIONS
|
|
59
|
-
// =============================================================================
|
|
60
|
-
function getColors(isDarkMode) {
|
|
61
|
-
return isDarkMode ? DARK_COLORS : LIGHT_COLORS;
|
|
62
|
-
}
|
|
63
|
-
function calculateArrowheadPoints(startPoint, endPoint) {
|
|
64
|
-
const dx = endPoint.x - startPoint.x;
|
|
65
|
-
const dy = endPoint.y - startPoint.y;
|
|
66
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
67
|
-
if (len === 0)
|
|
68
|
-
return '';
|
|
69
|
-
const ux = dx / len;
|
|
70
|
-
const uy = dy / len;
|
|
71
|
-
const px = -uy;
|
|
72
|
-
const py = ux;
|
|
73
|
-
const tipX = endPoint.x;
|
|
74
|
-
const tipY = endPoint.y;
|
|
75
|
-
const baseX = endPoint.x - ux * ARROW_SIZE;
|
|
76
|
-
const baseY = endPoint.y - uy * ARROW_SIZE;
|
|
77
|
-
const halfW = ARROW_SIZE * 0.5;
|
|
78
|
-
return `${tipX},${tipY} ${baseX + px * halfW},${baseY + py * halfW} ${baseX - px * halfW},${baseY - py * halfW}`;
|
|
79
|
-
}
|
|
80
|
-
function shortenEdgeEnd(points) {
|
|
81
|
-
if (points.length < 2)
|
|
82
|
-
return points;
|
|
83
|
-
const result = [...points];
|
|
84
|
-
const lastIndex = result.length - 1;
|
|
85
|
-
const secondLastIndex = lastIndex - 1;
|
|
86
|
-
const dx = result[lastIndex].x - result[secondLastIndex].x;
|
|
87
|
-
const dy = result[lastIndex].y - result[secondLastIndex].y;
|
|
88
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
89
|
-
if (len > ARROW_SIZE) {
|
|
90
|
-
const ratio = (len - ARROW_SIZE) / len;
|
|
91
|
-
result[lastIndex] = {
|
|
92
|
-
x: result[secondLastIndex].x + dx * ratio,
|
|
93
|
-
y: result[secondLastIndex].y + dy * ratio,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Split an edge label into lines for rendering.
|
|
100
|
-
* If the label contains a [technology] suffix, always put it on its own line.
|
|
101
|
-
* Otherwise, break long labels at a natural word boundary near the middle.
|
|
102
|
-
*/
|
|
103
|
-
function splitEdgeLabel(label) {
|
|
104
|
-
const techMatch = /^(.+?)\s*(\[.+\])$/.exec(label);
|
|
105
|
-
if (techMatch) {
|
|
106
|
-
return [techMatch[1].trim(), techMatch[2]];
|
|
107
|
-
}
|
|
108
|
-
const LINE_BREAK_THRESHOLD = 30;
|
|
109
|
-
if (label.length > LINE_BREAK_THRESHOLD) {
|
|
110
|
-
const mid = Math.floor(label.length / 2);
|
|
111
|
-
let breakIdx = label.lastIndexOf(' ', mid + 10);
|
|
112
|
-
if (breakIdx < mid - 15 || breakIdx === -1)
|
|
113
|
-
breakIdx = label.indexOf(' ', mid - 5);
|
|
114
|
-
if (breakIdx === -1)
|
|
115
|
-
breakIdx = mid;
|
|
116
|
-
return [label.slice(0, breakIdx).trim(), label.slice(breakIdx).trim()];
|
|
117
|
-
}
|
|
118
|
-
return [label];
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Wrap text into lines that fit within a given pixel width.
|
|
122
|
-
* Uses approximate character width for system-ui at the given font size.
|
|
123
|
-
*/
|
|
124
|
-
function wrapText(text, maxWidth, fontSize) {
|
|
125
|
-
const avgCharWidth = fontSize * 0.55;
|
|
126
|
-
const maxChars = Math.floor(maxWidth / avgCharWidth);
|
|
127
|
-
if (text.length <= maxChars)
|
|
128
|
-
return [text];
|
|
129
|
-
const words = text.split(' ');
|
|
130
|
-
const lines = [];
|
|
131
|
-
let current = '';
|
|
132
|
-
for (const word of words) {
|
|
133
|
-
const test = current ? `${current} ${word}` : word;
|
|
134
|
-
if (test.length > maxChars && current) {
|
|
135
|
-
lines.push(current);
|
|
136
|
-
current = word;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
current = test;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (current)
|
|
143
|
-
lines.push(current);
|
|
144
|
-
return lines;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Render a multi-line description as tspan elements within a text element.
|
|
148
|
-
*/
|
|
149
|
-
function DescriptionText({ x, y, text, nodeWidth, fill, fontSize, lineHeight = 14 }) {
|
|
150
|
-
// Leave padding on each side
|
|
151
|
-
const maxWidth = nodeWidth - 20;
|
|
152
|
-
const lines = wrapText(text, maxWidth, fontSize);
|
|
153
|
-
return (_jsx("text", { x: x, y: y, textAnchor: "middle", fill: fill, fontSize: fontSize, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.7, children: lines.map((line, i) => (_jsx("tspan", { x: x, dy: i === 0 ? 0 : lineHeight, children: line }, i))) }));
|
|
154
|
-
}
|
|
155
|
-
/** Build id→parentId map for O(1) parent lookups */
|
|
156
|
-
function buildParentMap(allNodes) {
|
|
157
|
-
const map = new Map();
|
|
158
|
-
for (const node of allNodes) {
|
|
159
|
-
map.set(node.id, node.element.parent);
|
|
160
|
-
}
|
|
161
|
-
return map;
|
|
162
|
-
}
|
|
163
|
-
/** Compute nesting depth for alternating boundary fills */
|
|
164
|
-
function getNestingDepth(element, parentMap) {
|
|
165
|
-
let depth = 0;
|
|
166
|
-
let parentId = element.parent;
|
|
167
|
-
while (parentId) {
|
|
168
|
-
depth++;
|
|
169
|
-
parentId = parentMap.get(parentId);
|
|
170
|
-
}
|
|
171
|
-
return depth;
|
|
172
|
-
}
|
|
173
|
-
function processEdgesForLegend(edges) {
|
|
174
|
-
const legendEntries = [];
|
|
175
|
-
let nextLetterIndex = 0;
|
|
176
|
-
const nextLetter = () => String.fromCharCode(65 + (nextLetterIndex++ % 26));
|
|
177
|
-
const processed = edges.map((edge) => {
|
|
178
|
-
if (!edge.label)
|
|
179
|
-
return { ...edge, isLegendRef: false, isStepNumber: false };
|
|
180
|
-
const stepMatch = STEP_NUMBER_PATTERN.exec(edge.label);
|
|
181
|
-
if (stepMatch) {
|
|
182
|
-
const stepNum = stepMatch[1];
|
|
183
|
-
const description = stepMatch[2];
|
|
184
|
-
legendEntries.push({ index: stepNum, label: description, isStep: true });
|
|
185
|
-
const showInline = description.length <= LEGEND_THRESHOLD;
|
|
186
|
-
return {
|
|
187
|
-
...edge,
|
|
188
|
-
label: stepNum,
|
|
189
|
-
displayLabel: showInline ? description : undefined,
|
|
190
|
-
isLegendRef: true,
|
|
191
|
-
isStepNumber: true,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (edge.label.length > LEGEND_THRESHOLD) {
|
|
195
|
-
const letter = nextLetter();
|
|
196
|
-
legendEntries.push({ index: letter, label: edge.label, isStep: false });
|
|
197
|
-
return { ...edge, label: letter, isLegendRef: true, isStepNumber: false };
|
|
198
|
-
}
|
|
199
|
-
return { ...edge, isLegendRef: false, isStepNumber: false };
|
|
200
|
-
});
|
|
201
|
-
// If multiple edges between same pair, or any one is in legend,
|
|
202
|
-
// move ALL edges in that pair to legend
|
|
203
|
-
const pairCounts = new Map();
|
|
204
|
-
for (const edge of processed) {
|
|
205
|
-
if (edge.label) {
|
|
206
|
-
const key = [edge.source, edge.target].sort().join('::');
|
|
207
|
-
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const pairNeedsLegend = new Map();
|
|
211
|
-
for (const edge of processed) {
|
|
212
|
-
const key = [edge.source, edge.target].sort().join('::');
|
|
213
|
-
if (edge.isLegendRef || (pairCounts.get(key) ?? 0) > 1) {
|
|
214
|
-
pairNeedsLegend.set(key, true);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
for (const edge of processed) {
|
|
218
|
-
if (!edge.isLegendRef && edge.label) {
|
|
219
|
-
const key = [edge.source, edge.target].sort().join('::');
|
|
220
|
-
if (pairNeedsLegend.get(key)) {
|
|
221
|
-
const letter = nextLetter();
|
|
222
|
-
legendEntries.push({ index: letter, label: edge.label, isStep: false });
|
|
223
|
-
edge.label = letter;
|
|
224
|
-
edge.isLegendRef = true;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return { edges: processed, legendEntries };
|
|
229
|
-
}
|
|
230
|
-
// =============================================================================
|
|
231
|
-
// LEGEND PLACEMENT
|
|
232
|
-
// =============================================================================
|
|
233
|
-
function computeLegendPlacement(legendEntries, layout) {
|
|
234
|
-
const longestLabel = Math.max(...legendEntries.map((e) => e.label.length), 0);
|
|
235
|
-
const legendW = Math.min(longestLabel * 6 + 40, 400);
|
|
236
|
-
const legendH = legendEntries.length * LEGEND_ROW_HEIGHT + LEGEND_MARGIN;
|
|
237
|
-
const margin = LEGEND_MARGIN;
|
|
238
|
-
const allNodes = layout.nodes;
|
|
239
|
-
function overlapsNodes(rx, ry, rw, rh) {
|
|
240
|
-
for (const node of allNodes) {
|
|
241
|
-
if (rx < node.x + node.width + margin &&
|
|
242
|
-
rx + rw + margin > node.x &&
|
|
243
|
-
ry < node.y + node.height + margin &&
|
|
244
|
-
ry + rh + margin > node.y) {
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
const step = 30;
|
|
251
|
-
let bestPlacement = null;
|
|
252
|
-
let bestScore = -Infinity;
|
|
253
|
-
const allowOverflow = legendW;
|
|
254
|
-
for (let cy = margin; cy <= layout.height - legendH - margin; cy += step) {
|
|
255
|
-
for (let cx = margin; cx <= layout.width + allowOverflow - legendW; cx += step) {
|
|
256
|
-
if (!overlapsNodes(cx, cy, legendW, legendH)) {
|
|
257
|
-
const withinX = cx + legendW <= layout.width;
|
|
258
|
-
const withinY = cy + legendH <= layout.height;
|
|
259
|
-
const withinBonus = (withinX ? 50 : 0) + (withinY ? 50 : 0);
|
|
260
|
-
const overflowX = Math.max(0, (cx + legendW) - layout.width);
|
|
261
|
-
const overflowY = Math.max(0, (cy + legendH) - layout.height);
|
|
262
|
-
const overflowPenalty = (overflowX + overflowY) * 0.5;
|
|
263
|
-
const score = withinBonus +
|
|
264
|
-
(cx / (layout.width || 1)) * 30 +
|
|
265
|
-
((1 - cy / (layout.height || 1)) * 20) -
|
|
266
|
-
overflowPenalty;
|
|
267
|
-
if (score > bestScore) {
|
|
268
|
-
bestScore = score;
|
|
269
|
-
bestPlacement = { x: cx, y: cy };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
for (const node of allNodes) {
|
|
275
|
-
const cx = node.x + node.width + margin;
|
|
276
|
-
const cy = node.y;
|
|
277
|
-
if (!overlapsNodes(cx, cy, legendW, legendH)) {
|
|
278
|
-
const withinX = cx + legendW <= layout.width;
|
|
279
|
-
const withinY = cy + legendH <= layout.height;
|
|
280
|
-
const withinBonus = (withinX ? 50 : 0) + (withinY ? 50 : 0);
|
|
281
|
-
const overflowX = Math.max(0, (cx + legendW) - layout.width);
|
|
282
|
-
const overflowY = Math.max(0, (cy + legendH) - layout.height);
|
|
283
|
-
const overflowPenalty = (overflowX + overflowY) * 0.5;
|
|
284
|
-
const score = withinBonus +
|
|
285
|
-
(cx / (layout.width || 1)) * 30 +
|
|
286
|
-
((1 - cy / (layout.height || 1)) * 20) -
|
|
287
|
-
overflowPenalty;
|
|
288
|
-
if (score > bestScore) {
|
|
289
|
-
bestScore = score;
|
|
290
|
-
bestPlacement = { x: cx, y: cy };
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (!bestPlacement) {
|
|
295
|
-
bestPlacement = { x: margin, y: layout.height + margin };
|
|
296
|
-
}
|
|
297
|
-
let totalWidth = layout.width;
|
|
298
|
-
let totalHeight = layout.height;
|
|
299
|
-
if (bestPlacement.x + legendW + margin > totalWidth) {
|
|
300
|
-
totalWidth = bestPlacement.x + legendW + margin;
|
|
301
|
-
}
|
|
302
|
-
if (bestPlacement.y + legendH + margin > totalHeight) {
|
|
303
|
-
totalHeight = bestPlacement.y + legendH + margin;
|
|
304
|
-
}
|
|
305
|
-
return {
|
|
306
|
-
x: bestPlacement.x,
|
|
307
|
-
y: bestPlacement.y,
|
|
308
|
-
width: legendW,
|
|
309
|
-
height: legendH,
|
|
310
|
-
totalWidth,
|
|
311
|
-
totalHeight,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Compute legend placement and dimensions for a layout.
|
|
316
|
-
* Exported for use by C4InteractiveRenderer to create a legend hit area.
|
|
317
|
-
* Returns null if the layout has no legend entries.
|
|
318
|
-
*/
|
|
319
|
-
export function computeLegendInfo(layout) {
|
|
320
|
-
const { legendEntries } = processEdgesForLegend(layout.edges);
|
|
321
|
-
if (legendEntries.length === 0)
|
|
322
|
-
return null;
|
|
323
|
-
const lp = computeLegendPlacement(legendEntries, layout);
|
|
324
|
-
return { x: lp.x, y: lp.y, width: lp.width, height: lp.height };
|
|
325
|
-
}
|
|
326
|
-
function SystemBoundary({ node, colors, parentMap }) {
|
|
327
|
-
const { x, y, width, height, element } = node;
|
|
328
|
-
const isExt = element.properties.external === true;
|
|
329
|
-
const hasChildren = (node.children?.length ?? 0) > 0 || element.properties.style === 'boundary';
|
|
330
|
-
const depth = getNestingDepth(element, parentMap);
|
|
331
|
-
const boundaryFillColor = depth % 2 === 0 ? colors.boundaryFill : colors.containerFill;
|
|
332
|
-
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: width, height: height, rx: BORDER_RADIUS, ry: BORDER_RADIUS, fill: isExt ? colors.externalFill : hasChildren ? boundaryFillColor : colors.containerFill, stroke: isExt ? colors.externalStroke : hasChildren ? colors.boundaryStroke : colors.containerStroke, strokeWidth: 2.5, strokeDasharray: (isExt || hasChildren) ? '8 4' : 'none' }), _jsx("text", { x: x + width / 2, y: hasChildren ? y + 24 : y + height / 2 - (element.properties.tech ? 6 : 0) - (element.properties.description ? 6 : 0), textAnchor: "middle", dominantBaseline: hasChildren ? undefined : 'middle', fill: hasChildren ? colors.textOnBoundary : colors.textPrimary, fontSize: NAME_FONT_SIZE, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: element.name }), element.properties.tech && (_jsxs("text", { x: x + width / 2, y: hasChildren ? y + 40 : y + height / 2 + 10 - (element.properties.description ? 6 : 0), textAnchor: "middle", fill: hasChildren ? colors.textOnBoundary : colors.textSecondary, fontSize: TECH_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.8, children: ["[", element.properties.tech, "]"] })), element.properties.description && !hasChildren && (_jsx(DescriptionText, { x: x + width / 2, y: y + height / 2 + (element.properties.tech ? 26 : 14), text: element.properties.description, nodeWidth: width, fill: colors.textSecondary, fontSize: DESCRIPTION_FONT_SIZE }))] }));
|
|
333
|
-
}
|
|
334
|
-
function Container({ node, colors, allNodes, parentMap }) {
|
|
335
|
-
const { x, y, width, height, element } = node;
|
|
336
|
-
const isExt = element.properties.external === true;
|
|
337
|
-
const hasChildren = (node.children?.length ?? 0) > 0 || element.properties.style === 'boundary';
|
|
338
|
-
if (element.properties.shape === 'cylinder') {
|
|
339
|
-
return _jsx(CylinderShape, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap });
|
|
340
|
-
}
|
|
341
|
-
if (element.properties.shape === 'queue') {
|
|
342
|
-
return _jsx(QueueShape, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap });
|
|
343
|
-
}
|
|
344
|
-
const depth = getNestingDepth(element, parentMap);
|
|
345
|
-
const boundaryFillColor = depth % 2 === 0 ? colors.boundaryFill : colors.containerFill;
|
|
346
|
-
const fill = isExt ? colors.externalFill : hasChildren ? boundaryFillColor : colors.containerFill;
|
|
347
|
-
const stroke = isExt ? colors.externalStroke : hasChildren ? colors.boundaryStroke : colors.containerStroke;
|
|
348
|
-
const textColor = hasChildren ? colors.textOnBoundary : colors.textPrimary;
|
|
349
|
-
const textY = hasChildren
|
|
350
|
-
? y + 24
|
|
351
|
-
: y + height / 2 - (element.properties.tech ? 6 : 0) - (element.properties.description ? 6 : 0);
|
|
352
|
-
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: width, height: height, rx: BORDER_RADIUS, ry: BORDER_RADIUS, fill: fill, stroke: stroke, strokeWidth: 2.5, strokeDasharray: (isExt || hasChildren) ? '8 4' : 'none' }), _jsx("text", { x: x + width / 2, y: textY, textAnchor: "middle", dominantBaseline: hasChildren ? undefined : 'middle', fill: textColor, fontSize: NAME_FONT_SIZE, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: element.name }), element.properties.tech && (_jsxs("text", { x: x + width / 2, y: y + height / 2 + 10 - (element.properties.description ? 6 : 0), textAnchor: "middle", fill: colors.textSecondary, fontSize: TECH_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.8, children: ["[", element.properties.tech, "]"] })), element.properties.description && !hasChildren && (_jsx(DescriptionText, { x: x + width / 2, y: y + height / 2 + (element.properties.tech ? 26 : 14), text: element.properties.description, nodeWidth: width, fill: colors.textSecondary, fontSize: DESCRIPTION_FONT_SIZE }))] }));
|
|
353
|
-
}
|
|
354
|
-
function CylinderShape({ node, colors }) {
|
|
355
|
-
const { x, y, width, height, element } = node;
|
|
356
|
-
const isExt = element.properties.external === true;
|
|
357
|
-
const ellipseRy = 12;
|
|
358
|
-
const bodyY = y + ellipseRy;
|
|
359
|
-
const bodyHeight = height - ellipseRy * 2;
|
|
360
|
-
const fill = isExt ? colors.externalFill : colors.containerFill;
|
|
361
|
-
const stroke = isExt ? colors.externalStroke : colors.containerStroke;
|
|
362
|
-
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: bodyY, width: width, height: bodyHeight, fill: fill, stroke: stroke, strokeWidth: 2.5, strokeDasharray: isExt ? '8 4' : 'none' }), _jsx("ellipse", { cx: x + width / 2, cy: y + height - ellipseRy, rx: width / 2, ry: ellipseRy, fill: fill, stroke: stroke, strokeWidth: 2.5, strokeDasharray: isExt ? '8 4' : 'none' }), _jsx("ellipse", { cx: x + width / 2, cy: y + ellipseRy, rx: width / 2, ry: ellipseRy, fill: fill, stroke: stroke, strokeWidth: 2.5, strokeDasharray: isExt ? '8 4' : 'none' }), _jsx("text", { x: x + width / 2, y: y + height / 2 - (element.properties.tech ? 6 : 0), textAnchor: "middle", dominantBaseline: "middle", fill: colors.textPrimary, fontSize: NAME_FONT_SIZE, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: element.name }), element.properties.tech && (_jsxs("text", { x: x + width / 2, y: y + height / 2 + 12, textAnchor: "middle", fill: colors.textSecondary, fontSize: TECH_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.8, children: ["[", element.properties.tech, "]"] }))] }));
|
|
363
|
-
}
|
|
364
|
-
function QueueShape({ node, colors }) {
|
|
365
|
-
const { x, y, width, height, element } = node;
|
|
366
|
-
const isExt = element.properties.external === true;
|
|
367
|
-
const fill = isExt ? colors.externalFill : colors.containerFill;
|
|
368
|
-
const stroke = isExt ? colors.externalStroke : colors.containerStroke;
|
|
369
|
-
const ellipseRx = 16;
|
|
370
|
-
return (_jsxs("g", { children: [_jsx("rect", { x: x + ellipseRx, y: y, width: width - ellipseRx * 2, height: height, fill: fill, stroke: fill }), _jsx("ellipse", { cx: x + ellipseRx, cy: y + height / 2, rx: ellipseRx, ry: height / 2, fill: fill, stroke: stroke, strokeWidth: 2.5, strokeDasharray: isExt ? '8 4' : 'none' }), _jsx("ellipse", { cx: x + width - ellipseRx, cy: y + height / 2, rx: ellipseRx, ry: height / 2, fill: fill, stroke: stroke, strokeWidth: 2.5, strokeDasharray: isExt ? '8 4' : 'none' }), _jsx("line", { x1: x + ellipseRx, y1: y, x2: x + width - ellipseRx, y2: y, stroke: stroke, strokeWidth: 2.5 }), _jsx("line", { x1: x + ellipseRx, y1: y + height, x2: x + width - ellipseRx, y2: y + height, stroke: stroke, strokeWidth: 2.5 }), _jsx("text", { x: x + width / 2, y: y + height / 2 - (element.properties.tech ? 6 : 0), textAnchor: "middle", dominantBaseline: "middle", fill: colors.textPrimary, fontSize: NAME_FONT_SIZE, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: element.name }), element.properties.tech && (_jsxs("text", { x: x + width / 2, y: y + height / 2 + 12, textAnchor: "middle", fill: colors.textSecondary, fontSize: TECH_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.8, children: ["[", element.properties.tech, "]"] })), element.properties.description && (_jsx(DescriptionText, { x: x + width / 2, y: y + height / 2 + (element.properties.tech ? 26 : 14), text: element.properties.description, nodeWidth: width, fill: colors.textSecondary, fontSize: DESCRIPTION_FONT_SIZE }))] }));
|
|
371
|
-
}
|
|
372
|
-
function Component({ node, colors }) {
|
|
373
|
-
const { x, y, width, height, element } = node;
|
|
374
|
-
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: width, height: height, rx: BORDER_RADIUS / 2, ry: BORDER_RADIUS / 2, fill: colors.componentFill, stroke: colors.componentStroke, strokeWidth: 2.5 }), _jsx("text", { x: x + width / 2, y: y + height / 2 - (element.properties.tech ? 4 : 0), textAnchor: "middle", dominantBaseline: "middle", fill: colors.textPrimary, fontSize: NAME_FONT_SIZE - 1, fontWeight: "500", fontFamily: "system-ui, -apple-system, sans-serif", children: element.name }), element.properties.tech && (_jsxs("text", { x: x + width / 2, y: y + height / 2 + 12, textAnchor: "middle", fill: colors.textSecondary, fontSize: TECH_FONT_SIZE - 1, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.8, children: ["[", element.properties.tech, "]"] }))] }));
|
|
375
|
-
}
|
|
376
|
-
function Person({ node, colors }) {
|
|
377
|
-
const { x, y, width, height, element } = node;
|
|
378
|
-
const centerX = x + width / 2;
|
|
379
|
-
const iconY = y + 12;
|
|
380
|
-
return (_jsxs("g", { children: [_jsx("circle", { cx: centerX, cy: iconY + PERSON_HEAD_RADIUS, r: PERSON_HEAD_RADIUS, fill: colors.personFill, stroke: colors.personStroke, strokeWidth: 1.5 }), _jsx("rect", { x: centerX - PERSON_BODY_WIDTH / 2, y: iconY + PERSON_HEAD_RADIUS * 2 + 4, width: PERSON_BODY_WIDTH, height: PERSON_BODY_HEIGHT, rx: PERSON_BODY_WIDTH / 2, ry: 8, fill: colors.personFill, stroke: colors.personStroke, strokeWidth: 1.5 }), _jsx("text", { x: centerX, y: y + height - 18, textAnchor: "middle", fill: colors.textOnBoundary, fontSize: NAME_FONT_SIZE, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: element.name }), element.properties.description && (_jsx("text", { x: centerX, y: y + height - 4, textAnchor: "middle", fill: colors.textOnBoundary, fontSize: DESCRIPTION_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", opacity: 0.7, children: element.properties.description.length > 20
|
|
381
|
-
? element.properties.description.slice(0, 17) + '...'
|
|
382
|
-
: element.properties.description }))] }));
|
|
383
|
-
}
|
|
384
|
-
function EdgeComponent({ edge, colors }) {
|
|
385
|
-
const { points, label } = edge;
|
|
386
|
-
if (points.length < 2)
|
|
387
|
-
return _jsx(_Fragment, {});
|
|
388
|
-
const shortenedPoints = shortenEdgeEnd(points);
|
|
389
|
-
const arrowPoints = calculateArrowheadPoints(shortenedPoints[shortenedPoints.length - 1], points[points.length - 1]);
|
|
390
|
-
const midpoint = {
|
|
391
|
-
x: (points[0].x + points[points.length - 1].x) / 2,
|
|
392
|
-
y: (points[0].y + points[points.length - 1].y) / 2,
|
|
393
|
-
};
|
|
394
|
-
const edgeDx = points[points.length - 1].x - points[0].x;
|
|
395
|
-
const edgeDy = points[points.length - 1].y - points[0].y;
|
|
396
|
-
let angleDeg = Math.atan2(edgeDy, edgeDx) * (180 / Math.PI);
|
|
397
|
-
if (angleDeg > 90)
|
|
398
|
-
angleDeg -= 180;
|
|
399
|
-
if (angleDeg < -90)
|
|
400
|
-
angleDeg += 180;
|
|
401
|
-
if (Math.abs(angleDeg) > 60)
|
|
402
|
-
angleDeg = 0;
|
|
403
|
-
const labelX = midpoint.x;
|
|
404
|
-
const labelY = midpoint.y;
|
|
405
|
-
const labelTransform = `rotate(${angleDeg}, ${labelX}, ${labelY})`;
|
|
406
|
-
// Compute edge paths with label clipping
|
|
407
|
-
let edgePaths;
|
|
408
|
-
if (label) {
|
|
409
|
-
if (edge.isStepNumber && edge.displayLabel) {
|
|
410
|
-
// Step number with inline text: clip around the displayLabel text (above the circle).
|
|
411
|
-
// The circle itself is opaque and covers the line naturally.
|
|
412
|
-
const displayLines = splitEdgeLabel(edge.displayLabel);
|
|
413
|
-
const longestLine = displayLines.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
414
|
-
const avgCharWidth = EDGE_LABEL_FONT_SIZE * 0.55;
|
|
415
|
-
const halfW = (longestLine.length * avgCharWidth) / 2 + 3;
|
|
416
|
-
const ascent = EDGE_LABEL_FONT_SIZE * 0.8;
|
|
417
|
-
const descent = EDGE_LABEL_FONT_SIZE * 0.2;
|
|
418
|
-
const pad = 3;
|
|
419
|
-
// displayLabel renders at labelY - 16
|
|
420
|
-
const textBaseline = labelY - 16;
|
|
421
|
-
const clipTop = textBaseline - ascent - pad;
|
|
422
|
-
const clipBottom = textBaseline + descent + pad;
|
|
423
|
-
const clipCenterY = (clipTop + clipBottom) / 2;
|
|
424
|
-
const halfH = (clipBottom - clipTop) / 2;
|
|
425
|
-
edgePaths = buildClippedEdgePaths(shortenedPoints, { x: labelX, y: clipCenterY }, halfW, halfH, angleDeg);
|
|
426
|
-
}
|
|
427
|
-
else if (edge.isStepNumber || edge.isLegendRef) {
|
|
428
|
-
// Circle/square shapes are opaque — no line clipping needed
|
|
429
|
-
edgePaths = [shortenedPoints.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')];
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
// Clip around text label
|
|
433
|
-
const lines = splitEdgeLabel(label);
|
|
434
|
-
const longestLine = lines.reduce((a, b) => (a.length > b.length ? a : b), '');
|
|
435
|
-
const avgCharWidth = EDGE_LABEL_FONT_SIZE * 0.55;
|
|
436
|
-
const halfW = (longestLine.length * avgCharWidth) / 2 + 3;
|
|
437
|
-
const lineHeight = 14;
|
|
438
|
-
const ascent = EDGE_LABEL_FONT_SIZE * 0.8;
|
|
439
|
-
const descent = EDGE_LABEL_FONT_SIZE * 0.2;
|
|
440
|
-
const pad = 3;
|
|
441
|
-
let clipTop;
|
|
442
|
-
let clipBottom;
|
|
443
|
-
if (lines.length === 1) {
|
|
444
|
-
const baseline = labelY + 4;
|
|
445
|
-
clipTop = baseline - ascent - pad;
|
|
446
|
-
clipBottom = baseline + descent + pad;
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
const firstBaseline = labelY - ((lines.length - 1) * lineHeight) / 2;
|
|
450
|
-
const lastBaseline = firstBaseline + (lines.length - 1) * lineHeight;
|
|
451
|
-
clipTop = firstBaseline - ascent - pad;
|
|
452
|
-
clipBottom = lastBaseline + descent + pad;
|
|
453
|
-
}
|
|
454
|
-
const clipCenterY = (clipTop + clipBottom) / 2;
|
|
455
|
-
const halfH = (clipBottom - clipTop) / 2;
|
|
456
|
-
edgePaths = buildClippedEdgePaths(shortenedPoints, { x: labelX, y: clipCenterY }, halfW, halfH, angleDeg);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
460
|
-
edgePaths = [shortenedPoints.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')];
|
|
461
|
-
}
|
|
462
|
-
// Render label content
|
|
463
|
-
let labelContent = null;
|
|
464
|
-
if (label) {
|
|
465
|
-
if (edge.isStepNumber || edge.isLegendRef) {
|
|
466
|
-
labelContent = (_jsxs(_Fragment, { children: [edge.isStepNumber ? (_jsx("circle", { cx: labelX, cy: labelY, r: 10, fill: colors.edgeLabelBackground, stroke: colors.edgeStroke, strokeWidth: 1.5 })) : (_jsx("rect", { x: labelX - 10, y: labelY - 10, width: 20, height: 20, rx: 3, fill: colors.edgeLabelBackground, stroke: colors.edgeStroke, strokeWidth: 1.5 })), _jsx("text", { x: labelX, y: labelY + 4, textAnchor: "middle", fill: colors.edgeStroke, fontSize: 10, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: label }), edge.isStepNumber && edge.displayLabel && (_jsx("text", { x: labelX, y: labelY - 16, textAnchor: "middle", fill: colors.edgeLabel, fontSize: EDGE_LABEL_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", transform: labelTransform, children: edge.displayLabel }))] }));
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
const lines = splitEdgeLabel(label);
|
|
470
|
-
const lineHeight = 14;
|
|
471
|
-
const startY = lines.length === 1
|
|
472
|
-
? labelY + 4
|
|
473
|
-
: labelY - ((lines.length - 1) * lineHeight) / 2;
|
|
474
|
-
labelContent = (_jsx("text", { x: labelX, textAnchor: "middle", fill: colors.edgeLabel, fontSize: EDGE_LABEL_FONT_SIZE, fontFamily: "system-ui, -apple-system, sans-serif", transform: labelTransform, children: lines.map((line, i) => (_jsx("tspan", { x: labelX, y: startY + i * lineHeight, children: line }, i))) }));
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return (_jsxs("g", { children: [edgePaths.map((d, i) => (_jsx("path", { d: d, fill: "none", stroke: colors.edgeStroke, strokeWidth: 1.5 }, i))), arrowPoints && _jsx("polygon", { points: arrowPoints, fill: colors.edgeStroke }), labelContent] }));
|
|
478
|
-
}
|
|
479
|
-
// =============================================================================
|
|
480
|
-
// LEGEND RENDERER
|
|
481
|
-
// =============================================================================
|
|
482
|
-
function Legend({ entries, placement, colors }) {
|
|
483
|
-
const circleR = 9;
|
|
484
|
-
const baseY = placement.y + circleR + 4;
|
|
485
|
-
return (_jsx("g", { className: "legend-layer", children: entries.map((entry, i) => {
|
|
486
|
-
const rowY = baseY + i * LEGEND_ROW_HEIGHT;
|
|
487
|
-
const symbolCenterX = placement.x + circleR;
|
|
488
|
-
const symbolCenterY = rowY - 4;
|
|
489
|
-
const textY = rowY;
|
|
490
|
-
return (_jsxs("g", { children: [entry.isStep ? (_jsx("circle", { cx: symbolCenterX, cy: symbolCenterY, r: circleR, fill: colors.edgeLabelBackground, stroke: colors.edgeStroke, strokeWidth: 1.5 })) : (_jsx("rect", { x: symbolCenterX - circleR, y: symbolCenterY - circleR, width: circleR * 2, height: circleR * 2, rx: 3, fill: colors.edgeLabelBackground, stroke: colors.edgeStroke, strokeWidth: 1.5 })), _jsx("text", { x: symbolCenterX, y: textY, textAnchor: "middle", fill: colors.edgeStroke, fontSize: 10, fontWeight: "600", fontFamily: "system-ui, -apple-system, sans-serif", children: entry.index }), _jsx("text", { x: placement.x + circleR * 2 + 8, y: textY, fill: colors.edgeLabel, fontSize: 11, fontFamily: "system-ui, -apple-system, sans-serif", children: entry.label })] }, i));
|
|
491
|
-
}) }));
|
|
492
|
-
}
|
|
493
|
-
// =============================================================================
|
|
494
|
-
// NODE RENDERER DISPATCHER
|
|
495
|
-
// =============================================================================
|
|
496
|
-
function renderNode(node, colors, allNodes, parentMap) {
|
|
497
|
-
const { element } = node;
|
|
498
|
-
switch (element.type) {
|
|
499
|
-
case 'system':
|
|
500
|
-
return _jsx(SystemBoundary, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap }, node.id);
|
|
501
|
-
case 'container':
|
|
502
|
-
return _jsx(Container, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap }, node.id);
|
|
503
|
-
case 'component':
|
|
504
|
-
return _jsx(Component, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap }, node.id);
|
|
505
|
-
case 'person':
|
|
506
|
-
return _jsx(Person, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap }, node.id);
|
|
507
|
-
default:
|
|
508
|
-
return _jsx(Container, { node: node, colors: colors, allNodes: allNodes, parentMap: parentMap }, node.id);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* C4 Diagram SVG Renderer.
|
|
513
|
-
*
|
|
514
|
-
* Pure component: takes layout + theme, returns SVG.
|
|
515
|
-
* Used directly in the editor and via renderToStaticMarkup for publishing.
|
|
516
|
-
*/
|
|
517
|
-
export function C4Renderer({ layout, isDarkMode }) {
|
|
518
|
-
const colors = getColors(isDarkMode);
|
|
519
|
-
const boundaryNodes = layout.nodes.filter((n) => n.element.type === 'system' &&
|
|
520
|
-
((n.children?.length ?? 0) > 0 || n.element.properties.style === 'boundary'));
|
|
521
|
-
const regularNodes = layout.nodes.filter((n) => !(n.element.type === 'system' &&
|
|
522
|
-
((n.children?.length ?? 0) > 0 || n.element.properties.style === 'boundary')));
|
|
523
|
-
const parentMap = buildParentMap(layout.nodes);
|
|
524
|
-
const { edges: processedEdges, legendEntries } = processEdgesForLegend(layout.edges);
|
|
525
|
-
let totalWidth = layout.width;
|
|
526
|
-
let totalHeight = layout.height;
|
|
527
|
-
let legendPlacement = null;
|
|
528
|
-
if (legendEntries.length > 0) {
|
|
529
|
-
const lp = computeLegendPlacement(legendEntries, layout);
|
|
530
|
-
legendPlacement = { x: lp.x, y: lp.y };
|
|
531
|
-
totalWidth = lp.totalWidth;
|
|
532
|
-
totalHeight = lp.totalHeight;
|
|
533
|
-
}
|
|
534
|
-
return (_jsxs("svg", { width: totalWidth, height: totalHeight, viewBox: `${layout.viewBoxX} ${layout.viewBoxY} ${totalWidth} ${totalHeight}`, xmlns: "http://www.w3.org/2000/svg", "data-diagram": "c4", style: { fontFamily: 'system-ui, -apple-system, sans-serif' }, children: [_jsx("g", { className: "boundaries-layer", children: boundaryNodes.map((node) => renderNode(node, colors, layout.nodes, parentMap)) }), _jsx("g", { className: "nodes-layer", children: regularNodes.map((node) => renderNode(node, colors, layout.nodes, parentMap)) }), _jsx("g", { className: "edges-layer", children: processedEdges.map((edge, i) => (_jsx(EdgeComponent, { edge: edge, colors: colors }, `${edge.source}-${edge.target}-${i}`))) }), legendPlacement && legendEntries.length > 0 && (_jsx(Legend, { entries: legendEntries, placement: legendPlacement, colors: colors }))] }));
|
|
535
|
-
}
|
|
536
|
-
export function C4RendererContent({ layout, isDarkMode, legendPositionOverride }) {
|
|
537
|
-
const colors = getColors(isDarkMode);
|
|
538
|
-
const boundaryNodes = layout.nodes.filter((n) => n.element.type === 'system' &&
|
|
539
|
-
((n.children?.length ?? 0) > 0 || n.element.properties.style === 'boundary'));
|
|
540
|
-
const regularNodes = layout.nodes.filter((n) => !(n.element.type === 'system' &&
|
|
541
|
-
((n.children?.length ?? 0) > 0 || n.element.properties.style === 'boundary')));
|
|
542
|
-
const parentMap = buildParentMap(layout.nodes);
|
|
543
|
-
const { edges: processedEdges, legendEntries } = processEdgesForLegend(layout.edges);
|
|
544
|
-
let legendPlacement = null;
|
|
545
|
-
if (legendEntries.length > 0) {
|
|
546
|
-
if (legendPositionOverride) {
|
|
547
|
-
legendPlacement = legendPositionOverride;
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
const lp = computeLegendPlacement(legendEntries, layout);
|
|
551
|
-
legendPlacement = { x: lp.x, y: lp.y };
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "boundaries-layer", children: boundaryNodes.map((node) => renderNode(node, colors, layout.nodes, parentMap)) }), _jsx("g", { className: "nodes-layer", children: regularNodes.map((node) => renderNode(node, colors, layout.nodes, parentMap)) }), _jsx("g", { className: "edges-layer", children: processedEdges.map((edge, i) => (_jsx(EdgeComponent, { edge: edge, colors: colors }, `${edge.source}-${edge.target}-${i}`))) }), legendPlacement && legendEntries.length > 0 && (_jsx(Legend, { entries: legendEntries, placement: legendPlacement, colors: colors }))] }));
|
|
555
|
-
}
|
|
556
|
-
export function C4ErrorDisplay({ errors, isDarkMode }) {
|
|
557
|
-
const errorColor = isDarkMode ? '#ff5e5b' : '#de0013'; // oklch(0.70 0.20 25) / oklch(0.55 0.25 25)
|
|
558
|
-
const bgColor = isDarkMode ? '#201616' : '#fbe9e7'; // oklch(0.20 0.02 25) / oklch(0.95 0.02 25)
|
|
559
|
-
const textColor = isDarkMode ? '#cacaca' : '#3d3d3d'; // oklch(0.85 0.00 0) / oklch(0.25 0.00 0)
|
|
560
|
-
return (_jsxs("div", { style: {
|
|
561
|
-
padding: '1rem',
|
|
562
|
-
backgroundColor: bgColor,
|
|
563
|
-
borderRadius: '8px',
|
|
564
|
-
border: `1px solid ${errorColor}`,
|
|
565
|
-
fontFamily: 'ui-monospace, monospace',
|
|
566
|
-
fontSize: '13px',
|
|
567
|
-
}, children: [_jsx("div", { style: { color: errorColor, fontWeight: 600, marginBottom: '0.5rem' }, children: "C4 Diagram Parse Error" }), errors.map((error, i) => (_jsxs("div", { style: { color: textColor, marginBottom: '0.25rem' }, children: ["Line ", error.line, ", Column ", error.column, ": ", error.message] }, i)))] }));
|
|
568
|
-
}
|
|
569
|
-
export default C4Renderer;
|