@diagrammo/dgmo 0.8.23 → 0.8.25

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.
Files changed (72) hide show
  1. package/.claude/commands/dgmo.md +60 -72
  2. package/dist/cli.cjs +119 -114
  3. package/dist/editor.cjs +0 -2
  4. package/dist/editor.cjs.map +1 -1
  5. package/dist/editor.js +0 -2
  6. package/dist/editor.js.map +1 -1
  7. package/dist/highlight.cjs +0 -2
  8. package/dist/highlight.cjs.map +1 -1
  9. package/dist/highlight.js +0 -2
  10. package/dist/highlight.js.map +1 -1
  11. package/dist/index.cjs +690 -278
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +105 -18
  14. package/dist/index.d.ts +105 -18
  15. package/dist/index.js +680 -277
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal.cjs +348 -51
  18. package/dist/internal.cjs.map +1 -1
  19. package/dist/internal.d.cts +93 -5
  20. package/dist/internal.d.ts +93 -5
  21. package/dist/internal.js +334 -38
  22. package/dist/internal.js.map +1 -1
  23. package/docs/guide/chart-area.md +17 -17
  24. package/docs/guide/chart-bar-stacked.md +12 -12
  25. package/docs/guide/chart-doughnut.md +10 -10
  26. package/docs/guide/chart-funnel.md +9 -9
  27. package/docs/guide/chart-heatmap.md +10 -10
  28. package/docs/guide/chart-kanban.md +2 -0
  29. package/docs/guide/chart-line.md +19 -19
  30. package/docs/guide/chart-multi-line.md +16 -16
  31. package/docs/guide/chart-pie.md +11 -11
  32. package/docs/guide/chart-polar-area.md +10 -10
  33. package/docs/guide/chart-radar.md +9 -9
  34. package/docs/guide/chart-scatter.md +24 -27
  35. package/docs/guide/index.md +3 -3
  36. package/docs/language-reference.md +46 -25
  37. package/fonts/Inter-Bold.ttf +0 -0
  38. package/fonts/Inter-Regular.ttf +0 -0
  39. package/fonts/LICENSE-Inter.txt +92 -0
  40. package/gallery/fixtures/bar-stacked.dgmo +12 -6
  41. package/gallery/fixtures/heatmap.dgmo +12 -6
  42. package/gallery/fixtures/multi-line.dgmo +11 -7
  43. package/gallery/fixtures/quadrant.dgmo +8 -8
  44. package/gallery/fixtures/scatter.dgmo +12 -12
  45. package/package.json +4 -2
  46. package/src/boxes-and-lines/parser.ts +13 -2
  47. package/src/boxes-and-lines/renderer.ts +22 -13
  48. package/src/chart-type-scoring.ts +162 -0
  49. package/src/chart-types.ts +437 -0
  50. package/src/cli.ts +147 -66
  51. package/src/completion.ts +0 -4
  52. package/src/d3.ts +9 -2
  53. package/src/dgmo-router.ts +85 -130
  54. package/src/editor/keywords.ts +0 -2
  55. package/src/fonts.ts +3 -2
  56. package/src/gantt/parser.ts +5 -1
  57. package/src/index.ts +24 -1
  58. package/src/infra/parser.ts +1 -1
  59. package/src/internal.ts +6 -2
  60. package/src/journey-map/layout.ts +7 -3
  61. package/src/journey-map/parser.ts +5 -1
  62. package/src/kanban/parser.ts +5 -1
  63. package/src/org/collapse.ts +1 -4
  64. package/src/org/parser.ts +1 -1
  65. package/src/org/renderer.ts +26 -17
  66. package/src/sequence/parser.ts +2 -2
  67. package/src/sequence/participant-inference.ts +0 -1
  68. package/src/sequence/renderer.ts +95 -263
  69. package/src/sharing.ts +0 -1
  70. package/src/sitemap/parser.ts +1 -1
  71. package/src/utils/tag-groups.ts +35 -5
  72. package/src/wireframe/parser.ts +3 -1
