@diagrammo/dgmo 0.8.11 → 0.8.12

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.
@@ -0,0 +1,36 @@
1
+ er Pirate Fleet
2
+
3
+ ships
4
+ id int pk
5
+ name varchar
6
+ ship_type varchar
7
+ cannons int
8
+ 1-aboard-* crew_members
9
+ 1-1 captains
10
+ 1-carries-* treasure
11
+
12
+ captains
13
+ id int pk
14
+ name varchar
15
+ ship_id int fk
16
+ bounty int
17
+ ?-frequents-1 ports
18
+ *-has-1 crew_members
19
+
20
+ crew_members
21
+ id int pk
22
+ name varchar
23
+ ship_id int fk
24
+ role varchar nullable
25
+
26
+ treasure
27
+ id int pk
28
+ name varchar
29
+ value int
30
+ ship_id int fk, nullable
31
+
32
+ ports
33
+ id int pk
34
+ name varchar
35
+ region varchar unique
36
+ 1-docks-* ships
@@ -0,0 +1,27 @@
1
+ kanban Plunder Sprint 7
2
+
3
+ tag Priority
4
+ Low(green)
5
+ Urgent(red)
6
+ High(orange)
7
+
8
+ tag Crew c
9
+ Blackbeard(red)
10
+ Anne Bonny(purple)
11
+ Calico Jack(teal)
12
+
13
+ [Awaiting Orders](red)
14
+ Recruit gunners at Tortuga | priority: High, c: Calico Jack
15
+ Chart new trade route | priority: Urgent, c: Anne Bonny
16
+ Scout the Windward Passage
17
+ Avoid Royal Navy patrols
18
+ Resupply rum and powder | priority: Low, c: Calico Jack
19
+
20
+ [Underway](orange) | wip: 2
21
+ Forge letters of marque | priority: High, c: Anne Bonny
22
+ Raid merchant convoy | priority: Urgent, c: Blackbeard
23
+ Three ships spotted off Nassau
24
+
25
+ [Done](green)
26
+ Bribe the harbour master | priority: High, c: Anne Bonny
27
+ Repair hull damage | priority: Low, c: Blackbeard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@ import {
9
9
  matchTagBlockHeading,
10
10
  injectDefaultTagMetadata,
11
11
  validateTagValues,
12
+ validateTagGroupNames,
12
13
  stripDefaultModifier,
13
14
  } from '../utils/tag-groups';
14
15
  import type { TagGroup } from '../utils/tag-groups';
@@ -531,6 +532,7 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
531
532
  if (result.tagGroups.length > 0) {
532
533
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
533
534
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
535
+ validateTagGroupNames(result.tagGroups, pushWarning);
534
536
  }
535
537
 
536
538
  return result;
@@ -14,7 +14,7 @@ import {
14
14
  TITLE_Y,
15
15
  } from '../utils/title-constants';
16
16
  import { contrastText, mix } from '../palettes/color-utils';
17
- import { resolveTagColor } from '../utils/tag-groups';
17
+ import { resolveTagColor, resolveActiveTagGroup } from '../utils/tag-groups';
18
18
  import type { TagGroup } from '../utils/tag-groups';
19
19
  import type { PaletteColors } from '../palettes';
20
20
  import type { ParsedBoxesAndLines, BLNode } from './types';
