@transitrix/diagrams 1.1.0 → 1.2.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__/resolver.test.js +1 -1
- package/dist/activity-card/__tests__/validate.test.js +23 -1
- package/dist/activity-card/resolver.js +23 -21
- 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__/validate.test.js +2 -2
- package/dist/fgca/parse-canonical.js +9 -3
- 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-fgca.js +1 -1
- 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__/resolver.test.ts +1 -1
- package/src/activity-card/__tests__/validate.test.ts +25 -1
- package/src/activity-card/resolver.ts +23 -21
- 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__/validate.test.ts +2 -2
- package/src/fgca/parse-canonical.ts +9 -3
- 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-fgca.ts +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { CapabilityNode, CapabilityMapHeader } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const TREE_NODE_WIDTH = 250;
|
|
4
|
+
export const TREE_NODE_HEIGHT = 64;
|
|
5
|
+
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
6
|
+
export const TREE_RANK_SEP = 80;
|
|
7
|
+
/** Vertical gap between sibling nodes. */
|
|
8
|
+
export const TREE_NODE_SEP = 24;
|
|
9
|
+
|
|
10
|
+
export interface CapabilityTreeNode {
|
|
11
|
+
id: string;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
data: CapabilityNode;
|
|
17
|
+
depth: number;
|
|
18
|
+
hasChildren: boolean;
|
|
19
|
+
isCollapsed: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CapabilityTreeEdge {
|
|
23
|
+
source: string;
|
|
24
|
+
target: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CapabilityTreeLevelCounts {
|
|
28
|
+
/** Nodes at depth 0–2 (pink band). */
|
|
29
|
+
band0: number;
|
|
30
|
+
/** Nodes at depth 3–4 (yellow band). */
|
|
31
|
+
band1: number;
|
|
32
|
+
/** Nodes at depth 5+ (blue band). */
|
|
33
|
+
band2: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CapabilityTreeLayout {
|
|
37
|
+
nodes: CapabilityTreeNode[];
|
|
38
|
+
edges: CapabilityTreeEdge[];
|
|
39
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
40
|
+
levelCounts: CapabilityTreeLevelCounts;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function layoutCapabilityTree(
|
|
44
|
+
map: CapabilityMapHeader,
|
|
45
|
+
collapsedIds: Set<string> = new Set(),
|
|
46
|
+
): CapabilityTreeLayout {
|
|
47
|
+
const nodes: CapabilityTreeNode[] = [];
|
|
48
|
+
const edges: CapabilityTreeEdge[] = [];
|
|
49
|
+
|
|
50
|
+
const colNextY = new Map<number, number>();
|
|
51
|
+
function getNextY(col: number): number {
|
|
52
|
+
return colNextY.get(col) ?? 0;
|
|
53
|
+
}
|
|
54
|
+
function advanceY(col: number, delta: number): void {
|
|
55
|
+
colNextY.set(col, (colNextY.get(col) ?? 0) + delta);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function placeNode(node: CapabilityNode, depth: number): { top: number; bottom: number } {
|
|
59
|
+
const isCollapsed = collapsedIds.has(node.id);
|
|
60
|
+
const hasChildren = Boolean(node.children?.length);
|
|
61
|
+
const visibleChildren = !isCollapsed && node.children?.length ? node.children : [];
|
|
62
|
+
|
|
63
|
+
const x = depth * (TREE_NODE_WIDTH + TREE_RANK_SEP);
|
|
64
|
+
|
|
65
|
+
if (visibleChildren.length === 0) {
|
|
66
|
+
const y = getNextY(depth);
|
|
67
|
+
advanceY(depth, TREE_NODE_HEIGHT + TREE_NODE_SEP);
|
|
68
|
+
nodes.push({ id: node.id, x, y, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
|
|
69
|
+
return { top: y, bottom: y + TREE_NODE_HEIGHT };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const childSpans: Array<{ top: number; bottom: number }> = [];
|
|
73
|
+
for (const child of visibleChildren) {
|
|
74
|
+
childSpans.push(placeNode(child, depth + 1));
|
|
75
|
+
edges.push({ source: node.id, target: child.id });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const spanTop = Math.min(...childSpans.map(s => s.top));
|
|
79
|
+
const spanBottom = Math.max(...childSpans.map(s => s.bottom));
|
|
80
|
+
const parentY = spanTop + (spanBottom - spanTop) / 2 - TREE_NODE_HEIGHT / 2;
|
|
81
|
+
|
|
82
|
+
const curColY = getNextY(depth);
|
|
83
|
+
const finalY = Math.max(parentY, curColY);
|
|
84
|
+
advanceY(depth, finalY - curColY + TREE_NODE_HEIGHT + TREE_NODE_SEP);
|
|
85
|
+
|
|
86
|
+
nodes.push({ id: node.id, x, y: finalY, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
|
|
87
|
+
return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + TREE_NODE_HEIGHT, spanBottom) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const root of map.capabilities) {
|
|
91
|
+
placeNode(root, 0);
|
|
92
|
+
for (const [col, y] of colNextY) {
|
|
93
|
+
colNextY.set(col, y + TREE_NODE_SEP);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (nodes.length === 0) {
|
|
98
|
+
return { nodes: [], edges: [], bounds: { x: 0, y: 0, width: 0, height: 0 }, levelCounts: { band0: 0, band1: 0, band2: 0 } };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const minX = Math.min(...nodes.map(n => n.x));
|
|
102
|
+
const minY = Math.min(...nodes.map(n => n.y));
|
|
103
|
+
const maxX = Math.max(...nodes.map(n => n.x + n.width));
|
|
104
|
+
const maxY = Math.max(...nodes.map(n => n.y + n.height));
|
|
105
|
+
|
|
106
|
+
const levelCounts: CapabilityTreeLevelCounts = { band0: 0, band1: 0, band2: 0 };
|
|
107
|
+
for (const n of nodes) {
|
|
108
|
+
if (n.depth <= 2) levelCounts.band0++;
|
|
109
|
+
else if (n.depth <= 4) levelCounts.band1++;
|
|
110
|
+
else levelCounts.band2++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { nodes, edges, bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }, levelCounts };
|
|
114
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { CapabilityMapHeader } from './types.js';
|
|
2
|
+
import { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_NODE_SEP } from './layout-tree.js';
|
|
3
|
+
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
4
|
+
import { escXml } from '../webview/render-util.js';
|
|
5
|
+
|
|
6
|
+
const PAD = 24;
|
|
7
|
+
const BTN_R = 8;
|
|
8
|
+
const LEGEND_H = 40;
|
|
9
|
+
const LABEL_MAX_CHARS = 28;
|
|
10
|
+
const LABEL_TRIM_CHARS = 26;
|
|
11
|
+
|
|
12
|
+
const BAND_LABELS = ['Levels 0–2', 'Levels 3–4', 'Levels 5+'] as const;
|
|
13
|
+
|
|
14
|
+
function depthBand(depth: number): 0 | 1 | 2 {
|
|
15
|
+
if (depth <= 2) return 0;
|
|
16
|
+
if (depth <= 4) return 1;
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function truncate(s: string, max: number, trim: number): string {
|
|
21
|
+
return s.length > max ? s.slice(0, trim) + '…' : s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RenderCapabilityTreeOptions {
|
|
25
|
+
collapsedIds?: Set<string>;
|
|
26
|
+
curvature?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Renders a capability-map as a left-to-right SVG node tree with DSM-matching
|
|
31
|
+
* visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
|
|
32
|
+
*
|
|
33
|
+
* The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
|
|
34
|
+
* that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
|
|
35
|
+
* in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
|
|
36
|
+
* for the webview click handler.
|
|
37
|
+
*/
|
|
38
|
+
export function renderCapabilityTreeSvg(
|
|
39
|
+
map: CapabilityMapHeader,
|
|
40
|
+
opts: RenderCapabilityTreeOptions = {},
|
|
41
|
+
): string {
|
|
42
|
+
const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE } = opts;
|
|
43
|
+
const layout = layoutCapabilityTree(map, collapsedIds);
|
|
44
|
+
|
|
45
|
+
if (layout.nodes.length === 0) {
|
|
46
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ox = -layout.bounds.x + PAD;
|
|
50
|
+
const oy = -layout.bounds.y + PAD;
|
|
51
|
+
const svgW = layout.bounds.width + PAD * 2 + BTN_R;
|
|
52
|
+
const svgH = layout.bounds.height + PAD * 2 + LEGEND_H + TREE_NODE_SEP;
|
|
53
|
+
|
|
54
|
+
const nodeMap = new Map(layout.nodes.map(n => [n.id, n]));
|
|
55
|
+
|
|
56
|
+
const edgeSvg = layout.edges.map(e => {
|
|
57
|
+
const s = nodeMap.get(e.source);
|
|
58
|
+
const t = nodeMap.get(e.target);
|
|
59
|
+
if (!s || !t) return '';
|
|
60
|
+
const sx = s.x + ox + s.width;
|
|
61
|
+
const sy = s.y + oy + s.height / 2;
|
|
62
|
+
const tx = t.x + ox;
|
|
63
|
+
const ty = t.y + oy + t.height / 2;
|
|
64
|
+
return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature)}" class="diagram-edge"/>`;
|
|
65
|
+
}).filter(Boolean).join('\n');
|
|
66
|
+
|
|
67
|
+
const nodeSvg = layout.nodes.map(n => {
|
|
68
|
+
const x = n.x + ox;
|
|
69
|
+
const y = n.y + oy;
|
|
70
|
+
const band = depthBand(n.depth);
|
|
71
|
+
const mat = Math.max(1, Math.min(5, n.data.current_maturity | 0));
|
|
72
|
+
|
|
73
|
+
const nameText = truncate(n.data.name, LABEL_MAX_CHARS, LABEL_TRIM_CHARS);
|
|
74
|
+
const nameTitle = n.data.name.length > LABEL_MAX_CHARS ? escXml(n.data.name) : '';
|
|
75
|
+
|
|
76
|
+
// Maturity badge: left side of node
|
|
77
|
+
const badgeW = 28;
|
|
78
|
+
const badgeH = 18;
|
|
79
|
+
const badgeX = x + 10;
|
|
80
|
+
const badgeY = y + (TREE_NODE_HEIGHT - badgeH) / 2;
|
|
81
|
+
|
|
82
|
+
// Text area: right of badge
|
|
83
|
+
const textX = badgeX + badgeW + 8;
|
|
84
|
+
const nameY = y + TREE_NODE_HEIGHT / 2 - 8;
|
|
85
|
+
const idY = y + TREE_NODE_HEIGHT / 2 + 9;
|
|
86
|
+
|
|
87
|
+
// Collapse button: centered on right edge of node
|
|
88
|
+
const btnCx = x + TREE_NODE_WIDTH;
|
|
89
|
+
const btnCy = y + TREE_NODE_HEIGHT / 2;
|
|
90
|
+
|
|
91
|
+
const collapseBtn = n.hasChildren
|
|
92
|
+
? `<circle class="tree-collapse-btn" cx="${btnCx}" cy="${btnCy}" r="${BTN_R}" data-node-id="${escXml(n.id)}" data-collapsed="${n.isCollapsed}"/>
|
|
93
|
+
<text class="tree-collapse-lbl" x="${btnCx}" y="${btnCy}" text-anchor="middle">${n.isCollapsed ? '+' : '−'}</text>`
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
|
|
97
|
+
<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"/>
|
|
98
|
+
<rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
|
|
99
|
+
<text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
|
|
100
|
+
<text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
|
|
101
|
+
<text class="text-id" x="${textX}" y="${idY}" dominant-baseline="central">${escXml(n.id)}</text>
|
|
102
|
+
${collapseBtn}
|
|
103
|
+
</g>`;
|
|
104
|
+
}).join('\n');
|
|
105
|
+
|
|
106
|
+
// Legend band: proportional coloured blocks per level-count band
|
|
107
|
+
const legendY = layout.bounds.height + PAD * 2 + TREE_NODE_SEP / 2;
|
|
108
|
+
const legendW = svgW - PAD * 2;
|
|
109
|
+
const totalNodes = layout.nodes.length || 1;
|
|
110
|
+
const bandCounts = [layout.levelCounts.band0, layout.levelCounts.band1, layout.levelCounts.band2];
|
|
111
|
+
let legendX = PAD;
|
|
112
|
+
const legendSvg = bandCounts.map((count, i) => {
|
|
113
|
+
const bw = Math.max(1, Math.round(legendW * count / totalNodes));
|
|
114
|
+
const bx = legendX;
|
|
115
|
+
legendX += bw;
|
|
116
|
+
if (count === 0) return '';
|
|
117
|
+
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"/>
|
|
118
|
+
<text class="text-id" x="${bx + bw / 2}" y="${legendY + 12}" text-anchor="middle" dominant-baseline="central">${escXml(BAND_LABELS[i])}</text>`;
|
|
119
|
+
}).filter(Boolean).join('\n');
|
|
120
|
+
|
|
121
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">
|
|
122
|
+
<defs>
|
|
123
|
+
<marker id="cap-tree-arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
124
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
125
|
+
</marker>
|
|
126
|
+
</defs>
|
|
127
|
+
${edgeSvg}
|
|
128
|
+
${nodeSvg}
|
|
129
|
+
${legendSvg}
|
|
130
|
+
</svg>`;
|
|
131
|
+
}
|
|
@@ -56,11 +56,11 @@ describe('validateFGCADoc', () => {
|
|
|
56
56
|
expect(result.errors.some(e => e.path === 'changes')).toBe(true);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
it('errors when
|
|
59
|
+
it('errors when actions array is missing', () => {
|
|
60
60
|
const { activities: _, ...without } = VALID_DOC;
|
|
61
61
|
const result = validateFGCADoc({ ...without, notation: 'fgca' });
|
|
62
62
|
expect(result.valid).toBe(false);
|
|
63
|
-
expect(result.errors.some(e => e.path === '
|
|
63
|
+
expect(result.errors.some(e => e.path === 'actions')).toBe(true);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it('errors on empty factor name', () => {
|
|
@@ -122,12 +122,16 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
122
122
|
const factorsRaw = asObjectArray(raw['factors']);
|
|
123
123
|
const goalsRaw = asObjectArray(raw['goals']);
|
|
124
124
|
const changesRaw = asObjectArray(raw['changes']);
|
|
125
|
-
|
|
125
|
+
// Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
|
|
126
|
+
const activitiesRaw = asObjectArray(raw['actions']) ?? asObjectArray(raw['activities']);
|
|
127
|
+
if ('activities' in raw && !('actions' in raw)) {
|
|
128
|
+
warnings.push({ code: 'FGCA-001', message: '"activities" key is deprecated — rename to "actions"' });
|
|
129
|
+
}
|
|
126
130
|
|
|
127
131
|
if (factorsRaw === null) errors.push({ code: 'FGCA-004', message: 'factors must be an array', path: 'factors' });
|
|
128
132
|
if (goalsRaw === null) errors.push({ code: 'FGCA-004', message: 'goals must be an array', path: 'goals' });
|
|
129
133
|
if (changesRaw === null) errors.push({ code: 'FGCA-004', message: 'changes must be an array', path: 'changes' });
|
|
130
|
-
if (activitiesRaw === null) errors.push({ code: 'FGCA-004', message: '
|
|
134
|
+
if (activitiesRaw === null) errors.push({ code: 'FGCA-004', message: 'actions must be an array', path: 'actions' });
|
|
131
135
|
|
|
132
136
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
133
137
|
|
|
@@ -288,11 +292,13 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
288
292
|
});
|
|
289
293
|
|
|
290
294
|
const internalActivities: ActivityItem[] = activities.map((el, i) => {
|
|
291
|
-
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `
|
|
295
|
+
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]`);
|
|
296
|
+
const typeVal = typeof el!['type'] === 'string' ? el!['type'] : undefined;
|
|
292
297
|
return {
|
|
293
298
|
id: intId(String(el!['id'])),
|
|
294
299
|
name: String(el!['name'] ?? ''),
|
|
295
300
|
goal_id: goalRefs.length > 0 ? intId(goalRefs[0]) : null,
|
|
301
|
+
...(typeVal !== undefined ? { type: typeVal } : {}),
|
|
296
302
|
};
|
|
297
303
|
});
|
|
298
304
|
|
package/src/fgca/types.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface ActivityItem {
|
|
|
23
23
|
name: string;
|
|
24
24
|
goal_id?: number | string | null;
|
|
25
25
|
activity_type_id?: number;
|
|
26
|
+
/** Canonical values: initiative | programme | project | work_package. Unknown values pass through without error. */
|
|
27
|
+
type?: string;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface ActivityTypeItem {
|
package/src/fgca/validate.ts
CHANGED
|
@@ -46,9 +46,17 @@ export function validateFGCADoc(input: unknown): FGCAValidationResult {
|
|
|
46
46
|
if (!Array.isArray(raw.factors)) errors.push({ code: 'SCHEMA_INVALID', message: 'factors must be an array', path: 'factors' });
|
|
47
47
|
if (!Array.isArray(raw.goals)) errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
|
|
48
48
|
if (!Array.isArray(raw.changes)) errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
|
|
49
|
-
|
|
49
|
+
// Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
|
|
50
|
+
const hasActions = Array.isArray(raw.actions);
|
|
51
|
+
const hasActivities = Array.isArray(raw.activities);
|
|
52
|
+
if (!hasActions && !hasActivities) {
|
|
53
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
54
|
+
} else if ('activities' in raw) {
|
|
55
|
+
warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
|
|
56
|
+
}
|
|
50
57
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
51
58
|
|
|
59
|
+
const actionsArr = (hasActions ? raw.actions : raw.activities) as unknown[];
|
|
52
60
|
const doc = raw as unknown as FGCADoc;
|
|
53
61
|
const collectIds = (arr: unknown[]): Set<number | string> => {
|
|
54
62
|
const out = new Set<number | string>();
|
|
@@ -62,7 +70,7 @@ export function validateFGCADoc(input: unknown): FGCAValidationResult {
|
|
|
62
70
|
};
|
|
63
71
|
const factorIds = collectIds(doc.factors as unknown as unknown[]);
|
|
64
72
|
const goalIds = collectIds(doc.goals as unknown as unknown[]);
|
|
65
|
-
const activityIds = collectIds(
|
|
73
|
+
const activityIds = collectIds(actionsArr);
|
|
66
74
|
|
|
67
75
|
for (let i = 0; i < doc.factors.length; i++) {
|
|
68
76
|
const f = doc.factors[i] as unknown;
|
|
@@ -114,15 +122,15 @@ export function validateFGCADoc(input: unknown): FGCAValidationResult {
|
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
for (let i = 0; i <
|
|
118
|
-
const a =
|
|
125
|
+
for (let i = 0; i < actionsArr.length; i++) {
|
|
126
|
+
const a = actionsArr[i];
|
|
119
127
|
if (!a || typeof a !== 'object') {
|
|
120
|
-
errors.push({ code: 'SCHEMA_INVALID', message: '
|
|
128
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'action entry must be an object', path: `actions[${i}]` });
|
|
121
129
|
continue;
|
|
122
130
|
}
|
|
123
|
-
const
|
|
124
|
-
if (typeof
|
|
125
|
-
if (typeof
|
|
131
|
+
const action = a as { id?: unknown; name?: unknown };
|
|
132
|
+
if (typeof action.id !== 'number' && typeof action.id !== 'string') errors.push({ code: 'SCHEMA_INVALID', message: 'action id must be a number or string', path: `actions[${i}]` });
|
|
133
|
+
if (typeof action.name !== 'string' || !action.name.trim()) errors.push({ code: 'EMPTY_NAME', message: `Action ${String(action.id)} has empty name`, path: `actions[${i}]` });
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
return { valid: errors.length === 0, errors, warnings };
|
package/src/theme/index.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/src/theme/themes.ts
CHANGED
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
LAYER_COLORS,
|
|
3
3
|
LEVEL_COLORS,
|
|
4
4
|
MATURITY_COLORS,
|
|
5
|
+
TREE_LEVEL_COLORS,
|
|
6
|
+
TREE_MATURITY_COLORS,
|
|
5
7
|
STRUCTURAL,
|
|
6
8
|
TYPOGRAPHY,
|
|
7
9
|
CSS_VAR,
|
|
@@ -75,7 +77,19 @@ function diagramVars(variant: 'light' | 'dark' | 'hc'): string {
|
|
|
75
77
|
const sc = STRUCTURAL;
|
|
76
78
|
const lv = LEVEL_COLORS[variant];
|
|
77
79
|
const mat = MATURITY_COLORS[variant];
|
|
80
|
+
const treeLevel = TREE_LEVEL_COLORS;
|
|
81
|
+
const treeMat = TREE_MATURITY_COLORS[variant];
|
|
78
82
|
const levelVars = lv.map((c, i) => `--ts-level-${i}:${c}`).join(';') + ';';
|
|
83
|
+
const treeVars = [
|
|
84
|
+
`${CSS_VAR.treeLevel0}:${treeLevel.band0[variant]}`,
|
|
85
|
+
`${CSS_VAR.treeLevel1}:${treeLevel.band1[variant]}`,
|
|
86
|
+
`${CSS_VAR.treeLevel2}:${treeLevel.band2[variant]}`,
|
|
87
|
+
`${CSS_VAR.treeMaturity1}:${treeMat[0]}`,
|
|
88
|
+
`${CSS_VAR.treeMaturity2}:${treeMat[1]}`,
|
|
89
|
+
`${CSS_VAR.treeMaturity3}:${treeMat[2]}`,
|
|
90
|
+
`${CSS_VAR.treeMaturity4}:${treeMat[3]}`,
|
|
91
|
+
`${CSS_VAR.treeMaturity5}:${treeMat[4]}`,
|
|
92
|
+
].join(';') + ';';
|
|
79
93
|
return [
|
|
80
94
|
`${CSS_VAR.layerDriver}:${lc.driver[variant]}`,
|
|
81
95
|
`${CSS_VAR.layerFactor}:${lc.factor[variant]}`,
|
|
@@ -92,7 +106,7 @@ function diagramVars(variant: 'light' | 'dark' | 'hc'): string {
|
|
|
92
106
|
`${CSS_VAR.maturity3}:${mat[2]}`,
|
|
93
107
|
`${CSS_VAR.maturity4}:${mat[3]}`,
|
|
94
108
|
`${CSS_VAR.maturity5}:${mat[4]}`,
|
|
95
|
-
].join(';') + ';' + levelVars;
|
|
109
|
+
].join(';') + ';' + levelVars + treeVars;
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
/** CSS for the webview shell (toolbar, canvas). */
|
|
@@ -155,7 +169,18 @@ ${levelRules}
|
|
|
155
169
|
.compliance-deadline{fill:var(${cv.statusErrorBg});stroke:var(${cv.statusErrorFg});}
|
|
156
170
|
.compliance-deadline text{fill:var(${cv.statusErrorFg});}
|
|
157
171
|
.compliance-badge{fill:var(${cv.statusErrorFg});}
|
|
158
|
-
.compliance-badge-text{fill:var(${cv.textInverse});font-family:${t.fontFamily};font-size:8px;font-weight:700;}
|
|
172
|
+
.compliance-badge-text{fill:var(${cv.textInverse});font-family:${t.fontFamily};font-size:8px;font-weight:700;}
|
|
173
|
+
.tree-level-0{fill:var(${cv.treeLevel0});}
|
|
174
|
+
.tree-level-1{fill:var(${cv.treeLevel1});}
|
|
175
|
+
.tree-level-2{fill:var(${cv.treeLevel2});}
|
|
176
|
+
.tree-maturity-1{fill:var(${cv.treeMaturity1});}
|
|
177
|
+
.tree-maturity-2{fill:var(${cv.treeMaturity2});}
|
|
178
|
+
.tree-maturity-3{fill:var(${cv.treeMaturity3});}
|
|
179
|
+
.tree-maturity-4{fill:var(${cv.treeMaturity4});}
|
|
180
|
+
.tree-maturity-5{fill:var(${cv.treeMaturity5});}
|
|
181
|
+
.tree-collapse-btn{cursor:pointer;fill:white;stroke:var(${cv.nodeStroke});stroke-width:1.5;}
|
|
182
|
+
.tree-collapse-btn:hover{fill:var(${cv.bgElevated});}
|
|
183
|
+
.tree-collapse-lbl{fill:var(${cv.textSecondary});font-family:${t.fontFamily};font-size:12px;font-weight:700;dominant-baseline:central;pointer-events:none;}`;
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
/**
|
package/src/theme/tokens.ts
CHANGED
|
@@ -48,6 +48,28 @@ export const MATURITY_COLORS = {
|
|
|
48
48
|
hc: ['#f87171', '#fb923c', '#facc15', '#a3e635', '#4ade80'],
|
|
49
49
|
} as const;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* DSM-matching capability tree node fill colors, grouped by depth band.
|
|
53
|
+
* band0: depth 0–2 (pink)
|
|
54
|
+
* band1: depth 3–4 (yellow)
|
|
55
|
+
* band2: depth 5+ (blue)
|
|
56
|
+
*/
|
|
57
|
+
export const TREE_LEVEL_COLORS = {
|
|
58
|
+
band0: { light: '#fee0e0', dark: '#3d0f0f', hc: '#ff9999' },
|
|
59
|
+
band1: { light: '#ffffcb', dark: '#3a3500', hc: '#ffff88' },
|
|
60
|
+
band2: { light: '#c2f0ff', dark: '#003d4d', hc: '#66ccff' },
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* DSM-matching maturity badge fills for the capability tree (L1–L5).
|
|
65
|
+
* Distinct from MATURITY_COLORS which drives the cards / compliance views.
|
|
66
|
+
*/
|
|
67
|
+
export const TREE_MATURITY_COLORS = {
|
|
68
|
+
light: ['#c1c1c1', '#ff3d67', '#ff9a59', '#fdef59', '#83cca3'],
|
|
69
|
+
dark: ['#555555', '#cc1040', '#d06020', '#c8c000', '#4d9970'],
|
|
70
|
+
hc: ['#cccccc', '#ff6699', '#ffbb77', '#ffff44', '#88ddaa'],
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
51
73
|
/**
|
|
52
74
|
* Typography hierarchy. All previews resolve text styling from these roles
|
|
53
75
|
* — never from inline font-* attributes on individual <text> elements.
|
|
@@ -128,6 +150,14 @@ export const CSS_VAR = {
|
|
|
128
150
|
maturity3: '--ts-maturity-3',
|
|
129
151
|
maturity4: '--ts-maturity-4',
|
|
130
152
|
maturity5: '--ts-maturity-5',
|
|
153
|
+
treeLevel0: '--ts-tree-level-0',
|
|
154
|
+
treeLevel1: '--ts-tree-level-1',
|
|
155
|
+
treeLevel2: '--ts-tree-level-2',
|
|
156
|
+
treeMaturity1: '--ts-tree-maturity-1',
|
|
157
|
+
treeMaturity2: '--ts-tree-maturity-2',
|
|
158
|
+
treeMaturity3: '--ts-tree-maturity-3',
|
|
159
|
+
treeMaturity4: '--ts-tree-maturity-4',
|
|
160
|
+
treeMaturity5: '--ts-tree-maturity-5',
|
|
131
161
|
} as const;
|
|
132
162
|
|
|
133
163
|
export function getBaseResetCss(): string {
|