@diagrammo/dgmo 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +180 -178
- package/dist/index.cjs +5296 -2209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -16
- package/dist/index.d.ts +236 -16
- package/dist/index.js +12423 -9343
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/renderer.ts +6 -6
- package/src/class/renderer.ts +183 -7
- package/src/cli.ts +3 -11
- package/src/colors.ts +3 -3
- package/src/d3.ts +128 -23
- package/src/dgmo-router.ts +3 -1
- package/src/er/renderer.ts +11 -5
- package/src/gantt/calculator.ts +677 -0
- package/src/gantt/parser.ts +761 -0
- package/src/gantt/renderer.ts +2125 -0
- package/src/gantt/resolver.ts +144 -0
- package/src/gantt/types.ts +168 -0
- package/src/index.ts +27 -0
- package/src/infra/renderer.ts +48 -12
- package/src/initiative-status/filter.ts +63 -0
- package/src/initiative-status/layout.ts +319 -67
- package/src/initiative-status/parser.ts +200 -25
- package/src/initiative-status/renderer.ts +293 -10
- package/src/initiative-status/types.ts +6 -0
- package/src/org/layout.ts +22 -55
- package/src/org/renderer.ts +4 -8
- package/src/palettes/dracula.ts +60 -0
- package/src/palettes/index.ts +8 -6
- package/src/palettes/monokai.ts +60 -0
- package/src/palettes/registry.ts +4 -2
- package/src/sequence/renderer.ts +5 -4
- package/src/sharing.ts +8 -0
- package/src/sitemap/renderer.ts +4 -4
- package/src/utils/duration.ts +212 -0
- package/src/utils/legend-constants.ts +1 -0
|
@@ -6,7 +6,21 @@ import * as d3Selection from 'd3-selection';
|
|
|
6
6
|
import * as d3Shape from 'd3-shape';
|
|
7
7
|
import { FONT_FAMILY } from '../fonts';
|
|
8
8
|
import { runInExportContainer, extractExportSvg } from '../utils/export-container';
|
|
9
|
+
import {
|
|
10
|
+
LEGEND_HEIGHT,
|
|
11
|
+
LEGEND_PILL_PAD,
|
|
12
|
+
LEGEND_PILL_FONT_SIZE,
|
|
13
|
+
LEGEND_PILL_FONT_W,
|
|
14
|
+
LEGEND_CAPSULE_PAD,
|
|
15
|
+
LEGEND_DOT_R,
|
|
16
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
17
|
+
LEGEND_ENTRY_FONT_W,
|
|
18
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
19
|
+
LEGEND_ENTRY_TRAIL,
|
|
20
|
+
LEGEND_GROUP_GAP,
|
|
21
|
+
} from '../utils/legend-constants';
|
|
9
22
|
import { contrastText, mix } from '../palettes/color-utils';
|
|
23
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
10
24
|
import type { PaletteColors } from '../palettes';
|
|
11
25
|
import type { ParsedInitiativeStatus, InitiativeStatus } from './types';
|
|
12
26
|
import type { ParticipantType } from '../sequence/parser';
|
|
@@ -67,6 +81,44 @@ function edgeStrokeColor(status: InitiativeStatus, palette: PaletteColors, isDar
|
|
|
67
81
|
return statusColor(status, palette, isDark);
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
// ============================================================
|
|
85
|
+
// Legend helpers
|
|
86
|
+
// ============================================================
|
|
87
|
+
|
|
88
|
+
interface ISLegendEntry {
|
|
89
|
+
label: string;
|
|
90
|
+
statusKey: InitiativeStatus;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const IS_STATUS_LABELS: Record<string, string> = {
|
|
94
|
+
done: 'Done',
|
|
95
|
+
wip: 'In Progress',
|
|
96
|
+
todo: 'To Do',
|
|
97
|
+
na: 'N/A',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const IS_STATUS_ORDER: InitiativeStatus[] = ['todo', 'wip', 'done', 'na'];
|
|
101
|
+
|
|
102
|
+
function collectStatuses(parsed: ParsedInitiativeStatus): ISLegendEntry[] {
|
|
103
|
+
const present = new Set<string>();
|
|
104
|
+
for (const n of parsed.nodes) {
|
|
105
|
+
if (n.status) present.add(n.status);
|
|
106
|
+
}
|
|
107
|
+
return IS_STATUS_ORDER
|
|
108
|
+
.filter((s) => s !== null && present.has(s))
|
|
109
|
+
.map((s) => ({ label: IS_STATUS_LABELS[s!], statusKey: s }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const LEGEND_GROUP_NAME = 'Status';
|
|
113
|
+
|
|
114
|
+
function legendEntriesWidth(entries: ISLegendEntry[]): number {
|
|
115
|
+
let w = 0;
|
|
116
|
+
for (const e of entries) {
|
|
117
|
+
w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
118
|
+
}
|
|
119
|
+
return w;
|
|
120
|
+
}
|
|
121
|
+
|
|
70
122
|
// ============================================================
|
|
71
123
|
// Edge path generator
|
|
72
124
|
// ============================================================
|
|
@@ -410,15 +462,31 @@ function renderNodeShape(
|
|
|
410
462
|
// Main renderer
|
|
411
463
|
// ============================================================
|
|
412
464
|
|
|
465
|
+
export interface ISRenderOptions {
|
|
466
|
+
onClickItem?: (lineNumber: number) => void;
|
|
467
|
+
exportDims?: { width?: number; height?: number };
|
|
468
|
+
legendActive?: boolean | null;
|
|
469
|
+
activeTagGroup?: string | null;
|
|
470
|
+
hiddenTagValues?: Map<string, Set<string>>;
|
|
471
|
+
tagGroups?: TagGroup[];
|
|
472
|
+
}
|
|
473
|
+
|
|
413
474
|
export function renderInitiativeStatus(
|
|
414
475
|
container: HTMLDivElement,
|
|
415
476
|
parsed: ParsedInitiativeStatus,
|
|
416
477
|
layout: ISLayoutResult,
|
|
417
478
|
palette: PaletteColors,
|
|
418
479
|
isDark: boolean,
|
|
419
|
-
|
|
420
|
-
exportDims?: { width?: number; height?: number }
|
|
480
|
+
options?: ISRenderOptions
|
|
421
481
|
): void {
|
|
482
|
+
const {
|
|
483
|
+
onClickItem,
|
|
484
|
+
exportDims,
|
|
485
|
+
legendActive,
|
|
486
|
+
activeTagGroup,
|
|
487
|
+
hiddenTagValues,
|
|
488
|
+
tagGroups,
|
|
489
|
+
} = options ?? {};
|
|
422
490
|
// Clear existing content
|
|
423
491
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
424
492
|
|
|
@@ -426,19 +494,28 @@ export function renderInitiativeStatus(
|
|
|
426
494
|
const height = exportDims?.height ?? container.clientHeight;
|
|
427
495
|
if (width <= 0 || height <= 0) return;
|
|
428
496
|
|
|
497
|
+
const legendEntries = collectStatuses(parsed);
|
|
498
|
+
const hasLegend = legendEntries.length > 1;
|
|
499
|
+
const isLegendExpanded = legendActive !== false;
|
|
500
|
+
|
|
501
|
+
const effectiveTagGroups = tagGroups ?? parsed.tagGroups ?? [];
|
|
502
|
+
const hasTagGroups = effectiveTagGroups.length > 0;
|
|
503
|
+
|
|
429
504
|
const titleHeight = parsed.title ? 40 : 0;
|
|
505
|
+
const LEGEND_FIXED_GAP = 8;
|
|
506
|
+
const legendReserve = (hasLegend || hasTagGroups) ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
430
507
|
|
|
431
508
|
// Scale to fit
|
|
432
509
|
const diagramW = layout.width;
|
|
433
510
|
const diagramH = layout.height;
|
|
434
|
-
const availH = height - titleHeight;
|
|
511
|
+
const availH = height - titleHeight - legendReserve;
|
|
435
512
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
436
513
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
437
514
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
438
515
|
|
|
439
516
|
const scaledW = diagramW * scale;
|
|
440
517
|
const offsetX = (width - scaledW) / 2;
|
|
441
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
518
|
+
const offsetY = titleHeight + legendReserve + DIAGRAM_PADDING;
|
|
442
519
|
|
|
443
520
|
// Create SVG
|
|
444
521
|
const svg = d3Selection
|
|
@@ -498,6 +575,201 @@ export function renderInitiativeStatus(
|
|
|
498
575
|
}
|
|
499
576
|
}
|
|
500
577
|
|
|
578
|
+
// ── Legend ──
|
|
579
|
+
if (hasLegend || hasTagGroups) {
|
|
580
|
+
const groupBg = isDark
|
|
581
|
+
? mix(palette.surface, palette.bg, 50)
|
|
582
|
+
: mix(palette.surface, palette.bg, 30);
|
|
583
|
+
|
|
584
|
+
// Build legend groups: Status + tag groups
|
|
585
|
+
interface LegendGroup {
|
|
586
|
+
name: string;
|
|
587
|
+
key: string; // lowercase key for data attribute
|
|
588
|
+
isStatus: boolean;
|
|
589
|
+
entries: { label: string; color: string; value: string }[];
|
|
590
|
+
width: number; // total width when expanded
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const legendGroups: LegendGroup[] = [];
|
|
594
|
+
|
|
595
|
+
// Status group (always first if entries exist)
|
|
596
|
+
if (hasLegend) {
|
|
597
|
+
const statusEntries = legendEntries.map((e) => ({
|
|
598
|
+
label: e.label,
|
|
599
|
+
color: statusColor(e.statusKey, palette, isDark),
|
|
600
|
+
value: e.statusKey ?? 'na',
|
|
601
|
+
}));
|
|
602
|
+
const pillW = LEGEND_GROUP_NAME.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
603
|
+
const entrW = legendEntriesWidth(legendEntries);
|
|
604
|
+
legendGroups.push({
|
|
605
|
+
name: LEGEND_GROUP_NAME,
|
|
606
|
+
key: 'status',
|
|
607
|
+
isStatus: true,
|
|
608
|
+
entries: statusEntries,
|
|
609
|
+
width: LEGEND_CAPSULE_PAD * 2 + pillW + LEGEND_ENTRY_TRAIL + entrW,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Tag groups
|
|
614
|
+
for (const tg of effectiveTagGroups) {
|
|
615
|
+
const entries = tg.entries.map((e) => ({
|
|
616
|
+
label: e.value,
|
|
617
|
+
color: e.color || palette.textMuted,
|
|
618
|
+
value: e.value.toLowerCase(),
|
|
619
|
+
}));
|
|
620
|
+
const pillW = tg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
621
|
+
let entrW = 0;
|
|
622
|
+
for (const e of entries) {
|
|
623
|
+
entrW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
624
|
+
}
|
|
625
|
+
legendGroups.push({
|
|
626
|
+
name: tg.name,
|
|
627
|
+
key: tg.name.toLowerCase(),
|
|
628
|
+
isStatus: false,
|
|
629
|
+
entries,
|
|
630
|
+
width: LEGEND_CAPSULE_PAD * 2 + pillW + 4 + entrW,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Determine which group is active/expanded
|
|
635
|
+
const activeKey = activeTagGroup?.toLowerCase() ?? null;
|
|
636
|
+
const isStatusExpanded = isLegendExpanded && activeKey === null;
|
|
637
|
+
|
|
638
|
+
// When a tag group is active, only show that group (mutual exclusion).
|
|
639
|
+
// When no tag group is active, show all pills (Status expanded + tag pills minified).
|
|
640
|
+
const visibleLegendGroups = activeKey !== null
|
|
641
|
+
? legendGroups.filter((lg) => !lg.isStatus && lg.key === activeKey)
|
|
642
|
+
: legendGroups;
|
|
643
|
+
|
|
644
|
+
// Compute total legend width
|
|
645
|
+
let totalLegendW = 0;
|
|
646
|
+
for (const lg of visibleLegendGroups) {
|
|
647
|
+
const isActive = lg.isStatus ? isStatusExpanded : (activeKey === lg.key);
|
|
648
|
+
const pillW = lg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
649
|
+
totalLegendW += isActive ? lg.width : pillW;
|
|
650
|
+
totalLegendW += LEGEND_GROUP_GAP;
|
|
651
|
+
}
|
|
652
|
+
totalLegendW -= LEGEND_GROUP_GAP; // remove trailing gap
|
|
653
|
+
|
|
654
|
+
const legendX = (width - totalLegendW) / 2;
|
|
655
|
+
const legendY = titleHeight;
|
|
656
|
+
|
|
657
|
+
const legendRow = svg
|
|
658
|
+
.append('g')
|
|
659
|
+
.attr('class', 'is-legend-row')
|
|
660
|
+
.attr('transform', `translate(${legendX}, ${legendY})`);
|
|
661
|
+
|
|
662
|
+
let cursorX = 0;
|
|
663
|
+
|
|
664
|
+
for (const lg of visibleLegendGroups) {
|
|
665
|
+
const isActive = lg.isStatus ? isStatusExpanded : (activeKey === lg.key);
|
|
666
|
+
const pillW = lg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
667
|
+
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
668
|
+
const groupW = isActive ? lg.width : pillW;
|
|
669
|
+
|
|
670
|
+
const gEl = legendRow
|
|
671
|
+
.append('g')
|
|
672
|
+
.attr('transform', `translate(${cursorX}, 0)`)
|
|
673
|
+
.attr('class', 'is-legend-group')
|
|
674
|
+
.attr('data-legend-group', lg.key)
|
|
675
|
+
.style('cursor', 'pointer');
|
|
676
|
+
|
|
677
|
+
if (isActive) {
|
|
678
|
+
// Outer capsule background
|
|
679
|
+
gEl.append('rect')
|
|
680
|
+
.attr('width', groupW)
|
|
681
|
+
.attr('height', LEGEND_HEIGHT)
|
|
682
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
683
|
+
.attr('fill', groupBg);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
687
|
+
const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
688
|
+
|
|
689
|
+
// Pill background
|
|
690
|
+
gEl.append('rect')
|
|
691
|
+
.attr('x', pillXOff)
|
|
692
|
+
.attr('y', pillYOff)
|
|
693
|
+
.attr('width', pillW)
|
|
694
|
+
.attr('height', pillH)
|
|
695
|
+
.attr('rx', pillH / 2)
|
|
696
|
+
.attr('fill', isActive ? palette.bg : groupBg);
|
|
697
|
+
|
|
698
|
+
// Active pill border
|
|
699
|
+
if (isActive) {
|
|
700
|
+
gEl.append('rect')
|
|
701
|
+
.attr('x', pillXOff)
|
|
702
|
+
.attr('y', pillYOff)
|
|
703
|
+
.attr('width', pillW)
|
|
704
|
+
.attr('height', pillH)
|
|
705
|
+
.attr('rx', pillH / 2)
|
|
706
|
+
.attr('fill', 'none')
|
|
707
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
708
|
+
.attr('stroke-width', 0.75);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Pill text
|
|
712
|
+
gEl.append('text')
|
|
713
|
+
.attr('x', pillXOff + pillW / 2)
|
|
714
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
715
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
716
|
+
.attr('font-weight', '500')
|
|
717
|
+
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
718
|
+
.attr('text-anchor', 'middle')
|
|
719
|
+
.attr('font-family', FONT_FAMILY)
|
|
720
|
+
.text(lg.name);
|
|
721
|
+
|
|
722
|
+
// Entries inside capsule (active only)
|
|
723
|
+
if (isActive) {
|
|
724
|
+
// Determine which values are hidden for this group
|
|
725
|
+
const hiddenSet = !lg.isStatus ? hiddenTagValues?.get(lg.key) : undefined;
|
|
726
|
+
|
|
727
|
+
let entryX = pillXOff + pillW + 4;
|
|
728
|
+
for (const entry of lg.entries) {
|
|
729
|
+
const isHidden = hiddenSet?.has(entry.value) ?? false;
|
|
730
|
+
|
|
731
|
+
const entryG = gEl.append('g')
|
|
732
|
+
.attr('data-legend-entry', entry.value)
|
|
733
|
+
.style('cursor', !lg.isStatus ? 'pointer' : 'default');
|
|
734
|
+
|
|
735
|
+
if (isHidden) {
|
|
736
|
+
// Hidden: hollow ring + dimmed text (strikethrough-like)
|
|
737
|
+
entryG.append('circle')
|
|
738
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
739
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
740
|
+
.attr('r', LEGEND_DOT_R)
|
|
741
|
+
.attr('fill', 'none')
|
|
742
|
+
.attr('stroke', entry.color)
|
|
743
|
+
.attr('stroke-width', 1.2)
|
|
744
|
+
.attr('opacity', 0.5);
|
|
745
|
+
} else {
|
|
746
|
+
// Visible: solid dot
|
|
747
|
+
entryG.append('circle')
|
|
748
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
749
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
750
|
+
.attr('r', LEGEND_DOT_R)
|
|
751
|
+
.attr('fill', entry.color);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
entryG.append('text')
|
|
755
|
+
.attr('x', entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP)
|
|
756
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
757
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
758
|
+
.attr('fill', palette.textMuted)
|
|
759
|
+
.attr('font-family', FONT_FAMILY)
|
|
760
|
+
.attr('opacity', isHidden ? 0.4 : 1)
|
|
761
|
+
.attr('text-decoration', isHidden ? 'line-through' : 'none')
|
|
762
|
+
.text(entry.label);
|
|
763
|
+
|
|
764
|
+
entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
cursorX += groupW + LEGEND_GROUP_GAP;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
}
|
|
772
|
+
|
|
501
773
|
// Content group
|
|
502
774
|
const contentG = svg
|
|
503
775
|
.append('g')
|
|
@@ -768,7 +1040,15 @@ export function renderInitiativeStatus(
|
|
|
768
1040
|
.append('g')
|
|
769
1041
|
.attr('transform', `translate(${node.x}, ${node.y})`)
|
|
770
1042
|
.attr('class', 'is-node')
|
|
771
|
-
.attr('data-line-number', String(node.lineNumber))
|
|
1043
|
+
.attr('data-line-number', String(node.lineNumber))
|
|
1044
|
+
.attr('data-is-status', node.status ?? 'na');
|
|
1045
|
+
|
|
1046
|
+
// Tag data attributes for hover dimming
|
|
1047
|
+
if (node.metadata) {
|
|
1048
|
+
for (const [key, val] of Object.entries(node.metadata)) {
|
|
1049
|
+
nodeG.attr(`data-tag-${key}`, val.toLowerCase());
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
772
1052
|
|
|
773
1053
|
if (onClickItem) {
|
|
774
1054
|
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
@@ -787,14 +1067,16 @@ export function renderInitiativeStatus(
|
|
|
787
1067
|
.attr('fill', 'transparent')
|
|
788
1068
|
.attr('class', 'is-node-hit-area');
|
|
789
1069
|
|
|
1070
|
+
// Always use status coloring regardless of legend state
|
|
790
1071
|
const fill = nodeFill(node.status, palette, isDark);
|
|
791
1072
|
const stroke = nodeStroke(node.status, palette, isDark);
|
|
792
1073
|
renderNodeShape(nodeG, node.shape, node.width, node.height, fill, stroke);
|
|
793
1074
|
|
|
1075
|
+
const textColor = contrastText(fill, '#eceff4', '#2e3440');
|
|
1076
|
+
|
|
794
1077
|
// Label placement: actors put label below the figure, others center inside
|
|
795
1078
|
const isActor = node.shape === 'actor';
|
|
796
1079
|
if (isActor) {
|
|
797
|
-
const textColor = nodeTextColor(node.status, palette, isDark);
|
|
798
1080
|
const fitted = fitTextToNode(node.label, node.width, node.height * 0.35);
|
|
799
1081
|
const labelY = node.height / 2 - fitted.fontSize * 0.3;
|
|
800
1082
|
for (let li = 0; li < fitted.lines.length; li++) {
|
|
@@ -811,7 +1093,6 @@ export function renderInitiativeStatus(
|
|
|
811
1093
|
}
|
|
812
1094
|
} else {
|
|
813
1095
|
const fitted = fitTextToNode(node.label, node.width, node.height);
|
|
814
|
-
const textColor = nodeTextColor(node.status, palette, isDark);
|
|
815
1096
|
const totalTextHeight = fitted.lines.length * fitted.fontSize * 1.3;
|
|
816
1097
|
const startY = -totalTextHeight / 2 + fitted.fontSize * 0.65;
|
|
817
1098
|
|
|
@@ -846,9 +1127,12 @@ export function renderInitiativeStatusForExport(
|
|
|
846
1127
|
const layout = layoutInitiativeStatus(parsed);
|
|
847
1128
|
const isDark = theme === 'dark';
|
|
848
1129
|
|
|
1130
|
+
const legendEntries = collectStatuses(parsed);
|
|
1131
|
+
const EXPORT_LEGEND_GAP = 8;
|
|
1132
|
+
const legendReserve = legendEntries.length > 1 ? LEGEND_HEIGHT + EXPORT_LEGEND_GAP : 0;
|
|
849
1133
|
const titleOffset = parsed.title ? 40 : 0;
|
|
850
1134
|
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
851
|
-
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
|
|
1135
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset + legendReserve;
|
|
852
1136
|
|
|
853
1137
|
return runInExportContainer(exportWidth, exportHeight, (container) => {
|
|
854
1138
|
renderInitiativeStatus(
|
|
@@ -857,8 +1141,7 @@ export function renderInitiativeStatusForExport(
|
|
|
857
1141
|
layout,
|
|
858
1142
|
palette,
|
|
859
1143
|
isDark,
|
|
860
|
-
|
|
861
|
-
{ width: exportWidth, height: exportHeight }
|
|
1144
|
+
{ exportDims: { width: exportWidth, height: exportHeight } }
|
|
862
1145
|
);
|
|
863
1146
|
return extractExportSvg(container, theme);
|
|
864
1147
|
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { DgmoError } from '../diagnostics';
|
|
6
6
|
import type { ParticipantType } from '../sequence/parser';
|
|
7
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
7
8
|
|
|
8
9
|
export type InitiativeStatus = 'done' | 'wip' | 'todo' | 'na' | null;
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ export interface ISNode {
|
|
|
14
15
|
status: InitiativeStatus;
|
|
15
16
|
shape: ParticipantType;
|
|
16
17
|
lineNumber: number;
|
|
18
|
+
metadata: Record<string, string>;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface ISEdge {
|
|
@@ -22,6 +24,7 @@ export interface ISEdge {
|
|
|
22
24
|
label?: string; // e.g. "getUser"
|
|
23
25
|
status: InitiativeStatus;
|
|
24
26
|
lineNumber: number;
|
|
27
|
+
metadata: Record<string, string>;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface ISGroup {
|
|
@@ -37,7 +40,10 @@ export interface ParsedInitiativeStatus {
|
|
|
37
40
|
nodes: ISNode[];
|
|
38
41
|
edges: ISEdge[];
|
|
39
42
|
groups: ISGroup[];
|
|
43
|
+
tagGroups: TagGroup[];
|
|
40
44
|
options: Record<string, string>;
|
|
45
|
+
/** Initial hidden tag values from `hide:` DSL directive. Map<groupKey, Set<values>> */
|
|
46
|
+
initialHiddenTagValues: Map<string, Set<string>>;
|
|
41
47
|
diagnostics: DgmoError[];
|
|
42
48
|
error: string | null;
|
|
43
49
|
}
|
package/src/org/layout.ts
CHANGED
|
@@ -1115,8 +1115,6 @@ export function layoutOrg(
|
|
|
1115
1115
|
let finalWidth = totalWidth;
|
|
1116
1116
|
let finalHeight = totalHeight;
|
|
1117
1117
|
|
|
1118
|
-
const legendPosition = parsed.options?.['legend-position'] ?? 'top';
|
|
1119
|
-
|
|
1120
1118
|
// When a tag group is active, only that group is laid out (full size).
|
|
1121
1119
|
// When none is active, all groups are laid out minified — unless
|
|
1122
1120
|
// expandAllLegend is set (export mode), which shows all groups expanded.
|
|
@@ -1128,62 +1126,31 @@ export function layoutOrg(
|
|
|
1128
1126
|
activeTagGroup != null || allExpanded ? g.width : g.minifiedWidth;
|
|
1129
1127
|
|
|
1130
1128
|
if (visibleGroups.length > 0) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const shift = (finalWidth - totalWidth) / 2;
|
|
1141
|
-
for (const n of layoutNodes) n.x += shift;
|
|
1142
|
-
for (const c of containers) c.x += shift;
|
|
1143
|
-
for (const e of layoutEdges) {
|
|
1144
|
-
for (const p of e.points) p.x += shift;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
const contentBottom = totalHeight - MARGIN;
|
|
1149
|
-
const legendY = contentBottom + LEGEND_GAP;
|
|
1150
|
-
const startX = (finalWidth - totalGroupsWidth) / 2;
|
|
1151
|
-
|
|
1152
|
-
let cx = startX;
|
|
1153
|
-
for (const g of visibleGroups) {
|
|
1154
|
-
g.x = cx;
|
|
1155
|
-
g.y = legendY;
|
|
1156
|
-
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
|
|
1160
|
-
} else {
|
|
1161
|
-
// Top: horizontal row above chart content, left-aligned
|
|
1162
|
-
const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
1163
|
-
|
|
1164
|
-
// Push all chart content down
|
|
1165
|
-
for (const n of layoutNodes) n.y += legendShift;
|
|
1166
|
-
for (const c of containers) c.y += legendShift;
|
|
1167
|
-
for (const e of layoutEdges) {
|
|
1168
|
-
for (const p of e.points) p.y += legendShift;
|
|
1169
|
-
}
|
|
1129
|
+
// Top: horizontal row above chart content, left-aligned
|
|
1130
|
+
const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
1131
|
+
|
|
1132
|
+
// Push all chart content down
|
|
1133
|
+
for (const n of layoutNodes) n.y += legendShift;
|
|
1134
|
+
for (const c of containers) c.y += legendShift;
|
|
1135
|
+
for (const e of layoutEdges) {
|
|
1136
|
+
for (const p of e.points) p.y += legendShift;
|
|
1137
|
+
}
|
|
1170
1138
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1139
|
+
const totalGroupsWidth =
|
|
1140
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1141
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1174
1142
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1143
|
+
let cx = MARGIN;
|
|
1144
|
+
for (const g of visibleGroups) {
|
|
1145
|
+
g.x = cx;
|
|
1146
|
+
g.y = MARGIN;
|
|
1147
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1148
|
+
}
|
|
1181
1149
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1150
|
+
finalHeight += legendShift;
|
|
1151
|
+
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1152
|
+
if (neededWidth > finalWidth) {
|
|
1153
|
+
finalWidth = neededWidth;
|
|
1187
1154
|
}
|
|
1188
1155
|
}
|
|
1189
1156
|
|
package/src/org/renderer.ts
CHANGED
|
@@ -115,7 +115,6 @@ export function renderOrg(
|
|
|
115
115
|
|
|
116
116
|
const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
|
|
117
117
|
const legendOnly = layout.nodes.length === 0;
|
|
118
|
-
const legendPosition = parsed.options?.['legend-position'] ?? 'bottom';
|
|
119
118
|
const hasLegend = layout.legend.length > 0;
|
|
120
119
|
|
|
121
120
|
// In app mode (not export), render the legend at a fixed size outside the
|
|
@@ -148,10 +147,9 @@ export function renderOrg(
|
|
|
148
147
|
// Center the diagram
|
|
149
148
|
const scaledW = diagramW * scale;
|
|
150
149
|
const offsetX = (width - scaledW) / 2;
|
|
151
|
-
const offsetY =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
: DIAGRAM_PADDING + titleReserve;
|
|
150
|
+
const offsetY = fixedLegend
|
|
151
|
+
? DIAGRAM_PADDING + legendReserve + titleReserve
|
|
152
|
+
: DIAGRAM_PADDING + titleReserve;
|
|
155
153
|
|
|
156
154
|
// Create SVG
|
|
157
155
|
const svg = d3Selection
|
|
@@ -516,9 +514,7 @@ export function renderOrg(
|
|
|
516
514
|
.attr('class', 'org-legend-fixed')
|
|
517
515
|
.attr(
|
|
518
516
|
'transform',
|
|
519
|
-
|
|
520
|
-
? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`
|
|
521
|
-
: `translate(0, ${DIAGRAM_PADDING + titleReserve})`
|
|
517
|
+
`translate(0, ${DIAGRAM_PADDING + titleReserve})`
|
|
522
518
|
)
|
|
523
519
|
: contentG;
|
|
524
520
|
const legendParent = legendParentBase;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PaletteConfig } from './types';
|
|
2
|
+
import { registerPalette } from './registry';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Dracula Palette Definition
|
|
6
|
+
// https://draculatheme.com/contribute
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
export const draculaPalette: PaletteConfig = {
|
|
10
|
+
id: 'dracula',
|
|
11
|
+
name: 'Dracula',
|
|
12
|
+
light: {
|
|
13
|
+
bg: '#f8f8f2', // foreground as light bg
|
|
14
|
+
surface: '#f0f0ec',
|
|
15
|
+
overlay: '#e8e8e2',
|
|
16
|
+
border: '#d8d8d2',
|
|
17
|
+
text: '#282a36', // background as light text
|
|
18
|
+
textMuted: '#44475a', // current line
|
|
19
|
+
primary: '#6272a4', // comment
|
|
20
|
+
secondary: '#bd93f9', // purple
|
|
21
|
+
accent: '#bd93f9', // purple
|
|
22
|
+
destructive: '#ff5555', // red
|
|
23
|
+
colors: {
|
|
24
|
+
red: '#ff5555',
|
|
25
|
+
orange: '#ffb86c',
|
|
26
|
+
yellow: '#f1fa8c',
|
|
27
|
+
green: '#50fa7b',
|
|
28
|
+
blue: '#6272a4', // comment blue
|
|
29
|
+
purple: '#bd93f9',
|
|
30
|
+
teal: '#5ac8b8', // muted cyan toward green
|
|
31
|
+
cyan: '#8be9fd',
|
|
32
|
+
gray: '#6272a4',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
dark: {
|
|
36
|
+
bg: '#282a36', // background
|
|
37
|
+
surface: '#343746', // between bg and current line
|
|
38
|
+
overlay: '#44475a', // current line
|
|
39
|
+
border: '#6272a4', // comment
|
|
40
|
+
text: '#f8f8f2', // foreground
|
|
41
|
+
textMuted: '#bcc2d4', // muted foreground
|
|
42
|
+
primary: '#bd93f9', // purple (Dracula's signature)
|
|
43
|
+
secondary: '#8be9fd', // cyan
|
|
44
|
+
accent: '#ff79c6', // pink
|
|
45
|
+
destructive: '#ff5555', // red
|
|
46
|
+
colors: {
|
|
47
|
+
red: '#ff5555',
|
|
48
|
+
orange: '#ffb86c',
|
|
49
|
+
yellow: '#f1fa8c',
|
|
50
|
+
green: '#50fa7b',
|
|
51
|
+
blue: '#6272a4', // comment blue
|
|
52
|
+
purple: '#bd93f9',
|
|
53
|
+
teal: '#5ac8b8', // muted cyan toward green
|
|
54
|
+
cyan: '#8be9fd',
|
|
55
|
+
gray: '#6272a4',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
registerPalette(draculaPalette);
|
package/src/palettes/index.ts
CHANGED
|
@@ -22,15 +22,17 @@ export {
|
|
|
22
22
|
contrastText,
|
|
23
23
|
} from './color-utils';
|
|
24
24
|
|
|
25
|
-
// Re-export palette definitions
|
|
26
|
-
export {
|
|
27
|
-
export { solarizedPalette } from './solarized';
|
|
25
|
+
// Re-export palette definitions (alphabetical)
|
|
26
|
+
export { boldPalette } from './bold';
|
|
28
27
|
export { catppuccinPalette } from './catppuccin';
|
|
29
|
-
export {
|
|
28
|
+
export { draculaPalette } from './dracula';
|
|
30
29
|
export { gruvboxPalette } from './gruvbox';
|
|
31
|
-
export {
|
|
30
|
+
export { monokaiPalette } from './monokai';
|
|
31
|
+
export { nordPalette } from './nord';
|
|
32
32
|
export { oneDarkPalette } from './one-dark';
|
|
33
|
-
export {
|
|
33
|
+
export { rosePinePalette } from './rose-pine';
|
|
34
|
+
export { solarizedPalette } from './solarized';
|
|
35
|
+
export { tokyoNightPalette } from './tokyo-night';
|
|
34
36
|
|
|
35
37
|
// Re-export Mermaid bridge
|
|
36
38
|
export { buildMermaidThemeVars, buildThemeCSS } from './mermaid-bridge';
|