@diagrammo/dgmo 0.15.0 → 0.16.0

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 (127) hide show
  1. package/README.md +23 -10
  2. package/dist/advanced.cjs +53094 -0
  3. package/dist/advanced.d.cts +4690 -0
  4. package/dist/advanced.d.ts +4690 -0
  5. package/dist/advanced.js +52849 -0
  6. package/dist/auto.cjs +2298 -2069
  7. package/dist/auto.js +132 -109
  8. package/dist/auto.mjs +2294 -2065
  9. package/dist/cli.cjs +175 -152
  10. package/dist/editor.cjs +8 -9
  11. package/dist/editor.js +8 -9
  12. package/dist/highlight.cjs +8 -9
  13. package/dist/highlight.js +8 -9
  14. package/dist/index.cjs +2281 -2048
  15. package/dist/index.d.cts +45 -1
  16. package/dist/index.d.ts +45 -1
  17. package/dist/index.js +2276 -2044
  18. package/dist/internal.cjs +2064 -1831
  19. package/dist/internal.d.cts +113 -113
  20. package/dist/internal.d.ts +113 -113
  21. package/dist/internal.js +2059 -1826
  22. package/dist/pert.cjs +325 -0
  23. package/dist/pert.d.cts +542 -0
  24. package/dist/pert.d.ts +542 -0
  25. package/dist/pert.js +294 -0
  26. package/docs/language-reference.md +83 -66
  27. package/gallery/fixtures/area.dgmo +3 -3
  28. package/gallery/fixtures/bar-stacked.dgmo +5 -5
  29. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  30. package/gallery/fixtures/c4-full.dgmo +8 -8
  31. package/gallery/fixtures/class-full.dgmo +2 -2
  32. package/gallery/fixtures/doughnut.dgmo +6 -6
  33. package/gallery/fixtures/flowchart-colors.dgmo +3 -3
  34. package/gallery/fixtures/function.dgmo +3 -3
  35. package/gallery/fixtures/gantt-full.dgmo +9 -9
  36. package/gallery/fixtures/gantt.dgmo +7 -7
  37. package/gallery/fixtures/infra-full.dgmo +6 -6
  38. package/gallery/fixtures/infra.dgmo +2 -2
  39. package/gallery/fixtures/kanban.dgmo +9 -9
  40. package/gallery/fixtures/line.dgmo +2 -2
  41. package/gallery/fixtures/multi-line.dgmo +3 -3
  42. package/gallery/fixtures/org-full.dgmo +6 -6
  43. package/gallery/fixtures/quadrant.dgmo +2 -2
  44. package/gallery/fixtures/sankey.dgmo +9 -9
  45. package/gallery/fixtures/scatter.dgmo +3 -3
  46. package/gallery/fixtures/sequence-tags-protocols.dgmo +8 -8
  47. package/gallery/fixtures/sequence-tags.dgmo +7 -7
  48. package/gallery/fixtures/sitemap-full.dgmo +7 -7
  49. package/gallery/fixtures/slope.dgmo +5 -5
  50. package/gallery/fixtures/spr-eras.dgmo +9 -9
  51. package/gallery/fixtures/timeline.dgmo +3 -3
  52. package/gallery/fixtures/venn.dgmo +3 -3
  53. package/package.json +28 -3
  54. package/src/advanced.ts +730 -0
  55. package/src/auto/index.ts +14 -13
  56. package/src/boxes-and-lines/layout.ts +481 -445
  57. package/src/boxes-and-lines/renderer.ts +5 -1
  58. package/src/c4/parser.ts +8 -8
  59. package/src/c4/renderer.ts +15 -8
  60. package/src/chart-types.ts +0 -5
  61. package/src/chart.ts +18 -9
  62. package/src/class/parser.ts +8 -15
  63. package/src/class/renderer.ts +17 -6
  64. package/src/cli.ts +15 -13
  65. package/src/completion-types.ts +28 -0
  66. package/src/completion.ts +28 -21
  67. package/src/cycle/layout.ts +2 -2
  68. package/src/cycle/parser.ts +14 -0
  69. package/src/cycle/renderer.ts +6 -3
  70. package/src/d3.ts +1537 -1164
  71. package/src/echarts.ts +37 -20
  72. package/src/editor/dgmo.grammar +1 -3
  73. package/src/editor/dgmo.grammar.js +8 -8
  74. package/src/editor/dgmo.grammar.terms.js +11 -12
  75. package/src/editor/highlight-api.ts +0 -1
  76. package/src/editor/highlight.ts +0 -1
  77. package/src/er/parser.ts +19 -20
  78. package/src/er/renderer.ts +20 -8
  79. package/src/gantt/calculator.ts +1 -11
  80. package/src/gantt/parser.ts +17 -17
  81. package/src/gantt/renderer.ts +9 -6
  82. package/src/graph/flowchart-parser.ts +19 -85
  83. package/src/graph/flowchart-renderer.ts +4 -9
  84. package/src/graph/layout.ts +0 -2
  85. package/src/graph/state-parser.ts +17 -62
  86. package/src/graph/state-renderer.ts +4 -9
  87. package/src/index.ts +17 -1
  88. package/src/infra/parser.ts +40 -30
  89. package/src/infra/renderer.ts +9 -6
  90. package/src/internal.ts +9 -721
  91. package/src/journey-map/parser.ts +10 -3
  92. package/src/journey-map/renderer.ts +3 -1
  93. package/src/kanban/parser.ts +12 -8
  94. package/src/kanban/renderer.ts +3 -1
  95. package/src/mindmap/layout.ts +1 -1
  96. package/src/mindmap/parser.ts +3 -3
  97. package/src/mindmap/renderer.ts +2 -1
  98. package/src/org/parser.ts +3 -3
  99. package/src/org/renderer.ts +5 -4
  100. package/src/pert/layout.ts +1 -1
  101. package/src/pert/monte-carlo.ts +2 -2
  102. package/src/pert/parser.ts +10 -10
  103. package/src/pert/renderer.ts +7 -2
  104. package/src/pert/types.ts +1 -1
  105. package/src/pyramid/parser.ts +12 -0
  106. package/src/raci/parser.ts +44 -14
  107. package/src/raci/renderer.ts +3 -2
  108. package/src/raci/types.ts +4 -3
  109. package/src/ring/parser.ts +12 -0
  110. package/src/sequence/parser.ts +15 -9
  111. package/src/sequence/renderer.ts +2 -5
  112. package/src/sitemap/layout.ts +0 -2
  113. package/src/sitemap/parser.ts +12 -38
  114. package/src/sitemap/renderer.ts +13 -13
  115. package/src/sitemap/types.ts +0 -1
  116. package/src/tech-radar/interactive.ts +1 -1
  117. package/src/tech-radar/renderer.ts +6 -4
  118. package/src/tech-radar/types.ts +2 -0
  119. package/src/utils/arrows.ts +3 -28
  120. package/src/utils/legend-d3.ts +12 -6
  121. package/src/utils/legend-layout.ts +1 -1
  122. package/src/utils/legend-types.ts +1 -1
  123. package/src/utils/parsing.ts +64 -35
  124. package/src/utils/tag-groups.ts +109 -30
  125. package/src/wireframe/layout.ts +11 -7
  126. package/src/wireframe/parser.ts +4 -4
  127. package/src/wireframe/renderer.ts +5 -2
