@diagrammo/dgmo 0.8.4 → 0.8.6
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.
- package/.claude/commands/dgmo.md +300 -0
- package/.cursorrules +20 -2
- package/.github/copilot-instructions.md +20 -2
- package/.windsurfrules +20 -2
- package/AGENTS.md +23 -3
- package/dist/cli.cjs +191 -189
- package/dist/editor.cjs +5 -18
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +5 -18
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +543 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +513 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3253 -3356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -56
- package/dist/index.d.ts +77 -56
- package/dist/index.js +3247 -3349
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +113 -33
- package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
- package/gallery/fixtures/slope.dgmo +7 -6
- package/package.json +26 -6
- package/src/boxes-and-lines/collapse.ts +78 -0
- package/src/boxes-and-lines/layout.ts +319 -0
- package/src/boxes-and-lines/parser.ts +694 -0
- package/src/boxes-and-lines/renderer.ts +848 -0
- package/src/boxes-and-lines/types.ts +40 -0
- package/src/c4/parser.ts +10 -5
- package/src/c4/renderer.ts +232 -56
- package/src/chart.ts +9 -4
- package/src/cli.ts +49 -6
- package/src/completion.ts +25 -33
- package/src/d3.ts +187 -46
- package/src/dgmo-router.ts +3 -7
- package/src/echarts.ts +38 -2
- package/src/editor/highlight-api.ts +444 -0
- package/src/editor/keywords.ts +6 -19
- package/src/er/parser.ts +10 -4
- package/src/gantt/parser.ts +7 -4
- package/src/gantt/renderer.ts +3 -5
- package/src/index.ts +106 -50
- package/src/infra/parser.ts +7 -5
- package/src/infra/renderer.ts +2 -2
- package/src/kanban/parser.ts +7 -5
- package/src/kanban/renderer.ts +43 -18
- package/src/org/parser.ts +7 -4
- package/src/org/renderer.ts +40 -29
- package/src/sequence/parser.ts +11 -5
- package/src/sequence/renderer.ts +114 -45
- package/src/sitemap/parser.ts +8 -4
- package/src/sitemap/renderer.ts +137 -57
- package/src/utils/legend-svg.ts +44 -20
- package/src/utils/parsing.ts +1 -1
- package/src/utils/tag-groups.ts +21 -1
- package/gallery/fixtures/initiative-status-full.dgmo +0 -46
- package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
- package/gallery/fixtures/initiative-status.dgmo +0 -9
- package/src/initiative-status/collapse.ts +0 -76
- package/src/initiative-status/filter.ts +0 -63
- package/src/initiative-status/layout.ts +0 -650
- package/src/initiative-status/parser.ts +0 -629
- package/src/initiative-status/renderer.ts +0 -1199
- package/src/initiative-status/types.ts +0 -57
package/src/sitemap/renderer.ts
CHANGED
|
@@ -8,10 +8,7 @@ import { FONT_FAMILY } from '../fonts';
|
|
|
8
8
|
import type { PaletteColors } from '../palettes';
|
|
9
9
|
import { mix } from '../palettes/color-utils';
|
|
10
10
|
import type { ParsedSitemap } from './types';
|
|
11
|
-
import type {
|
|
12
|
-
SitemapLayoutResult,
|
|
13
|
-
SitemapLegendGroup,
|
|
14
|
-
} from './layout';
|
|
11
|
+
import type { SitemapLayoutResult, SitemapLegendGroup } from './layout';
|
|
15
12
|
import {
|
|
16
13
|
LEGEND_HEIGHT,
|
|
17
14
|
LEGEND_PILL_PAD,
|
|
@@ -63,7 +60,11 @@ const LEGEND_FIXED_GAP = 8; // gap between fixed legend and scaled diagram — l
|
|
|
63
60
|
// Color helpers
|
|
64
61
|
// ============================================================
|
|
65
62
|
|
|
66
|
-
function nodeFill(
|
|
63
|
+
function nodeFill(
|
|
64
|
+
palette: PaletteColors,
|
|
65
|
+
isDark: boolean,
|
|
66
|
+
nodeColor?: string
|
|
67
|
+
): string {
|
|
67
68
|
const color = nodeColor ?? palette.primary;
|
|
68
69
|
return mix(color, isDark ? palette.surface : palette.bg, 25);
|
|
69
70
|
}
|
|
@@ -72,7 +73,11 @@ function nodeStroke(_palette: PaletteColors, nodeColor?: string): string {
|
|
|
72
73
|
return nodeColor ?? _palette.primary;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
function containerFill(
|
|
76
|
+
function containerFill(
|
|
77
|
+
palette: PaletteColors,
|
|
78
|
+
isDark: boolean,
|
|
79
|
+
nodeColor?: string
|
|
80
|
+
): string {
|
|
76
81
|
if (nodeColor) {
|
|
77
82
|
return mix(nodeColor, isDark ? palette.surface : palette.bg, 10);
|
|
78
83
|
}
|
|
@@ -87,7 +92,8 @@ function containerStroke(palette: PaletteColors, nodeColor?: string): string {
|
|
|
87
92
|
// Curve generator
|
|
88
93
|
// ============================================================
|
|
89
94
|
|
|
90
|
-
const lineGenerator = d3Shape
|
|
95
|
+
const lineGenerator = d3Shape
|
|
96
|
+
.line<{ x: number; y: number }>()
|
|
91
97
|
.x((d) => d.x)
|
|
92
98
|
.y((d) => d.y)
|
|
93
99
|
.curve(d3Shape.curveBasis);
|
|
@@ -107,7 +113,7 @@ export function renderSitemap(
|
|
|
107
113
|
onClickItem?: (lineNumber: number) => void,
|
|
108
114
|
exportDims?: { width?: number; height?: number },
|
|
109
115
|
activeTagGroup?: string | null,
|
|
110
|
-
hiddenAttributes?: Set<string
|
|
116
|
+
hiddenAttributes?: Set<string>
|
|
111
117
|
): void {
|
|
112
118
|
// Clear existing content
|
|
113
119
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
@@ -139,7 +145,8 @@ export function renderSitemap(
|
|
|
139
145
|
// Remove the legend space from diagram height — legend is rendered separately
|
|
140
146
|
diagramH -= layoutLegendShift;
|
|
141
147
|
}
|
|
142
|
-
const availH =
|
|
148
|
+
const availH =
|
|
149
|
+
height - DIAGRAM_PADDING * 2 - fixedReserveTop - fixedReserveBottom;
|
|
143
150
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
144
151
|
const scaleY = availH / diagramH;
|
|
145
152
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
@@ -236,7 +243,10 @@ export function renderSitemap(
|
|
|
236
243
|
for (const group of parsed.tagGroups) {
|
|
237
244
|
displayNames.set(group.name.toLowerCase(), group.name);
|
|
238
245
|
for (const entry of group.entries) {
|
|
239
|
-
tagColors.set(
|
|
246
|
+
tagColors.set(
|
|
247
|
+
`${group.name.toLowerCase()}:${entry.value.toLowerCase()}`,
|
|
248
|
+
entry.color
|
|
249
|
+
);
|
|
240
250
|
}
|
|
241
251
|
}
|
|
242
252
|
|
|
@@ -257,7 +267,9 @@ export function renderSitemap(
|
|
|
257
267
|
}
|
|
258
268
|
|
|
259
269
|
if (onClickItem) {
|
|
260
|
-
cG.style('cursor', 'pointer').on('click', () =>
|
|
270
|
+
cG.style('cursor', 'pointer').on('click', () =>
|
|
271
|
+
onClickItem(c.lineNumber)
|
|
272
|
+
);
|
|
261
273
|
}
|
|
262
274
|
|
|
263
275
|
// Tag metadata for legend hover dimming
|
|
@@ -284,7 +296,10 @@ export function renderSitemap(
|
|
|
284
296
|
// Container label
|
|
285
297
|
cG.append('text')
|
|
286
298
|
.attr('x', c.width / 2)
|
|
287
|
-
.attr(
|
|
299
|
+
.attr(
|
|
300
|
+
'y',
|
|
301
|
+
CONTAINER_HEADER_HEIGHT / 2 + CONTAINER_LABEL_FONT_SIZE / 2 - 2
|
|
302
|
+
)
|
|
288
303
|
.attr('text-anchor', 'middle')
|
|
289
304
|
.attr('fill', palette.text)
|
|
290
305
|
.attr('font-size', CONTAINER_LABEL_FONT_SIZE)
|
|
@@ -294,7 +309,9 @@ export function renderSitemap(
|
|
|
294
309
|
// Container metadata
|
|
295
310
|
const metaEntries = Object.entries(c.metadata);
|
|
296
311
|
if (metaEntries.length > 0) {
|
|
297
|
-
const metaDisplayKeys = metaEntries.map(
|
|
312
|
+
const metaDisplayKeys = metaEntries.map(
|
|
313
|
+
([k]) => displayNames.get(k) ?? k
|
|
314
|
+
);
|
|
298
315
|
const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
|
|
299
316
|
const valueX = 10 + (maxKeyLen + 2) * (CONTAINER_META_FONT_SIZE * 0.6);
|
|
300
317
|
const metaStartY = CONTAINER_HEADER_HEIGHT + CONTAINER_META_FONT_SIZE - 2;
|
|
@@ -303,7 +320,8 @@ export function renderSitemap(
|
|
|
303
320
|
const [key, value] = metaEntries[i];
|
|
304
321
|
const displayKey = metaDisplayKeys[i];
|
|
305
322
|
const rowY = metaStartY + i * CONTAINER_META_LINE_HEIGHT;
|
|
306
|
-
const valColor =
|
|
323
|
+
const valColor =
|
|
324
|
+
tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
|
|
307
325
|
|
|
308
326
|
cG.append('text')
|
|
309
327
|
.attr('x', 10)
|
|
@@ -324,9 +342,11 @@ export function renderSitemap(
|
|
|
324
342
|
// Collapsed accent bar
|
|
325
343
|
if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
|
|
326
344
|
const clipId = `clip-${c.nodeId}`;
|
|
327
|
-
cG.append('clipPath')
|
|
345
|
+
cG.append('clipPath')
|
|
346
|
+
.attr('id', clipId)
|
|
328
347
|
.append('rect')
|
|
329
|
-
.attr('width', c.width)
|
|
348
|
+
.attr('width', c.width)
|
|
349
|
+
.attr('height', c.height)
|
|
330
350
|
.attr('rx', CONTAINER_RADIUS);
|
|
331
351
|
cG.append('rect')
|
|
332
352
|
.attr('y', c.height - COLLAPSE_BAR_HEIGHT)
|
|
@@ -410,14 +430,17 @@ export function renderSitemap(
|
|
|
410
430
|
.attr('data-line-number', String(node.lineNumber)) as GSelection;
|
|
411
431
|
|
|
412
432
|
if (node.hasChildren) {
|
|
413
|
-
nodeG
|
|
433
|
+
nodeG
|
|
434
|
+
.attr('data-node-toggle', node.id)
|
|
414
435
|
.attr('tabindex', '0')
|
|
415
436
|
.attr('role', 'button')
|
|
416
437
|
.attr('aria-expanded', String(!node.hiddenCount));
|
|
417
438
|
}
|
|
418
439
|
|
|
419
440
|
if (onClickItem) {
|
|
420
|
-
nodeG
|
|
441
|
+
nodeG
|
|
442
|
+
.style('cursor', 'pointer')
|
|
443
|
+
.on('click', () => onClickItem(node.lineNumber));
|
|
421
444
|
}
|
|
422
445
|
|
|
423
446
|
// Tag metadata for legend hover dimming
|
|
@@ -431,7 +454,8 @@ export function renderSitemap(
|
|
|
431
454
|
const stroke = nodeStroke(palette, node.color);
|
|
432
455
|
|
|
433
456
|
// Card background
|
|
434
|
-
nodeG
|
|
457
|
+
nodeG
|
|
458
|
+
.append('rect')
|
|
435
459
|
.attr('x', 0)
|
|
436
460
|
.attr('y', 0)
|
|
437
461
|
.attr('width', node.width)
|
|
@@ -442,7 +466,8 @@ export function renderSitemap(
|
|
|
442
466
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
443
467
|
|
|
444
468
|
// Label
|
|
445
|
-
nodeG
|
|
469
|
+
nodeG
|
|
470
|
+
.append('text')
|
|
446
471
|
.attr('x', node.width / 2)
|
|
447
472
|
.attr('y', HEADER_HEIGHT / 2 + LABEL_FONT_SIZE / 2 - 2)
|
|
448
473
|
.attr('text-anchor', 'middle')
|
|
@@ -455,7 +480,8 @@ export function renderSitemap(
|
|
|
455
480
|
const metaEntries = Object.entries(node.metadata);
|
|
456
481
|
if (metaEntries.length > 0) {
|
|
457
482
|
// Separator line
|
|
458
|
-
nodeG
|
|
483
|
+
nodeG
|
|
484
|
+
.append('line')
|
|
459
485
|
.attr('x1', 0)
|
|
460
486
|
.attr('y1', HEADER_HEIGHT)
|
|
461
487
|
.attr('x2', node.width)
|
|
@@ -463,24 +489,30 @@ export function renderSitemap(
|
|
|
463
489
|
.attr('stroke', stroke)
|
|
464
490
|
.attr('stroke-opacity', 0.3);
|
|
465
491
|
|
|
466
|
-
const metaDisplayKeys = metaEntries.map(
|
|
492
|
+
const metaDisplayKeys = metaEntries.map(
|
|
493
|
+
([k]) => displayNames.get(k) ?? k
|
|
494
|
+
);
|
|
467
495
|
const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
|
|
468
496
|
const valueX = 10 + (maxKeyLen + 2) * (META_FONT_SIZE * 0.6);
|
|
469
497
|
|
|
470
498
|
for (let i = 0; i < metaEntries.length; i++) {
|
|
471
499
|
const [key, value] = metaEntries[i];
|
|
472
500
|
const displayKey = metaDisplayKeys[i];
|
|
473
|
-
const rowY =
|
|
474
|
-
|
|
501
|
+
const rowY =
|
|
502
|
+
HEADER_HEIGHT + SEPARATOR_GAP + (i + 1) * META_LINE_HEIGHT - 4;
|
|
503
|
+
const valColor =
|
|
504
|
+
tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
|
|
475
505
|
|
|
476
|
-
nodeG
|
|
506
|
+
nodeG
|
|
507
|
+
.append('text')
|
|
477
508
|
.attr('x', 10)
|
|
478
509
|
.attr('y', rowY)
|
|
479
510
|
.attr('fill', palette.textMuted)
|
|
480
511
|
.attr('font-size', META_FONT_SIZE)
|
|
481
512
|
.text(`${displayKey}:`);
|
|
482
513
|
|
|
483
|
-
nodeG
|
|
514
|
+
nodeG
|
|
515
|
+
.append('text')
|
|
484
516
|
.attr('x', valueX)
|
|
485
517
|
.attr('y', rowY)
|
|
486
518
|
.attr('fill', valColor)
|
|
@@ -492,11 +524,15 @@ export function renderSitemap(
|
|
|
492
524
|
// Collapsed accent bar
|
|
493
525
|
if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
|
|
494
526
|
const clipId = `clip-${node.id}`;
|
|
495
|
-
nodeG
|
|
527
|
+
nodeG
|
|
528
|
+
.append('clipPath')
|
|
529
|
+
.attr('id', clipId)
|
|
496
530
|
.append('rect')
|
|
497
|
-
.attr('width', node.width)
|
|
531
|
+
.attr('width', node.width)
|
|
532
|
+
.attr('height', node.height)
|
|
498
533
|
.attr('rx', CARD_RADIUS);
|
|
499
|
-
nodeG
|
|
534
|
+
nodeG
|
|
535
|
+
.append('rect')
|
|
500
536
|
.attr('y', node.height - COLLAPSE_BAR_HEIGHT)
|
|
501
537
|
.attr('width', node.width)
|
|
502
538
|
.attr('height', COLLAPSE_BAR_HEIGHT)
|
|
@@ -509,7 +545,15 @@ export function renderSitemap(
|
|
|
509
545
|
// --- Render legend ---
|
|
510
546
|
if (exportDims && hasLegend) {
|
|
511
547
|
// Export mode: render inside the scaled content group
|
|
512
|
-
renderLegend(
|
|
548
|
+
renderLegend(
|
|
549
|
+
contentG,
|
|
550
|
+
layout.legend,
|
|
551
|
+
palette,
|
|
552
|
+
isDark,
|
|
553
|
+
activeTagGroup,
|
|
554
|
+
undefined,
|
|
555
|
+
hiddenAttributes
|
|
556
|
+
);
|
|
513
557
|
}
|
|
514
558
|
|
|
515
559
|
// --- Fixed title + legend (appended AFTER mainG so they paint on top
|
|
@@ -546,7 +590,15 @@ export function renderSitemap(
|
|
|
546
590
|
if (activeTagGroup) {
|
|
547
591
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
548
592
|
}
|
|
549
|
-
renderLegend(
|
|
593
|
+
renderLegend(
|
|
594
|
+
legendParent,
|
|
595
|
+
layout.legend,
|
|
596
|
+
palette,
|
|
597
|
+
isDark,
|
|
598
|
+
activeTagGroup,
|
|
599
|
+
width,
|
|
600
|
+
hiddenAttributes
|
|
601
|
+
);
|
|
550
602
|
}
|
|
551
603
|
}
|
|
552
604
|
|
|
@@ -561,13 +613,16 @@ function renderLegend(
|
|
|
561
613
|
isDark: boolean,
|
|
562
614
|
activeTagGroup?: string | null,
|
|
563
615
|
fixedWidth?: number,
|
|
564
|
-
hiddenAttributes?: Set<string
|
|
616
|
+
hiddenAttributes?: Set<string>
|
|
565
617
|
): void {
|
|
566
618
|
if (legendGroups.length === 0) return;
|
|
567
619
|
|
|
568
|
-
const visibleGroups =
|
|
569
|
-
|
|
570
|
-
|
|
620
|
+
const visibleGroups =
|
|
621
|
+
activeTagGroup != null
|
|
622
|
+
? legendGroups.filter(
|
|
623
|
+
(g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
|
|
624
|
+
)
|
|
625
|
+
: legendGroups;
|
|
571
626
|
|
|
572
627
|
const groupBg = isDark
|
|
573
628
|
? mix(palette.surface, palette.bg, 50)
|
|
@@ -591,7 +646,8 @@ function renderLegend(
|
|
|
591
646
|
|
|
592
647
|
for (const group of visibleGroups) {
|
|
593
648
|
const isActive = activeTagGroup != null;
|
|
594
|
-
const pillW =
|
|
649
|
+
const pillW =
|
|
650
|
+
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
595
651
|
|
|
596
652
|
const gX = fixedPositions?.get(group.name) ?? group.x;
|
|
597
653
|
const gY = fixedPositions ? 0 : group.y;
|
|
@@ -605,7 +661,8 @@ function renderLegend(
|
|
|
605
661
|
|
|
606
662
|
// Outer capsule background (active/expanded only)
|
|
607
663
|
if (isActive) {
|
|
608
|
-
legendG
|
|
664
|
+
legendG
|
|
665
|
+
.append('rect')
|
|
609
666
|
.attr('width', group.width)
|
|
610
667
|
.attr('height', LEGEND_HEIGHT)
|
|
611
668
|
.attr('rx', LEGEND_HEIGHT / 2)
|
|
@@ -613,11 +670,12 @@ function renderLegend(
|
|
|
613
670
|
}
|
|
614
671
|
|
|
615
672
|
const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
616
|
-
const pillYOff =
|
|
617
|
-
const pillH = LEGEND_HEIGHT -
|
|
673
|
+
const pillYOff = LEGEND_CAPSULE_PAD;
|
|
674
|
+
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
618
675
|
|
|
619
676
|
// Pill background
|
|
620
|
-
legendG
|
|
677
|
+
legendG
|
|
678
|
+
.append('rect')
|
|
621
679
|
.attr('x', pillXOff)
|
|
622
680
|
.attr('y', pillYOff)
|
|
623
681
|
.attr('width', pillW)
|
|
@@ -627,7 +685,8 @@ function renderLegend(
|
|
|
627
685
|
|
|
628
686
|
// Active pill border
|
|
629
687
|
if (isActive) {
|
|
630
|
-
legendG
|
|
688
|
+
legendG
|
|
689
|
+
.append('rect')
|
|
631
690
|
.attr('x', pillXOff)
|
|
632
691
|
.attr('y', pillYOff)
|
|
633
692
|
.attr('width', pillW)
|
|
@@ -639,7 +698,8 @@ function renderLegend(
|
|
|
639
698
|
}
|
|
640
699
|
|
|
641
700
|
// Pill text
|
|
642
|
-
legendG
|
|
701
|
+
legendG
|
|
702
|
+
.append('text')
|
|
643
703
|
.attr('x', pillXOff + pillW / 2)
|
|
644
704
|
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
645
705
|
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
@@ -656,14 +716,16 @@ function renderLegend(
|
|
|
656
716
|
const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
|
|
657
717
|
const hitPad = 6;
|
|
658
718
|
|
|
659
|
-
const eyeG = legendG
|
|
719
|
+
const eyeG = legendG
|
|
720
|
+
.append('g')
|
|
660
721
|
.attr('class', 'sitemap-legend-eye')
|
|
661
722
|
.attr('data-legend-visibility', groupKey)
|
|
662
723
|
.style('cursor', 'pointer')
|
|
663
724
|
.attr('opacity', isHidden ? 0.4 : 0.7);
|
|
664
725
|
|
|
665
726
|
// Transparent hit area for easier clicking
|
|
666
|
-
eyeG
|
|
727
|
+
eyeG
|
|
728
|
+
.append('rect')
|
|
667
729
|
.attr('x', eyeX - hitPad)
|
|
668
730
|
.attr('y', eyeY - hitPad)
|
|
669
731
|
.attr('width', LEGEND_EYE_SIZE + hitPad * 2)
|
|
@@ -671,7 +733,8 @@ function renderLegend(
|
|
|
671
733
|
.attr('fill', 'transparent')
|
|
672
734
|
.attr('pointer-events', 'all');
|
|
673
735
|
|
|
674
|
-
eyeG
|
|
736
|
+
eyeG
|
|
737
|
+
.append('path')
|
|
675
738
|
.attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
|
|
676
739
|
.attr('transform', `translate(${eyeX}, ${eyeY})`)
|
|
677
740
|
.attr('fill', 'none')
|
|
@@ -683,28 +746,35 @@ function renderLegend(
|
|
|
683
746
|
|
|
684
747
|
// Entries (active/expanded only)
|
|
685
748
|
if (isActive) {
|
|
686
|
-
const eyeShift =
|
|
749
|
+
const eyeShift =
|
|
750
|
+
fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
|
|
687
751
|
let entryX = pillXOff + pillW + 4 + eyeShift;
|
|
688
752
|
for (const entry of group.entries) {
|
|
689
|
-
const entryG = legendG
|
|
753
|
+
const entryG = legendG
|
|
754
|
+
.append('g')
|
|
690
755
|
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
691
756
|
.style('cursor', 'pointer');
|
|
692
757
|
|
|
693
|
-
entryG
|
|
758
|
+
entryG
|
|
759
|
+
.append('circle')
|
|
694
760
|
.attr('cx', entryX + LEGEND_DOT_R)
|
|
695
761
|
.attr('cy', LEGEND_HEIGHT / 2)
|
|
696
762
|
.attr('r', LEGEND_DOT_R)
|
|
697
763
|
.attr('fill', entry.color);
|
|
698
764
|
|
|
699
765
|
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
700
|
-
entryG
|
|
766
|
+
entryG
|
|
767
|
+
.append('text')
|
|
701
768
|
.attr('x', textX)
|
|
702
769
|
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
703
770
|
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
704
771
|
.attr('fill', palette.textMuted)
|
|
705
772
|
.text(entry.value);
|
|
706
773
|
|
|
707
|
-
entryX =
|
|
774
|
+
entryX =
|
|
775
|
+
textX +
|
|
776
|
+
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
777
|
+
LEGEND_ENTRY_TRAIL;
|
|
708
778
|
}
|
|
709
779
|
}
|
|
710
780
|
}
|
|
@@ -717,7 +787,7 @@ function renderLegend(
|
|
|
717
787
|
export async function renderSitemapForExport(
|
|
718
788
|
content: string,
|
|
719
789
|
theme: 'light' | 'dark' | 'transparent',
|
|
720
|
-
palette?: PaletteColors
|
|
790
|
+
palette?: PaletteColors
|
|
721
791
|
): Promise<string> {
|
|
722
792
|
const { parseSitemap } = await import('./parser');
|
|
723
793
|
const { layoutSitemap } = await import('./layout');
|
|
@@ -725,7 +795,8 @@ export async function renderSitemapForExport(
|
|
|
725
795
|
const { injectBranding } = await import('../branding');
|
|
726
796
|
|
|
727
797
|
const isDark = theme === 'dark';
|
|
728
|
-
const effectivePalette =
|
|
798
|
+
const effectivePalette =
|
|
799
|
+
palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
729
800
|
|
|
730
801
|
const parsed = parseSitemap(content, effectivePalette);
|
|
731
802
|
if (parsed.error || parsed.roots.length === 0) return '';
|
|
@@ -744,10 +815,18 @@ export async function renderSitemapForExport(
|
|
|
744
815
|
container.style.left = '-9999px';
|
|
745
816
|
document.body.appendChild(container);
|
|
746
817
|
|
|
747
|
-
renderSitemap(
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
818
|
+
renderSitemap(
|
|
819
|
+
container,
|
|
820
|
+
parsed,
|
|
821
|
+
sitemapLayout,
|
|
822
|
+
effectivePalette,
|
|
823
|
+
isDark,
|
|
824
|
+
undefined,
|
|
825
|
+
{
|
|
826
|
+
width: exportWidth,
|
|
827
|
+
height: exportHeight,
|
|
828
|
+
}
|
|
829
|
+
);
|
|
751
830
|
|
|
752
831
|
const svgEl = container.querySelector('svg');
|
|
753
832
|
if (!svgEl) {
|
|
@@ -766,6 +845,7 @@ export async function renderSitemapForExport(
|
|
|
766
845
|
const svgHtml = svgEl.outerHTML;
|
|
767
846
|
document.body.removeChild(container);
|
|
768
847
|
|
|
769
|
-
const brandColor =
|
|
848
|
+
const brandColor =
|
|
849
|
+
theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
770
850
|
return injectBranding(svgHtml, brandColor);
|
|
771
851
|
}
|
package/src/utils/legend-svg.ts
CHANGED
|
@@ -48,7 +48,11 @@ export interface LegendRenderResult {
|
|
|
48
48
|
// ── Helpers ──────────────────────────────────────────────────
|
|
49
49
|
|
|
50
50
|
function esc(s: string): string {
|
|
51
|
-
return s
|
|
51
|
+
return s
|
|
52
|
+
.replace(/&/g, '&')
|
|
53
|
+
.replace(/</g, '<')
|
|
54
|
+
.replace(/>/g, '>')
|
|
55
|
+
.replace(/"/g, '"');
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function pillWidth(name: string): number {
|
|
@@ -58,12 +62,20 @@ function pillWidth(name: string): number {
|
|
|
58
62
|
function entriesWidth(entries: Array<{ value: string }>): number {
|
|
59
63
|
let w = 0;
|
|
60
64
|
for (const e of entries) {
|
|
61
|
-
w +=
|
|
65
|
+
w +=
|
|
66
|
+
LEGEND_DOT_R * 2 +
|
|
67
|
+
LEGEND_ENTRY_DOT_GAP +
|
|
68
|
+
measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
69
|
+
LEGEND_ENTRY_TRAIL;
|
|
62
70
|
}
|
|
63
71
|
return w;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
function groupTotalWidth(
|
|
74
|
+
function groupTotalWidth(
|
|
75
|
+
name: string,
|
|
76
|
+
entries: Array<{ value: string }>,
|
|
77
|
+
isActive: boolean
|
|
78
|
+
): number {
|
|
67
79
|
const pw = pillWidth(name);
|
|
68
80
|
if (!isActive) return pw;
|
|
69
81
|
return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
|
|
@@ -73,7 +85,7 @@ function groupTotalWidth(name: string, entries: Array<{ value: string }>, isActi
|
|
|
73
85
|
|
|
74
86
|
export function renderLegendSvg(
|
|
75
87
|
groups: LegendGroupData[],
|
|
76
|
-
options: LegendRenderOptions
|
|
88
|
+
options: LegendRenderOptions
|
|
77
89
|
): LegendRenderResult {
|
|
78
90
|
if (groups.length === 0) return { svg: '', height: 0, width: 0 };
|
|
79
91
|
|
|
@@ -86,7 +98,8 @@ export function renderLegendSvg(
|
|
|
86
98
|
const items = groups
|
|
87
99
|
.filter((g) => g.entries.length > 0)
|
|
88
100
|
.map((g) => {
|
|
89
|
-
const isActive =
|
|
101
|
+
const isActive =
|
|
102
|
+
!!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
|
|
90
103
|
const pw = pillWidth(g.name);
|
|
91
104
|
const tw = groupTotalWidth(g.name, g.entries, isActive);
|
|
92
105
|
return { group: g, isActive, pillWidth: pw, totalWidth: tw };
|
|
@@ -94,11 +107,17 @@ export function renderLegendSvg(
|
|
|
94
107
|
|
|
95
108
|
if (items.length === 0) return { svg: '', height: 0, width: 0 };
|
|
96
109
|
|
|
97
|
-
const totalWidth =
|
|
110
|
+
const totalWidth =
|
|
111
|
+
items.reduce((s, it) => s + it.totalWidth, 0) +
|
|
112
|
+
(items.length - 1) * LEGEND_GROUP_GAP;
|
|
98
113
|
|
|
99
114
|
// Center over the plot area when grid offsets are provided, otherwise full container
|
|
100
|
-
const plotLeft = options.gridLeftPct
|
|
101
|
-
|
|
115
|
+
const plotLeft = options.gridLeftPct
|
|
116
|
+
? (containerWidth * options.gridLeftPct) / 100
|
|
117
|
+
: 0;
|
|
118
|
+
const plotRight = options.gridRightPct
|
|
119
|
+
? containerWidth - (containerWidth * options.gridRightPct) / 100
|
|
120
|
+
: containerWidth;
|
|
102
121
|
const plotWidth = plotRight - plotLeft;
|
|
103
122
|
let x = Math.max(0, plotLeft + (plotWidth - totalWidth) / 2);
|
|
104
123
|
|
|
@@ -112,29 +131,29 @@ export function renderLegendSvg(
|
|
|
112
131
|
// Outer capsule (active only)
|
|
113
132
|
if (item.isActive) {
|
|
114
133
|
inner.push(
|
|
115
|
-
`<rect width="${item.totalWidth}" height="${LEGEND_HEIGHT}" rx="${LEGEND_HEIGHT / 2}" fill="${esc(groupBg)}"
|
|
134
|
+
`<rect width="${item.totalWidth}" height="${LEGEND_HEIGHT}" rx="${LEGEND_HEIGHT / 2}" fill="${esc(groupBg)}"/>`
|
|
116
135
|
);
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
120
|
-
const pillYOff =
|
|
121
|
-
const h =
|
|
139
|
+
const pillYOff = LEGEND_CAPSULE_PAD;
|
|
140
|
+
const h = pillH;
|
|
122
141
|
|
|
123
142
|
// Pill background
|
|
124
143
|
inner.push(
|
|
125
|
-
`<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="${esc(item.isActive ? palette.bg : groupBg)}"
|
|
144
|
+
`<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="${esc(item.isActive ? palette.bg : groupBg)}"/>`
|
|
126
145
|
);
|
|
127
146
|
|
|
128
147
|
// Active pill border
|
|
129
148
|
if (item.isActive) {
|
|
130
149
|
inner.push(
|
|
131
|
-
`<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="none" stroke="${esc(mix(palette.textMuted, palette.bg, 50))}" stroke-width="0.75"
|
|
150
|
+
`<rect x="${pillXOff}" y="${pillYOff}" width="${item.pillWidth}" height="${h}" rx="${h / 2}" fill="none" stroke="${esc(mix(palette.textMuted, palette.bg, 50))}" stroke-width="0.75"/>`
|
|
132
151
|
);
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
// Pill text
|
|
136
155
|
inner.push(
|
|
137
|
-
`<text x="${pillXOff + item.pillWidth / 2}" y="${LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2}" font-size="${LEGEND_PILL_FONT_SIZE}" font-weight="500" fill="${esc(item.isActive ? palette.text : palette.textMuted)}" text-anchor="middle" font-family="${esc(FONT_FAMILY)}">${esc(item.group.name)}</text
|
|
156
|
+
`<text x="${pillXOff + item.pillWidth / 2}" y="${LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2}" font-size="${LEGEND_PILL_FONT_SIZE}" font-weight="500" fill="${esc(item.isActive ? palette.text : palette.textMuted)}" text-anchor="middle" font-family="${esc(FONT_FAMILY)}">${esc(item.group.name)}</text>`
|
|
138
157
|
);
|
|
139
158
|
|
|
140
159
|
// Entry dots + labels (active only)
|
|
@@ -144,23 +163,28 @@ export function renderLegendSvg(
|
|
|
144
163
|
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
145
164
|
inner.push(
|
|
146
165
|
`<g data-legend-entry="${esc(entry.value.toLowerCase())}" data-series-name="${esc(entry.value)}" style="cursor:pointer">` +
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
`<circle cx="${entryX + LEGEND_DOT_R}" cy="${LEGEND_HEIGHT / 2}" r="${LEGEND_DOT_R}" fill="${esc(entry.color)}"/>` +
|
|
167
|
+
`<text x="${textX}" y="${LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1}" font-size="${LEGEND_ENTRY_FONT_SIZE}" fill="${esc(palette.textMuted)}" font-family="${esc(FONT_FAMILY)}">${esc(entry.value)}</text>` +
|
|
168
|
+
`</g>`
|
|
150
169
|
);
|
|
151
|
-
entryX =
|
|
170
|
+
entryX =
|
|
171
|
+
textX +
|
|
172
|
+
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
173
|
+
LEGEND_ENTRY_TRAIL;
|
|
152
174
|
}
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
parts.push(
|
|
156
|
-
`<g transform="translate(${x},0)" data-legend-group="${esc(groupKey)}" style="cursor:pointer">${inner.join('')}</g
|
|
178
|
+
`<g transform="translate(${x},0)" data-legend-group="${esc(groupKey)}" style="cursor:pointer">${inner.join('')}</g>`
|
|
157
179
|
);
|
|
158
180
|
|
|
159
181
|
x += item.totalWidth + LEGEND_GROUP_GAP;
|
|
160
182
|
}
|
|
161
183
|
|
|
162
184
|
const classAttr = className ? ` class="${esc(className)}"` : '';
|
|
163
|
-
const activeAttr = activeGroup
|
|
185
|
+
const activeAttr = activeGroup
|
|
186
|
+
? ` data-legend-active="${esc(activeGroup.toLowerCase())}"`
|
|
187
|
+
: '';
|
|
164
188
|
const svg = `<g${classAttr}${activeAttr}>${parts.join('')}</g>`;
|
|
165
189
|
|
|
166
190
|
return { svg, height: LEGEND_HEIGHT, width: totalWidth };
|
package/src/utils/parsing.ts
CHANGED
|
@@ -41,11 +41,11 @@ export const ALL_CHART_TYPES = new Set([
|
|
|
41
41
|
'org',
|
|
42
42
|
'kanban',
|
|
43
43
|
'c4',
|
|
44
|
-
'initiative-status',
|
|
45
44
|
'state',
|
|
46
45
|
'sitemap',
|
|
47
46
|
'infra',
|
|
48
47
|
'gantt',
|
|
48
|
+
'boxes-and-lines',
|
|
49
49
|
]);
|
|
50
50
|
|
|
51
51
|
/** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */
|
package/src/utils/tag-groups.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface TagGroup {
|
|
|
16
16
|
name: string;
|
|
17
17
|
alias?: string;
|
|
18
18
|
entries: TagEntry[];
|
|
19
|
-
/**
|
|
19
|
+
/** Default value for nodes without explicit metadata. First entry unless another is marked `default`. */
|
|
20
20
|
defaultValue?: string;
|
|
21
21
|
lineNumber: number;
|
|
22
22
|
}
|
|
@@ -30,6 +30,26 @@ export interface TagBlockMatch {
|
|
|
30
30
|
inlineValues?: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// ── Default Modifier ────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strip trailing `default` keyword from a tag entry string.
|
|
37
|
+
* Returns the cleaned text and whether the keyword was present.
|
|
38
|
+
*
|
|
39
|
+
* Examples:
|
|
40
|
+
* "NA(gray) default" → { text: "NA(gray)", isDefault: true }
|
|
41
|
+
* "Done(green)" → { text: "Done(green)", isDefault: false }
|
|
42
|
+
*/
|
|
43
|
+
export function stripDefaultModifier(text: string): {
|
|
44
|
+
text: string;
|
|
45
|
+
isDefault: boolean;
|
|
46
|
+
} {
|
|
47
|
+
if (/\bdefault\s*$/.test(text)) {
|
|
48
|
+
return { text: text.replace(/\s+default\s*$/, '').trim(), isDefault: true };
|
|
49
|
+
}
|
|
50
|
+
return { text, isDefault: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
// ── Regexes ─────────────────────────────────────────────────
|
|
34
54
|
|
|
35
55
|
/** Canonical syntax: line starting with `tag` keyword (no colon). */
|