@@ -315,8 +315,12 @@ export function renderBoxesAndLines(
315
315
  const height = exportDims?.height ?? container.clientHeight;
316
316
  if (width <= 0 || height <= 0) return;
317
317
 
318
- // Determine active tag group
319
- const activeGroup = activeTagGroup ?? parsed.options['active-tag'] ?? null;
318
+ // Determine active tag group — shared utility handles priority chain
319
+ const activeGroup = resolveActiveTagGroup(
320
+ parsed.tagGroups,
321
+ parsed.options['active-tag'],
322
+ activeTagGroup
323
+ );
320
324
 
321
325
  // Build hidden set
322
326
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
@@ -736,9 +740,13 @@ export function renderBoxesAndLinesForExport(
736
740
  layout: BLLayoutResult,
737
741
  palette: PaletteColors,
738
742
  isDark: boolean,
739
- options?: { exportDims?: { width: number; height: number } }
743
+ options?: {
744
+ exportDims?: { width: number; height: number };
745
+ activeTagGroup?: string | null;
746
+ }
740
747
  ): void {
741
748
  renderBoxesAndLines(container, parsed, layout, palette, isDark, {
742
749
  exportDims: options?.exportDims,
750
+ activeTagGroup: options?.activeTagGroup,
743
751
  });
744
752
  }
package/src/c4/parser.ts CHANGED
@@ -8,6 +8,7 @@ import type { TagGroup } from '../utils/tag-groups';
8
8
  import {
9
9
  matchTagBlockHeading,
10
10
  stripDefaultModifier,
11
+ validateTagGroupNames,
11
12
  } from '../utils/tag-groups';
12
13
  import { inferParticipantType } from '../sequence/participant-inference';
13
14
  import {
@@ -84,7 +85,7 @@ const VALID_SHAPES = new Set<string>([
84
85
  ]);
85
86
 
86
87
  /** Known top-level option keys for C4 diagrams. */
87
- const KNOWN_C4_OPTIONS = new Set<string>(['layout']);
88
+ const KNOWN_C4_OPTIONS = new Set<string>(['layout', 'active-tag']);
88
89
 
89
90
  /** Known C4 boolean options (bare keyword = on). */
90
91
  const KNOWN_C4_BOOLEANS = new Set<string>(['direction-tb']);
@@ -829,6 +830,9 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
829
830
  // ── Post-parse validation ───────────────────────────────
830
831
  validateRelationshipTargets(result, knownNames, pushError);
831
832
  validateDeploymentRefs(result, knownNames, pushError);
833
+ validateTagGroupNames(result.tagGroups, (line, msg) =>
834
+ pushError(line, msg, 'warning')
835
+ );
832
836
 
833
837
  return result;
834
838
  }
package/src/completion.ts CHANGED
@@ -258,21 +258,33 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
258
258
  },
259
259
  }),
260
260
  ],
261
- ['er', withGlobals()],
261
+ [
262
+ 'er',
263
+ withGlobals({
264
+ 'active-tag': { description: 'Active tag group name' },
265
+ }),
266
+ ],
262
267
  [
263
268
  'org',
264
269
  withGlobals({
265
270
  'sub-node-label': { description: 'Label for sub-nodes' },
266
271
  'show-sub-node-count': { description: 'Show sub-node counts' },
272
+ 'active-tag': { description: 'Active tag group name' },
267
273
  }),
268
274
  ],
269
275
  [
270
276
  'kanban',
271
277
  withGlobals({
272
278
  'no-auto-color': { description: 'Disable automatic card coloring' },
279
+ 'active-tag': { description: 'Active tag group name' },
280
+ }),
281
+ ],
282
+ [
283
+ 'c4',
284
+ withGlobals({
285
+ 'active-tag': { description: 'Active tag group name' },
273
286
  }),
274
287
  ],
275
- ['c4', withGlobals()],
276
288
  [
277
289
  'state',
278
290
  withGlobals({
@@ -284,6 +296,7 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
284
296
  'sitemap',
285
297
  withGlobals({
286
298
  'direction-tb': { description: 'Switch to top-to-bottom layout' },
299
+ 'active-tag': { description: 'Active tag group name' },
287
300
  }),
288
301
  ],
289
302
  [
@@ -298,6 +311,7 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
298
311
  'slo-availability': { description: 'SLO availability target (0-1)' },
299
312
  'slo-p90-latency-ms': { description: 'SLO p90 latency target in ms' },
300
313
  'slo-warning-margin': { description: 'SLO warning margin percentage' },
314
+ 'active-tag': { description: 'Active tag group name' },
301
315
  }),
302
316
  ],
