@diagrammo/dgmo 0.8.2 → 0.8.4

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 (120) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +185 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +189 -194
  9. package/dist/editor.cjs +336 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3699 -1564
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +7 -6
  18. package/dist/index.d.ts +7 -6
  19. package/dist/index.js +3699 -1564
  20. package/dist/index.js.map +1 -1
  21. package/docs/language-reference.md +822 -1060
  22. package/gallery/fixtures/arc.dgmo +18 -0
  23. package/gallery/fixtures/area.dgmo +19 -0
  24. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  25. package/gallery/fixtures/bar.dgmo +10 -0
  26. package/gallery/fixtures/c4-full.dgmo +52 -0
  27. package/gallery/fixtures/c4.dgmo +17 -0
  28. package/gallery/fixtures/chord.dgmo +12 -0
  29. package/gallery/fixtures/class-basic.dgmo +14 -0
  30. package/gallery/fixtures/class-full.dgmo +43 -0
  31. package/gallery/fixtures/doughnut.dgmo +8 -0
  32. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  33. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  35. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  36. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  37. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  38. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  39. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  40. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  41. package/gallery/fixtures/function.dgmo +8 -0
  42. package/gallery/fixtures/funnel.dgmo +7 -0
  43. package/gallery/fixtures/gantt-full.dgmo +49 -0
  44. package/gallery/fixtures/gantt.dgmo +42 -0
  45. package/gallery/fixtures/heatmap.dgmo +8 -0
  46. package/gallery/fixtures/infra-full.dgmo +78 -0
  47. package/gallery/fixtures/infra-overload.dgmo +25 -0
  48. package/gallery/fixtures/infra.dgmo +47 -0
  49. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  50. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  51. package/gallery/fixtures/initiative-status.dgmo +9 -0
  52. package/gallery/fixtures/line.dgmo +19 -0
  53. package/gallery/fixtures/multi-line.dgmo +11 -0
  54. package/gallery/fixtures/org-basic.dgmo +16 -0
  55. package/gallery/fixtures/org-full.dgmo +69 -0
  56. package/gallery/fixtures/org-teams.dgmo +25 -0
  57. package/gallery/fixtures/pie.dgmo +9 -0
  58. package/gallery/fixtures/polar-area.dgmo +8 -0
  59. package/gallery/fixtures/quadrant.dgmo +18 -0
  60. package/gallery/fixtures/radar.dgmo +8 -0
  61. package/gallery/fixtures/sankey.dgmo +31 -0
  62. package/gallery/fixtures/scatter.dgmo +21 -0
  63. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  64. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  65. package/gallery/fixtures/sequence.dgmo +35 -0
  66. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  67. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  68. package/gallery/fixtures/slope.dgmo +8 -0
  69. package/gallery/fixtures/spr-eras.dgmo +62 -0
  70. package/gallery/fixtures/state.dgmo +30 -0
  71. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  72. package/gallery/fixtures/timeline.dgmo +32 -0
  73. package/gallery/fixtures/venn.dgmo +10 -0
  74. package/gallery/fixtures/wordcloud.dgmo +24 -0
  75. package/package.json +51 -2
  76. package/src/c4/layout.ts +372 -90
  77. package/src/c4/parser.ts +113 -62
  78. package/src/chart.ts +149 -64
  79. package/src/class/parser.ts +84 -28
  80. package/src/class/renderer.ts +2 -2
  81. package/src/cli.ts +179 -77
  82. package/src/completion.ts +381 -182
  83. package/src/d3.ts +1026 -428
  84. package/src/dgmo-mermaid.ts +16 -13
  85. package/src/dgmo-router.ts +70 -24
  86. package/src/echarts.ts +682 -169
  87. package/src/editor/dgmo.grammar +69 -0
  88. package/src/editor/dgmo.grammar.d.ts +2 -0
  89. package/src/editor/dgmo.grammar.js +18 -0
  90. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  91. package/src/editor/dgmo.grammar.terms.js +35 -0
  92. package/src/editor/highlight.ts +36 -0
  93. package/src/editor/index.ts +28 -0
  94. package/src/editor/keywords.ts +220 -0
  95. package/src/editor/tokens.ts +30 -0
  96. package/src/er/parser.ts +55 -29
  97. package/src/er/renderer.ts +112 -53
  98. package/src/gantt/calculator.ts +91 -29
  99. package/src/gantt/parser.ts +291 -97
  100. package/src/gantt/renderer.ts +1120 -350
  101. package/src/graph/flowchart-parser.ts +48 -75
  102. package/src/graph/state-parser.ts +54 -27
  103. package/src/infra/parser.ts +161 -177
  104. package/src/infra/renderer.ts +723 -271
  105. package/src/infra/types.ts +0 -1
  106. package/src/initiative-status/parser.ts +144 -56
  107. package/src/kanban/parser.ts +27 -19
  108. package/src/org/layout.ts +111 -44
  109. package/src/org/parser.ts +71 -27
  110. package/src/org/resolver.ts +3 -3
  111. package/src/palettes/index.ts +3 -2
  112. package/src/render.ts +1 -2
  113. package/src/sequence/parser.ts +209 -100
  114. package/src/sitemap/parser.ts +73 -44
  115. package/src/utils/arrows.ts +2 -22
  116. package/src/utils/duration.ts +39 -21
  117. package/src/utils/legend-constants.ts +0 -2
  118. package/src/utils/parsing.ts +82 -72
  119. package/src/utils/tag-groups.ts +4 -41
  120. package/src/infra/serialize.ts +0 -67