@@ -121,14 +121,18 @@ export function layoutJourneyMap(
121
121
  ? parsed.phases.flatMap((p) => p.steps)
122
122
  : parsed.steps;
123
123
 
124
- // Compute step card heights based on content (matches kanban card sizing)
124
+ // Compute step card heights based on content (matches kanban card sizing).
125
+ // Char-width constants MUST match the renderer's wrapText() in renderer.ts
126
+ // (`fontSize * 0.6`) and the title wrap (`TITLE_CHAR_WIDTH`) — otherwise the
127
+ // layout reserves too little vertical space and rendered text overflows.
125
128
  const annoIconIndent = ANNO_ICON_SIZE + ANNO_ICON_GAP;
126
129
  const annoTextW = STEP_CARD_WIDTH - CARD_PADDING_X * 2 - annoIconIndent;
127
130
  const descTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
128
- const charWidth = 4.8; // average char width at FONT_SIZE_META (10px)
131
+ const FONT_SIZE_META = 10;
132
+ const charWidth = FONT_SIZE_META * 0.6; // matches renderer wrapText()
129
133
 
130
134
  const titleTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
131
- const titleCharWidth = 6.5; // average char width at FONT_SIZE_STEP (12px)
135
+ const titleCharWidth = 6.5; // matches renderer TITLE_CHAR_WIDTH (FONT_SIZE_STEP 12px)
132
136
  const TITLE_LINE_HEIGHT = 16;
133
137
 
134
138
  const stepHeights = allSteps.map((step) => {
@@ -408,7 +408,11 @@ export function parseJourneyMap(
408
408
  return fail(1, 'No phases or steps found');
409
409
  }
410
410
 
411
- validateTagGroupNames(result.tagGroups, warn);
411
+ validateTagGroupNames(result.tagGroups, warn, (line, msg) => {
412
+ const diag = makeDgmoError(line, msg);
413
+ result.diagnostics.push(diag);
414
+ if (!result.error) result.error = formatDgmoError(diag);
415
+ });
412
416
 
413
417
  return result;
414
418
  }
@@ -373,7 +373,11 @@ export function parseKanban(
373
373
  return fail(1, 'No columns found. Use [Column Name] to define columns');
374
374
  }
375
375
 
376
- validateTagGroupNames(result.tagGroups, warn);
376
+ validateTagGroupNames(result.tagGroups, warn, (line, msg) => {
377
+ const diag = makeDgmoError(line, msg);
378
+ result.diagnostics.push(diag);
379
+ if (!result.error) result.error = formatDgmoError(diag);
380
+ });
377
381
 
378
382
  return result;
379
383
  }
@@ -72,10 +72,7 @@ function computeHiddenCounts(
72
72
  }
73
73
 
74
74
  /** Remove children of collapsed nodes on the cloned tree. */
75
- function pruneCollapsed(
76
- node: OrgNode,
77
- collapsedIds: Set<string>
78
- ): void {
75
+ function pruneCollapsed(node: OrgNode, collapsedIds: Set<string>): void {
79
76
  for (const child of node.children) {
80
77
  pruneCollapsed(child, collapsedIds);
81
78
  }
package/src/org/parser.ts CHANGED
@@ -344,7 +344,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
344
344
  collectAll(result.roots);
345
345
 
346
346
  validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
347
- validateTagGroupNames(result.tagGroups, pushWarning);
347
+ validateTagGroupNames(result.tagGroups, pushWarning, pushError);
348
348
  }
349
349
 
350
350
  if (
@@ -138,14 +138,16 @@ export function renderOrg(
138
138
  const titleReserve = fixedTitle ? TITLE_HEIGHT : 0;
139
139
 
140
140
  // Ancestor breadcrumb trail (focus mode) — rendered inside the scaled group
141
- const hasAncestorTrail = !exportDims && ancestorPath && ancestorPath.length > 0;
141
+ const hasAncestorTrail =
142
+ !exportDims && ancestorPath && ancestorPath.length > 0;
142
143
  const ancestorTrailHeight = hasAncestorTrail
143
144
  ? ancestorPath.length * ANCESTOR_ROW_HEIGHT + ANCESTOR_TRAIL_BOTTOM_GAP
144
145
  : 0;
145
146
 
146
147
  // Compute scale to fit diagram in viewport
147
148
  const diagramW = layout.width;
148
- let diagramH = layout.height + (fixedTitle ? 0 : titleOffset) + ancestorTrailHeight;
149
+ let diagramH =
150
+ layout.height + (fixedTitle ? 0 : titleOffset) + ancestorTrailHeight;
149
151
  if (fixedLegend) {
150
152
  // Remove the legend space from diagram height — legend is rendered separately
151
153
  diagramH -= layoutLegendShift;
@@ -594,15 +596,17 @@ export function renderOrg(
594
596
  : rootContainer
595
597
  ? rootContainer.x + rootContainer.width / 2
596
598
  : null;
597
- const rootTopY = rootNode ? rootNode.y : rootContainer ? rootContainer.y : null;
599
+ const rootTopY = rootNode
600
+ ? rootNode.y
601
+ : rootContainer
602
+ ? rootContainer.y
603
+ : null;
598
604
  if (rootCenterX !== null && rootTopY !== null) {
599
605
  // Trail connects directly to the top edge of the root node.
600
606
  // The last ancestor dot sits ANCESTOR_TRAIL_BOTTOM_GAP above the root.
601
607
  const trailBottomY = rootTopY - ANCESTOR_TRAIL_BOTTOM_GAP;
602
608
 
603
- const trailG = contentG
604
- .append('g')
605
- .attr('class', 'org-ancestor-trail');
609
+ const trailG = contentG.append('g').attr('class', 'org-ancestor-trail');
606
610
 
607
611
  const count = ancestorPath!.length;
608
612
 
@@ -631,13 +635,14 @@ export function renderOrg(
631
635
  const dotY = dotPositions[i];
632
636
 
633
637
  // Resolve color from tag groups (same logic as node cards)
634
- const resolvedColor = ancestor.color
635
- ?? resolveTagColor(
636
- ancestor.metadata,
637
- parsed.tagGroups,
638
- activeTagGroup ?? null,
639
- ancestor.isContainer
640
- );
638
+ const resolvedColor =
639
+ ancestor.color ??
640
+ resolveTagColor(
641
+ ancestor.metadata,
642
+ parsed.tagGroups,
643
+ activeTagGroup ?? null,
644
+ ancestor.isContainer
645
+ );
641
646
  const dotColor = resolvedColor ?? palette.textMuted;
642
647
 
643
648
  const rowG = trailG
@@ -676,14 +681,18 @@ export function renderOrg(
676
681
  // Hover effect
677
682
  rowG
678
683
  .on('mouseenter', function () {
679
- d3Selection.select(this).select('circle')
684
+ d3Selection
685
+ .select(this)
686
+ .select('circle')
680
687
  .attr('r', ANCESTOR_DOT_R + 1);
681
688
  d3Selection.select(this).select('text').attr('fill', palette.text);
682
689
  })
683
690
  .on('mouseleave', function () {
684
- d3Selection.select(this).select('circle')
685
- .attr('r', ANCESTOR_DOT_R);
686
- d3Selection.select(this).select('text').attr('fill', palette.textMuted);
691
+ d3Selection.select(this).select('circle').attr('r', ANCESTOR_DOT_R);
692
+ d3Selection
693
+ .select(this)
694
+ .select('text')
695
+ .attr('fill', palette.textMuted);
687
696
  });
688
697
  }
689
698
  }
@@ -26,7 +26,7 @@ import {
26
26
  const KNOWN_SEQ_OPTIONS = new Set(['active-tag']);
27
27
 
28
28
  /** Known sequence-diagram boolean options (bare keyword or `no-` prefix). */
29
- const KNOWN_SEQ_BOOLEANS = new Set(['activations', 'collapse-notes']);
29
+ const KNOWN_SEQ_BOOLEANS = new Set(['activations']);
30
30
 
31
31
  /**
32
32
  * Participant types that can be declared via "Name is a type" syntax.
@@ -1345,7 +1345,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1345
1345
  entities.push({ metadata: g.metadata, lineNumber: g.lineNumber });
1346
1346
  }
1347
1347
  validateTagValues(entities, result.tagGroups, pushWarning, suggest);
1348
- validateTagGroupNames(result.tagGroups, pushWarning);
1348
+ validateTagGroupNames(result.tagGroups, pushWarning, pushError);
1349
1349
  }
1350
1350
 
1351
1351
  return result;
@@ -185,7 +185,6 @@ const PARTICIPANT_RULES: readonly InferenceRule[] = [
185
185
  { pattern: /^Admin$/i, type: 'actor' },
186
186
  { pattern: /^User$/i, type: 'actor' },
187
187
  { pattern: /^Customer$/i, type: 'actor' },
188
- { pattern: /^Client$/i, type: 'actor' },
189
188
  { pattern: /^Agent$/i, type: 'actor' },
190
189
  { pattern: /^Person$/i, type: 'actor' },
191
190
  { pattern: /^Buyer$/i, type: 'actor' },
@@ -62,8 +62,6 @@ const NOTE_CHAR_W = 6;
62
62
  const NOTE_CHARS_PER_LINE = Math.floor(
63
63
  (NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W
64
64
  );
65
- const COLLAPSED_NOTE_H = 20;
66
- const COLLAPSED_NOTE_W = 40;
67
65
  const ACTIVATION_WIDTH = 10;
68
66
  const SELF_CALL_HEIGHT = 25;
69
67
  const SELF_CALL_WIDTH = 30;
@@ -557,13 +555,8 @@ export interface SectionMessageGroup {
557
555
  export interface SequenceRenderOptions {
558
556
  collapsedSections?: Set<number>; // keyed by section lineNumber
559
557
  collapsedGroups?: Set<number>; // keyed by group lineNumber
560
- expandedNoteLines?: Set<number>; // keyed by note lineNumber; undefined = all expanded (CLI default)
561
558
  exportWidth?: number; // Explicit width for CLI/export rendering (bypasses getBoundingClientRect)
562
559
  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
567
560
  }
568
561
 
569
562
  /**
@@ -960,15 +953,6 @@ export function renderSequenceDiagram(
960
953
  const collapsedGroupIds = collapsed?.collapsedGroupIds ?? new Map();
961
954
 
962
955
  const collapsedSections = options?.collapsedSections;
963
- const expandedNoteLines = options?.expandedNoteLines;
964
- const collapseNotesDisabled =
965
- parsedOptions['collapse-notes']?.toLowerCase() === 'no';
966
- // A note is expanded if: expandedNoteLines is undefined (CLI/export),
967
- // collapse-notes: no is set, or the note's lineNumber is in the set.
968
- const isNoteExpanded = (note: SequenceNote): boolean =>
969
- expandedNoteLines === undefined ||
970
- collapseNotesDisabled ||
971
- expandedNoteLines.has(note.lineNumber);
972
956
 
973
957
  const sourceParticipants = collapsed
974
958
  ? collapsed.participants
@@ -1232,9 +1216,7 @@ export function renderSequenceDiagram(
1232
1216
  const note = els[j] as SequenceNote;
1233
1217
  const sc = isNoteAfterSelfCall(note);
1234
1218
  const maxW = noteEffectiveMaxW(note.participantId, note.position, sc);
1235
- const noteH = isNoteExpanded(note)
1236
- ? computeNoteHeight(note.text, charsForWidth(maxW))
1237
- : COLLAPSED_NOTE_H;
1219
+ const noteH = computeNoteHeight(note.text, charsForWidth(maxW));
1238
1220
  totalExtent += noteH + NOTE_OFFSET_BELOW;
1239
1221
  j++;
1240
1222
  }
@@ -1500,9 +1482,10 @@ export function renderSequenceDiagram(
1500
1482
  prevNote.position,
1501
1483
  isNoteAfterSelfCall(prevNote)
1502
1484
  );
1503
- const prevNoteH = isNoteExpanded(prevNote)
1504
- ? computeNoteHeight(prevNote.text, charsForWidth(prevMaxW))
1505
- : COLLAPSED_NOTE_H;
1485
+ const prevNoteH = computeNoteHeight(
1486
+ prevNote.text,
1487
+ charsForWidth(prevMaxW)
1488
+ );
1506
1489
  noteTopY = prevNoteY + prevNoteH + NOTE_OFFSET_BELOW;
1507
1490
  } else {
1508
1491
  // First note after a message — use larger offset after self-calls
@@ -1539,9 +1522,7 @@ export function renderSequenceDiagram(
1539
1522
  note.position,
1540
1523
  isNoteAfterSelfCall(note)
1541
1524
  );
1542
- const noteH = isNoteExpanded(note)
1543
- ? computeNoteHeight(note.text, charsForWidth(maxW))
1544
- : COLLAPSED_NOTE_H;
1525
+ const noteH = computeNoteHeight(note.text, charsForWidth(maxW));
1545
1526
  contentBottomY = Math.max(
1546
1527
  contentBottomY,
1547
1528
  noteTopY + noteH + NOTE_TRAILING_GAP
@@ -1736,32 +1717,6 @@ export function renderSequenceDiagram(
1736
1717
  }
1737
1718
  }
1738
1719
 
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
1720
  const hasTagGroups = parsed.tagGroups.length > 0;
1766
1721
 
1767
1722
  // Build set of collapsed group names for drill-bar rendering
@@ -2632,8 +2587,6 @@ export function renderSequenceDiagram(
2632
2587
  ? mix(palette.surface, palette.bg, 50)
2633
2588
  : mix(palette.bg, palette.surface, 15);
2634
2589
 
2635
- const collapsedNoteFill = mix(palette.textMuted, palette.bg, 15);
2636
-
2637
2590
  const renderNoteElements = (els: SequenceElement[]): void => {
2638
2591
  for (const el of els) {
2639
2592
  if (isSequenceNote(el)) {
@@ -2642,168 +2595,98 @@ export function renderSequenceDiagram(
2642
2595
  const noteTopY = noteYMap.get(el);
2643
2596
  if (noteTopY === undefined) continue;
2644
2597
 
2645
- const expanded = isNoteExpanded(el);
2646
2598
  const isRight = el.position === 'right';
2599
+ const afterSelfCall = isNoteAfterSelfCall(el);
2600
+ const maxW = noteEffectiveMaxW(
2601
+ el.participantId,
2602
+ el.position,
2603
+ afterSelfCall
2604
+ );
2605
+ const maxChars = charsForWidth(maxW);
2606
+ const wrappedLines = wrapTextLines(el.text, maxChars);
2607
+ const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
2608
+ const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
2609
+ const noteW = Math.min(
2610
+ maxW,
2611
+ Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
2612
+ );
2613
+ // Shift notes past self-call loopback when applicable
2614
+ const rightOffset =
2615
+ afterSelfCall && isRight
2616
+ ? ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH + NOTE_GAP
2617
+ : ACTIVATION_WIDTH + NOTE_GAP;
2618
+ const noteX = isRight
2619
+ ? px + rightOffset
2620
+ : px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
2621
+
2622
+ const noteG = svg
2623
+ .append('g')
2624
+ .attr('class', 'note')
2625
+ .attr('data-note-toggle', '')
2626
+ .attr('data-line-number', String(el.lineNumber))
2627
+ .attr('data-line-end', String(el.endLineNumber));
2628
+
2629
+ // Folded-corner path
2630
+ noteG
2631
+ .append('path')
2632
+ .attr(
2633
+ 'd',
2634
+ [
2635
+ `M ${noteX} ${noteTopY}`,
2636
+ `L ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
2637
+ `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
2638
+ `L ${noteX + noteW} ${noteTopY + noteH}`,
2639
+ `L ${noteX} ${noteTopY + noteH}`,
2640
+ 'Z',
2641
+ ].join(' ')
2642
+ )
2643
+ .attr('fill', noteFill)
2644
+ .attr('stroke', palette.textMuted)
2645
+ .attr('stroke-width', 0.75)
2646
+ .attr('class', 'note-box');
2647
2647
 
2648
- if (expanded) {
2649
- // --- Expanded note: full folded-corner box with wrapped text ---
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);
2658
- const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
2659
- const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
2660
- const noteW = Math.min(
2661
- maxW,
2662
- Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
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;
2669
- const noteX = isRight
2670
- ? px + rightOffset
2671
- : px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
2672
-
2673
- const noteG = svg
2674
- .append('g')
2675
- .attr('class', 'note')
2676
- .attr('data-note-toggle', '')
2677
- .attr('data-line-number', String(el.lineNumber))
2678
- .attr('data-line-end', String(el.endLineNumber));
2679
-
2680
- // Folded-corner path
2681
- noteG
2682
- .append('path')
2683
- .attr(
2684
- 'd',
2685
- [
2686
- `M ${noteX} ${noteTopY}`,
2687
- `L ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
2688
- `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
2689
- `L ${noteX + noteW} ${noteTopY + noteH}`,
2690
- `L ${noteX} ${noteTopY + noteH}`,
2691
- 'Z',
2692
- ].join(' ')
2693
- )
2694
- .attr('fill', noteFill)
2695
- .attr('stroke', palette.textMuted)
2696
- .attr('stroke-width', 0.75)
2697
- .attr('class', 'note-box');
2698
-
2699
- // Fold triangle
2700
- noteG
2701
- .append('path')
2702
- .attr(
2703
- 'd',
2704
- [
2705
- `M ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
2706
- `L ${noteX + noteW - NOTE_FOLD} ${noteTopY + NOTE_FOLD}`,
2707
- `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
2708
- ].join(' ')
2709
- )
2710
- .attr('fill', 'none')
2711
- .attr('stroke', palette.textMuted)
2712
- .attr('stroke-width', 0.75)
2713
- .attr('class', 'note-fold');
2714
-
2715
- // Render text with inline markdown
2716
- wrappedLines.forEach((line, li) => {
2717
- const textY = noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
2718
- const isBullet = line.startsWith('- ');
2719
- const bulletIndent = isBullet ? 10 : 0;
2720
- const displayLine = isBullet ? line.slice(2) : line;
2721
- const textEl = noteG
2648
+ // Fold triangle
2649
+ noteG
2650
+ .append('path')
2651
+ .attr(
2652
+ 'd',
2653
+ [
2654
+ `M ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
2655
+ `L ${noteX + noteW - NOTE_FOLD} ${noteTopY + NOTE_FOLD}`,
2656
+ `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
2657
+ ].join(' ')
2658
+ )
2659
+ .attr('fill', 'none')
2660
+ .attr('stroke', palette.textMuted)
2661
+ .attr('stroke-width', 0.75)
2662
+ .attr('class', 'note-fold');
2663
+
2664
+ // Render text with inline markdown
2665
+ wrappedLines.forEach((line, li) => {
2666
+ const textY = noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
2667
+ const isBullet = line.startsWith('- ');
2668
+ const bulletIndent = isBullet ? 10 : 0;
2669
+ const displayLine = isBullet ? line.slice(2) : line;
2670
+ const textEl = noteG
2671
+ .append('text')
2672
+ .attr('x', noteX + NOTE_PAD_H + bulletIndent)
2673
+ .attr('y', textY)
2674
+ .attr('fill', palette.text)
2675
+ .attr('font-size', NOTE_FONT_SIZE)
2676
+ .attr('class', 'note-text');
2677
+
2678
+ if (isBullet) {
2679
+ noteG
2722
2680
  .append('text')
2723
- .attr('x', noteX + NOTE_PAD_H + bulletIndent)
2681
+ .attr('x', noteX + NOTE_PAD_H)
2724
2682
  .attr('y', textY)
2725
2683
  .attr('fill', palette.text)
2726
2684
  .attr('font-size', NOTE_FONT_SIZE)
2727
- .attr('class', 'note-text');
2728
-
2729
- if (isBullet) {
2730
- noteG
2731
- .append('text')
2732
- .attr('x', noteX + NOTE_PAD_H)
2733
- .attr('y', textY)
2734
- .attr('fill', palette.text)
2735
- .attr('font-size', NOTE_FONT_SIZE)
2736
- .text('\u2022');
2737
- }
2738
-
2739
- renderInlineText(textEl, displayLine, palette, NOTE_FONT_SIZE);
2740
- });
2741
- } else {
2742
- // --- Collapsed note: compact indicator ---
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;
2749
- const noteX = isRight
2750
- ? px + rightOffsetC
2751
- : px - ACTIVATION_WIDTH - NOTE_GAP - COLLAPSED_NOTE_W;
2752
-
2753
- const noteG = svg
2754
- .append('g')
2755
- .attr('class', 'note note-collapsed')
2756
- .attr('data-note-toggle', '')
2757
- .attr('data-line-number', String(el.lineNumber))
2758
- .attr('data-line-end', String(el.endLineNumber))
2759
- .style('cursor', 'pointer');
2760
-
2761
- // Small folded-corner rectangle
2762
- noteG
2763
- .append('path')
2764
- .attr(
2765
- 'd',
2766
- [
2767
- `M ${noteX} ${noteTopY}`,
2768
- `L ${noteX + COLLAPSED_NOTE_W - cFold} ${noteTopY}`,
2769
- `L ${noteX + COLLAPSED_NOTE_W} ${noteTopY + cFold}`,
2770
- `L ${noteX + COLLAPSED_NOTE_W} ${noteTopY + COLLAPSED_NOTE_H}`,
2771
- `L ${noteX} ${noteTopY + COLLAPSED_NOTE_H}`,
2772
- 'Z',
2773
- ].join(' ')
2774
- )
2775
- .attr('fill', collapsedNoteFill)
2776
- .attr('stroke', palette.border)
2777
- .attr('stroke-width', 0.75)
2778
- .attr('class', 'note-box');
2779
-
2780
- // Fold triangle
2781
- noteG
2782
- .append('path')
2783
- .attr(
2784
- 'd',
2785
- [
2786
- `M ${noteX + COLLAPSED_NOTE_W - cFold} ${noteTopY}`,
2787
- `L ${noteX + COLLAPSED_NOTE_W - cFold} ${noteTopY + cFold}`,
2788
- `L ${noteX + COLLAPSED_NOTE_W} ${noteTopY + cFold}`,
2789
- ].join(' ')
2790
- )
2791
- .attr('fill', 'none')
2792
- .attr('stroke', palette.border)
2793
- .attr('stroke-width', 0.75)
2794
- .attr('class', 'note-fold');
2685
+ .text('\u2022');
2686
+ }
2795
2687
 
2796
- // "..." text
2797
- noteG
2798
- .append('text')
2799
- .attr('x', noteX + COLLAPSED_NOTE_W / 2)
2800
- .attr('y', noteTopY + COLLAPSED_NOTE_H / 2 + 3)
2801
- .attr('text-anchor', 'middle')
2802
- .attr('fill', palette.textMuted)
2803
- .attr('font-size', 9)
2804
- .attr('class', 'note-text')
2805
- .text('\u2026');
2806
- }
2688
+ renderInlineText(textEl, displayLine, palette, NOTE_FONT_SIZE);
2689
+ });
2807
2690
  } else if (isSequenceBlock(el)) {
2808
2691
  renderNoteElements(el.children);
2809
2692
  if (el.elseIfBranches) {
@@ -2822,9 +2705,7 @@ export function renderSequenceDiagram(
2822
2705
 
2823
2706
  // Render legend LAST so it sits on top of all other SVG elements
2824
2707
  // (group boxes, lifelines, participants, etc.) and can receive clicks.
2825
- if (hasTagGroups || showNotesControl) {
2826
- const controlsExpanded = options?.controlsExpanded ?? false;
2827
-
2708
+ if (hasTagGroups) {
2828
2709
  const legendY = TOP_MARGIN + titleOffset;
2829
2710
  const resolvedGroups = parsed.tagGroups
2830
2711
  .filter((tg) => tg.entries.length > 0)
@@ -2836,41 +2717,17 @@ export function renderSequenceDiagram(
2836
2717
  })),
2837
2718
  }));
2838
2719
 
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
2720
  const legendConfig: LegendConfig = {
2856
2721
  groups: resolvedGroups,
2857
2722
  position: { placement: 'top-center', titleRelation: 'below-title' },
2858
2723
  mode: 'fixed',
2859
- controlsGroup,
2860
2724
  };
2861
2725
  const legendState: LegendState = {
2862
2726
  activeGroup: activeTagGroup ?? null,
2863
- controlsExpanded,
2727
+ controlsExpanded: false,
2864
2728
  };
2865
2729
 
2866
- const legendCallbacks: LegendCallbacks = {
2867
- onControlsExpand: () => {
2868
- options?.onToggleControlsExpand?.();
2869
- },
2870
- onControlsToggle: (_toggleId: string, active: boolean) => {
2871
- options?.onExpandAllNotes?.(active);
2872
- },
2873
- };
2730
+ const legendCallbacks: LegendCallbacks = {};
2874
2731
 
2875
2732
  const legendG = svg
2876
2733
  .append('g')
@@ -2891,7 +2748,7 @@ export function renderSequenceDiagram(
2891
2748
  /**
2892
2749
  * Build a mapping from each note's lineNumber to the lineNumber of its
2893
2750
  * associated message (the last message before the note in document order).
2894
- * Used by the app to expand notes when cursor is on the associated message.
2751
+ * Used by the app to highlight the associated message when cursor is on a note.
2895
2752
  */
2896
2753
  export function buildNoteMessageMap(
2897
2754
  elements: SequenceElement[]
@@ -2924,31 +2781,6 @@ export function buildNoteMessageMap(
2924
2781
  return map;
2925
2782
  }
2926
2783
 
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
-
2952
2784
  function renderParticipant(
2953
2785
  svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
2954
2786
  participant: SequenceParticipant,
package/src/sharing.ts CHANGED
@@ -21,7 +21,6 @@ export interface CompactViewState {
21
21
  rm?: string; // render mode override
22
22
  htv?: Record<string, string[]>; // hidden tag values
23
23
  ha?: string[]; // hidden attributes
24
- enl?: number[]; // expanded note lines (sequence)
25
24
  sem?: boolean; // semantic colors (ER)
26
25
  cm?: boolean; // compact meta (kanban)
27
26
  c4l?: string; // C4 level
@@ -509,7 +509,7 @@ export function parseSitemap(
509
509
  };
510
510
  collectAll(result.roots);
511
511
  validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
512
- validateTagGroupNames(result.tagGroups, pushWarning);
512
+ validateTagGroupNames(result.tagGroups, pushWarning, pushError);
513
513
  }
514
514
 
515
515
  if (