@@ -1,4 +1,5 @@
1
1
  import type { PaletteColors } from '../palettes';
2
+ import { resolveColorWithDiagnostic } from '../colors';
2
3
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
3
4
  import {
4
5
  matchTagBlockHeading,
@@ -138,8 +139,14 @@ export function parseJourneyMap(
138
139
  const key = part.substring(0, colonIdx).trim().toLowerCase();
139
140
  const value = part.substring(colonIdx + 1).trim();
140
141
  if (key === 'color') {
141
- const resolved = extractColor(`x(${value})`, palette);
142
- personaColor = resolved.color;
142
+ // Resolve the color name directly (no synthetic parens wrap).
143
+ personaColor =
144
+ resolveColorWithDiagnostic(
145
+ value,
146
+ lineNumber,
147
+ result.diagnostics,
148
+ palette
149
+ ) ?? undefined;
143
150
  }
144
151
  }
145
152
  }
@@ -209,7 +216,7 @@ export function parseJourneyMap(
209
216
  if (!color) {
210
217
  warn(
211
218
  lineNumber,
212
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
219
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
213
220
  );
214
221
  continue;
215
222
  }
@@ -36,6 +36,7 @@ export interface JourneyMapInteractiveOptions {
36
36
  collapsedPhases?: Set<string>;
37
37
  /** Called when a phase is toggled */
38
38
  onPhaseToggle?: (phaseName: string) => void;
39
+ exportMode?: boolean;
39
40
  }
40
41
 
41
42
  // ============================================================
@@ -313,7 +314,7 @@ export function renderJourneyMap(
313
314
  titleRelation: 'inline-with-title',
314
315
  },
315
316
  titleWidth: 0,
316
- mode: exportDims ? 'inline' : 'fixed',
317
+ mode: options?.exportMode ? 'export' : 'preview',
317
318
  };