@@ -6,26 +6,22 @@ import type { PaletteColors } from '../palettes';
6
6
  import { resolveColor } from '../colors';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
8
  import type { TagGroup } from '../utils/tag-groups';
9
- import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
9
+ import {
10
+ isTagBlockHeading,
11
+ matchTagBlockHeading,
12
+ validateTagValues,
13
+ } from '../utils/tag-groups';
10
14
  import {
11
15
  measureIndent,
12
16
  extractColor,
13
17
  parsePipeMetadata,
14
- normalizeDirection,
15
18
  inferArrowColor,
16
- MULTIPLE_PIPE_WARNING,
17
- CHART_TYPE_RE,
18
- TITLE_RE,
19
- OPTION_RE,
19
+ MULTIPLE_PIPE_ERROR,
20
20
  parseFirstLine,
21
21
  OPTION_NOCOLON_RE,
22
22
  ALL_CHART_TYPES,
23
23
  } from '../utils/parsing';
24
- import type {
25
- SitemapNode,
26
- SitemapDirection,
27
- ParsedSitemap,
28
- } from './types';
24
+ import type { SitemapNode, ParsedSitemap } from './types';
29
25
 
30
26
  // ============================================================
31
27
  // Regexes
@@ -48,7 +44,7 @@ const BARE_ARROW_RE = /^->\s*(.+)$/;
48
44
 
