@diagrammo/dgmo 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +197 -154
- package/dist/index.cjs +8647 -3447
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -58
- package/dist/index.d.ts +503 -58
- package/dist/index.js +8379 -3200
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +578 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1553 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sharing.ts +8 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Sitemap Diagram SVG Renderer
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import * as d3Selection from 'd3-selection';
|
|
6
|
+
import * as d3Shape from 'd3-shape';
|
|
7
|
+
import { FONT_FAMILY } from '../fonts';
|
|
8
|
+
import type { PaletteColors } from '../palettes';
|
|
9
|
+
import { mix } from '../palettes/color-utils';
|
|
10
|
+
import type { ParsedSitemap } from './types';
|
|
11
|
+
import type {
|
|
12
|
+
SitemapLayoutResult,
|
|
13
|
+
SitemapLayoutNode,
|
|
14
|
+
SitemapLayoutEdge,
|
|
15
|
+
SitemapContainerBounds,
|
|
16
|
+
SitemapLegendGroup,
|
|
17
|
+
} from './layout';
|
|
18
|
+
|
|
19
|
+
// ============================================================
|
|
20
|
+
// Constants
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
const DIAGRAM_PADDING = 20;
|
|
24
|
+
const MAX_SCALE = 3;
|
|
25
|
+
const TITLE_HEIGHT = 30;
|
|
26
|
+
const TITLE_FONT_SIZE = 18;
|
|
27
|
+
const LABEL_FONT_SIZE = 13;
|
|
28
|
+
const META_FONT_SIZE = 11;
|
|
29
|
+
const META_LINE_HEIGHT = 16;
|
|
30
|
+
const HEADER_HEIGHT = 28;
|
|
31
|
+
const SEPARATOR_GAP = 6;
|
|
32
|
+
const EDGE_STROKE_WIDTH = 1.5;
|
|
33
|
+
const NODE_STROKE_WIDTH = 1.5;
|
|
34
|
+
const CARD_RADIUS = 6;
|
|
35
|
+
const CONTAINER_RADIUS = 8;
|
|
36
|
+
const CONTAINER_LABEL_FONT_SIZE = 13;
|
|
37
|
+
const CONTAINER_META_FONT_SIZE = 11;
|
|
38
|
+
const CONTAINER_META_LINE_HEIGHT = 16;
|
|
39
|
+
const CONTAINER_HEADER_HEIGHT = 28;
|
|
40
|
+
const ARROWHEAD_W = 10;
|
|
41
|
+
const ARROWHEAD_H = 7;
|
|
42
|
+
const EDGE_LABEL_FONT_SIZE = 11;
|
|
43
|
+
|
|
44
|
+
// Collapsed-node accent bar
|
|
45
|
+
const COLLAPSE_BAR_HEIGHT = 6;
|
|
46
|
+
|
|
47
|
+
// Legend
|
|
48
|
+
const LEGEND_HEIGHT = 28;
|
|
49
|
+
const LEGEND_FIXED_GAP = 8;
|
|
50
|
+
const LEGEND_PILL_PAD = 16;
|
|
51
|
+
const LEGEND_PILL_FONT_SIZE = 11;
|
|
52
|
+
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
53
|
+
const LEGEND_CAPSULE_PAD = 4;
|
|
54
|
+
const LEGEND_DOT_R = 4;
|
|
55
|
+
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
56
|
+
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
57
|
+
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
58
|
+
const LEGEND_ENTRY_TRAIL = 8;
|
|
59
|
+
const LEGEND_GROUP_GAP = 12;
|
|
60
|
+
const LEGEND_EYE_SIZE = 14;
|
|
61
|
+
const LEGEND_EYE_GAP = 6;
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Color helpers
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
function nodeFill(palette: PaletteColors, isDark: boolean, nodeColor?: string): string {
|
|
68
|
+
const color = nodeColor ?? palette.primary;
|
|
69
|
+
return mix(color, isDark ? palette.surface : palette.bg, 25);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nodeStroke(_palette: PaletteColors, nodeColor?: string): string {
|
|
73
|
+
return nodeColor ?? _palette.primary;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function containerFill(palette: PaletteColors, isDark: boolean, nodeColor?: string): string {
|
|
77
|
+
if (nodeColor) {
|
|
78
|
+
return mix(nodeColor, isDark ? palette.surface : palette.bg, 10);
|
|
79
|
+
}
|
|
80
|
+
return mix(palette.surface, palette.bg, 40);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function containerStroke(palette: PaletteColors, nodeColor?: string): string {
|
|
84
|
+
return nodeColor ?? palette.textMuted;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Curve generator
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
const lineGenerator = d3Shape.line<{ x: number; y: number }>()
|
|
92
|
+
.x((d) => d.x)
|
|
93
|
+
.y((d) => d.y)
|
|
94
|
+
.curve(d3Shape.curveBasis);
|
|
95
|
+
|
|
96
|
+
// ============================================================
|
|
97
|
+
// Main Renderer
|
|
98
|
+
// ============================================================
|
|
99
|
+
|
|
100
|
+
type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
101
|
+
|
|
102
|
+
export function renderSitemap(
|
|
103
|
+
container: HTMLDivElement,
|
|
104
|
+
parsed: ParsedSitemap,
|
|
105
|
+
layout: SitemapLayoutResult,
|
|
106
|
+
palette: PaletteColors,
|
|
107
|
+
isDark: boolean,
|
|
108
|
+
onClickItem?: (lineNumber: number) => void,
|
|
109
|
+
exportDims?: { width?: number; height?: number },
|
|
110
|
+
activeTagGroup?: string | null,
|
|
111
|
+
hiddenAttributes?: Set<string>,
|
|
112
|
+
): void {
|
|
113
|
+
// Clear existing content
|
|
114
|
+
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
115
|
+
|
|
116
|
+
const width = exportDims?.width ?? container.clientWidth;
|
|
117
|
+
const height = exportDims?.height ?? container.clientHeight;
|
|
118
|
+
if (width <= 0 || height <= 0) return;
|
|
119
|
+
|
|
120
|
+
const hasLegend = layout.legend.length > 0;
|
|
121
|
+
|
|
122
|
+
// In app mode (not export), render the title and legend at fixed size
|
|
123
|
+
// outside the scaled group so they stay legible on large sitemaps.
|
|
124
|
+
// Layout order: Title → Legend → Diagram content.
|
|
125
|
+
const layoutLegendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP; // 40px — what layout added
|
|
126
|
+
const fixedLegend = !exportDims && hasLegend;
|
|
127
|
+
const fixedTitle = fixedLegend && !!parsed.title;
|
|
128
|
+
const fixedTitleH = fixedTitle ? TITLE_HEIGHT : 0;
|
|
129
|
+
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
130
|
+
// Total fixed pixel space above the scaled content
|
|
131
|
+
const fixedReserve = fixedTitleH + legendReserveH;
|
|
132
|
+
// Title inside scaled group only when legend is NOT fixed
|
|
133
|
+
const titleOffset = !fixedTitle && parsed.title ? TITLE_HEIGHT : 0;
|
|
134
|
+
|
|
135
|
+
// Compute scale to fit diagram in viewport
|
|
136
|
+
const diagramW = layout.width;
|
|
137
|
+
let diagramH = layout.height + titleOffset;
|
|
138
|
+
if (fixedLegend) {
|
|
139
|
+
// Remove the legend space from diagram height — legend is rendered separately
|
|
140
|
+
diagramH -= layoutLegendShift;
|
|
141
|
+
}
|
|
142
|
+
const availH = height - DIAGRAM_PADDING * 2 - fixedReserve;
|
|
143
|
+
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
144
|
+
const scaleY = availH / diagramH;
|
|
145
|
+
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
146
|
+
|
|
147
|
+
const scaledW = diagramW * scale;
|
|
148
|
+
const offsetX = (width - scaledW) / 2;
|
|
149
|
+
const offsetY = DIAGRAM_PADDING + fixedReserve;
|
|
150
|
+
|
|
151
|
+
// Create SVG
|
|
152
|
+
const svg = d3Selection
|
|
153
|
+
.select(container)
|
|
154
|
+
.append('svg')
|
|
155
|
+
.attr('width', width)
|
|
156
|
+
.attr('height', height)
|
|
157
|
+
.style('font-family', FONT_FAMILY);
|
|
158
|
+
|
|
159
|
+
// Defs: arrowhead markers
|
|
160
|
+
const defs = svg.append('defs');
|
|
161
|
+
|
|
162
|
+
// Default arrowhead
|
|
163
|
+
defs
|
|
164
|
+
.append('marker')
|
|
165
|
+
.attr('id', 'sm-arrow')
|
|
166
|
+
.attr('viewBox', `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`)
|
|
167
|
+
.attr('refX', ARROWHEAD_W)
|
|
168
|
+
.attr('refY', ARROWHEAD_H / 2)
|
|
169
|
+
.attr('markerWidth', ARROWHEAD_W)
|
|
170
|
+
.attr('markerHeight', ARROWHEAD_H)
|
|
171
|
+
.attr('orient', 'auto')
|
|
172
|
+
.append('polygon')
|
|
173
|
+
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
174
|
+
.attr('fill', palette.textMuted);
|
|
175
|
+
|
|
176
|
+
// Colored arrowheads
|
|
177
|
+
const edgeColors = new Set<string>();
|
|
178
|
+
for (const edge of layout.edges) {
|
|
179
|
+
if (edge.color) edgeColors.add(edge.color);
|
|
180
|
+
}
|
|
181
|
+
for (const color of edgeColors) {
|
|
182
|
+
const id = `sm-arrow-${color.replace('#', '')}`;
|
|
183
|
+
defs
|
|
184
|
+
.append('marker')
|
|
185
|
+
.attr('id', id)
|
|
186
|
+
.attr('viewBox', `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`)
|
|
187
|
+
.attr('refX', ARROWHEAD_W)
|
|
188
|
+
.attr('refY', ARROWHEAD_H / 2)
|
|
189
|
+
.attr('markerWidth', ARROWHEAD_W)
|
|
190
|
+
.attr('markerHeight', ARROWHEAD_H)
|
|
191
|
+
.attr('orient', 'auto')
|
|
192
|
+
.append('polygon')
|
|
193
|
+
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
194
|
+
.attr('fill', color);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Main content group with scale/translate
|
|
198
|
+
const mainG = svg
|
|
199
|
+
.append('g')
|
|
200
|
+
.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
|
201
|
+
|
|
202
|
+
// Title (scaled, only when legend is NOT fixed)
|
|
203
|
+
if (!fixedTitle && parsed.title) {
|
|
204
|
+
const titleEl = mainG
|
|
205
|
+
.append('text')
|
|
206
|
+
.attr('x', diagramW / 2)
|
|
207
|
+
.attr('y', TITLE_FONT_SIZE)
|
|
208
|
+
.attr('text-anchor', 'middle')
|
|
209
|
+
.attr('fill', palette.text)
|
|
210
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
211
|
+
.attr('font-weight', 'bold')
|
|
212
|
+
.attr('class', 'sitemap-title chart-title');
|
|
213
|
+
|
|
214
|
+
if (parsed.titleLineNumber) {
|
|
215
|
+
titleEl.attr('data-line-number', parsed.titleLineNumber);
|
|
216
|
+
if (onClickItem) {
|
|
217
|
+
titleEl
|
|
218
|
+
.style('cursor', 'pointer')
|
|
219
|
+
.on('click', () => onClickItem(parsed.titleLineNumber!));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
titleEl.text(parsed.title);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Content group (offset by title; pull up by legendShift when legend is fixed)
|
|
227
|
+
const contentShift = fixedLegend ? -layoutLegendShift : 0;
|
|
228
|
+
const contentG = mainG
|
|
229
|
+
.append('g')
|
|
230
|
+
.attr('transform', `translate(0, ${titleOffset + contentShift})`);
|
|
231
|
+
|
|
232
|
+
// Build display name map + tag color lookup from tag groups
|
|
233
|
+
const displayNames = new Map<string, string>();
|
|
234
|
+
// tagColors: "groupkey:valueLower" → hex color
|
|
235
|
+
const tagColors = new Map<string, string>();
|
|
236
|
+
for (const group of parsed.tagGroups) {
|
|
237
|
+
displayNames.set(group.name.toLowerCase(), group.name);
|
|
238
|
+
for (const entry of group.entries) {
|
|
239
|
+
tagColors.set(`${group.name.toLowerCase()}:${entry.value.toLowerCase()}`, entry.color);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- Render containers (bottom layer) ---
|
|
244
|
+
for (const c of layout.containers) {
|
|
245
|
+
const cG = contentG
|
|
246
|
+
.append('g')
|
|
247
|
+
.attr('transform', `translate(${c.x}, ${c.y})`)
|
|
248
|
+
.attr('class', 'sitemap-container')
|
|
249
|
+
.attr('data-line-number', String(c.lineNumber)) as GSelection;
|
|
250
|
+
|
|
251
|
+
if (c.hasChildren) {
|
|
252
|
+
cG.attr('data-node-toggle', c.nodeId)
|
|
253
|
+
.attr('tabindex', '0')
|
|
254
|
+
.attr('role', 'button')
|
|
255
|
+
.attr('aria-expanded', String(!c.hiddenCount))
|
|
256
|
+
.attr('aria-label', c.label);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (onClickItem) {
|
|
260
|
+
cG.style('cursor', 'pointer').on('click', () => onClickItem(c.lineNumber));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Tag metadata for legend hover dimming
|
|
264
|
+
if (activeTagGroup) {
|
|
265
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
266
|
+
const tagVal = c.tagMetadata[tagKey];
|
|
267
|
+
if (tagVal) cG.attr(`data-tag-${tagKey}`, tagVal.toLowerCase());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const fill = containerFill(palette, isDark, c.color);
|
|
271
|
+
const stroke = containerStroke(palette, c.color);
|
|
272
|
+
|
|
273
|
+
cG.append('rect')
|
|
274
|
+
.attr('x', 0)
|
|
275
|
+
.attr('y', 0)
|
|
276
|
+
.attr('width', c.width)
|
|
277
|
+
.attr('height', c.height)
|
|
278
|
+
.attr('rx', CONTAINER_RADIUS)
|
|
279
|
+
.attr('fill', fill)
|
|
280
|
+
.attr('stroke', stroke)
|
|
281
|
+
.attr('stroke-opacity', 0.35)
|
|
282
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
283
|
+
|
|
284
|
+
// Container label
|
|
285
|
+
cG.append('text')
|
|
286
|
+
.attr('x', c.width / 2)
|
|
287
|
+
.attr('y', CONTAINER_HEADER_HEIGHT / 2 + CONTAINER_LABEL_FONT_SIZE / 2 - 2)
|
|
288
|
+
.attr('text-anchor', 'middle')
|
|
289
|
+
.attr('fill', palette.text)
|
|
290
|
+
.attr('font-size', CONTAINER_LABEL_FONT_SIZE)
|
|
291
|
+
.attr('font-weight', 'bold')
|
|
292
|
+
.text(c.label);
|
|
293
|
+
|
|
294
|
+
// Container metadata
|
|
295
|
+
const metaEntries = Object.entries(c.metadata);
|
|
296
|
+
if (metaEntries.length > 0) {
|
|
297
|
+
const metaDisplayKeys = metaEntries.map(([k]) => displayNames.get(k) ?? k);
|
|
298
|
+
const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
|
|
299
|
+
const valueX = 10 + (maxKeyLen + 2) * (CONTAINER_META_FONT_SIZE * 0.6);
|
|
300
|
+
const metaStartY = CONTAINER_HEADER_HEIGHT + CONTAINER_META_FONT_SIZE - 2;
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < metaEntries.length; i++) {
|
|
303
|
+
const [key, value] = metaEntries[i];
|
|
304
|
+
const displayKey = metaDisplayKeys[i];
|
|
305
|
+
const rowY = metaStartY + i * CONTAINER_META_LINE_HEIGHT;
|
|
306
|
+
const valColor = tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
|
|
307
|
+
|
|
308
|
+
cG.append('text')
|
|
309
|
+
.attr('x', 10)
|
|
310
|
+
.attr('y', rowY)
|
|
311
|
+
.attr('fill', palette.textMuted)
|
|
312
|
+
.attr('font-size', CONTAINER_META_FONT_SIZE)
|
|
313
|
+
.text(`${displayKey}: `);
|
|
314
|
+
|
|
315
|
+
cG.append('text')
|
|
316
|
+
.attr('x', valueX)
|
|
317
|
+
.attr('y', rowY)
|
|
318
|
+
.attr('fill', valColor)
|
|
319
|
+
.attr('font-size', CONTAINER_META_FONT_SIZE)
|
|
320
|
+
.text(value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Collapsed accent bar
|
|
325
|
+
if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
|
|
326
|
+
const clipId = `clip-${c.nodeId}`;
|
|
327
|
+
cG.append('clipPath').attr('id', clipId)
|
|
328
|
+
.append('rect')
|
|
329
|
+
.attr('width', c.width).attr('height', c.height)
|
|
330
|
+
.attr('rx', CONTAINER_RADIUS);
|
|
331
|
+
cG.append('rect')
|
|
332
|
+
.attr('y', c.height - COLLAPSE_BAR_HEIGHT)
|
|
333
|
+
.attr('width', c.width)
|
|
334
|
+
.attr('height', COLLAPSE_BAR_HEIGHT)
|
|
335
|
+
.attr('fill', c.color ?? palette.primary)
|
|
336
|
+
.attr('opacity', 0.5)
|
|
337
|
+
.attr('clip-path', `url(#${clipId})`);
|
|
338
|
+
|
|
339
|
+
cG.append('text')
|
|
340
|
+
.attr('x', c.width / 2)
|
|
341
|
+
.attr('y', c.height - COLLAPSE_BAR_HEIGHT - 6)
|
|
342
|
+
.attr('text-anchor', 'middle')
|
|
343
|
+
.attr('fill', palette.textMuted)
|
|
344
|
+
.attr('font-size', META_FONT_SIZE)
|
|
345
|
+
.text(`+${c.hiddenCount}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- Render edges (middle layer) ---
|
|
350
|
+
for (const edge of layout.edges) {
|
|
351
|
+
if (edge.points.length < 2) continue;
|
|
352
|
+
|
|
353
|
+
const edgeG = contentG
|
|
354
|
+
.append('g')
|
|
355
|
+
.attr('class', 'sitemap-edge-group')
|
|
356
|
+
.attr('data-line-number', String(edge.lineNumber));
|
|
357
|
+
|
|
358
|
+
const edgeColor = edge.color ?? palette.textMuted;
|
|
359
|
+
const markerId = edge.color
|
|
360
|
+
? `sm-arrow-${edge.color.replace('#', '')}`
|
|
361
|
+
: 'sm-arrow';
|
|
362
|
+
|
|
363
|
+
const pathD = lineGenerator(edge.points);
|
|
364
|
+
if (pathD) {
|
|
365
|
+
edgeG
|
|
366
|
+
.append('path')
|
|
367
|
+
.attr('d', pathD)
|
|
368
|
+
.attr('fill', 'none')
|
|
369
|
+
.attr('stroke', edgeColor)
|
|
370
|
+
.attr('stroke-width', EDGE_STROKE_WIDTH)
|
|
371
|
+
.attr('marker-end', `url(#${markerId})`)
|
|
372
|
+
.attr('class', 'sitemap-edge');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Edge label with background badge
|
|
376
|
+
if (edge.label && edge.points.length >= 2) {
|
|
377
|
+
const mid = edge.points[Math.floor(edge.points.length / 2)];
|
|
378
|
+
const labelW = edge.label.length * EDGE_LABEL_FONT_SIZE * 0.6 + 10;
|
|
379
|
+
const labelH = EDGE_LABEL_FONT_SIZE + 6;
|
|
380
|
+
|
|
381
|
+
edgeG
|
|
382
|
+
.append('rect')
|
|
383
|
+
.attr('x', mid.x - labelW / 2)
|
|
384
|
+
.attr('y', mid.y - labelH / 2 - 1)
|
|
385
|
+
.attr('width', labelW)
|
|
386
|
+
.attr('height', labelH)
|
|
387
|
+
.attr('rx', 3)
|
|
388
|
+
.attr('fill', palette.bg)
|
|
389
|
+
.attr('opacity', 0.85)
|
|
390
|
+
.attr('class', 'sitemap-edge-label-bg');
|
|
391
|
+
|
|
392
|
+
edgeG
|
|
393
|
+
.append('text')
|
|
394
|
+
.attr('x', mid.x)
|
|
395
|
+
.attr('y', mid.y + 4)
|
|
396
|
+
.attr('text-anchor', 'middle')
|
|
397
|
+
.attr('fill', edgeColor)
|
|
398
|
+
.attr('font-size', EDGE_LABEL_FONT_SIZE)
|
|
399
|
+
.attr('class', 'sitemap-edge-label')
|
|
400
|
+
.text(edge.label);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- Render page cards (top layer) ---
|
|
405
|
+
for (const node of layout.nodes) {
|
|
406
|
+
const nodeG = contentG
|
|
407
|
+
.append('g')
|
|
408
|
+
.attr('transform', `translate(${node.x - node.width / 2}, ${node.y})`)
|
|
409
|
+
.attr('class', 'sitemap-node')
|
|
410
|
+
.attr('data-line-number', String(node.lineNumber)) as GSelection;
|
|
411
|
+
|
|
412
|
+
if (node.hasChildren) {
|
|
413
|
+
nodeG.attr('data-node-toggle', node.id)
|
|
414
|
+
.attr('tabindex', '0')
|
|
415
|
+
.attr('role', 'button')
|
|
416
|
+
.attr('aria-expanded', String(!node.hiddenCount));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (onClickItem) {
|
|
420
|
+
nodeG.style('cursor', 'pointer').on('click', () => onClickItem(node.lineNumber));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Tag metadata for legend hover dimming
|
|
424
|
+
if (activeTagGroup) {
|
|
425
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
426
|
+
const tagVal = node.tagMetadata[tagKey];
|
|
427
|
+
if (tagVal) nodeG.attr(`data-tag-${tagKey}`, tagVal.toLowerCase());
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const fill = nodeFill(palette, isDark, node.color);
|
|
431
|
+
const stroke = nodeStroke(palette, node.color);
|
|
432
|
+
|
|
433
|
+
// Card background
|
|
434
|
+
nodeG.append('rect')
|
|
435
|
+
.attr('x', 0)
|
|
436
|
+
.attr('y', 0)
|
|
437
|
+
.attr('width', node.width)
|
|
438
|
+
.attr('height', node.height)
|
|
439
|
+
.attr('rx', CARD_RADIUS)
|
|
440
|
+
.attr('fill', fill)
|
|
441
|
+
.attr('stroke', stroke)
|
|
442
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
443
|
+
|
|
444
|
+
// Label
|
|
445
|
+
nodeG.append('text')
|
|
446
|
+
.attr('x', node.width / 2)
|
|
447
|
+
.attr('y', HEADER_HEIGHT / 2 + LABEL_FONT_SIZE / 2 - 2)
|
|
448
|
+
.attr('text-anchor', 'middle')
|
|
449
|
+
.attr('fill', palette.text)
|
|
450
|
+
.attr('font-size', LABEL_FONT_SIZE)
|
|
451
|
+
.attr('font-weight', 'bold')
|
|
452
|
+
.text(node.label);
|
|
453
|
+
|
|
454
|
+
// Separator and metadata
|
|
455
|
+
const metaEntries = Object.entries(node.metadata);
|
|
456
|
+
if (metaEntries.length > 0) {
|
|
457
|
+
// Separator line
|
|
458
|
+
nodeG.append('line')
|
|
459
|
+
.attr('x1', 0)
|
|
460
|
+
.attr('y1', HEADER_HEIGHT)
|
|
461
|
+
.attr('x2', node.width)
|
|
462
|
+
.attr('y2', HEADER_HEIGHT)
|
|
463
|
+
.attr('stroke', stroke)
|
|
464
|
+
.attr('stroke-opacity', 0.3);
|
|
465
|
+
|
|
466
|
+
const metaDisplayKeys = metaEntries.map(([k]) => displayNames.get(k) ?? k);
|
|
467
|
+
const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
|
|
468
|
+
const valueX = 10 + (maxKeyLen + 2) * (META_FONT_SIZE * 0.6);
|
|
469
|
+
|
|
470
|
+
for (let i = 0; i < metaEntries.length; i++) {
|
|
471
|
+
const [key, value] = metaEntries[i];
|
|
472
|
+
const displayKey = metaDisplayKeys[i];
|
|
473
|
+
const rowY = HEADER_HEIGHT + SEPARATOR_GAP + (i + 1) * META_LINE_HEIGHT - 4;
|
|
474
|
+
const valColor = tagColors.get(`${key}:${value.toLowerCase()}`) ?? palette.text;
|
|
475
|
+
|
|
476
|
+
nodeG.append('text')
|
|
477
|
+
.attr('x', 10)
|
|
478
|
+
.attr('y', rowY)
|
|
479
|
+
.attr('fill', palette.textMuted)
|
|
480
|
+
.attr('font-size', META_FONT_SIZE)
|
|
481
|
+
.text(`${displayKey}:`);
|
|
482
|
+
|
|
483
|
+
nodeG.append('text')
|
|
484
|
+
.attr('x', valueX)
|
|
485
|
+
.attr('y', rowY)
|
|
486
|
+
.attr('fill', valColor)
|
|
487
|
+
.attr('font-size', META_FONT_SIZE)
|
|
488
|
+
.text(value);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Collapsed accent bar
|
|
493
|
+
if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
|
|
494
|
+
const clipId = `clip-${node.id}`;
|
|
495
|
+
nodeG.append('clipPath').attr('id', clipId)
|
|
496
|
+
.append('rect')
|
|
497
|
+
.attr('width', node.width).attr('height', node.height)
|
|
498
|
+
.attr('rx', CARD_RADIUS);
|
|
499
|
+
nodeG.append('rect')
|
|
500
|
+
.attr('y', node.height - COLLAPSE_BAR_HEIGHT)
|
|
501
|
+
.attr('width', node.width)
|
|
502
|
+
.attr('height', COLLAPSE_BAR_HEIGHT)
|
|
503
|
+
.attr('fill', node.color ?? palette.primary)
|
|
504
|
+
.attr('opacity', 0.5)
|
|
505
|
+
.attr('clip-path', `url(#${clipId})`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// --- Render legend ---
|
|
510
|
+
if (exportDims && hasLegend) {
|
|
511
|
+
// Export mode: render inside the scaled content group
|
|
512
|
+
renderLegend(contentG, layout.legend, palette, isDark, activeTagGroup, undefined, hiddenAttributes);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// --- Fixed title + legend (appended AFTER mainG so they paint on top
|
|
516
|
+
// and receive pointer events without being blocked by scaled content) ---
|
|
517
|
+
if (fixedTitle) {
|
|
518
|
+
const titleEl = svg
|
|
519
|
+
.append('text')
|
|
520
|
+
.attr('x', width / 2)
|
|
521
|
+
.attr('y', DIAGRAM_PADDING + TITLE_FONT_SIZE)
|
|
522
|
+
.attr('text-anchor', 'middle')
|
|
523
|
+
.attr('fill', palette.text)
|
|
524
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
525
|
+
.attr('font-weight', 'bold')
|
|
526
|
+
.attr('class', 'sitemap-title chart-title')
|
|
527
|
+
.style('font-family', FONT_FAMILY);
|
|
528
|
+
|
|
529
|
+
if (parsed.titleLineNumber) {
|
|
530
|
+
titleEl.attr('data-line-number', parsed.titleLineNumber);
|
|
531
|
+
if (onClickItem) {
|
|
532
|
+
titleEl
|
|
533
|
+
.style('cursor', 'pointer')
|
|
534
|
+
.on('click', () => onClickItem(parsed.titleLineNumber!));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
titleEl.text(parsed.title!);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (fixedLegend) {
|
|
542
|
+
const legendParent = svg
|
|
543
|
+
.append('g')
|
|
544
|
+
.attr('class', 'sitemap-legend-fixed')
|
|
545
|
+
.attr('transform', `translate(0, ${DIAGRAM_PADDING + fixedTitleH})`);
|
|
546
|
+
renderLegend(legendParent, layout.legend, palette, isDark, activeTagGroup, width, hiddenAttributes);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ============================================================
|
|
551
|
+
// Legend rendering
|
|
552
|
+
// ============================================================
|
|
553
|
+
|
|
554
|
+
// Eye icon SVG paths (14×14 viewBox)
|
|
555
|
+
const EYE_OPEN_PATH =
|
|
556
|
+
'M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z';
|
|
557
|
+
const EYE_CLOSED_PATH =
|
|
558
|
+
'M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1';
|
|
559
|
+
|
|
560
|
+
function renderLegend(
|
|
561
|
+
parent: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
562
|
+
legendGroups: SitemapLegendGroup[],
|
|
563
|
+
palette: PaletteColors,
|
|
564
|
+
isDark: boolean,
|
|
565
|
+
activeTagGroup?: string | null,
|
|
566
|
+
fixedWidth?: number,
|
|
567
|
+
hiddenAttributes?: Set<string>,
|
|
568
|
+
): void {
|
|
569
|
+
if (legendGroups.length === 0) return;
|
|
570
|
+
|
|
571
|
+
const visibleGroups = activeTagGroup != null
|
|
572
|
+
? legendGroups.filter((g) => g.name.toLowerCase() === activeTagGroup.toLowerCase())
|
|
573
|
+
: legendGroups;
|
|
574
|
+
|
|
575
|
+
const groupBg = isDark
|
|
576
|
+
? mix(palette.surface, palette.bg, 50)
|
|
577
|
+
: mix(palette.surface, palette.bg, 30);
|
|
578
|
+
|
|
579
|
+
// For fixed legend: compute pixel-space positions centered in SVG width
|
|
580
|
+
let fixedPositions: Map<string, number> | undefined;
|
|
581
|
+
if (fixedWidth != null && visibleGroups.length > 0) {
|
|
582
|
+
fixedPositions = new Map();
|
|
583
|
+
const effectiveW = (g: SitemapLegendGroup) =>
|
|
584
|
+
activeTagGroup != null ? g.width : g.minifiedWidth;
|
|
585
|
+
const totalW =
|
|
586
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
587
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
588
|
+
let cx = (fixedWidth - totalW) / 2;
|
|
589
|
+
for (const g of visibleGroups) {
|
|
590
|
+
fixedPositions.set(g.name, cx);
|
|
591
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for (const group of visibleGroups) {
|
|
596
|
+
const isActive = activeTagGroup != null;
|
|
597
|
+
const pillW = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
598
|
+
|
|
599
|
+
const gX = fixedPositions?.get(group.name) ?? group.x;
|
|
600
|
+
const gY = fixedPositions ? 0 : group.y;
|
|
601
|
+
|
|
602
|
+
const legendG = parent
|
|
603
|
+
.append('g')
|
|
604
|
+
.attr('transform', `translate(${gX}, ${gY})`)
|
|
605
|
+
.attr('class', 'sitemap-legend-group')
|
|
606
|
+
.attr('data-legend-group', group.name.toLowerCase())
|
|
607
|
+
.style('cursor', 'pointer');
|
|
608
|
+
|
|
609
|
+
// Outer capsule background (active/expanded only)
|
|
610
|
+
if (isActive) {
|
|
611
|
+
legendG.append('rect')
|
|
612
|
+
.attr('width', group.width)
|
|
613
|
+
.attr('height', LEGEND_HEIGHT)
|
|
614
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
615
|
+
.attr('fill', groupBg);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
619
|
+
const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
620
|
+
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
621
|
+
|
|
622
|
+
// Pill background
|
|
623
|
+
legendG.append('rect')
|
|
624
|
+
.attr('x', pillXOff)
|
|
625
|
+
.attr('y', pillYOff)
|
|
626
|
+
.attr('width', pillW)
|
|
627
|
+
.attr('height', pillH)
|
|
628
|
+
.attr('rx', pillH / 2)
|
|
629
|
+
.attr('fill', isActive ? palette.bg : groupBg);
|
|
630
|
+
|
|
631
|
+
// Active pill border
|
|
632
|
+
if (isActive) {
|
|
633
|
+
legendG.append('rect')
|
|
634
|
+
.attr('x', pillXOff)
|
|
635
|
+
.attr('y', pillYOff)
|
|
636
|
+
.attr('width', pillW)
|
|
637
|
+
.attr('height', pillH)
|
|
638
|
+
.attr('rx', pillH / 2)
|
|
639
|
+
.attr('fill', 'none')
|
|
640
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
641
|
+
.attr('stroke-width', 0.75);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Pill text
|
|
645
|
+
legendG.append('text')
|
|
646
|
+
.attr('x', pillXOff + pillW / 2)
|
|
647
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
648
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
649
|
+
.attr('font-weight', '500')
|
|
650
|
+
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
651
|
+
.attr('text-anchor', 'middle')
|
|
652
|
+
.text(group.name);
|
|
653
|
+
|
|
654
|
+
// Eye icon for visibility toggle (active only, app mode)
|
|
655
|
+
if (isActive && fixedWidth != null) {
|
|
656
|
+
const groupKey = group.name.toLowerCase();
|
|
657
|
+
const isHidden = hiddenAttributes?.has(groupKey) ?? false;
|
|
658
|
+
const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
|
|
659
|
+
const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
|
|
660
|
+
const hitPad = 6;
|
|
661
|
+
|
|
662
|
+
const eyeG = legendG.append('g')
|
|
663
|
+
.attr('class', 'sitemap-legend-eye')
|
|
664
|
+
.attr('data-legend-visibility', groupKey)
|
|
665
|
+
.style('cursor', 'pointer')
|
|
666
|
+
.attr('opacity', isHidden ? 0.4 : 0.7);
|
|
667
|
+
|
|
668
|
+
// Transparent hit area for easier clicking
|
|
669
|
+
eyeG.append('rect')
|
|
670
|
+
.attr('x', eyeX - hitPad)
|
|
671
|
+
.attr('y', eyeY - hitPad)
|
|
672
|
+
.attr('width', LEGEND_EYE_SIZE + hitPad * 2)
|
|
673
|
+
.attr('height', LEGEND_EYE_SIZE + hitPad * 2)
|
|
674
|
+
.attr('fill', 'transparent')
|
|
675
|
+
.attr('pointer-events', 'all');
|
|
676
|
+
|
|
677
|
+
eyeG.append('path')
|
|
678
|
+
.attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
|
|
679
|
+
.attr('transform', `translate(${eyeX}, ${eyeY})`)
|
|
680
|
+
.attr('fill', 'none')
|
|
681
|
+
.attr('stroke', palette.textMuted)
|
|
682
|
+
.attr('stroke-width', 1.2)
|
|
683
|
+
.attr('stroke-linecap', 'round')
|
|
684
|
+
.attr('stroke-linejoin', 'round');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Entries (active/expanded only)
|
|
688
|
+
if (isActive) {
|
|
689
|
+
const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
|
|
690
|
+
let entryX = pillXOff + pillW + 4 + eyeShift;
|
|
691
|
+
for (const entry of group.entries) {
|
|
692
|
+
const entryG = legendG.append('g')
|
|
693
|
+
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
694
|
+
.style('cursor', 'pointer');
|
|
695
|
+
|
|
696
|
+
entryG.append('circle')
|
|
697
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
698
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
699
|
+
.attr('r', LEGEND_DOT_R)
|
|
700
|
+
.attr('fill', entry.color);
|
|
701
|
+
|
|
702
|
+
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
703
|
+
entryG.append('text')
|
|
704
|
+
.attr('x', textX)
|
|
705
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
706
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
707
|
+
.attr('fill', palette.textMuted)
|
|
708
|
+
.text(entry.value);
|
|
709
|
+
|
|
710
|
+
entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ============================================================
|
|
717
|
+
// Export convenience function
|
|
718
|
+
// ============================================================
|
|
719
|
+
|
|
720
|
+
export async function renderSitemapForExport(
|
|
721
|
+
content: string,
|
|
722
|
+
theme: 'light' | 'dark' | 'transparent',
|
|
723
|
+
palette?: PaletteColors,
|
|
724
|
+
): Promise<string> {
|
|
725
|
+
const { parseSitemap } = await import('./parser');
|
|
726
|
+
const { layoutSitemap } = await import('./layout');
|
|
727
|
+
const { getPalette } = await import('../palettes');
|
|
728
|
+
const { injectBranding } = await import('../branding');
|
|
729
|
+
|
|
730
|
+
const isDark = theme === 'dark';
|
|
731
|
+
const effectivePalette = palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
732
|
+
|
|
733
|
+
const parsed = parseSitemap(content, effectivePalette);
|
|
734
|
+
if (parsed.error || parsed.roots.length === 0) return '';
|
|
735
|
+
|
|
736
|
+
const sitemapLayout = layoutSitemap(parsed, undefined, null, undefined, true);
|
|
737
|
+
|
|
738
|
+
const PADDING = 20;
|
|
739
|
+
const titleOffset = parsed.title ? 30 : 0;
|
|
740
|
+
const exportWidth = sitemapLayout.width + PADDING * 2;
|
|
741
|
+
const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
|
|
742
|
+
|
|
743
|
+
const container = document.createElement('div');
|
|
744
|
+
container.style.width = `${exportWidth}px`;
|
|
745
|
+
container.style.height = `${exportHeight}px`;
|
|
746
|
+
container.style.position = 'absolute';
|
|
747
|
+
container.style.left = '-9999px';
|
|
748
|
+
document.body.appendChild(container);
|
|
749
|
+
|
|
750
|
+
renderSitemap(container, parsed, sitemapLayout, effectivePalette, isDark, undefined, {
|
|
751
|
+
width: exportWidth,
|
|
752
|
+
height: exportHeight,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const svgEl = container.querySelector('svg');
|
|
756
|
+
if (!svgEl) {
|
|
757
|
+
document.body.removeChild(container);
|
|
758
|
+
return '';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (theme === 'transparent') {
|
|
762
|
+
svgEl.style.background = 'none';
|
|
763
|
+
} else if (!svgEl.style.background) {
|
|
764
|
+
svgEl.style.background = effectivePalette.bg;
|
|
765
|
+
}
|
|
766
|
+
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
767
|
+
svgEl.style.fontFamily = FONT_FAMILY;
|
|
768
|
+
|
|
769
|
+
const svgHtml = svgEl.outerHTML;
|
|
770
|
+
document.body.removeChild(container);
|
|
771
|
+
|
|
772
|
+
const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
773
|
+
return injectBranding(svgHtml, brandColor);
|
|
774
|
+
}
|