318
319
 
319
320
  const legendState: LegendState = { activeGroup: effectiveActiveGroup };
@@ -1559,6 +1560,7 @@ export function renderJourneyMapForExport(
1559
1560
  const container = document.createElement('div');
1560
1561
  renderJourneyMap(container, parsed, palette, isDark, {
1561
1562
  exportDims: { width: layout.totalWidth, height: layout.totalHeight },
1563
+ exportMode: true,
1562
1564
  });
1563
1565
 
1564
1566
  const svgEl = container.querySelector('svg');
@@ -26,10 +26,11 @@ import type {
26
26
  // Regex patterns
27
27
  // ============================================================
28
28
 
29
- // [Column Name], [Column Name](color), [Column Name] as <alias>, [Column Name] | wip: 3, etc.
29
+ // [Column Name], [Column Name] color, [Column Name] as <alias>, [Column Name] | wip: 3, etc.
30
+ // Universal §1.5 trailing-token: color is a bare token after `]`.
30
31
  // Captures: [1]=label [2]=color [3]=alias (TD-18) [4]=pipe meta
31
32
  const COLUMN_RE =
32
- /^\[(.+?)\](?:\s*\(([^)]+)\))?(?:\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11}))?\s*(?:\|\s*(.+))?$/;
33
+ /^\[(.+?)\](?:\s+(\S+))?(?:\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11}))?\s*(?:\|\s*(.+))?$/;
33
34
  // Legacy delimiter
34
35
  const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
35
36
 
@@ -180,7 +181,7 @@ export function parseKanban(
180
181
  }
181
182
  }
182
183
 
183
- // Tag group entries (indented Value(color) under tag heading)
184
+ // Tag group entries (indented Value color under tag heading)
184
185
  // First entry is the default unless another is marked `default`
