@diagrammo/dgmo 0.2.20 → 0.2.22
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 +101 -99
- package/dist/index.cjs +983 -282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.js +976 -281
- 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/echarts.ts +7 -8
- 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 +566 -0
- package/src/kanban/types.ts +45 -0
- package/src/org/layout.ts +92 -66
- package/src/org/parser.ts +15 -1
- package/src/org/renderer.ts +94 -167
- package/src/sequence/renderer.ts +7 -5
package/src/org/layout.ts
CHANGED
|
@@ -52,10 +52,12 @@ export interface OrgContainerBounds {
|
|
|
52
52
|
export interface OrgLegendEntry {
|
|
53
53
|
value: string;
|
|
54
54
|
color: string;
|
|
55
|
+
isDefault?: boolean;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export interface OrgLegendGroup {
|
|
58
59
|
name: string;
|
|
60
|
+
alias?: string;
|
|
59
61
|
entries: OrgLegendEntry[];
|
|
60
62
|
x: number;
|
|
61
63
|
y: number;
|
|
@@ -97,18 +99,17 @@ const CONTAINER_META_LINE_HEIGHT = 16;
|
|
|
97
99
|
const STACK_V_GAP = 20;
|
|
98
100
|
|
|
99
101
|
|
|
100
|
-
// Legend
|
|
102
|
+
// Legend (kanban-style pills)
|
|
101
103
|
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;
|
|
104
|
+
const LEGEND_HEIGHT = 28;
|
|
105
|
+
const LEGEND_PILL_PAD = 16;
|
|
106
|
+
const LEGEND_PILL_FONT_W = 11 * 0.6;
|
|
107
|
+
const LEGEND_CAPSULE_PAD = 4;
|
|
108
|
+
const LEGEND_DOT_R = 4;
|
|
109
|
+
const LEGEND_ENTRY_FONT_W = 10 * 0.6;
|
|
110
|
+
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
111
|
+
const LEGEND_ENTRY_TRAIL = 8;
|
|
112
|
+
const LEGEND_GROUP_GAP = 12;
|
|
112
113
|
|
|
113
114
|
// ============================================================
|
|
114
115
|
// Helpers
|
|
@@ -271,49 +272,48 @@ function centerHeavyChildren(node: TreeNode): void {
|
|
|
271
272
|
// Layout
|
|
272
273
|
// ============================================================
|
|
273
274
|
|
|
274
|
-
function computeLegendGroups(tagGroups: OrgTagGroup[],
|
|
275
|
+
function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean): OrgLegendGroup[] {
|
|
275
276
|
const groups: OrgLegendGroup[] = [];
|
|
276
277
|
|
|
277
278
|
for (const group of tagGroups) {
|
|
278
279
|
if (group.entries.length === 0) continue;
|
|
279
280
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
281
|
+
// Pill label includes alias if present (e.g., "Rank (r)")
|
|
282
|
+
const pillLabel = group.alias ? `${group.name} (${group.alias})` : group.name;
|
|
283
|
+
const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
284
|
+
// Minified pill shows just the group name (no alias)
|
|
285
|
+
const minPillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
286
|
+
|
|
287
|
+
// Capsule: pad + pill + gap + entries + pad
|
|
288
|
+
const isDefaultValue = group.defaultValue?.toLowerCase();
|
|
289
|
+
let entriesWidth = 0;
|
|
290
|
+
for (const entry of group.entries) {
|
|
291
|
+
const entryLabel = isDefaultValue === entry.value.toLowerCase()
|
|
292
|
+
? `${entry.value} (default)`
|
|
293
|
+
: entry.value;
|
|
294
|
+
entriesWidth +=
|
|
295
|
+
LEGEND_DOT_R * 2 +
|
|
296
|
+
LEGEND_ENTRY_DOT_GAP +
|
|
297
|
+
entryLabel.length * LEGEND_ENTRY_FONT_W +
|
|
298
|
+
LEGEND_ENTRY_TRAIL;
|
|
297
299
|
}
|
|
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;
|
|
300
|
+
const capsuleWidth =
|
|
301
|
+
LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
|
|
307
302
|
|
|
308
303
|
groups.push({
|
|
309
304
|
name: group.name,
|
|
310
|
-
|
|
305
|
+
alias: group.alias,
|
|
306
|
+
entries: group.entries.map((e) => ({
|
|
307
|
+
value: e.value,
|
|
308
|
+
color: e.color,
|
|
309
|
+
isDefault: group.defaultValue?.toLowerCase() === e.value.toLowerCase() || undefined,
|
|
310
|
+
})),
|
|
311
311
|
x: 0,
|
|
312
312
|
y: 0,
|
|
313
|
-
width:
|
|
314
|
-
height:
|
|
315
|
-
minifiedWidth,
|
|
316
|
-
minifiedHeight:
|
|
313
|
+
width: capsuleWidth,
|
|
314
|
+
height: LEGEND_HEIGHT,
|
|
315
|
+
minifiedWidth: minPillWidth,
|
|
316
|
+
minifiedHeight: LEGEND_HEIGHT,
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -356,7 +356,30 @@ export function layoutOrg(
|
|
|
356
356
|
hiddenAttributes?: Set<string>
|
|
357
357
|
): OrgLayoutResult {
|
|
358
358
|
if (parsed.roots.length === 0) {
|
|
359
|
-
|
|
359
|
+
// Legend-only: compute and position legend groups even without nodes
|
|
360
|
+
const showEyeIcons = hiddenAttributes !== undefined;
|
|
361
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons);
|
|
362
|
+
if (legendGroups.length === 0) {
|
|
363
|
+
return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Legend-only mode: stack groups vertically, all expanded
|
|
367
|
+
let cy = MARGIN;
|
|
368
|
+
let maxWidth = 0;
|
|
369
|
+
for (const g of legendGroups) {
|
|
370
|
+
g.x = MARGIN;
|
|
371
|
+
g.y = cy;
|
|
372
|
+
cy += LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
373
|
+
if (g.width > maxWidth) maxWidth = g.width;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
nodes: [],
|
|
377
|
+
edges: [],
|
|
378
|
+
containers: [],
|
|
379
|
+
legend: legendGroups,
|
|
380
|
+
width: maxWidth + MARGIN * 2,
|
|
381
|
+
height: cy - LEGEND_GROUP_GAP + MARGIN,
|
|
382
|
+
};
|
|
360
383
|
}
|
|
361
384
|
|
|
362
385
|
// Inject default tag group values into node metadata for display.
|
|
@@ -1084,7 +1107,7 @@ export function layoutOrg(
|
|
|
1084
1107
|
let finalWidth = totalWidth;
|
|
1085
1108
|
let finalHeight = totalHeight;
|
|
1086
1109
|
|
|
1087
|
-
const legendPosition = parsed.options?.['legend-position'] ?? '
|
|
1110
|
+
const legendPosition = parsed.options?.['legend-position'] ?? 'top';
|
|
1088
1111
|
|
|
1089
1112
|
// When a tag group is active, only that group is laid out (full size).
|
|
1090
1113
|
// When none is active, all groups are laid out minified.
|
|
@@ -1101,7 +1124,7 @@ export function layoutOrg(
|
|
|
1101
1124
|
// Bottom: center legend groups horizontally below diagram content
|
|
1102
1125
|
const totalGroupsWidth =
|
|
1103
1126
|
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1104
|
-
(visibleGroups.length - 1) *
|
|
1127
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1105
1128
|
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1106
1129
|
|
|
1107
1130
|
if (neededWidth > totalWidth) {
|
|
@@ -1119,36 +1142,39 @@ export function layoutOrg(
|
|
|
1119
1142
|
const startX = (finalWidth - totalGroupsWidth) / 2;
|
|
1120
1143
|
|
|
1121
1144
|
let cx = startX;
|
|
1122
|
-
let maxH = 0;
|
|
1123
1145
|
for (const g of visibleGroups) {
|
|
1124
1146
|
g.x = cx;
|
|
1125
1147
|
g.y = legendY;
|
|
1126
|
-
cx += effectiveW(g) +
|
|
1127
|
-
const h = effectiveH(g);
|
|
1128
|
-
if (h > maxH) maxH = h;
|
|
1148
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1129
1149
|
}
|
|
1130
1150
|
|
|
1131
|
-
finalHeight = totalHeight + LEGEND_GAP +
|
|
1151
|
+
finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
|
|
1132
1152
|
} else {
|
|
1133
|
-
// Top
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
for (const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
legendY += effectiveH(g) + LEGEND_V_GAP;
|
|
1153
|
+
// Top: horizontal row above chart content, left-aligned
|
|
1154
|
+
const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
1155
|
+
|
|
1156
|
+
// Push all chart content down
|
|
1157
|
+
for (const n of layoutNodes) n.y += legendShift;
|
|
1158
|
+
for (const c of containers) c.y += legendShift;
|
|
1159
|
+
for (const e of layoutEdges) {
|
|
1160
|
+
for (const p of e.points) p.y += legendShift;
|
|
1142
1161
|
}
|
|
1143
1162
|
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1163
|
+
const totalGroupsWidth =
|
|
1164
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1165
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1166
|
+
|
|
1167
|
+
let cx = MARGIN;
|
|
1168
|
+
for (const g of visibleGroups) {
|
|
1169
|
+
g.x = cx;
|
|
1170
|
+
g.y = MARGIN;
|
|
1171
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1147
1172
|
}
|
|
1148
1173
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1174
|
+
finalHeight += legendShift;
|
|
1175
|
+
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1176
|
+
if (neededWidth > finalWidth) {
|
|
1177
|
+
finalWidth = neededWidth;
|
|
1152
1178
|
}
|
|
1153
1179
|
}
|
|
1154
1180
|
}
|
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,170 +445,109 @@ export function renderOrg(
|
|
|
457
445
|
|
|
458
446
|
}
|
|
459
447
|
|
|
460
|
-
// Render legend —
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
448
|
+
// Render legend — kanban-style pills.
|
|
449
|
+
// Skip in export mode (unless legend-only chart).
|
|
450
|
+
// Legend-only (no nodes): all groups rendered as expanded capsules.
|
|
451
|
+
// Active group: only that group rendered as capsule (pill + entries).
|
|
452
|
+
// No active group: all groups rendered as standalone pills.
|
|
453
|
+
const legendOnly = layout.nodes.length === 0;
|
|
454
|
+
if (!exportDims || legendOnly) for (const group of layout.legend) {
|
|
464
455
|
const isActive =
|
|
465
|
-
|
|
466
|
-
|
|
456
|
+
legendOnly ||
|
|
457
|
+
(activeTagGroup != null &&
|
|
458
|
+
group.name.toLowerCase() === activeTagGroup.toLowerCase());
|
|
467
459
|
|
|
468
|
-
// When a group is active, skip all other groups entirely
|
|
469
|
-
if (activeTagGroup != null && !isActive) continue;
|
|
460
|
+
// When a group is active, skip all other groups entirely (not in legend-only mode)
|
|
461
|
+
if (!legendOnly && activeTagGroup != null && !isActive) continue;
|
|
470
462
|
|
|
471
|
-
|
|
472
|
-
|
|
463
|
+
const groupBg = isDark
|
|
464
|
+
? mix(palette.surface, palette.bg, 50)
|
|
465
|
+
: mix(palette.surface, palette.bg, 30);
|
|
473
466
|
|
|
474
|
-
|
|
475
|
-
const
|
|
467
|
+
// Pill label: include alias when expanded (e.g., "Rank (r)")
|
|
468
|
+
const pillLabel = isActive && group.alias ? `${group.name} (${group.alias})` : group.name;
|
|
469
|
+
const pillWidth =
|
|
470
|
+
pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
476
471
|
|
|
477
472
|
const gEl = contentG
|
|
478
473
|
.append('g')
|
|
479
474
|
.attr('transform', `translate(${group.x}, ${group.y})`)
|
|
480
475
|
.attr('class', 'org-legend-group')
|
|
481
476
|
.attr('data-legend-group', group.name.toLowerCase())
|
|
482
|
-
.style('cursor', 'pointer');
|
|
477
|
+
.style('cursor', legendOnly ? 'default' : 'pointer');
|
|
483
478
|
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
.attr('fill', legendFill);
|
|
479
|
+
// Outer capsule background (active only)
|
|
480
|
+
if (isActive) {
|
|
481
|
+
gEl
|
|
482
|
+
.append('rect')
|
|
483
|
+
.attr('width', group.width)
|
|
484
|
+
.attr('height', LEGEND_HEIGHT)
|
|
485
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
486
|
+
.attr('fill', groupBg);
|
|
487
|
+
}
|
|
494
488
|
|
|
489
|
+
const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
490
|
+
const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
491
|
+
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
492
|
+
|
|
493
|
+
// Pill background
|
|
494
|
+
gEl
|
|
495
|
+
.append('rect')
|
|
496
|
+
.attr('x', pillX)
|
|
497
|
+
.attr('y', pillY)
|
|
498
|
+
.attr('width', pillWidth)
|
|
499
|
+
.attr('height', pillH)
|
|
500
|
+
.attr('rx', pillH / 2)
|
|
501
|
+
.attr('fill', isActive ? palette.bg : groupBg);
|
|
502
|
+
|
|
503
|
+
// Active pill border
|
|
495
504
|
if (isActive) {
|
|
496
|
-
|
|
497
|
-
.
|
|
498
|
-
.attr('
|
|
499
|
-
.attr('
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
.attr('
|
|
503
|
-
.attr('
|
|
504
|
-
.attr('stroke
|
|
505
|
+
gEl
|
|
506
|
+
.append('rect')
|
|
507
|
+
.attr('x', pillX)
|
|
508
|
+
.attr('y', pillY)
|
|
509
|
+
.attr('width', pillWidth)
|
|
510
|
+
.attr('height', pillH)
|
|
511
|
+
.attr('rx', pillH / 2)
|
|
512
|
+
.attr('fill', 'none')
|
|
513
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
514
|
+
.attr('stroke-width', 0.75);
|
|
505
515
|
}
|
|
506
516
|
|
|
507
|
-
//
|
|
517
|
+
// Pill text
|
|
508
518
|
gEl
|
|
509
519
|
.append('text')
|
|
510
|
-
.attr('x',
|
|
511
|
-
.attr('y',
|
|
512
|
-
.attr('
|
|
513
|
-
.attr('font-
|
|
514
|
-
.attr('
|
|
515
|
-
.text
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
520
|
+
.attr('x', pillX + pillWidth / 2)
|
|
521
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
522
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
523
|
+
.attr('font-weight', '500')
|
|
524
|
+
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
525
|
+
.attr('text-anchor', 'middle')
|
|
526
|
+
.text(pillLabel);
|
|
527
|
+
|
|
528
|
+
// Entries inside capsule (active only)
|
|
529
|
+
if (isActive) {
|
|
530
|
+
let entryX = pillX + pillWidth + 4;
|
|
531
|
+
for (const entry of group.entries) {
|
|
532
|
+
gEl
|
|
556
533
|
.append('circle')
|
|
557
|
-
.attr('cx',
|
|
558
|
-
.attr('cy',
|
|
559
|
-
.attr('r',
|
|
534
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
535
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
536
|
+
.attr('r', LEGEND_DOT_R)
|
|
537
|
+
.attr('fill', entry.color);
|
|
538
|
+
|
|
539
|
+
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
540
|
+
const entryLabel = entry.isDefault ? `${entry.value} (default)` : entry.value;
|
|
541
|
+
gEl
|
|
542
|
+
.append('text')
|
|
543
|
+
.attr('x', textX)
|
|
544
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
545
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
560
546
|
.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
|
-
}
|
|
547
|
+
.text(entryLabel);
|
|
575
548
|
|
|
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
|
-
}
|
|
549
|
+
entryX = textX + entryLabel.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
590
550
|
}
|
|
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
551
|
}
|
|
625
552
|
}
|
|
626
553
|
}
|
|
@@ -635,7 +562,7 @@ export function renderOrgForExport(
|
|
|
635
562
|
palette: PaletteColors
|
|
636
563
|
): string {
|
|
637
564
|
const parsed = parseOrg(content, palette);
|
|
638
|
-
if (parsed.error
|
|
565
|
+
if (parsed.error) return '';
|
|
639
566
|
|
|
640
567
|
// Extract hide option for export: cards sized without hidden attributes
|
|
641
568
|
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;
|