@diagrammo/dgmo 0.8.5 → 0.8.7

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 (65) hide show
  1. package/.claude/commands/dgmo.md +34 -27
  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/README.md +0 -1
  7. package/dist/cli.cjs +189 -190
  8. package/dist/editor.cjs +3 -18
  9. package/dist/editor.cjs.map +1 -1
  10. package/dist/editor.js +3 -18
  11. package/dist/editor.js.map +1 -1
  12. package/dist/highlight.cjs +4 -21
  13. package/dist/highlight.cjs.map +1 -1
  14. package/dist/highlight.js +4 -21
  15. package/dist/highlight.js.map +1 -1
  16. package/dist/index.cjs +2791 -2999
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +56 -56
  19. package/dist/index.d.ts +56 -56
  20. package/dist/index.js +2786 -2992
  21. package/dist/index.js.map +1 -1
  22. package/docs/ai-integration.md +1 -1
  23. package/docs/language-reference.md +112 -121
  24. package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
  25. package/package.json +1 -1
  26. package/src/boxes-and-lines/collapse.ts +78 -0
  27. package/src/boxes-and-lines/layout.ts +319 -0
  28. package/src/boxes-and-lines/parser.ts +697 -0
  29. package/src/boxes-and-lines/renderer.ts +848 -0
  30. package/src/boxes-and-lines/types.ts +40 -0
  31. package/src/c4/parser.ts +10 -5
  32. package/src/c4/renderer.ts +232 -56
  33. package/src/chart.ts +9 -4
  34. package/src/cli.ts +6 -5
  35. package/src/completion.ts +25 -33
  36. package/src/d3.ts +26 -27
  37. package/src/dgmo-router.ts +3 -7
  38. package/src/echarts.ts +38 -2
  39. package/src/editor/keywords.ts +4 -19
  40. package/src/er/parser.ts +10 -4
  41. package/src/gantt/parser.ts +10 -4
  42. package/src/gantt/renderer.ts +3 -5
  43. package/src/index.ts +17 -26
  44. package/src/infra/parser.ts +10 -5
  45. package/src/infra/renderer.ts +2 -2
  46. package/src/kanban/parser.ts +10 -5
  47. package/src/kanban/renderer.ts +43 -18
  48. package/src/org/parser.ts +7 -4
  49. package/src/org/renderer.ts +40 -29
  50. package/src/sequence/parser.ts +11 -5
  51. package/src/sequence/renderer.ts +114 -45
  52. package/src/sitemap/parser.ts +8 -4
  53. package/src/sitemap/renderer.ts +137 -57
  54. package/src/utils/legend-svg.ts +44 -20
  55. package/src/utils/parsing.ts +1 -1
  56. package/src/utils/tag-groups.ts +59 -15
  57. package/gallery/fixtures/initiative-status-full.dgmo +0 -46
  58. package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
  59. package/gallery/fixtures/initiative-status.dgmo +0 -9
  60. package/src/initiative-status/collapse.ts +0 -76
  61. package/src/initiative-status/filter.ts +0 -63
  62. package/src/initiative-status/layout.ts +0 -650
  63. package/src/initiative-status/parser.ts +0 -629
  64. package/src/initiative-status/renderer.ts +0 -1199
  65. package/src/initiative-status/types.ts +0 -57