303
317
  [
@@ -310,6 +324,7 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
310
324
  sort: { description: 'Sort order', values: ['time', 'group', 'tag'] },
311
325
  'critical-path': { description: 'Show critical path' },
312
326
  dependencies: { description: 'Show dependencies' },
327
+ 'active-tag': { description: 'Active tag group name' },
313
328
  }),
314
329
  ],
315
330
  [
package/src/d3.ts CHANGED
@@ -193,7 +193,9 @@ import {
193
193
  import {
194
194
  matchTagBlockHeading,
195
195
  validateTagValues,
196
+ validateTagGroupNames,
196
197
  resolveTagColor,
198
+ resolveActiveTagGroup,
197
199
  stripDefaultModifier,
198
200
  } from './utils/tag-groups';
199
201
  import type { TagGroup } from './utils/tag-groups';
@@ -1477,6 +1479,9 @@ export function parseVisualization(
1477
1479
  result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1478
1480
  suggest
1479
1481
  );
1482
+ validateTagGroupNames(result.timelineTagGroups, (line, msg) =>
1483
+ result.diagnostics.push(makeDgmoError(line, msg, 'warning'))
1484
+ );
1480
1485
  for (const group of result.timelineTagGroups) {
1481
1486
  if (!group.defaultValue) continue;
1482
1487
  const key = group.name.toLowerCase();
@@ -3405,6 +3410,78 @@ function buildEraTooltipHtml(era: TimelineEra): string {
3405
3410
  // Timeline Renderer
3406
3411
  // ============================================================
3407
3412
 
3413
+ /**
3414
+ * Renders timeline group legend as pills (colored dot + text in rounded rect),
3415
+ * matching the centralized legend pill style.
3416
+ */
3417
+ function renderTimelineGroupLegend(
3418
+ g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
3419
+ groups: TimelineGroup[],
3420
+ groupColorMap: Map<string, string>,
3421
+ textColor: string,
3422
+ palette: PaletteColors,
3423
+ isDark: boolean,
3424
+ legendY: number,
3425
+ onHover: (name: string) => void,
3426
+ onLeave: () => void
3427
+ ): void {
3428
+ const PILL_H = 22;
3429
+ const DOT_R = 4;
3430
+ const DOT_GAP = 4;
3431
+ const PAD_X = 10;
3432
+ const FONT_SIZE = 11;
3433
+ const GAP = 8;
3434
+ const pillBg = isDark
3435
+ ? mix(palette.surface, palette.bg, 50)
3436
+ : mix(palette.surface, palette.bg, 30);
3437
+
3438
+ let legendX = 0;
3439
+ for (const grp of groups) {
3440
+ const color = groupColorMap.get(grp.name) ?? textColor;
3441
+ const textW = measureLegendText(grp.name, FONT_SIZE);
3442
+ const pillW = PAD_X + DOT_R * 2 + DOT_GAP + textW + PAD_X;
3443
+
3444
+ const itemG = g
3445
+ .append('g')
3446
+ .attr('class', 'tl-legend-item')
3447
+ .attr('data-group', grp.name)
3448
+ .style('cursor', 'pointer')
3449
+ .on('mouseenter', () => onHover(grp.name))
3450
+ .on('mouseleave', () => onLeave());
3451
+
3452
+ // Pill background
3453
+ itemG
3454
+ .append('rect')
3455
+ .attr('x', legendX)
3456
+ .attr('y', legendY - PILL_H / 2)
3457
+ .attr('width', pillW)
3458
+ .attr('height', PILL_H)
3459
+ .attr('rx', PILL_H / 2)
3460
+ .attr('fill', pillBg);
3461
+
3462
+ // Colored dot
3463
+ itemG
3464
+ .append('circle')
3465
+ .attr('cx', legendX + PAD_X + DOT_R)
3466
+ .attr('cy', legendY)
3467
+ .attr('r', DOT_R)
3468
+ .attr('fill', color);
3469
+
3470
+ // Label text
3471
+ itemG
3472
+ .append('text')
3473
+ .attr('x', legendX + PAD_X + DOT_R * 2 + DOT_GAP)
3474
+ .attr('y', legendY)
3475
+ .attr('dy', '0.35em')
3476
+ .attr('fill', textColor)
3477
+ .attr('font-size', `${FONT_SIZE}px`)
3478
+ .attr('font-family', FONT_FAMILY)
3479
+ .text(grp.name);
3480
+
3481
+ legendX += pillW + GAP;
3482
+ }
3483
+ }
3484
+
3408
3485
  /**
3409
3486
  * Renders a timeline chart into the given container using D3.
3410
3487
  * Supports horizontal (default) and vertical orientation.
@@ -4038,38 +4115,19 @@ export function renderTimeline(
4038
4115
  );
4039
4116
  }
4040
4117
 
4041
- // Group legend
4118
+ // Group legend (pill style)
4042
4119
  if (timelineGroups.length > 0) {
4043
- let legendX = 0;
4044
- const legendY = -55;
4045
- for (const grp of timelineGroups) {
4046
- const color = groupColorMap.get(grp.name) ?? textColor;
4047
- const itemG = g
4048
- .append('g')
4049
- .attr('class', 'tl-legend-item')
4050
- .attr('data-group', grp.name)
4051
- .style('cursor', 'pointer')
4052
- .on('mouseenter', () => fadeToGroup(g, grp.name))
4053
- .on('mouseleave', () => fadeReset(g));
4054
-
4055
- itemG
4056
- .append('circle')
4057
- .attr('cx', legendX)
4058
- .attr('cy', legendY)
4059
- .attr('r', 5)
4060
- .attr('fill', color);
4061
-
4062
- itemG
4063
- .append('text')
4064
- .attr('x', legendX + 10)
4065
- .attr('y', legendY)
4066
- .attr('dy', '0.35em')
4067
- .attr('fill', textColor)
4068
- .attr('font-size', '11px')
4069
- .text(grp.name);
4070
-
4071
- legendX += grp.name.length * 7 + 30;
4072
- }
4120
+ renderTimelineGroupLegend(
4121
+ g,
4122
+ timelineGroups,
4123
+ groupColorMap,
4124
+ textColor,
4125
+ palette,
4126
+ isDark,
4127
+ -55,
4128
+ (name) => fadeToGroup(g, name),
4129
+ () => fadeReset(g)
4130
+ );
4073
4131
  }
4074
4132
 
4075
4133
  g.append('line')
@@ -4656,38 +4714,20 @@ export function renderTimeline(
4656
4714
  );
4657
4715
  }
4658
4716
 
4659
- // Group legend at top-left
4717
+ // Group legend at top-left (pill style)
4660
4718
  if (timelineGroups.length > 0) {
4661
- let legendX = 0;
4662
4719
  const legendY = timelineScale ? -75 : -55;
4663
- for (const grp of timelineGroups) {
4664
- const color = groupColorMap.get(grp.name) ?? textColor;
4665
- const itemG = g
4666
- .append('g')
4667
- .attr('class', 'tl-legend-item')
4668
- .attr('data-group', grp.name)
4669
- .style('cursor', 'pointer')
4670
- .on('mouseenter', () => fadeToGroup(g, grp.name))
4671
- .on('mouseleave', () => fadeReset(g));
4672
-
4673
- itemG
4674
- .append('circle')
4675
- .attr('cx', legendX)
4676
- .attr('cy', legendY)
4677
- .attr('r', 5)
4678
- .attr('fill', color);
4679
-
4680
- itemG
4681
- .append('text')
4682
- .attr('x', legendX + 10)
4683
- .attr('y', legendY)
4684
- .attr('dy', '0.35em')
4685
- .attr('fill', textColor)
4686
- .attr('font-size', '11px')
4687
- .text(grp.name);
4688
-
4689
- legendX += grp.name.length * 7 + 30;
4690
- }
4720
+ renderTimelineGroupLegend(
4721
+ g,
4722
+ timelineGroups,
4723
+ groupColorMap,
4724
+ textColor,
4725
+ palette,
4726
+ isDark,
4727
+ legendY,
4728
+ (name) => fadeToGroup(g, name),
4729
+ () => fadeReset(g)
4730
+ );
4691
4731
  }
4692
4732
 
4693
4733
  sorted.forEach((ev, i) => {
@@ -6658,8 +6698,11 @@ export async function renderForExport(
6658
6698
 
6659
6699
  // Apply interactive collapse state when provided
6660
6700
  const collapsedNodes = orgExportState?.collapsedNodes;
6661
- const activeTagGroup =
6662
- orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6701
+ const activeTagGroup = resolveActiveTagGroup(
6702
+ orgParsed.tagGroups,
6703
+ orgParsed.options['active-tag'],
6704
+ orgExportState?.activeTagGroup ?? options?.tagGroup
6705
+ );
6663
6706
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6664
6707
 
6665
6708
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6709,8 +6752,11 @@ export async function renderForExport(
6709
6752
 
6710
6753
  // Apply interactive collapse state when provided
6711
6754
  const collapsedNodes = orgExportState?.collapsedNodes;
6712
- const activeTagGroup =
6713
- orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6755
+ const activeTagGroup = resolveActiveTagGroup(
6756
+ sitemapParsed.tagGroups,
6757
+ sitemapParsed.options['active-tag'],
6758
+ orgExportState?.activeTagGroup ?? options?.tagGroup
6759
+ );
6714
6760
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6715
6761
 
6716
6762
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6767,7 +6813,11 @@ export async function renderForExport(
6767
6813
  theme === 'dark',
6768
6814
  undefined,
6769
6815
  undefined,
6770
- options?.tagGroup
6816
+ resolveActiveTagGroup(
6817
+ kanbanParsed.tagGroups,
6818
+ kanbanParsed.options['active-tag'],
6819
+ options?.tagGroup
6820
+ )
6771
6821
  );
6772
6822
  return finalizeSvgExport(container, theme, effectivePalette, options);
6773
6823
  }
@@ -6824,7 +6874,11 @@ export async function renderForExport(
6824
6874
  theme === 'dark',
6825
6875
  undefined,
6826
6876
  { width: exportWidth, height: exportHeight },
6827
- options?.tagGroup
6877
+ resolveActiveTagGroup(
6878
+ erParsed.tagGroups,
6879
+ erParsed.options['active-tag'],
6880
+ options?.tagGroup
6881
+ )
6828
6882
  );
6829
6883
  return finalizeSvgExport(container, theme, effectivePalette, options);
6830
6884
  }
@@ -6852,7 +6906,10 @@ export async function renderForExport(
6852
6906
  blLayout,
6853
6907
  effectivePalette,
6854
6908
  theme === 'dark',
6855
- { exportDims: { width: exportWidth, height: exportHeight } }
6909
+ {
6910
+ exportDims: { width: exportWidth, height: exportHeight },
6911
+ activeTagGroup: options?.tagGroup,
6912
+ }
6856
6913
  );
6857
6914
  return finalizeSvgExport(container, theme, effectivePalette, options);
6858
6915
  }
@@ -6909,7 +6966,11 @@ export async function renderForExport(
6909
6966
  theme === 'dark',
6910
6967
  undefined,
6911
6968
  { width: exportWidth, height: exportHeight },
6912
- options?.tagGroup
6969
+ resolveActiveTagGroup(
6970
+ c4Parsed.tagGroups,
6971
+ c4Parsed.options['active-tag'],
6972
+ options?.tagGroup
6973
+ )
6913
6974
  );
6914
6975
  return finalizeSvgExport(container, theme, effectivePalette, options);
6915
6976
  }
@@ -6951,7 +7012,11 @@ export async function renderForExport(
6951
7012
 
6952
7013
  const infraComputed = computeInfra(infraParsed);
6953
7014
  const infraLayout = layoutInfra(infraComputed);
6954
- const activeTagGroup = options?.tagGroup ?? null;
7015
+ const activeTagGroup = resolveActiveTagGroup(
7016
+ infraParsed.tagGroups,
7017
+ infraParsed.options['active-tag'],
7018
+ options?.tagGroup
7019
+ );
6955
7020
 
6956
7021
  const titleOffset = infraParsed.title ? 40 : 0;
6957
7022
  const legendGroups = computeInfraLegendGroups(
@@ -7104,7 +7169,11 @@ export async function renderForExport(
7104
7169
  isDark,
7105
7170
  undefined,
7106
7171
  dims,
7107
- orgExportState?.activeTagGroup ?? options?.tagGroup,
7172
+ resolveActiveTagGroup(
7173
+ parsed.timelineTagGroups,
7174
+ undefined,
7175
+ orgExportState?.activeTagGroup ?? options?.tagGroup
7176
+ ),
7108
7177
  orgExportState?.swimlaneTagGroup
7109
7178
  );
7110
7179
  } else if (parsed.type === 'venn') {
package/src/echarts.ts CHANGED
@@ -977,7 +977,7 @@ function buildChordOption(
977
977
  };
978
978
  });
979
979
  })(),
980
- roam: true,
980
+ roam: false,
981
981
  label: {
982
982
  position: 'right',
983
983
  formatter: '{b}',
package/src/er/parser.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  matchTagBlockHeading,
13
13
  validateTagValues,
14
+ validateTagGroupNames,
14
15
  stripDefaultModifier,
15
16
  } from '../utils/tag-groups';
16
17
  import type { TagGroup } from '../utils/tag-groups';
@@ -54,7 +55,7 @@ const CONSTRAINT_MAP: Record<string, ERConstraint> = {
54
55
  };
55
56
 
56
57
  // Known options (space-separated, no colon)
57
- const KNOWN_OPTIONS = new Set(['notation']);
58
+ const KNOWN_OPTIONS = new Set(['notation', 'active-tag']);
58
59
 
59
60
  // ============================================================
60
61
  // Cardinality parsing
@@ -418,6 +419,9 @@ export function parseERDiagram(
418
419
  result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
419
420
  suggest
420
421
  );
422
+ validateTagGroupNames(result.tagGroups, (line, msg) =>
423
+ result.diagnostics.push(makeDgmoError(line, msg, 'warning'))
424
+ );
421
425
 
422
426
  // Inject defaults for tables without explicit tags
423
427
  for (const group of result.tagGroups) {
@@ -8,6 +8,7 @@ import type { TagGroup } from '../utils/tag-groups';
8
8
  import {
9
9
  matchTagBlockHeading,
10
10
  stripDefaultModifier,
11
+ validateTagGroupNames,
11
12
  } from '../utils/tag-groups';
12
13
  import {
13
14
  measureIndent,
@@ -134,6 +135,7 @@ export function parseGantt(
134
135
  dependencies: true,
135
136
  sort: 'default',
136
137
  defaultSwimlaneGroup: null,
138
+ activeTag: null,
137
139
  optionLineNumbers: {},
138
140
  holidaysLineNumber: null,
139
141
  },
@@ -645,6 +647,9 @@ export function parseGantt(
645
647
  );
646
648
  }
647
649
  break;
650
+ case 'active-tag':
651
+ result.options.activeTag = value;
652
+ break;
648
653
  }
649
654
  continue;
650
655
  }
@@ -872,6 +877,8 @@ export function parseGantt(
872
877
  result.options.sort = 'default';
873
878
  }
874
879
 
880
+ validateTagGroupNames(result.tagGroups, warn);
881
+
875
882
  return result;
876
883
 
877
884
  // ── Helper: create a task ───────────────────────────────
@@ -1033,6 +1040,7 @@ const KNOWN_OPTIONS = new Set([
1033
1040
  'dependencies',
1034
1041
  'chart',
1035
1042
  'sort',
1043
+ 'active-tag',
1036
1044
  ]);
1037
1045
 
1038
1046
  /** Boolean options that can appear as bare keywords or with `no-` prefix. */
@@ -7,7 +7,7 @@ import * as d3Selection from 'd3-selection';
7
7
  import { FONT_FAMILY } from '../fonts';
8
8
  import { getSeriesColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
- import { resolveTagColor } from '../utils/tag-groups';
10
+ import { resolveTagColor, resolveActiveTagGroup } from '../utils/tag-groups';
11
11
  import { computeTimeTicks } from '../d3';
12
12
  import {
13
13
  LEGEND_HEIGHT,
@@ -227,12 +227,11 @@ export function renderGantt(
227
227
  // ── Compute layout dimensions ───────────────────────────
228
228
 
229
229
  const seriesColors = getSeriesColors(palette);
230
- let currentActiveGroup: string | null =
231
- options?.currentActiveGroup !== undefined
232
- ? options.currentActiveGroup
233
- : resolved.tagGroups.length > 0
234
- ? resolved.tagGroups[0].name
235
- : null;
230
+ let currentActiveGroup: string | null = resolveActiveTagGroup(
231
+ resolved.tagGroups,
232
+ resolved.options.activeTag ?? undefined,
233
+ options?.currentActiveGroup
234
+ );
236
235
  let criticalPathActive = false;
237
236
 
238
237
  // ── Build row list (structural vs tag mode) ─────────────
@@ -115,6 +115,7 @@ export interface GanttOptions {
115
115
  dependencies: boolean;
116
116
  sort: 'default' | 'tag';
117
117
  defaultSwimlaneGroup: string | null; // tag group name from `sort: tag:Team`
118
+ activeTag: string | null; // from `active-tag GroupName` or `active-tag none`
118
119
  /** Line numbers for option/block keywords — maps key to source line */
119
120
  optionLineNumbers: Record<string, number>;
120
121
  holidaysLineNumber: number | null;
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  matchTagBlockHeading,
17
17
  stripDefaultModifier,
18
+ validateTagGroupNames,
18
19
  } from '../utils/tag-groups';
19
20
  import type {
20
21
  ParsedInfra,
@@ -77,6 +78,7 @@ const TOP_LEVEL_OPTIONS = new Set([
77
78
  'default-latency-ms',
78
79
  'default-uptime',
79
80
  'default-rps',
81
+ 'active-tag',
80
82
  ]);
81
83
 
82
84
  // ============================================================
@@ -726,6 +728,8 @@ export function parseInfra(content: string): ParsedInfra {
726
728
  }
727
729
  }
728
730
 
731
+ validateTagGroupNames(result.tagGroups, warn);
732
+
729
733
  return result;
730
734
  }
731
735
 
@@ -4,6 +4,7 @@ import { resolveColor } from '../colors';
4
4
  import {
5
5
  matchTagBlockHeading,
6
6
  stripDefaultModifier,
7
+ validateTagGroupNames,
7
8
  } from '../utils/tag-groups';
8
9
  import {
9
10
  measureIndent,
@@ -29,7 +30,7 @@ const COLUMN_RE = /^\[(.+?)\](?:\s*\(([^)]+)\))?\s*(?:\|\s*(.+))?$/;
29
30
  const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
30
31
 
31
32
  /** Known kanban options (key-value). */
32
- const KNOWN_OPTIONS = new Set(['hide']);
33
+ const KNOWN_OPTIONS = new Set(['hide', 'active-tag']);
33
34
  /** Known kanban boolean options (bare keyword = on). */
34
35
  const KNOWN_BOOLEANS = new Set<string>(['no-auto-color']);
35
36
 
@@ -366,6 +367,8 @@ export function parseKanban(
366
367
  return fail(1, 'No columns found. Use [Column Name] to define columns');
367
368
  }
368
369
 
370
+ validateTagGroupNames(result.tagGroups, warn);
371
+
369
372
  return result;
370
373
  }
371
374