@diagrammo/dgmo 0.8.20 → 0.8.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/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +142 -90
- package/dist/editor.cjs +30 -4
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +30 -4
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +25 -3
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +25 -3
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +21201 -12886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +646 -89
- package/dist/index.d.ts +646 -89
- package/dist/index.js +21178 -12889
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/guide/registry.json +1 -0
- package/docs/language-reference.md +249 -4
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +360 -42
- package/src/boxes-and-lines/parser.ts +94 -11
- package/src/boxes-and-lines/renderer.ts +371 -114
- package/src/boxes-and-lines/types.ts +2 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/completion.ts +253 -0
- package/src/cycle/layout.ts +732 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +539 -0
- package/src/cycle/types.ts +77 -0
- package/src/d3.ts +240 -40
- package/src/dgmo-router.ts +15 -0
- package/src/echarts.ts +7 -4
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +26 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +5 -10
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +78 -0
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +30 -6
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1456 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +325 -63
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +373 -0
- package/src/mindmap/renderer.ts +544 -0
- package/src/mindmap/text-wrap.ts +217 -0
- package/src/mindmap/types.ts +55 -0
- package/src/org/parser.ts +2 -6
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +273 -56
- package/src/sharing.ts +3 -0
- package/src/sitemap/layout.ts +56 -18
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1058 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +5 -3
- package/src/utils/parsing.ts +48 -7
- package/src/utils/tag-groups.ts +46 -60
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- package/src/wireframe/types.ts +110 -0
package/src/sequence/renderer.ts
CHANGED
|
@@ -28,7 +28,11 @@ import type { ResolvedTagMap } from './tag-resolution';
|
|
|
28
28
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
29
29
|
import { LEGEND_HEIGHT } from '../utils/legend-constants';
|
|
30
30
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
LegendCallbacks,
|
|
33
|
+
LegendConfig,
|
|
34
|
+
LegendState,
|
|
35
|
+
} from '../utils/legend-types';
|
|
32
36
|
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
|
|
33
37
|
|
|
34
38
|
// ============================================================
|
|
@@ -60,6 +64,11 @@ const NOTE_CHARS_PER_LINE = Math.floor(
|
|
|
60
64
|
);
|
|
61
65
|
const COLLAPSED_NOTE_H = 20;
|
|
62
66
|
const COLLAPSED_NOTE_W = 40;
|
|
67
|
+
const ACTIVATION_WIDTH = 10;
|
|
68
|
+
const SELF_CALL_HEIGHT = 25;
|
|
69
|
+
const SELF_CALL_WIDTH = 30;
|
|
70
|
+
// Max note width that keeps a note within one participant lane
|
|
71
|
+
const NOTE_LANE_MAX = PARTICIPANT_GAP - ACTIVATION_WIDTH - NOTE_GAP; // 135px
|
|
63
72
|
|
|
64
73
|
function wrapTextLines(text: string, maxChars: number): string[] {
|
|
65
74
|
const rawLines = text.split('\n');
|
|
@@ -68,14 +77,26 @@ function wrapTextLines(text: string, maxChars: number): string[] {
|
|
|
68
77
|
if (line.length <= maxChars) {
|
|
69
78
|
wrapped.push(line);
|
|
70
79
|
} else {
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
// Preserve bullet prefix: keep "- " glued to the first content word
|
|
81
|
+
// so wrapping never produces a bare "-" line.
|
|
82
|
+
const bulletPrefix = line.startsWith('- ') ? '- ' : '';
|
|
83
|
+
const content = bulletPrefix ? line.slice(2) : line;
|
|
84
|
+
const words = content.split(' ');
|
|
85
|
+
let current = bulletPrefix;
|
|
73
86
|
for (const word of words) {
|
|
74
|
-
|
|
87
|
+
const candidate = current ? current + ' ' + word : word;
|
|
88
|
+
if (
|
|
89
|
+
current &&
|
|
90
|
+
current !== bulletPrefix &&
|
|
91
|
+
candidate.length > maxChars
|
|
92
|
+
) {
|
|
75
93
|
wrapped.push(current);
|
|
76
94
|
current = word;
|
|
77
95
|
} else {
|
|
78
|
-
current =
|
|
96
|
+
current =
|
|
97
|
+
current && current !== bulletPrefix
|
|
98
|
+
? current + ' ' + word
|
|
99
|
+
: current + word;
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
if (current) wrapped.push(current);
|
|
@@ -539,6 +560,10 @@ export interface SequenceRenderOptions {
|
|
|
539
560
|
expandedNoteLines?: Set<number>; // keyed by note lineNumber; undefined = all expanded (CLI default)
|
|
540
561
|
exportWidth?: number; // Explicit width for CLI/export rendering (bypasses getBoundingClientRect)
|
|
541
562
|
activeTagGroup?: string | null; // Active tag group name for tag-driven recoloring; null = explicitly none
|
|
563
|
+
expandAllNotes?: boolean; // Whether the "Expand Notes" toggle is active
|
|
564
|
+
onExpandAllNotes?: (expand: boolean) => void; // Toggle all notes expanded/collapsed
|
|
565
|
+
controlsExpanded?: boolean; // Controls group expanded state (managed by React)
|
|
566
|
+
onToggleControlsExpand?: () => void; // Callback to toggle controls group
|
|
542
567
|
}
|
|
543
568
|
|
|
544
569
|
/**
|
|
@@ -953,6 +978,37 @@ export function renderSequenceDiagram(
|
|
|
953
978
|
);
|
|
954
979
|
if (participants.length === 0) return;
|
|
955
980
|
|
|
981
|
+
// Participant index lookup — used to clamp note width within one lane
|
|
982
|
+
const participantIndexMap = new Map<string, number>();
|
|
983
|
+
participants.forEach((p, i) => participantIndexMap.set(p.id, i));
|
|
984
|
+
|
|
985
|
+
// Extra X shift for notes after self-calls
|
|
986
|
+
const SELF_CALL_NOTE_X_SHIFT =
|
|
987
|
+
ACTIVATION_WIDTH / 2 +
|
|
988
|
+
SELF_CALL_WIDTH +
|
|
989
|
+
NOTE_GAP -
|
|
990
|
+
(ACTIVATION_WIDTH + NOTE_GAP); // 25px
|
|
991
|
+
|
|
992
|
+
const noteEffectiveMaxW = (
|
|
993
|
+
participantId: string,
|
|
994
|
+
position: 'right' | 'left',
|
|
995
|
+
afterSelfCall = false
|
|
996
|
+
): number => {
|
|
997
|
+
const idx = participantIndexMap.get(participantId);
|
|
998
|
+
if (idx === undefined) return NOTE_MAX_W;
|
|
999
|
+
const hasNeighbor =
|
|
1000
|
+
position === 'right' ? idx < participants.length - 1 : idx > 0;
|
|
1001
|
+
if (!hasNeighbor) return NOTE_MAX_W;
|
|
1002
|
+
const laneMax =
|
|
1003
|
+
afterSelfCall && position === 'right'
|
|
1004
|
+
? NOTE_LANE_MAX - SELF_CALL_NOTE_X_SHIFT
|
|
1005
|
+
: NOTE_LANE_MAX;
|
|
1006
|
+
return Math.min(NOTE_MAX_W, laneMax);
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const charsForWidth = (maxW: number): number =>
|
|
1010
|
+
Math.floor((maxW - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
|
|
1011
|
+
|
|
956
1012
|
const activationsOff = parsedOptions.activations?.toLowerCase() === 'off';
|
|
957
1013
|
|
|
958
1014
|
// Tag resolution — shared utility handles priority chain:
|
|
@@ -1049,7 +1105,39 @@ export function renderSequenceDiagram(
|
|
|
1049
1105
|
return msgToLastStep.get(closestMsgIndex) ?? -1;
|
|
1050
1106
|
};
|
|
1051
1107
|
|
|
1052
|
-
//
|
|
1108
|
+
// Check whether a note's preceding message is a self-call.
|
|
1109
|
+
// Self-call loopback arrows extend SELF_CALL_HEIGHT below the step Y,
|
|
1110
|
+
// so notes after self-calls need a larger vertical offset.
|
|
1111
|
+
const isNoteAfterSelfCall = (note: SequenceNote): boolean => {
|
|
1112
|
+
let closestMsgIndex = -1;
|
|
1113
|
+
let closestLine = -1;
|
|
1114
|
+
for (let mi = 0; mi < messages.length; mi++) {
|
|
1115
|
+
if (
|
|
1116
|
+
messages[mi].lineNumber < note.lineNumber &&
|
|
1117
|
+
messages[mi].lineNumber > closestLine
|
|
1118
|
+
) {
|
|
1119
|
+
closestLine = messages[mi].lineNumber;
|
|
1120
|
+
closestMsgIndex = mi;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (closestMsgIndex < 0) return false;
|
|
1124
|
+
const msg = messages[closestMsgIndex];
|
|
1125
|
+
return msg.from === msg.to;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
// Extra gap below self-call loop before note starts
|
|
1129
|
+
const SELF_CALL_NOTE_GAP = 8;
|
|
1130
|
+
const noteOffsetBelow = (note: SequenceNote): number =>
|
|
1131
|
+
isNoteAfterSelfCall(note)
|
|
1132
|
+
? SELF_CALL_HEIGHT + NOTE_OFFSET_BELOW + SELF_CALL_NOTE_GAP
|
|
1133
|
+
: NOTE_OFFSET_BELOW;
|
|
1134
|
+
|
|
1135
|
+
// Find the first visible message index in an element subtree.
|
|
1136
|
+
// Use lineNumber lookup instead of indexOf — collapse projection creates
|
|
1137
|
+
// separate spread copies for messages[] and elements[], breaking reference equality.
|
|
1138
|
+
const msgLineToIdx = new Map<number, number>();
|
|
1139
|
+
messages.forEach((m, i) => msgLineToIdx.set(m.lineNumber, i));
|
|
1140
|
+
|
|
1053
1141
|
const findFirstMsgIndex = (els: SequenceElement[]): number => {
|
|
1054
1142
|
for (const el of els) {
|
|
1055
1143
|
if (isSequenceBlock(el)) {
|
|
@@ -1064,7 +1152,7 @@ export function renderSequenceDiagram(
|
|
|
1064
1152
|
const elseIdx = findFirstMsgIndex(el.elseChildren);
|
|
1065
1153
|
if (elseIdx >= 0) return elseIdx;
|
|
1066
1154
|
} else if (!isSequenceSection(el) && !isSequenceNote(el)) {
|
|
1067
|
-
const idx =
|
|
1155
|
+
const idx = msgLineToIdx.get(el.lineNumber) ?? -1;
|
|
1068
1156
|
if (idx >= 0 && !hiddenMsgIndices.has(idx)) return idx;
|
|
1069
1157
|
}
|
|
1070
1158
|
}
|
|
@@ -1121,8 +1209,11 @@ export function renderSequenceDiagram(
|
|
|
1121
1209
|
// When notes share horizontal space with subsequent arrows, generous vertical clearance
|
|
1122
1210
|
// is needed so note boxes don't visually cover message labels.
|
|
1123
1211
|
const NOTE_TRAILING_GAP = 35;
|
|
1124
|
-
const computeNoteHeight = (
|
|
1125
|
-
|
|
1212
|
+
const computeNoteHeight = (
|
|
1213
|
+
text: string,
|
|
1214
|
+
maxChars: number = NOTE_CHARS_PER_LINE
|
|
1215
|
+
): number => {
|
|
1216
|
+
const lines = wrapTextLines(text, maxChars);
|
|
1126
1217
|
return lines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
|
|
1127
1218
|
};
|
|
1128
1219
|
let trailingNoteSpace = 0; // extra space for notes at the end with no following message
|
|
@@ -1131,15 +1222,18 @@ export function renderSequenceDiagram(
|
|
|
1131
1222
|
const el = els[i];
|
|
1132
1223
|
if (isSequenceNote(el)) {
|
|
1133
1224
|
// Total vertical extent of notes from the message arrow:
|
|
1134
|
-
//
|
|
1225
|
+
// offset (gap above first note — larger after self-calls)
|
|
1135
1226
|
// + each note's height + NOTE_OFFSET_BELOW (inter-note gap)
|
|
1136
1227
|
// + NOTE_TRAILING_GAP (gap below last note — clears next message label)
|
|
1137
|
-
|
|
1228
|
+
const firstOffset = noteOffsetBelow(el as SequenceNote);
|
|
1229
|
+
let totalExtent = firstOffset;
|
|
1138
1230
|
let j = i;
|
|
1139
1231
|
while (j < els.length && isSequenceNote(els[j])) {
|
|
1140
1232
|
const note = els[j] as SequenceNote;
|
|
1233
|
+
const sc = isNoteAfterSelfCall(note);
|
|
1234
|
+
const maxW = noteEffectiveMaxW(note.participantId, note.position, sc);
|
|
1141
1235
|
const noteH = isNoteExpanded(note)
|
|
1142
|
-
? computeNoteHeight(note.text)
|
|
1236
|
+
? computeNoteHeight(note.text, charsForWidth(maxW))
|
|
1143
1237
|
: COLLAPSED_NOTE_H;
|
|
1144
1238
|
totalExtent += noteH + NOTE_OFFSET_BELOW;
|
|
1145
1239
|
j++;
|
|
@@ -1401,13 +1495,18 @@ export function renderSequenceDiagram(
|
|
|
1401
1495
|
let noteTopY: number;
|
|
1402
1496
|
if (prevNoteY !== undefined && prevNote) {
|
|
1403
1497
|
// Stack below previous note
|
|
1498
|
+
const prevMaxW = noteEffectiveMaxW(
|
|
1499
|
+
prevNote.participantId,
|
|
1500
|
+
prevNote.position,
|
|
1501
|
+
isNoteAfterSelfCall(prevNote)
|
|
1502
|
+
);
|
|
1404
1503
|
const prevNoteH = isNoteExpanded(prevNote)
|
|
1405
|
-
? computeNoteHeight(prevNote.text)
|
|
1504
|
+
? computeNoteHeight(prevNote.text, charsForWidth(prevMaxW))
|
|
1406
1505
|
: COLLAPSED_NOTE_H;
|
|
1407
1506
|
noteTopY = prevNoteY + prevNoteH + NOTE_OFFSET_BELOW;
|
|
1408
1507
|
} else {
|
|
1409
|
-
// First note after a message
|
|
1410
|
-
noteTopY = stepY(si) +
|
|
1508
|
+
// First note after a message — use larger offset after self-calls
|
|
1509
|
+
noteTopY = stepY(si) + noteOffsetBelow(el);
|
|
1411
1510
|
}
|
|
1412
1511
|
noteYMap.set(el, noteTopY);
|
|
1413
1512
|
} else if (isSequenceBlock(el)) {
|
|
@@ -1435,8 +1534,13 @@ export function renderSequenceDiagram(
|
|
|
1435
1534
|
)
|
|
1436
1535
|
: layoutEndY;
|
|
1437
1536
|
for (const [note, noteTopY] of noteYMap) {
|
|
1537
|
+
const maxW = noteEffectiveMaxW(
|
|
1538
|
+
note.participantId,
|
|
1539
|
+
note.position,
|
|
1540
|
+
isNoteAfterSelfCall(note)
|
|
1541
|
+
);
|
|
1438
1542
|
const noteH = isNoteExpanded(note)
|
|
1439
|
-
? computeNoteHeight(note.text)
|
|
1543
|
+
? computeNoteHeight(note.text, charsForWidth(maxW))
|
|
1440
1544
|
: COLLAPSED_NOTE_H;
|
|
1441
1545
|
contentBottomY = Math.max(
|
|
1442
1546
|
contentBottomY,
|
|
@@ -1632,39 +1736,33 @@ export function renderSequenceDiagram(
|
|
|
1632
1736
|
}
|
|
1633
1737
|
}
|
|
1634
1738
|
|
|
1635
|
-
//
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
palette,
|
|
1663
|
-
isDark,
|
|
1664
|
-
undefined,
|
|
1665
|
-
svgWidth
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1739
|
+
// Collect all note line numbers (for controls group visibility + "all expanded" check)
|
|
1740
|
+
const allNoteLineNumbers: number[] = [];
|
|
1741
|
+
const collectNoteLines = (els: SequenceElement[]): void => {
|
|
1742
|
+
for (const el of els) {
|
|
1743
|
+
if (isSequenceNote(el)) {
|
|
1744
|
+
allNoteLineNumbers.push(el.lineNumber);
|
|
1745
|
+
} else if (isSequenceBlock(el)) {
|
|
1746
|
+
collectNoteLines(el.children);
|
|
1747
|
+
if ('elseChildren' in el) collectNoteLines(el.elseChildren);
|
|
1748
|
+
if ('branches' in el && Array.isArray(el.branches)) {
|
|
1749
|
+
for (const branch of el.branches) {
|
|
1750
|
+
collectNoteLines(branch.children);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
collectNoteLines(elements);
|
|
1757
|
+
|
|
1758
|
+
// Show controls group only in interactive mode (expandedNoteLines defined)
|
|
1759
|
+
// when notes exist and collapse-notes is not disabled
|
|
1760
|
+
const showNotesControl =
|
|
1761
|
+
allNoteLineNumbers.length > 0 &&
|
|
1762
|
+
!collapseNotesDisabled &&
|
|
1763
|
+
expandedNoteLines !== undefined;
|
|
1764
|
+
|
|
1765
|
+
const hasTagGroups = parsed.tagGroups.length > 0;
|
|
1668
1766
|
|
|
1669
1767
|
// Build set of collapsed group names for drill-bar rendering
|
|
1670
1768
|
const collapsedGroupNames = new Set<string>();
|
|
@@ -2093,7 +2191,6 @@ export function renderSequenceDiagram(
|
|
|
2093
2191
|
}
|
|
2094
2192
|
|
|
2095
2193
|
// Render activation rectangles (behind arrows)
|
|
2096
|
-
const ACTIVATION_WIDTH = 10;
|
|
2097
2194
|
const ACTIVATION_NEST_OFFSET = 6;
|
|
2098
2195
|
activations.forEach((act) => {
|
|
2099
2196
|
const px = participantX.get(act.participantId);
|
|
@@ -2285,8 +2382,7 @@ export function renderSequenceDiagram(
|
|
|
2285
2382
|
}
|
|
2286
2383
|
|
|
2287
2384
|
// Render steps (calls and returns in stack-inferred order)
|
|
2288
|
-
|
|
2289
|
-
const SELF_CALL_HEIGHT = 25;
|
|
2385
|
+
// SELF_CALL_WIDTH is now a module-level constant
|
|
2290
2386
|
renderSteps.forEach((step, i) => {
|
|
2291
2387
|
const fromX = participantX.get(step.from);
|
|
2292
2388
|
const toX = participantX.get(step.to);
|
|
@@ -2354,6 +2450,10 @@ export function renderSequenceDiagram(
|
|
|
2354
2450
|
.attr('y', y + SELF_CALL_HEIGHT / 2 + 4)
|
|
2355
2451
|
.attr('text-anchor', 'start')
|
|
2356
2452
|
.attr('fill', arrowColor)
|
|
2453
|
+
.attr('paint-order', 'stroke fill')
|
|
2454
|
+
.attr('stroke', palette.bg)
|
|
2455
|
+
.attr('stroke-width', 4)
|
|
2456
|
+
.attr('stroke-linejoin', 'round')
|
|
2357
2457
|
.attr('font-size', 12)
|
|
2358
2458
|
.attr('class', 'message-label')
|
|
2359
2459
|
.attr(
|
|
@@ -2424,6 +2524,10 @@ export function renderSequenceDiagram(
|
|
|
2424
2524
|
.attr('y', y - 8)
|
|
2425
2525
|
.attr('text-anchor', 'middle')
|
|
2426
2526
|
.attr('fill', arrowColor)
|
|
2527
|
+
.attr('paint-order', 'stroke fill')
|
|
2528
|
+
.attr('stroke', palette.bg)
|
|
2529
|
+
.attr('stroke-width', 4)
|
|
2530
|
+
.attr('stroke-linejoin', 'round')
|
|
2427
2531
|
.attr('font-size', 12)
|
|
2428
2532
|
.attr('class', 'message-label')
|
|
2429
2533
|
.attr(
|
|
@@ -2498,6 +2602,10 @@ export function renderSequenceDiagram(
|
|
|
2498
2602
|
.attr('y', y - 6)
|
|
2499
2603
|
.attr('text-anchor', 'middle')
|
|
2500
2604
|
.attr('fill', returnColor)
|
|
2605
|
+
.attr('paint-order', 'stroke fill')
|
|
2606
|
+
.attr('stroke', palette.bg)
|
|
2607
|
+
.attr('stroke-width', 4)
|
|
2608
|
+
.attr('stroke-linejoin', 'round')
|
|
2501
2609
|
.attr('font-size', 11)
|
|
2502
2610
|
.attr('class', 'message-label')
|
|
2503
2611
|
.attr(
|
|
@@ -2539,15 +2647,27 @@ export function renderSequenceDiagram(
|
|
|
2539
2647
|
|
|
2540
2648
|
if (expanded) {
|
|
2541
2649
|
// --- Expanded note: full folded-corner box with wrapped text ---
|
|
2542
|
-
const
|
|
2650
|
+
const afterSelfCall = isNoteAfterSelfCall(el);
|
|
2651
|
+
const maxW = noteEffectiveMaxW(
|
|
2652
|
+
el.participantId,
|
|
2653
|
+
el.position,
|
|
2654
|
+
afterSelfCall
|
|
2655
|
+
);
|
|
2656
|
+
const maxChars = charsForWidth(maxW);
|
|
2657
|
+
const wrappedLines = wrapTextLines(el.text, maxChars);
|
|
2543
2658
|
const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
|
|
2544
2659
|
const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
|
|
2545
2660
|
const noteW = Math.min(
|
|
2546
|
-
|
|
2661
|
+
maxW,
|
|
2547
2662
|
Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
|
|
2548
2663
|
);
|
|
2664
|
+
// Shift notes past self-call loopback when applicable
|
|
2665
|
+
const rightOffset =
|
|
2666
|
+
afterSelfCall && isRight
|
|
2667
|
+
? ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH + NOTE_GAP
|
|
2668
|
+
: ACTIVATION_WIDTH + NOTE_GAP;
|
|
2549
2669
|
const noteX = isRight
|
|
2550
|
-
? px +
|
|
2670
|
+
? px + rightOffset
|
|
2551
2671
|
: px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
|
|
2552
2672
|
|
|
2553
2673
|
const noteG = svg
|
|
@@ -2621,8 +2741,13 @@ export function renderSequenceDiagram(
|
|
|
2621
2741
|
} else {
|
|
2622
2742
|
// --- Collapsed note: compact indicator ---
|
|
2623
2743
|
const cFold = 6;
|
|
2744
|
+
const afterSelfCallC = isNoteAfterSelfCall(el);
|
|
2745
|
+
const rightOffsetC =
|
|
2746
|
+
afterSelfCallC && isRight
|
|
2747
|
+
? ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH + NOTE_GAP
|
|
2748
|
+
: ACTIVATION_WIDTH + NOTE_GAP;
|
|
2624
2749
|
const noteX = isRight
|
|
2625
|
-
? px +
|
|
2750
|
+
? px + rightOffsetC
|
|
2626
2751
|
: px - ACTIVATION_WIDTH - NOTE_GAP - COLLAPSED_NOTE_W;
|
|
2627
2752
|
|
|
2628
2753
|
const noteG = svg
|
|
@@ -2694,6 +2819,73 @@ export function renderSequenceDiagram(
|
|
|
2694
2819
|
if (elements && elements.length > 0) {
|
|
2695
2820
|
renderNoteElements(elements);
|
|
2696
2821
|
}
|
|
2822
|
+
|
|
2823
|
+
// Render legend LAST so it sits on top of all other SVG elements
|
|
2824
|
+
// (group boxes, lifelines, participants, etc.) and can receive clicks.
|
|
2825
|
+
if (hasTagGroups || showNotesControl) {
|
|
2826
|
+
const controlsExpanded = options?.controlsExpanded ?? false;
|
|
2827
|
+
|
|
2828
|
+
const legendY = TOP_MARGIN + titleOffset;
|
|
2829
|
+
const resolvedGroups = parsed.tagGroups
|
|
2830
|
+
.filter((tg) => tg.entries.length > 0)
|
|
2831
|
+
.map((tg) => ({
|
|
2832
|
+
name: tg.name,
|
|
2833
|
+
entries: tg.entries.map((e) => ({
|
|
2834
|
+
value: e.value,
|
|
2835
|
+
color: e.color,
|
|
2836
|
+
})),
|
|
2837
|
+
}));
|
|
2838
|
+
|
|
2839
|
+
const allExpanded = showNotesControl && (options?.expandAllNotes ?? false);
|
|
2840
|
+
|
|
2841
|
+
const controlsGroup = showNotesControl
|
|
2842
|
+
? {
|
|
2843
|
+
toggles: [
|
|
2844
|
+
{
|
|
2845
|
+
id: 'expand-all-notes',
|
|
2846
|
+
type: 'toggle' as const,
|
|
2847
|
+
label: 'Expand Notes',
|
|
2848
|
+
active: allExpanded,
|
|
2849
|
+
onToggle: () => {},
|
|
2850
|
+
},
|
|
2851
|
+
],
|
|
2852
|
+
}
|
|
2853
|
+
: undefined;
|
|
2854
|
+
|
|
2855
|
+
const legendConfig: LegendConfig = {
|
|
2856
|
+
groups: resolvedGroups,
|
|
2857
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
2858
|
+
mode: 'fixed',
|
|
2859
|
+
controlsGroup,
|
|
2860
|
+
};
|
|
2861
|
+
const legendState: LegendState = {
|
|
2862
|
+
activeGroup: activeTagGroup ?? null,
|
|
2863
|
+
controlsExpanded,
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
const legendCallbacks: LegendCallbacks = {
|
|
2867
|
+
onControlsExpand: () => {
|
|
2868
|
+
options?.onToggleControlsExpand?.();
|
|
2869
|
+
},
|
|
2870
|
+
onControlsToggle: (_toggleId: string, active: boolean) => {
|
|
2871
|
+
options?.onExpandAllNotes?.(active);
|
|
2872
|
+
},
|
|
2873
|
+
};
|
|
2874
|
+
|
|
2875
|
+
const legendG = svg
|
|
2876
|
+
.append('g')
|
|
2877
|
+
.attr('class', 'sequence-legend')
|
|
2878
|
+
.attr('transform', `translate(0,${legendY})`);
|
|
2879
|
+
renderLegendD3(
|
|
2880
|
+
legendG,
|
|
2881
|
+
legendConfig,
|
|
2882
|
+
legendState,
|
|
2883
|
+
palette,
|
|
2884
|
+
isDark,
|
|
2885
|
+
legendCallbacks,
|
|
2886
|
+
svgWidth
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2697
2889
|
}
|
|
2698
2890
|
|
|
2699
2891
|
/**
|
|
@@ -2732,6 +2924,31 @@ export function buildNoteMessageMap(
|
|
|
2732
2924
|
return map;
|
|
2733
2925
|
}
|
|
2734
2926
|
|
|
2927
|
+
/**
|
|
2928
|
+
* Collect all note line numbers from a sequence diagram's elements.
|
|
2929
|
+
* Used by the app to compute the "expand all" set.
|
|
2930
|
+
*/
|
|
2931
|
+
export function collectNoteLineNumbers(elements: SequenceElement[]): number[] {
|
|
2932
|
+
const result: number[] = [];
|
|
2933
|
+
const walk = (els: SequenceElement[]): void => {
|
|
2934
|
+
for (const el of els) {
|
|
2935
|
+
if (isSequenceNote(el)) {
|
|
2936
|
+
result.push(el.lineNumber);
|
|
2937
|
+
} else if (isSequenceBlock(el)) {
|
|
2938
|
+
walk(el.children);
|
|
2939
|
+
if (el.elseIfBranches) {
|
|
2940
|
+
for (const branch of el.elseIfBranches) {
|
|
2941
|
+
walk(branch.children);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
walk(el.elseChildren);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
walk(elements);
|
|
2949
|
+
return result;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2735
2952
|
function renderParticipant(
|
|
2736
2953
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2737
2954
|
participant: SequenceParticipant,
|
package/src/sharing.ts
CHANGED
|
@@ -30,6 +30,9 @@ export interface CompactViewState {
|
|
|
30
30
|
rps?: number; // RPS multiplier (infra)
|
|
31
31
|
spd?: number; // playback speed (infra)
|
|
32
32
|
io?: Record<string, number>; // instance overrides (infra)
|
|
33
|
+
hd?: boolean; // hide descriptions (mindmap)
|
|
34
|
+
cbd?: boolean; // color by depth (mindmap)
|
|
35
|
+
rq?: string; // radar quadrant focus (tech-radar position)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface DecodedDiagramUrl {
|
package/src/sitemap/layout.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface SitemapLayoutNode {
|
|
|
22
22
|
metadata: Record<string, string>;
|
|
23
23
|
/** Original (unfiltered) metadata for tag-based coloring and hover dimming */
|
|
24
24
|
tagMetadata: Record<string, string>;
|
|
25
|
+
description?: string[];
|
|
25
26
|
isContainer: boolean;
|
|
26
27
|
lineNumber: number;
|
|
27
28
|
color?: string;
|
|
@@ -161,23 +162,36 @@ function filterMetadata(
|
|
|
161
162
|
return filtered;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
function computeCardWidth(
|
|
165
|
+
function computeCardWidth(
|
|
166
|
+
label: string,
|
|
167
|
+
meta: Record<string, string>,
|
|
168
|
+
descLines?: string[]
|
|
169
|
+
): number {
|
|
165
170
|
let maxChars = label.length;
|
|
166
171
|
for (const [key, value] of Object.entries(meta)) {
|
|
167
172
|
const lineChars = key.length + 2 + value.length;
|
|
168
173
|
if (lineChars > maxChars) maxChars = lineChars;
|
|
169
174
|
}
|
|
175
|
+
if (descLines) {
|
|
176
|
+
for (const dl of descLines) {
|
|
177
|
+
if (dl.length > maxChars) maxChars = dl.length;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
170
180
|
return Math.max(
|
|
171
181
|
MIN_CARD_WIDTH,
|
|
172
182
|
Math.ceil(maxChars * CHAR_WIDTH) + CARD_H_PAD * 2
|
|
173
183
|
);
|
|
174
184
|
}
|
|
175
185
|
|
|
176
|
-
function computeCardHeight(
|
|
186
|
+
function computeCardHeight(
|
|
187
|
+
meta: Record<string, string>,
|
|
188
|
+
descLineCount = 0
|
|
189
|
+
): number {
|
|
177
190
|
const metaCount = Object.keys(meta).length;
|
|
178
|
-
|
|
191
|
+
const contentCount = metaCount + descLineCount;
|
|
192
|
+
if (contentCount === 0) return HEADER_HEIGHT + CARD_V_PAD;
|
|
179
193
|
return (
|
|
180
|
-
HEADER_HEIGHT + SEPARATOR_GAP +
|
|
194
|
+
HEADER_HEIGHT + SEPARATOR_GAP + contentCount * META_LINE_HEIGHT + CARD_V_PAD
|
|
181
195
|
);
|
|
182
196
|
}
|
|
183
197
|
|
|
@@ -307,8 +321,8 @@ function flattenNodes(
|
|
|
307
321
|
parentPageId,
|
|
308
322
|
meta,
|
|
309
323
|
fullMeta: { ...node.metadata },
|
|
310
|
-
width: computeCardWidth(node.label, meta),
|
|
311
|
-
height: computeCardHeight(meta),
|
|
324
|
+
width: computeCardWidth(node.label, meta, node.description),
|
|
325
|
+
height: computeCardHeight(meta, node.description?.length ?? 0),
|
|
312
326
|
});
|
|
313
327
|
// Pages can have children too (nested pages) — this page becomes the parentPageId
|
|
314
328
|
if (node.children.length > 0) {
|
|
@@ -396,15 +410,15 @@ export function layoutSitemap(
|
|
|
396
410
|
const pageNodeIds = new Set<string>();
|
|
397
411
|
const collapsedContainerIds = new Set<string>();
|
|
398
412
|
|
|
399
|
-
// Identify containers vs pages, and detect collapsed
|
|
413
|
+
// Identify containers vs pages, and detect collapsed containers.
|
|
414
|
+
// A container is collapsed iff hiddenCounts records it with a positive count
|
|
415
|
+
// (meaning collapseSitemapTree pruned its descendants). Source-level empty
|
|
416
|
+
// containers (never had children) are NOT treated as collapsed.
|
|
400
417
|
for (const flat of flatNodes) {
|
|
401
418
|
if (flat.sitemapNode.isContainer) {
|
|
402
419
|
containerIds.add(flat.sitemapNode.id);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
(f) => f.parentContainerId === flat.sitemapNode.id
|
|
406
|
-
);
|
|
407
|
-
if (!hasAnyChild) {
|
|
420
|
+
const hidden = hiddenCounts?.get(flat.sitemapNode.id) ?? 0;
|
|
421
|
+
if (hidden > 0) {
|
|
408
422
|
collapsedContainerIds.add(flat.sitemapNode.id);
|
|
409
423
|
}
|
|
410
424
|
} else {
|
|
@@ -412,16 +426,31 @@ export function layoutSitemap(
|
|
|
412
426
|
}
|
|
413
427
|
}
|
|
414
428
|
|
|
429
|
+
// Sibling-page floor for collapsed containers — prevents collapsed containers
|
|
430
|
+
// from rendering smaller than meta-rich page cards in the same layout.
|
|
431
|
+
let pageMaxW = 0;
|
|
432
|
+
let pageMaxH = 0;
|
|
433
|
+
for (const f of flatNodes) {
|
|
434
|
+
if (!f.sitemapNode.isContainer) {
|
|
435
|
+
if (f.width > pageMaxW) pageMaxW = f.width;
|
|
436
|
+
if (f.height > pageMaxH) pageMaxH = f.height;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
415
440
|
// Add nodes to dagre
|
|
416
441
|
for (const flat of flatNodes) {
|
|
417
442
|
const node = flat.sitemapNode;
|
|
418
443
|
if (node.isContainer) {
|
|
419
444
|
if (collapsedContainerIds.has(node.id)) {
|
|
420
|
-
// Collapsed container — regular node with explicit dimensions
|
|
445
|
+
// Collapsed container — regular node with explicit dimensions.
|
|
446
|
+
// Floor to max page-card dims so collapsed containers never look
|
|
447
|
+
// smaller than sibling page cards.
|
|
448
|
+
const flooredW = Math.max(flat.width, pageMaxW);
|
|
449
|
+
const flooredH = Math.max(flat.height, pageMaxH);
|
|
421
450
|
g.setNode(node.id, {
|
|
422
451
|
label: node.label,
|
|
423
|
-
width:
|
|
424
|
-
height:
|
|
452
|
+
width: flooredW,
|
|
453
|
+
height: flooredH,
|
|
425
454
|
});
|
|
426
455
|
} else {
|
|
427
456
|
// Regular container — compound node with padding for child layout
|
|
@@ -504,6 +533,7 @@ export function layoutSitemap(
|
|
|
504
533
|
label: node.label,
|
|
505
534
|
metadata: flat.meta,
|
|
506
535
|
tagMetadata: flat.fullMeta,
|
|
536
|
+
description: node.description,
|
|
507
537
|
isContainer: false,
|
|
508
538
|
lineNumber: node.lineNumber,
|
|
509
539
|
color: resolveNodeColor(node, parsed.tagGroups, activeTagGroup ?? null),
|
|
@@ -546,7 +576,15 @@ export function layoutSitemap(
|
|
|
546
576
|
node.children.length > 0 || (hc != null && hc > 0) || undefined,
|
|
547
577
|
});
|
|
548
578
|
} else {
|
|
549
|
-
// Fallback
|
|
579
|
+
// Fallback — still apply the floor for consistency
|
|
580
|
+
const isCollapsed = collapsedContainerIds.has(node.id);
|
|
581
|
+
const flooredW = isCollapsed
|
|
582
|
+
? Math.max(flat.width, pageMaxW)
|
|
583
|
+
: flat.width;
|
|
584
|
+
const fallbackH = isCollapsed
|
|
585
|
+
? flat.height
|
|
586
|
+
: labelHeight + CONTAINER_PAD_BOTTOM;
|
|
587
|
+
const flooredH = isCollapsed ? Math.max(fallbackH, pageMaxH) : fallbackH;
|
|
550
588
|
layoutContainers.push({
|
|
551
589
|
nodeId: node.id,
|
|
552
590
|
label: node.label,
|
|
@@ -556,8 +594,8 @@ export function layoutSitemap(
|
|
|
556
594
|
tagMetadata: flat.fullMeta,
|
|
557
595
|
x: MARGIN,
|
|
558
596
|
y: MARGIN,
|
|
559
|
-
width:
|
|
560
|
-
height:
|
|
597
|
+
width: flooredW,
|
|
598
|
+
height: flooredH,
|
|
561
599
|
labelHeight,
|
|
562
600
|
hiddenCount: hc,
|
|
563
601
|
hasChildren:
|