@transitrix/diagrams 1.7.6 → 1.8.2
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/__tests__/node-size-presets.test.d.ts +1 -0
- package/dist/__tests__/node-size-presets.test.js +30 -0
- package/dist/activities/layout.js +7 -7
- package/dist/activities/types.d.ts +4 -0
- package/dist/blocks/__tests__/layout.test.js +2 -2
- package/dist/blocks/layout.js +3 -2
- package/dist/capability-map/dsm-layout.js +1 -1
- package/dist/capability-map/dsm-schema.d.ts +1 -1
- package/dist/capability-map/dsm-schema.js +1 -1
- package/dist/capability-map/layout-tree.d.ts +6 -2
- package/dist/capability-map/layout-tree.js +12 -10
- package/dist/capability-map/render-capability-tree.d.ts +2 -9
- package/dist/capability-map/render-capability-tree.js +16 -28
- package/dist/fgca/preview-layout.d.ts +10 -6
- package/dist/fgca/preview-layout.js +16 -15
- package/dist/node-size-presets.d.ts +63 -0
- package/dist/node-size-presets.js +117 -0
- package/dist/process-blueprint/layout.js +4 -47
- package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
- package/dist/webview/__tests__/entity-text-layout.test.js +60 -0
- package/dist/webview/__tests__/entry.test.js +3 -3
- package/dist/webview/__tests__/render-blocks.test.js +10 -13
- package/dist/webview/__tests__/render-goals.test.js +41 -0
- package/dist/webview/entity-text-layout.d.ts +95 -0
- package/dist/webview/entity-text-layout.js +277 -0
- package/dist/webview/entry.js +7 -3
- package/dist/webview/notation-style.d.ts +1 -1
- package/dist/webview/notation-style.js +1 -1
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +29 -32
- package/dist/webview/render-blocks.d.ts +4 -1
- package/dist/webview/render-blocks.js +27 -108
- package/dist/webview/render-fgca.d.ts +15 -8
- package/dist/webview/render-fgca.js +41 -44
- package/dist/webview/render-goals.d.ts +2 -0
- package/dist/webview/render-goals.js +21 -36
- package/dist/webview/render-process-blueprint.d.ts +4 -10
- package/dist/webview/render-process-blueprint.js +18 -64
- package/package.json +1 -1
- package/src/__tests__/node-size-presets.test.ts +50 -0
- package/src/activities/layout.ts +12 -7
- package/src/activities/types.ts +4 -0
- package/src/blocks/__tests__/layout.test.ts +2 -2
- package/src/blocks/layout.ts +3 -2
- package/src/capability-map/dsm-layout.ts +1 -1
- package/src/capability-map/dsm-schema.ts +1 -1
- package/src/capability-map/index.ts +1 -1
- package/src/capability-map/layout-tree.ts +17 -9
- package/src/capability-map/render-capability-tree.ts +18 -30
- package/src/fgca/preview-layout.ts +24 -17
- package/src/node-size-presets.ts +180 -0
- package/src/process-blueprint/layout.ts +4 -48
- package/src/webview/__tests__/entity-text-layout.test.ts +73 -0
- package/src/webview/__tests__/entry.test.ts +3 -3
- package/src/webview/__tests__/render-blocks.test.ts +11 -14
- package/src/webview/__tests__/render-goals.test.ts +42 -0
- package/src/webview/entity-text-layout.ts +355 -0
- package/src/webview/entry.ts +8 -4
- package/src/webview/notation-style.ts +1 -1
- package/src/webview/render-activities.ts +38 -27
- package/src/webview/render-blocks.ts +35 -118
- package/src/webview/render-fgca.ts +59 -44
- package/src/webview/render-goals.ts +22 -34
- package/src/webview/render-process-blueprint.ts +30 -65
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block/cell size presets for diagram renderers.
|
|
3
|
+
*
|
|
4
|
+
* Entity nodes (Goals, DGCA/DGA, Blocks leaves, Activities, Capability Map)
|
|
5
|
+
* share one width × height ladder. Heights use fixed tiers (72 / 80 / 96 px)
|
|
6
|
+
* so name + type + id never collide; widths scale independently (200 / 250 / 320).
|
|
7
|
+
*
|
|
8
|
+
* Process Blueprint uses a separate cell grid, scaled from the same tiers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type NodeSizePreset = 'compact' | 'normal' | 'wide';
|
|
12
|
+
|
|
13
|
+
export const NODE_SIZE_PRESET_VALUES: readonly NodeSizePreset[] = ['compact', 'normal', 'wide'] as const;
|
|
14
|
+
|
|
15
|
+
export interface BoxDimensions {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BlocksLeafDimensions extends BoxDimensions {}
|
|
21
|
+
|
|
22
|
+
export interface ProcessBlueprintDimensions {
|
|
23
|
+
legendColumnWidth: number;
|
|
24
|
+
stageColumnWidth: number;
|
|
25
|
+
stageHeaderHeight: number;
|
|
26
|
+
goalRowHeight: number;
|
|
27
|
+
resultRowHeight: number;
|
|
28
|
+
aspectRowMinHeight: number;
|
|
29
|
+
pillHeight: number;
|
|
30
|
+
pillGap: number;
|
|
31
|
+
cellPadding: number;
|
|
32
|
+
textLineHeight: number;
|
|
33
|
+
textCharWidth: number;
|
|
34
|
+
cellTextPadX: number;
|
|
35
|
+
cellTextPadY: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CapabilityMapDimensions {
|
|
39
|
+
nodeWidth: number;
|
|
40
|
+
nodeHeight: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type PresetTable<T> = Record<NodeSizePreset, T>;
|
|
44
|
+
|
|
45
|
+
/** Shared entity-node width tiers (px). */
|
|
46
|
+
export const ENTITY_NODE_WIDTH: Record<NodeSizePreset, number> = {
|
|
47
|
+
compact: 200,
|
|
48
|
+
normal: 250,
|
|
49
|
+
wide: 320,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Shared entity-node height tiers (px) — room for name + type + id. */
|
|
53
|
+
export const ENTITY_NODE_HEIGHT: Record<NodeSizePreset, number> = {
|
|
54
|
+
compact: 72,
|
|
55
|
+
normal: 80,
|
|
56
|
+
wide: 96,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function entityNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
60
|
+
return { width: ENTITY_NODE_WIDTH[preset], height: ENTITY_NODE_HEIGHT[preset] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Canonical entity-node sizes — used by every box-based notation preview. */
|
|
64
|
+
export const ENTITY_NODE_SIZE: PresetTable<BoxDimensions> = {
|
|
65
|
+
compact: entityNodeSize('compact'),
|
|
66
|
+
normal: entityNodeSize('normal'),
|
|
67
|
+
wide: entityNodeSize('wide'),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
71
|
+
export const GOALS_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
72
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
73
|
+
export const DGCA_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
74
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
75
|
+
export const BLOCKS_LEAF_SIZE = ENTITY_NODE_SIZE;
|
|
76
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
77
|
+
export const ACTION_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
78
|
+
|
|
79
|
+
export const CAPABILITY_MAP_NODE_SIZE: PresetTable<CapabilityMapDimensions> = {
|
|
80
|
+
compact: { nodeWidth: ENTITY_NODE_WIDTH.compact, nodeHeight: ENTITY_NODE_HEIGHT.compact },
|
|
81
|
+
normal: { nodeWidth: ENTITY_NODE_WIDTH.normal, nodeHeight: ENTITY_NODE_HEIGHT.normal },
|
|
82
|
+
wide: { nodeWidth: ENTITY_NODE_WIDTH.wide, nodeHeight: ENTITY_NODE_HEIGHT.wide },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const PROCESS_BLUEPRINT_NORMAL: ProcessBlueprintDimensions = {
|
|
86
|
+
legendColumnWidth: 140,
|
|
87
|
+
stageColumnWidth: 220,
|
|
88
|
+
stageHeaderHeight: 40,
|
|
89
|
+
goalRowHeight: 56,
|
|
90
|
+
resultRowHeight: 56,
|
|
91
|
+
aspectRowMinHeight: 60,
|
|
92
|
+
pillHeight: 40,
|
|
93
|
+
pillGap: 4,
|
|
94
|
+
cellPadding: 8,
|
|
95
|
+
textLineHeight: 17,
|
|
96
|
+
textCharWidth: 7.5,
|
|
97
|
+
cellTextPadX: 10,
|
|
98
|
+
cellTextPadY: 10,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function scaleBlueprint(
|
|
102
|
+
base: ProcessBlueprintDimensions,
|
|
103
|
+
widthScale: number,
|
|
104
|
+
heightScale: number,
|
|
105
|
+
): ProcessBlueprintDimensions {
|
|
106
|
+
const w = (n: number) => Math.round(n * widthScale);
|
|
107
|
+
const h = (n: number) => Math.round(n * heightScale);
|
|
108
|
+
return {
|
|
109
|
+
legendColumnWidth: w(base.legendColumnWidth),
|
|
110
|
+
stageColumnWidth: w(base.stageColumnWidth),
|
|
111
|
+
stageHeaderHeight: h(base.stageHeaderHeight),
|
|
112
|
+
goalRowHeight: h(base.goalRowHeight),
|
|
113
|
+
resultRowHeight: h(base.resultRowHeight),
|
|
114
|
+
aspectRowMinHeight: h(base.aspectRowMinHeight),
|
|
115
|
+
pillHeight: h(base.pillHeight),
|
|
116
|
+
pillGap: base.pillGap,
|
|
117
|
+
cellPadding: base.cellPadding,
|
|
118
|
+
textLineHeight: h(base.textLineHeight),
|
|
119
|
+
textCharWidth: base.textCharWidth,
|
|
120
|
+
cellTextPadX: base.cellTextPadX,
|
|
121
|
+
cellTextPadY: base.cellTextPadY,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const PB_WIDTH_SCALE: Record<NodeSizePreset, number> = {
|
|
126
|
+
compact: ENTITY_NODE_WIDTH.compact / ENTITY_NODE_WIDTH.normal,
|
|
127
|
+
normal: 1,
|
|
128
|
+
wide: ENTITY_NODE_WIDTH.wide / ENTITY_NODE_WIDTH.normal,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const PB_HEIGHT_SCALE: Record<NodeSizePreset, number> = {
|
|
132
|
+
compact: ENTITY_NODE_HEIGHT.compact / ENTITY_NODE_HEIGHT.normal,
|
|
133
|
+
normal: 1,
|
|
134
|
+
wide: ENTITY_NODE_HEIGHT.wide / ENTITY_NODE_HEIGHT.normal,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions> = {
|
|
138
|
+
compact: scaleBlueprint(PROCESS_BLUEPRINT_NORMAL, PB_WIDTH_SCALE.compact, PB_HEIGHT_SCALE.compact),
|
|
139
|
+
normal: PROCESS_BLUEPRINT_NORMAL,
|
|
140
|
+
wide: scaleBlueprint(PROCESS_BLUEPRINT_NORMAL, PB_WIDTH_SCALE.wide, PB_HEIGHT_SCALE.wide),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export type NodeSizeNotation =
|
|
144
|
+
| 'goals'
|
|
145
|
+
| 'dgca'
|
|
146
|
+
| 'dga'
|
|
147
|
+
| 'blocks'
|
|
148
|
+
| 'action'
|
|
149
|
+
| 'processBlueprint'
|
|
150
|
+
| 'capabilityMap';
|
|
151
|
+
|
|
152
|
+
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
153
|
+
export function parseNodeSizePreset(value: string | undefined): NodeSizePreset {
|
|
154
|
+
if (value === 'compact' || value === 'wide') return value;
|
|
155
|
+
return 'normal';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function resolveGoalsNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
159
|
+
return ENTITY_NODE_SIZE[preset];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function resolveDgcaNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
163
|
+
return ENTITY_NODE_SIZE[preset];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resolveBlocksLeafSize(preset: NodeSizePreset): BlocksLeafDimensions {
|
|
167
|
+
return ENTITY_NODE_SIZE[preset];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveActionNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
171
|
+
return ENTITY_NODE_SIZE[preset];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function resolveProcessBlueprintSize(preset: NodeSizePreset): ProcessBlueprintDimensions {
|
|
175
|
+
return PROCESS_BLUEPRINT_SIZE[preset];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveCapabilityMapNodeSize(preset: NodeSizePreset): CapabilityMapDimensions {
|
|
179
|
+
return CAPABILITY_MAP_NODE_SIZE[preset];
|
|
180
|
+
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
StageHeaderCell,
|
|
19
19
|
StageTextCell,
|
|
20
20
|
} from './types.js';
|
|
21
|
+
import { wrapTextLines } from '../webview/entity-text-layout.js';
|
|
21
22
|
|
|
22
23
|
const ASPECT_CATEGORIES: AspectCategory[] = ['systems', 'actors', 'equipment', 'information_entities'];
|
|
23
24
|
|
|
@@ -78,56 +79,11 @@ function resolveOptions(options: ProcessBlueprintLayoutOptions | undefined): Res
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
* Greedy word-wrap to a max characters-per-line budget.
|
|
82
|
-
*
|
|
83
|
-
* is truncated with an ellipsis so a single pathological cell can't grow the
|
|
84
|
-
* whole row unbounded.
|
|
82
|
+
* Greedy word-wrap to a max characters-per-line budget. Delegates to the shared
|
|
83
|
+
* shared text-layout module.
|
|
85
84
|
*/
|
|
86
85
|
function wrapText(text: string, maxChars: number, maxLines: number): string[] {
|
|
87
|
-
|
|
88
|
-
if (trimmed.length === 0) return [];
|
|
89
|
-
const budget = Math.max(1, maxChars);
|
|
90
|
-
|
|
91
|
-
const words = trimmed.split(/\s+/);
|
|
92
|
-
const lines: string[] = [];
|
|
93
|
-
let cur = '';
|
|
94
|
-
const flush = (): void => {
|
|
95
|
-
if (cur.length > 0) {
|
|
96
|
-
lines.push(cur);
|
|
97
|
-
cur = '';
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
for (const word of words) {
|
|
102
|
-
if (word.length > budget) {
|
|
103
|
-
// Token wider than the cell: hard-break it across lines.
|
|
104
|
-
flush();
|
|
105
|
-
let rest = word;
|
|
106
|
-
while (rest.length > budget) {
|
|
107
|
-
lines.push(rest.slice(0, budget));
|
|
108
|
-
rest = rest.slice(budget);
|
|
109
|
-
}
|
|
110
|
-
cur = rest;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const candidate = cur.length > 0 ? `${cur} ${word}` : word;
|
|
114
|
-
if (candidate.length > budget) {
|
|
115
|
-
flush();
|
|
116
|
-
cur = word;
|
|
117
|
-
} else {
|
|
118
|
-
cur = candidate;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
flush();
|
|
122
|
-
|
|
123
|
-
if (lines.length > maxLines) {
|
|
124
|
-
const capped = lines.slice(0, maxLines);
|
|
125
|
-
const last = capped[maxLines - 1];
|
|
126
|
-
const clipped = last.length > budget - 1 ? last.slice(0, budget - 1) : last;
|
|
127
|
-
capped[maxLines - 1] = `${clipped.replace(/…$/, '')}…`;
|
|
128
|
-
return capped;
|
|
129
|
-
}
|
|
130
|
-
return lines;
|
|
86
|
+
return wrapTextLines(text, maxChars, maxLines);
|
|
131
87
|
}
|
|
132
88
|
|
|
133
89
|
function buildStageIndex(stages: Stage[]): Map<string, number> {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
layoutCenteredEntityText,
|
|
4
|
+
LINE_H_PRIMARY,
|
|
5
|
+
LINE_H_SECONDARY,
|
|
6
|
+
maxCharsForInnerWidth,
|
|
7
|
+
ROW_GROUP_GAP,
|
|
8
|
+
wrapWords,
|
|
9
|
+
wrapTextLines,
|
|
10
|
+
} from '../entity-text-layout.js';
|
|
11
|
+
|
|
12
|
+
describe('entity-text-layout', () => {
|
|
13
|
+
it('wrapWords respects max lines and truncates with ellipsis', () => {
|
|
14
|
+
const long = 'one two three four five six seven eight nine ten eleven twelve';
|
|
15
|
+
const lines = wrapWords(long, 12, 2);
|
|
16
|
+
expect(lines.length).toBeLessThanOrEqual(2);
|
|
17
|
+
expect(lines[lines.length - 1]).toMatch(/…$/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('maxCharsForInnerWidth scales with inner width', () => {
|
|
21
|
+
expect(maxCharsForInnerWidth(160, 7)).toBeGreaterThan(maxCharsForInnerWidth(120, 7));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('layoutCenteredEntityText keeps name and type labels separated', () => {
|
|
25
|
+
const specs = layoutCenteredEntityText({
|
|
26
|
+
boxX: 0,
|
|
27
|
+
boxY: 0,
|
|
28
|
+
boxWidth: 250,
|
|
29
|
+
boxHeight: 80,
|
|
30
|
+
name: 'Grow enterprise value',
|
|
31
|
+
type: 'Strategic',
|
|
32
|
+
id: 'GOAL-VALUE-1',
|
|
33
|
+
nameMaxLines: 2,
|
|
34
|
+
idMaxLines: 1,
|
|
35
|
+
});
|
|
36
|
+
const nameY = specs.find((l) => l.cls === 'text-primary')!.y;
|
|
37
|
+
const typeY = specs.find((l) => l.cls === 'text-secondary')!.y;
|
|
38
|
+
const minGap = typeY - nameY - LINE_H_PRIMARY / 2 - LINE_H_SECONDARY / 2;
|
|
39
|
+
expect(minGap).toBeGreaterThanOrEqual(ROW_GROUP_GAP - 1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('layoutCenteredEntityText emits more name capacity on wide boxes', () => {
|
|
43
|
+
const narrow = layoutCenteredEntityText({
|
|
44
|
+
boxX: 0,
|
|
45
|
+
boxY: 0,
|
|
46
|
+
boxWidth: 200,
|
|
47
|
+
boxHeight: 80,
|
|
48
|
+
name: 'Improve customer onboarding experience across regions',
|
|
49
|
+
id: 'GOAL-001',
|
|
50
|
+
nameMaxLines: 2,
|
|
51
|
+
idMaxLines: 1,
|
|
52
|
+
});
|
|
53
|
+
const wide = layoutCenteredEntityText({
|
|
54
|
+
boxX: 0,
|
|
55
|
+
boxY: 0,
|
|
56
|
+
boxWidth: 320,
|
|
57
|
+
boxHeight: 80,
|
|
58
|
+
name: 'Improve customer onboarding experience across regions',
|
|
59
|
+
id: 'GOAL-001',
|
|
60
|
+
nameMaxLines: 2,
|
|
61
|
+
idMaxLines: 1,
|
|
62
|
+
});
|
|
63
|
+
const narrowPrimary = narrow.filter((l) => l.cls === 'text-primary').map((l) => l.text).join(' ');
|
|
64
|
+
const widePrimary = wide.filter((l) => l.cls === 'text-primary').map((l) => l.text).join(' ');
|
|
65
|
+
expect(widePrimary.replace(/…/g, '').length).toBeGreaterThanOrEqual(narrowPrimary.replace(/…/g, '').length);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('wrapTextLines hard-breaks oversized tokens (blueprint cells)', () => {
|
|
69
|
+
const lines = wrapTextLines('supercalifragilisticexpialidocious word', 10, 3);
|
|
70
|
+
expect(lines.length).toBeGreaterThan(1);
|
|
71
|
+
expect(lines.every((l) => l.length <= 10)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -47,9 +47,9 @@ describe('webview/entry — Step 2 host API', () => {
|
|
|
47
47
|
|
|
48
48
|
it('routes non-goals kinds to their validator after Step 4 (no NOTATION-NOT-WIRED)', () => {
|
|
49
49
|
// Every supported kind is now wired to its validator + renderer. A
|
|
50
|
-
// malformed
|
|
51
|
-
// the
|
|
52
|
-
const r = render('
|
|
50
|
+
// malformed dgca document must surface a structured validation error from
|
|
51
|
+
// the dgca validator — never the Step-2/3 "not wired" placeholder.
|
|
52
|
+
const r = render('dgca', 'changes: []\n');
|
|
53
53
|
expect(r.status).toBe('error');
|
|
54
54
|
expect(r.errors.length).toBeGreaterThan(0);
|
|
55
55
|
expect(r.errors.every((e) => e.code !== 'NOTATION-NOT-WIRED')).toBe(true);
|
|
@@ -10,15 +10,11 @@ import { describe, expect, it } from 'vitest';
|
|
|
10
10
|
|
|
11
11
|
import type { BlocksFile } from '../../blocks/types.js';
|
|
12
12
|
import { renderBlocksSvg } from '../render-blocks.js';
|
|
13
|
+
import { CHAR_W_PRIMARY as CHAR_W, CHAR_W_ID, TEXT_MARGIN_X } from '../entity-text-layout.js';
|
|
14
|
+
import { ENTITY_NODE_SIZE } from '../../node-size-presets.js';
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// can compute the same overflow bounds the renderer uses.
|
|
17
|
-
const CHAR_W = 7;
|
|
18
|
-
const CHAR_W_ID = 6;
|
|
19
|
-
|
|
20
|
-
// Same horizontal margin the renderer reserves so text never abuts the rect.
|
|
21
|
-
const TEXT_MARGIN_X = 8;
|
|
16
|
+
const LEAF_W = ENTITY_NODE_SIZE.normal.width;
|
|
17
|
+
const LEAF_H = ENTITY_NODE_SIZE.normal.height;
|
|
22
18
|
|
|
23
19
|
function leafDoc(name: string, id: string): BlocksFile {
|
|
24
20
|
return {
|
|
@@ -84,7 +80,8 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
84
80
|
});
|
|
85
81
|
|
|
86
82
|
it('wraps a long ID to at most 2 lines and truncates with …', () => {
|
|
87
|
-
const longId =
|
|
83
|
+
const longId =
|
|
84
|
+
'VERY_LONG_BLOCK_IDENTIFIER_WITH_MANY_SEGMENTS_AND_MORE_AND_MORE_EXTRA_TAIL_TO_FORCE_TRUNCATION';
|
|
88
85
|
const svg = renderBlocksSvg(leafDoc('Short name', longId));
|
|
89
86
|
const idLines = extractLabels(svg).filter((l) => l.cls === 'text-id');
|
|
90
87
|
expect(idLines.length).toBeGreaterThan(0);
|
|
@@ -94,7 +91,7 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
94
91
|
}
|
|
95
92
|
});
|
|
96
93
|
|
|
97
|
-
it(
|
|
94
|
+
it(`keeps every label inside the ${LEAF_W}px leaf width (approximated by CHAR_W * length)`, () => {
|
|
98
95
|
const svg = renderBlocksSvg(
|
|
99
96
|
leafDoc(
|
|
100
97
|
'Personal data records that linger after consent withdrawal across many systems',
|
|
@@ -102,7 +99,7 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
102
99
|
),
|
|
103
100
|
);
|
|
104
101
|
const labels = extractLabels(svg);
|
|
105
|
-
const inner =
|
|
102
|
+
const inner = LEAF_W - TEXT_MARGIN_X * 2;
|
|
106
103
|
for (const l of labels) {
|
|
107
104
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
108
105
|
expect(l.text.length * cw).toBeLessThanOrEqual(inner);
|
|
@@ -134,8 +131,8 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
134
131
|
),
|
|
135
132
|
);
|
|
136
133
|
const labels = extractLabels(svg);
|
|
137
|
-
//
|
|
138
|
-
const containerW =
|
|
134
|
+
// Container width derives from its single leaf child: leafW + 2×padding.
|
|
135
|
+
const containerW = LEAF_W + 24;
|
|
139
136
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
140
137
|
for (const l of labels) {
|
|
141
138
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
@@ -152,7 +149,7 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
152
149
|
'Active systems that are erasure-reachable within the standard 30-day operational window';
|
|
153
150
|
const svg = renderBlocksSvg(containerDoc(longName, 'ACTIVE'));
|
|
154
151
|
const labels = extractLabels(svg);
|
|
155
|
-
const containerW =
|
|
152
|
+
const containerW = LEAF_W + 24;
|
|
156
153
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
157
154
|
const primaries = labels.filter((l) => l.cls === 'text-primary');
|
|
158
155
|
for (const l of primaries) {
|
|
@@ -65,6 +65,48 @@ describe('renderGoalsSvg', () => {
|
|
|
65
65
|
expect(svg).toContain('height="0"');
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it('renders name, type, and id on separate lines without overlap', () => {
|
|
69
|
+
const tree: GoalTree = {
|
|
70
|
+
goal_types: [
|
|
71
|
+
{ name: 'Strategic', level: 0 },
|
|
72
|
+
{ name: 'Objective', level: 1 },
|
|
73
|
+
],
|
|
74
|
+
goals: [
|
|
75
|
+
{
|
|
76
|
+
id: 1,
|
|
77
|
+
canonical_id: 'GOAL-VALUE-1',
|
|
78
|
+
name: 'Grow enterprise value',
|
|
79
|
+
type: 'Strategic',
|
|
80
|
+
level: 0,
|
|
81
|
+
parent_id: 0,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 2,
|
|
85
|
+
canonical_id: 'GOAL-COST-1',
|
|
86
|
+
name: 'Reduce run cost 20%',
|
|
87
|
+
type: 'Objective',
|
|
88
|
+
level: 1,
|
|
89
|
+
parent_id: 1,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
const svg = renderGoalsSvg(tree);
|
|
94
|
+
expect(svg).toContain('Grow enterprise value');
|
|
95
|
+
expect(svg).toContain('Strategic');
|
|
96
|
+
expect(svg).toContain('GOAL-VALUE-1');
|
|
97
|
+
const typeMatches = [...svg.matchAll(/class="text-secondary"[^>]*y="(\d+)"/g)];
|
|
98
|
+
const nameMatches = [...svg.matchAll(/class="text-primary"[^>]*y="(\d+)"/g)];
|
|
99
|
+
expect(typeMatches.length).toBeGreaterThan(0);
|
|
100
|
+
for (const type of typeMatches) {
|
|
101
|
+
const typeY = Number(type[1]);
|
|
102
|
+
const nearestName = nameMatches
|
|
103
|
+
.map((m) => Number(m[1]))
|
|
104
|
+
.filter((y) => y < typeY)
|
|
105
|
+
.sort((a, b) => b - a)[0];
|
|
106
|
+
expect(typeY - nearestName).toBeGreaterThanOrEqual(18);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
68
110
|
it('shows canonical goal ids in node labels when present', () => {
|
|
69
111
|
const tree: GoalTree = {
|
|
70
112
|
goal_types: [{ name: 'Strategy', level: 0 }],
|