@diagrammo/dgmo 0.14.1 → 0.15.1

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 (71) hide show
  1. package/README.md +14 -1
  2. package/dist/advanced.cjs +53069 -0
  3. package/dist/advanced.d.cts +4691 -0
  4. package/dist/advanced.d.ts +4691 -0
  5. package/dist/advanced.js +52823 -0
  6. package/dist/auto.cjs +1557 -1295
  7. package/dist/auto.js +132 -713
  8. package/dist/auto.mjs +1553 -1291
  9. package/dist/cli.cjs +173 -150
  10. package/dist/editor.cjs +1 -0
  11. package/dist/editor.js +1 -0
  12. package/dist/highlight.cjs +1 -0
  13. package/dist/highlight.js +1 -0
  14. package/dist/index.cjs +2031 -4722
  15. package/dist/index.d.cts +96 -4464
  16. package/dist/index.d.ts +96 -4464
  17. package/dist/index.js +2024 -4475
  18. package/dist/internal.cjs +51930 -553
  19. package/dist/internal.d.cts +4526 -102
  20. package/dist/internal.d.ts +4526 -102
  21. package/dist/internal.js +51721 -548
  22. package/dist/pert.cjs +1 -1
  23. package/dist/pert.js +1 -1
  24. package/docs/language-reference.md +67 -17
  25. package/package.json +18 -3
  26. package/src/advanced.ts +731 -0
  27. package/src/auto/index.ts +14 -13
  28. package/src/boxes-and-lines/layout.ts +481 -445
  29. package/src/c4/parser.ts +7 -7
  30. package/src/chart-types.ts +0 -5
  31. package/src/class/parser.ts +1 -9
  32. package/src/cli.ts +9 -7
  33. package/src/completion-types.ts +28 -0
  34. package/src/completion.ts +15 -18
  35. package/src/cycle/layout.ts +2 -2
  36. package/src/d3.ts +1455 -1122
  37. package/src/echarts.ts +11 -11
  38. package/src/editor/keywords.ts +1 -0
  39. package/src/er/parser.ts +1 -9
  40. package/src/er/renderer.ts +1 -1
  41. package/src/gantt/calculator.ts +1 -11
  42. package/src/gantt/parser.ts +16 -16
  43. package/src/gantt/renderer.ts +2 -2
  44. package/src/graph/flowchart-parser.ts +1 -1
  45. package/src/graph/flowchart-renderer.ts +1 -1
  46. package/src/graph/state-renderer.ts +1 -1
  47. package/src/index.ts +213 -690
  48. package/src/infra/parser.ts +57 -25
  49. package/src/infra/renderer.ts +2 -2
  50. package/src/internal.ts +11 -17
  51. package/src/kanban/parser.ts +2 -2
  52. package/src/mindmap/layout.ts +1 -1
  53. package/src/mindmap/parser.ts +1 -1
  54. package/src/org/parser.ts +1 -1
  55. package/src/org/renderer.ts +1 -1
  56. package/src/palettes/index.ts +39 -0
  57. package/src/pert/layout.ts +1 -1
  58. package/src/pert/monte-carlo.ts +2 -2
  59. package/src/pert/parser.ts +3 -3
  60. package/src/raci/parser.ts +4 -4
  61. package/src/raci/renderer.ts +1 -1
  62. package/src/render.ts +17 -1
  63. package/src/sequence/renderer.ts +1 -4
  64. package/src/sitemap/parser.ts +1 -1
  65. package/src/tech-radar/interactive.ts +1 -1
  66. package/src/tech-radar/renderer.ts +1 -1
  67. package/src/themes.ts +22 -0
  68. package/src/utils/tag-groups.ts +11 -12
  69. package/src/wireframe/layout.ts +11 -7
  70. package/src/wireframe/parser.ts +2 -2
  71. package/src/wireframe/renderer.ts +5 -2
@@ -194,6 +194,24 @@ export function parseInfra(content: string): ParsedInfra {
194
194
  if (!m) return { label: trimmed };
195
195
  return { label: m[1].trim(), alias: m[2] };
196
196
  }
197
+
198
+ // Infra nodes have no "is a <type>" declaration — capability comes from
199
+ // properties (cache-hit, buffer, drain-rate, …). Strip the suffix if a
200
+ // user wrote it (likely copying from sequence/c4 syntax) and warn.
201
+ const IS_A_SUFFIX = /^(.*?)\s+is\s+an?\s+[A-Za-z][\w-]*\s*$/i;
202
+ function peelInfraDecorations(
203
+ rawName: string,
204
+ lineNumber: number
205
+ ): { label: string; alias?: string } {
206
+ const peeled = peelAlias(rawName);
207
+ const m = peeled.label.match(IS_A_SUFFIX);
208
+ if (!m) return peeled;
209
+ warn(
210
+ lineNumber,
211
+ `Infra nodes don't use 'is a <type>' — types are inferred from properties (cache-hit, buffer, drain-rate, …). Drop the 'is a' suffix.`
212
+ );
213
+ return { label: m[1].trim(), alias: peeled.alias };
214
+ }
197
215
  /**
198
216
  * Resolve a connection-target token. If the token exactly matches a
199
217
  * declared alias, return the bound canonical id. Otherwise fall
@@ -333,11 +351,11 @@ export function parseInfra(content: string): ParsedInfra {
333
351
 
334
352
  // animate (default ON) / no-animate
335
353
  if (trimmed === 'animate') {
336
- result.options.animate = 'on';
354
+ result.options['animate'] = 'on';
337
355
  continue;
338
356
  }
339
357
  if (trimmed === 'no-animate') {
340
- result.options.animate = 'off';
358
+ result.options['animate'] = 'off';
341
359
  continue;
342
360
  }
343
361
 
@@ -399,7 +417,7 @@ export function parseInfra(content: string): ParsedInfra {
399
417
  finishCurrentTagGroup();
400
418
 
401
419
  const rawName = (compMatch[1] ?? compMatch[2] ?? '').trim();
402
- const peeled = peelAlias(rawName);
420
+ const peeled = peelInfraDecorations(rawName, lineNumber);
403
421
  const name = peeled.label;
404
422
  const rest = compMatch[3] || '';
405
423
  const { tags } = extractPipeMetadata(rest);
@@ -482,7 +500,7 @@ export function parseInfra(content: string): ParsedInfra {
482
500
  if (compMatch) {
483
501
  finishCurrentTagGroup();
484
502
  const rawName = (compMatch[1] ?? compMatch[2] ?? '').trim();
485
- const peeled = peelAlias(rawName);
503
+ const peeled = peelInfraDecorations(rawName, lineNumber);
486
504
  const name = peeled.label;
487
505
  const rest = compMatch[3] || '';
488
506
  const { tags: nodeTags } = extractPipeMetadata(rest);
@@ -532,11 +550,11 @@ export function parseInfra(content: string): ParsedInfra {
532
550
  const pipeMeta = extractPipeMetadata(targetRaw);
533
551
  const targetName = pipeMeta.clean || targetRaw;
534
552
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
535
- const split = pipeMeta.tags.split
536
- ? parseFloat(pipeMeta.tags.split)
553
+ const split = pipeMeta.tags['split']
554
+ ? parseFloat(pipeMeta.tags['split'])
537
555
  : null;
538
- const fanoutRaw = pipeMeta.tags.fanout
539
- ? parseInt(pipeMeta.tags.fanout, 10)
556
+ const fanoutRaw = pipeMeta.tags['fanout']
557
+ ? parseInt(pipeMeta.tags['fanout'], 10)
540
558
  : null;
541
559
  if (fanoutRaw !== null && fanoutRaw < 1) {
542
560
  warn(
@@ -570,11 +588,11 @@ export function parseInfra(content: string): ParsedInfra {
570
588
  const pipeMeta = extractPipeMetadata(targetRaw);
571
589
  const targetName = pipeMeta.clean || targetRaw;
572
590
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
573
- const split = pipeMeta.tags.split
574
- ? parseFloat(pipeMeta.tags.split)
591
+ const split = pipeMeta.tags['split']
592
+ ? parseFloat(pipeMeta.tags['split'])
575
593
  : null;
576
- const fanoutRaw = pipeMeta.tags.fanout
577
- ? parseInt(pipeMeta.tags.fanout, 10)
594
+ const fanoutRaw = pipeMeta.tags['fanout']
595
+ ? parseInt(pipeMeta.tags['fanout'], 10)
578
596
  : null;
579
597
  if (fanoutRaw !== null && fanoutRaw < 1) {
580
598
  warn(
@@ -613,11 +631,11 @@ export function parseInfra(content: string): ParsedInfra {
613
631
  const pipeMeta = extractPipeMetadata(targetRaw);
614
632
  const targetName = pipeMeta.clean || targetRaw;
615
633
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
616
- const split = pipeMeta.tags.split
617
- ? parseFloat(pipeMeta.tags.split)
634
+ const split = pipeMeta.tags['split']
635
+ ? parseFloat(pipeMeta.tags['split'])
618
636
  : null;
619
- const fanoutRaw = pipeMeta.tags.fanout
620
- ? parseInt(pipeMeta.tags.fanout, 10)
637
+ const fanoutRaw = pipeMeta.tags['fanout']
638
+ ? parseInt(pipeMeta.tags['fanout'], 10)
621
639
  : null;
622
640
  if (fanoutRaw !== null && fanoutRaw < 1) {
623
641
  warn(
@@ -651,11 +669,11 @@ export function parseInfra(content: string): ParsedInfra {
651
669
  const pipeMeta = extractPipeMetadata(targetRaw);
652
670
  const targetName = pipeMeta.clean || targetRaw;
653
671
  warnUnparsedPipeMeta(targetName, lineNumber, warn);
654
- const split = pipeMeta.tags.split
655
- ? parseFloat(pipeMeta.tags.split)
672
+ const split = pipeMeta.tags['split']
673
+ ? parseFloat(pipeMeta.tags['split'])
656
674
  : null;
657
- const fanoutRaw = pipeMeta.tags.fanout
658
- ? parseInt(pipeMeta.tags.fanout, 10)
675
+ const fanoutRaw = pipeMeta.tags['fanout']
676
+ ? parseInt(pipeMeta.tags['fanout'], 10)
659
677
  : null;
660
678
  if (fanoutRaw !== null && fanoutRaw < 1) {
661
679
  warn(
@@ -764,7 +782,7 @@ export function parseInfra(content: string): ParsedInfra {
764
782
  const compMatch = trimmed.match(COMPONENT_RE);
765
783
  if (compMatch) {
766
784
  const rawName = (compMatch[1] ?? compMatch[2] ?? '').trim();
767
- const peeled = peelAlias(rawName);
785
+ const peeled = peelInfraDecorations(rawName, lineNumber);
768
786
  const name = peeled.label;
769
787
  const rest = compMatch[3] || '';
770
788
  const { tags: nodeTags } = extractPipeMetadata(rest);
@@ -796,10 +814,13 @@ export function parseInfra(content: string): ParsedInfra {
796
814
  finishCurrentTagGroup();
797
815
  currentGroup = null;
798
816
 
799
- const name = (compMatch[1] ?? compMatch[2] ?? '').trim();
817
+ const rawName = (compMatch[1] ?? compMatch[2] ?? '').trim();
818
+ const peeled = peelInfraDecorations(rawName, lineNumber);
819
+ const name = peeled.label;
800
820
  const rest = compMatch[3] || '';
801
821
  const { tags } = extractPipeMetadata(rest);
802
822
  const id = nodeId(name);
823
+ if (peeled.alias) nameAliasMap.set(peeled.alias, id);
803
824
 
804
825
  currentNode = {
805
826
  id,
@@ -871,12 +892,23 @@ export function parseInfra(content: string): ParsedInfra {
871
892
  // Symbol extraction (for completion API)
872
893
  // ============================================================
873
894
 
874
- import type { DiagramSymbols } from '../completion';
895
+ import type { DiagramSymbols } from '../completion-types';
875
896
 
876
897
  /**
877
898
  * Extract component names (entities) from infra document text.
878
899
  * Used by the dgmo completion API for ghost hints and popup completions.
879
900
  */
901
+ /** Strip ` as <alias>` and ` is a/an <type>` decorations from a node name
902
+ * so completion suggests the bare identifier the user will reference. */
903
+ function stripNodeDecorations(name: string): string {
904
+ let s = name.trim();
905
+ const aliasMatch = s.match(/^(.*?)\s+as\s+[A-Za-z][A-Za-z0-9_]{0,11}\s*$/);
906
+ if (aliasMatch) s = aliasMatch[1].trim();
907
+ const isAMatch = s.match(/^(.*?)\s+is\s+an?\s+[A-Za-z][\w-]*\s*$/i);
908
+ if (isAMatch) s = isAMatch[1].trim();
909
+ return s;
910
+ }
911
+
880
912
  export function extractSymbols(docText: string): DiagramSymbols {
881
913
  const entities: string[] = [];
882
914
  let inMetadata = true;
@@ -916,7 +948,7 @@ export function extractSymbols(docText: string): DiagramSymbols {
916
948
  if (/^\[/.test(line)) continue; // [Group] header
917
949
  const m = COMPONENT_RE.exec(line);
918
950
  if (m) {
919
- const name = (m[1] ?? m[2] ?? '').trim();
951
+ const name = stripNodeDecorations((m[1] ?? m[2] ?? '').trim());
920
952
  if (name && !entities.includes(name)) entities.push(name);
921
953
  }
922
954
  } else {
@@ -940,7 +972,7 @@ export function extractSymbols(docText: string): DiagramSymbols {
940
972
  continue;
941
973
  const m = COMPONENT_RE.exec(line);
942
974
  if (m) {
943
- const name = (m[1] ?? m[2] ?? '').trim();
975
+ const name = stripNodeDecorations((m[1] ?? m[2] ?? '').trim());
944
976
  if (name && !entities.includes(name)) entities.push(name);
945
977
  }
946
978
  }
@@ -1165,7 +1165,7 @@ function renderEdgePaths(
1165
1165
  nodes: InfraLayoutNode[],
1166
1166
  groups: InfraLayoutGroup[],
1167
1167
  palette: PaletteColors,
1168
- isDark: boolean,
1168
+ _isDark: boolean,
1169
1169
  animate: boolean,
1170
1170
  direction: 'LR' | 'TB',
1171
1171
  speedMultiplier: number = 1
@@ -1245,7 +1245,7 @@ function renderEdgeLabels(
1245
1245
  nodes: InfraLayoutNode[],
1246
1246
  groups: InfraLayoutGroup[],
1247
1247
  palette: PaletteColors,
1248
- isDark: boolean,
1248
+ _isDark: boolean,
1249
1249
  animate: boolean,
1250
1250
  direction: 'LR' | 'TB'
1251
1251
  ) {
package/src/internal.ts CHANGED
@@ -1,20 +1,14 @@
1
1
  // ============================================================
2
- // @diagrammo/dgmo/internal — internal helpers for app consumers.
3
- // Not part of the public API; may change between versions.
2
+ // @diagrammo/dgmo/internal — DEPRECATED ALIAS for @diagrammo/dgmo/advanced
3
+ //
4
+ // This subpath was renamed to `/advanced` to match its actual contract
5
+ // ("low-level but supported" rather than "private"). Update your imports:
6
+ //
7
+ // import { … } from '@diagrammo/dgmo/internal';
8
+ // →
9
+ // import { … } from '@diagrammo/dgmo/advanced';
10
+ //
11
+ // The `/internal` subpath will be removed in 0.17.x.
4
12
  // ============================================================
5
13
 
6
- export { parseDataRowValues } from './chart';
7
- export {
8
- computeCardArchive,
9
- computeCardMove,
10
- isArchiveColumn,
11
- } from './kanban/mutations';
12
- export {
13
- applyGroupOrdering,
14
- applyPositionOverrides,
15
- buildNoteMessageMap,
16
- buildRenderSequence,
17
- computeActivations,
18
- groupMessagesBySection,
19
- } from './sequence/renderer';
20
- export { orderArcNodes } from './d3';
14
+ export * from './advanced';
@@ -270,8 +270,8 @@ export function parseKanban(
270
270
  parsePipeMetadata(pipeSegments, metaAliasMap)
271
271
  );
272
272
  // Extract wip from metadata
273
- if (columnMetadata.wip) {
274
- const wipVal = parseInt(columnMetadata.wip, 10);
273
+ if (columnMetadata['wip']) {
274
+ const wipVal = parseInt(columnMetadata['wip'], 10);
275
275
  if (!isNaN(wipVal)) {
276
276
  wipLimit = wipVal;
277
277
  }
@@ -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>;
@@ -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
package/src/org/parser.ts CHANGED
@@ -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,
@@ -235,7 +235,7 @@ export function renderOrg(
235
235
  const rootNodeIds = new Set(parsed.roots.map((r) => r.id));
236
236
 
237
237
  // Render container backgrounds (bottom layer)
238
- const colorOff = parsed.options?.color === 'off';
238
+ const colorOff = parsed.options?.['color'] === 'off';
239
239
  for (const c of layout.containers) {
240
240
  const cG = contentG
241
241
  .append('g')
@@ -34,3 +34,42 @@ export { tokyoNightPalette } from './tokyo-night';
34
34
 
35
35
  export { draculaPalette } from './dracula';
36
36
  export { monokaiPalette } from './monokai';
37
+
38
+ // ============================================================
39
+ // Public namespace — `palettes` for use with render()
40
+ // ============================================================
41
+
42
+ import { boldPalette } from './bold';
43
+ import { catppuccinPalette } from './catppuccin';
44
+ import { draculaPalette } from './dracula';
45
+ import { gruvboxPalette } from './gruvbox';
46
+ import { monokaiPalette } from './monokai';
47
+ import { nordPalette } from './nord';
48
+ import { oneDarkPalette } from './one-dark';
49
+ import { rosePinePalette } from './rose-pine';
50
+ import { solarizedPalette } from './solarized';
51
+ import { tokyoNightPalette } from './tokyo-night';
52
+
53
+ import type { PaletteConfig } from './types';
54
+
55
+ /**
56
+ * All built-in palettes, keyed by camelCase id. Use directly with render():
57
+ *
58
+ * await render(text, { palette: palettes.catppuccin });
59
+ *
60
+ * For preference/settings storage, the `.id` field of each entry is the
61
+ * canonical string (e.g. `'tokyo-night'`, `'nord'`) — that's the wire format
62
+ * used by share URLs and the CLI `--palette` flag.
63
+ */
64
+ export const palettes = {
65
+ nord: nordPalette,
66
+ catppuccin: catppuccinPalette,
67
+ solarized: solarizedPalette,
68
+ gruvbox: gruvboxPalette,
69
+ tokyoNight: tokyoNightPalette,
70
+ oneDark: oneDarkPalette,
71
+ rosePine: rosePinePalette,
72
+ dracula: draculaPalette,
73
+ monokai: monokaiPalette,
74
+ bold: boldPalette,
75
+ } as const satisfies Record<string, PaletteConfig>;
@@ -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
@@ -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,
@@ -389,9 +389,9 @@ export function parseRaci(
389
389
  let roleColor: string | undefined;
390
390
  if (segments.length > 1) {
391
391
  const meta = parsePipeMetadata(segments);
392
- if (meta.color) {
392
+ if (meta['color']) {
393
393
  roleColor = resolveColorWithDiagnostic(
394
- meta.color,
394
+ meta['color'],
395
395
  j + 1,
396
396
  result.diagnostics,
397
397
  palette
@@ -473,9 +473,9 @@ export function parseRaci(
473
473
  let phaseColor: string | undefined;
474
474
  if (phaseMatch[2]) {
475
475
  const meta = parsePipeMetadata(['', phaseMatch[2]]);
476
- if (meta.color) {
476
+ if (meta['color']) {
477
477
  phaseColor = resolveColorWithDiagnostic(
478
- meta.color,
478
+ meta['color'],
479
479
  lineNumber,
480
480
  result.diagnostics,
481
481
  palette
@@ -1150,7 +1150,7 @@ function renderTaskRow(
1150
1150
  surfaceBg: string,
1151
1151
  solid: boolean,
1152
1152
  taskDiagnostics: Map<string, TaskDiagnosticBucket> | null,
1153
- hasAnyDiagnostic: boolean,
1153
+ _hasAnyDiagnostic: boolean,
1154
1154
  rowContent: RowContent | undefined,
1155
1155
  onClickLine?: (lineNumber: number) => void,
1156
1156
  _onMarkerDragStart?: (source: RaciDragSource, e: PointerEvent) => void
package/src/render.ts CHANGED
@@ -13,7 +13,7 @@ import type { CompactViewState } from './sharing';
13
13
  async function ensureDom(): Promise<void> {
14
14
  if (typeof document !== 'undefined') return;
15
15
 
16
- const { JSDOM } = await import('jsdom');
16
+ const { JSDOM } = await loadJsdom();
17
17
  const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
18
18
  const win = dom.window;
19
19
 
@@ -39,6 +39,22 @@ async function ensureDom(): Promise<void> {
39
39
  });
40
40
  }
41
41
 
42
+ /**
43
+ * Load jsdom server-side. The specifier is constructed at runtime so
44
+ * downstream bundlers (Vite, Rollup, esbuild, webpack) cannot statically
45
+ * resolve it. Without this indirection, every browser bundle of
46
+ * @diagrammo/dgmo emits a 5+ MB jsdom chunk even though `ensureDom()`
47
+ * guards execution with a `typeof document` check — the guard prevents
48
+ * runtime evaluation, but the static dependency edge still pulls jsdom
49
+ * into the bundle.
50
+ */
51
+ async function loadJsdom(): Promise<typeof import('jsdom')> {
52
+ const spec = ['js', 'dom'].join('');
53
+ return import(/* @vite-ignore */ /* webpackIgnore: true */ spec) as Promise<
54
+ typeof import('jsdom')
55
+ >;
56
+ }
57
+
42
58
  /**
43
59
  * Render DGMO source to an SVG string.
44
60
  *
@@ -949,9 +949,6 @@ export function renderSequenceDiagram(
949
949
  const messages = collapsed ? collapsed.messages : parsed.messages;
950
950
  const elements = collapsed ? collapsed.elements : parsed.elements;
951
951
  const groups = collapsed ? collapsed.groups : parsed.groups;
952
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
953
- const collapsedGroupIds = collapsed?.collapsedGroupIds ?? new Map();
954
-
955
952
  const collapsedSections = options?.collapsedSections;
956
953
 
957
954
  const sourceParticipants = collapsed
@@ -1006,7 +1003,7 @@ export function renderSequenceDiagram(
1006
1003
  const charsForWidth = (maxW: number): number =>
1007
1004
  Math.floor((maxW - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
1008
1005
 
1009
- const activationsOff = parsedOptions.activations?.toLowerCase() === 'off';
1006
+ const activationsOff = parsedOptions['activations']?.toLowerCase() === 'off';
1010
1007
 
1011
1008
  // Tag resolution — shared utility handles priority chain:
1012
1009
  // programmatic override → diagram-level active-tag → auto-activate first group
@@ -566,7 +566,7 @@ export function parseSitemap(
566
566
  function parseNodeLabel(
567
567
  trimmed: string,
568
568
  lineNumber: number,
569
- palette: PaletteColors | undefined,
569
+ _palette: PaletteColors | undefined,
570
570
  counter: number,
571
571
  metaAliasMap: Map<string, string> = new Map(),
572
572
  warnFn?: (line: number, msg: string) => void,
@@ -188,7 +188,7 @@ function renderQuarterCircle(
188
188
  width: number,
189
189
  height: number,
190
190
  mutedColor: string,
191
- tooltip: HTMLDivElement,
191
+ _tooltip: HTMLDivElement,
192
192
  rootContainer: HTMLElement,
193
193
  onClickItem?: (lineNumber: number) => void
194
194
  ): void {
@@ -920,7 +920,7 @@ import type { TechRadarBlip } from './types';
920
920
 
921
921
  function createBlipPopover(
922
922
  container: HTMLElement,
923
- palette: PaletteColors,
923
+ _palette: PaletteColors,
924
924
  isDark: boolean
925
925
  ): HTMLDivElement {
926
926
  container.style.position = 'relative';
package/src/themes.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Theme — render mode flag. Selects which palette variant the renderer uses
3
+ * for background and text:
4
+ * - 'light' → palette.light colors
5
+ * - 'dark' → palette.dark colors
6
+ * - 'transparent' → no background fill (for embedding in colored containers)
7
+ */
8
+ export type Theme = 'light' | 'dark' | 'transparent';
9
+
10
+ /**
11
+ * `themes` namespace — use with render() options for a typed handle:
12
+ *
13
+ * await render(text, { theme: themes.dark });
14
+ *
15
+ * Passing the raw string `'dark'` also works (the underlying type is the
16
+ * string-literal union); the namespace is the conventional path.
17
+ */
18
+ export const themes = {
19
+ light: 'light',
20
+ dark: 'dark',
21
+ transparent: 'transparent',
22
+ } as const satisfies Record<string, Theme>;
@@ -418,25 +418,21 @@ export function injectDefaultTagMetadata(
418
418
  *
419
419
  * 1. Programmatic override (from render API / CLI flag) — highest priority
420
420
  * 2. Diagram-level `active-tag` option (from parsed source)
421
- * 3. No coloring (null) collapsed-by-default
421
+ * 3. Auto-activate first declared tag group
422
+ * 4. No coloring (null)
422
423
  *
423
424
  * The sentinel value `"none"` (case-insensitive) at any level means
424
- * "suppress tag coloring."
425
+ * "suppress tag coloring." Diagrams with tag groups render colored by
426
+ * default across every render path (CLI, export, share-link, app); use
427
+ * `active-tag none` to opt out.
425
428
  *
426
- * Note: there is no auto-activate-first-group fallback. Coloring is
427
- * opt-in: either the source carries `active-tag <name>` or the caller
428
- * (typically the app on user click) supplies a programmatic override.
429
- * Static exports therefore render the legend as a row of collapsed
430
- * pills with nodes uncolored — consistent with the app's pre-click
431
- * default.
432
- *
433
- * @param _tagGroups Declared tag groups (kept for API stability; unused since auto-activation removed)
429
+ * @param tagGroups Declared tag groups (only `.name` is used)
434
430
  * @param explicitActiveTag Value of `active-tag` option from parsed diagram, if any
435
431
  * @param programmaticOverride Value from render API / CLI; `undefined` = not set,
436
432
  * `null` or `''` = explicitly no coloring
437
433
  */
438
434
  export function resolveActiveTagGroup(
439
- _tagGroups: ReadonlyArray<{ name: string }>,
435
+ tagGroups: ReadonlyArray<{ name: string }>,
440
436
  explicitActiveTag: string | undefined,
441
437
  programmaticOverride?: string | null
442
438
  ): string | null {
@@ -453,7 +449,10 @@ export function resolveActiveTagGroup(
453
449
  return explicitActiveTag;
454
450
  }
455
451
 
456
- // 3. No explicit activation → no coloring (collapsed-by-default)
452
+ // 3. Auto-activate first declared group
453
+ if (tagGroups.length > 0) return tagGroups[0].name;
454
+
455
+ // 4. No tag groups → no coloring
457
456
  return null;
458
457
  }
459
458
 
@@ -333,7 +333,8 @@ function layoutElement(
333
333
 
334
334
  // Container — layout children
335
335
  const isInlineRow =
336
- el.metadata._inlineRow === 'true' || el.metadata._labelField === 'true';
336
+ el.metadata['_inlineRow'] === 'true' ||
337
+ el.metadata['_labelField'] === 'true';
337
338
  const padTop = isInlineRow ? 0 : GROUP_PADDING_TOP;
338
339
  const padBottom = isInlineRow ? 0 : GROUP_PADDING_BOTTOM;
339
340
  const padX = isInlineRow ? 0 : GROUP_PADDING_X;
@@ -402,8 +403,8 @@ function allocateEqualWidths(
402
403
  function getElementHeight(el: WireframeElement): number {
403
404
  if (el.type === 'heading') {
404
405
  return el.headingLevel === 2
405
- ? (ELEMENT_HEIGHTS.subheading ?? 36)
406
- : (ELEMENT_HEIGHTS.heading ?? 48);
406
+ ? (ELEMENT_HEIGHTS['subheading'] ?? 36)
407
+ : (ELEMENT_HEIGHTS['heading'] ?? 48);
407
408
  }
408
409
 
409
410
  if (el.type === 'textInput' && el.fieldVariant === 'textarea') {
@@ -421,11 +422,11 @@ function getElementHeight(el: WireframeElement): number {
421
422
  if (el.type === 'image') {
422
423
  if (el.imageHint === 'round') return 80;
423
424
  if (el.imageHint === 'wide') return 80;
424
- return ELEMENT_HEIGHTS.image ?? 120;
425
+ return ELEMENT_HEIGHTS['image'] ?? 120;
425
426
  }
426
427
 
427
428
  // Label-field wrapper
428
- if (el.metadata._labelField === 'true') {
429
+ if (el.metadata['_labelField'] === 'true') {
429
430
  return 36; // input height
430
431
  }
431
432
 
@@ -434,7 +435,7 @@ function getElementHeight(el: WireframeElement): number {
434
435
 
435
436
  function getSpacingAfter(el: WireframeElement): number {
436
437
  if (el.type === 'heading' && el.headingLevel === 2) {
437
- return SPACING_AFTER.subheading ?? 12;
438
+ return SPACING_AFTER['subheading'] ?? 12;
438
439
  }
439
440
  return SPACING_AFTER[el.type] ?? 8;
440
441
  }
@@ -448,7 +449,10 @@ function computeFieldAlignX(children: WireframeElement[]): number {
448
449
  let labelFieldCount = 0;
449
450
 
450
451
  for (const child of children) {
451
- if (child.metadata._labelField === 'true' && child.children.length >= 2) {
452
+ if (
453
+ child.metadata['_labelField'] === 'true' &&
454
+ child.children.length >= 2
455
+ ) {
452
456
  const labelEl = child.children[0];
453
457
  const labelWidth = labelEl.label.length * CHAR_WIDTH;
454
458
  maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
@@ -650,7 +650,7 @@ export function parseWireframe(content: string): ParsedWireframe {
650
650
  wrapper.isContainer = true;
651
651
  wrapper.orientation = 'horizontal';
652
652
  wrapper.children = children;
653
- wrapper.metadata._inlineRow = 'true';
653
+ wrapper.metadata['_inlineRow'] = 'true';
654
654
  pushElement(wrapper);
655
655
  }
656
656
 
@@ -852,7 +852,7 @@ export function parseWireframe(content: string): ParsedWireframe {
852
852
  wrapper.isContainer = true;
853
853
  wrapper.orientation = 'horizontal';
854
854
  wrapper.children.push(labelEl, fieldEl);
855
- wrapper.metadata._labelField = 'true';
855
+ wrapper.metadata['_labelField'] = 'true';
856
856
  pushElement(wrapper);
857
857
  }
858
858
  } else {