@diagrammo/dgmo 0.2.20 → 0.2.21
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/dist/cli.cjs +99 -97
- package/dist/index.cjs +949 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +942 -261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/d3.ts +44 -1
- package/src/dgmo-router.ts +8 -1
- package/src/index.ts +11 -0
- package/src/kanban/mutations.ts +183 -0
- package/src/kanban/parser.ts +389 -0
- package/src/kanban/renderer.ts +564 -0
- package/src/kanban/types.ts +45 -0
- package/src/org/layout.ts +61 -55
- package/src/org/parser.ts +15 -1
- package/src/org/renderer.ts +79 -160
- package/src/sequence/renderer.ts +7 -5
package/src/org/layout.ts
CHANGED
|
@@ -97,18 +97,17 @@ const CONTAINER_META_LINE_HEIGHT = 16;
|
|
|
97
97
|
const STACK_V_GAP = 20;
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
// Legend
|
|
100
|
+
// Legend (kanban-style pills)
|
|
101
101
|
const LEGEND_GAP = 30;
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const EYE_ICON_GAP = 6;
|
|
102
|
+
const LEGEND_HEIGHT = 28;
|
|
103
|
+
const LEGEND_PILL_PAD = 16;
|
|
104
|
+
const LEGEND_PILL_FONT_W = 11 * 0.6;
|
|
105
|
+
const LEGEND_CAPSULE_PAD = 4;
|
|
106
|
+
const LEGEND_DOT_R = 4;
|
|
107
|
+
const LEGEND_ENTRY_FONT_W = 10 * 0.6;
|
|
108
|
+
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
109
|
+
const LEGEND_ENTRY_TRAIL = 8;
|
|
110
|
+
const LEGEND_GROUP_GAP = 12;
|
|
112
111
|
|
|
113
112
|
// ============================================================
|
|
114
113
|
// Helpers
|
|
@@ -271,49 +270,35 @@ function centerHeavyChildren(node: TreeNode): void {
|
|
|
271
270
|
// Layout
|
|
272
271
|
// ============================================================
|
|
273
272
|
|
|
274
|
-
function computeLegendGroups(tagGroups: OrgTagGroup[],
|
|
273
|
+
function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean): OrgLegendGroup[] {
|
|
275
274
|
const groups: OrgLegendGroup[] = [];
|
|
276
275
|
|
|
277
276
|
for (const group of tagGroups) {
|
|
278
277
|
if (group.entries.length === 0) continue;
|
|
279
278
|
|
|
280
|
-
const
|
|
281
|
-
(e) =>
|
|
282
|
-
LEGEND_DOT_R * 2 + LEGEND_DOT_TEXT_GAP + e.value.length * CHAR_WIDTH
|
|
283
|
-
);
|
|
279
|
+
const pillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
284
280
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
maxW = entryWidths[idx];
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (maxW > 0) colWidths.push(maxW);
|
|
281
|
+
// Capsule: pad + pill + gap + entries + pad
|
|
282
|
+
let entriesWidth = 0;
|
|
283
|
+
for (const entry of group.entries) {
|
|
284
|
+
entriesWidth +=
|
|
285
|
+
LEGEND_DOT_R * 2 +
|
|
286
|
+
LEGEND_ENTRY_DOT_GAP +
|
|
287
|
+
entry.value.length * LEGEND_ENTRY_FONT_W +
|
|
288
|
+
LEGEND_ENTRY_TRAIL;
|
|
297
289
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const headerWidth = group.name.length * CHAR_WIDTH + eyeExtra;
|
|
301
|
-
const totalColumnsWidth =
|
|
302
|
-
colWidths.reduce((s, w) => s + w, 0) +
|
|
303
|
-
(colWidths.length - 1) * LEGEND_ENTRY_GAP;
|
|
304
|
-
const maxRowWidth = Math.max(headerWidth, totalColumnsWidth);
|
|
305
|
-
|
|
306
|
-
const minifiedWidth = group.name.length * CHAR_WIDTH + LEGEND_PAD * 2;
|
|
290
|
+
const capsuleWidth =
|
|
291
|
+
LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
|
|
307
292
|
|
|
308
293
|
groups.push({
|
|
309
294
|
name: group.name,
|
|
310
295
|
entries: group.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
311
296
|
x: 0,
|
|
312
297
|
y: 0,
|
|
313
|
-
width:
|
|
314
|
-
height:
|
|
315
|
-
minifiedWidth,
|
|
316
|
-
minifiedHeight:
|
|
298
|
+
width: capsuleWidth,
|
|
299
|
+
height: LEGEND_HEIGHT,
|
|
300
|
+
minifiedWidth: pillWidth,
|
|
301
|
+
minifiedHeight: LEGEND_HEIGHT,
|
|
317
302
|
});
|
|
318
303
|
}
|
|
319
304
|
|
|
@@ -356,7 +341,28 @@ export function layoutOrg(
|
|
|
356
341
|
hiddenAttributes?: Set<string>
|
|
357
342
|
): OrgLayoutResult {
|
|
358
343
|
if (parsed.roots.length === 0) {
|
|
359
|
-
|
|
344
|
+
// Legend-only: compute and position legend groups even without nodes
|
|
345
|
+
const showEyeIcons = hiddenAttributes !== undefined;
|
|
346
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons);
|
|
347
|
+
if (legendGroups.length === 0) {
|
|
348
|
+
return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Layout legend groups horizontally (all minified when no nodes)
|
|
352
|
+
let cx = MARGIN;
|
|
353
|
+
for (const g of legendGroups) {
|
|
354
|
+
g.x = cx;
|
|
355
|
+
g.y = MARGIN;
|
|
356
|
+
cx += g.minifiedWidth + LEGEND_GROUP_GAP;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
nodes: [],
|
|
360
|
+
edges: [],
|
|
361
|
+
containers: [],
|
|
362
|
+
legend: legendGroups,
|
|
363
|
+
width: cx - LEGEND_GROUP_GAP + MARGIN,
|
|
364
|
+
height: LEGEND_HEIGHT + MARGIN * 2,
|
|
365
|
+
};
|
|
360
366
|
}
|
|
361
367
|
|
|
362
368
|
// Inject default tag group values into node metadata for display.
|
|
@@ -1101,7 +1107,7 @@ export function layoutOrg(
|
|
|
1101
1107
|
// Bottom: center legend groups horizontally below diagram content
|
|
1102
1108
|
const totalGroupsWidth =
|
|
1103
1109
|
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1104
|
-
(visibleGroups.length - 1) *
|
|
1110
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1105
1111
|
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1106
1112
|
|
|
1107
1113
|
if (neededWidth > totalWidth) {
|
|
@@ -1119,34 +1125,34 @@ export function layoutOrg(
|
|
|
1119
1125
|
const startX = (finalWidth - totalGroupsWidth) / 2;
|
|
1120
1126
|
|
|
1121
1127
|
let cx = startX;
|
|
1122
|
-
let maxH = 0;
|
|
1123
1128
|
for (const g of visibleGroups) {
|
|
1124
1129
|
g.x = cx;
|
|
1125
1130
|
g.y = legendY;
|
|
1126
|
-
cx += effectiveW(g) +
|
|
1127
|
-
const h = effectiveH(g);
|
|
1128
|
-
if (h > maxH) maxH = h;
|
|
1131
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1129
1132
|
}
|
|
1130
1133
|
|
|
1131
|
-
finalHeight = totalHeight + LEGEND_GAP +
|
|
1134
|
+
finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
|
|
1132
1135
|
} else {
|
|
1133
|
-
// Top
|
|
1134
|
-
const
|
|
1136
|
+
// Top: horizontal row at top-right
|
|
1137
|
+
const totalGroupsWidth =
|
|
1138
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1139
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1135
1140
|
const legendStartX = totalWidth - MARGIN + LEGEND_GAP;
|
|
1136
|
-
|
|
1141
|
+
const legendY = MARGIN;
|
|
1137
1142
|
|
|
1143
|
+
let cx = legendStartX;
|
|
1138
1144
|
for (const g of visibleGroups) {
|
|
1139
|
-
g.x =
|
|
1145
|
+
g.x = cx;
|
|
1140
1146
|
g.y = legendY;
|
|
1141
|
-
|
|
1147
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1142
1148
|
}
|
|
1143
1149
|
|
|
1144
|
-
const legendRight = legendStartX +
|
|
1150
|
+
const legendRight = legendStartX + totalGroupsWidth + MARGIN;
|
|
1145
1151
|
if (legendRight > finalWidth) {
|
|
1146
1152
|
finalWidth = legendRight;
|
|
1147
1153
|
}
|
|
1148
1154
|
|
|
1149
|
-
const legendBottom = legendY
|
|
1155
|
+
const legendBottom = legendY + LEGEND_HEIGHT + MARGIN;
|
|
1150
1156
|
if (legendBottom > finalHeight) {
|
|
1151
1157
|
finalHeight = legendBottom;
|
|
1152
1158
|
}
|
package/src/org/parser.ts
CHANGED
|
@@ -79,6 +79,20 @@ const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
|
|
|
79
79
|
const TITLE_RE = /^title\s*:\s*(.+)/i;
|
|
80
80
|
const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
81
81
|
|
|
82
|
+
// ============================================================
|
|
83
|
+
// Inference
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
/** Returns true if content contains tag group headings (`## ...`), suggesting an org chart. */
|
|
87
|
+
export function looksLikeOrg(content: string): boolean {
|
|
88
|
+
for (const line of content.split('\n')) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
91
|
+
if (GROUP_HEADING_RE.test(trimmed)) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
// ============================================================
|
|
83
97
|
// Parser
|
|
84
98
|
// ============================================================
|
|
@@ -301,7 +315,7 @@ export function parseOrg(
|
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
317
|
|
|
304
|
-
if (result.roots.length === 0 && !result.error) {
|
|
318
|
+
if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
|
|
305
319
|
const diag = makeDgmoError(1, 'No nodes found in org chart');
|
|
306
320
|
result.diagnostics.push(diag);
|
|
307
321
|
result.error = formatDgmoError(diag);
|
package/src/org/renderer.ts
CHANGED
|
@@ -36,29 +36,17 @@ const CONTAINER_HEADER_HEIGHT = 28;
|
|
|
36
36
|
const COLLAPSE_BAR_HEIGHT = 6;
|
|
37
37
|
const COLLAPSE_BAR_INSET = 0;
|
|
38
38
|
|
|
39
|
-
// Legend
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
// Eye icon (12×12 viewBox, scaled from 0,0 to 12,12)
|
|
52
|
-
const EYE_ICON_SIZE = 12;
|
|
53
|
-
const EYE_ICON_GAP = 6;
|
|
54
|
-
// Open eye: elliptical outline + circle pupil
|
|
55
|
-
const EYE_OPEN_PATH =
|
|
56
|
-
'M1 6C1 6 3 2 6 2C9 2 11 6 11 6C11 6 9 10 6 10C3 10 1 6 1 6Z';
|
|
57
|
-
const EYE_PUPIL_CX = 6;
|
|
58
|
-
const EYE_PUPIL_CY = 6;
|
|
59
|
-
const EYE_PUPIL_R = 1.8;
|
|
60
|
-
// Closed eye: same outline + diagonal slash
|
|
61
|
-
const EYE_SLASH_PATH = 'M2 2L10 10';
|
|
39
|
+
// Legend (kanban-style pills)
|
|
40
|
+
const LEGEND_HEIGHT = 28;
|
|
41
|
+
const LEGEND_PILL_PAD = 16;
|
|
42
|
+
const LEGEND_PILL_FONT_SIZE = 11;
|
|
43
|
+
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
44
|
+
const LEGEND_CAPSULE_PAD = 4;
|
|
45
|
+
const LEGEND_DOT_R = 4;
|
|
46
|
+
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
47
|
+
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
48
|
+
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
49
|
+
const LEGEND_ENTRY_TRAIL = 8;
|
|
62
50
|
|
|
63
51
|
// ============================================================
|
|
64
52
|
// Color helpers (inline to avoid cross-module import issues)
|
|
@@ -457,10 +445,11 @@ export function renderOrg(
|
|
|
457
445
|
|
|
458
446
|
}
|
|
459
447
|
|
|
460
|
-
// Render legend —
|
|
461
|
-
//
|
|
462
|
-
// Active group
|
|
463
|
-
|
|
448
|
+
// Render legend — kanban-style pills.
|
|
449
|
+
// Skip in export mode (unless legend-only chart).
|
|
450
|
+
// Active group: only that group rendered as capsule (pill + entries).
|
|
451
|
+
// No active group: all groups rendered as standalone pills.
|
|
452
|
+
if (!exportDims || layout.nodes.length === 0) for (const group of layout.legend) {
|
|
464
453
|
const isActive =
|
|
465
454
|
activeTagGroup != null &&
|
|
466
455
|
group.name.toLowerCase() === activeTagGroup.toLowerCase();
|
|
@@ -468,11 +457,10 @@ export function renderOrg(
|
|
|
468
457
|
// When a group is active, skip all other groups entirely
|
|
469
458
|
if (activeTagGroup != null && !isActive) continue;
|
|
470
459
|
|
|
471
|
-
|
|
472
|
-
const isMinified = activeTagGroup == null;
|
|
460
|
+
const groupBg = mix(palette.surface, palette.bg, isDark ? 35 : 20);
|
|
473
461
|
|
|
474
|
-
const
|
|
475
|
-
|
|
462
|
+
const pillWidth =
|
|
463
|
+
group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
476
464
|
|
|
477
465
|
const gEl = contentG
|
|
478
466
|
.append('g')
|
|
@@ -481,146 +469,77 @@ export function renderOrg(
|
|
|
481
469
|
.attr('data-legend-group', group.name.toLowerCase())
|
|
482
470
|
.style('cursor', 'pointer');
|
|
483
471
|
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
.attr('fill', legendFill);
|
|
472
|
+
// Outer capsule background (active only)
|
|
473
|
+
if (isActive) {
|
|
474
|
+
gEl
|
|
475
|
+
.append('rect')
|
|
476
|
+
.attr('width', group.width)
|
|
477
|
+
.attr('height', LEGEND_HEIGHT)
|
|
478
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
479
|
+
.attr('fill', groupBg);
|
|
480
|
+
}
|
|
494
481
|
|
|
482
|
+
const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
483
|
+
const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
484
|
+
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
485
|
+
|
|
486
|
+
// Pill background
|
|
487
|
+
gEl
|
|
488
|
+
.append('rect')
|
|
489
|
+
.attr('x', pillX)
|
|
490
|
+
.attr('y', pillY)
|
|
491
|
+
.attr('width', pillWidth)
|
|
492
|
+
.attr('height', pillH)
|
|
493
|
+
.attr('rx', pillH / 2)
|
|
494
|
+
.attr('fill', isActive ? palette.bg : groupBg);
|
|
495
|
+
|
|
496
|
+
// Active pill border
|
|
495
497
|
if (isActive) {
|
|
496
|
-
|
|
497
|
-
.
|
|
498
|
-
.attr('
|
|
499
|
-
.attr('
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
.attr('
|
|
503
|
-
.attr('
|
|
504
|
-
.attr('stroke
|
|
498
|
+
gEl
|
|
499
|
+
.append('rect')
|
|
500
|
+
.attr('x', pillX)
|
|
501
|
+
.attr('y', pillY)
|
|
502
|
+
.attr('width', pillWidth)
|
|
503
|
+
.attr('height', pillH)
|
|
504
|
+
.attr('rx', pillH / 2)
|
|
505
|
+
.attr('fill', 'none')
|
|
506
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
507
|
+
.attr('stroke-width', 0.75);
|
|
505
508
|
}
|
|
506
509
|
|
|
507
|
-
//
|
|
510
|
+
// Pill text
|
|
508
511
|
gEl
|
|
509
512
|
.append('text')
|
|
510
|
-
.attr('x',
|
|
511
|
-
.attr('y',
|
|
512
|
-
.attr('
|
|
513
|
-
.attr('font-
|
|
514
|
-
.attr('
|
|
513
|
+
.attr('x', pillX + pillWidth / 2)
|
|
514
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
515
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
516
|
+
.attr('font-weight', '500')
|
|
517
|
+
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
518
|
+
.attr('text-anchor', 'middle')
|
|
515
519
|
.text(group.name);
|
|
516
520
|
|
|
517
|
-
//
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const groupKey = group.name.toLowerCase();
|
|
523
|
-
const isHidden = hiddenAttributes.has(groupKey);
|
|
524
|
-
const eyeX =
|
|
525
|
-
LEGEND_PAD + group.name.length * LEGEND_CHAR_WIDTH + EYE_ICON_GAP;
|
|
526
|
-
const eyeY = (LEGEND_HEADER_H - EYE_ICON_SIZE) / 2;
|
|
527
|
-
|
|
528
|
-
const eyeG = gEl
|
|
529
|
-
.append('g')
|
|
530
|
-
.attr('class', 'org-legend-eye')
|
|
531
|
-
.attr('data-legend-visibility', groupKey)
|
|
532
|
-
.attr('transform', `translate(${eyeX}, ${eyeY})`);
|
|
533
|
-
|
|
534
|
-
// Transparent hit area
|
|
535
|
-
eyeG
|
|
536
|
-
.append('rect')
|
|
537
|
-
.attr('x', -4)
|
|
538
|
-
.attr('y', -4)
|
|
539
|
-
.attr('width', EYE_ICON_SIZE + 8)
|
|
540
|
-
.attr('height', EYE_ICON_SIZE + 8)
|
|
541
|
-
.attr('fill', 'transparent');
|
|
542
|
-
|
|
543
|
-
// Eye outline
|
|
544
|
-
eyeG
|
|
545
|
-
.append('path')
|
|
546
|
-
.attr('d', EYE_OPEN_PATH)
|
|
547
|
-
.attr('fill', isHidden ? 'none' : palette.textMuted)
|
|
548
|
-
.attr('fill-opacity', isHidden ? 0 : 0.15)
|
|
549
|
-
.attr('stroke', palette.textMuted)
|
|
550
|
-
.attr('stroke-width', 1.2)
|
|
551
|
-
.attr('opacity', isHidden ? 0.5 : 0.7);
|
|
552
|
-
|
|
553
|
-
if (!isHidden) {
|
|
554
|
-
// Pupil (only when visible)
|
|
555
|
-
eyeG
|
|
521
|
+
// Entries inside capsule (active only)
|
|
522
|
+
if (isActive) {
|
|
523
|
+
let entryX = pillX + pillWidth + 4;
|
|
524
|
+
for (const entry of group.entries) {
|
|
525
|
+
gEl
|
|
556
526
|
.append('circle')
|
|
557
|
-
.attr('cx',
|
|
558
|
-
.attr('cy',
|
|
559
|
-
.attr('r',
|
|
527
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
528
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
529
|
+
.attr('r', LEGEND_DOT_R)
|
|
530
|
+
.attr('fill', entry.color);
|
|
531
|
+
|
|
532
|
+
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
533
|
+
gEl
|
|
534
|
+
.append('text')
|
|
535
|
+
.attr('x', textX)
|
|
536
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
537
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
560
538
|
.attr('fill', palette.textMuted)
|
|
561
|
-
.
|
|
562
|
-
} else {
|
|
563
|
-
// Slash through the eye (hidden state)
|
|
564
|
-
eyeG
|
|
565
|
-
.append('line')
|
|
566
|
-
.attr('x1', 2)
|
|
567
|
-
.attr('y1', 2)
|
|
568
|
-
.attr('x2', 10)
|
|
569
|
-
.attr('y2', 10)
|
|
570
|
-
.attr('stroke', palette.textMuted)
|
|
571
|
-
.attr('stroke-width', 1.5)
|
|
572
|
-
.attr('opacity', 0.5);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
539
|
+
.text(entry.value);
|
|
575
540
|
|
|
576
|
-
|
|
577
|
-
const entryWidths = group.entries.map(
|
|
578
|
-
(e) =>
|
|
579
|
-
LEGEND_DOT_R * 2 + LEGEND_DOT_TEXT_GAP + e.value.length * LEGEND_CHAR_WIDTH
|
|
580
|
-
);
|
|
581
|
-
const numRows = Math.ceil(group.entries.length / LEGEND_MAX_PER_ROW);
|
|
582
|
-
const colWidths: number[] = [];
|
|
583
|
-
for (let col = 0; col < LEGEND_MAX_PER_ROW; col++) {
|
|
584
|
-
let maxW = 0;
|
|
585
|
-
for (let r = 0; r < numRows; r++) {
|
|
586
|
-
const idx = r * LEGEND_MAX_PER_ROW + col;
|
|
587
|
-
if (idx < entryWidths.length && entryWidths[idx] > maxW) {
|
|
588
|
-
maxW = entryWidths[idx];
|
|
589
|
-
}
|
|
541
|
+
entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
590
542
|
}
|
|
591
|
-
if (maxW > 0) colWidths.push(maxW);
|
|
592
|
-
}
|
|
593
|
-
const colX: number[] = [LEGEND_PAD];
|
|
594
|
-
for (let c = 1; c < colWidths.length; c++) {
|
|
595
|
-
colX.push(colX[c - 1] + colWidths[c - 1] + LEGEND_ENTRY_GAP);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Entries: colored dot + value label
|
|
599
|
-
for (let i = 0; i < group.entries.length; i++) {
|
|
600
|
-
const entry = group.entries[i];
|
|
601
|
-
const row = Math.floor(i / LEGEND_MAX_PER_ROW);
|
|
602
|
-
const col = i % LEGEND_MAX_PER_ROW;
|
|
603
|
-
const entryX = colX[col];
|
|
604
|
-
|
|
605
|
-
const entryY =
|
|
606
|
-
LEGEND_HEADER_H + row * LEGEND_ENTRY_H + LEGEND_ENTRY_H / 2;
|
|
607
|
-
|
|
608
|
-
// Colored dot
|
|
609
|
-
gEl
|
|
610
|
-
.append('circle')
|
|
611
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
612
|
-
.attr('cy', entryY)
|
|
613
|
-
.attr('r', LEGEND_DOT_R)
|
|
614
|
-
.attr('fill', entry.color);
|
|
615
|
-
|
|
616
|
-
// Value label
|
|
617
|
-
gEl
|
|
618
|
-
.append('text')
|
|
619
|
-
.attr('x', entryX + LEGEND_DOT_R * 2 + LEGEND_DOT_TEXT_GAP)
|
|
620
|
-
.attr('y', entryY + LEGEND_FONT_SIZE / 2 - 2)
|
|
621
|
-
.attr('fill', palette.text)
|
|
622
|
-
.attr('font-size', LEGEND_FONT_SIZE)
|
|
623
|
-
.text(entry.value);
|
|
624
543
|
}
|
|
625
544
|
}
|
|
626
545
|
}
|
|
@@ -635,7 +554,7 @@ export function renderOrgForExport(
|
|
|
635
554
|
palette: PaletteColors
|
|
636
555
|
): string {
|
|
637
556
|
const parsed = parseOrg(content, palette);
|
|
638
|
-
if (parsed.error
|
|
557
|
+
if (parsed.error) return '';
|
|
639
558
|
|
|
640
559
|
// Extract hide option for export: cards sized without hidden attributes
|
|
641
560
|
const hideOption = parsed.options?.['hide'];
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -911,11 +911,13 @@ export function renderSequenceDiagram(
|
|
|
911
911
|
msgToLastStep.set(step.messageIndex, si);
|
|
912
912
|
});
|
|
913
913
|
|
|
914
|
-
// Map a note to the
|
|
915
|
-
// (the
|
|
914
|
+
// Map a note to the last render-step index of its preceding message
|
|
915
|
+
// (the return arrow if present, otherwise the call arrow).
|
|
916
|
+
// This ensures notes are positioned below the return arrow so they
|
|
917
|
+
// don't overlap it.
|
|
916
918
|
// If the note's closest preceding message is hidden (collapsed section), return -1
|
|
917
919
|
// so the note is hidden along with its section.
|
|
918
|
-
const
|
|
920
|
+
const findAssociatedLastStep = (note: SequenceNote): number => {
|
|
919
921
|
// First find the closest preceding message (ignoring hidden filter)
|
|
920
922
|
let closestMsgIndex = -1;
|
|
921
923
|
let closestLine = -1;
|
|
@@ -933,7 +935,7 @@ export function renderSequenceDiagram(
|
|
|
933
935
|
return -1;
|
|
934
936
|
}
|
|
935
937
|
if (closestMsgIndex < 0) return -1;
|
|
936
|
-
return
|
|
938
|
+
return msgToLastStep.get(closestMsgIndex) ?? -1;
|
|
937
939
|
};
|
|
938
940
|
|
|
939
941
|
// Find the first visible message index in an element subtree
|
|
@@ -1259,7 +1261,7 @@ export function renderSequenceDiagram(
|
|
|
1259
1261
|
for (let i = 0; i < els.length; i++) {
|
|
1260
1262
|
const el = els[i];
|
|
1261
1263
|
if (isSequenceNote(el)) {
|
|
1262
|
-
const si =
|
|
1264
|
+
const si = findAssociatedLastStep(el);
|
|
1263
1265
|
if (si < 0) continue;
|
|
1264
1266
|
// Check if there's a preceding note that we should stack below
|
|
1265
1267
|
const prevNote = i > 0 && isSequenceNote(els[i - 1]) ? (els[i - 1] as SequenceNote) : null;
|