185
186
  if (currentTagGroup && !contentStarted) {
186
187
  const indent = measureIndent(line);
@@ -190,7 +191,7 @@ export function parseKanban(
190
191
  if (!color) {
191
192
  warn(
192
193
  lineNumber,
193
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
194
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
194
195
  );
195
196
  continue;
196
197
  }
@@ -247,9 +248,12 @@ export function parseKanban(
247
248
 
248
249
  columnCounter++;
249
250
  const colName = columnMatch[1].trim();
250
- const colColor = columnMatch[2]
251
+ // Trailing token after `]` must be a recognized color word (§1.5).
252
+ // If it isn't, the line is malformed — emit the standard diagnostic.
253
+ const rawTrailing = columnMatch[2]?.trim();
254
+ const colColor = rawTrailing
251
255
  ? resolveColorWithDiagnostic(
252
- columnMatch[2].trim(),
256
+ rawTrailing,
253
257
  lineNumber,
254
258
  result.diagnostics,
255
259
  palette
@@ -270,8 +274,8 @@ export function parseKanban(
270
274
  parsePipeMetadata(pipeSegments, metaAliasMap)
271
275
  );
272
276
  // Extract wip from metadata
273
- if (columnMetadata.wip) {
274
- const wipVal = parseInt(columnMetadata.wip, 10);
277
+ if (columnMetadata['wip']) {
278
+ const wipVal = parseInt(columnMetadata['wip'], 10);
275
279
  if (!isNaN(wipVal)) {
276
280
  wipLimit = wipVal;
277
281
  }
@@ -37,6 +37,7 @@ interface KanbanInteractiveOptions {
37
37
  collapsedLanes?: Set<string>;
38
38
  collapsedColumns?: Set<string>;
39
39
  compactMeta?: boolean;
40
+ exportMode?: boolean;
40
41
  }
41
42
 
42
43
  // ============================================================
@@ -330,7 +331,7 @@ export function renderKanban(
330
331
  const legendConfig: LegendConfig = {
331
332
  groups: parsed.tagGroups,
332
333
  position: { placement: 'top-center', titleRelation: 'inline-with-title' },
333
- mode: exportDims ? 'inline' : 'fixed',
334
+ mode: options?.exportMode ? 'export' : 'preview',
334
335
  };
335
336
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
336
337
  const legendG = svg
@@ -682,6 +683,7 @@ export function renderKanbanForExport(
682
683
  const container = document.createElement('div');
683
684
  renderKanban(container, parsed, palette, isDark, {
684
685
  exportDims: { width: layout.totalWidth, height: layout.totalHeight },
686
+ exportMode: true,
685
687
  });
686
688
 
687
689
  const svgEl = container.querySelector('svg');
@@ -64,7 +64,7 @@ interface PositionedNode {
64
64
 
65
65
  export function layoutMindmap(
66
66
  parsed: ParsedMindmap,
67
- palette: PaletteColors,
67
+ _palette: PaletteColors,
68
68
  options?: {
69
69
  interactive?: boolean;
70
70
  hiddenCounts?: Map<string, number>;
@@ -179,7 +179,7 @@ export function parseMindmap(
179
179
  }
180
180
  }
181
181
 
182
- // Tag group entries (indented Value(color) under tag heading)
182
+ // Tag group entries (indented Value color under tag heading)
183
183
  if (currentTagGroup && !contentStarted) {
184
184
  const indent = measureIndent(line);
185
185
  if (indent > 0) {
@@ -188,7 +188,7 @@ export function parseMindmap(
188
188
  if (!color) {
189
189
  pushError(
190
190
  lineNumber,
191
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
191
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
192
192
  );
193
193
  continue;
194
194
  }
@@ -297,7 +297,7 @@ export function parseMindmap(
297
297
  function parseNodeLine(
298
298
  trimmed: string,
299
299
  lineNumber: number,
300
- palette: PaletteColors | undefined,
300
+ _palette: PaletteColors | undefined,
301
301
  counter: number,
302
302
  aliasMap: Map<string, string>,
303
303
  warnFn: (line: number, msg: string) => void
@@ -95,6 +95,7 @@ export function renderMindmap(
95
95
  onToggleDescriptions?: (active: boolean) => void;
96
96
  controlsExpanded?: boolean;
97
97
  onToggleControlsExpand?: () => void;
98
+ exportMode?: boolean;
98
99
  }
99
100
  ): void {
100
101
  const isExport = !!exportDims;
@@ -234,7 +235,7 @@ export function renderMindmap(
234
235
  };
235
236
  }),
236
237
  position: { placement: 'top-center', titleRelation: 'below-title' },
237
- mode: 'fixed',
238
+ mode: options?.exportMode ? 'export' : 'preview',
238
239
  controlsGroup: controlsToggles,
239
240
  };
240
241
  const legendState: LegendState = {
package/src/org/parser.ts CHANGED
@@ -235,7 +235,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
235
235
  }
236
236
  }
237
237
 
238
- // Tag group entries (indented Value(color) under tag heading)
238
+ // Tag group entries (indented Value color under tag heading)
239
239
  // First entry is the default unless another is marked `default`
240
240
  if (currentTagGroup && !contentStarted) {
241
241
  const indent = measureIndent(line);
@@ -245,7 +245,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
245
245
  if (!color) {
246
246
  pushError(
247
247
  lineNumber,
248
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
248
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
249
249
  );
250
250
  continue;
251
251
  }
@@ -389,7 +389,7 @@ function parseNodeLabel(
389
389
  trimmed: string,
390
390
  _indent: number,
391
391
  lineNumber: number,
392
- palette: PaletteColors | undefined,
392
+ _palette: PaletteColors | undefined,
393
393
  counter: number,
394
394
  metaAliasMap: Map<string, string> = new Map(),
395
395
  warnFn?: (line: number, msg: string) => void,
@@ -110,7 +110,8 @@ export function renderOrg(
110
110
  exportDims?: { width?: number; height?: number },
111
111
  activeTagGroup?: string | null,
112
112
  hiddenAttributes?: Set<string>,
113
- ancestorPath?: AncestorInfo[]
113
+ ancestorPath?: AncestorInfo[],
114
+ exportMode?: boolean
114
115
  ): void {
115
116
  // Clear existing content
116
117
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -235,7 +236,7 @@ export function renderOrg(
235
236
  const rootNodeIds = new Set(parsed.roots.map((r) => r.id));
236
237
 
237
238
  // Render container backgrounds (bottom layer)
238
- const colorOff = parsed.options?.color === 'off';
239
+ const colorOff = parsed.options?.['color'] === 'off';
239
240
  for (const c of layout.containers) {
240
241
  const cG = contentG
241
242
  .append('g')
@@ -759,7 +760,7 @@ export function renderOrg(
759
760
  },
760
761
  ],
761
762
  position: { placement: 'top-center', titleRelation: 'below-title' },
762
- mode: 'fixed',
763
+ mode: exportMode ? 'export' : 'preview',
763
764
  };
764
765
  const singleState: LegendState = { activeGroup: lg.name };
765
766
  const groupG = legendParentBase
@@ -783,7 +784,7 @@ export function renderOrg(
783
784
  const legendConfig: LegendConfig = {
784
785
  groups,
785
786
  position: { placement: 'top-center', titleRelation: 'below-title' },
786
- mode: 'fixed',
787
+ mode: exportMode ? 'export' : 'preview',
787
788
  capsulePillAddonWidth: eyeAddonWidth,
788
789
  };
789
790
  const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
@@ -488,7 +488,7 @@ export function relayoutPert(
488
488
  function applySwimLanes(
489
489
  g: any,
490
490
  resolved: ResolvedPert,
491
- memberToGroup: Map<string, string>,
491
+ _memberToGroup: Map<string, string>,
492
492
  collapsedGroupIds: ReadonlySet<string>
493
493
  ): boolean {
494
494
  const expanded = resolved.groups.filter(
@@ -134,8 +134,8 @@ interface SimulationOptions {
134
134
  function simulate(
135
135
  resolved: ResolvedPert,
136
136
  expanded: ExpandedActivity[],
137
- predecessors: Map<string, string[]>,
138
- successors: Map<string, string[]>,
137
+ _predecessors: Map<string, string[]>,
138
+ _successors: Map<string, string[]>,
139
139
  topo: string[],
140
140
  terminals: string[],
141
141
  poisoned: Set<string>,
@@ -42,7 +42,7 @@ import type {
42
42
  DeclarationSite,
43
43
  ReferenceSite,
44
44
  } from './internal';
45
- import type { DiagramSymbols } from '../completion';
45
+ import type { DiagramSymbols } from '../completion-types';
46
46
 
47
47
  // ============================================================
48
48
  // Regexes / constants
@@ -448,7 +448,7 @@ export interface ParsePertOptions {
448
448
  now?: Date;
449
449
  /**
450
450
  * Active palette — used when resolving color names on `tag` entries
451
- * (e.g. `High(red)` → palette.colors.red). Optional; when omitted the
451
+ * (e.g. `High red` → palette.colors.red). Optional; when omitted the
452
452
  * universal default color map is used.
453
453
  */
454
454
  palette?: PaletteColors;
@@ -503,7 +503,7 @@ export function parsePert(
503
503
 
504
504
  /**
505
505
  * Tag groups declared at the top of the diagram. A `tag …` heading
506
- * opens a block; entries (indented `Value(color)` lines) accumulate
506
+ * opens a block; entries (indented `Value color` lines) accumulate
507
507
  * until the first non-tag content line closes it.
508
508
  */
509
509
  const tagGroups: TagGroup[] = [];
@@ -576,7 +576,7 @@ export function parsePert(
576
576
  // layer is responsible for routing.
577
577
  }
578
578
 
579
- // ── Tag-block phase. `tag Priority as p\n High(red)\n Low(green)`
579
+ // ── Tag-block phase. `tag Priority as p\n High red\n Low green`
580
580
  // lives BEFORE diagram content; once any group / activity / arrow
581
581
  // is seen, `contentStarted` flips and further `tag …` headings
582
582
  // emit an error.
@@ -599,7 +599,7 @@ export function parsePert(
599
599
  );
600
600
  }
601
601
  tagGroups.push(currentTagGroup);
602
- // Inline values (e.g. `tag Priority as p Low(green), High(red)`).
602
+ // Inline values (e.g. `tag Priority as p Low green, High red`).
603
603
  if (tagBlockMatch.inlineValues) {
604
604
  for (const raw of tagBlockMatch.inlineValues) {
605
605
  const { text, isDefault } = stripDefaultModifier(raw);
@@ -612,7 +612,7 @@ export function parsePert(
612
612
  if (!color) {
613
613
  warn(
614
614
  lineNumber,
615
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
615
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
616
616
  );
617
617
  continue;
618
618
  }
@@ -624,7 +624,7 @@ export function parsePert(
624
624
  }
625
625
  continue;
626
626
  }
627
- // Indented `Value(color)` entry under an open tag block.
627
+ // Indented `Value color` entry under an open tag block.
628
628
  if (currentTagGroup && indent > 0) {
629
629
  const { text, isDefault } = stripDefaultModifier(trimmed);
630
630
  const { label, color } = extractColor(
@@ -636,7 +636,7 @@ export function parsePert(
636
636
  if (!color) {
637
637
  warn(
638
638
  lineNumber,
639
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
639
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
640
640
  );
641
641
  continue;
642
642
  }
@@ -675,7 +675,7 @@ export function parsePert(
675
675
  id,
676
676
  name,
677
677
  activityIds: [],
678
- collapsed: meta.collapsed === 'true',
678
+ collapsed: meta['collapsed'] === 'true',
679
679
  lineNumber,
680
680
  ...(Object.keys(tags).length > 0 && { tags }),
681
681
  });
@@ -1027,7 +1027,7 @@ export function parsePert(
1027
1027
  name: decl.name,
1028
1028
  ...(decl.alias !== undefined && { alias: decl.alias }),
1029
1029
  duration: estimate,
1030
- ...(meta.confidence && { confidence: meta.confidence }),
1030
+ ...(meta['confidence'] && { confidence: meta['confidence'] }),
1031
1031
  ...(decl.groupHint !== undefined && { groupId: decl.groupHint }),
1032
1032
  lineNumber: decl.lineNumber,
1033
1033
  isMilestone,
@@ -388,6 +388,8 @@ export interface PertRenderOptions {
388
388
  * through to the parsed `active-tag` directive.
389
389
  */
390
390
  activeTagOverride?: string | null;
391
+ /** True when rendering for export — strips collapsed pills and cog from legend. */
392
+ exportMode?: boolean;
391
393
  }
392
394
 
393
395
  export function renderPert(
@@ -568,6 +570,7 @@ export function renderPert(
568
570
  y: tagLegendY,
569
571
  width: exportWidth,
570
572
  activeGroup: tagLegendActive,
573
+ exportMode: options.exportMode,
571
574
  });
572
575
  }
573
576
 
@@ -681,6 +684,7 @@ export function renderPertForExport(
681
684
  title: hasTitle ? parsed.title : null,
682
685
  subtitle: resolved.projectSubtitle,
683
686
  exportDims: { width: exportWidth, height: exportHeight },
687
+ exportMode: true,
684
688
  });
685
689
  const svgEl = container.querySelector('svg');
686
690
  if (!svgEl) return '';
@@ -2990,6 +2994,7 @@ interface TagLegendArgs {
2990
2994
  y: number;
2991
2995
  width: number;
2992
2996
  activeGroup: string | null;
2997
+ exportMode?: boolean;
2993
2998
  }
2994
2999
 
2995
3000
  /**
@@ -3008,7 +3013,7 @@ function renderTagLegendRow(
3008
3013
  ): void {
3009
3014
  if (resolved.tagGroups.length === 0) return;
3010
3015
 
3011
- const { x, y, width, activeGroup } = args;
3016
+ const { x, y, width, activeGroup, exportMode } = args;
3012
3017
  const groups = resolved.tagGroups.map((g) => ({
3013
3018
  name: g.name,
3014
3019
  entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
@@ -3024,7 +3029,7 @@ function renderTagLegendRow(
3024
3029
  {
3025
3030
  groups,
3026
3031
  position: { placement: 'top-center', titleRelation: 'below-title' },
3027
- mode: 'fixed',
3032
+ mode: exportMode ? 'export' : 'preview',
3028
3033
  },
3029
3034
  { activeGroup },
3030
3035
  palette,
package/src/pert/types.ts CHANGED
@@ -215,7 +215,7 @@ export interface ParsedPert {
215
215
  groups: PertGroup[];
216
216
  /**
217
217
  * Tag groups declared at the top of the diagram (`tag Priority as p
218
- * High(red), Low(green)`). Drive node fill via `resolveTagColor()`.
218
+ * High red, Low green`). Drive node fill via `resolveTagColor()`.
219
219
  * Empty when no `tag` blocks are declared.
220
220
  */
221
221
  tagGroups: TagGroup[];
@@ -7,6 +7,7 @@ import {
7
7
  measureIndent,
8
8
  parseFirstLine,
9
9
  parsePipeMetadata,
10
+ peelTrailingColorName,
10
11
  tryParseSharedOption,
11
12
  } from '../utils/parsing';
12
13
  import type { ParsedPyramid, PyramidLayer } from './types';
@@ -146,6 +147,17 @@ export function parsePyramid(content: string): ParsedPyramid {
146
147
  continue;
147
148
  }
148
149
 
150
+ // Universal trailing-token shortcut: `Label color` is equivalent to
151
+ // `Label | color: <name>` when color is the only metadata key (§1.5).
152
+ if (!color) {
153
+ const { label: stripped, colorName: shortcutColor } =
154
+ peelTrailingColorName(label);
155
+ if (shortcutColor) {
156
+ color = shortcutColor;
157
+ label = stripped;
158
+ }
159
+ }
160
+
149
161
  currentLayer = {
150
162
  label,
151
163
  lineNumber: lineNum,
@@ -24,6 +24,7 @@ import {
24
24
  measureIndent,
25
25
  parseFirstLine,
26
26
  parsePipeMetadata,
27
+ peelTrailingColorName,
27
28
  OPTION_NOCOLON_RE,
28
29
  tryParseSharedOption,
29
30
  } from '../utils/parsing';
@@ -82,10 +83,10 @@ const KNOWN_BOOLEANS = new Set<string>([
82
83
  ...Object.keys(VARIANT_LOCK_DIRECTIVES),
83
84
  ]);
84
85
 
85
- // Allow optional trailing `| key: value, ...` after the bracket,
86
- // e.g. `[Voyage] | color: blue` matches the modern dgmo idiom
87
- // (cycle / pyramid / ring / journey-map / boxes-and-lines).
88
- const PHASE_RE = /^\[(.+?)\]\s*(?:\|\s*(.+))?\s*$/;
86
+ // Allow optional trailing color shortcut and/or pipe metadata after the
87
+ // bracket: `[Voyage] blue | desc: …` (per §1.5 universal trailing-token
88
+ // rule + the modern cycle/pyramid/ring/journey-map/b&l idiom).
89
+ const PHASE_RE = /^\[(.+?)\](?:\s+(\S+))?(?:\s*\|\s*(.+))?\s*$/;
89
90
  const ROLE_ASSIGNMENT_RE = /^([^:]+):\s*(.*)$/;
90
91
 
91
92
  /**
@@ -382,22 +383,36 @@ export function parseRaci(
382
383
  // Strip a possible trailing comma (user habit tolerance,
383
384
  // matches `collectIndentedValues`).
384
385
  const stripped = nextTrim.replace(/,\s*$/, '');
385
- // Optional pipe metadata: `Cap | color: blue` — matches
386
- // every modern chart-type's per-element styling form.
386
+ // Optional pipe metadata: `Cap | color: blue` — long form.
387
+ // Optional trailing-token shortcut: `Cap blue` — short form (§1.5).
387
388
  const segments = stripped.split('|').map((s) => s.trim());
388
- const roleLabel = segments[0] ?? '';
389
+ let roleLabel = segments[0] ?? '';
389
390
  let roleColor: string | undefined;
390
391
  if (segments.length > 1) {
391
392
  const meta = parsePipeMetadata(segments);
392
- if (meta.color) {
393
+ if (meta['color']) {
393
394
  roleColor = resolveColorWithDiagnostic(
394
- meta.color,
395
+ meta['color'],
395
396
  j + 1,
396
397
  result.diagnostics,
397
398
  palette
398
399
  );
399
400
  }
400
401
  }
402
+ // Apply shortcut only when pipe metadata didn't already set color.
403
+ if (!roleColor) {
404
+ const { label: stripLabel, colorName: shortcutColor } =
405
+ peelTrailingColorName(roleLabel);
406
+ if (shortcutColor) {
407
+ roleColor = resolveColorWithDiagnostic(
408
+ shortcutColor,
409
+ j + 1,
410
+ result.diagnostics,
411
+ palette
412
+ );
413
+ roleLabel = stripLabel;
414
+ }
415
+ }
401
416
  if (roleLabel) getOrAddRole(roleLabel, j + 1, roleColor);
402
417
  }
403
418
  i = j - 1; // outer loop's i++ lands on the first non-block line
@@ -469,13 +484,28 @@ export function parseRaci(
469
484
  errorAt(lineNumber, 'Phase label is empty.');
470
485
  continue;
471
486
  }
472
- // Optional pipe metadata: `[Voyage] | color: blue`.
487
+ // PHASE_RE captures: 1=label, 2=optional trailing-token color, 3=pipe meta.
488
+ // Long pipe form (`[Voyage] | color: blue`) wins over the shortcut.
473
489
  let phaseColor: string | undefined;
474
- if (phaseMatch[2]) {
475
- const meta = parsePipeMetadata(['', phaseMatch[2]]);
476
- if (meta.color) {
490
+ const trailingToken = phaseMatch[2];
491
+ const pipeMeta = phaseMatch[3];
492
+ if (pipeMeta) {
493
+ const meta = parsePipeMetadata(['', pipeMeta]);
494
+ if (meta['color']) {
495
+ phaseColor = resolveColorWithDiagnostic(
496
+ meta['color'],
497
+ lineNumber,
498
+ result.diagnostics,
499
+ palette
500
+ );
501
+ }
502
+ }
503
+ if (!phaseColor && trailingToken) {
504
+ // Trailing token must be a recognized color word, or it's a parse error.
505
+ const { colorName } = peelTrailingColorName(`x ${trailingToken}`);
506
+ if (colorName) {
477
507
  phaseColor = resolveColorWithDiagnostic(
478
- meta.color,
508
+ colorName,
479
509
  lineNumber,
480
510
  result.diagnostics,
481
511
  palette
@@ -602,7 +602,8 @@ export function renderRaci(
602
602
  parsed.roles.forEach((roleId, i) => {
603
603
  const cx = roleX(i) + COLUMN_INSET;
604
604
  const cw = roleColW - 2 * COLUMN_INSET;
605
- // Per-role color from `Cap(blue)` syntax. When the user provides
605
+ // Per-role color from `Cap blue` trailing-token (or `Cap | color: blue`)
606
+ // syntax. When the user provides
606
607
  // one, it wins; otherwise rotate through marker-safe accents so
607
608
  // each column has a subtle visual identity instead of every column
608
609
  // reading as the same neutral gray.
@@ -1150,7 +1151,7 @@ function renderTaskRow(
1150
1151
  surfaceBg: string,
1151
1152
  solid: boolean,
1152
1153
  taskDiagnostics: Map<string, TaskDiagnosticBucket> | null,
1153
- hasAnyDiagnostic: boolean,
1154
+ _hasAnyDiagnostic: boolean,
1154
1155
  rowContent: RowContent | undefined,
1155
1156
  onClickLine?: (lineNumber: number) => void,
1156
1157
  _onMarkerDragStart?: (source: RaciDragSource, e: PointerEvent) => void
package/src/raci/types.ts CHANGED
@@ -67,9 +67,10 @@ export interface ParsedRaci {
67
67
  /** Display name for each role (parallel to `roles`). */
68
68
  roleDisplayNames: string[];
69
69
  /**
70
- * Optional per-role palette color from `Cap(blue)` suffix in the
71
- * roles block. Parallel to `roles`; entries default to `undefined`
72
- * (renderer falls back to the neutral column tint).
70
+ * Optional per-role palette color from the `Cap blue` trailing-token
71
+ * suffix in the roles block (or the long pipe form `Cap | color: blue`).
72
+ * Parallel to `roles`; entries default to `undefined` (renderer falls
73
+ * back to the neutral column tint).
73
74
  */
74
75
  roleColors: Array<string | undefined>;
75
76
  phases: RaciPhase[];
@@ -8,6 +8,7 @@ import {
8
8
  measureIndent,
9
9
  parseFirstLine,
10
10
  parsePipeMetadata,
11
+ peelTrailingColorName,
11
12
  tryParseSharedOption,
12
13
  PIPE_KEY_VALUE_PREFIX_RE,
13
14
  PIPE_LIKELY_STRUCTURED_TAIL_RE,
@@ -175,6 +176,17 @@ export function parseRing(content: string): ParsedRing {
175
176
  continue;
176
177
  }
177
178
 
179
+ // Universal trailing-token shortcut: `Label color` equivalent to
180
+ // `Label | color: <name>` when color is not already set (§1.5).
181
+ if (!color) {
182
+ const { label: stripped, colorName: shortcutColor } =
183
+ peelTrailingColorName(label);
184
+ if (shortcutColor) {
185
+ color = shortcutColor;
186
+ label = stripped;
187
+ }
188
+ }
189
+
178
190
  currentLayer = {
179
191
  label,
180
192
  lineNumber: lineNum,