@@ -8,10 +8,7 @@ import { FONT_FAMILY } from '../fonts';
8
8
  import type { PaletteColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
10
  import type { ParsedSitemap } from './types';
11
- import type {
12
- SitemapLayoutResult,
13
- SitemapLegendGroup,
14
- } from './layout';
11
+ import type { SitemapLayoutResult, SitemapLegendGroup } from './layout';
15
12
  import {
16
13
  LEGEND_HEIGHT,
17
14
  LEGEND_PILL_PAD,
@@ -63,7 +60,11 @@ const LEGEND_FIXED_GAP = 8; // gap between fixed legend and scaled diagram — l
63
60
  // Color helpers
64
61
  // ============================================================
65
62
 
66
- function nodeFill(palette: PaletteColors, isDark: boolean, nodeColor?: string): string {
63
+ function nodeFill(
64
+ palette: PaletteColors,
65
+ isDark: boolean,
66
+ nodeColor?: string
67
+ ): string {
67
68
  const color = nodeColor ?? palette.primary;
68
69
  return mix(color, isDark ? palette.surface : palette.bg, 25);
69
70
  }
@@ -72,7 +73,11 @@ function nodeStroke(_palette: PaletteColors, nodeColor?: string): string {
72
73
  return nodeColor ?? _palette.primary;
73
74
  }
74
75
 
75
- function containerFill(palette: PaletteColors, isDark: boolean, nodeColor?: string): string {
76
+ function containerFill(
77
+ palette: PaletteColors,
78
+ isDark: boolean,
79
+ nodeColor?: string
80
+ ): string {
76
81
  if (nodeColor) {
77
82
  return mix(nodeColor, isDark ? palette.surface : palette.bg, 10);
78
83
  }
@@ -87,7 +92,8 @@ function containerStroke(palette: PaletteColors, nodeColor?: string): string {
87
92
  // Curve generator
88
93
  // ============================================================
89
94
 
90
- const lineGenerator = d3Shape.line<{ x: number; y: number }>()
95
+ const lineGenerator = d3Shape
96
+ .line<{ x: number; y: number }>()
91
97
  .x((d) => d.x)
92
98
  .y((d) => d.y)
93
99
  .curve(d3Shape.curveBasis);
@@ -107,7 +113,7 @@ export function renderSitemap(
107
113
  onClickItem?: (lineNumber: number) => void,
108
114
  exportDims?: { width?: number; height?: number },
109
115
  activeTagGroup?: string | null,
110
- hiddenAttributes?: Set<string>,
116
+ hiddenAttributes?: Set<string>
111
117
  ): void {
112
118
  // Clear existing content
113
119
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -139,7 +145,8 @@ export function renderSitemap(
139
145
  // Remove the legend space from diagram height — legend is rendered separately
140
146
  diagramH -= layoutLegendShift;
141
147
  }
142
- const availH = height - DIAGRAM_PADDING * 2 - fixedReserveTop - fixedReserveBottom;
148
+ const availH =
149
+ height - DIAGRAM_PADDING * 2 - fixedReserveTop - fixedReserveBottom;
143
150
  const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
144
151
  const scaleY = availH / diagramH;
145
152
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
@@ -236,7 +243,10 @@ export function renderSitemap(
236
243
  for (const group of parsed.tagGroups) {
237
244
  displayNames.set(group.name.toLowerCase(), group.name);
238
245
  for (const entry of group.entries) {
239
- tagColors.set(`${group.name.toLowerCase()}:${entry.value.toLowerCase()}`, entry.color);
246
+ tagColors.set(
247
+ `${group.name.toLowerCase()}:${entry.value.toLowerCase()}`,
248
+ entry.color
249
+ );
240
250
  }
241
251
  }
242
252
 
@@ -257,7 +267,9 @@ export function renderSitemap(
257
267
  }
258
268
 
259
269
  if (onClickItem) {
260
- cG.style('cursor', 'pointer').on('click', () => onClickItem(c.lineNumber));
270
+ cG.style('cursor', 'pointer').on('click', () =>
271
+ onClickItem(c.lineNumber)
272
+ );
261
273
  }
262
274
 
263
275
  // Tag metadata for legend hover dimming
@@ -284,7 +296,10 @@ export function renderSitemap(
284
296
  // Container label
285
297
  cG.append('text')
286
298
  .attr('x', c.width / 2)
287
- .attr('y', CONTAINER_HEADER_HEIGHT / 2 + CONTAINER_LABEL_FONT_SIZE / 2 - 2)
299
+ .attr(
300
+ 'y',
301
+ CONTAINER_HEADER_HEIGHT / 2 + CONTAINER_LABEL_FONT_SIZE / 2 - 2
302
+ )
288
303
  .attr('text-anchor', 'middle')
289
304
  .attr('fill', palette.text)
290
305
  .attr('font-size', CONTAINER_LABEL_FONT_SIZE)
@@ -294,7 +309,9 @@ export function renderSitemap(
294
309
  // Container metadata
295
310
  const metaEntries = Object.entries(c.metadata);
296
311
  if (metaEntries.length > 0) {
297
- const metaDisplayKeys = metaEntries.map(([k]) => displayNames.get(k) ?? k);
312
+ const metaDisplayKeys = metaEntries.map(
313
+ ([k]) => displayNames.get(k) ?? k
314
+ );
298
315
  const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
299
316
  const valueX = 10 + (maxKeyLen + 2) * (CONTAINER_META_FONT_SIZE * 0.6);
300
317
  const metaStartY = CONTAINER_HEADER_HEIGHT + CONTAINER_META_FONT_SIZE - 2;
@@ -303,7 +320,8 @@ export function renderSitemap(
303
320
  const [key, value] = metaEntries[i];
304
321
  const displayKey = metaDisplayKeys[i];
305
322
  const rowY = metaStartY + i * CONTAINER_META_LINE_HEIGHT;
306
- const valColor = tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
323
+ const valColor =
324
+ tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
307
325
 
308
326
  cG.append('text')
309
327
  .attr('x', 10)
@@ -324,9 +342,11 @@ export function renderSitemap(
324
342
  // Collapsed accent bar
325
343
  if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
326
344
  const clipId = `clip-${c.nodeId}`;
327
- cG.append('clipPath').attr('id', clipId)
345
+ cG.append('clipPath')
346
+ .attr('id', clipId)
328
347
  .append('rect')
329
- .attr('width', c.width).attr('height', c.height)
348
+ .attr('width', c.width)
349
+ .attr('height', c.height)
330
350
  .attr('rx', CONTAINER_RADIUS);
331
351
  cG.append('rect')
332
352
  .attr('y', c.height - COLLAPSE_BAR_HEIGHT)
@@ -410,14 +430,17 @@ export function renderSitemap(
410
430
  .attr('data-line-number', String(node.lineNumber)) as GSelection;
411
431
 
412
432
  if (node.hasChildren) {
413
- nodeG.attr('data-node-toggle', node.id)
433
+ nodeG
434
+ .attr('data-node-toggle', node.id)
414
435
  .attr('tabindex', '0')
415
436
  .attr('role', 'button')
416
437
  .attr('aria-expanded', String(!node.hiddenCount));
417
438
  }
418
439
 
419
440
  if (onClickItem) {
420
- nodeG.style('cursor', 'pointer').on('click', () => onClickItem(node.lineNumber));
441
+ nodeG
442
+ .style('cursor', 'pointer')
443
+ .on('click', () => onClickItem(node.lineNumber));
421
444
  }
422
445
 
423
446
  // Tag metadata for legend hover dimming
@@ -431,7 +454,8 @@ export function renderSitemap(
431
454
  const stroke = nodeStroke(palette, node.color);
432
455
 
433
456
  // Card background
434
- nodeG.append('rect')
457
+ nodeG
458
+ .append('rect')
435
459
  .attr('x', 0)
436
460
  .attr('y', 0)
437
461
  .attr('width', node.width)
@@ -442,7 +466,8 @@ export function renderSitemap(
442
466
  .attr('stroke-width', NODE_STROKE_WIDTH);
443
467
 
444
468
  // Label
445
- nodeG.append('text')
469
+ nodeG
470
+ .append('text')
446
471
  .attr('x', node.width / 2)
447
472
  .attr('y', HEADER_HEIGHT / 2 + LABEL_FONT_SIZE / 2 - 2)
448
473
  .attr('text-anchor', 'middle')
@@ -455,7 +480,8 @@ export function renderSitemap(
455
480
  const metaEntries = Object.entries(node.metadata);
456
481
  if (metaEntries.length > 0) {
457
482
  // Separator line
458
- nodeG.append('line')
483
+ nodeG
484
+ .append('line')
459
485
  .attr('x1', 0)
460
486
  .attr('y1', HEADER_HEIGHT)
461
487
  .attr('x2', node.width)
@@ -463,24 +489,30 @@ export function renderSitemap(
463
489
  .attr('stroke', stroke)
464
490
  .attr('stroke-opacity', 0.3);
465
491
 
466
- const metaDisplayKeys = metaEntries.map(([k]) => displayNames.get(k) ?? k);
492
+ const metaDisplayKeys = metaEntries.map(
493
+ ([k]) => displayNames.get(k) ?? k
494
+ );
467
495
  const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
468
496
  const valueX = 10 + (maxKeyLen + 2) * (META_FONT_SIZE * 0.6);
469
497
 
470
498
  for (let i = 0; i < metaEntries.length; i++) {
471
499
  const [key, value] = metaEntries[i];
472
500
  const displayKey = metaDisplayKeys[i];
473
- const rowY = HEADER_HEIGHT + SEPARATOR_GAP + (i + 1) * META_LINE_HEIGHT - 4;
474
- const valColor = tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
501
+ const rowY =
502
+ HEADER_HEIGHT + SEPARATOR_GAP + (i + 1) * META_LINE_HEIGHT - 4;
503
+ const valColor =
504
+ tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
475
505
 
476
- nodeG.append('text')
506
+ nodeG
507
+ .append('text')
477
508
  .attr('x', 10)
478
509
  .attr('y', rowY)
479
510
  .attr('fill', palette.textMuted)
480
511
  .attr('font-size', META_FONT_SIZE)
481
512
  .text(`${displayKey}:`);
482
513
 
483
- nodeG.append('text')
514
+ nodeG
515
+ .append('text')
484
516
  .attr('x', valueX)
485
517
  .attr('y', rowY)
486
518
  .attr('fill', valColor)
@@ -492,11 +524,15 @@ export function renderSitemap(
492
524
  // Collapsed accent bar
493
525
  if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
494
526
  const clipId = `clip-${node.id}`;
495
- nodeG.append('clipPath').attr('id', clipId)
527
+ nodeG
528
+ .append('clipPath')
529
+ .attr('id', clipId)
496
530
  .append('rect')
497
- .attr('width', node.width).attr('height', node.height)
531
+ .attr('width', node.width)
532
+ .attr('height', node.height)
498
533
  .attr('rx', CARD_RADIUS);
499
- nodeG.append('rect')
534
+ nodeG
535
+ .append('rect')
500
536
  .attr('y', node.height - COLLAPSE_BAR_HEIGHT)
501
537
  .attr('width', node.width)
502
538
  .attr('height', COLLAPSE_BAR_HEIGHT)
@@ -509,7 +545,15 @@ export function renderSitemap(
509
545
  // --- Render legend ---
510
546
  if (exportDims && hasLegend) {
511
547
  // Export mode: render inside the scaled content group
512
- renderLegend(contentG, layout.legend, palette, isDark, activeTagGroup, undefined, hiddenAttributes);
548
+ renderLegend(
549
+ contentG,
550
+ layout.legend,
551
+ palette,
552
+ isDark,
553
+ activeTagGroup,
554
+ undefined,
555
+ hiddenAttributes
556
+ );
513
557
  }
514
558
 
515
559
  // --- Fixed title + legend (appended AFTER mainG so they paint on top
@@ -546,7 +590,15 @@ export function renderSitemap(
546
590
  if (activeTagGroup) {
547
591
  legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
548
592
  }
549
- renderLegend(legendParent, layout.legend, palette, isDark, activeTagGroup, width, hiddenAttributes);
593
+ renderLegend(
594
+ legendParent,
595
+ layout.legend,
596
+ palette,
597
+ isDark,
598
+ activeTagGroup,
599
+ width,
600
+ hiddenAttributes
601
+ );
550
602
  }
551
603
  }
552
604
 
@@ -561,13 +613,16 @@ function renderLegend(
561
613
  isDark: boolean,
562
614
  activeTagGroup?: string | null,
563
615
  fixedWidth?: number,
564
- hiddenAttributes?: Set<string>,
616
+ hiddenAttributes?: Set<string>
565
617
  ): void {
566
618
  if (legendGroups.length === 0) return;
567
619
 
568
- const visibleGroups = activeTagGroup != null
569
- ? legendGroups.filter((g) => g.name.toLowerCase() === activeTagGroup.toLowerCase())
570
- : legendGroups;
620
+ const visibleGroups =
621
+ activeTagGroup != null
622
+ ? legendGroups.filter(
623
+ (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
624
+ )
625
+ : legendGroups;
571
626
 
572
627
  const groupBg = isDark
573
628
  ? mix(palette.surface, palette.bg, 50)
@@ -591,7 +646,8 @@ function renderLegend(
591
646
 
592
647
  for (const group of visibleGroups) {
593
648
  const isActive = activeTagGroup != null;
594
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
649
+ const pillW =
650
+ measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
595
651
 
596
652
  const gX = fixedPositions?.get(group.name) ?? group.x;
597
653
  const gY = fixedPositions ? 0 : group.y;
@@ -605,7 +661,8 @@ function renderLegend(
605
661
 
606
662
  // Outer capsule background (active/expanded only)
607
663
  if (isActive) {
608
- legendG.append('rect')
664
+ legendG
665
+ .append('rect')
609
666
  .attr('width', group.width)
610
667
  .attr('height', LEGEND_HEIGHT)
611
668
  .attr('rx', LEGEND_HEIGHT / 2)
@@ -613,11 +670,12 @@ function renderLegend(
613
670
  }
614
671
 
615
672
  const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
616
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
617
- const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
673
+ const pillYOff = LEGEND_CAPSULE_PAD;
674
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
618
675
 
619
676
  // Pill background
620
- legendG.append('rect')
677
+ legendG
678
+ .append('rect')
621
679
  .attr('x', pillXOff)
622
680
  .attr('y', pillYOff)
623
681
  .attr('width', pillW)
@@ -627,7 +685,8 @@ function renderLegend(
627
685
 
628
686
  // Active pill border
629
687
  if (isActive) {
630
- legendG.append('rect')
688
+ legendG
689
+ .append('rect')
631
690
  .attr('x', pillXOff)
632
691
  .attr('y', pillYOff)
633
692
  .attr('width', pillW)
@@ -639,7 +698,8 @@ function renderLegend(
639
698
  }
640
699
 
641
700
  // Pill text
642
- legendG.append('text')
701
+ legendG
702
+ .append('text')
643
703
  .attr('x', pillXOff + pillW / 2)
644
704
  .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
645
705
  .attr('font-size', LEGEND_PILL_FONT_SIZE)
@@ -656,14 +716,16 @@ function renderLegend(
656
716
  const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
657
717
  const hitPad = 6;
658
718
 
659
- const eyeG = legendG.append('g')
719
+ const eyeG = legendG
720
+ .append('g')
660
721
  .attr('class', 'sitemap-legend-eye')
661
722
  .attr('data-legend-visibility', groupKey)
662
723
  .style('cursor', 'pointer')
663
724
  .attr('opacity', isHidden ? 0.4 : 0.7);
664
725
 
665
726
  // Transparent hit area for easier clicking
666
- eyeG.append('rect')
727
+ eyeG
728
+ .append('rect')
667
729
  .attr('x', eyeX - hitPad)
668
730
  .attr('y', eyeY - hitPad)
669
731
  .attr('width', LEGEND_EYE_SIZE + hitPad * 2)
@@ -671,7 +733,8 @@ function renderLegend(
671
733
  .attr('fill', 'transparent')
672
734
  .attr('pointer-events', 'all');
673
735
 
674
- eyeG.append('path')
736
+ eyeG
737
+ .append('path')
675
738
  .attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
676
739
  .attr('transform', `translate(${eyeX}, ${eyeY})`)
677
740
  .attr('fill', 'none')
@@ -683,28 +746,35 @@ function renderLegend(
683
746
 
684
747
  // Entries (active/expanded only)
685
748
  if (isActive) {
686
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
749
+ const eyeShift =
750
+ fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
687
751
  let entryX = pillXOff + pillW + 4 + eyeShift;
688
752
  for (const entry of group.entries) {
689
- const entryG = legendG.append('g')
753
+ const entryG = legendG
754
+ .append('g')
690
755
  .attr('data-legend-entry', entry.value.toLowerCase())
691
756
  .style('cursor', 'pointer');
692
757
 
693
- entryG.append('circle')
758
+ entryG
759
+ .append('circle')
694
760
  .attr('cx', entryX + LEGEND_DOT_R)
695
761
  .attr('cy', LEGEND_HEIGHT / 2)
696
762
  .attr('r', LEGEND_DOT_R)
697
763
  .attr('fill', entry.color);
698
764
 
699
765
  const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
700
- entryG.append('text')
766
+ entryG
767
+ .append('text')
701
768
  .attr('x', textX)
702
769
  .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
703
770
  .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
704
771
  .attr('fill', palette.textMuted)
705
772
  .text(entry.value);
706
773
 
707
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
774
+ entryX =
775
+ textX +
776
+ measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
777
+ LEGEND_ENTRY_TRAIL;
708
778
  }
709
779
  }
710
780
  }
@@ -717,7 +787,7 @@ function renderLegend(
717
787
  export async function renderSitemapForExport(
718
788
  content: string,
719
789
  theme: 'light' | 'dark' | 'transparent',
720
- palette?: PaletteColors,
790
+ palette?: PaletteColors
721
791
  ): Promise<string> {
722
792
  const { parseSitemap } = await import('./parser');
723
793
  const { layoutSitemap } = await import('./layout');
@@ -725,7 +795,8 @@ export async function renderSitemapForExport(
725
795
  const { injectBranding } = await import('../branding');
726
796
 
727
797
  const isDark = theme === 'dark';
728
- const effectivePalette = palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
798
+ const effectivePalette =
799
+ palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
729
800
 
730
801
  const parsed = parseSitemap(content, effectivePalette);
731
802
  if (parsed.error || parsed.roots.length === 0) return '';
@@ -744,10 +815,18 @@ export async function renderSitemapForExport(
744
815
  container.style.left = '-9999px';
745
816
  document.body.appendChild(container);
746
817
 
747
- renderSitemap(container, parsed, sitemapLayout, effectivePalette, isDark, undefined, {
748
- width: exportWidth,
749
- height: exportHeight,
750
- });
818
+ renderSitemap(
819
+ container,
820
+ parsed,
821
+ sitemapLayout,
822
+ effectivePalette,
823
+ isDark,
824
+ undefined,
825
+ {
826
+ width: exportWidth,
827
+ height: exportHeight,
828
+ }
829
+ );
751
830
 
752
831
  const svgEl = container.querySelector('svg');
753
832
  if (!svgEl) {
@@ -766,6 +845,7 @@ export async function renderSitemapForExport(
766
845
  const svgHtml = svgEl.outerHTML;
767
846
  document.body.removeChild(container);
768
847
 
769
- const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
848
+ const brandColor =
849
+ theme === 'transparent' ? '#888' : effectivePalette.textMuted;
770
850
  return injectBranding(svgHtml, brandColor);
771
851
  }
@@ -48,7 +48,11 @@ export interface LegendRenderResult {
48
48
  // ── Helpers ──────────────────────────────────────────────────
49
49
 
50
50
  function esc(s: string): string {
51
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
51
+ return s
52
+ .replace(/&/g, '&amp;')
53
+ .replace(/</g, '&lt;')
54
+ .replace(/>/g, '&gt;')
55
+ .replace(/"/g, '&quot;');
52
56
  }
53
57
 
54
58
  function pillWidth(name: string): number {
@@ -58,12 +62,20 @@ function pillWidth(name: string): number {
58
62
  function entriesWidth(entries: Array<{ value: string }>): number {
59
63
  let w = 0;
60
64
  for (const e of entries) {
61
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
65
+ w +=
66
+ LEGEND_DOT_R * 2 +
67
+ LEGEND_ENTRY_DOT_GAP +
68
+ measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) +
69
+ LEGEND_ENTRY_TRAIL;
62
70
  }
63
71
  return w;
64
72
  }
65
73
 
66
- function groupTotalWidth(name: string, entries: Array<{ value: string }>, isActive: boolean): number {
74
+ function groupTotalWidth(
75
+ name: string,
76
+ entries: Array<{ value: string }>,
77
+ isActive: boolean
78
+ ): number {
67
79
  const pw = pillWidth(name);
68
80
  if (!isActive) return pw;
69
81
  return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
@@ -73,7 +85,7 @@ function groupTotalWidth(name: string, entries: Array<{ value: string }>, isActi
73
85
 
74
86
  export function renderLegendSvg(
75
87
  groups: LegendGroupData[],
76
- options: LegendRenderOptions,
88
+ options: LegendRenderOptions
77
89
  ): LegendRenderResult {
78
90
  if (groups.length === 0) return { svg: '', height: 0, width: 0 };
79
91
 
@@ -86,7 +98,8 @@ export function renderLegendSvg(
86
98
  const items = groups
87
99
  .filter((g) => g.entries.length > 0)
88
100
  .map((g) => {
89
- const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
101
+ const isActive =
102
+ !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
90
103
  const pw = pillWidth(g.name);
91
104
  const tw = groupTotalWidth(g.name, g.entries, isActive);
92
105
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
@@ -94,11 +107,17 @@ export function renderLegendSvg(
94
107
 
95
108
  if (items.length === 0) return { svg: '', height: 0, width: 0 };
96
109
 
97
- const totalWidth = items.reduce((s, it) => s + it.totalWidth, 0) + (items.length - 1) * LEGEND_GROUP_GAP;
110
+ const totalWidth =
111
+ items.reduce((s, it) => s + it.totalWidth, 0) +
112
+ (items.length - 1) * LEGEND_GROUP_GAP;
98
113
 
99
114
  // Center over the plot area when grid offsets are provided, otherwise full container
100
- const plotLeft = options.gridLeftPct ? (containerWidth * options.gridLeftPct) / 100 : 0;
101
- const plotRight = options.gridRightPct ? containerWidth - (containerWidth * options.gridRightPct) / 100 : containerWidth;
115
+ const plotLeft = options.gridLeftPct
116
+ ? (containerWidth * options.gridLeftPct) / 100
117
+ : 0;
118
+ const plotRight = options.gridRightPct
119
+ ? containerWidth - (containerWidth * options.gridRightPct) / 100
120
+ : containerWidth;
102
121
  const plotWidth = plotRight - plotLeft;
103
122
  let x = Math.max(0, plotLeft + (plotWidth - totalWidth) / 2);
104
123
 
@@ -112,29 +131,29 @@ export function renderLegendSvg(
112
131
  // Outer capsule (active only)
113
132
  if (item.isActive) {
114
133
  inner.push(
115
- `<rect width="${item.totalWidth}" height="${LEGEND_HEIGHT}" rx="${LEGEND_HEIGHT / 2}" fill="${esc(groupBg)}"/>`,
134
+ `<rect width="${item.totalWidth}" height="${LEGEND_HEIGHT}" rx="${LEGEND_HEIGHT / 2}" fill="${esc(groupBg)}"/>`
116
135
  );
117
136
  }
118
137
 
119
138
  const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
120
- const pillYOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
121
- const h = item.isActive ? pillH : LEGEND_HEIGHT;
139
+ const pillYOff = LEGEND_CAPSULE_PAD;
140
+ const h = pillH;
122
141
 
123
142
  // Pill background
124
143
  inner.push(
125
- `<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="${esc(item.isActive ? palette.bg : groupBg)}"/>`,
144
+ `<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="${esc(item.isActive ? palette.bg : groupBg)}"/>`
126
145
  );
127
146
 
128
147
  // Active pill border
129
148
  if (item.isActive) {
130
149
  inner.push(
131
- `<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="none" stroke="${esc(mix(palette.textMuted, palette.bg, 50))}" stroke-width="0.75"/>`,
150
+ `<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="none" stroke="${esc(mix(palette.textMuted, palette.bg, 50))}" stroke-width="0.75"/>`
132
151
  );
133
152
  }
134
153
 
135
154
  // Pill text
136
155
  inner.push(
137
- `<text x="${pillXOff + item.pillWidth / 2}" y="${LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2}" font-size="${LEGEND_PILL_FONT_SIZE}" font-weight="500" fill="${esc(item.isActive ? palette.text : palette.textMuted)}" text-anchor="middle" font-family="${esc(FONT_FAMILY)}">${esc(item.group.name)}</text>`,
156
+ `<text x="${pillXOff + item.pillWidth / 2}" y="${LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2}" font-size="${LEGEND_PILL_FONT_SIZE}" font-weight="500" fill="${esc(item.isActive ? palette.text : palette.textMuted)}" text-anchor="middle" font-family="${esc(FONT_FAMILY)}">${esc(item.group.name)}</text>`
138
157
  );
139
158
 
140
159
  // Entry dots + labels (active only)
@@ -144,23 +163,28 @@ export function renderLegendSvg(
144
163
  const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
145
164
  inner.push(
146
165
  `<g data-legend-entry="${esc(entry.value.toLowerCase())}" data-series-name="${esc(entry.value)}" style="cursor:pointer">` +
147
- `<circle cx="${entryX + LEGEND_DOT_R}" cy="${LEGEND_HEIGHT / 2}" r="${LEGEND_DOT_R}" fill="${esc(entry.color)}"/>` +
148
- `<text x="${textX}" y="${LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1}" font-size="${LEGEND_ENTRY_FONT_SIZE}" fill="${esc(palette.textMuted)}" font-family="${esc(FONT_FAMILY)}">${esc(entry.value)}</text>` +
149
- `</g>`,
166
+ `<circle cx="${entryX + LEGEND_DOT_R}" cy="${LEGEND_HEIGHT / 2}" r="${LEGEND_DOT_R}" fill="${esc(entry.color)}"/>` +
167
+ `<text x="${textX}" y="${LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1}" font-size="${LEGEND_ENTRY_FONT_SIZE}" fill="${esc(palette.textMuted)}" font-family="${esc(FONT_FAMILY)}">${esc(entry.value)}</text>` +
168
+ `</g>`
150
169
  );
151
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
170
+ entryX =
171
+ textX +
172
+ measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
173
+ LEGEND_ENTRY_TRAIL;
152
174
  }
153
175
  }
154
176
 
155
177
  parts.push(
156
- `<g transform="translate(${x},0)" data-legend-group="${esc(groupKey)}" style="cursor:pointer">${inner.join('')}</g>`,
178
+ `<g transform="translate(${x},0)" data-legend-group="${esc(groupKey)}" style="cursor:pointer">${inner.join('')}</g>`
157
179
  );
158
180
 
159
181
  x += item.totalWidth + LEGEND_GROUP_GAP;
160
182
  }
161
183
 
162
184
  const classAttr = className ? ` class="${esc(className)}"` : '';
163
- const activeAttr = activeGroup ? ` data-legend-active="${esc(activeGroup.toLowerCase())}"` : '';
185
+ const activeAttr = activeGroup
186
+ ? ` data-legend-active="${esc(activeGroup.toLowerCase())}"`
187
+ : '';
164
188
  const svg = `<g${classAttr}${activeAttr}>${parts.join('')}</g>`;
165
189
 
166
190
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
@@ -41,11 +41,11 @@ export const ALL_CHART_TYPES = new Set([
41
41
  'org',
42
42
  'kanban',
43
43
  'c4',
44
- 'initiative-status',
45
44
  'state',
46
45
  'sitemap',
47
46
  'infra',
48
47
  'gantt',
48
+ 'boxes-and-lines',
49
49
  ]);
50
50
 
51
51
  /** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */