@diagrammo/dgmo 0.6.2 ā 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/.claude/commands/dgmo.md +231 -13
- package/AGENTS.md +148 -0
- package/dist/cli.cjs +341 -165
- package/dist/index.cjs +4900 -1685
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -18
- package/dist/index.d.ts +259 -18
- package/dist/index.js +4642 -1436
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/c4/layout.ts +0 -5
- package/src/c4/parser.ts +0 -16
- package/src/c4/renderer.ts +7 -11
- package/src/class/layout.ts +0 -1
- package/src/class/parser.ts +28 -0
- package/src/class/renderer.ts +189 -34
- package/src/cli.ts +566 -25
- package/src/colors.ts +3 -3
- package/src/completion.ts +58 -0
- package/src/d3.ts +179 -122
- package/src/dgmo-router.ts +3 -58
- package/src/echarts.ts +96 -55
- package/src/er/parser.ts +30 -1
- package/src/er/renderer.ts +12 -7
- 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/graph/flowchart-parser.ts +27 -4
- package/src/graph/flowchart-renderer.ts +1 -2
- package/src/graph/state-parser.ts +0 -1
- package/src/graph/state-renderer.ts +1 -3
- package/src/index.ts +37 -0
- package/src/infra/compute.ts +0 -7
- package/src/infra/layout.ts +0 -2
- package/src/infra/parser.ts +46 -4
- package/src/infra/renderer.ts +49 -27
- 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 +298 -35
- package/src/initiative-status/types.ts +6 -0
- package/src/kanban/parser.ts +0 -2
- package/src/org/layout.ts +22 -59
- package/src/org/renderer.ts +11 -36
- 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/parser.ts +14 -11
- package/src/sequence/renderer.ts +5 -6
- package/src/sequence/tag-resolution.ts +0 -1
- package/src/sharing.ts +8 -0
- package/src/sitemap/layout.ts +1 -14
- package/src/sitemap/parser.ts +1 -2
- package/src/sitemap/renderer.ts +4 -7
- package/src/utils/arrows.ts +7 -7
- package/src/utils/duration.ts +212 -0
- package/src/utils/export-container.ts +40 -0
- package/src/utils/legend-constants.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "DGMO diagram markup language ā parser, renderer, and color system",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
".claude/commands",
|
|
30
30
|
".github/copilot-instructions.md",
|
|
31
31
|
".cursorrules",
|
|
32
|
-
".windsurfrules"
|
|
32
|
+
".windsurfrules",
|
|
33
|
+
"AGENTS.md"
|
|
33
34
|
],
|
|
34
35
|
"sideEffects": false,
|
|
35
36
|
"scripts": {
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"test:watch": "vitest",
|
|
41
42
|
"gallery": "pnpm build && node scripts/generate-gallery.mjs",
|
|
42
43
|
"check:duplication": "jscpd ./src",
|
|
44
|
+
"check:deadcode": "knip",
|
|
43
45
|
"postinstall": "node -e \"console.log('\\nš” Claude Code user? Run: dgmo --install-claude-skill\\n')\""
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
@@ -64,9 +66,9 @@
|
|
|
64
66
|
"@types/d3-scale": "^4.0.8",
|
|
65
67
|
"@types/d3-selection": "^3.0.11",
|
|
66
68
|
"@types/d3-shape": "^3.1.7",
|
|
67
|
-
"@types/dagre": "^0.7.54",
|
|
68
69
|
"@types/jsdom": "^28.0.0",
|
|
69
70
|
"jscpd": "^4.0.8",
|
|
71
|
+
"knip": "^6.0.1",
|
|
70
72
|
"tsup": "^8.5.1",
|
|
71
73
|
"typescript": "^5.7.3",
|
|
72
74
|
"vitest": "^4.0.18"
|
package/src/c4/layout.ts
CHANGED
|
@@ -86,7 +86,6 @@ const DESC_LINE_HEIGHT = 16;
|
|
|
86
86
|
const DESC_CHAR_WIDTH = 6.5;
|
|
87
87
|
const CARD_V_PAD = 14;
|
|
88
88
|
const CARD_H_PAD = 20;
|
|
89
|
-
const TECH_LINE_HEIGHT = 16;
|
|
90
89
|
const META_LINE_HEIGHT = 16;
|
|
91
90
|
const META_CHAR_WIDTH = 6.5;
|
|
92
91
|
const MARGIN = 40;
|
|
@@ -478,10 +477,6 @@ export function rollUpContextRelationships(parsed: ParsedC4): ContextRelationshi
|
|
|
478
477
|
const allRels = collectAllRelationships(parsed.elements, ownerMap);
|
|
479
478
|
|
|
480
479
|
// Also include orphan relationships
|
|
481
|
-
for (const rel of parsed.relationships) {
|
|
482
|
-
// Orphan rels have no source element name ā skip them for context roll-up
|
|
483
|
-
}
|
|
484
|
-
|
|
485
480
|
// Separate system-level (explicit) from nested (rolled-up)
|
|
486
481
|
const topLevelNames = new Set(parsed.elements.map((e) => e.name));
|
|
487
482
|
const explicitKeys = new Set<string>();
|
package/src/c4/parser.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import type { PaletteColors } from '../palettes';
|
|
6
|
-
import type { DgmoError } from '../diagnostics';
|
|
7
6
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
8
7
|
import type { TagGroup } from '../utils/tag-groups';
|
|
9
8
|
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
@@ -751,21 +750,6 @@ function attachElement(
|
|
|
751
750
|
// Post-parse validation
|
|
752
751
|
// ============================================================
|
|
753
752
|
|
|
754
|
-
function collectAllNames(result: ParsedC4): Map<string, number> {
|
|
755
|
-
const names = new Map<string, number>();
|
|
756
|
-
function walk(elements: C4Element[]) {
|
|
757
|
-
for (const el of elements) {
|
|
758
|
-
names.set(el.name.toLowerCase(), el.lineNumber);
|
|
759
|
-
walk(el.children);
|
|
760
|
-
for (const g of el.groups) {
|
|
761
|
-
walk(g.children);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
walk(result.elements);
|
|
766
|
-
return names;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
753
|
function validateRelationshipTargets(
|
|
770
754
|
result: ParsedC4,
|
|
771
755
|
knownNames: Map<string, number>,
|
package/src/c4/renderer.ts
CHANGED
|
@@ -9,8 +9,7 @@ import type { PaletteColors } from '../palettes';
|
|
|
9
9
|
import { mix } from '../palettes/color-utils';
|
|
10
10
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
11
11
|
import type { ParsedC4 } from './types';
|
|
12
|
-
import type {
|
|
13
|
-
import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary, C4LegendGroup } from './layout';
|
|
12
|
+
import type { C4LayoutResult, C4LayoutEdge, C4LegendGroup } from './layout';
|
|
14
13
|
import { parseC4 } from './parser';
|
|
15
14
|
import { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment, collectCardMetadata } from './layout';
|
|
16
15
|
import {
|
|
@@ -50,7 +49,6 @@ const CARD_V_PAD = 14;
|
|
|
50
49
|
const TYPE_LABEL_HEIGHT = 18;
|
|
51
50
|
const DIVIDER_GAP = 6;
|
|
52
51
|
const NAME_HEIGHT = 20;
|
|
53
|
-
const TECH_LINE_HEIGHT = 16;
|
|
54
52
|
const META_FONT_SIZE = 11;
|
|
55
53
|
const META_CHAR_WIDTH = 6.5;
|
|
56
54
|
const META_LINE_HEIGHT = 16;
|
|
@@ -257,9 +255,8 @@ export function renderC4Context(
|
|
|
257
255
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
258
256
|
|
|
259
257
|
const scaledW = diagramW * scale;
|
|
260
|
-
const scaledH = diagramH * scale;
|
|
261
258
|
const offsetX = (width - scaledW) / 2;
|
|
262
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
259
|
+
const offsetY = titleHeight + DIAGRAM_PADDING + legendReserveH;
|
|
263
260
|
|
|
264
261
|
const svg = d3Selection
|
|
265
262
|
.select(container)
|
|
@@ -595,12 +592,12 @@ export function renderC4Context(
|
|
|
595
592
|
|
|
596
593
|
// āā Legend āā
|
|
597
594
|
if (hasLegend) {
|
|
598
|
-
// App mode: fixed overlay at SVG
|
|
595
|
+
// App mode: fixed overlay at SVG top so it's always readable regardless of scale.
|
|
599
596
|
// Export mode: render inside scaled contentG at layout coordinates.
|
|
600
597
|
const legendParent = fixedLegend
|
|
601
598
|
? svg.append('g')
|
|
602
599
|
.attr('class', 'c4-legend-fixed')
|
|
603
|
-
.attr('transform', `translate(0, ${
|
|
600
|
+
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
|
|
604
601
|
: contentG.append('g').attr('class', 'c4-legend');
|
|
605
602
|
if (activeTagGroup) {
|
|
606
603
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
@@ -1300,9 +1297,8 @@ export function renderC4Containers(
|
|
|
1300
1297
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
1301
1298
|
|
|
1302
1299
|
const scaledW = diagramW * scale;
|
|
1303
|
-
const scaledH = diagramH * scale;
|
|
1304
1300
|
const offsetX = (width - scaledW) / 2;
|
|
1305
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
1301
|
+
const offsetY = titleHeight + DIAGRAM_PADDING + legendReserveH;
|
|
1306
1302
|
|
|
1307
1303
|
const svg = d3Selection
|
|
1308
1304
|
.select(container)
|
|
@@ -1711,12 +1707,12 @@ export function renderC4Containers(
|
|
|
1711
1707
|
|
|
1712
1708
|
// āā Legend āā
|
|
1713
1709
|
if (hasLegend) {
|
|
1714
|
-
// App mode: fixed overlay at SVG
|
|
1710
|
+
// App mode: fixed overlay at SVG top so it's always readable regardless of scale.
|
|
1715
1711
|
// Export mode: render inside scaled contentG at layout coordinates.
|
|
1716
1712
|
const legendParent = fixedLegend
|
|
1717
1713
|
? svg.append('g')
|
|
1718
1714
|
.attr('class', 'c4-legend-fixed')
|
|
1719
|
-
.attr('transform', `translate(0, ${
|
|
1715
|
+
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
|
|
1720
1716
|
: contentG.append('g').attr('class', 'c4-legend');
|
|
1721
1717
|
if (activeTagGroup) {
|
|
1722
1718
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
package/src/class/layout.ts
CHANGED
package/src/class/parser.ts
CHANGED
|
@@ -383,3 +383,31 @@ export function looksLikeClassDiagram(content: string): boolean {
|
|
|
383
383
|
|
|
384
384
|
return false;
|
|
385
385
|
}
|
|
386
|
+
|
|
387
|
+
// ============================================================
|
|
388
|
+
// Symbol extraction (for completion API)
|
|
389
|
+
// ============================================================
|
|
390
|
+
|
|
391
|
+
import type { DiagramSymbols } from '../completion';
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Extract class names (entities) from class diagram document text.
|
|
395
|
+
* Used by the dgmo completion API for ghost hints and popup completions.
|
|
396
|
+
*/
|
|
397
|
+
export function extractSymbols(docText: string): DiagramSymbols {
|
|
398
|
+
const entities: string[] = [];
|
|
399
|
+
let inMetadata = true;
|
|
400
|
+
for (const rawLine of docText.split('\n')) {
|
|
401
|
+
const line = rawLine.trim();
|
|
402
|
+
if (inMetadata && /^[a-z-]+\s*:/i.test(line)) continue;
|
|
403
|
+
inMetadata = false;
|
|
404
|
+
if (line.length === 0 || /^\s/.test(rawLine)) continue;
|
|
405
|
+
const m = CLASS_DECL_RE.exec(line);
|
|
406
|
+
if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
kind: 'class',
|
|
410
|
+
entities,
|
|
411
|
+
keywords: ['extends', 'implements', 'abstract', 'interface', 'enum'],
|
|
412
|
+
};
|
|
413
|
+
}
|
package/src/class/renderer.ts
CHANGED
|
@@ -5,10 +5,23 @@
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
6
|
import * as d3Shape from 'd3-shape';
|
|
7
7
|
import { FONT_FAMILY } from '../fonts';
|
|
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
|
+
} from '../utils/legend-constants';
|
|
8
21
|
import type { PaletteColors } from '../palettes';
|
|
9
22
|
import { mix } from '../palettes/color-utils';
|
|
10
23
|
import type { ParsedClassDiagram, ClassModifier, RelationshipType } from './types';
|
|
11
|
-
import type { ClassLayoutResult
|
|
24
|
+
import type { ClassLayoutResult } from './layout';
|
|
12
25
|
import { parseClassDiagram } from './parser';
|
|
13
26
|
import { layoutClassDiagram } from './layout';
|
|
14
27
|
|
|
@@ -50,6 +63,49 @@ function nodeStroke(palette: PaletteColors, modifier: ClassModifier | undefined,
|
|
|
50
63
|
return nodeColor ?? modifierColor(modifier, palette, colorOff);
|
|
51
64
|
}
|
|
52
65
|
|
|
66
|
+
// ============================================================
|
|
67
|
+
// Legend helpers
|
|
68
|
+
// ============================================================
|
|
69
|
+
|
|
70
|
+
interface ClassLegendEntry {
|
|
71
|
+
label: string;
|
|
72
|
+
colorKey: keyof PaletteColors['colors'];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CLASS_TYPE_MAP: Record<string, ClassLegendEntry> = {
|
|
76
|
+
class: { label: 'Class', colorKey: 'teal' },
|
|
77
|
+
abstract: { label: 'Abstract', colorKey: 'purple' },
|
|
78
|
+
interface: { label: 'Interface', colorKey: 'blue' },
|
|
79
|
+
enum: { label: 'Enum', colorKey: 'yellow' },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const CLASS_TYPE_ORDER = ['class', 'abstract', 'interface', 'enum'];
|
|
83
|
+
|
|
84
|
+
function collectClassTypes(parsed: ParsedClassDiagram): ClassLegendEntry[] {
|
|
85
|
+
if (parsed.options?.color === 'off') return [];
|
|
86
|
+
|
|
87
|
+
const present = new Set<string>();
|
|
88
|
+
for (const c of parsed.classes) {
|
|
89
|
+
if (c.color) continue; // explicit color override ā skip
|
|
90
|
+
present.add(c.modifier ?? 'class');
|
|
91
|
+
}
|
|
92
|
+
return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LEGEND_GROUP_NAME = 'Type';
|
|
96
|
+
|
|
97
|
+
function legendEntriesWidth(entries: ClassLegendEntry[]): number {
|
|
98
|
+
let w = 0;
|
|
99
|
+
for (const e of entries) {
|
|
100
|
+
w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
101
|
+
}
|
|
102
|
+
return w;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function classTypeKey(modifier: ClassModifier | undefined): string {
|
|
106
|
+
return modifier ?? 'class';
|
|
107
|
+
}
|
|
108
|
+
|
|
53
109
|
// ============================================================
|
|
54
110
|
// Visibility symbols
|
|
55
111
|
// ============================================================
|
|
@@ -99,8 +155,6 @@ function isSourceMarker(type: RelationshipType): boolean {
|
|
|
99
155
|
// Main renderer
|
|
100
156
|
// ============================================================
|
|
101
157
|
|
|
102
|
-
type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
103
|
-
|
|
104
158
|
export function renderClassDiagram(
|
|
105
159
|
container: HTMLDivElement,
|
|
106
160
|
parsed: ParsedClassDiagram,
|
|
@@ -108,7 +162,8 @@ export function renderClassDiagram(
|
|
|
108
162
|
palette: PaletteColors,
|
|
109
163
|
isDark: boolean,
|
|
110
164
|
onClickItem?: (lineNumber: number) => void,
|
|
111
|
-
exportDims?: { width?: number; height?: number }
|
|
165
|
+
exportDims?: { width?: number; height?: number },
|
|
166
|
+
legendActive?: boolean | null
|
|
112
167
|
): void {
|
|
113
168
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
114
169
|
|
|
@@ -116,18 +171,22 @@ export function renderClassDiagram(
|
|
|
116
171
|
const height = exportDims?.height ?? container.clientHeight;
|
|
117
172
|
if (width <= 0 || height <= 0) return;
|
|
118
173
|
|
|
174
|
+
const legendEntries = collectClassTypes(parsed);
|
|
175
|
+
const hasLegend = legendEntries.length > 1; // only show when multiple types present
|
|
176
|
+
|
|
119
177
|
const titleHeight = parsed.title ? 40 : 0;
|
|
178
|
+
const LEGEND_FIXED_GAP = 8;
|
|
179
|
+
const legendReserve = hasLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
120
180
|
const diagramW = layout.width;
|
|
121
181
|
const diagramH = layout.height;
|
|
122
|
-
const availH = height - titleHeight;
|
|
182
|
+
const availH = height - titleHeight - legendReserve;
|
|
123
183
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
124
184
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
125
185
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
126
186
|
|
|
127
187
|
const scaledW = diagramW * scale;
|
|
128
|
-
const scaledH = diagramH * scale;
|
|
129
188
|
const offsetX = (width - scaledW) / 2;
|
|
130
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
189
|
+
const offsetY = titleHeight + legendReserve + DIAGRAM_PADDING;
|
|
131
190
|
|
|
132
191
|
const svg = d3Selection
|
|
133
192
|
.select(container)
|
|
@@ -246,6 +305,114 @@ export function renderClassDiagram(
|
|
|
246
305
|
}
|
|
247
306
|
}
|
|
248
307
|
|
|
308
|
+
// āā Legend āā
|
|
309
|
+
// legendActive: true = expanded (default), false = collapsed pill only
|
|
310
|
+
const isLegendExpanded = legendActive !== false;
|
|
311
|
+
if (hasLegend) {
|
|
312
|
+
const groupBg = isDark
|
|
313
|
+
? mix(palette.surface, palette.bg, 50)
|
|
314
|
+
: mix(palette.surface, palette.bg, 30);
|
|
315
|
+
|
|
316
|
+
const pillWidth = LEGEND_GROUP_NAME.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
317
|
+
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
318
|
+
const entriesW = legendEntriesWidth(legendEntries);
|
|
319
|
+
|
|
320
|
+
const totalW = isLegendExpanded
|
|
321
|
+
? LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesW
|
|
322
|
+
: pillWidth;
|
|
323
|
+
|
|
324
|
+
const legendX = (width - totalW) / 2;
|
|
325
|
+
const legendY = titleHeight;
|
|
326
|
+
|
|
327
|
+
const legendG = svg
|
|
328
|
+
.append('g')
|
|
329
|
+
.attr('class', 'cd-legend')
|
|
330
|
+
.attr('data-legend-group', 'type')
|
|
331
|
+
.attr('transform', `translate(${legendX}, ${legendY})`)
|
|
332
|
+
.style('cursor', 'pointer');
|
|
333
|
+
|
|
334
|
+
if (isLegendExpanded) {
|
|
335
|
+
// Outer capsule
|
|
336
|
+
legendG.append('rect')
|
|
337
|
+
.attr('width', totalW)
|
|
338
|
+
.attr('height', LEGEND_HEIGHT)
|
|
339
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
340
|
+
.attr('fill', groupBg);
|
|
341
|
+
|
|
342
|
+
// Inner pill
|
|
343
|
+
legendG.append('rect')
|
|
344
|
+
.attr('x', LEGEND_CAPSULE_PAD)
|
|
345
|
+
.attr('y', LEGEND_CAPSULE_PAD)
|
|
346
|
+
.attr('width', pillWidth)
|
|
347
|
+
.attr('height', pillH)
|
|
348
|
+
.attr('rx', pillH / 2)
|
|
349
|
+
.attr('fill', palette.bg);
|
|
350
|
+
|
|
351
|
+
legendG.append('rect')
|
|
352
|
+
.attr('x', LEGEND_CAPSULE_PAD)
|
|
353
|
+
.attr('y', LEGEND_CAPSULE_PAD)
|
|
354
|
+
.attr('width', pillWidth)
|
|
355
|
+
.attr('height', pillH)
|
|
356
|
+
.attr('rx', pillH / 2)
|
|
357
|
+
.attr('fill', 'none')
|
|
358
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
359
|
+
.attr('stroke-width', 0.75);
|
|
360
|
+
|
|
361
|
+
legendG.append('text')
|
|
362
|
+
.attr('x', LEGEND_CAPSULE_PAD + pillWidth / 2)
|
|
363
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
364
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
365
|
+
.attr('font-weight', '500')
|
|
366
|
+
.attr('fill', palette.text)
|
|
367
|
+
.attr('text-anchor', 'middle')
|
|
368
|
+
.attr('font-family', FONT_FAMILY)
|
|
369
|
+
.text(LEGEND_GROUP_NAME);
|
|
370
|
+
|
|
371
|
+
// Entries
|
|
372
|
+
let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
|
|
373
|
+
for (const entry of legendEntries) {
|
|
374
|
+
const color = palette.colors[entry.colorKey];
|
|
375
|
+
const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry)!;
|
|
376
|
+
|
|
377
|
+
const entryG = legendG.append('g')
|
|
378
|
+
.attr('data-legend-entry', typeKey);
|
|
379
|
+
|
|
380
|
+
entryG.append('circle')
|
|
381
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
382
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
383
|
+
.attr('r', LEGEND_DOT_R)
|
|
384
|
+
.attr('fill', color);
|
|
385
|
+
|
|
386
|
+
entryG.append('text')
|
|
387
|
+
.attr('x', entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP)
|
|
388
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
389
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
390
|
+
.attr('fill', palette.textMuted)
|
|
391
|
+
.attr('font-family', FONT_FAMILY)
|
|
392
|
+
.text(entry.label);
|
|
393
|
+
|
|
394
|
+
entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// Collapsed: single muted pill
|
|
398
|
+
legendG.append('rect')
|
|
399
|
+
.attr('width', pillWidth)
|
|
400
|
+
.attr('height', LEGEND_HEIGHT)
|
|
401
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
402
|
+
.attr('fill', groupBg);
|
|
403
|
+
|
|
404
|
+
legendG.append('text')
|
|
405
|
+
.attr('x', pillWidth / 2)
|
|
406
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
407
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
408
|
+
.attr('font-weight', '500')
|
|
409
|
+
.attr('fill', palette.textMuted)
|
|
410
|
+
.attr('text-anchor', 'middle')
|
|
411
|
+
.attr('font-family', FONT_FAMILY)
|
|
412
|
+
.text(LEGEND_GROUP_NAME);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
249
416
|
// āā Content group āā
|
|
250
417
|
const contentG = svg
|
|
251
418
|
.append('g')
|
|
@@ -322,7 +489,8 @@ export function renderClassDiagram(
|
|
|
322
489
|
.attr('transform', `translate(${node.x}, ${node.y})`)
|
|
323
490
|
.attr('class', 'cd-class')
|
|
324
491
|
.attr('data-line-number', String(node.lineNumber))
|
|
325
|
-
.attr('data-node-id', node.id)
|
|
492
|
+
.attr('data-node-id', node.id)
|
|
493
|
+
.attr('data-cd-type', classTypeKey(node.modifier));
|
|
326
494
|
|
|
327
495
|
if (onClickItem) {
|
|
328
496
|
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
@@ -333,8 +501,11 @@ export function renderClassDiagram(
|
|
|
333
501
|
const w = node.width;
|
|
334
502
|
const h = node.height;
|
|
335
503
|
const colorOff = parsed.options?.color === 'off';
|
|
336
|
-
|
|
337
|
-
const
|
|
504
|
+
// When legend is collapsed, use neutral color for nodes without explicit color
|
|
505
|
+
const neutralize = hasLegend && !isLegendExpanded && !node.color;
|
|
506
|
+
const effectiveColor = neutralize ? palette.primary : node.color;
|
|
507
|
+
const fill = nodeFill(palette, isDark, node.modifier, effectiveColor, colorOff);
|
|
508
|
+
const stroke = nodeStroke(palette, node.modifier, effectiveColor, colorOff);
|
|
338
509
|
|
|
339
510
|
// Outer rectangle
|
|
340
511
|
nodeG.append('rect')
|
|
@@ -506,16 +677,13 @@ export function renderClassDiagramForExport(
|
|
|
506
677
|
const layout = layoutClassDiagram(parsed);
|
|
507
678
|
const isDark = theme === 'dark';
|
|
508
679
|
|
|
509
|
-
const
|
|
680
|
+
const legendEntries = collectClassTypes(parsed);
|
|
681
|
+
const EXPORT_LEGEND_GAP = 8;
|
|
682
|
+
const legendReserve = legendEntries.length > 1 ? LEGEND_HEIGHT + EXPORT_LEGEND_GAP : 0;
|
|
510
683
|
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
511
|
-
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
|
|
512
|
-
|
|
513
|
-
container
|
|
514
|
-
container.style.position = 'absolute';
|
|
515
|
-
container.style.left = '-9999px';
|
|
516
|
-
document.body.appendChild(container);
|
|
517
|
-
|
|
518
|
-
try {
|
|
684
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0) + legendReserve;
|
|
685
|
+
|
|
686
|
+
return runInExportContainer(exportWidth, exportHeight, (container) => {
|
|
519
687
|
renderClassDiagram(
|
|
520
688
|
container,
|
|
521
689
|
parsed,
|
|
@@ -525,19 +693,6 @@ export function renderClassDiagramForExport(
|
|
|
525
693
|
undefined,
|
|
526
694
|
{ width: exportWidth, height: exportHeight }
|
|
527
695
|
);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (!svgEl) return '';
|
|
531
|
-
|
|
532
|
-
if (theme === 'transparent') {
|
|
533
|
-
svgEl.style.background = 'none';
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
537
|
-
svgEl.style.fontFamily = FONT_FAMILY;
|
|
538
|
-
|
|
539
|
-
return svgEl.outerHTML;
|
|
540
|
-
} finally {
|
|
541
|
-
document.body.removeChild(container);
|
|
542
|
-
}
|
|
696
|
+
return extractExportSvg(container, theme);
|
|
697
|
+
});
|
|
543
698
|
}
|