@transitrix/diagrams 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activities/__tests__/validate.test.js +32 -1
- package/dist/activities/validate.js +21 -5
- package/dist/activity-card/__tests__/example.test.js +1 -3
- package/dist/activity-card/__tests__/layout.test.js +2 -9
- package/dist/activity-card/__tests__/resolver.test.js +26 -1
- package/dist/activity-card/__tests__/validate.test.js +23 -1
- package/dist/activity-card/layout.js +1 -7
- package/dist/activity-card/resolver.js +34 -23
- package/dist/activity-card/types.d.ts +1 -1
- package/dist/activity-card/validate.js +22 -11
- package/dist/capability-map/index.d.ts +4 -0
- package/dist/capability-map/index.js +2 -0
- package/dist/capability-map/layout-tree.d.ts +42 -0
- package/dist/capability-map/layout-tree.js +65 -0
- package/dist/capability-map/render-capability-tree.d.ts +15 -0
- package/dist/capability-map/render-capability-tree.js +108 -0
- package/dist/capability-map/types.d.ts +2 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +14 -1
- package/dist/fgca/__tests__/validate.test.js +2 -2
- package/dist/fgca/parse-canonical.js +20 -24
- package/dist/fgca/preview-layout.js +50 -1
- package/dist/fgca/types.d.ts +2 -0
- package/dist/fgca/validate.js +19 -11
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/themes.js +26 -3
- package/dist/theme/tokens.d.ts +40 -0
- package/dist/theme/tokens.js +28 -0
- package/dist/webview/render-activity-card.js +2 -3
- package/dist/webview/render-fgca.js +2 -2
- package/dist/webview/render-goals.js +28 -5
- package/package.json +1 -1
- package/src/activities/__tests__/validate.test.ts +35 -1
- package/src/activities/validate.ts +21 -5
- package/src/activity-card/__tests__/example.test.ts +1 -3
- package/src/activity-card/__tests__/layout.test.ts +2 -9
- package/src/activity-card/__tests__/resolver.test.ts +28 -1
- package/src/activity-card/__tests__/validate.test.ts +25 -1
- package/src/activity-card/layout.ts +1 -8
- package/src/activity-card/resolver.ts +34 -23
- package/src/activity-card/types.ts +1 -1
- package/src/activity-card/validate.ts +20 -11
- package/src/capability-map/index.ts +11 -0
- package/src/capability-map/layout-tree.ts +114 -0
- package/src/capability-map/render-capability-tree.ts +131 -0
- package/src/capability-map/types.ts +2 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +15 -1
- package/src/fgca/__tests__/validate.test.ts +2 -2
- package/src/fgca/parse-canonical.ts +20 -30
- package/src/fgca/preview-layout.ts +53 -1
- package/src/fgca/types.ts +2 -0
- package/src/fgca/validate.ts +16 -8
- package/src/theme/index.ts +1 -1
- package/src/theme/themes.ts +27 -2
- package/src/theme/tokens.ts +30 -0
- package/src/webview/render-activity-card.ts +2 -3
- package/src/webview/render-fgca.ts +2 -2
- package/src/webview/render-goals.ts +26 -5
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_NODE_SEP } from './layout-tree.js';
|
|
2
|
+
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
3
|
+
import { escXml } from '../webview/render-util.js';
|
|
4
|
+
const PAD = 24;
|
|
5
|
+
const BTN_R = 8;
|
|
6
|
+
const LEGEND_H = 40;
|
|
7
|
+
const LABEL_MAX_CHARS = 28;
|
|
8
|
+
const LABEL_TRIM_CHARS = 26;
|
|
9
|
+
const BAND_LABELS = ['Levels 0–2', 'Levels 3–4', 'Levels 5+'];
|
|
10
|
+
function depthBand(depth) {
|
|
11
|
+
if (depth <= 2)
|
|
12
|
+
return 0;
|
|
13
|
+
if (depth <= 4)
|
|
14
|
+
return 1;
|
|
15
|
+
return 2;
|
|
16
|
+
}
|
|
17
|
+
function truncate(s, max, trim) {
|
|
18
|
+
return s.length > max ? s.slice(0, trim) + '…' : s;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Renders a capability-map as a left-to-right SVG node tree with DSM-matching
|
|
22
|
+
* visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
|
|
23
|
+
*
|
|
24
|
+
* The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
|
|
25
|
+
* that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
|
|
26
|
+
* in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
|
|
27
|
+
* for the webview click handler.
|
|
28
|
+
*/
|
|
29
|
+
export function renderCapabilityTreeSvg(map, opts = {}) {
|
|
30
|
+
const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE } = opts;
|
|
31
|
+
const layout = layoutCapabilityTree(map, collapsedIds);
|
|
32
|
+
if (layout.nodes.length === 0) {
|
|
33
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
34
|
+
}
|
|
35
|
+
const ox = -layout.bounds.x + PAD;
|
|
36
|
+
const oy = -layout.bounds.y + PAD;
|
|
37
|
+
const svgW = layout.bounds.width + PAD * 2 + BTN_R;
|
|
38
|
+
const svgH = layout.bounds.height + PAD * 2 + LEGEND_H + TREE_NODE_SEP;
|
|
39
|
+
const nodeMap = new Map(layout.nodes.map(n => [n.id, n]));
|
|
40
|
+
const edgeSvg = layout.edges.map(e => {
|
|
41
|
+
const s = nodeMap.get(e.source);
|
|
42
|
+
const t = nodeMap.get(e.target);
|
|
43
|
+
if (!s || !t)
|
|
44
|
+
return '';
|
|
45
|
+
const sx = s.x + ox + s.width;
|
|
46
|
+
const sy = s.y + oy + s.height / 2;
|
|
47
|
+
const tx = t.x + ox;
|
|
48
|
+
const ty = t.y + oy + t.height / 2;
|
|
49
|
+
return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature)}" class="diagram-edge"/>`;
|
|
50
|
+
}).filter(Boolean).join('\n');
|
|
51
|
+
const nodeSvg = layout.nodes.map(n => {
|
|
52
|
+
const x = n.x + ox;
|
|
53
|
+
const y = n.y + oy;
|
|
54
|
+
const band = depthBand(n.depth);
|
|
55
|
+
const mat = Math.max(1, Math.min(5, n.data.current_maturity | 0));
|
|
56
|
+
const nameText = truncate(n.data.name, LABEL_MAX_CHARS, LABEL_TRIM_CHARS);
|
|
57
|
+
const nameTitle = n.data.name.length > LABEL_MAX_CHARS ? escXml(n.data.name) : '';
|
|
58
|
+
// Maturity badge: left side of node
|
|
59
|
+
const badgeW = 28;
|
|
60
|
+
const badgeH = 18;
|
|
61
|
+
const badgeX = x + 10;
|
|
62
|
+
const badgeY = y + (TREE_NODE_HEIGHT - badgeH) / 2;
|
|
63
|
+
// Text area: right of badge
|
|
64
|
+
const textX = badgeX + badgeW + 8;
|
|
65
|
+
const nameY = y + TREE_NODE_HEIGHT / 2 - 8;
|
|
66
|
+
const idY = y + TREE_NODE_HEIGHT / 2 + 9;
|
|
67
|
+
// Collapse button: centered on right edge of node
|
|
68
|
+
const btnCx = x + TREE_NODE_WIDTH;
|
|
69
|
+
const btnCy = y + TREE_NODE_HEIGHT / 2;
|
|
70
|
+
const collapseBtn = n.hasChildren
|
|
71
|
+
? `<circle class="tree-collapse-btn" cx="${btnCx}" cy="${btnCy}" r="${BTN_R}" data-node-id="${escXml(n.id)}" data-collapsed="${n.isCollapsed}"/>
|
|
72
|
+
<text class="tree-collapse-lbl" x="${btnCx}" y="${btnCy}" text-anchor="middle">${n.isCollapsed ? '+' : '−'}</text>`
|
|
73
|
+
: '';
|
|
74
|
+
return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
|
|
75
|
+
<rect class="tree-level-${band}" x="${x}" y="${y}" width="${TREE_NODE_WIDTH}" height="${TREE_NODE_HEIGHT}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
|
|
76
|
+
<rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
|
|
77
|
+
<text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
|
|
78
|
+
<text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
|
|
79
|
+
<text class="text-id" x="${textX}" y="${idY}" dominant-baseline="central">${escXml(n.id)}</text>
|
|
80
|
+
${collapseBtn}
|
|
81
|
+
</g>`;
|
|
82
|
+
}).join('\n');
|
|
83
|
+
// Legend band: proportional coloured blocks per level-count band
|
|
84
|
+
const legendY = layout.bounds.height + PAD * 2 + TREE_NODE_SEP / 2;
|
|
85
|
+
const legendW = svgW - PAD * 2;
|
|
86
|
+
const totalNodes = layout.nodes.length || 1;
|
|
87
|
+
const bandCounts = [layout.levelCounts.band0, layout.levelCounts.band1, layout.levelCounts.band2];
|
|
88
|
+
let legendX = PAD;
|
|
89
|
+
const legendSvg = bandCounts.map((count, i) => {
|
|
90
|
+
const bw = Math.max(1, Math.round(legendW * count / totalNodes));
|
|
91
|
+
const bx = legendX;
|
|
92
|
+
legendX += bw;
|
|
93
|
+
if (count === 0)
|
|
94
|
+
return '';
|
|
95
|
+
return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="1"/>
|
|
96
|
+
<text class="text-id" x="${bx + bw / 2}" y="${legendY + 12}" text-anchor="middle" dominant-baseline="central">${escXml(BAND_LABELS[i])}</text>`;
|
|
97
|
+
}).filter(Boolean).join('\n');
|
|
98
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">
|
|
99
|
+
<defs>
|
|
100
|
+
<marker id="cap-tree-arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
101
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
102
|
+
</marker>
|
|
103
|
+
</defs>
|
|
104
|
+
${edgeSvg}
|
|
105
|
+
${nodeSvg}
|
|
106
|
+
${legendSvg}
|
|
107
|
+
</svg>`;
|
|
108
|
+
}
|
|
@@ -143,9 +143,22 @@ describe('parseCanonicalFGCA — edge-driving fields populated', () => {
|
|
|
143
143
|
expect(parsed.goals[0].factor).toHaveLength(1);
|
|
144
144
|
// goal → change edge
|
|
145
145
|
expect(parsed.changes[0].goal_id).not.toBe(0);
|
|
146
|
+
expect(parsed.changes[0].goal_id).not.toBe('');
|
|
146
147
|
// change → activity edge
|
|
147
148
|
expect(parsed.changes[0].activity_ids).toHaveLength(1);
|
|
148
149
|
});
|
|
150
|
+
it('preserves canonical string IDs — not converted to sequential numbers', () => {
|
|
151
|
+
const r = parseCanonicalFGCA(VALID);
|
|
152
|
+
expect(r.valid).toBe(true);
|
|
153
|
+
const parsed = r.parsed;
|
|
154
|
+
expect(parsed.factors[0].id).toBe('FACTOR-1');
|
|
155
|
+
expect(parsed.goals[0].id).toBe('GOAL-1');
|
|
156
|
+
expect(parsed.goals[0].factor[0].id).toBe('FACTOR-1');
|
|
157
|
+
expect(parsed.changes[0].id).toBe('CHANGE-1');
|
|
158
|
+
expect(parsed.changes[0].goal_id).toBe('GOAL-1');
|
|
159
|
+
expect(parsed.changes[0].activity_ids[0]).toBe('ACTIVITY-1');
|
|
160
|
+
expect(parsed.activities[0].id).toBe('ACTIVITY-1');
|
|
161
|
+
});
|
|
149
162
|
});
|
|
150
163
|
describe('parseCanonicalFGA', () => {
|
|
151
164
|
const VALID_FGA = {
|
|
@@ -218,7 +231,7 @@ describe('parseCanonicalFGA — example file regression', () => {
|
|
|
218
231
|
expect(r.errors).toEqual([]);
|
|
219
232
|
expect(r.valid).toBe(true);
|
|
220
233
|
// Every activity must carry a goal_id, or the FGA preview draws no edges.
|
|
221
|
-
expect(r.parsed.activities.every((a) => a.goal_id != null && a.goal_id !== 0)).toBe(true);
|
|
234
|
+
expect(r.parsed.activities.every((a) => a.goal_id != null && a.goal_id !== 0 && a.goal_id !== '')).toBe(true);
|
|
222
235
|
});
|
|
223
236
|
}
|
|
224
237
|
});
|
|
@@ -47,11 +47,11 @@ describe('validateFGCADoc', () => {
|
|
|
47
47
|
expect(result.valid).toBe(false);
|
|
48
48
|
expect(result.errors.some(e => e.path === 'changes')).toBe(true);
|
|
49
49
|
});
|
|
50
|
-
it('errors when
|
|
50
|
+
it('errors when actions array is missing', () => {
|
|
51
51
|
const { activities: _, ...without } = VALID_DOC;
|
|
52
52
|
const result = validateFGCADoc({ ...without, notation: 'fgca' });
|
|
53
53
|
expect(result.valid).toBe(false);
|
|
54
|
-
expect(result.errors.some(e => e.path === '
|
|
54
|
+
expect(result.errors.some(e => e.path === 'actions')).toBe(true);
|
|
55
55
|
});
|
|
56
56
|
it('errors on empty factor name', () => {
|
|
57
57
|
const result = validateFGCADoc({ ...VALID_DOC, factors: [{ id: 1, name: '' }] });
|
|
@@ -91,7 +91,11 @@ export function parseCanonicalFGCA(input) {
|
|
|
91
91
|
const factorsRaw = asObjectArray(raw['factors']);
|
|
92
92
|
const goalsRaw = asObjectArray(raw['goals']);
|
|
93
93
|
const changesRaw = asObjectArray(raw['changes']);
|
|
94
|
-
|
|
94
|
+
// Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
|
|
95
|
+
const activitiesRaw = asObjectArray(raw['actions']) ?? asObjectArray(raw['activities']);
|
|
96
|
+
if ('activities' in raw && !('actions' in raw)) {
|
|
97
|
+
warnings.push({ code: 'FGCA-001', message: '"activities" key is deprecated — rename to "actions"' });
|
|
98
|
+
}
|
|
95
99
|
if (factorsRaw === null)
|
|
96
100
|
errors.push({ code: 'FGCA-004', message: 'factors must be an array', path: 'factors' });
|
|
97
101
|
if (goalsRaw === null)
|
|
@@ -99,7 +103,7 @@ export function parseCanonicalFGCA(input) {
|
|
|
99
103
|
if (changesRaw === null)
|
|
100
104
|
errors.push({ code: 'FGCA-004', message: 'changes must be an array', path: 'changes' });
|
|
101
105
|
if (activitiesRaw === null)
|
|
102
|
-
errors.push({ code: 'FGCA-004', message: '
|
|
106
|
+
errors.push({ code: 'FGCA-004', message: 'actions must be an array', path: 'actions' });
|
|
103
107
|
if (errors.length > 0)
|
|
104
108
|
return { valid: false, errors, warnings };
|
|
105
109
|
// From here we know all four arrays exist.
|
|
@@ -175,29 +179,19 @@ export function parseCanonicalFGCA(input) {
|
|
|
175
179
|
}
|
|
176
180
|
return out;
|
|
177
181
|
}
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
allCanonicalIds.forEach((id, i) => idMap.toInternal.set(id, i + 1));
|
|
182
|
-
function intId(canonical) {
|
|
183
|
-
const n = idMap.toInternal.get(canonical);
|
|
184
|
-
if (n === undefined) {
|
|
185
|
-
// Shouldn't happen — we built the map from the same sets.
|
|
186
|
-
throw new Error(`internal: canonical id "${canonical}" not mapped`);
|
|
187
|
-
}
|
|
188
|
-
return n;
|
|
189
|
-
}
|
|
190
|
-
// Layer entries (internal form).
|
|
182
|
+
// Layer entries (internal form). Canonical string IDs are passed through
|
|
183
|
+
// directly — no conversion to sequential numbers. The layout and renderer
|
|
184
|
+
// treat IDs as opaque keys and also display them verbatim to the user.
|
|
191
185
|
const internalFactors = factors.map((el) => ({
|
|
192
|
-
id:
|
|
186
|
+
id: String(el['id']),
|
|
193
187
|
name: String(el['name'] ?? ''),
|
|
194
188
|
}));
|
|
195
189
|
const internalGoals = goals.map((el, i) => {
|
|
196
190
|
const refIds = checkRefArray(el, 'factors', factorIds, FACTOR_OR_DRIVER_ID_RE, 'FGCA-008', `goals[${i}]`);
|
|
197
191
|
return {
|
|
198
|
-
id:
|
|
192
|
+
id: String(el['id']),
|
|
199
193
|
name: String(el['name'] ?? ''),
|
|
200
|
-
factor: refIds.map((r) => ({ id:
|
|
194
|
+
factor: refIds.map((r) => ({ id: r })),
|
|
201
195
|
};
|
|
202
196
|
});
|
|
203
197
|
// Build a reverse mapping: which changes does each activity reference (via
|
|
@@ -230,18 +224,20 @@ export function parseCanonicalFGCA(input) {
|
|
|
230
224
|
const canonicalChangeId = String(el['id']);
|
|
231
225
|
const linkedActivityIds = activitiesForChange.get(canonicalChangeId) ?? [];
|
|
232
226
|
return {
|
|
233
|
-
id:
|
|
227
|
+
id: canonicalChangeId,
|
|
234
228
|
name: String(el['name'] ?? ''),
|
|
235
|
-
goal_id: goalRefs.length > 0 ?
|
|
236
|
-
activity_ids: linkedActivityIds
|
|
229
|
+
goal_id: goalRefs.length > 0 ? goalRefs[0] : '',
|
|
230
|
+
activity_ids: linkedActivityIds,
|
|
237
231
|
};
|
|
238
232
|
});
|
|
239
233
|
const internalActivities = activities.map((el, i) => {
|
|
240
|
-
const goalRefs = checkRefArray(el, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `
|
|
234
|
+
const goalRefs = checkRefArray(el, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]`);
|
|
235
|
+
const typeVal = typeof el['type'] === 'string' ? el['type'] : undefined;
|
|
241
236
|
return {
|
|
242
|
-
id:
|
|
237
|
+
id: String(el['id']),
|
|
243
238
|
name: String(el['name'] ?? ''),
|
|
244
|
-
goal_id: goalRefs.length > 0 ?
|
|
239
|
+
goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
|
|
240
|
+
...(typeVal !== undefined ? { type: typeVal } : {}),
|
|
245
241
|
};
|
|
246
242
|
});
|
|
247
243
|
// FACTOR.references_constraint grammar check (FGCA-015).
|
|
@@ -61,18 +61,67 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
61
61
|
change: changes.map(c => ({ id: `change_${c.id}`, label: c.name })),
|
|
62
62
|
activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name })),
|
|
63
63
|
};
|
|
64
|
+
// Build predecessor map: for each node, which node IDs in the previous column
|
|
65
|
+
// connect to it from the left. Used for barycenter crossing minimization.
|
|
66
|
+
const predecessors = new Map();
|
|
67
|
+
for (const g of doc.goals) {
|
|
68
|
+
predecessors.set(`goal_${g.id}`, (g.factor ?? []).map(f => `driver_${f.id}`));
|
|
69
|
+
}
|
|
70
|
+
if (!hideChanges) {
|
|
71
|
+
for (const c of changes) {
|
|
72
|
+
predecessors.set(`change_${c.id}`, [`goal_${c.goal_id}`]);
|
|
73
|
+
}
|
|
74
|
+
const coveredActs = new Set(changes.flatMap(c => c.activity_ids.map(String)));
|
|
75
|
+
for (const a of doc.activities) {
|
|
76
|
+
const preds = changes
|
|
77
|
+
.filter(c => c.activity_ids.map(String).includes(String(a.id)))
|
|
78
|
+
.map(c => `change_${c.id}`);
|
|
79
|
+
if (a.goal_id != null && !coveredActs.has(String(a.id)))
|
|
80
|
+
preds.push(`goal_${a.goal_id}`);
|
|
81
|
+
predecessors.set(`activity_${a.id}`, preds);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const connectedViaChange = new Set();
|
|
86
|
+
for (const c of changes) {
|
|
87
|
+
for (const aid of c.activity_ids) {
|
|
88
|
+
predecessors.set(`activity_${aid}`, [`goal_${c.goal_id}`]);
|
|
89
|
+
connectedViaChange.add(String(aid));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const a of doc.activities) {
|
|
93
|
+
if (a.goal_id != null && !connectedViaChange.has(String(a.id))) {
|
|
94
|
+
predecessors.set(`activity_${a.id}`, [`goal_${a.goal_id}`]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Sort a column's items by the barycenter of their predecessors' y-centres.
|
|
99
|
+
// Nodes with no predecessors sort last (Infinity barycenter) so they don't
|
|
100
|
+
// displace connected nodes.
|
|
101
|
+
function barycentricSort(items, yCenters) {
|
|
102
|
+
return [...items].sort((a, b) => {
|
|
103
|
+
const pA = (predecessors.get(a.id) ?? []).map(p => yCenters.get(p) ?? 0).filter(v => v > 0);
|
|
104
|
+
const pB = (predecessors.get(b.id) ?? []).map(p => yCenters.get(p) ?? 0).filter(v => v > 0);
|
|
105
|
+
const bcA = pA.length > 0 ? pA.reduce((s, v) => s + v, 0) / pA.length : Infinity;
|
|
106
|
+
const bcB = pB.length > 0 ? pB.reduce((s, v) => s + v, 0) / pB.length : Infinity;
|
|
107
|
+
return bcA - bcB;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
64
110
|
const nodeMap = new Map();
|
|
65
111
|
const nodes = [];
|
|
66
112
|
const columns = [];
|
|
113
|
+
const yCenters = new Map(); // nodeId → y + FGCA_NODE_H / 2
|
|
67
114
|
for (let ci = 0; ci < cols.length; ci++) {
|
|
68
115
|
const col = cols[ci];
|
|
69
116
|
const x = FGCA_PAD + ci * colStride;
|
|
70
117
|
columns.push({ col, x });
|
|
118
|
+
const items = ci === 0 ? colItems[col] : barycentricSort(colItems[col], yCenters);
|
|
71
119
|
let y = FGCA_PAD + FGCA_HEADER_H + rowGap;
|
|
72
|
-
for (const item of
|
|
120
|
+
for (const item of items) {
|
|
73
121
|
const node = { id: item.id, x, y, label: item.label, col };
|
|
74
122
|
nodes.push(node);
|
|
75
123
|
nodeMap.set(item.id, node);
|
|
124
|
+
yCenters.set(item.id, y + FGCA_NODE_H / 2);
|
|
76
125
|
y += FGCA_NODE_H + rowGap;
|
|
77
126
|
}
|
|
78
127
|
}
|
package/dist/fgca/types.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ActivityItem {
|
|
|
22
22
|
name: string;
|
|
23
23
|
goal_id?: number | string | null;
|
|
24
24
|
activity_type_id?: number;
|
|
25
|
+
/** Canonical values: initiative | programme | project | work_package. Unknown values pass through without error. */
|
|
26
|
+
type?: string;
|
|
25
27
|
}
|
|
26
28
|
export interface ActivityTypeItem {
|
|
27
29
|
id: number | string;
|
package/dist/fgca/validate.js
CHANGED
|
@@ -22,10 +22,18 @@ export function validateFGCADoc(input) {
|
|
|
22
22
|
errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
|
|
23
23
|
if (!Array.isArray(raw.changes))
|
|
24
24
|
errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
|
|
26
|
+
const hasActions = Array.isArray(raw.actions);
|
|
27
|
+
const hasActivities = Array.isArray(raw.activities);
|
|
28
|
+
if (!hasActions && !hasActivities) {
|
|
29
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
30
|
+
}
|
|
31
|
+
else if ('activities' in raw) {
|
|
32
|
+
warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
|
|
33
|
+
}
|
|
27
34
|
if (errors.length > 0)
|
|
28
35
|
return { valid: false, errors, warnings };
|
|
36
|
+
const actionsArr = (hasActions ? raw.actions : raw.activities);
|
|
29
37
|
const doc = raw;
|
|
30
38
|
const collectIds = (arr) => {
|
|
31
39
|
const out = new Set();
|
|
@@ -40,7 +48,7 @@ export function validateFGCADoc(input) {
|
|
|
40
48
|
};
|
|
41
49
|
const factorIds = collectIds(doc.factors);
|
|
42
50
|
const goalIds = collectIds(doc.goals);
|
|
43
|
-
const activityIds = collectIds(
|
|
51
|
+
const activityIds = collectIds(actionsArr);
|
|
44
52
|
for (let i = 0; i < doc.factors.length; i++) {
|
|
45
53
|
const f = doc.factors[i];
|
|
46
54
|
if (!f || typeof f !== 'object') {
|
|
@@ -95,17 +103,17 @@ export function validateFGCADoc(input) {
|
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
|
-
for (let i = 0; i <
|
|
99
|
-
const a =
|
|
106
|
+
for (let i = 0; i < actionsArr.length; i++) {
|
|
107
|
+
const a = actionsArr[i];
|
|
100
108
|
if (!a || typeof a !== 'object') {
|
|
101
|
-
errors.push({ code: 'SCHEMA_INVALID', message: '
|
|
109
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'action entry must be an object', path: `actions[${i}]` });
|
|
102
110
|
continue;
|
|
103
111
|
}
|
|
104
|
-
const
|
|
105
|
-
if (typeof
|
|
106
|
-
errors.push({ code: 'SCHEMA_INVALID', message: '
|
|
107
|
-
if (typeof
|
|
108
|
-
errors.push({ code: 'EMPTY_NAME', message: `
|
|
112
|
+
const action = a;
|
|
113
|
+
if (typeof action.id !== 'number' && typeof action.id !== 'string')
|
|
114
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'action id must be a number or string', path: `actions[${i}]` });
|
|
115
|
+
if (typeof action.name !== 'string' || !action.name.trim())
|
|
116
|
+
errors.push({ code: 'EMPTY_NAME', message: `Action ${String(action.id)} has empty name`, path: `actions[${i}]` });
|
|
109
117
|
}
|
|
110
118
|
return { valid: errors.length === 0, errors, warnings };
|
|
111
119
|
}
|
package/dist/theme/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { AdaptiveTokens } from './tokens.js';
|
|
2
|
-
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
2
|
+
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
3
3
|
export type { ThemeId } from './themes.js';
|
|
4
4
|
export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
|
package/dist/theme/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
1
|
+
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
2
2
|
export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
|
package/dist/theme/themes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
|
|
1
|
+
import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
|
|
2
2
|
const TRANSITRIX_LIGHT = {
|
|
3
3
|
bg: '#ffffff',
|
|
4
4
|
bgSurface: '#f8fafc',
|
|
@@ -60,7 +60,19 @@ function diagramVars(variant) {
|
|
|
60
60
|
const sc = STRUCTURAL;
|
|
61
61
|
const lv = LEVEL_COLORS[variant];
|
|
62
62
|
const mat = MATURITY_COLORS[variant];
|
|
63
|
+
const treeLevel = TREE_LEVEL_COLORS;
|
|
64
|
+
const treeMat = TREE_MATURITY_COLORS[variant];
|
|
63
65
|
const levelVars = lv.map((c, i) => `--ts-level-${i}:${c}`).join(';') + ';';
|
|
66
|
+
const treeVars = [
|
|
67
|
+
`${CSS_VAR.treeLevel0}:${treeLevel.band0[variant]}`,
|
|
68
|
+
`${CSS_VAR.treeLevel1}:${treeLevel.band1[variant]}`,
|
|
69
|
+
`${CSS_VAR.treeLevel2}:${treeLevel.band2[variant]}`,
|
|
70
|
+
`${CSS_VAR.treeMaturity1}:${treeMat[0]}`,
|
|
71
|
+
`${CSS_VAR.treeMaturity2}:${treeMat[1]}`,
|
|
72
|
+
`${CSS_VAR.treeMaturity3}:${treeMat[2]}`,
|
|
73
|
+
`${CSS_VAR.treeMaturity4}:${treeMat[3]}`,
|
|
74
|
+
`${CSS_VAR.treeMaturity5}:${treeMat[4]}`,
|
|
75
|
+
].join(';') + ';';
|
|
64
76
|
return [
|
|
65
77
|
`${CSS_VAR.layerDriver}:${lc.driver[variant]}`,
|
|
66
78
|
`${CSS_VAR.layerFactor}:${lc.factor[variant]}`,
|
|
@@ -77,7 +89,7 @@ function diagramVars(variant) {
|
|
|
77
89
|
`${CSS_VAR.maturity3}:${mat[2]}`,
|
|
78
90
|
`${CSS_VAR.maturity4}:${mat[3]}`,
|
|
79
91
|
`${CSS_VAR.maturity5}:${mat[4]}`,
|
|
80
|
-
].join(';') + ';' + levelVars;
|
|
92
|
+
].join(';') + ';' + levelVars + treeVars;
|
|
81
93
|
}
|
|
82
94
|
/** CSS for the webview shell (toolbar, canvas). */
|
|
83
95
|
function shellCss() {
|
|
@@ -136,7 +148,18 @@ ${levelRules}
|
|
|
136
148
|
.compliance-deadline{fill:var(${cv.statusErrorBg});stroke:var(${cv.statusErrorFg});}
|
|
137
149
|
.compliance-deadline text{fill:var(${cv.statusErrorFg});}
|
|
138
150
|
.compliance-badge{fill:var(${cv.statusErrorFg});}
|
|
139
|
-
.compliance-badge-text{fill:var(${cv.textInverse});font-family:${t.fontFamily};font-size:8px;font-weight:700;}
|
|
151
|
+
.compliance-badge-text{fill:var(${cv.textInverse});font-family:${t.fontFamily};font-size:8px;font-weight:700;}
|
|
152
|
+
.tree-level-0{fill:var(${cv.treeLevel0});}
|
|
153
|
+
.tree-level-1{fill:var(${cv.treeLevel1});}
|
|
154
|
+
.tree-level-2{fill:var(${cv.treeLevel2});}
|
|
155
|
+
.tree-maturity-1{fill:var(${cv.treeMaturity1});}
|
|
156
|
+
.tree-maturity-2{fill:var(${cv.treeMaturity2});}
|
|
157
|
+
.tree-maturity-3{fill:var(${cv.treeMaturity3});}
|
|
158
|
+
.tree-maturity-4{fill:var(${cv.treeMaturity4});}
|
|
159
|
+
.tree-maturity-5{fill:var(${cv.treeMaturity5});}
|
|
160
|
+
.tree-collapse-btn{cursor:pointer;fill:white;stroke:var(${cv.nodeStroke});stroke-width:1.5;}
|
|
161
|
+
.tree-collapse-btn:hover{fill:var(${cv.bgElevated});}
|
|
162
|
+
.tree-collapse-lbl{fill:var(${cv.textSecondary});font-family:${t.fontFamily};font-size:12px;font-weight:700;dominant-baseline:central;pointer-events:none;}`;
|
|
140
163
|
}
|
|
141
164
|
/**
|
|
142
165
|
* Generates a self-contained CSS block for embedding inside a standalone SVG file.
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -82,6 +82,38 @@ export declare const MATURITY_COLORS: {
|
|
|
82
82
|
readonly dark: readonly ["#7f1d1d", "#92400e", "#854d0e", "#3f6212", "#14532d"];
|
|
83
83
|
readonly hc: readonly ["#f87171", "#fb923c", "#facc15", "#a3e635", "#4ade80"];
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* DSM-matching capability tree node fill colors, grouped by depth band.
|
|
87
|
+
* band0: depth 0–2 (pink)
|
|
88
|
+
* band1: depth 3–4 (yellow)
|
|
89
|
+
* band2: depth 5+ (blue)
|
|
90
|
+
*/
|
|
91
|
+
export declare const TREE_LEVEL_COLORS: {
|
|
92
|
+
readonly band0: {
|
|
93
|
+
readonly light: "#fee0e0";
|
|
94
|
+
readonly dark: "#3d0f0f";
|
|
95
|
+
readonly hc: "#ff9999";
|
|
96
|
+
};
|
|
97
|
+
readonly band1: {
|
|
98
|
+
readonly light: "#ffffcb";
|
|
99
|
+
readonly dark: "#3a3500";
|
|
100
|
+
readonly hc: "#ffff88";
|
|
101
|
+
};
|
|
102
|
+
readonly band2: {
|
|
103
|
+
readonly light: "#c2f0ff";
|
|
104
|
+
readonly dark: "#003d4d";
|
|
105
|
+
readonly hc: "#66ccff";
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* DSM-matching maturity badge fills for the capability tree (L1–L5).
|
|
110
|
+
* Distinct from MATURITY_COLORS which drives the cards / compliance views.
|
|
111
|
+
*/
|
|
112
|
+
export declare const TREE_MATURITY_COLORS: {
|
|
113
|
+
readonly light: readonly ["#c1c1c1", "#ff3d67", "#ff9a59", "#fdef59", "#83cca3"];
|
|
114
|
+
readonly dark: readonly ["#555555", "#cc1040", "#d06020", "#c8c000", "#4d9970"];
|
|
115
|
+
readonly hc: readonly ["#cccccc", "#ff6699", "#ffbb77", "#ffff44", "#88ddaa"];
|
|
116
|
+
};
|
|
85
117
|
/**
|
|
86
118
|
* Typography hierarchy. All previews resolve text styling from these roles
|
|
87
119
|
* — never from inline font-* attributes on individual <text> elements.
|
|
@@ -160,5 +192,13 @@ export declare const CSS_VAR: {
|
|
|
160
192
|
readonly maturity3: "--ts-maturity-3";
|
|
161
193
|
readonly maturity4: "--ts-maturity-4";
|
|
162
194
|
readonly maturity5: "--ts-maturity-5";
|
|
195
|
+
readonly treeLevel0: "--ts-tree-level-0";
|
|
196
|
+
readonly treeLevel1: "--ts-tree-level-1";
|
|
197
|
+
readonly treeLevel2: "--ts-tree-level-2";
|
|
198
|
+
readonly treeMaturity1: "--ts-tree-maturity-1";
|
|
199
|
+
readonly treeMaturity2: "--ts-tree-maturity-2";
|
|
200
|
+
readonly treeMaturity3: "--ts-tree-maturity-3";
|
|
201
|
+
readonly treeMaturity4: "--ts-tree-maturity-4";
|
|
202
|
+
readonly treeMaturity5: "--ts-tree-maturity-5";
|
|
163
203
|
};
|
|
164
204
|
export declare function getBaseResetCss(): string;
|
package/dist/theme/tokens.js
CHANGED
|
@@ -42,6 +42,26 @@ export const MATURITY_COLORS = {
|
|
|
42
42
|
dark: ['#7f1d1d', '#92400e', '#854d0e', '#3f6212', '#14532d'],
|
|
43
43
|
hc: ['#f87171', '#fb923c', '#facc15', '#a3e635', '#4ade80'],
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* DSM-matching capability tree node fill colors, grouped by depth band.
|
|
47
|
+
* band0: depth 0–2 (pink)
|
|
48
|
+
* band1: depth 3–4 (yellow)
|
|
49
|
+
* band2: depth 5+ (blue)
|
|
50
|
+
*/
|
|
51
|
+
export const TREE_LEVEL_COLORS = {
|
|
52
|
+
band0: { light: '#fee0e0', dark: '#3d0f0f', hc: '#ff9999' },
|
|
53
|
+
band1: { light: '#ffffcb', dark: '#3a3500', hc: '#ffff88' },
|
|
54
|
+
band2: { light: '#c2f0ff', dark: '#003d4d', hc: '#66ccff' },
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* DSM-matching maturity badge fills for the capability tree (L1–L5).
|
|
58
|
+
* Distinct from MATURITY_COLORS which drives the cards / compliance views.
|
|
59
|
+
*/
|
|
60
|
+
export const TREE_MATURITY_COLORS = {
|
|
61
|
+
light: ['#c1c1c1', '#ff3d67', '#ff9a59', '#fdef59', '#83cca3'],
|
|
62
|
+
dark: ['#555555', '#cc1040', '#d06020', '#c8c000', '#4d9970'],
|
|
63
|
+
hc: ['#cccccc', '#ff6699', '#ffbb77', '#ffff44', '#88ddaa'],
|
|
64
|
+
};
|
|
45
65
|
/**
|
|
46
66
|
* Typography hierarchy. All previews resolve text styling from these roles
|
|
47
67
|
* — never from inline font-* attributes on individual <text> elements.
|
|
@@ -101,6 +121,14 @@ export const CSS_VAR = {
|
|
|
101
121
|
maturity3: '--ts-maturity-3',
|
|
102
122
|
maturity4: '--ts-maturity-4',
|
|
103
123
|
maturity5: '--ts-maturity-5',
|
|
124
|
+
treeLevel0: '--ts-tree-level-0',
|
|
125
|
+
treeLevel1: '--ts-tree-level-1',
|
|
126
|
+
treeLevel2: '--ts-tree-level-2',
|
|
127
|
+
treeMaturity1: '--ts-tree-maturity-1',
|
|
128
|
+
treeMaturity2: '--ts-tree-maturity-2',
|
|
129
|
+
treeMaturity3: '--ts-tree-maturity-3',
|
|
130
|
+
treeMaturity4: '--ts-tree-maturity-4',
|
|
131
|
+
treeMaturity5: '--ts-tree-maturity-5',
|
|
104
132
|
};
|
|
105
133
|
export function getBaseResetCss() {
|
|
106
134
|
return `*,*:before,*:after{box-sizing:border-box;}html,body{margin:0;padding:0;}`;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* CROSS-DOCUMENT RESOLUTION CAVEAT
|
|
13
13
|
* The Activity Card is the only MULTI-DOCUMENT Studio notation: the card YAML
|
|
14
14
|
* names a project Activity, and the project's name, dates, motivation chain
|
|
15
|
-
* (Drivers →
|
|
15
|
+
* (Drivers → Goals → Changes) and child activities are pulled BY
|
|
16
16
|
* REFERENCE from the canonical element + relation store (`canon/elements/**`,
|
|
17
17
|
* `canon/relations/**`; see `../activity-card/resolver.ts`). That resolution
|
|
18
18
|
* requires reading those files from disk, which is NOT available inside the
|
|
@@ -108,10 +108,9 @@ export function renderActivityCardBody(layout, ox, oy) {
|
|
|
108
108
|
parts.push(`<text class="text-primary" x="${r.x + ox + 12}" y="${r.y + oy + 44 + i * 18}" style="dominant-baseline:central">${escXml(line)}</text>`);
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
-
// Chain sections (Drivers →
|
|
111
|
+
// Chain sections (Drivers → Goals → Changes).
|
|
112
112
|
const SECTION_LEVEL = {
|
|
113
113
|
drivers: 4,
|
|
114
|
-
assessments: 5,
|
|
115
114
|
goals: 5,
|
|
116
115
|
changes: 6,
|
|
117
116
|
};
|
|
@@ -20,7 +20,7 @@ const COL_LABELS = {
|
|
|
20
20
|
driver: 'Drivers (D)',
|
|
21
21
|
goal: 'Goals (G)',
|
|
22
22
|
change: 'Changes (C)',
|
|
23
|
-
activity: '
|
|
23
|
+
activity: 'Actions (A)',
|
|
24
24
|
};
|
|
25
25
|
/**
|
|
26
26
|
* Canonical FGCA/FGA body — the column headers, node rects and edge paths that
|
|
@@ -67,7 +67,7 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature)
|
|
|
67
67
|
`<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${NODE_W}" height="${NODE_H}" rx="8"/>`,
|
|
68
68
|
`<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
|
|
69
69
|
twoLines
|
|
70
|
-
? `<text class="text-
|
|
70
|
+
? `<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>`
|
|
71
71
|
: '',
|
|
72
72
|
`<text class="text-id" x="${n.x + NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(entityId)}</text>`,
|
|
73
73
|
]
|
|
@@ -15,12 +15,11 @@ import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js
|
|
|
15
15
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
16
16
|
import { escXml } from './render-util.js';
|
|
17
17
|
const NODE_W = 250;
|
|
18
|
-
const NODE_H =
|
|
18
|
+
const NODE_H = 72;
|
|
19
19
|
const RANK_SEP = 100;
|
|
20
20
|
const NODE_SEP = 24;
|
|
21
21
|
const PAD = 24;
|
|
22
|
-
const
|
|
23
|
-
const LABEL_TRIM = 34;
|
|
22
|
+
const LABEL_CHARS = 30;
|
|
24
23
|
/**
|
|
25
24
|
* Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
|
|
26
25
|
* spacing, then delegates the actual SVG emission to {@link renderGoalsLayoutSvg}
|
|
@@ -81,10 +80,34 @@ export function renderGoalsLayoutSvg(layout, options = {}) {
|
|
|
81
80
|
const y = n.y + oy;
|
|
82
81
|
const level = n.data.level % 8;
|
|
83
82
|
const labelText = n.data.name ?? String(n.id);
|
|
84
|
-
const
|
|
83
|
+
const words = labelText.split(' ');
|
|
84
|
+
let line1 = '';
|
|
85
|
+
let line2 = '';
|
|
86
|
+
for (const w of words) {
|
|
87
|
+
if ((line1 + ' ' + w).trim().length <= LABEL_CHARS) {
|
|
88
|
+
line1 = (line1 + ' ' + w).trim();
|
|
89
|
+
}
|
|
90
|
+
else if ((line2 + ' ' + w).trim().length <= LABEL_CHARS) {
|
|
91
|
+
line2 = (line2 + ' ' + w).trim();
|
|
92
|
+
}
|
|
93
|
+
else if (!line2) {
|
|
94
|
+
line2 = w.slice(0, LABEL_CHARS - 2) + '…';
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const twoLines = line2.length > 0;
|
|
99
|
+
const nameY1 = twoLines ? y + 12 : y + 16;
|
|
100
|
+
const nameY2 = y + 26;
|
|
101
|
+
const typeY = twoLines ? y + 43 : y + 35;
|
|
102
|
+
const idY = twoLines ? y + 59 : y + 55;
|
|
103
|
+
const typeLabel = n.data.type ?? '';
|
|
104
|
+
const idText = String(n.data.id);
|
|
85
105
|
return `<g>
|
|
86
106
|
<rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>
|
|
87
|
-
<text class="text-primary" x="${x + n.width / 2}" y="${
|
|
107
|
+
<text class="text-primary" x="${x + n.width / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>${twoLines ? `
|
|
108
|
+
<text class="text-primary" x="${x + n.width / 2}" y="${nameY2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>` : ''}${typeLabel ? `
|
|
109
|
+
<text class="text-secondary" x="${x + n.width / 2}" y="${typeY}" text-anchor="middle" dominant-baseline="central">${escXml(typeLabel)}</text>` : ''}
|
|
110
|
+
<text class="text-id" x="${x + n.width / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(idText)}</text>
|
|
88
111
|
</g>`;
|
|
89
112
|
})
|
|
90
113
|
.join('\n');
|