@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
package/src/completion.ts CHANGED
@@ -273,7 +273,6 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
273
273
  }),
274
274
  ],
275
275
  ['c4', withGlobals()],
276
- ['initiative-status', withGlobals()],
277
276
  [
278
277
  'state',
279
278
  withGlobals({
@@ -313,6 +312,14 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
313
312
  dependencies: { description: 'Show dependencies' },
314
313
  }),
315
314
  ],
315
+ [
316
+ 'boxes-and-lines',
317
+ withGlobals({
318
+ direction: { description: 'Layout direction', values: ['LR', 'TB'] },
319
+ 'active-tag': { description: 'Active tag group name' },
320
+ hide: { description: 'Hide tag:value pairs' },
321
+ }),
322
+ ],
316
323
  ]);
317
324
 
318
325
  // ============================================================
@@ -351,11 +358,11 @@ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
351
358
  org: 'Organization chart',
352
359
  kanban: 'Kanban board',
353
360
  c4: 'C4 architecture diagram',
354
- 'initiative-status': 'Initiative status diagram',
355
361
  state: 'State diagram',
356
362
  sitemap: 'Sitemap diagram',
357
363
  infra: 'Infrastructure diagram',
358
364
  gantt: 'Gantt chart',
365
+ 'boxes-and-lines': 'Boxes and lines diagram',
359
366
  };
360
367
 
361
368
  /** All chart types with descriptions, for chart type autocomplete. Excludes `multi-line` alias. */
@@ -481,28 +488,12 @@ export const PIPE_METADATA = new Map<
481
488
  },
482
489
  ],
483
490
  [
484
- 'initiative-status',
491
+ 'boxes-and-lines',
485
492
  {
486
493
  node: {
487
- done: { description: 'Completed' },
488
- doing: { description: 'In progress' },
489
- todo: { description: 'Not started' },
490
- blocked: { description: 'Blocked' },
491
- na: { description: 'Not applicable' },
492
- wip: { description: 'Work in progress (alias for doing)' },
493
- paused: { description: 'Paused (alias for blocked)' },
494
- waiting: { description: 'Waiting (alias for blocked)' },
495
- },
496
- edge: {
497
- done: { description: 'Completed' },
498
- doing: { description: 'In progress' },
499
- todo: { description: 'Not started' },
500
- blocked: { description: 'Blocked' },
501
- na: { description: 'Not applicable' },
502
- wip: { description: 'Work in progress (alias for doing)' },
503
- paused: { description: 'Paused (alias for blocked)' },
504
- waiting: { description: 'Waiting (alias for blocked)' },
494
+ description: { description: 'Node description text' },
505
495
  },
496
+ edge: {},
506
497
  },
507
498
  ],
508
499
  ]);
@@ -935,12 +926,12 @@ function extractGanttSymbols(docText: string): DiagramSymbols {
935
926
  }
936
927
 
937
928
  // ============================================================
938
- // Initiative-status extractor
929
+ // Boxes-and-lines extractor
939
930
  // ============================================================
940
931
 
941
- const IS_ARROW_RE = /^(\S+)\s+(?:-.*)?->\s+(\S+)/;
932
+ const BL_ARROW_RE = /^(\S+)\s+(?:-.*)?(?:->|<->)\s+(\S+)/;
942
933
 
