@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/src/infra/renderer.ts
CHANGED
|
@@ -16,6 +16,19 @@ import type { InfraRole } from './roles';
|
|
|
16
16
|
import { parseInfra } from './parser';
|
|
17
17
|
import { computeInfra } from './compute';
|
|
18
18
|
import { layoutInfra } from './layout';
|
|
19
|
+
import {
|
|
20
|
+
LEGEND_HEIGHT,
|
|
21
|
+
LEGEND_PILL_PAD,
|
|
22
|
+
LEGEND_PILL_FONT_SIZE,
|
|
23
|
+
LEGEND_PILL_FONT_W,
|
|
24
|
+
LEGEND_CAPSULE_PAD,
|
|
25
|
+
LEGEND_DOT_R,
|
|
26
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
27
|
+
LEGEND_ENTRY_FONT_W,
|
|
28
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
29
|
+
LEGEND_ENTRY_TRAIL,
|
|
30
|
+
LEGEND_GROUP_GAP,
|
|
31
|
+
} from '../utils/legend-constants';
|
|
19
32
|
|
|
20
33
|
// ============================================================
|
|
21
34
|
// Constants
|
|
@@ -39,22 +52,7 @@ const NODE_PAD_BOTTOM = 10;
|
|
|
39
52
|
const COLLAPSE_BAR_HEIGHT = 6;
|
|
40
53
|
const COLLAPSE_BAR_INSET = 0;
|
|
41
54
|
|
|
42
|
-
//
|
|
43
|
-
const LEGEND_HEIGHT = 28;
|
|
44
|
-
const LEGEND_PILL_PAD = 16;
|
|
45
|
-
const LEGEND_PILL_FONT_SIZE = 11;
|
|
46
|
-
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
47
|
-
const LEGEND_CAPSULE_PAD = 4;
|
|
48
|
-
const LEGEND_DOT_R = 4;
|
|
49
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
50
|
-
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
51
|
-
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
52
|
-
const LEGEND_ENTRY_TRAIL = 8;
|
|
53
|
-
const LEGEND_GROUP_GAP = 12;
|
|
54
|
-
const LEGEND_FIXED_GAP = 16; // gap between fixed legend and scaled diagram
|
|
55
|
-
const SPEED_BADGE_H_PAD = 5; // horizontal padding inside active speed badge
|
|
56
|
-
const SPEED_BADGE_V_PAD = 3; // vertical padding inside active speed badge
|
|
57
|
-
const SPEED_BADGE_GAP = 6; // gap between speed option slots
|
|
55
|
+
const LEGEND_FIXED_GAP = 16; // gap between fixed legend and scaled diagram — local, not shared
|
|
58
56
|
|
|
59
57
|
// Health colors (from UX spec)
|
|
60
58
|
const COLOR_HEALTHY = '#22c55e';
|
|
@@ -1311,21 +1309,6 @@ export function computeInfraLegendGroups(
|
|
|
1311
1309
|
return groups;
|
|
1312
1310
|
}
|
|
1313
1311
|
|
|
1314
|
-
/** Compute total width for the playback pill (speed only). */
|
|
1315
|
-
function computePlaybackWidth(playback: InfraPlaybackState | undefined): number {
|
|
1316
|
-
if (!playback) return 0;
|
|
1317
|
-
const pillWidth = 'Playback'.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
1318
|
-
if (!playback.expanded) return pillWidth;
|
|
1319
|
-
|
|
1320
|
-
let entriesW = 8; // gap after pill
|
|
1321
|
-
entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6; // play/pause
|
|
1322
|
-
for (const s of playback.speedOptions) {
|
|
1323
|
-
entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
|
|
1324
|
-
}
|
|
1325
|
-
return LEGEND_CAPSULE_PAD * 2 + pillWidth + entriesW;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
/** Whether a separate Scenario pill should render. */
|
|
1329
1312
|
function renderLegend(
|
|
1330
1313
|
rootSvg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1331
1314
|
legendGroups: InfraLegendGroup[],
|
|
@@ -1334,21 +1317,21 @@ function renderLegend(
|
|
|
1334
1317
|
palette: PaletteColors,
|
|
1335
1318
|
isDark: boolean,
|
|
1336
1319
|
activeGroup: string | null,
|
|
1337
|
-
playback?: InfraPlaybackState,
|
|
1338
1320
|
) {
|
|
1339
|
-
if (legendGroups.length === 0
|
|
1321
|
+
if (legendGroups.length === 0) return;
|
|
1340
1322
|
|
|
1341
1323
|
const legendG = rootSvg.append('g')
|
|
1342
1324
|
.attr('transform', `translate(0, ${legendY})`);
|
|
1343
1325
|
|
|
1326
|
+
if (activeGroup) {
|
|
1327
|
+
legendG.attr('data-legend-active', activeGroup.toLowerCase());
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1344
1330
|
// Compute centered positions
|
|
1345
1331
|
const effectiveW = (g: InfraLegendGroup) =>
|
|
1346
1332
|
activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
|
|
1347
|
-
const playbackW = computePlaybackWidth(playback);
|
|
1348
|
-
const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
|
|
1349
1333
|
const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0)
|
|
1350
|
-
+ (legendGroups.length - 1) * LEGEND_GROUP_GAP
|
|
1351
|
-
+ trailingGaps + playbackW;
|
|
1334
|
+
+ (legendGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1352
1335
|
let cursorX = (totalWidth - totalLegendW) / 2;
|
|
1353
1336
|
|
|
1354
1337
|
for (const group of legendGroups) {
|
|
@@ -1366,7 +1349,6 @@ function renderLegend(
|
|
|
1366
1349
|
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1367
1350
|
.attr('class', 'infra-legend-group')
|
|
1368
1351
|
.attr('data-legend-group', group.name.toLowerCase())
|
|
1369
|
-
.attr('data-legend-type', group.type)
|
|
1370
1352
|
.style('cursor', 'pointer');
|
|
1371
1353
|
|
|
1372
1354
|
// Outer capsule background (active only)
|
|
@@ -1400,7 +1382,7 @@ function renderLegend(
|
|
|
1400
1382
|
.attr('height', pillH)
|
|
1401
1383
|
.attr('rx', pillH / 2)
|
|
1402
1384
|
.attr('fill', 'none')
|
|
1403
|
-
.attr('stroke',
|
|
1385
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
1404
1386
|
.attr('stroke-width', 0.75);
|
|
1405
1387
|
}
|
|
1406
1388
|
|
|
@@ -1422,15 +1404,12 @@ function renderLegend(
|
|
|
1422
1404
|
const entryG = gEl
|
|
1423
1405
|
.append('g')
|
|
1424
1406
|
.attr('class', 'infra-legend-entry')
|
|
1425
|
-
.attr('data-legend-entry', entry.key)
|
|
1426
|
-
.attr('data-legend-type', group.type)
|
|
1407
|
+
.attr('data-legend-entry', entry.key.toLowerCase())
|
|
1427
1408
|
.attr('data-legend-color', entry.color)
|
|
1409
|
+
.attr('data-legend-type', group.type)
|
|
1410
|
+
.attr('data-legend-tag-group', group.type === 'tag' ? (group.tagKey ?? '') : null)
|
|
1428
1411
|
.style('cursor', 'pointer');
|
|
1429
1412
|
|
|
1430
|
-
if (group.type === 'tag' && group.tagKey) {
|
|
1431
|
-
entryG.attr('data-legend-tag-group', group.tagKey);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
1413
|
entryG.append('circle')
|
|
1435
1414
|
.attr('cx', entryX + LEGEND_DOT_R)
|
|
1436
1415
|
.attr('cy', LEGEND_HEIGHT / 2)
|
|
@@ -1453,127 +1432,12 @@ function renderLegend(
|
|
|
1453
1432
|
cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
|
|
1454
1433
|
}
|
|
1455
1434
|
|
|
1456
|
-
// Playback pill — speed + pause only
|
|
1457
|
-
if (playback) {
|
|
1458
|
-
const isExpanded = playback.expanded;
|
|
1459
|
-
const groupBg = isDark
|
|
1460
|
-
? mix(palette.bg, palette.text, 85)
|
|
1461
|
-
: mix(palette.bg, palette.text, 92);
|
|
1462
|
-
|
|
1463
|
-
const pillLabel = 'Playback';
|
|
1464
|
-
const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
1465
|
-
const fullW = computePlaybackWidth(playback);
|
|
1466
|
-
|
|
1467
|
-
const pbG = legendG
|
|
1468
|
-
.append('g')
|
|
1469
|
-
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1470
|
-
.attr('class', 'infra-legend-group infra-playback-pill')
|
|
1471
|
-
.style('cursor', 'pointer');
|
|
1472
|
-
|
|
1473
|
-
if (isExpanded) {
|
|
1474
|
-
pbG.append('rect')
|
|
1475
|
-
.attr('width', fullW)
|
|
1476
|
-
.attr('height', LEGEND_HEIGHT)
|
|
1477
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
1478
|
-
.attr('fill', groupBg);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
|
|
1482
|
-
const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
|
|
1483
|
-
const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
1484
|
-
|
|
1485
|
-
pbG.append('rect')
|
|
1486
|
-
.attr('x', pillXOff).attr('y', pillYOff)
|
|
1487
|
-
.attr('width', pillWidth).attr('height', pillH)
|
|
1488
|
-
.attr('rx', pillH / 2)
|
|
1489
|
-
.attr('fill', isExpanded ? palette.bg : groupBg);
|
|
1490
|
-
|
|
1491
|
-
if (isExpanded) {
|
|
1492
|
-
pbG.append('rect')
|
|
1493
|
-
.attr('x', pillXOff).attr('y', pillYOff)
|
|
1494
|
-
.attr('width', pillWidth).attr('height', pillH)
|
|
1495
|
-
.attr('rx', pillH / 2)
|
|
1496
|
-
.attr('fill', 'none')
|
|
1497
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
1498
|
-
.attr('stroke-width', 0.75);
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
pbG.append('text')
|
|
1502
|
-
.attr('x', pillXOff + pillWidth / 2)
|
|
1503
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
1504
|
-
.attr('font-family', FONT_FAMILY)
|
|
1505
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
1506
|
-
.attr('font-weight', '500')
|
|
1507
|
-
.attr('fill', isExpanded ? palette.text : palette.textMuted)
|
|
1508
|
-
.attr('text-anchor', 'middle')
|
|
1509
|
-
.text(pillLabel);
|
|
1510
|
-
|
|
1511
|
-
if (isExpanded) {
|
|
1512
|
-
let entryX = pillXOff + pillWidth + 8;
|
|
1513
|
-
const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
|
|
1514
|
-
|
|
1515
|
-
const ppLabel = playback.paused ? '▶' : '⏸';
|
|
1516
|
-
pbG.append('text')
|
|
1517
|
-
.attr('x', entryX).attr('y', entryY)
|
|
1518
|
-
.attr('font-family', FONT_FAMILY)
|
|
1519
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
1520
|
-
.attr('fill', palette.textMuted)
|
|
1521
|
-
.attr('data-playback-action', 'toggle-pause')
|
|
1522
|
-
.style('cursor', 'pointer')
|
|
1523
|
-
.text(ppLabel);
|
|
1524
|
-
entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
|
|
1525
|
-
|
|
1526
|
-
for (const s of playback.speedOptions) {
|
|
1527
|
-
const label = `${s}x`;
|
|
1528
|
-
const isActive = playback.speed === s;
|
|
1529
|
-
const slotW = label.length * LEGEND_ENTRY_FONT_W + SPEED_BADGE_H_PAD * 2;
|
|
1530
|
-
const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
|
|
1531
|
-
const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
|
|
1532
|
-
|
|
1533
|
-
// Wrap in <g> with data attrs so a single element carries the action,
|
|
1534
|
-
// and both rect and text inherit the hit target cleanly.
|
|
1535
|
-
const speedG = pbG.append('g')
|
|
1536
|
-
.attr('data-playback-action', 'set-speed')
|
|
1537
|
-
.attr('data-playback-value', String(s))
|
|
1538
|
-
.style('cursor', 'pointer');
|
|
1539
|
-
|
|
1540
|
-
// Badge rect: filled for active, transparent hit-target for inactive
|
|
1541
|
-
speedG.append('rect')
|
|
1542
|
-
.attr('x', entryX)
|
|
1543
|
-
.attr('y', badgeY)
|
|
1544
|
-
.attr('width', slotW)
|
|
1545
|
-
.attr('height', badgeH)
|
|
1546
|
-
.attr('rx', badgeH / 2)
|
|
1547
|
-
.attr('fill', isActive ? palette.primary : 'transparent');
|
|
1548
|
-
|
|
1549
|
-
speedG.append('text')
|
|
1550
|
-
.attr('x', entryX + slotW / 2).attr('y', entryY)
|
|
1551
|
-
.attr('font-family', FONT_FAMILY)
|
|
1552
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
1553
|
-
.attr('font-weight', isActive ? '600' : '400')
|
|
1554
|
-
.attr('fill', isActive ? palette.bg : palette.textMuted)
|
|
1555
|
-
.attr('text-anchor', 'middle')
|
|
1556
|
-
.text(label);
|
|
1557
|
-
entryX += slotW + SPEED_BADGE_GAP;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
cursorX += fullW + LEGEND_GROUP_GAP;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
1435
|
}
|
|
1565
1436
|
|
|
1566
1437
|
// ============================================================
|
|
1567
1438
|
// Main render
|
|
1568
1439
|
// ============================================================
|
|
1569
1440
|
|
|
1570
|
-
export interface InfraPlaybackState {
|
|
1571
|
-
expanded: boolean;
|
|
1572
|
-
paused: boolean;
|
|
1573
|
-
speed: number;
|
|
1574
|
-
speedOptions: readonly number[];
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
1441
|
export function renderInfra(
|
|
1578
1442
|
container: HTMLDivElement,
|
|
1579
1443
|
layout: InfraLayoutResult,
|
|
@@ -1584,7 +1448,7 @@ export function renderInfra(
|
|
|
1584
1448
|
tagGroups?: InfraTagGroup[],
|
|
1585
1449
|
activeGroup?: string | null,
|
|
1586
1450
|
animate?: boolean,
|
|
1587
|
-
|
|
1451
|
+
_playback?: unknown,
|
|
1588
1452
|
expandedNodeIds?: Set<string> | null,
|
|
1589
1453
|
exportMode?: boolean,
|
|
1590
1454
|
collapsedNodes?: Set<string> | null,
|
|
@@ -1594,7 +1458,7 @@ export function renderInfra(
|
|
|
1594
1458
|
|
|
1595
1459
|
// Build legend groups
|
|
1596
1460
|
const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette, layout.edges);
|
|
1597
|
-
const hasLegend = legendGroups.length > 0
|
|
1461
|
+
const hasLegend = legendGroups.length > 0;
|
|
1598
1462
|
// In app mode (not export), legend is rendered as a separate fixed-size SVG
|
|
1599
1463
|
const fixedLegend = !exportMode && hasLegend;
|
|
1600
1464
|
const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT : 0;
|
|
@@ -1707,9 +1571,9 @@ export function renderInfra(
|
|
|
1707
1571
|
.attr('viewBox', `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP}`)
|
|
1708
1572
|
.attr('preserveAspectRatio', 'xMidYMid meet')
|
|
1709
1573
|
.style('display', 'block');
|
|
1710
|
-
renderLegend(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP / 2, palette, isDark, activeGroup ?? null
|
|
1574
|
+
renderLegend(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP / 2, palette, isDark, activeGroup ?? null);
|
|
1711
1575
|
} else {
|
|
1712
|
-
renderLegend(rootSvg, legendGroups, totalWidth, titleOffset + layout.height + 4, palette, isDark, activeGroup ?? null
|
|
1576
|
+
renderLegend(rootSvg, legendGroups, totalWidth, titleOffset + layout.height + 4, palette, isDark, activeGroup ?? null);
|
|
1713
1577
|
}
|
|
1714
1578
|
}
|
|
1715
1579
|
}
|
package/src/infra/types.ts
CHANGED
|
@@ -99,13 +99,6 @@ export interface InfraTagGroup {
|
|
|
99
99
|
lineNumber: number;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export interface InfraScenario {
|
|
103
|
-
name: string;
|
|
104
|
-
/** Node property overrides: nodeId -> { key: value } */
|
|
105
|
-
overrides: Record<string, Record<string, string | number>>;
|
|
106
|
-
lineNumber: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
102
|
export interface ParsedInfra {
|
|
110
103
|
type: 'infra';
|
|
111
104
|
title: string | null;
|
|
@@ -115,7 +108,6 @@ export interface ParsedInfra {
|
|
|
115
108
|
edges: InfraEdge[];
|
|
116
109
|
groups: InfraGroup[];
|
|
117
110
|
tagGroups: InfraTagGroup[];
|
|
118
|
-
scenarios: InfraScenario[];
|
|
119
111
|
options: Record<string, string>;
|
|
120
112
|
diagnostics: DgmoError[];
|
|
121
113
|
error: string | null;
|
|
@@ -128,9 +120,7 @@ export interface ParsedInfra {
|
|
|
128
120
|
export interface InfraComputeParams {
|
|
129
121
|
rps?: number; // override edge rps (for slider)
|
|
130
122
|
instanceOverrides?: Record<string, number>; // nodeId -> instance count override
|
|
131
|
-
|
|
132
|
-
/** Per-node property overrides: nodeId -> { propertyKey: numericValue }.
|
|
133
|
-
* Applied after scenario overrides. Lets sliders adjust cache-hit, etc. */
|
|
123
|
+
/** Per-node property overrides: nodeId -> { propertyKey: numericValue }. */
|
|
134
124
|
propertyOverrides?: Record<string, Record<string, number>>;
|
|
135
125
|
/** Set of group IDs that should be treated as collapsed (virtual nodes). */
|
|
136
126
|
collapsedGroups?: Set<string>;
|
|
@@ -265,16 +265,19 @@ export function layoutInitiativeStatus(
|
|
|
265
265
|
}
|
|
266
266
|
} else if (isYDisplaced) {
|
|
267
267
|
// 3-point diagonal: exit bottom/top-center of source, enter left-center of target.
|
|
268
|
-
//
|
|
268
|
+
// yOffset applied as X-spread at exit AND Y-spread at entry so parallel edges maintain
|
|
269
|
+
// a consistent visual gap along their entire length (not just at one end).
|
|
269
270
|
const exitY = tgt.y > src.y + NODESEP
|
|
270
271
|
? src.y + src.height / 2 // target is below — exit bottom
|
|
271
272
|
: src.y - src.height / 2; // target is above — exit top
|
|
272
|
-
const
|
|
273
|
-
const
|
|
273
|
+
const spreadExitX = src.x + yOffset;
|
|
274
|
+
const spreadEntryY = tgt.y + yOffset;
|
|
275
|
+
const midX = (spreadExitX + enterX) / 2; // always monotone ✓ (yOffset << node gap)
|
|
276
|
+
const midY = (exitY + spreadEntryY) / 2;
|
|
274
277
|
points = [
|
|
275
|
-
{ x:
|
|
276
|
-
{ x: midX,
|
|
277
|
-
{ x: enterX,
|
|
278
|
+
{ x: spreadExitX, y: exitY },
|
|
279
|
+
{ x: midX, y: midY },
|
|
280
|
+
{ x: enterX, y: spreadEntryY },
|
|
278
281
|
];
|
|
279
282
|
} else if (tgt.x > src.x && !hasIntermediateRank) {
|
|
280
283
|
// 4-point elbow: adjacent-rank forward edges (unchanged)
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -10,6 +10,13 @@ import { renderInlineText } from '../utils/inline-markdown';
|
|
|
10
10
|
import type { ParsedKanban, KanbanColumn, KanbanCard, KanbanTagGroup } from './types';
|
|
11
11
|
import { parseKanban } from './parser';
|
|
12
12
|
import { isArchiveColumn } from './mutations';
|
|
13
|
+
import {
|
|
14
|
+
LEGEND_HEIGHT,
|
|
15
|
+
LEGEND_PILL_FONT_SIZE,
|
|
16
|
+
LEGEND_DOT_R,
|
|
17
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
18
|
+
LEGEND_CAPSULE_PAD,
|
|
19
|
+
} from '../utils/legend-constants';
|
|
13
20
|
|
|
14
21
|
// ============================================================
|
|
15
22
|
// Constants
|
|
@@ -36,10 +43,6 @@ const CARD_META_FONT_SIZE = 10;
|
|
|
36
43
|
const WIP_FONT_SIZE = 10;
|
|
37
44
|
const COLUMN_RADIUS = 8;
|
|
38
45
|
const COLUMN_HEADER_RADIUS = 8;
|
|
39
|
-
const LEGEND_HEIGHT = 28;
|
|
40
|
-
const LEGEND_FONT_SIZE = 11;
|
|
41
|
-
const LEGEND_DOT_R = 4;
|
|
42
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
43
46
|
|
|
44
47
|
// ============================================================
|
|
45
48
|
// Tag color resolution
|
|
@@ -106,9 +109,8 @@ function computeLayout(
|
|
|
106
109
|
parsed: ParsedKanban,
|
|
107
110
|
_palette: PaletteColors
|
|
108
111
|
): { columns: ColumnLayout[]; totalWidth: number; totalHeight: number } {
|
|
109
|
-
// Title
|
|
110
|
-
const
|
|
111
|
-
const headerHeight = hasHeader ? Math.max(TITLE_HEIGHT, LEGEND_HEIGHT) + 8 : 0;
|
|
112
|
+
// Title row
|
|
113
|
+
const headerHeight = parsed.title ? TITLE_HEIGHT + 8 : 0;
|
|
112
114
|
const startY = DIAGRAM_PADDING + headerHeight;
|
|
113
115
|
|
|
114
116
|
// Estimate column widths based on content
|
|
@@ -189,7 +191,8 @@ function computeLayout(
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING;
|
|
192
|
-
const
|
|
194
|
+
const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
|
|
195
|
+
const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING + legendSpace;
|
|
193
196
|
|
|
194
197
|
return { columns: columnLayouts, totalWidth, totalHeight };
|
|
195
198
|
}
|
|
@@ -237,18 +240,19 @@ export function renderKanban(
|
|
|
237
240
|
.text(parsed.title);
|
|
238
241
|
}
|
|
239
242
|
|
|
240
|
-
// Legend (
|
|
243
|
+
// Legend (bottom of diagram)
|
|
241
244
|
if (parsed.tagGroups.length > 0) {
|
|
242
|
-
const legendY =
|
|
243
|
-
|
|
244
|
-
const titleTextWidth = parsed.title
|
|
245
|
-
? parsed.title.length * TITLE_FONT_SIZE * 0.6 + 16
|
|
246
|
-
: 0;
|
|
247
|
-
let legendX = DIAGRAM_PADDING + titleTextWidth;
|
|
245
|
+
const legendY = height - LEGEND_HEIGHT;
|
|
246
|
+
let legendX = DIAGRAM_PADDING;
|
|
248
247
|
const groupBg = isDark
|
|
249
248
|
? mix(palette.surface, palette.bg, 50)
|
|
250
249
|
: mix(palette.surface, palette.bg, 30);
|
|
251
|
-
const capsulePad =
|
|
250
|
+
const capsulePad = LEGEND_CAPSULE_PAD;
|
|
251
|
+
|
|
252
|
+
const legendContainer = svg.append('g').attr('class', 'kanban-legend');
|
|
253
|
+
if (activeTagGroup) {
|
|
254
|
+
legendContainer.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
255
|
+
}
|
|
252
256
|
|
|
253
257
|
for (const group of parsed.tagGroups) {
|
|
254
258
|
const isActive =
|
|
@@ -257,7 +261,7 @@ export function renderKanban(
|
|
|
257
261
|
// When a group is active, skip all other groups entirely
|
|
258
262
|
if (activeTagGroup != null && !isActive) continue;
|
|
259
263
|
|
|
260
|
-
const pillTextWidth = group.name.length *
|
|
264
|
+
const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
|
|
261
265
|
const pillWidth = pillTextWidth + 16;
|
|
262
266
|
|
|
263
267
|
// Measure total capsule width for active groups (pill + entries)
|
|
@@ -272,7 +276,7 @@ export function renderKanban(
|
|
|
272
276
|
|
|
273
277
|
// Outer capsule background for active group
|
|
274
278
|
if (isActive) {
|
|
275
|
-
|
|
279
|
+
legendContainer
|
|
276
280
|
.append('rect')
|
|
277
281
|
.attr('x', legendX)
|
|
278
282
|
.attr('y', legendY)
|
|
@@ -286,7 +290,7 @@ export function renderKanban(
|
|
|
286
290
|
|
|
287
291
|
// Pill background
|
|
288
292
|
const pillBg = isActive ? palette.bg : groupBg;
|
|
289
|
-
|
|
293
|
+
legendContainer
|
|
290
294
|
.append('rect')
|
|
291
295
|
.attr('x', pillX)
|
|
292
296
|
.attr('y', legendY + (isActive ? capsulePad : 0))
|
|
@@ -298,7 +302,7 @@ export function renderKanban(
|
|
|
298
302
|
.attr('data-legend-group', group.name.toLowerCase());
|
|
299
303
|
|
|
300
304
|
if (isActive) {
|
|
301
|
-
|
|
305
|
+
legendContainer
|
|
302
306
|
.append('rect')
|
|
303
307
|
.attr('x', pillX)
|
|
304
308
|
.attr('y', legendY + capsulePad)
|
|
@@ -311,11 +315,11 @@ export function renderKanban(
|
|
|
311
315
|
}
|
|
312
316
|
|
|
313
317
|
// Pill text
|
|
314
|
-
|
|
318
|
+
legendContainer
|
|
315
319
|
.append('text')
|
|
316
320
|
.attr('x', pillX + pillWidth / 2)
|
|
317
|
-
.attr('y', legendY + LEGEND_HEIGHT / 2 +
|
|
318
|
-
.attr('font-size',
|
|
321
|
+
.attr('y', legendY + LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
322
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
319
323
|
.attr('font-weight', '500')
|
|
320
324
|
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
321
325
|
.attr('text-anchor', 'middle')
|
|
@@ -325,7 +329,7 @@ export function renderKanban(
|
|
|
325
329
|
if (isActive) {
|
|
326
330
|
let entryX = pillX + pillWidth + 4;
|
|
327
331
|
for (const entry of group.entries) {
|
|
328
|
-
const entryG =
|
|
332
|
+
const entryG = legendContainer
|
|
329
333
|
.append('g')
|
|
330
334
|
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
331
335
|
.style('cursor', 'pointer');
|
package/src/org/renderer.ts
CHANGED
|
@@ -10,6 +10,23 @@ import type { ParsedOrg } from './parser';
|
|
|
10
10
|
import type { OrgLayoutResult, OrgLayoutNode } from './layout';
|
|
11
11
|
import { parseOrg } from './parser';
|
|
12
12
|
import { layoutOrg } from './layout';
|
|
13
|
+
import {
|
|
14
|
+
LEGEND_HEIGHT,
|
|
15
|
+
LEGEND_PILL_PAD,
|
|
16
|
+
LEGEND_PILL_FONT_SIZE,
|
|
17
|
+
LEGEND_PILL_FONT_W,
|
|
18
|
+
LEGEND_CAPSULE_PAD,
|
|
19
|
+
LEGEND_DOT_R,
|
|
20
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
21
|
+
LEGEND_ENTRY_FONT_W,
|
|
22
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
23
|
+
LEGEND_ENTRY_TRAIL,
|
|
24
|
+
LEGEND_GROUP_GAP,
|
|
25
|
+
LEGEND_EYE_SIZE,
|
|
26
|
+
LEGEND_EYE_GAP,
|
|
27
|
+
EYE_OPEN_PATH,
|
|
28
|
+
EYE_CLOSED_PATH,
|
|
29
|
+
} from '../utils/legend-constants';
|
|
13
30
|
|
|
14
31
|
// ============================================================
|
|
15
32
|
// Constants
|
|
@@ -37,27 +54,7 @@ const CONTAINER_HEADER_HEIGHT = 28;
|
|
|
37
54
|
const COLLAPSE_BAR_HEIGHT = 6;
|
|
38
55
|
const COLLAPSE_BAR_INSET = 0;
|
|
39
56
|
|
|
40
|
-
//
|
|
41
|
-
const LEGEND_HEIGHT = 28;
|
|
42
|
-
const LEGEND_PILL_PAD = 16;
|
|
43
|
-
const LEGEND_PILL_FONT_SIZE = 11;
|
|
44
|
-
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
45
|
-
const LEGEND_CAPSULE_PAD = 4;
|
|
46
|
-
const LEGEND_DOT_R = 4;
|
|
47
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
48
|
-
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
49
|
-
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
50
|
-
const LEGEND_ENTRY_TRAIL = 8;
|
|
51
|
-
const LEGEND_GROUP_GAP = 12;
|
|
52
|
-
const LEGEND_EYE_SIZE = 14;
|
|
53
|
-
const LEGEND_EYE_GAP = 6;
|
|
54
|
-
const LEGEND_FIXED_GAP = 8; // gap between fixed legend and scaled diagram
|
|
55
|
-
|
|
56
|
-
// Eye icon SVG paths (14×14 viewBox)
|
|
57
|
-
const EYE_OPEN_PATH =
|
|
58
|
-
'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';
|
|
59
|
-
const EYE_CLOSED_PATH =
|
|
60
|
-
'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';
|
|
57
|
+
const LEGEND_FIXED_GAP = 8; // gap between fixed legend and scaled diagram — local, not shared
|
|
61
58
|
|
|
62
59
|
// ============================================================
|
|
63
60
|
// Color helpers
|
|
@@ -117,7 +114,7 @@ export function renderOrg(
|
|
|
117
114
|
|
|
118
115
|
const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
|
|
119
116
|
const legendOnly = layout.nodes.length === 0;
|
|
120
|
-
const legendPosition = parsed.options?.['legend-position'] ?? '
|
|
117
|
+
const legendPosition = parsed.options?.['legend-position'] ?? 'bottom';
|
|
121
118
|
const hasLegend = layout.legend.length > 0;
|
|
122
119
|
|
|
123
120
|
// In app mode (not export), render the legend at a fixed size outside the
|
|
@@ -512,7 +509,7 @@ export function renderOrg(
|
|
|
512
509
|
}
|
|
513
510
|
|
|
514
511
|
// Choose parent: unscaled group for fixedLegend, contentG for legend-only
|
|
515
|
-
const
|
|
512
|
+
const legendParentBase = fixedLegend
|
|
516
513
|
? svg
|
|
517
514
|
.append('g')
|
|
518
515
|
.attr('class', 'org-legend-fixed')
|
|
@@ -523,6 +520,10 @@ export function renderOrg(
|
|
|
523
520
|
: `translate(0, ${DIAGRAM_PADDING + titleReserve})`
|
|
524
521
|
)
|
|
525
522
|
: contentG;
|
|
523
|
+
const legendParent = legendParentBase;
|
|
524
|
+
if (fixedLegend && activeTagGroup) {
|
|
525
|
+
legendParentBase.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
526
|
+
}
|
|
526
527
|
|
|
527
528
|
for (const group of visibleGroups) {
|
|
528
529
|
const isActive =
|
package/src/render.ts
CHANGED
|
@@ -52,7 +52,7 @@ export async function render(
|
|
|
52
52
|
c4Level?: 'context' | 'containers' | 'components' | 'deployment';
|
|
53
53
|
c4System?: string;
|
|
54
54
|
c4Container?: string;
|
|
55
|
-
|
|
55
|
+
tagGroup?: string;
|
|
56
56
|
},
|
|
57
57
|
): Promise<string> {
|
|
58
58
|
const theme = options?.theme ?? 'light';
|
|
@@ -75,6 +75,6 @@ export async function render(
|
|
|
75
75
|
c4Level: options?.c4Level,
|
|
76
76
|
c4System: options?.c4System,
|
|
77
77
|
c4Container: options?.c4Container,
|
|
78
|
-
|
|
78
|
+
tagGroup: options?.tagGroup,
|
|
79
79
|
});
|
|
80
80
|
}
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -25,6 +25,19 @@ import type {
|
|
|
25
25
|
import { isSequenceBlock, isSequenceSection, isSequenceNote } from './parser';
|
|
26
26
|
import { resolveSequenceTags } from './tag-resolution';
|
|
27
27
|
import type { ResolvedTagMap } from './tag-resolution';
|
|
28
|
+
import {
|
|
29
|
+
LEGEND_HEIGHT,
|
|
30
|
+
LEGEND_PILL_PAD,
|
|
31
|
+
LEGEND_PILL_FONT_SIZE,
|
|
32
|
+
LEGEND_PILL_FONT_W,
|
|
33
|
+
LEGEND_CAPSULE_PAD,
|
|
34
|
+
LEGEND_DOT_R,
|
|
35
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
36
|
+
LEGEND_ENTRY_FONT_W,
|
|
37
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
38
|
+
LEGEND_ENTRY_TRAIL,
|
|
39
|
+
LEGEND_GROUP_GAP,
|
|
40
|
+
} from '../utils/legend-constants';
|
|
28
41
|
|
|
29
42
|
// ============================================================
|
|
30
43
|
// Layout Constants
|
|
@@ -54,19 +67,6 @@ const NOTE_CHARS_PER_LINE = Math.floor((NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD)
|
|
|
54
67
|
const COLLAPSED_NOTE_H = 20;
|
|
55
68
|
const COLLAPSED_NOTE_W = 40;
|
|
56
69
|
|
|
57
|
-
// Legend rendering constants (consistent with org chart legend)
|
|
58
|
-
const LEGEND_HEIGHT = 28;
|
|
59
|
-
const LEGEND_PILL_PAD = 16;
|
|
60
|
-
const LEGEND_PILL_FONT_SIZE = 11;
|
|
61
|
-
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
62
|
-
const LEGEND_CAPSULE_PAD = 4;
|
|
63
|
-
const LEGEND_DOT_R = 4;
|
|
64
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
65
|
-
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
66
|
-
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
67
|
-
const LEGEND_ENTRY_TRAIL = 8;
|
|
68
|
-
const LEGEND_GROUP_GAP = 12;
|
|
69
|
-
const LEGEND_BOTTOM_GAP = 8;
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
function wrapTextLines(text: string, maxChars: number): string[] {
|
|
@@ -1282,12 +1282,10 @@ export function renderSequenceDiagram(
|
|
|
1282
1282
|
|
|
1283
1283
|
// Compute cumulative Y positions for each step, with section dividers as stable anchors
|
|
1284
1284
|
const titleOffset = title ? TITLE_HEIGHT : 0;
|
|
1285
|
-
const legendOffset =
|
|
1286
|
-
parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_BOTTOM_GAP : 0;
|
|
1287
1285
|
const groupOffset =
|
|
1288
1286
|
groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
|
|
1289
1287
|
const participantStartY =
|
|
1290
|
-
TOP_MARGIN + titleOffset +
|
|
1288
|
+
TOP_MARGIN + titleOffset + PARTICIPANT_Y_OFFSET + groupOffset;
|
|
1291
1289
|
const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
|
|
1292
1290
|
const hasActors = participants.some((p) => p.type === 'actor');
|
|
1293
1291
|
const messageStartOffset = MESSAGE_START_OFFSET + (hasActors ? 20 : 0);
|
|
@@ -1388,11 +1386,13 @@ export function renderSequenceDiagram(
|
|
|
1388
1386
|
participants.length * PARTICIPANT_GAP,
|
|
1389
1387
|
PARTICIPANT_BOX_WIDTH + 40
|
|
1390
1388
|
);
|
|
1391
|
-
const
|
|
1389
|
+
const contentHeight =
|
|
1392
1390
|
participantStartY +
|
|
1393
1391
|
PARTICIPANT_BOX_HEIGHT +
|
|
1394
1392
|
Math.max(lifelineLength, 40) +
|
|
1395
1393
|
40;
|
|
1394
|
+
const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
|
|
1395
|
+
const totalHeight = contentHeight + legendSpace;
|
|
1396
1396
|
|
|
1397
1397
|
const containerWidth = options?.exportWidth ?? container.getBoundingClientRect().width;
|
|
1398
1398
|
const svgWidth = Math.max(totalWidth, containerWidth);
|
|
@@ -1571,7 +1571,7 @@ export function renderSequenceDiagram(
|
|
|
1571
1571
|
|
|
1572
1572
|
// Render legend pills for tag groups
|
|
1573
1573
|
if (parsed.tagGroups.length > 0) {
|
|
1574
|
-
const legendY =
|
|
1574
|
+
const legendY = contentHeight;
|
|
1575
1575
|
const groupBg = isDark
|
|
1576
1576
|
? mix(palette.surface, palette.bg, 50)
|
|
1577
1577
|
: mix(palette.surface, palette.bg, 30);
|
|
@@ -1615,8 +1615,13 @@ export function renderSequenceDiagram(
|
|
|
1615
1615
|
(legendItems.length - 1) * LEGEND_GROUP_GAP;
|
|
1616
1616
|
let legendX = (svgWidth - totalLegendWidth) / 2;
|
|
1617
1617
|
|
|
1618
|
+
const legendContainer = svg.append('g').attr('class', 'sequence-legend');
|
|
1619
|
+
if (activeTagGroup) {
|
|
1620
|
+
legendContainer.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1618
1623
|
for (const item of legendItems) {
|
|
1619
|
-
const gEl =
|
|
1624
|
+
const gEl = legendContainer
|
|
1620
1625
|
.append('g')
|
|
1621
1626
|
.attr('transform', `translate(${legendX}, ${legendY})`)
|
|
1622
1627
|
.attr('class', 'sequence-legend-group')
|