@diagrammo/dgmo 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +163 -162
- package/dist/index.cjs +378 -512
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -20
- package/dist/index.d.ts +5 -20
- package/dist/index.js +378 -512
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/layout.ts +7 -67
- package/src/c4/renderer.ts +122 -119
- package/src/cli.ts +6 -38
- package/src/d3.ts +55 -35
- package/src/echarts.ts +24 -24
- package/src/er/renderer.ts +15 -9
- package/src/index.ts +2 -2
- package/src/infra/compute.ts +1 -21
- package/src/infra/parser.ts +5 -32
- package/src/infra/renderer.ts +28 -164
- package/src/infra/types.ts +1 -11
- package/src/initiative-status/layout.ts +9 -6
- package/src/kanban/renderer.ts +28 -24
- package/src/org/renderer.ts +24 -23
- package/src/render.ts +2 -2
- package/src/sequence/renderer.ts +24 -19
- package/src/sitemap/layout.ts +7 -14
- package/src/sitemap/renderer.ts +30 -29
- package/src/utils/legend-constants.ts +25 -0
package/package.json
CHANGED
package/src/c4/layout.ts
CHANGED
|
@@ -603,19 +603,12 @@ export function computeC4NodeDimensions(
|
|
|
603
603
|
// Legend Helpers
|
|
604
604
|
// ============================================================
|
|
605
605
|
|
|
606
|
-
function computeLegendGroups(
|
|
607
|
-
tagGroups: OrgTagGroup[],
|
|
608
|
-
usedValuesByGroup?: Map<string, Set<string>>
|
|
609
|
-
): C4LegendGroup[] {
|
|
606
|
+
function computeLegendGroups(tagGroups: OrgTagGroup[]): C4LegendGroup[] {
|
|
610
607
|
const result: C4LegendGroup[] = [];
|
|
611
608
|
|
|
612
609
|
for (const group of tagGroups) {
|
|
613
610
|
const entries: C4LegendEntry[] = [];
|
|
614
611
|
for (const entry of group.entries) {
|
|
615
|
-
if (usedValuesByGroup) {
|
|
616
|
-
const used = usedValuesByGroup.get(group.name.toLowerCase());
|
|
617
|
-
if (!used?.has(entry.value.toLowerCase())) continue;
|
|
618
|
-
}
|
|
619
612
|
entries.push({ value: entry.value, color: entry.color });
|
|
620
613
|
}
|
|
621
614
|
if (entries.length === 0) continue;
|
|
@@ -813,20 +806,9 @@ export function layoutC4Context(
|
|
|
813
806
|
let totalWidth = nodes.length > 0 ? maxX - minX + MARGIN * 2 : 0;
|
|
814
807
|
let totalHeight = nodes.length > 0 ? maxY - minY + MARGIN * 2 : 0;
|
|
815
808
|
|
|
816
|
-
// Legend
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
for (const group of parsed.tagGroups) {
|
|
820
|
-
const key = group.name.toLowerCase();
|
|
821
|
-
const val = el.metadata[key];
|
|
822
|
-
if (val) {
|
|
823
|
-
if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, new Set());
|
|
824
|
-
usedValuesByGroup.get(key)!.add(val.toLowerCase());
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const legendGroups = computeLegendGroups(parsed.tagGroups, usedValuesByGroup);
|
|
809
|
+
// Legend: show all defined tag groups and entries so users see the full
|
|
810
|
+
// tag vocabulary regardless of which elements are visible at this view level.
|
|
811
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups);
|
|
830
812
|
|
|
831
813
|
// Position legend below diagram
|
|
832
814
|
if (legendGroups.length > 0) {
|
|
@@ -1236,20 +1218,7 @@ export function layoutC4Containers(
|
|
|
1236
1218
|
let totalWidth = maxX - minX + MARGIN * 2;
|
|
1237
1219
|
let totalHeight = maxY - minY + MARGIN * 2;
|
|
1238
1220
|
|
|
1239
|
-
|
|
1240
|
-
const usedValuesByGroup = new Map<string, Set<string>>();
|
|
1241
|
-
for (const el of [...containers, ...externals]) {
|
|
1242
|
-
for (const group of parsed.tagGroups) {
|
|
1243
|
-
const key = group.name.toLowerCase();
|
|
1244
|
-
const val = el.metadata[key];
|
|
1245
|
-
if (val) {
|
|
1246
|
-
if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, new Set());
|
|
1247
|
-
usedValuesByGroup.get(key)!.add(val.toLowerCase());
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
const legendGroups = computeLegendGroups(parsed.tagGroups, usedValuesByGroup);
|
|
1221
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups);
|
|
1253
1222
|
|
|
1254
1223
|
// Position legend below diagram
|
|
1255
1224
|
if (legendGroups.length > 0) {
|
|
@@ -1722,24 +1691,8 @@ export function layoutC4Components(
|
|
|
1722
1691
|
let totalWidth = maxX - minX + MARGIN * 2;
|
|
1723
1692
|
let totalHeight = maxY - minY + MARGIN * 2;
|
|
1724
1693
|
|
|
1725
|
-
// Legend
|
|
1726
|
-
const usedValuesByGroup = new Map<string, Set<string>>();
|
|
1727
|
-
for (const el of [...components, ...externals]) {
|
|
1728
|
-
for (const group of parsed.tagGroups) {
|
|
1729
|
-
const key = group.name.toLowerCase();
|
|
1730
|
-
// Check element + ancestors for inherited values
|
|
1731
|
-
let val = el.metadata[key];
|
|
1732
|
-
if (!val && components.includes(el)) {
|
|
1733
|
-
val = targetContainer.metadata[key] ?? system.metadata[key];
|
|
1734
|
-
}
|
|
1735
|
-
if (val) {
|
|
1736
|
-
if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, new Set());
|
|
1737
|
-
usedValuesByGroup.get(key)!.add(val.toLowerCase());
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
1694
|
|
|
1742
|
-
const legendGroups = computeLegendGroups(parsed.tagGroups
|
|
1695
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups);
|
|
1743
1696
|
|
|
1744
1697
|
// Position legend below diagram
|
|
1745
1698
|
if (legendGroups.length > 0) {
|
|
@@ -2104,20 +2057,7 @@ export function layoutC4Deployment(
|
|
|
2104
2057
|
let totalWidth = maxX - minX + MARGIN * 2;
|
|
2105
2058
|
let totalHeight = maxY - minY + MARGIN * 2;
|
|
2106
2059
|
|
|
2107
|
-
|
|
2108
|
-
const usedValuesByGroup = new Map<string, Set<string>>();
|
|
2109
|
-
for (const r of refEntries) {
|
|
2110
|
-
for (const group of parsed.tagGroups) {
|
|
2111
|
-
const key = group.name.toLowerCase();
|
|
2112
|
-
const val = r.element.metadata[key];
|
|
2113
|
-
if (val) {
|
|
2114
|
-
if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, new Set());
|
|
2115
|
-
usedValuesByGroup.get(key)!.add(val.toLowerCase());
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
const legendGroups = computeLegendGroups(parsed.tagGroups, usedValuesByGroup);
|
|
2060
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups);
|
|
2121
2061
|
|
|
2122
2062
|
if (legendGroups.length > 0) {
|
|
2123
2063
|
const legendY = totalHeight + MARGIN;
|
package/src/c4/renderer.ts
CHANGED
|
@@ -10,9 +10,22 @@ import { mix } from '../palettes/color-utils';
|
|
|
10
10
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
11
11
|
import type { ParsedC4 } from './types';
|
|
12
12
|
import type { C4Shape } from './types';
|
|
13
|
-
import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary } from './layout';
|
|
13
|
+
import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary, C4LegendGroup } from './layout';
|
|
14
14
|
import { parseC4 } from './parser';
|
|
15
15
|
import { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment, collectCardMetadata } from './layout';
|
|
16
|
+
import {
|
|
17
|
+
LEGEND_HEIGHT,
|
|
18
|
+
LEGEND_PILL_FONT_SIZE,
|
|
19
|
+
LEGEND_PILL_FONT_W,
|
|
20
|
+
LEGEND_PILL_PAD,
|
|
21
|
+
LEGEND_DOT_R,
|
|
22
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
23
|
+
LEGEND_ENTRY_FONT_W,
|
|
24
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
25
|
+
LEGEND_ENTRY_TRAIL,
|
|
26
|
+
LEGEND_CAPSULE_PAD,
|
|
27
|
+
LEGEND_GROUP_GAP,
|
|
28
|
+
} from '../utils/legend-constants';
|
|
16
29
|
|
|
17
30
|
// ============================================================
|
|
18
31
|
// Constants
|
|
@@ -59,16 +72,6 @@ const PERSON_ICON_W = PERSON_ARM_SPAN * 2; // total width including arms
|
|
|
59
72
|
const PERSON_SW = 1.5;
|
|
60
73
|
|
|
61
74
|
// Legend constants (match org)
|
|
62
|
-
const LEGEND_HEIGHT = 28;
|
|
63
|
-
const LEGEND_PILL_FONT_SIZE = 11;
|
|
64
|
-
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
65
|
-
const LEGEND_PILL_PAD = 16;
|
|
66
|
-
const LEGEND_DOT_R = 4;
|
|
67
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
68
|
-
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
69
|
-
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
70
|
-
const LEGEND_ENTRY_TRAIL = 8;
|
|
71
|
-
const LEGEND_CAPSULE_PAD = 4;
|
|
72
75
|
|
|
73
76
|
// ============================================================
|
|
74
77
|
// Color helpers
|
|
@@ -239,8 +242,16 @@ export function renderC4Context(
|
|
|
239
242
|
|
|
240
243
|
const titleHeight = parsed.title ? TITLE_HEIGHT + 10 : 0;
|
|
241
244
|
const diagramW = layout.width;
|
|
242
|
-
const
|
|
243
|
-
|
|
245
|
+
const hasLegend = layout.legend.length > 0;
|
|
246
|
+
// In app mode, legend is a fixed overlay outside the scaled group.
|
|
247
|
+
// C4 layout adds MARGIN(40) + LEGEND_HEIGHT below content — remove that from diagramH.
|
|
248
|
+
const C4_LAYOUT_MARGIN = 40;
|
|
249
|
+
const LEGEND_FIXED_GAP = 8;
|
|
250
|
+
const fixedLegend = !exportDims && hasLegend;
|
|
251
|
+
const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
|
|
252
|
+
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
253
|
+
const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
|
|
254
|
+
const availH = height - titleHeight - legendReserveH;
|
|
244
255
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
245
256
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
246
257
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
@@ -428,6 +439,23 @@ export function renderC4Context(
|
|
|
428
439
|
.attr('data-line-number', String(node.lineNumber))
|
|
429
440
|
.attr('data-node-id', node.id);
|
|
430
441
|
|
|
442
|
+
if (activeTagGroup) {
|
|
443
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
444
|
+
const tagValue = node.metadata[tagKey];
|
|
445
|
+
if (tagValue) {
|
|
446
|
+
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
|
447
|
+
} else {
|
|
448
|
+
// Fall back to the group's defaultValue so hover-dimming works for
|
|
449
|
+
// nodes that inherit the default (e.g. sc: Internal default).
|
|
450
|
+
const tagGroup = parsed.tagGroups.find(
|
|
451
|
+
(g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
|
|
452
|
+
);
|
|
453
|
+
if (tagGroup?.defaultValue) {
|
|
454
|
+
nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
431
459
|
if (node.importPath) {
|
|
432
460
|
nodeG.attr('data-import-path', node.importPath);
|
|
433
461
|
}
|
|
@@ -566,101 +594,18 @@ export function renderC4Context(
|
|
|
566
594
|
}
|
|
567
595
|
|
|
568
596
|
// ── Legend ──
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
: mix(palette.surface, palette.bg, 30);
|
|
580
|
-
|
|
581
|
-
const pillLabel = group.name;
|
|
582
|
-
const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
583
|
-
|
|
584
|
-
const gEl = contentG
|
|
585
|
-
.append('g')
|
|
586
|
-
.attr('transform', `translate(${group.x}, ${group.y})`)
|
|
587
|
-
.attr('class', 'c4-legend-group')
|
|
588
|
-
.attr('data-legend-group', group.name.toLowerCase())
|
|
589
|
-
.style('cursor', 'pointer');
|
|
590
|
-
|
|
591
|
-
if (isActive) {
|
|
592
|
-
gEl
|
|
593
|
-
.append('rect')
|
|
594
|
-
.attr('width', group.width)
|
|
595
|
-
.attr('height', LEGEND_HEIGHT)
|
|
596
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
597
|
-
.attr('fill', groupBg);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
601
|
-
const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
602
|
-
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
603
|
-
|
|
604
|
-
gEl
|
|
605
|
-
.append('rect')
|
|
606
|
-
.attr('x', pillX)
|
|
607
|
-
.attr('y', pillY)
|
|
608
|
-
.attr('width', pillWidth)
|
|
609
|
-
.attr('height', pillH)
|
|
610
|
-
.attr('rx', pillH / 2)
|
|
611
|
-
.attr('fill', isActive ? palette.bg : groupBg);
|
|
612
|
-
|
|
613
|
-
if (isActive) {
|
|
614
|
-
gEl
|
|
615
|
-
.append('rect')
|
|
616
|
-
.attr('x', pillX)
|
|
617
|
-
.attr('y', pillY)
|
|
618
|
-
.attr('width', pillWidth)
|
|
619
|
-
.attr('height', pillH)
|
|
620
|
-
.attr('rx', pillH / 2)
|
|
621
|
-
.attr('fill', 'none')
|
|
622
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
623
|
-
.attr('stroke-width', 0.75);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
gEl
|
|
627
|
-
.append('text')
|
|
628
|
-
.attr('x', pillX + pillWidth / 2)
|
|
629
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
630
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
631
|
-
.attr('font-weight', '500')
|
|
632
|
-
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
633
|
-
.attr('text-anchor', 'middle')
|
|
634
|
-
.text(pillLabel);
|
|
635
|
-
|
|
636
|
-
if (isActive) {
|
|
637
|
-
let entryX = pillX + pillWidth + 4;
|
|
638
|
-
for (const entry of group.entries) {
|
|
639
|
-
const entryG = gEl
|
|
640
|
-
.append('g')
|
|
641
|
-
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
642
|
-
.style('cursor', 'pointer');
|
|
643
|
-
|
|
644
|
-
entryG
|
|
645
|
-
.append('circle')
|
|
646
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
647
|
-
.attr('cy', LEGEND_HEIGHT / 2)
|
|
648
|
-
.attr('r', LEGEND_DOT_R)
|
|
649
|
-
.attr('fill', entry.color);
|
|
650
|
-
|
|
651
|
-
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
652
|
-
entryG
|
|
653
|
-
.append('text')
|
|
654
|
-
.attr('x', textX)
|
|
655
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
656
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
657
|
-
.attr('fill', palette.textMuted)
|
|
658
|
-
.text(entry.value);
|
|
659
|
-
|
|
660
|
-
entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
597
|
+
if (hasLegend) {
|
|
598
|
+
// App mode: fixed overlay at SVG bottom so it's always readable regardless of scale.
|
|
599
|
+
// Export mode: render inside scaled contentG at layout coordinates.
|
|
600
|
+
const legendParent = fixedLegend
|
|
601
|
+
? svg.append('g')
|
|
602
|
+
.attr('class', 'c4-legend-fixed')
|
|
603
|
+
.attr('transform', `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`)
|
|
604
|
+
: contentG.append('g').attr('class', 'c4-legend');
|
|
605
|
+
if (activeTagGroup) {
|
|
606
|
+
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
663
607
|
}
|
|
608
|
+
renderLegend(legendParent as GSelection, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
|
|
664
609
|
}
|
|
665
610
|
}
|
|
666
611
|
|
|
@@ -1189,29 +1134,52 @@ function placeEdgeLabels(
|
|
|
1189
1134
|
}
|
|
1190
1135
|
|
|
1191
1136
|
function renderLegend(
|
|
1192
|
-
|
|
1137
|
+
parent: GSelection,
|
|
1193
1138
|
layout: C4LayoutResult,
|
|
1194
1139
|
palette: PaletteColors,
|
|
1195
1140
|
isDark: boolean,
|
|
1196
|
-
activeTagGroup?: string | null
|
|
1141
|
+
activeTagGroup?: string | null,
|
|
1142
|
+
/** When set, center groups horizontally across this width (fixed overlay mode). */
|
|
1143
|
+
fixedWidth?: number | null
|
|
1197
1144
|
): void {
|
|
1198
|
-
|
|
1145
|
+
const visibleGroups = activeTagGroup != null
|
|
1146
|
+
? layout.legend.filter((g) => g.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase())
|
|
1147
|
+
: layout.legend;
|
|
1148
|
+
|
|
1149
|
+
const pillWidthOf = (g: C4LegendGroup) => g.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
1150
|
+
const effectiveW = (g: C4LegendGroup) => activeTagGroup != null ? g.width : pillWidthOf(g);
|
|
1151
|
+
|
|
1152
|
+
// In fixed mode, compute centered x-positions
|
|
1153
|
+
let fixedPositions: Map<string, number> | null = null;
|
|
1154
|
+
if (fixedWidth != null && visibleGroups.length > 0) {
|
|
1155
|
+
fixedPositions = new Map();
|
|
1156
|
+
const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0)
|
|
1157
|
+
+ (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1158
|
+
let cx = Math.max(DIAGRAM_PADDING, (fixedWidth - totalW) / 2);
|
|
1159
|
+
for (const g of visibleGroups) {
|
|
1160
|
+
fixedPositions.set(g.name, cx);
|
|
1161
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
for (const group of visibleGroups) {
|
|
1199
1166
|
const isActive =
|
|
1200
1167
|
activeTagGroup != null &&
|
|
1201
1168
|
group.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase();
|
|
1202
1169
|
|
|
1203
|
-
if (activeTagGroup != null && !isActive) continue;
|
|
1204
|
-
|
|
1205
1170
|
const groupBg = isDark
|
|
1206
1171
|
? mix(palette.surface, palette.bg, 50)
|
|
1207
1172
|
: mix(palette.surface, palette.bg, 30);
|
|
1208
1173
|
|
|
1209
1174
|
const pillLabel = group.name;
|
|
1210
|
-
const pillWidth =
|
|
1175
|
+
const pillWidth = pillWidthOf(group);
|
|
1176
|
+
|
|
1177
|
+
const gX = fixedPositions?.get(group.name) ?? group.x;
|
|
1178
|
+
const gY = fixedPositions != null ? 0 : group.y;
|
|
1211
1179
|
|
|
1212
|
-
const gEl =
|
|
1180
|
+
const gEl = parent
|
|
1213
1181
|
.append('g')
|
|
1214
|
-
.attr('transform', `translate(${
|
|
1182
|
+
.attr('transform', `translate(${gX}, ${gY})`)
|
|
1215
1183
|
.attr('class', 'c4-legend-group')
|
|
1216
1184
|
.attr('data-legend-group', group.name.toLowerCase())
|
|
1217
1185
|
.style('cursor', 'pointer');
|
|
@@ -1317,8 +1285,16 @@ export function renderC4Containers(
|
|
|
1317
1285
|
|
|
1318
1286
|
const titleHeight = parsed.title ? TITLE_HEIGHT + 10 : 0;
|
|
1319
1287
|
const diagramW = layout.width;
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1288
|
+
const hasLegend = layout.legend.length > 0;
|
|
1289
|
+
// In app mode, legend is a fixed overlay outside the scaled group.
|
|
1290
|
+
// C4 layout adds MARGIN(40) + LEGEND_HEIGHT below content — remove that from diagramH.
|
|
1291
|
+
const C4_LAYOUT_MARGIN = 40;
|
|
1292
|
+
const LEGEND_FIXED_GAP = 8;
|
|
1293
|
+
const fixedLegend = !exportDims && hasLegend;
|
|
1294
|
+
const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
|
|
1295
|
+
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
1296
|
+
const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
|
|
1297
|
+
const availH = height - titleHeight - legendReserveH;
|
|
1322
1298
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
1323
1299
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
1324
1300
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
@@ -1508,6 +1484,23 @@ export function renderC4Containers(
|
|
|
1508
1484
|
.attr('data-line-number', String(node.lineNumber))
|
|
1509
1485
|
.attr('data-node-id', node.id);
|
|
1510
1486
|
|
|
1487
|
+
if (activeTagGroup) {
|
|
1488
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
1489
|
+
const tagValue = node.metadata[tagKey];
|
|
1490
|
+
if (tagValue) {
|
|
1491
|
+
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
|
1492
|
+
} else {
|
|
1493
|
+
// Fall back to the group's defaultValue so hover-dimming works for
|
|
1494
|
+
// nodes that inherit the default (e.g. sc: Internal default).
|
|
1495
|
+
const tagGroup = parsed.tagGroups.find(
|
|
1496
|
+
(g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
|
|
1497
|
+
);
|
|
1498
|
+
if (tagGroup?.defaultValue) {
|
|
1499
|
+
nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1511
1504
|
if (node.shape) {
|
|
1512
1505
|
nodeG.attr('data-shape', node.shape);
|
|
1513
1506
|
}
|
|
@@ -1717,8 +1710,18 @@ export function renderC4Containers(
|
|
|
1717
1710
|
}
|
|
1718
1711
|
|
|
1719
1712
|
// ── Legend ──
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1713
|
+
if (hasLegend) {
|
|
1714
|
+
// App mode: fixed overlay at SVG bottom so it's always readable regardless of scale.
|
|
1715
|
+
// Export mode: render inside scaled contentG at layout coordinates.
|
|
1716
|
+
const legendParent = fixedLegend
|
|
1717
|
+
? svg.append('g')
|
|
1718
|
+
.attr('class', 'c4-legend-fixed')
|
|
1719
|
+
.attr('transform', `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`)
|
|
1720
|
+
: contentG.append('g').attr('class', 'c4-legend');
|
|
1721
|
+
if (activeTagGroup) {
|
|
1722
|
+
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
1723
|
+
}
|
|
1724
|
+
renderLegend(legendParent as GSelection, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
|
|
1722
1725
|
}
|
|
1723
1726
|
}
|
|
1724
1727
|
|
package/src/cli.ts
CHANGED
|
@@ -73,6 +73,7 @@ Options:
|
|
|
73
73
|
--c4-level <level> C4 render level: context (default), containers, components, deployment
|
|
74
74
|
--c4-system <name> System to drill into (with --c4-level containers or components)
|
|
75
75
|
--c4-container <name> Container to drill into (with --c4-level components)
|
|
76
|
+
--tag-group <name> Pre-select a tag group for static export coloring
|
|
76
77
|
--no-branding Omit diagrammo.app branding from exports
|
|
77
78
|
--copy Copy URL to clipboard (only with -o url)
|
|
78
79
|
--json Output structured JSON to stdout
|
|
@@ -102,8 +103,7 @@ function parseArgs(argv: string[]): {
|
|
|
102
103
|
c4Level: 'context' | 'containers' | 'components' | 'deployment';
|
|
103
104
|
c4System: string | undefined;
|
|
104
105
|
c4Container: string | undefined;
|
|
105
|
-
|
|
106
|
-
listScenarios: boolean;
|
|
106
|
+
tagGroup: string | undefined;
|
|
107
107
|
} {
|
|
108
108
|
const result = {
|
|
109
109
|
input: undefined as string | undefined,
|
|
@@ -119,8 +119,7 @@ function parseArgs(argv: string[]): {
|
|
|
119
119
|
c4Level: 'context' as 'context' | 'containers' | 'components' | 'deployment',
|
|
120
120
|
c4System: undefined as string | undefined,
|
|
121
121
|
c4Container: undefined as string | undefined,
|
|
122
|
-
|
|
123
|
-
listScenarios: false,
|
|
122
|
+
tagGroup: undefined as string | undefined,
|
|
124
123
|
};
|
|
125
124
|
|
|
126
125
|
const args = argv.slice(2); // skip node + script
|
|
@@ -174,11 +173,8 @@ function parseArgs(argv: string[]): {
|
|
|
174
173
|
} else if (arg === '--c4-container') {
|
|
175
174
|
result.c4Container = args[++i];
|
|
176
175
|
i++;
|
|
177
|
-
} else if (arg === '--
|
|
178
|
-
result.
|
|
179
|
-
i++;
|
|
180
|
-
} else if (arg === '--list-scenarios') {
|
|
181
|
-
result.listScenarios = true;
|
|
176
|
+
} else if (arg === '--tag-group') {
|
|
177
|
+
result.tagGroup = args[++i];
|
|
182
178
|
i++;
|
|
183
179
|
} else if (arg === '--no-branding') {
|
|
184
180
|
result.noBranding = true;
|
|
@@ -440,34 +436,6 @@ async function main(): Promise<void> {
|
|
|
440
436
|
}
|
|
441
437
|
}
|
|
442
438
|
|
|
443
|
-
// List scenarios (infra diagrams)
|
|
444
|
-
if (opts.listScenarios) {
|
|
445
|
-
const { parseInfra } = await import('./infra/parser');
|
|
446
|
-
const infraParsed = parseInfra(content);
|
|
447
|
-
if (infraParsed.scenarios.length === 0) {
|
|
448
|
-
console.log('(no scenarios defined)');
|
|
449
|
-
} else {
|
|
450
|
-
for (const s of infraParsed.scenarios) {
|
|
451
|
-
console.log(s.name);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Validate --scenario name against defined scenarios
|
|
458
|
-
if (opts.scenario) {
|
|
459
|
-
const { parseInfra } = await import('./infra/parser');
|
|
460
|
-
const infraParsed = parseInfra(content);
|
|
461
|
-
const match = infraParsed.scenarios.find((s) => s.name.toLowerCase() === opts.scenario!.toLowerCase());
|
|
462
|
-
if (!match) {
|
|
463
|
-
const available = infraParsed.scenarios.map((s) => s.name);
|
|
464
|
-
const msg = available.length > 0
|
|
465
|
-
? `Error: Unknown scenario "${opts.scenario}". Available: ${available.join(', ')}`
|
|
466
|
-
: `Error: Unknown scenario "${opts.scenario}". No scenarios defined in this file.`;
|
|
467
|
-
exitWithJsonError(msg);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
439
|
// Validate C4 options
|
|
472
440
|
if (opts.c4Level === 'containers' && !opts.c4System) {
|
|
473
441
|
exitWithJsonError('Error: --c4-system is required when --c4-level is containers');
|
|
@@ -488,7 +456,7 @@ async function main(): Promise<void> {
|
|
|
488
456
|
c4Level: opts.c4Level,
|
|
489
457
|
c4System: opts.c4System,
|
|
490
458
|
c4Container: opts.c4Container,
|
|
491
|
-
|
|
459
|
+
tagGroup: opts.tagGroup,
|
|
492
460
|
});
|
|
493
461
|
|
|
494
462
|
if (!svg) {
|