49
45
  function parseArrowLine(
50
46
  trimmed: string,
51
- palette?: PaletteColors,
47
+ palette?: PaletteColors
52
48
  ): { label?: string; color?: string; target: string } | null {
53
49
  // Bare arrow: -> Target
54
50
  const bareMatch = trimmed.match(BARE_ARROW_RE);
@@ -61,7 +57,7 @@ function parseArrowLine(
61
57
  if (arrowMatch) {
62
58
  const label = arrowMatch[1]?.trim() || undefined;
63
59
  let color = arrowMatch[2]
64
- ? resolveColor(arrowMatch[2].trim(), palette) ?? undefined
60
+ ? (resolveColor(arrowMatch[2].trim(), palette) ?? undefined)
65
61
  : undefined;
66
62
  if (label && !color) {
67
63
  color = inferArrowColor(label);
@@ -92,7 +88,7 @@ export function looksLikeSitemap(content: string): boolean {
92
88
  if (!trimmed || trimmed.startsWith('//')) continue;
93
89
 
94
90
  // Skip header lines
95
- if (CHART_TYPE_RE.test(trimmed) || TITLE_RE.test(trimmed)) continue;
91
+ if (parseFirstLine(trimmed)) continue;
96
92
  if (isTagBlockHeading(trimmed)) continue;
97
93
 
98
94
  if (/^-.*->\s*.+/.test(trimmed) || /^->\s*.+/.test(trimmed)) {
@@ -108,7 +104,7 @@ export function looksLikeSitemap(content: string): boolean {
108
104
  // Exclude flowchart: flowchart arrows connect shaped nodes like (X) -> [Y]
109
105
  // Sitemap arrows are indented under a parent node, target is plain text
110
106
  const hasFlowchartShapes =
111
- /[\])][ \t]*-.*->/.test(content) || /->[ \t]*[\[(<\/]/.test(content);
107
+ /[\])][ \t]*-.*->/.test(content) || /->[ \t]*[[(</]/.test(content);
112
108
 
113
109
  return !hasFlowchartShapes;
114
110
  }
@@ -119,12 +115,12 @@ export function looksLikeSitemap(content: string): boolean {
119
115
 
120
116
  export function parseSitemap(
121
117
  content: string,
122
- palette?: PaletteColors,
118
+ palette?: PaletteColors
123
119
  ): ParsedSitemap {
124
120
  const result: ParsedSitemap = {
125
121
  title: null,
126
122
  titleLineNumber: null,
127
- direction: 'TB',
123
+ direction: 'LR',
128
124
  roots: [],
129
125
  edges: [],
130
126
  tagGroups: [],
@@ -226,10 +222,6 @@ export function parseSitemap(
226
222
  pushError(lineNumber, 'Tag groups must appear before sitemap content');
227
223
  continue;
228
224
  }
229
- if (tagBlockMatch.deprecated) {
230
- pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
231
- continue;
232
- }
233
225
  currentTagGroup = {
234
226
  name: tagBlockMatch.name,
235
227
  alias: tagBlockMatch.alias,
@@ -237,7 +229,10 @@ export function parseSitemap(
237
229
  lineNumber,
238
230
  };
239
231
  if (tagBlockMatch.alias) {
240
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
232
+ aliasMap.set(
233
+ tagBlockMatch.alias.toLowerCase(),
234
+ tagBlockMatch.name.toLowerCase()
235
+ );
241
236
  }
242
237
  result.tagGroups.push(currentTagGroup);
243
238
  continue;
@@ -245,18 +240,22 @@ export function parseSitemap(
245
240
 
246
241
  // Generic header options (space-separated, before content/tag groups)
247
242
  // Skip lines with `|` (pipe metadata) or `->` (arrows) — those are content
248
- if (!contentStarted && !currentTagGroup && measureIndent(line) === 0
249
- && !trimmed.includes('|') && !trimmed.includes('->')) {
243
+ if (
244
+ !contentStarted &&
245
+ !currentTagGroup &&
246
+ measureIndent(line) === 0 &&
247
+ !trimmed.includes('|') &&
248
+ !trimmed.includes('->')
249
+ ) {
250
+ // Bare boolean: direction-tb
251
+ if (/^direction-tb$/i.test(trimmed)) {
252
+ result.direction = 'TB';
253
+ continue;
254
+ }
255
+
250
256
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
251
257
  if (optMatch) {
252
258
  const key = optMatch[1].trim().toLowerCase();
253
- if (key === 'direction' || key === 'orientation') {
254
- const dir = normalizeDirection(optMatch[2]);
255
- if (dir) {
256
- result.direction = dir as SitemapDirection;
257
- }
258
- continue;
259
- }
260
259
  result.options[key] = optMatch[2].trim();
261
260
  continue;
262
261
  }
@@ -270,7 +269,7 @@ export function parseSitemap(
270
269
  if (!color) {
271
270
  pushError(
272
271
  lineNumber,
273
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
272
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
274
273
  );
275
274
  continue;
276
275
  }
@@ -286,7 +285,7 @@ export function parseSitemap(
286
285
  continue;
287
286
  }
288
287
  // Non-indented line after tag group — fall through to content
289
- currentTagGroup = null;
288
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
290
289
  }
291
290
 
292
291
  // --- Content phase ---
@@ -320,8 +319,9 @@ export function parseSitemap(
320
319
  const containerMatch = trimmed.match(CONTAINER_RE);
321
320
 
322
321
  // Check for metadata syntax: key: value
323
- const metadataMatch =
324
- trimmed.includes('|') ? null : trimmed.match(METADATA_RE);
322
+ const metadataMatch = trimmed.includes('|')
323
+ ? null
324
+ : trimmed.match(METADATA_RE);
325
325
 
326
326
  if (containerMatch) {
327
327
  const rawLabel = containerMatch[1].trim();
@@ -333,7 +333,10 @@ export function parseSitemap(
333
333
  if (pipeStr) {
334
334
  // Build segments array compatible with parsePipeMetadata (first element is label, rest are pipe parts)
335
335
  const pipeSegments = ['', pipeStr];
336
- Object.assign(containerMetadata, parsePipeMetadata(pipeSegments, aliasMap));
336
+ Object.assign(
337
+ containerMetadata,
338
+ parsePipeMetadata(pipeSegments, aliasMap)
339
+ );
337
340
  }
338
341
 
339
342
  containerCounter++;
@@ -365,7 +368,14 @@ export function parseSitemap(
365
368
  } else if (metadataMatch && indentStack.length === 0) {
366
369
  // Could be a node label containing ':'
367
370
  if (indent === 0) {
368
- const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
371
+ const node = parseNodeLabel(
372
+ trimmed,
373
+ lineNumber,
374
+ palette,
375
+ ++nodeCounter,
376
+ aliasMap,
377
+ pushWarning
378
+ );
369
379
  attachNode(node, indent, indentStack, result);
370
380
  labelToNode.set(node.label.toLowerCase(), node);
371
381
  } else {
@@ -373,7 +383,14 @@ export function parseSitemap(
373
383
  }
374
384
  } else {
375
385
  // Node label — possibly with pipe-delimited metadata
376
- const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
386
+ const node = parseNodeLabel(
387
+ trimmed,
388
+ lineNumber,
389
+ palette,
390
+ ++nodeCounter,
391
+ aliasMap,
392
+ pushWarning
393
+ );
377
394
  attachNode(node, indent, indentStack, result);
378
395
  labelToNode.set(node.label.toLowerCase(), node);
379
396
  }
@@ -416,7 +433,11 @@ export function parseSitemap(
416
433
  validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
417
434
  }
418
435
 
419
- if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
436
+ if (
437
+ result.roots.length === 0 &&
438
+ result.tagGroups.length === 0 &&
439
+ !result.error
440
+ ) {
420
441
  const diag = makeDgmoError(1, 'No pages found in sitemap');
421
442
  result.diagnostics.push(diag);
422
443
  result.error = formatDgmoError(diag);
@@ -435,12 +456,16 @@ function parseNodeLabel(
435
456
  palette: PaletteColors | undefined,
436
457
  counter: number,
437
458
  aliasMap: Map<string, string> = new Map(),
438
- warnFn?: (line: number, msg: string) => void,
459
+ warnFn?: (line: number, msg: string) => void
439
460
  ): SitemapNode {
440
461
  const segments = trimmed.split('|').map((s) => s.trim());
441
462
  const rawLabel = segments[0];
442
463
  const { label, color } = extractColor(rawLabel, palette);
443
- const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_WARNING) : undefined);
464
+ const metadata = parsePipeMetadata(
465
+ segments,
466
+ aliasMap,
467
+ warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined
468
+ );
444
469
 
445
470
  return {
446
471
  id: `node-${counter}`,
@@ -458,7 +483,7 @@ function attachNode(
458
483
  node: SitemapNode,
459
484
  indent: number,
460
485
  indentStack: { node: SitemapNode; indent: number }[],
461
- result: ParsedSitemap,
486
+ result: ParsedSitemap
462
487
  ): void {
463
488
  // Pop stack entries with indent >= current indent
464
489
  while (indentStack.length > 0) {
@@ -471,7 +496,11 @@ function attachNode(
471
496
  const parent = indentStack[indentStack.length - 1].node;
472
497
  node.parentId = parent.id;
473
498
  // Cascade container metadata to child nodes (child overrides on conflict)
474
- if (parent.isContainer && Object.keys(parent.metadata).length > 0 && !node.isContainer) {
499
+ if (
500
+ parent.isContainer &&
501
+ Object.keys(parent.metadata).length > 0 &&
502
+ !node.isContainer
503
+ ) {
475
504
  node.metadata = { ...parent.metadata, ...node.metadata };
476
505
  }
477
506
  parent.children.push(node);
@@ -484,7 +513,7 @@ function attachNode(
484
513
 
485
514
  function findParentNode(
486
515
  indent: number,
487
- indentStack: { node: SitemapNode; indent: number }[],
516
+ indentStack: { node: SitemapNode; indent: number }[]
488
517
  ): SitemapNode | null {
489
518
  for (let i = indentStack.length - 1; i >= 0; i--) {
490
519
  if (indentStack[i].indent < indent) {
@@ -34,7 +34,7 @@ const ARROW_CHARS = ['->', '~>'];
34
34
  * - `null` if not a labeled arrow (caller should fall through to bare patterns)
35
35
  */
36
36
  export function parseArrow(
37
- line: string,
37
+ line: string
38
38
  ): ParsedArrow | { error: string } | null {
39
39
  // Check bidi patterns first — return error
40
40
  if (BIDI_SYNC_RE.test(line) || BIDI_ASYNC_RE.test(line)) {
@@ -47,8 +47,7 @@ export function parseArrow(
47
47
  // Check deprecated return arrow patterns — return error
48
48
  if (RETURN_SYNC_LABELED_RE.test(line) || RETURN_ASYNC_LABELED_RE.test(line)) {
49
49
  const m =
50
- line.match(RETURN_SYNC_LABELED_RE) ??
51
- line.match(RETURN_ASYNC_LABELED_RE);
50
+ line.match(RETURN_SYNC_LABELED_RE) ?? line.match(RETURN_ASYNC_LABELED_RE);
52
51
  const from = m![3];
53
52
  const to = m![1];
54
53
  const label = m![2].trim();
@@ -93,22 +92,3 @@ export function parseArrow(
93
92
 
94
93
  return null;
95
94
  }
96
-
97
- /**
98
- * Match an arrow segment and extract label + async flag.
99
- * Handles: `->`, `-label->`, `~>`, `~label~>`.
100
- * Returns null if no arrow pattern matched.
101
- */
102
- export function matchArrowLabel(segment: string): { label: string; async: boolean } | null {
103
- // Async labeled: ~label~>
104
- const asyncLabeled = segment.match(/^~(.+?)~>$/);
105
- if (asyncLabeled) return { label: asyncLabeled[1].trim(), async: true };
106
- // Async bare: ~>
107
- if (segment.trim() === '~>') return { label: '', async: true };
108
- // Sync labeled: -label->
109
- const syncLabeled = segment.match(/^-(.+?)->$/);
110
- if (syncLabeled) return { label: syncLabeled[1].trim(), async: false };
111
- // Sync bare: ->
112
- if (segment.trim() === '->') return { label: '', async: false };
113
- return null;
114
- }
@@ -2,12 +2,26 @@
2
2
  // Duration & Business Day Arithmetic
3
3
  // ============================================================
4
4
 
5
- import type { Duration, DurationUnit, GanttHolidays, Offset, Weekday } from '../gantt/types';
5
+ import type {
6
+ Duration,
7
+ DurationUnit,
8
+ GanttHolidays,
9
+ Offset,
10
+ Weekday,
11
+ } from '../gantt/types';
6
12
 
7
13
  // ── Weekday constants ─────────────────────────────────────
8
14
 
9
15
  /** JS Date.getDay() → Weekday mapping (0=Sun, 1=Mon, ..., 6=Sat) */
10
- const JS_DAY_TO_WEEKDAY: Weekday[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
16
+ const JS_DAY_TO_WEEKDAY: Weekday[] = [
17
+ 'sun',
18
+ 'mon',
19
+ 'tue',
20
+ 'wed',
21
+ 'thu',
22
+ 'fri',
23
+ 'sat',
24
+ ];
11
25
 
12
26
  /**
13
27
  * Check if a date is a workday (not a weekend and not a holiday).
@@ -15,7 +29,7 @@ const JS_DAY_TO_WEEKDAY: Weekday[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri',
15
29
  export function isWorkday(
16
30
  date: Date,
17
31
  workweek: Weekday[],
18
- holidaySet: Set<string>,
32
+ holidaySet: Set<string>
19
33
  ): boolean {
20
34
  const dayName = JS_DAY_TO_WEEKDAY[date.getDay()];
21
35
  if (!workweek.includes(dayName)) return false;
@@ -68,7 +82,7 @@ export function addBusinessDays(
68
82
  count: number,
69
83
  workweek: Weekday[],
70
84
  holidaySet: Set<string>,
71
- direction: 1 | -1 = 1,
85
+ direction: 1 | -1 = 1
72
86
  ): Date {
73
87
  const days = Math.round(Math.abs(count));
74
88
  if (days === 0) return new Date(startDate);
@@ -97,13 +111,19 @@ export function addGanttDuration(
97
111
  duration: Duration,
98
112
  holidays: GanttHolidays,
99
113
  holidaySet: Set<string>,
100
- direction: 1 | -1 = 1,
114
+ direction: 1 | -1 = 1
101
115
  ): Date {
102
116
  const { amount, unit } = duration;
103
117
 
104
118
  switch (unit) {
105
119
  case 'bd':
106
- return addBusinessDays(startDate, amount, holidays.workweek, holidaySet, direction);
120
+ return addBusinessDays(
121
+ startDate,
122
+ amount,
123
+ holidays.workweek,
124
+ holidaySet,
125
+ direction
126
+ );
107
127
 
108
128
  case 'd': {
109
129
  const result = new Date(startDate);
@@ -119,8 +139,10 @@ export function addGanttDuration(
119
139
 
120
140
  case 'm': {
121
141
  const result = new Date(startDate);
122
- const wholeMonths = direction === -1 ? Math.round(amount) : Math.floor(amount);
123
- const fractionalDays = direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
142
+ const wholeMonths =
143
+ direction === -1 ? Math.round(amount) : Math.floor(amount);
144
+ const fractionalDays =
145
+ direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
124
146
  result.setMonth(result.getMonth() + wholeMonths * direction);
125
147
  if (fractionalDays > 0) {
126
148
  result.setDate(result.getDate() + fractionalDays * direction);
@@ -131,8 +153,10 @@ export function addGanttDuration(
131
153
  case 'q': {
132
154
  const result = new Date(startDate);
133
155
  const totalMonths = amount * 3;
134
- const wholeMonths = direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
135
- const fractionalDays = direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
156
+ const wholeMonths =
157
+ direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
158
+ const fractionalDays =
159
+ direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
136
160
  result.setMonth(result.getMonth() + wholeMonths * direction);
137
161
  if (fractionalDays > 0) {
138
162
  result.setDate(result.getDate() + fractionalDays * direction);
@@ -142,8 +166,10 @@ export function addGanttDuration(
142
166
 
143
167
  case 'y': {
144
168
  const result = new Date(startDate);
145
- const wholeYears = direction === -1 ? Math.round(amount) : Math.floor(amount);
146
- const fractionalMonths = direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
169
+ const wholeYears =
170
+ direction === -1 ? Math.round(amount) : Math.floor(amount);
171
+ const fractionalMonths =
172
+ direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
147
173
  result.setFullYear(result.getFullYear() + wholeYears * direction);
148
174
  if (fractionalMonths > 0) {
149
175
  result.setMonth(result.getMonth() + fractionalMonths * direction);
@@ -221,7 +247,7 @@ export function parseGanttDate(s: string): Date {
221
247
  }
222
248
  }
223
249
 
224
- const parts = datePart.split('-').map(p => parseInt(p, 10));
250
+ const parts = datePart.split('-').map((p) => parseInt(p, 10));
225
251
  const year = parts[0];
226
252
  const month = parts.length >= 2 ? parts[1] - 1 : 0; // JS months are 0-based
227
253
  const day = parts.length >= 3 ? parts[2] : 1;
@@ -238,11 +264,3 @@ export function formatGanttDate(date: Date): string {
238
264
  if (h === 0 && m === 0) return dateStr;
239
265
  return `${dateStr} ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
240
266
  }
241
-
242
- /**
243
- * Calculate the difference in calendar days between two dates.
244
- */
245
- export function daysBetween(a: Date, b: Date): number {
246
- const msPerDay = 86400000;
247
- return Math.round((b.getTime() - a.getTime()) / msPerDay);
248
- }
@@ -6,11 +6,9 @@
6
6
  export const LEGEND_HEIGHT = 28;
7
7
  export const LEGEND_PILL_PAD = 16;
8
8
  export const LEGEND_PILL_FONT_SIZE = 11;
9
- export const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
10
9
  export const LEGEND_CAPSULE_PAD = 4;
11
10
  export const LEGEND_DOT_R = 4;
12
11
  export const LEGEND_ENTRY_FONT_SIZE = 10;
13
- export const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
14
12
  export const LEGEND_ENTRY_DOT_GAP = 4;
15
13
  export const LEGEND_ENTRY_TRAIL = 8;
16
14
  export const LEGEND_GROUP_GAP = 12;