@diagrammo/dgmo 0.8.5 → 0.8.6

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 (64) hide show
  1. package/.claude/commands/dgmo.md +33 -0
  2. package/.cursorrules +20 -2
  3. package/.github/copilot-instructions.md +20 -2
  4. package/.windsurfrules +20 -2
  5. package/AGENTS.md +23 -3
  6. package/dist/cli.cjs +189 -190
  7. package/dist/editor.cjs +3 -18
  8. package/dist/editor.cjs.map +1 -1
  9. package/dist/editor.js +3 -18
  10. package/dist/editor.js.map +1 -1
  11. package/dist/highlight.cjs +4 -21
  12. package/dist/highlight.cjs.map +1 -1
  13. package/dist/highlight.js +4 -21
  14. package/dist/highlight.js.map +1 -1
  15. package/dist/index.cjs +2785 -2996
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +56 -56
  18. package/dist/index.d.ts +56 -56
  19. package/dist/index.js +2780 -2989
  20. package/dist/index.js.map +1 -1
  21. package/docs/ai-integration.md +1 -1
  22. package/docs/language-reference.md +97 -25
  23. package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
  24. package/package.json +1 -1
  25. package/src/boxes-and-lines/collapse.ts +78 -0
  26. package/src/boxes-and-lines/layout.ts +319 -0
  27. package/src/boxes-and-lines/parser.ts +694 -0
  28. package/src/boxes-and-lines/renderer.ts +848 -0
  29. package/src/boxes-and-lines/types.ts +40 -0
  30. package/src/c4/parser.ts +10 -5
  31. package/src/c4/renderer.ts +232 -56
  32. package/src/chart.ts +9 -4
  33. package/src/cli.ts +6 -5
  34. package/src/completion.ts +25 -33
  35. package/src/d3.ts +26 -27
  36. package/src/dgmo-router.ts +3 -7
  37. package/src/echarts.ts +38 -2
  38. package/src/editor/keywords.ts +4 -19
  39. package/src/er/parser.ts +10 -4
  40. package/src/gantt/parser.ts +7 -4
  41. package/src/gantt/renderer.ts +3 -5
  42. package/src/index.ts +17 -26
  43. package/src/infra/parser.ts +7 -5
  44. package/src/infra/renderer.ts +2 -2
  45. package/src/kanban/parser.ts +7 -5
  46. package/src/kanban/renderer.ts +43 -18
  47. package/src/org/parser.ts +7 -4
  48. package/src/org/renderer.ts +40 -29
  49. package/src/sequence/parser.ts +11 -5
  50. package/src/sequence/renderer.ts +114 -45
  51. package/src/sitemap/parser.ts +8 -4
  52. package/src/sitemap/renderer.ts +137 -57
  53. package/src/utils/legend-svg.ts +44 -20
  54. package/src/utils/parsing.ts +1 -1
  55. package/src/utils/tag-groups.ts +21 -1
  56. package/gallery/fixtures/initiative-status-full.dgmo +0 -46
  57. package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
  58. package/gallery/fixtures/initiative-status.dgmo +0 -9
  59. package/src/initiative-status/collapse.ts +0 -76
  60. package/src/initiative-status/filter.ts +0 -63
  61. package/src/initiative-status/layout.ts +0 -650
  62. package/src/initiative-status/parser.ts +0 -629
  63. package/src/initiative-status/renderer.ts +0 -1199
  64. package/src/initiative-status/types.ts +0 -57
@@ -62,12 +62,12 @@ const NOTE_FONT_SIZE = 10;
62
62
  const NOTE_LINE_H = 14;
63
63
  const NOTE_GAP = 15;
64
64
  const NOTE_CHAR_W = 6;
65
- const NOTE_CHARS_PER_LINE = Math.floor((NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
65
+ const NOTE_CHARS_PER_LINE = Math.floor(
66
+ (NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W
67
+ );
66
68
  const COLLAPSED_NOTE_H = 20;
67
69
  const COLLAPSED_NOTE_W = 40;
68
70
 
69
-
70
-
71
71
  function wrapTextLines(text: string, maxChars: number): string[] {
72
72
  const rawLines = text.split('\n');
73
73
  const wrapped: string[] = [];
@@ -97,7 +97,9 @@ function wrapTextLines(text: string, maxChars: number): string[] {
97
97
  * Approximate max chars based on font-size 13 (~7.5px per char average).
98
98
  */
99
99
  const LABEL_CHAR_WIDTH = 7.5;
100
- const LABEL_MAX_CHARS = Math.floor((PARTICIPANT_BOX_WIDTH - 10) / LABEL_CHAR_WIDTH); // ~14 chars
100
+ const LABEL_MAX_CHARS = Math.floor(
101
+ (PARTICIPANT_BOX_WIDTH - 10) / LABEL_CHAR_WIDTH
102
+ ); // ~14 chars
101
103
 
102
104
  function splitParticipantLabel(label: string): string[] {
103
105
  if (label.length <= LABEL_MAX_CHARS) return [label];
@@ -114,7 +116,8 @@ function splitParticipantLabel(label: string): string[] {
114
116
  }
115
117
 
116
118
  // Split on camelCase boundaries: "UserLookupCloudFx" → ["User", "Lookup", "Cloud", "Fx"]
117
- const camelParts = label.replace(/([a-z])([A-Z])/g, '$1\x00$2')
119
+ const camelParts = label
120
+ .replace(/([a-z])([A-Z])/g, '$1\x00$2')
118
121
  .replace(/([A-Z]+)([A-Z][a-z])/g, '$1\x00$2')
119
122
  .split('\x00');
120
123
  if (camelParts.length > 1) {
@@ -142,13 +145,18 @@ function wrapLabelWords(words: string[]): string[] {
142
145
  }
143
146
 
144
147
  // Shared fill/stroke helpers — accept optional color override for per-participant coloring
145
- const fill = (palette: PaletteColors, isDark: boolean, color?: string): string =>
148
+ const fill = (
149
+ palette: PaletteColors,
150
+ isDark: boolean,
151
+ color?: string
152
+ ): string =>
146
153
  color
147
154
  ? mix(color, isDark ? palette.surface : palette.bg, isDark ? 30 : 40)
148
155
  : isDark
149
156
  ? mix(palette.overlay, palette.surface, 50)
150
157
  : mix(palette.bg, palette.surface, 50);
151
- const stroke = (palette: PaletteColors, color?: string): string => color || palette.border;
158
+ const stroke = (palette: PaletteColors, color?: string): string =>
159
+ color || palette.border;
152
160
  const SW = 1.5;
153
161
  const W = PARTICIPANT_BOX_WIDTH;
154
162
  const H = PARTICIPANT_BOX_HEIGHT;
@@ -904,11 +912,14 @@ export function renderSequenceDiagram(
904
912
  const { title, messages, elements, groups, options: parsedOptions } = parsed;
905
913
  const collapsedSections = options?.collapsedSections;
906
914
  const expandedNoteLines = options?.expandedNoteLines;
907
- const collapseNotesDisabled = parsedOptions['collapse-notes']?.toLowerCase() === 'no';
915
+ const collapseNotesDisabled =
916
+ parsedOptions['collapse-notes']?.toLowerCase() === 'no';
908
917
  // A note is expanded if: expandedNoteLines is undefined (CLI/export),
909
918
  // collapse-notes: no is set, or the note's lineNumber is in the set.
910
919
  const isNoteExpanded = (note: SequenceNote): boolean =>
911
- expandedNoteLines === undefined || collapseNotesDisabled || expandedNoteLines.has(note.lineNumber);
920
+ expandedNoteLines === undefined ||
921
+ collapseNotesDisabled ||
922
+ expandedNoteLines.has(note.lineNumber);
912
923
  const participants = applyPositionOverrides(
913
924
  applyGroupOrdering(parsed.participants, groups, messages)
914
925
  );
@@ -928,11 +939,14 @@ export function renderSequenceDiagram(
928
939
  if (activeTagGroup) {
929
940
  tagMap = resolveSequenceTags(parsed, activeTagGroup);
930
941
  const tg = parsed.tagGroups.find(
931
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase(),
942
+ (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
932
943
  );
933
944
  if (tg) {
934
945
  for (const entry of tg.entries) {
935
- tagValueToColor.set(entry.value.toLowerCase(), resolveColor(entry.color) ?? entry.color);
946
+ tagValueToColor.set(
947
+ entry.value.toLowerCase(),
948
+ resolveColor(entry.color) ?? entry.color
949
+ );
936
950
  }
937
951
  }
938
952
  }
@@ -962,9 +976,7 @@ export function renderSequenceDiagram(
962
976
  : allRenderSteps;
963
977
  // Drop unlabeled returns — they add visual noise without conveying information.
964
978
  // Labeled returns (explicit <- value) are kept.
965
- renderSteps = renderSteps.filter(
966
- (s) => s.type === 'call' || s.label
967
- );
979
+ renderSteps = renderSteps.filter((s) => s.type === 'call' || s.label);
968
980
  const activations = activationsOff ? [] : computeActivations(renderSteps);
969
981
  const stepSpacing = 35;
970
982
 
@@ -1034,7 +1046,7 @@ export function renderSequenceDiagram(
1034
1046
  };
1035
1047
 
1036
1048
  // Section layout constants
1037
- const SECTION_TOP_PAD = 35; // space above section divider line (matches stepSpacing)
1049
+ const SECTION_TOP_PAD = 35; // space above section divider line (matches stepSpacing)
1038
1050
  const SECTION_BOTTOM_PAD = 45; // space below section divider line before next content
1039
1051
 
1040
1052
  // Block spacing via extraBeforeMsg (sections handled separately below)
@@ -1282,11 +1294,16 @@ export function renderSequenceDiagram(
1282
1294
  // Compute cumulative Y positions for each step, with section dividers as stable anchors
1283
1295
  const titleOffset = title ? TITLE_HEIGHT : 0;
1284
1296
  const LEGEND_FIXED_GAP = 8;
1285
- const legendTopSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
1297
+ const legendTopSpace =
1298
+ parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
1286
1299
  const groupOffset =
1287
1300
  groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
1288
1301
  const participantStartY =
1289
- TOP_MARGIN + titleOffset + legendTopSpace + PARTICIPANT_Y_OFFSET + groupOffset;
1302
+ TOP_MARGIN +
1303
+ titleOffset +
1304
+ legendTopSpace +
1305
+ PARTICIPANT_Y_OFFSET +
1306
+ groupOffset;
1290
1307
  const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
1291
1308
  const hasActors = participants.some((p) => p.type === 'actor');
1292
1309
  const messageStartOffset = MESSAGE_START_OFFSET + (hasActors ? 20 : 0);
@@ -1339,7 +1356,10 @@ export function renderSequenceDiagram(
1339
1356
  const si = findAssociatedLastStep(el);
1340
1357
  if (si < 0) continue;
1341
1358
  // Check if there's a preceding note that we should stack below
1342
- const prevNote = i > 0 && isSequenceNote(els[i - 1]) ? (els[i - 1] as SequenceNote) : null;
1359
+ const prevNote =
1360
+ i > 0 && isSequenceNote(els[i - 1])
1361
+ ? (els[i - 1] as SequenceNote)
1362
+ : null;
1343
1363
  const prevNoteY = prevNote ? noteYMap.get(prevNote) : undefined;
1344
1364
  let noteTopY: number;
1345
1365
  if (prevNoteY !== undefined && prevNote) {
@@ -1378,8 +1398,13 @@ export function renderSequenceDiagram(
1378
1398
  )
1379
1399
  : layoutEndY;
1380
1400
  for (const [note, noteTopY] of noteYMap) {
1381
- const noteH = isNoteExpanded(note) ? computeNoteHeight(note.text) : COLLAPSED_NOTE_H;
1382
- contentBottomY = Math.max(contentBottomY, noteTopY + noteH + NOTE_TRAILING_GAP);
1401
+ const noteH = isNoteExpanded(note)
1402
+ ? computeNoteHeight(note.text)
1403
+ : COLLAPSED_NOTE_H;
1404
+ contentBottomY = Math.max(
1405
+ contentBottomY,
1406
+ noteTopY + noteH + NOTE_TRAILING_GAP
1407
+ );
1383
1408
  }
1384
1409
  const messageAreaHeight = contentBottomY - lifelineStartY0;
1385
1410
  const lifelineLength = messageAreaHeight + LIFELINE_TAIL;
@@ -1394,7 +1419,8 @@ export function renderSequenceDiagram(
1394
1419
  40;
1395
1420
  const totalHeight = contentHeight;
1396
1421
 
1397
- const containerWidth = options?.exportWidth ?? container.getBoundingClientRect().width;
1422
+ const containerWidth =
1423
+ options?.exportWidth ?? container.getBoundingClientRect().width;
1398
1424
  const svgWidth = Math.max(totalWidth, containerWidth);
1399
1425
 
1400
1426
  // Center the diagram horizontally
@@ -1528,7 +1554,7 @@ export function renderSequenceDiagram(
1528
1554
  // Helper: resolve marker ref for tag-colored arrows
1529
1555
  const coloredMarker = (
1530
1556
  type: 'call' | 'async' | 'return',
1531
- tagColor?: string,
1557
+ tagColor?: string
1532
1558
  ): string => {
1533
1559
  if (tagColor) {
1534
1560
  const hex = tagColor.replace('#', '');
@@ -1578,7 +1604,7 @@ export function renderSequenceDiagram(
1578
1604
 
1579
1605
  // Pre-compute pill/capsule widths for centering
1580
1606
  const legendItems: Array<{
1581
- group: typeof parsed.tagGroups[0];
1607
+ group: (typeof parsed.tagGroups)[0];
1582
1608
  isActive: boolean;
1583
1609
  pillWidth: number;
1584
1610
  totalWidth: number;
@@ -1589,7 +1615,8 @@ export function renderSequenceDiagram(
1589
1615
  const isActive =
1590
1616
  !!activeTagGroup &&
1591
1617
  tg.name.toLowerCase() === activeTagGroup.toLowerCase();
1592
- const pillWidth = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1618
+ const pillWidth =
1619
+ measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1593
1620
  const entries = tg.entries.map((e) => ({
1594
1621
  value: e.value,
1595
1622
  color: resolveColor(e.color) ?? e.color,
@@ -1639,8 +1666,8 @@ export function renderSequenceDiagram(
1639
1666
  }
1640
1667
 
1641
1668
  const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
1642
- const pillYOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
1643
- const pillH = LEGEND_HEIGHT - (item.isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
1669
+ const pillYOff = LEGEND_CAPSULE_PAD;
1670
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
1644
1671
 
1645
1672
  // Pill background
1646
1673
  gEl
@@ -1702,7 +1729,10 @@ export function renderSequenceDiagram(
1702
1729
  .attr('fill', palette.textMuted)
1703
1730
  .text(entry.value);
1704
1731
 
1705
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1732
+ entryX =
1733
+ textX +
1734
+ measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
1735
+ LEGEND_ENTRY_TRAIL;
1706
1736
  }
1707
1737
  }
1708
1738
 
@@ -1732,7 +1762,11 @@ export function renderSequenceDiagram(
1732
1762
  const groupTagValue = tagKey && group.metadata?.[tagKey];
1733
1763
  const groupTagColor = getTagColor(groupTagValue || undefined);
1734
1764
  const fillColor = groupTagColor
1735
- ? mix(groupTagColor, isDark ? palette.surface : palette.bg, isDark ? 15 : 20)
1765
+ ? mix(
1766
+ groupTagColor,
1767
+ isDark ? palette.surface : palette.bg,
1768
+ isDark ? 15 : 20
1769
+ )
1736
1770
  : isDark
1737
1771
  ? palette.surface
1738
1772
  : palette.bg;
@@ -1778,7 +1812,16 @@ export function renderSequenceDiagram(
1778
1812
  tagKey && pTagValue
1779
1813
  ? { key: tagKey, value: pTagValue.toLowerCase() }
1780
1814
  : undefined;
1781
- renderParticipant(svg, participant, cx, cy, palette, isDark, pTagColor, pTagAttr);
1815
+ renderParticipant(
1816
+ svg,
1817
+ participant,
1818
+ cx,
1819
+ cy,
1820
+ palette,
1821
+ isDark,
1822
+ pTagColor,
1823
+ pTagAttr
1824
+ );
1782
1825
 
1783
1826
  // Render lifeline
1784
1827
  const lifelineEl = svg
@@ -2031,7 +2074,11 @@ export function renderSequenceDiagram(
2031
2074
  .attr('height', y2 - y1)
2032
2075
  .attr('fill', isDark ? palette.surface : palette.bg);
2033
2076
 
2034
- const actFill = mix(actBaseColor, isDark ? palette.surface : palette.bg, isDark ? 15 : 30);
2077
+ const actFill = mix(
2078
+ actBaseColor,
2079
+ isDark ? palette.surface : palette.bg,
2080
+ isDark ? 15 : 30
2081
+ );
2035
2082
  const actRect = svg
2036
2083
  .append('rect')
2037
2084
  .attr('x', x)
@@ -2143,8 +2190,12 @@ export function renderSequenceDiagram(
2143
2190
  const bandX = sectionLineX1 - 10;
2144
2191
  const bandWidth = sectionLineX2 - sectionLineX1 + 20;
2145
2192
  const bandOpacity = isCollapsed
2146
- ? (isDark ? 0.35 : 0.25)
2147
- : (isDark ? 0.1 : 0.08);
2193
+ ? isDark
2194
+ ? 0.35
2195
+ : 0.25
2196
+ : isDark
2197
+ ? 0.1
2198
+ : 0.08;
2148
2199
  sectionG
2149
2200
  .append('rect')
2150
2201
  .attr('x', bandX)
@@ -2230,14 +2281,18 @@ export function renderSequenceDiagram(
2230
2281
  const x = arrowEdgeX(step.from, i, 'right');
2231
2282
 
2232
2283
  // Hit area for self-call
2233
- svg.append('rect')
2284
+ svg
2285
+ .append('rect')
2234
2286
  .attr('x', x)
2235
2287
  .attr('y', y - 5)
2236
2288
  .attr('width', SELF_CALL_WIDTH)
2237
2289
  .attr('height', SELF_CALL_HEIGHT + 10)
2238
2290
  .attr('fill', 'transparent')
2239
2291
  .attr('class', 'message-hit-area')
2240
- .attr('data-line-number', String(messages[step.messageIndex].lineNumber))
2292
+ .attr(
2293
+ 'data-line-number',
2294
+ String(messages[step.messageIndex].lineNumber)
2295
+ )
2241
2296
  .attr('data-msg-index', String(step.messageIndex))
2242
2297
  .attr('data-step-index', String(i));
2243
2298
 
@@ -2291,14 +2346,18 @@ export function renderSequenceDiagram(
2291
2346
  const x2 = arrowEdgeX(step.to, i, goingRight ? 'left' : 'right');
2292
2347
 
2293
2348
  // Hit area for call arrow
2294
- svg.append('rect')
2349
+ svg
2350
+ .append('rect')
2295
2351
  .attr('x', Math.min(x1, x2))
2296
2352
  .attr('y', y - HIT_H / 2)
2297
2353
  .attr('width', Math.abs(x2 - x1))
2298
2354
  .attr('height', HIT_H)
2299
2355
  .attr('fill', 'transparent')
2300
2356
  .attr('class', 'message-hit-area')
2301
- .attr('data-line-number', String(messages[step.messageIndex].lineNumber))
2357
+ .attr(
2358
+ 'data-line-number',
2359
+ String(messages[step.messageIndex].lineNumber)
2360
+ )
2302
2361
  .attr('data-msg-index', String(step.messageIndex))
2303
2362
  .attr('data-step-index', String(i));
2304
2363
 
@@ -2361,14 +2420,18 @@ export function renderSequenceDiagram(
2361
2420
  const returnColor = msgTagColor || palette.textMuted;
2362
2421
 
2363
2422
  // Hit area for return arrow
2364
- svg.append('rect')
2423
+ svg
2424
+ .append('rect')
2365
2425
  .attr('x', Math.min(x1, x2))
2366
2426
  .attr('y', y - HIT_H / 2)
2367
2427
  .attr('width', Math.abs(x2 - x1))
2368
2428
  .attr('height', HIT_H)
2369
2429
  .attr('fill', 'transparent')
2370
2430
  .attr('class', 'message-hit-area')
2371
- .attr('data-line-number', String(messages[step.messageIndex].lineNumber))
2431
+ .attr(
2432
+ 'data-line-number',
2433
+ String(messages[step.messageIndex].lineNumber)
2434
+ )
2372
2435
  .attr('data-msg-index', String(step.messageIndex))
2373
2436
  .attr('data-step-index', String(i));
2374
2437
 
@@ -2494,8 +2557,7 @@ export function renderSequenceDiagram(
2494
2557
 
2495
2558
  // Render text with inline markdown
2496
2559
  wrappedLines.forEach((line, li) => {
2497
- const textY =
2498
- noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
2560
+ const textY = noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
2499
2561
  const isBullet = line.startsWith('- ');
2500
2562
  const bulletIndent = isBullet ? 10 : 0;
2501
2563
  const displayLine = isBullet ? line.slice(2) : line;
@@ -2602,7 +2664,9 @@ export function renderSequenceDiagram(
2602
2664
  * associated message (the last message before the note in document order).
2603
2665
  * Used by the app to expand notes when cursor is on the associated message.
2604
2666
  */
2605
- export function buildNoteMessageMap(elements: SequenceElement[]): Map<number, number> {
2667
+ export function buildNoteMessageMap(
2668
+ elements: SequenceElement[]
2669
+ ): Map<number, number> {
2606
2670
  const map = new Map<number, number>();
2607
2671
  let lastMessageLine = -1;
2608
2672
 
@@ -2639,7 +2703,7 @@ function renderParticipant(
2639
2703
  palette: PaletteColors,
2640
2704
  isDark: boolean,
2641
2705
  color?: string,
2642
- tagAttr?: { key: string; value: string },
2706
+ tagAttr?: { key: string; value: string }
2643
2707
  ): void {
2644
2708
  const g = svg
2645
2709
  .append('g')
@@ -2691,7 +2755,8 @@ function renderParticipant(
2691
2755
  const labelLines = splitParticipantLabel(participant.label);
2692
2756
  const fontSize = 13;
2693
2757
  const lineHeight = fontSize + 2;
2694
- const textEl = g.append('text')
2758
+ const textEl = g
2759
+ .append('text')
2695
2760
  .attr('x', 0)
2696
2761
  .attr('text-anchor', 'middle')
2697
2762
  .attr('fill', palette.text)
@@ -2700,7 +2765,10 @@ function renderParticipant(
2700
2765
 
2701
2766
  if (labelLines.length === 1) {
2702
2767
  textEl
2703
- .attr('y', isActor ? PARTICIPANT_BOX_HEIGHT + 14 : PARTICIPANT_BOX_HEIGHT / 2 + 5)
2768
+ .attr(
2769
+ 'y',
2770
+ isActor ? PARTICIPANT_BOX_HEIGHT + 14 : PARTICIPANT_BOX_HEIGHT / 2 + 5
2771
+ )
2704
2772
  .text(participant.label);
2705
2773
  } else {
2706
2774
  // Multi-line: vertically center the lines within the box (or below for actors)
@@ -2710,7 +2778,8 @@ function renderParticipant(
2710
2778
  : PARTICIPANT_BOX_HEIGHT / 2 + 5 - (totalHeight - lineHeight) / 2;
2711
2779
 
2712
2780
  labelLines.forEach((line, i) => {
2713
- textEl.append('tspan')
2781
+ textEl
2782
+ .append('tspan')
2714
2783
  .attr('x', 0)
2715
2784
  .attr('dy', i === 0 ? `${baseY}px` : `${lineHeight}px`)
2716
2785
  .text(line);
@@ -10,6 +10,7 @@ import {
10
10
  isTagBlockHeading,
11
11
  matchTagBlockHeading,
12
12
  validateTagValues,
13
+ stripDefaultModifier,
13
14
  } from '../utils/tag-groups';
14
15
  import {
15
16
  measureIndent,
@@ -261,11 +262,13 @@ export function parseSitemap(
261
262
  }
262
263
  }
263
264
 
264
- // Tag group entries (indented Value(color) under tag: heading)
265
+ // Tag group entries (indented Value(color) under tag heading)
266
+ // First entry is the default unless another is marked `default`
265
267
  if (currentTagGroup && !contentStarted) {
266
268
  const indent = measureIndent(line);
267
269
  if (indent > 0) {
268
- const { label, color } = extractColor(trimmed, palette);
270
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
271
+ const { label, color } = extractColor(cleanEntry, palette);
269
272
  if (!color) {
270
273
  pushError(
271
274
  lineNumber,
@@ -278,8 +281,9 @@ export function parseSitemap(
278
281
  color,
279
282
  lineNumber,
280
283
  });
281
- // First entry is the default
282
- if (currentTagGroup.entries.length === 1) {
284
+ if (isDefault) {
285
+ currentTagGroup.defaultValue = label;
286
+ } else if (currentTagGroup.entries.length === 1) {
283
287
  currentTagGroup.defaultValue = label;
284
288
  }
285
289
  continue;