943
- function extractInitiativeStatusSymbols(docText: string): DiagramSymbols {
934
+ function extractBoxesAndLinesSymbols(docText: string): DiagramSymbols {
944
935
  const lines = docText.split('\n');
945
936
  const entities: string[] = [];
946
937
  let pastFirstLine = false;
@@ -968,8 +959,11 @@ function extractInitiativeStatusSymbols(docText: string): DiagramSymbols {
968
959
  inTagBlock = false;
969
960
  }
970
961
 
971
- // Edge lines: Source -> Target or Source -label-> Target
972
- const arrowMatch = trimmed.match(IS_ARROW_RE);
962
+ // Skip groups
963
+ if (/^\[.+?\]/.test(trimmed)) continue;
964
+
965
+ // Edge lines
966
+ const arrowMatch = trimmed.match(BL_ARROW_RE);
973
967
  if (arrowMatch) {
974
968
  const src = arrowMatch[1].split('|')[0].trim();
975
969
  const dst = arrowMatch[2].split('|')[0].trim();
@@ -978,14 +972,12 @@ function extractInitiativeStatusSymbols(docText: string): DiagramSymbols {
978
972
  continue;
979
973
  }
980
974
 
981
- // Node lines: Label | status or just Label (at root indent)
982
- if (indent === 0) {
983
- const label = trimmed.split('|')[0].trim();
984
- if (label && !entities.includes(label)) entities.push(label);
985
- }
975
+ // Node lines
976
+ const label = trimmed.split('|')[0].split('[')[0].trim();
977
+ if (label && !entities.includes(label)) entities.push(label);
986
978
  }
987
979
 
988
- return { kind: 'initiative-status', entities, keywords: [] };
980
+ return { kind: 'boxes-and-lines', entities, keywords: [] };
989
981
  }
990
982
 
991
983
  // ============================================================
@@ -1001,4 +993,4 @@ registerExtractor('state', extractStateSymbols);
1001
993
  registerExtractor('sitemap', extractSitemapSymbols);
1002
994
  registerExtractor('c4', extractC4Symbols);
1003
995
  registerExtractor('gantt', extractGanttSymbols);
1004
- registerExtractor('initiative-status', extractInitiativeStatusSymbols);
996
+ registerExtractor('boxes-and-lines', extractBoxesAndLinesSymbols);
package/src/d3.ts CHANGED
@@ -193,6 +193,7 @@ import {
193
193
  matchTagBlockHeading,
194
194
  validateTagValues,
195
195
  resolveTagColor,
196
+ stripDefaultModifier,
196
197
  } from './utils/tag-groups';
197
198
  import type { TagGroup } from './utils/tag-groups';
198
199
  import {
@@ -570,16 +571,16 @@ export function parseVisualization(
570
571
  }
571
572
  }
572
573
 
573
- // Timeline tag group entries (indented under tag: heading)
574
+ // Timeline tag group entries (indented under tag heading)
574
575
  if (currentTimelineTagGroup && indent > 0) {
575
- const trimmedEntry = line;
576
- const isDefault = /\bdefault\s*$/.test(trimmedEntry);
577
- const entryText = isDefault
578
- ? trimmedEntry.replace(/\s+default\s*$/, '').trim()
579
- : trimmedEntry;
576
+ const { text: entryText, isDefault } = stripDefaultModifier(line);
580
577
  const { label, color } = extractColor(entryText, palette);
581
578
  if (color) {
582
- if (isDefault) currentTimelineTagGroup.defaultValue = label;
579
+ if (isDefault) {
580
+ currentTimelineTagGroup.defaultValue = label;
581
+ } else if (currentTimelineTagGroup.entries.length === 0) {
582
+ currentTimelineTagGroup.defaultValue = label;
583
+ }
583
584
  currentTimelineTagGroup.entries.push({
584
585
  value: label,
585
586
  color,
@@ -1014,9 +1015,9 @@ export function parseVisualization(
1014
1015
  continue;
1015
1016
  }
1016
1017
 
1017
- // Data points: Label x, y
1018
+ // Data points: Label x, y OR Label x y
1018
1019
  const pointMatch = line.match(
1019
- /^(.+?)\s+([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
1020
+ /^(.+?)\s+([0-9]*\.?[0-9]+)\s*[,\s]\s*([0-9]*\.?[0-9]+)\s*$/
1020
1021
  );
1021
1022
  if (pointMatch) {
1022
1023
  const label = pointMatch[1].trim();
@@ -5047,8 +5048,8 @@ export function renderTimeline(
5047
5048
  }
5048
5049
 
5049
5050
  const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
5050
- const pillYOff = isActive ? LG_CAPSULE_PAD : 0;
5051
- const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
5051
+ const pillYOff = LG_CAPSULE_PAD;
5052
+ const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
5052
5053
 
5053
5054
  // Pill background
5054
5055
  gEl
@@ -6856,29 +6857,27 @@ export async function renderForExport(
6856
6857
  return finalizeSvgExport(container, theme, effectivePalette, options);
6857
6858
  }
6858
6859
 
6859
- if (detectedType === 'initiative-status') {
6860
- const { parseInitiativeStatus } =
6861
- await import('./initiative-status/parser');
6862
- const { layoutInitiativeStatus } =
6863
- await import('./initiative-status/layout');
6864
- const { renderInitiativeStatus } =
6865
- await import('./initiative-status/renderer');
6860
+ if (detectedType === 'boxes-and-lines') {
6861
+ const { parseBoxesAndLines } = await import('./boxes-and-lines/parser');
6862
+ const { layoutBoxesAndLines } = await import('./boxes-and-lines/layout');
6863
+ const { renderBoxesAndLinesForExport } =
6864
+ await import('./boxes-and-lines/renderer');
6866
6865
 
6867
6866
  const effectivePalette = await resolveExportPalette(theme, palette);
6868
- const isParsed = parseInitiativeStatus(content);
6869
- if (isParsed.error || isParsed.nodes.length === 0) return '';
6867
+ const blParsed = parseBoxesAndLines(content);
6868
+ if (blParsed.error || blParsed.nodes.length === 0) return '';
6870
6869
 
6871
- const isLayout = layoutInitiativeStatus(isParsed);
6870
+ const blLayout = layoutBoxesAndLines(blParsed);
6872
6871
  const PADDING = 20;
6873
- const titleOffset = isParsed.title ? 40 : 0;
6874
- const exportWidth = isLayout.width + PADDING * 2;
6875
- const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
6872
+ const titleOffset = blParsed.title ? 40 : 0;
6873
+ const exportWidth = blLayout.width + PADDING * 2;
6874
+ const exportHeight = blLayout.height + PADDING * 2 + titleOffset;
6876
6875
  const container = createExportContainer(exportWidth, exportHeight);
6877
6876
 
6878
- renderInitiativeStatus(
6877
+ renderBoxesAndLinesForExport(
6879
6878
  container,
6880
- isParsed,
6881
- isLayout,
6879
+ blParsed,
6880
+ blLayout,
6882
6881
  effectivePalette,
6883
6882
  theme === 'dark',
6884
6883
  { exportDims: { width: exportWidth, height: exportHeight } }
@@ -13,13 +13,10 @@ import { parseVisualization } from './d3';
13
13
  import { parseOrg, looksLikeOrg } from './org/parser';
14
14
  import { parseKanban } from './kanban/parser';
15
15
  import { parseC4 } from './c4/parser';
16
- import {
17
- looksLikeInitiativeStatus,
18
- parseInitiativeStatus,
19
- } from './initiative-status/parser';
20
16
  import { looksLikeSitemap, parseSitemap } from './sitemap/parser';
21
17
  import { parseInfra } from './infra/parser';
22
18
  import { parseGantt } from './gantt/parser';
19
+ import { parseBoxesAndLines } from './boxes-and-lines/parser';
23
20
  import { parseFirstLine } from './utils/parsing';
24
21
  import type { DgmoError } from './diagnostics';
25
22
 
@@ -97,7 +94,6 @@ export function parseDgmoChartType(content: string): string | null {
97
94
  if (looksLikeFlowchart(content)) return 'flowchart';
98
95
  if (looksLikeClassDiagram(content)) return 'class';
99
96
  if (looksLikeERDiagram(content)) return 'er';
100
- if (looksLikeInitiativeStatus(content)) return 'initiative-status';
101
97
  if (looksLikeState(content)) return 'state';
102
98
  if (looksLikeSitemap(content)) return 'sitemap';
103
99
  if (looksLikeOrg(content)) return 'org';
@@ -147,11 +143,11 @@ const DIAGRAM_TYPES = new Set([
147
143
  'org',
148
144
  'kanban',
149
145
  'c4',
150
- 'initiative-status',
151
146
  'state',
152
147
  'sitemap',
153
148
  'infra',
154
149
  'gantt',
150
+ 'boxes-and-lines',
155
151
  ]);
156
152
  const EXTENDED_CHART_TYPES = new Set([
157
153
  'scatter',
@@ -226,11 +222,11 @@ const PARSE_DISPATCH = new Map<
226
222
  ['org', (c) => parseOrg(c)],
227
223
  ['kanban', (c) => parseKanban(c)],
228
224
  ['c4', (c) => parseC4(c)],
229
- ['initiative-status', (c) => parseInitiativeStatus(c)],
230
225
  ['state', (c) => parseState(c)],
231
226
  ['sitemap', (c) => parseSitemap(c)],
232
227
  ['infra', (c) => parseInfra(c)],
233
228
  ['gantt', (c) => parseGantt(c)],
229
+ ['boxes-and-lines', (c) => parseBoxesAndLines(c)],
234
230
  ]);
235
231
 
236
232
  /**
package/src/echarts.ts CHANGED
@@ -2632,6 +2632,33 @@ function segmentLabelFormatter(parsed: ParsedChart): string {
2632
2632
 
2633
2633
  // ── Pie / Doughnut ───────────────────────────────────────────
2634
2634
 
2635
+ /**
2636
+ * Compute pie label config: shrink radius and font when labels are long
2637
+ * so nothing gets truncated or wrapped.
2638
+ */
2639
+ function pieLabelLayout(parsed: ParsedChart): {
2640
+ outerRadius: number;
2641
+ fontSize: number;
2642
+ } {
2643
+ const formatter = segmentLabelFormatter(parsed);
2644
+ const total = parsed.data.reduce((s, d) => s + d.value, 0);
2645
+ const maxLen = parsed.data.reduce((mx, d) => {
2646
+ const label = formatter
2647
+ .replace('{b}', d.label)
2648
+ .replace('{c}', String(d.value))
2649
+ .replace('{d}', total > 0 ? ((d.value / total) * 100).toFixed(2) : '0');
2650
+ return Math.max(mx, label.length);
2651
+ }, 0);
2652
+
2653
+ // Shrink radius and font for longer labels so they fit without truncation.
2654
+ // The chart renders in containers of varying width (800px in-app to 1200px CLI),
2655
+ // so we need enough margin for labels at the smallest reasonable container.
2656
+ if (maxLen > 30) return { outerRadius: 38, fontSize: 11 };
2657
+ if (maxLen > 24) return { outerRadius: 45, fontSize: 12 };
2658
+ if (maxLen > 18) return { outerRadius: 55, fontSize: 13 };
2659
+ return { outerRadius: 70, fontSize: 14 };
2660
+ }
2661
+
2635
2662
  function buildPieOption(
2636
2663
  parsed: ParsedChart,
2637
2664
  textColor: string,
@@ -2655,6 +2682,8 @@ function buildPieOption(
2655
2682
  };
2656
2683
  });
2657
2684
 
2685
+ const { outerRadius, fontSize } = pieLabelLayout(parsed);
2686
+
2658
2687
  return {
2659
2688
  ...CHART_BASE,
2660
2689
  ...HIDE_AXES,
@@ -2666,13 +2695,16 @@ function buildPieOption(
2666
2695
  series: [
2667
2696
  {
2668
2697
  type: 'pie',
2669
- radius: isDoughnut ? ['40%', '70%'] : ['0%', '70%'],
2698
+ radius: isDoughnut
2699
+ ? [`${Math.round(outerRadius * 0.57)}%`, `${outerRadius}%`]
2700
+ : ['0%', `${outerRadius}%`],
2670
2701
  data,
2671
2702
  label: {
2672
2703
  position: 'outside',
2673
2704
  formatter: segmentLabelFormatter(parsed),
2674
2705
  color: textColor,
2675
2706
  fontFamily: FONT_FAMILY,
2707
+ fontSize,
2676
2708
  },
2677
2709
  labelLine: { show: true },
2678
2710
  emphasis: EMPHASIS_SELF,
@@ -2790,13 +2822,17 @@ function buildPolarAreaOption(
2790
2822
  {
2791
2823
  type: 'pie',
2792
2824
  roseType: 'radius',
2793
- radius: ['10%', '70%'],
2825
+ radius: (() => {
2826
+ const { outerRadius } = pieLabelLayout(parsed);
2827
+ return [`${Math.round(outerRadius * 0.14)}%`, `${outerRadius}%`];
2828
+ })(),
2794
2829
  data,
2795
2830
  label: {
2796
2831
  position: 'outside',
2797
2832
  formatter: segmentLabelFormatter(parsed),
2798
2833
  color: textColor,
2799
2834
  fontFamily: FONT_FAMILY,
2835
+ fontSize: pieLabelLayout(parsed).fontSize,
2800
2836
  },
2801
2837
  labelLine: { show: true },
2802
2838
  emphasis: EMPHASIS_SELF,
@@ -8,11 +8,11 @@ export const CHART_TYPES = new Set([
8
8
  'org',
9
9
  'kanban',
10
10
  'c4',
11
- 'initiative-status',
12
11
  'state',
13
12
  'sitemap',
14
13
  'infra',
15
14
  'gantt',
15
+ 'boxes-and-lines',
16
16
  // Data chart types
17
17
  'bar',
18
18
  'line',
@@ -86,6 +86,8 @@ export const DIRECTIVE_KEYWORDS = new Set([
86
86
  'import',
87
87
  'active-tag',
88
88
  'hide',
89
+ 'mode',
90
+ 'direction',
89
91
  // ER
90
92
  'notation',
91
93
  // Class
@@ -146,8 +148,6 @@ export const DIRECTIVE_KEYWORDS = new Set([
146
148
  // Layout
147
149
  'direction-tb',
148
150
  'direction-lr',
149
- // Initiative-status
150
- 'contains',
151
151
  // Data chart metadata
152
152
  'title',
153
153
  'series',
@@ -172,7 +172,7 @@ export const CONTROL_KEYWORDS = new Set([
172
172
  'note',
173
173
  ]);
174
174
 
175
- /** Status keywords — initiative-status, kanban. */
175
+ /** Status keywords — kanban. */
176
176
  export const STATUS_KEYWORDS = new Set([
177
177
  'na',
178
178
  'todo',
@@ -190,21 +190,6 @@ export const MODIFIER_KEYWORDS = new Set([
190
190
  'aka',
191
191
  'position',
192
192
  'default',
193
- // Sequence participant types
194
- 'actor',
195
- 'service',
196
- 'database',
197
- 'queue',
198
- 'cache',
199
- 'gateway',
200
- 'external',
201
- 'networking',
202
- 'frontend',
203
- // C4 element types
204
- 'system',
205
- 'person',
206
- 'container',
207
- 'component',
208
193
  // ER column modifiers
209
194
  'pk',
210
195
  'fk',
package/src/er/parser.ts CHANGED
@@ -8,7 +8,11 @@ import {
8
8
  parseFirstLine,
9
9
  OPTION_NOCOLON_RE,
10
10
  } from '../utils/parsing';
11
- import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
11
+ import {
12
+ matchTagBlockHeading,
13
+ validateTagValues,
14
+ stripDefaultModifier,
15
+ } from '../utils/tag-groups';
12
16
  import type { TagGroup } from '../utils/tag-groups';
13
17
  import type {
14
18
  ParsedERDiagram,
@@ -273,7 +277,8 @@ export function parseERDiagram(
273
277
 
274
278
  // Tag group entries (indented under tag heading)
275
279
  if (currentTagGroup && !contentStarted && indent > 0) {
276
- const { label, color } = extractColor(trimmed, palette);
280
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
281
+ const { label, color } = extractColor(cleanEntry, palette);
277
282
  if (!color) {
278
283
  result.diagnostics.push(
279
284
  makeDgmoError(
@@ -284,8 +289,9 @@ export function parseERDiagram(
284
289
  );
285
290
  continue;
286
291
  }
287
- // First entry becomes the default
288
- if (currentTagGroup.entries.length === 0) {
292
+ if (isDefault) {
293
+ currentTagGroup.defaultValue = label;
294
+ } else if (currentTagGroup.entries.length === 0) {
289
295
  currentTagGroup.defaultValue = label;
290
296
  }
291
297
  currentTagGroup.entries.push({ value: label, color, lineNumber });
@@ -5,7 +5,10 @@
5
5
  import { makeDgmoError, formatDgmoError } from '../diagnostics';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
  import type { TagGroup } from '../utils/tag-groups';
8
- import { matchTagBlockHeading } from '../utils/tag-groups';
8
+ import {
9
+ matchTagBlockHeading,
10
+ stripDefaultModifier,
11
+ } from '../utils/tag-groups';
9
12
  import {
10
13
  measureIndent,
11
14
  extractColor,
@@ -357,9 +360,10 @@ export function parseGantt(
357
360
  // fall through to process this line normally
358
361
  } else {
359
362
  // Parse tag entry: `Value(color)` or `Value`
360
- // First entry is the default (no `default` keyword needed)
363
+ // First entry is the default unless another is marked `default`
361
364
  if (COMMENT_RE.test(line)) continue;
362
- const extracted = extractColor(line, palette);
365
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(line);
366
+ const extracted = extractColor(cleanEntry, palette);
363
367
  const color =
364
368
  extracted.color ||
365
369
  seriesColors[currentTagGroup.entries.length % seriesColors.length] ||
@@ -370,7 +374,9 @@ export function parseGantt(
370
374
  color,
371
375
  lineNumber,
372
376
  });
373
- if (isFirstEntry) {
377
+ if (isDefault) {
378
+ currentTagGroup.defaultValue = extracted.label;
379
+ } else if (isFirstEntry) {
374
380
  currentTagGroup.defaultValue = extracted.label;
375
381
  }
376
382
  continue;
@@ -298,7 +298,7 @@ export function renderGantt(
298
298
  .append('g')
299
299
  .attr('transform', `translate(${leftMargin}, ${marginTop})`);
300
300
 
301
- // ── Title (y=30, consistent with timeline/initiative-status) ──
301
+ // ── Title (y=30, consistent with timeline) ──
302
302
 
303
303
  if (title) {
304
304
  svg
@@ -1952,9 +1952,7 @@ function renderTagLegend(
1952
1952
  measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
1953
1953
  LEGEND_PILL_PAD +
1954
1954
  iconReserve;
1955
- const pillH = isActive
1956
- ? LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2
1957
- : LEGEND_HEIGHT;
1955
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
1958
1956
  const groupW = groupWidths[i];
1959
1957
 
1960
1958
  const gEl = legendRow
@@ -1979,7 +1977,7 @@ function renderTagLegend(
1979
1977
  }
1980
1978
 
1981
1979
  const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
1982
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
1980
+ const pillYOff = LEGEND_CAPSULE_PAD;
1983
1981
 
1984
1982
  // Pill background
1985
1983
  gEl
package/src/index.ts CHANGED
@@ -204,36 +204,27 @@ export {
204
204
  renderC4DeploymentForExport,
205
205
  } from './c4/renderer';
206
206
 
207
- export {
208
- parseInitiativeStatus,
209
- looksLikeInitiativeStatus,
210
- } from './initiative-status/parser';
207
+ export { parseBoxesAndLines } from './boxes-and-lines/parser';
211
208
  export type {
212
- ParsedInitiativeStatus,
213
- ISNode,
214
- ISEdge,
215
- ISGroup,
216
- InitiativeStatus,
217
- } from './initiative-status/types';
218
-
219
- export { layoutInitiativeStatus } from './initiative-status/layout';
209
+ ParsedBoxesAndLines,
210
+ BLNode,
211
+ BLEdge,
212
+ BLGroup,
213
+ } from './boxes-and-lines/types';
214
+ export { layoutBoxesAndLines } from './boxes-and-lines/layout';
220
215
  export type {
221
- ISLayoutResult,
222
- ISLayoutNode,
223
- ISLayoutEdge,
224
- ISLayoutGroup,
225
- } from './initiative-status/layout';
226
-
216
+ BLLayoutResult,
217
+ BLLayoutNode,
218
+ BLLayoutEdge,
219
+ BLLayoutGroup,
220
+ } from './boxes-and-lines/layout';
227
221
  export {
228
- renderInitiativeStatus,
229
- renderInitiativeStatusForExport,
230
- } from './initiative-status/renderer';
231
- export type { ISRenderOptions } from './initiative-status/renderer';
232
-
233
- export { collapseInitiativeStatus } from './initiative-status/collapse';
234
- export type { CollapseResult } from './initiative-status/collapse';
222
+ renderBoxesAndLines,
223
+ renderBoxesAndLinesForExport,
224
+ } from './boxes-and-lines/renderer';
235
225
 
236
- export { filterInitiativeStatusByTags } from './initiative-status/filter';
226
+ export { collapseBoxesAndLines } from './boxes-and-lines/collapse';
227
+ export type { BLCollapseResult } from './boxes-and-lines/collapse';
237
228
 
238
229
  export { parseSitemap, looksLikeSitemap } from './sitemap/parser';
239
230
 
@@ -12,7 +12,10 @@ import {
12
12
  parseFirstLine,
13
13
  OPTION_NOCOLON_RE,
14
14
  } from '../utils/parsing';
15
- import { matchTagBlockHeading } from '../utils/tag-groups';
15
+ import {
16
+ matchTagBlockHeading,
17
+ stripDefaultModifier,
18
+ } from '../utils/tag-groups';
16
19
  import type {
17
20
  ParsedInfra,
18
21
  InfraNode,
@@ -338,17 +341,19 @@ export function parseInfra(content: string): ParsedInfra {
338
341
 
339
342
  // ---- Indented lines ----
340
343
 
341
- // Tag value inside tag group — first value is the default
344
+ // Tag value inside tag group — first value is the default unless another is marked `default`
342
345
  if (currentTagGroup && indent > 0) {
343
- const tvMatch = trimmed.match(TAG_VALUE_RE);
346
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
347
+ const tvMatch = cleanEntry.match(TAG_VALUE_RE);
344
348
  if (tvMatch) {
345
349
  const valueName = tvMatch[1].trim();
346
350
  currentTagGroup.values.push({
347
351
  name: valueName,
348
352
  color: tvMatch[2]?.trim(),
349
353
  });
350
- // First value is the default
351
- if (currentTagGroup.values.length === 1) {
354
+ if (isDefault) {
355
+ currentTagGroup.defaultValue = valueName;
356
+ } else if (currentTagGroup.values.length === 1) {
352
357
  currentTagGroup.defaultValue = valueName;
353
358
  }
354
359
  continue;
@@ -2069,8 +2069,8 @@ function renderLegend(
2069
2069
  }
2070
2070
 
2071
2071
  const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
2072
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
2073
- const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
2072
+ const pillYOff = LEGEND_CAPSULE_PAD;
2073
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
2074
2074
 
2075
2075
  // Pill background
2076
2076
  gEl
@@ -1,7 +1,10 @@
1
1
  import type { PaletteColors } from '../palettes';
2
2
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
3
3
  import { resolveColor } from '../colors';
4
- import { matchTagBlockHeading } from '../utils/tag-groups';
4
+ import {
5
+ matchTagBlockHeading,
6
+ stripDefaultModifier,
7
+ } from '../utils/tag-groups';
5
8
  import {
6
9
  measureIndent,
7
10
  extractColor,
@@ -166,11 +169,12 @@ export function parseKanban(
166
169
  }
167
170
 
168
171
  // Tag group entries (indented Value(color) under tag heading)
169
- // First entry is implicitly the default.
172
+ // First entry is the default unless another is marked `default`
170
173
  if (currentTagGroup && !contentStarted) {
171
174
  const indent = measureIndent(line);
172
175
  if (indent > 0) {
173
- const { label, color } = extractColor(trimmed, palette);
176
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
177
+ const { label, color } = extractColor(cleanEntry, palette);
174
178
  if (!color) {
175
179
  warn(
176
180
  lineNumber,
@@ -178,8 +182,9 @@ export function parseKanban(
178
182
  );
179
183
  continue;
180
184
  }
181
- // First entry is the default
182
- if (currentTagGroup.entries.length === 0) {
185
+ if (isDefault) {
186
+ currentTagGroup.defaultValue = label;
187
+ } else if (currentTagGroup.entries.length === 0) {
183
188
  currentTagGroup.defaultValue = label;
184
189
  }
185
190
  currentTagGroup.entries.push({