@diagrammo/dgmo 0.8.17 → 0.8.19
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 +103 -103
- package/dist/editor.cjs +1 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +1 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +1 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1306 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -15
- package/dist/index.d.ts +120 -15
- package/dist/index.js +1325 -151
- package/dist/index.js.map +1 -1
- package/docs/guide/how-dgmo-thinks.md +277 -0
- package/docs/guide/registry.json +1 -0
- package/gallery/fixtures/gantt-sprints.dgmo +20 -0
- package/package.json +1 -1
- package/src/colors.ts +1 -1
- package/src/editor/dgmo.grammar +1 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/gantt/calculator.ts +120 -7
- package/src/gantt/parser.ts +98 -3
- package/src/gantt/renderer.ts +410 -95
- package/src/gantt/types.ts +23 -2
- package/src/index.ts +10 -2
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +14 -2
- package/src/sequence/renderer.ts +186 -49
- package/src/sharing.ts +86 -49
- package/src/utils/duration.ts +16 -2
- package/src/utils/legend-constants.ts +11 -0
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +148 -17
- package/src/utils/legend-types.ts +45 -0
package/src/gantt/types.ts
CHANGED
|
@@ -7,8 +7,17 @@ import type { TagGroup } from '../utils/tag-groups';
|
|
|
7
7
|
|
|
8
8
|
// ── Duration ────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
/** Calendar units: d (days), w (weeks), m (months), q (quarters), y (years), h (hours), min (minutes). bd = business days. */
|
|
11
|
-
export type DurationUnit =
|
|
10
|
+
/** Calendar units: d (days), w (weeks), m (months), q (quarters), y (years), h (hours), min (minutes). bd = business days. s = sprints. */
|
|
11
|
+
export type DurationUnit =
|
|
12
|
+
| 'd'
|
|
13
|
+
| 'bd'
|
|
14
|
+
| 'w'
|
|
15
|
+
| 'm'
|
|
16
|
+
| 'q'
|
|
17
|
+
| 'y'
|
|
18
|
+
| 'h'
|
|
19
|
+
| 'min'
|
|
20
|
+
| 's';
|
|
12
21
|
|
|
13
22
|
export interface Duration {
|
|
14
23
|
amount: number;
|
|
@@ -119,6 +128,11 @@ export interface GanttOptions {
|
|
|
119
128
|
/** Line numbers for option/block keywords — maps key to source line */
|
|
120
129
|
optionLineNumbers: Record<string, number>;
|
|
121
130
|
holidaysLineNumber: number | null;
|
|
131
|
+
// ── Sprint options ─────────────────────────────────────────
|
|
132
|
+
sprintLength: Duration | null; // default { amount: 2, unit: 'w' } when sprint mode active
|
|
133
|
+
sprintNumber: number | null; // which sprint the chart starts at (default 1)
|
|
134
|
+
sprintStart: string | null; // YYYY-MM-DD — date that sprintNumber begins
|
|
135
|
+
sprintMode: 'auto' | 'explicit' | null; // auto = activated by `s` unit, explicit = sprint-* option present
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
// ── Parsed Result ───────────────────────────────────────────
|
|
@@ -158,6 +172,12 @@ export interface ResolvedGroup {
|
|
|
158
172
|
depth: number;
|
|
159
173
|
}
|
|
160
174
|
|
|
175
|
+
export interface ResolvedSprint {
|
|
176
|
+
number: number;
|
|
177
|
+
startDate: Date;
|
|
178
|
+
endDate: Date;
|
|
179
|
+
}
|
|
180
|
+
|
|
161
181
|
export interface ResolvedSchedule {
|
|
162
182
|
tasks: ResolvedTask[];
|
|
163
183
|
groups: ResolvedGroup[];
|
|
@@ -167,6 +187,7 @@ export interface ResolvedSchedule {
|
|
|
167
187
|
tagGroups: TagGroup[];
|
|
168
188
|
eras: GanttEra[];
|
|
169
189
|
markers: GanttMarker[];
|
|
190
|
+
sprints: ResolvedSprint[];
|
|
170
191
|
options: GanttOptions;
|
|
171
192
|
diagnostics: DgmoError[];
|
|
172
193
|
error: string | null;
|
package/src/index.ts
CHANGED
|
@@ -410,6 +410,9 @@ export type {
|
|
|
410
410
|
SequenceRenderOptions,
|
|
411
411
|
} from './sequence/renderer';
|
|
412
412
|
|
|
413
|
+
export { applyCollapseProjection } from './sequence/collapse';
|
|
414
|
+
export type { CollapsedView } from './sequence/collapse';
|
|
415
|
+
|
|
413
416
|
// ============================================================
|
|
414
417
|
// Colors & Palettes
|
|
415
418
|
// ============================================================
|
|
@@ -461,11 +464,16 @@ export type { PaletteConfig, PaletteColors } from './palettes';
|
|
|
461
464
|
// Sharing (URL encoding/decoding)
|
|
462
465
|
// ============================================================
|
|
463
466
|
|
|
464
|
-
export {
|
|
467
|
+
export {
|
|
468
|
+
encodeDiagramUrl,
|
|
469
|
+
decodeDiagramUrl,
|
|
470
|
+
encodeViewState,
|
|
471
|
+
decodeViewState,
|
|
472
|
+
} from './sharing';
|
|
465
473
|
export type {
|
|
466
474
|
EncodeDiagramUrlOptions,
|
|
467
475
|
EncodeDiagramUrlResult,
|
|
468
|
-
|
|
476
|
+
CompactViewState,
|
|
469
477
|
DecodedDiagramUrl,
|
|
470
478
|
} from './sharing';
|
|
471
479
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Collapse Projection for Sequence Diagram Groups
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Pure projection function that transforms a parsed sequence diagram
|
|
6
|
+
// by collapsing specified groups into single virtual participants.
|
|
7
|
+
// The parsed AST (ParsedSequenceDgmo) stays immutable.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ParsedSequenceDgmo,
|
|
11
|
+
SequenceElement,
|
|
12
|
+
SequenceGroup,
|
|
13
|
+
SequenceMessage,
|
|
14
|
+
SequenceParticipant,
|
|
15
|
+
} from './parser';
|
|
16
|
+
import { isSequenceBlock, isSequenceNote, isSequenceSection } from './parser';
|
|
17
|
+
|
|
18
|
+
export interface CollapsedView {
|
|
19
|
+
participants: SequenceParticipant[];
|
|
20
|
+
messages: SequenceMessage[];
|
|
21
|
+
elements: SequenceElement[];
|
|
22
|
+
groups: SequenceGroup[];
|
|
23
|
+
/** Maps member participant ID → collapsed group name */
|
|
24
|
+
collapsedGroupIds: Map<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Project a parsed sequence diagram into a collapsed view.
|
|
29
|
+
*
|
|
30
|
+
* @param parsed - The immutable parsed sequence diagram
|
|
31
|
+
* @param collapsedGroups - Set of group lineNumbers that should be collapsed
|
|
32
|
+
* @returns A new CollapsedView with remapped participants, messages, elements, and groups
|
|
33
|
+
*/
|
|
34
|
+
export function applyCollapseProjection(
|
|
35
|
+
parsed: ParsedSequenceDgmo,
|
|
36
|
+
collapsedGroups: Set<number>
|
|
37
|
+
): CollapsedView {
|
|
38
|
+
if (collapsedGroups.size === 0) {
|
|
39
|
+
return {
|
|
40
|
+
participants: parsed.participants,
|
|
41
|
+
messages: parsed.messages,
|
|
42
|
+
elements: parsed.elements,
|
|
43
|
+
groups: parsed.groups,
|
|
44
|
+
collapsedGroupIds: new Map(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build memberToGroup map: participantId → group name
|
|
49
|
+
const memberToGroup = new Map<string, string>();
|
|
50
|
+
const collapsedGroupNames = new Set<string>();
|
|
51
|
+
for (const group of parsed.groups) {
|
|
52
|
+
if (collapsedGroups.has(group.lineNumber)) {
|
|
53
|
+
collapsedGroupNames.add(group.name);
|
|
54
|
+
for (const memberId of group.participantIds) {
|
|
55
|
+
memberToGroup.set(memberId, group.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Participants: remove members of collapsed groups, insert virtual participant per group
|
|
61
|
+
// Skip non-member participants that collide with a collapsed group name
|
|
62
|
+
const participants: SequenceParticipant[] = [];
|
|
63
|
+
const insertedGroups = new Set<string>();
|
|
64
|
+
|
|
65
|
+
for (const p of parsed.participants) {
|
|
66
|
+
const groupName = memberToGroup.get(p.id);
|
|
67
|
+
if (groupName) {
|
|
68
|
+
// Replace first occurrence with virtual group participant
|
|
69
|
+
if (!insertedGroups.has(groupName)) {
|
|
70
|
+
insertedGroups.add(groupName);
|
|
71
|
+
const group = parsed.groups.find(
|
|
72
|
+
(g) => g.name === groupName && collapsedGroups.has(g.lineNumber)
|
|
73
|
+
)!;
|
|
74
|
+
participants.push({
|
|
75
|
+
id: groupName,
|
|
76
|
+
label: groupName,
|
|
77
|
+
type: 'default',
|
|
78
|
+
lineNumber: group.lineNumber,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Skip member — it's absorbed into the group
|
|
82
|
+
} else if (collapsedGroupNames.has(p.id)) {
|
|
83
|
+
// Skip — participant name collides with a collapsed group name;
|
|
84
|
+
// the virtual group participant takes precedence
|
|
85
|
+
} else {
|
|
86
|
+
participants.push(p);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Remap helper
|
|
91
|
+
const remap = (id: string): string => memberToGroup.get(id) ?? id;
|
|
92
|
+
|
|
93
|
+
// Messages: remap from/to, preserving order
|
|
94
|
+
const messages: SequenceMessage[] = parsed.messages.map((msg) => ({
|
|
95
|
+
...msg,
|
|
96
|
+
from: remap(msg.from),
|
|
97
|
+
to: remap(msg.to),
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Elements: deep clone with remapping and internal return suppression
|
|
101
|
+
const elements = remapElements(parsed.elements, memberToGroup);
|
|
102
|
+
|
|
103
|
+
// Groups: remove collapsed groups (they're now virtual participants)
|
|
104
|
+
const groups = parsed.groups.filter(
|
|
105
|
+
(g) => !collapsedGroups.has(g.lineNumber)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
participants,
|
|
110
|
+
messages,
|
|
111
|
+
elements,
|
|
112
|
+
groups,
|
|
113
|
+
collapsedGroupIds: memberToGroup,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Deep clone and remap elements, suppressing internal returns within collapsed groups.
|
|
119
|
+
*/
|
|
120
|
+
function remapElements(
|
|
121
|
+
elements: SequenceElement[],
|
|
122
|
+
memberToGroup: Map<string, string>
|
|
123
|
+
): SequenceElement[] {
|
|
124
|
+
const remap = (id: string): string => memberToGroup.get(id) ?? id;
|
|
125
|
+
const result: SequenceElement[] = [];
|
|
126
|
+
|
|
127
|
+
for (const el of elements) {
|
|
128
|
+
if (isSequenceSection(el)) {
|
|
129
|
+
// Sections have no participant references — pass through unchanged
|
|
130
|
+
result.push(el);
|
|
131
|
+
} else if (isSequenceNote(el)) {
|
|
132
|
+
// Remap note participant
|
|
133
|
+
result.push({
|
|
134
|
+
...el,
|
|
135
|
+
participantId: remap(el.participantId),
|
|
136
|
+
});
|
|
137
|
+
} else if (isSequenceBlock(el)) {
|
|
138
|
+
// Recurse into block children
|
|
139
|
+
result.push({
|
|
140
|
+
...el,
|
|
141
|
+
children: remapElements(el.children, memberToGroup),
|
|
142
|
+
elseChildren: remapElements(el.elseChildren, memberToGroup),
|
|
143
|
+
...(el.elseIfBranches
|
|
144
|
+
? {
|
|
145
|
+
elseIfBranches: el.elseIfBranches.map((branch) => ({
|
|
146
|
+
...branch,
|
|
147
|
+
children: remapElements(branch.children, memberToGroup),
|
|
148
|
+
})),
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
// Message element
|
|
154
|
+
const msg = el as SequenceMessage;
|
|
155
|
+
const from = remap(msg.from);
|
|
156
|
+
const to = remap(msg.to);
|
|
157
|
+
|
|
158
|
+
// Suppress internal return: both endpoints in same collapsed group
|
|
159
|
+
// and this is a return message (unlabeled response)
|
|
160
|
+
if (from === to && from !== msg.from && !msg.label) {
|
|
161
|
+
continue; // internal return suppressed
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
result.push({ ...msg, from, to });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
package/src/sequence/parser.ts
CHANGED
|
@@ -154,6 +154,8 @@ export interface SequenceGroup {
|
|
|
154
154
|
lineNumber: number;
|
|
155
155
|
/** Pipe-delimited tag metadata (e.g. `[Backend | t: Product]`) */
|
|
156
156
|
metadata?: Record<string, string>;
|
|
157
|
+
/** Whether this group is collapsed by default */
|
|
158
|
+
collapsed?: boolean;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
/**
|
|
@@ -502,8 +504,17 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
502
504
|
const groupColor = groupMatch[2]?.trim();
|
|
503
505
|
let groupMeta: Record<string, string> | undefined;
|
|
504
506
|
|
|
505
|
-
// Parse pipe metadata AFTER the closing bracket
|
|
506
|
-
|
|
507
|
+
// Parse collapse keyword and pipe metadata AFTER the closing bracket
|
|
508
|
+
let afterBracket = groupMatch[3]?.trim() || '';
|
|
509
|
+
let isCollapsed = false;
|
|
510
|
+
|
|
511
|
+
// Extract `collapse` keyword (before any pipe metadata)
|
|
512
|
+
const collapseMatch = afterBracket.match(/^collapse\b/i);
|
|
513
|
+
if (collapseMatch) {
|
|
514
|
+
isCollapsed = true;
|
|
515
|
+
afterBracket = afterBracket.slice(collapseMatch[0].length).trim();
|
|
516
|
+
}
|
|
517
|
+
|
|
507
518
|
if (afterBracket.startsWith('|')) {
|
|
508
519
|
const segments = afterBracket.split('|');
|
|
509
520
|
const meta = parsePipeMetadata(segments, aliasMap, () =>
|
|
@@ -524,6 +535,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
524
535
|
participantIds: [],
|
|
525
536
|
lineNumber,
|
|
526
537
|
...(groupMeta ? { metadata: groupMeta } : {}),
|
|
538
|
+
...(isCollapsed ? { collapsed: true } : {}),
|
|
527
539
|
};
|
|
528
540
|
result.groups.push(activeGroup);
|
|
529
541
|
continue;
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
SequenceParticipant,
|
|
22
22
|
} from './parser';
|
|
23
23
|
import { isSequenceBlock, isSequenceSection, isSequenceNote } from './parser';
|
|
24
|
+
import { applyCollapseProjection } from './collapse';
|
|
25
|
+
import type { CollapsedView } from './collapse';
|
|
24
26
|
import { resolveSequenceTags } from './tag-resolution';
|
|
25
27
|
import type { ResolvedTagMap } from './tag-resolution';
|
|
26
28
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
@@ -533,6 +535,7 @@ export interface SectionMessageGroup {
|
|
|
533
535
|
|
|
534
536
|
export interface SequenceRenderOptions {
|
|
535
537
|
collapsedSections?: Set<number>; // keyed by section lineNumber
|
|
538
|
+
collapsedGroups?: Set<number>; // keyed by group lineNumber
|
|
536
539
|
expandedNoteLines?: Set<number>; // keyed by note lineNumber; undefined = all expanded (CLI default)
|
|
537
540
|
exportWidth?: number; // Explicit width for CLI/export rendering (bypasses getBoundingClientRect)
|
|
538
541
|
activeTagGroup?: string | null; // Active tag group name for tag-driven recoloring; null = explicitly none
|
|
@@ -900,7 +903,37 @@ export function renderSequenceDiagram(
|
|
|
900
903
|
// Clear previous content
|
|
901
904
|
d3Selection.select(container).selectAll('*').remove();
|
|
902
905
|
|
|
903
|
-
const { title,
|
|
906
|
+
const { title, options: parsedOptions } = parsed;
|
|
907
|
+
|
|
908
|
+
// Compute effective collapsed groups: union of syntax-declared and runtime-toggled
|
|
909
|
+
const effectiveCollapsedGroups = new Set<number>();
|
|
910
|
+
for (const group of parsed.groups) {
|
|
911
|
+
if (group.collapsed) effectiveCollapsedGroups.add(group.lineNumber);
|
|
912
|
+
}
|
|
913
|
+
if (options?.collapsedGroups) {
|
|
914
|
+
for (const ln of options.collapsedGroups) {
|
|
915
|
+
// Toggle: if already in the set (from syntax), remove it (user expanded);
|
|
916
|
+
// if not in the set, add it (user collapsed)
|
|
917
|
+
if (effectiveCollapsedGroups.has(ln)) {
|
|
918
|
+
effectiveCollapsedGroups.delete(ln);
|
|
919
|
+
} else {
|
|
920
|
+
effectiveCollapsedGroups.add(ln);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Apply collapse projection before participant ordering
|
|
926
|
+
const collapsed: CollapsedView | null =
|
|
927
|
+
effectiveCollapsedGroups.size > 0
|
|
928
|
+
? applyCollapseProjection(parsed, effectiveCollapsedGroups)
|
|
929
|
+
: null;
|
|
930
|
+
|
|
931
|
+
const messages = collapsed ? collapsed.messages : parsed.messages;
|
|
932
|
+
const elements = collapsed ? collapsed.elements : parsed.elements;
|
|
933
|
+
const groups = collapsed ? collapsed.groups : parsed.groups;
|
|
934
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
935
|
+
const collapsedGroupIds = collapsed?.collapsedGroupIds ?? new Map();
|
|
936
|
+
|
|
904
937
|
const collapsedSections = options?.collapsedSections;
|
|
905
938
|
const expandedNoteLines = options?.expandedNoteLines;
|
|
906
939
|
const collapseNotesDisabled =
|
|
@@ -911,8 +944,12 @@ export function renderSequenceDiagram(
|
|
|
911
944
|
expandedNoteLines === undefined ||
|
|
912
945
|
collapseNotesDisabled ||
|
|
913
946
|
expandedNoteLines.has(note.lineNumber);
|
|
947
|
+
|
|
948
|
+
const sourceParticipants = collapsed
|
|
949
|
+
? collapsed.participants
|
|
950
|
+
: parsed.participants;
|
|
914
951
|
const participants = applyPositionOverrides(
|
|
915
|
-
applyGroupOrdering(
|
|
952
|
+
applyGroupOrdering(sourceParticipants, groups, messages)
|
|
916
953
|
);
|
|
917
954
|
if (participants.length === 0) return;
|
|
918
955
|
|
|
@@ -1158,6 +1195,15 @@ export function renderSequenceDiagram(
|
|
|
1158
1195
|
const preSectionMsgIndices: number[] = [];
|
|
1159
1196
|
const sectionRegions: SectionRegion[] = [];
|
|
1160
1197
|
{
|
|
1198
|
+
// Build lineNumber → message index lookup. This is used instead of
|
|
1199
|
+
// messages.indexOf() because collapse projection creates spread copies
|
|
1200
|
+
// of messages, breaking reference equality.
|
|
1201
|
+
const msgLineToIndex = new Map<number, number>();
|
|
1202
|
+
messages.forEach((m, i) => msgLineToIndex.set(m.lineNumber, i));
|
|
1203
|
+
|
|
1204
|
+
const findMsgIndex = (child: SequenceElement): number =>
|
|
1205
|
+
msgLineToIndex.get(child.lineNumber) ?? -1;
|
|
1206
|
+
|
|
1161
1207
|
const collectMsgIndicesFromBlock = (
|
|
1162
1208
|
block: import('./parser').SequenceBlock
|
|
1163
1209
|
): number[] => {
|
|
@@ -1166,7 +1212,7 @@ export function renderSequenceDiagram(
|
|
|
1166
1212
|
if (isSequenceBlock(child)) {
|
|
1167
1213
|
indices.push(...collectMsgIndicesFromBlock(child));
|
|
1168
1214
|
} else if (!isSequenceSection(child) && !isSequenceNote(child)) {
|
|
1169
|
-
const idx =
|
|
1215
|
+
const idx = findMsgIndex(child);
|
|
1170
1216
|
if (idx >= 0) indices.push(idx);
|
|
1171
1217
|
}
|
|
1172
1218
|
}
|
|
@@ -1176,7 +1222,7 @@ export function renderSequenceDiagram(
|
|
|
1176
1222
|
if (isSequenceBlock(child)) {
|
|
1177
1223
|
indices.push(...collectMsgIndicesFromBlock(child));
|
|
1178
1224
|
} else if (!isSequenceSection(child) && !isSequenceNote(child)) {
|
|
1179
|
-
const idx =
|
|
1225
|
+
const idx = findMsgIndex(child);
|
|
1180
1226
|
if (idx >= 0) indices.push(idx);
|
|
1181
1227
|
}
|
|
1182
1228
|
}
|
|
@@ -1186,7 +1232,7 @@ export function renderSequenceDiagram(
|
|
|
1186
1232
|
if (isSequenceBlock(child)) {
|
|
1187
1233
|
indices.push(...collectMsgIndicesFromBlock(child));
|
|
1188
1234
|
} else if (!isSequenceSection(child) && !isSequenceNote(child)) {
|
|
1189
|
-
const idx =
|
|
1235
|
+
const idx = findMsgIndex(child);
|
|
1190
1236
|
if (idx >= 0) indices.push(idx);
|
|
1191
1237
|
}
|
|
1192
1238
|
}
|
|
@@ -1202,7 +1248,7 @@ export function renderSequenceDiagram(
|
|
|
1202
1248
|
} else if (isSequenceBlock(el)) {
|
|
1203
1249
|
currentTarget.push(...collectMsgIndicesFromBlock(el));
|
|
1204
1250
|
} else {
|
|
1205
|
-
const idx =
|
|
1251
|
+
const idx = findMsgIndex(el);
|
|
1206
1252
|
if (idx >= 0) currentTarget.push(idx);
|
|
1207
1253
|
}
|
|
1208
1254
|
}
|
|
@@ -1285,8 +1331,10 @@ export function renderSequenceDiagram(
|
|
|
1285
1331
|
const LEGEND_FIXED_GAP = 8;
|
|
1286
1332
|
const legendTopSpace =
|
|
1287
1333
|
parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
1334
|
+
// Use parsed.groups (not projected groups) to keep vertical space consistent
|
|
1335
|
+
// even when all groups are collapsed into virtual participants
|
|
1288
1336
|
const groupOffset =
|
|
1289
|
-
groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
|
|
1337
|
+
parsed.groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
|
|
1290
1338
|
const participantStartY =
|
|
1291
1339
|
TOP_MARGIN +
|
|
1292
1340
|
titleOffset +
|
|
@@ -1618,7 +1666,23 @@ export function renderSequenceDiagram(
|
|
|
1618
1666
|
);
|
|
1619
1667
|
}
|
|
1620
1668
|
|
|
1621
|
-
//
|
|
1669
|
+
// Build set of collapsed group names for drill-bar rendering
|
|
1670
|
+
const collapsedGroupNames = new Set<string>();
|
|
1671
|
+
const collapsedGroupMeta = new Map<
|
|
1672
|
+
string,
|
|
1673
|
+
{ lineNumber: number; metadata?: Record<string, string> }
|
|
1674
|
+
>();
|
|
1675
|
+
for (const group of parsed.groups) {
|
|
1676
|
+
if (effectiveCollapsedGroups.has(group.lineNumber)) {
|
|
1677
|
+
collapsedGroupNames.add(group.name);
|
|
1678
|
+
collapsedGroupMeta.set(group.name, {
|
|
1679
|
+
lineNumber: group.lineNumber,
|
|
1680
|
+
metadata: group.metadata,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Render group boxes (behind participant shapes) — skip collapsed groups
|
|
1622
1686
|
for (const group of groups) {
|
|
1623
1687
|
if (group.participantIds.length === 0) continue;
|
|
1624
1688
|
|
|
@@ -1650,7 +1714,15 @@ export function renderSequenceDiagram(
|
|
|
1650
1714
|
: palette.bg;
|
|
1651
1715
|
const strokeColor = groupTagColor || palette.textMuted;
|
|
1652
1716
|
|
|
1653
|
-
svg
|
|
1717
|
+
const groupG = svg
|
|
1718
|
+
.append('g')
|
|
1719
|
+
.attr('class', 'group-box-wrapper')
|
|
1720
|
+
.attr('data-group-toggle', '')
|
|
1721
|
+
.attr('data-group-line', String(group.lineNumber))
|
|
1722
|
+
.attr('cursor', 'pointer');
|
|
1723
|
+
groupG.append('title').text('Click to collapse');
|
|
1724
|
+
|
|
1725
|
+
groupG
|
|
1654
1726
|
.append('rect')
|
|
1655
1727
|
.attr('x', minX)
|
|
1656
1728
|
.attr('y', boxY)
|
|
@@ -1661,11 +1733,10 @@ export function renderSequenceDiagram(
|
|
|
1661
1733
|
.attr('stroke', strokeColor)
|
|
1662
1734
|
.attr('stroke-width', 1)
|
|
1663
1735
|
.attr('stroke-opacity', 0.5)
|
|
1664
|
-
.attr('class', 'group-box')
|
|
1665
|
-
.attr('data-group-line', String(group.lineNumber));
|
|
1736
|
+
.attr('class', 'group-box');
|
|
1666
1737
|
|
|
1667
1738
|
// Group label
|
|
1668
|
-
|
|
1739
|
+
groupG
|
|
1669
1740
|
.append('text')
|
|
1670
1741
|
.attr('x', minX + 8)
|
|
1671
1742
|
.attr('y', boxY + GROUP_LABEL_SIZE + 4)
|
|
@@ -1674,7 +1745,6 @@ export function renderSequenceDiagram(
|
|
|
1674
1745
|
.attr('font-weight', 'bold')
|
|
1675
1746
|
.attr('opacity', 0.7)
|
|
1676
1747
|
.attr('class', 'group-label')
|
|
1677
|
-
.attr('data-group-line', String(group.lineNumber))
|
|
1678
1748
|
.text(group.name);
|
|
1679
1749
|
}
|
|
1680
1750
|
|
|
@@ -1690,6 +1760,16 @@ export function renderSequenceDiagram(
|
|
|
1690
1760
|
tagKey && pTagValue
|
|
1691
1761
|
? { key: tagKey, value: pTagValue.toLowerCase() }
|
|
1692
1762
|
: undefined;
|
|
1763
|
+
// For collapsed group participants, resolve tag color from group metadata
|
|
1764
|
+
const isCollapsedGroup = collapsedGroupNames.has(participant.id);
|
|
1765
|
+
let effectiveTagColor = pTagColor;
|
|
1766
|
+
if (isCollapsedGroup && !effectiveTagColor) {
|
|
1767
|
+
const meta = collapsedGroupMeta.get(participant.id);
|
|
1768
|
+
if (meta?.metadata && tagKey) {
|
|
1769
|
+
effectiveTagColor = getTagColor(meta.metadata[tagKey]);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1693
1773
|
renderParticipant(
|
|
1694
1774
|
svg,
|
|
1695
1775
|
participant,
|
|
@@ -1697,18 +1777,104 @@ export function renderSequenceDiagram(
|
|
|
1697
1777
|
cy,
|
|
1698
1778
|
palette,
|
|
1699
1779
|
isDark,
|
|
1700
|
-
|
|
1780
|
+
effectiveTagColor,
|
|
1701
1781
|
pTagAttr
|
|
1702
1782
|
);
|
|
1703
1783
|
|
|
1704
|
-
//
|
|
1784
|
+
// Collapsed group: re-render participant box at full group height + drill-bar
|
|
1785
|
+
if (isCollapsedGroup) {
|
|
1786
|
+
const meta = collapsedGroupMeta.get(participant.id)!;
|
|
1787
|
+
const drillColor = effectiveTagColor || palette.textMuted;
|
|
1788
|
+
const drillBarH = 6;
|
|
1789
|
+
const boxW = PARTICIPANT_BOX_WIDTH;
|
|
1790
|
+
// Match the group box dimensions
|
|
1791
|
+
const fullH =
|
|
1792
|
+
PARTICIPANT_BOX_HEIGHT + GROUP_PADDING_TOP + GROUP_PADDING_BOTTOM;
|
|
1793
|
+
const clipId = `clip-drill-group-${participant.id.replace(/[^a-zA-Z0-9-]/g, '-')}`;
|
|
1794
|
+
|
|
1795
|
+
// Add toggle attributes to the participant <g> so any click on it
|
|
1796
|
+
// (overlay rect, label, drill-bar) walks up and triggers the toggle
|
|
1797
|
+
const participantG = svg.select<SVGGElement>(
|
|
1798
|
+
`.participant[data-participant-id="${participant.id}"]`
|
|
1799
|
+
);
|
|
1800
|
+
participantG
|
|
1801
|
+
.attr('data-group-toggle', '')
|
|
1802
|
+
.attr('data-group-line', String(meta.lineNumber))
|
|
1803
|
+
.attr('cursor', 'pointer');
|
|
1804
|
+
participantG.append('title').text('Click to expand');
|
|
1805
|
+
|
|
1806
|
+
// Overlay a taller rect to replace the standard participant box
|
|
1807
|
+
const pFill = effectiveTagColor
|
|
1808
|
+
? mix(
|
|
1809
|
+
effectiveTagColor,
|
|
1810
|
+
isDark ? palette.surface : palette.bg,
|
|
1811
|
+
isDark ? 30 : 40
|
|
1812
|
+
)
|
|
1813
|
+
: isDark
|
|
1814
|
+
? mix(palette.overlay, palette.surface, 50)
|
|
1815
|
+
: mix(palette.bg, palette.surface, 50);
|
|
1816
|
+
const pStroke = effectiveTagColor || palette.border;
|
|
1817
|
+
|
|
1818
|
+
// Taller box inside the participant <g> (local coords, y=0 is participant cy)
|
|
1819
|
+
participantG
|
|
1820
|
+
.append('rect')
|
|
1821
|
+
.attr('x', -boxW / 2)
|
|
1822
|
+
.attr('y', -GROUP_PADDING_TOP)
|
|
1823
|
+
.attr('width', boxW)
|
|
1824
|
+
.attr('height', fullH)
|
|
1825
|
+
.attr('rx', 6)
|
|
1826
|
+
.attr('fill', pFill)
|
|
1827
|
+
.attr('stroke', pStroke)
|
|
1828
|
+
.attr('stroke-width', 1.5);
|
|
1829
|
+
|
|
1830
|
+
// Re-render label centered in the taller box (local coords)
|
|
1831
|
+
participantG
|
|
1832
|
+
.append('text')
|
|
1833
|
+
.attr('x', 0)
|
|
1834
|
+
.attr('y', -GROUP_PADDING_TOP + fullH / 2)
|
|
1835
|
+
.attr('text-anchor', 'middle')
|
|
1836
|
+
.attr('dominant-baseline', 'central')
|
|
1837
|
+
.attr('fill', palette.text)
|
|
1838
|
+
.attr('font-size', 13)
|
|
1839
|
+
.attr('font-weight', 500)
|
|
1840
|
+
.text(participant.label);
|
|
1841
|
+
|
|
1842
|
+
// Drill-bar at bottom (local coords)
|
|
1843
|
+
participantG
|
|
1844
|
+
.append('clipPath')
|
|
1845
|
+
.attr('id', clipId)
|
|
1846
|
+
.append('rect')
|
|
1847
|
+
.attr('x', -boxW / 2)
|
|
1848
|
+
.attr('y', -GROUP_PADDING_TOP)
|
|
1849
|
+
.attr('width', boxW)
|
|
1850
|
+
.attr('height', fullH)
|
|
1851
|
+
.attr('rx', 6);
|
|
1852
|
+
|
|
1853
|
+
participantG
|
|
1854
|
+
.append('rect')
|
|
1855
|
+
.attr('class', 'sequence-drill-bar')
|
|
1856
|
+
.attr('x', -boxW / 2)
|
|
1857
|
+
.attr('y', -GROUP_PADDING_TOP + fullH - drillBarH)
|
|
1858
|
+
.attr('width', boxW)
|
|
1859
|
+
.attr('height', drillBarH)
|
|
1860
|
+
.attr('fill', drillColor)
|
|
1861
|
+
.attr('clip-path', `url(#${clipId})`);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Render lifeline — collapsed groups start below the taller box
|
|
1865
|
+
const llY = isCollapsedGroup
|
|
1866
|
+
? lifelineStartY + GROUP_PADDING_BOTTOM
|
|
1867
|
+
: lifelineStartY;
|
|
1868
|
+
const llColor = isCollapsedGroup
|
|
1869
|
+
? effectiveTagColor || palette.textMuted
|
|
1870
|
+
: pTagColor || palette.textMuted;
|
|
1705
1871
|
const lifelineEl = svg
|
|
1706
1872
|
.append('line')
|
|
1707
1873
|
.attr('x1', cx)
|
|
1708
|
-
.attr('y1',
|
|
1874
|
+
.attr('y1', llY)
|
|
1709
1875
|
.attr('x2', cx)
|
|
1710
1876
|
.attr('y2', lifelineStartY + lifelineLength)
|
|
1711
|
-
.attr('stroke',
|
|
1877
|
+
.attr('stroke', llColor)
|
|
1712
1878
|
.attr('stroke-width', 1)
|
|
1713
1879
|
.attr('stroke-dasharray', '6 4')
|
|
1714
1880
|
.attr('class', 'lifeline')
|
|
@@ -2104,43 +2270,14 @@ export function renderSequenceDiagram(
|
|
|
2104
2270
|
? `${sec.label} (${msgCount} ${msgCount === 1 ? 'message' : 'messages'})`
|
|
2105
2271
|
: sec.label;
|
|
2106
2272
|
|
|
2107
|
-
// Collapsed sections use white text for contrast against the darker band
|
|
2108
|
-
const labelColor = isCollapsed ? '#ffffff' : lineColor;
|
|
2109
|
-
|
|
2110
|
-
// Chevron indicator
|
|
2111
|
-
const chevronSpace = 14;
|
|
2112
|
-
const labelX = (sectionLineX1 + sectionLineX2) / 2;
|
|
2113
|
-
const chevronX = labelX - (labelText.length * 3.5 + 8 + chevronSpace / 2);
|
|
2114
|
-
const chevronY = secY;
|
|
2115
|
-
if (isCollapsed) {
|
|
2116
|
-
// Right-pointing triangle ▶
|
|
2117
|
-
sectionG
|
|
2118
|
-
.append('path')
|
|
2119
|
-
.attr(
|
|
2120
|
-
'd',
|
|
2121
|
-
`M ${chevronX} ${chevronY - 4} L ${chevronX + 6} ${chevronY} L ${chevronX} ${chevronY + 4} Z`
|
|
2122
|
-
)
|
|
2123
|
-
.attr('fill', labelColor)
|
|
2124
|
-
.attr('class', 'section-chevron');
|
|
2125
|
-
} else {
|
|
2126
|
-
// Down-pointing triangle ▼
|
|
2127
|
-
sectionG
|
|
2128
|
-
.append('path')
|
|
2129
|
-
.attr(
|
|
2130
|
-
'd',
|
|
2131
|
-
`M ${chevronX - 1} ${chevronY - 3} L ${chevronX + 7} ${chevronY - 3} L ${chevronX + 3} ${chevronY + 3} Z`
|
|
2132
|
-
)
|
|
2133
|
-
.attr('fill', labelColor)
|
|
2134
|
-
.attr('class', 'section-chevron');
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
2273
|
// Centered label text
|
|
2274
|
+
const labelX = (sectionLineX1 + sectionLineX2) / 2;
|
|
2138
2275
|
sectionG
|
|
2139
2276
|
.append('text')
|
|
2140
|
-
.attr('x', labelX
|
|
2277
|
+
.attr('x', labelX)
|
|
2141
2278
|
.attr('y', secY + 4)
|
|
2142
2279
|
.attr('text-anchor', 'middle')
|
|
2143
|
-
.attr('fill',
|
|
2280
|
+
.attr('fill', lineColor)
|
|
2144
2281
|
.attr('font-size', 11)
|
|
2145
2282
|
.attr('font-weight', 'bold')
|
|
2146
2283
|
.attr('class', 'section-label')
|