@transitrix/diagrams 1.8.1 → 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 +1 -1
- 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/layout-tree.d.ts +1 -1
- package/dist/capability-map/layout-tree.js +1 -1
- package/dist/fgca/preview-layout.d.ts +2 -2
- package/dist/fgca/preview-layout.js +3 -2
- package/dist/node-size-presets.d.ts +17 -4
- package/dist/node-size-presets.js +86 -72
- package/dist/webview/__tests__/entity-text-layout.test.js +20 -3
- package/dist/webview/__tests__/render-blocks.test.js +9 -6
- package/dist/webview/__tests__/render-goals.test.js +41 -0
- package/dist/webview/entity-text-layout.js +2 -4
- package/package.json +1 -1
- package/src/__tests__/node-size-presets.test.ts +50 -0
- package/src/activities/layout.ts +1 -1
- 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/layout-tree.ts +1 -1
- package/src/fgca/preview-layout.ts +3 -2
- package/src/node-size-presets.ts +95 -72
- package/src/webview/__tests__/entity-text-layout.test.ts +23 -2
- package/src/webview/__tests__/render-blocks.test.ts +11 -6
- package/src/webview/__tests__/render-goals.test.ts +42 -0
- package/src/webview/entity-text-layout.ts +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ACTION_NODE_SIZE, BLOCKS_LEAF_SIZE, CAPABILITY_MAP_NODE_SIZE, DGCA_NODE_SIZE, ENTITY_NODE_HEIGHT, ENTITY_NODE_SIZE, ENTITY_NODE_WIDTH, GOALS_NODE_SIZE, PROCESS_BLUEPRINT_SIZE, resolveGoalsNodeSize, } from '../node-size-presets.js';
|
|
3
|
+
describe('node-size-presets', () => {
|
|
4
|
+
it('uses shared width and height tiers for entity nodes', () => {
|
|
5
|
+
expect(ENTITY_NODE_WIDTH).toEqual({ compact: 200, normal: 250, wide: 320 });
|
|
6
|
+
expect(ENTITY_NODE_HEIGHT).toEqual({ compact: 72, normal: 80, wide: 96 });
|
|
7
|
+
expect(ENTITY_NODE_SIZE.normal).toEqual({ width: 250, height: 80 });
|
|
8
|
+
expect(ENTITY_NODE_SIZE.compact).toEqual({ width: 200, height: 72 });
|
|
9
|
+
expect(ENTITY_NODE_SIZE.wide).toEqual({ width: 320, height: 96 });
|
|
10
|
+
});
|
|
11
|
+
it('exposes the same entity sizes for every box-based notation', () => {
|
|
12
|
+
for (const table of [GOALS_NODE_SIZE, DGCA_NODE_SIZE, BLOCKS_LEAF_SIZE, ACTION_NODE_SIZE]) {
|
|
13
|
+
expect(table).toBe(ENTITY_NODE_SIZE);
|
|
14
|
+
expect(table.normal).toEqual(ENTITY_NODE_SIZE.normal);
|
|
15
|
+
}
|
|
16
|
+
expect(CAPABILITY_MAP_NODE_SIZE.normal).toEqual({
|
|
17
|
+
nodeWidth: ENTITY_NODE_WIDTH.normal,
|
|
18
|
+
nodeHeight: ENTITY_NODE_HEIGHT.normal,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('scales wide above normal and compact below', () => {
|
|
22
|
+
expect(resolveGoalsNodeSize('wide').height).toBeGreaterThan(resolveGoalsNodeSize('normal').height);
|
|
23
|
+
expect(resolveGoalsNodeSize('compact').height).toBeLessThan(resolveGoalsNodeSize('normal').height);
|
|
24
|
+
expect(resolveGoalsNodeSize('wide').width).toBeGreaterThan(resolveGoalsNodeSize('normal').width);
|
|
25
|
+
});
|
|
26
|
+
it('scales process blueprint from the same tier ratios', () => {
|
|
27
|
+
expect(PROCESS_BLUEPRINT_SIZE.wide.stageColumnWidth).toBeGreaterThan(PROCESS_BLUEPRINT_SIZE.normal.stageColumnWidth);
|
|
28
|
+
expect(PROCESS_BLUEPRINT_SIZE.compact.goalRowHeight).toBeLessThan(PROCESS_BLUEPRINT_SIZE.normal.goalRowHeight);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -108,8 +108,8 @@ describe('layoutNestedBlocks', () => {
|
|
|
108
108
|
},
|
|
109
109
|
});
|
|
110
110
|
const a = layout.blocks[0];
|
|
111
|
-
expect(a.width).toBe(
|
|
112
|
-
expect(a.height).toBe(
|
|
111
|
+
expect(a.width).toBe(250);
|
|
112
|
+
expect(a.height).toBe(80);
|
|
113
113
|
});
|
|
114
114
|
it('honours custom layout options', () => {
|
|
115
115
|
const layout = layoutNestedBlocks({
|
package/dist/blocks/layout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLevel, getParentAddress } from './address.js';
|
|
2
2
|
const DEFAULT_NODE_WIDTH = 250;
|
|
3
|
-
const DEFAULT_NODE_HEIGHT =
|
|
3
|
+
const DEFAULT_NODE_HEIGHT = 80;
|
|
4
4
|
const DEFAULT_RANK_SEP = 80;
|
|
5
5
|
const DEFAULT_NODE_SEP = 24;
|
|
6
6
|
/** Width of the virtual organisation-root node — narrower than a capability
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CapabilityNode, CapabilityMapHeader } from './types.js';
|
|
2
2
|
export declare const TREE_NODE_WIDTH = 250;
|
|
3
|
-
export declare const TREE_NODE_HEIGHT =
|
|
3
|
+
export declare const TREE_NODE_HEIGHT = 80;
|
|
4
4
|
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
5
5
|
export declare const TREE_RANK_SEP = 80;
|
|
6
6
|
/** Vertical gap between sibling nodes. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const TREE_NODE_WIDTH = 250;
|
|
2
|
-
export const TREE_NODE_HEIGHT =
|
|
2
|
+
export const TREE_NODE_HEIGHT = 80;
|
|
3
3
|
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
4
4
|
export const TREE_RANK_SEP = 80;
|
|
5
5
|
/** Vertical gap between sibling nodes. */
|
|
@@ -82,8 +82,8 @@ export interface FGCAPreviewLayout {
|
|
|
82
82
|
width: number;
|
|
83
83
|
height: number;
|
|
84
84
|
}
|
|
85
|
-
export declare const FGCA_NODE_W
|
|
86
|
-
export declare const FGCA_NODE_H
|
|
85
|
+
export declare const FGCA_NODE_W: number;
|
|
86
|
+
export declare const FGCA_NODE_H: number;
|
|
87
87
|
export declare const FGCA_HEADER_H = 32;
|
|
88
88
|
export declare const FGCA_PAD = 20;
|
|
89
89
|
export declare const FGCA_DEFAULT_COL_GAP = 160;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// so the gap geometry is unit-testable: the extension has no test harness.
|
|
7
7
|
//
|
|
8
8
|
// No React, no DOM, no vscode. Safe to call in Node.js or a test environment.
|
|
9
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
9
10
|
/**
|
|
10
11
|
* Trims a DGCA/DGA doc to a scope.
|
|
11
12
|
*
|
|
@@ -40,8 +41,8 @@ export function selectScopedFGCA(doc, scope) {
|
|
|
40
41
|
}
|
|
41
42
|
// Default node + frame geometry. Inter-node gaps are user-configurable via
|
|
42
43
|
// Inter-node gaps are user-configurable via spacing settings; node size uses presets.
|
|
43
|
-
export const FGCA_NODE_W =
|
|
44
|
-
export const FGCA_NODE_H =
|
|
44
|
+
export const FGCA_NODE_W = ENTITY_NODE_SIZE.normal.width;
|
|
45
|
+
export const FGCA_NODE_H = ENTITY_NODE_SIZE.normal.height;
|
|
45
46
|
export const FGCA_HEADER_H = 32;
|
|
46
47
|
export const FGCA_PAD = 20;
|
|
47
48
|
export const FGCA_DEFAULT_COL_GAP = 160;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block/cell size presets for diagram renderers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
9
|
*/
|
|
7
10
|
export type NodeSizePreset = 'compact' | 'normal' | 'wide';
|
|
8
11
|
export declare const NODE_SIZE_PRESET_VALUES: readonly NodeSizePreset[];
|
|
@@ -32,12 +35,22 @@ export interface CapabilityMapDimensions {
|
|
|
32
35
|
nodeHeight: number;
|
|
33
36
|
}
|
|
34
37
|
type PresetTable<T> = Record<NodeSizePreset, T>;
|
|
38
|
+
/** Shared entity-node width tiers (px). */
|
|
39
|
+
export declare const ENTITY_NODE_WIDTH: Record<NodeSizePreset, number>;
|
|
40
|
+
/** Shared entity-node height tiers (px) — room for name + type + id. */
|
|
41
|
+
export declare const ENTITY_NODE_HEIGHT: Record<NodeSizePreset, number>;
|
|
42
|
+
/** Canonical entity-node sizes — used by every box-based notation preview. */
|
|
43
|
+
export declare const ENTITY_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
44
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
35
45
|
export declare const GOALS_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
46
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
36
47
|
export declare const DGCA_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
37
|
-
|
|
48
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
49
|
+
export declare const BLOCKS_LEAF_SIZE: PresetTable<BoxDimensions>;
|
|
50
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
38
51
|
export declare const ACTION_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
39
|
-
export declare const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions>;
|
|
40
52
|
export declare const CAPABILITY_MAP_NODE_SIZE: PresetTable<CapabilityMapDimensions>;
|
|
53
|
+
export declare const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions>;
|
|
41
54
|
export type NodeSizeNotation = 'goals' | 'dgca' | 'dga' | 'blocks' | 'action' | 'processBlueprint' | 'capabilityMap';
|
|
42
55
|
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
43
56
|
export declare function parseNodeSizePreset(value: string | undefined): NodeSizePreset;
|
|
@@ -1,81 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block/cell size presets for diagram renderers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
9
|
*/
|
|
7
10
|
export const NODE_SIZE_PRESET_VALUES = ['compact', 'normal', 'wide'];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/** Shared entity-node width tiers (px). */
|
|
12
|
+
export const ENTITY_NODE_WIDTH = {
|
|
13
|
+
compact: 200,
|
|
14
|
+
normal: 250,
|
|
15
|
+
wide: 320,
|
|
12
16
|
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** Shared entity-node height tiers (px) — room for name + type + id. */
|
|
18
|
+
export const ENTITY_NODE_HEIGHT = {
|
|
19
|
+
compact: 72,
|
|
20
|
+
normal: 80,
|
|
21
|
+
wide: 96,
|
|
17
22
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
function entityNodeSize(preset) {
|
|
24
|
+
return { width: ENTITY_NODE_WIDTH[preset], height: ENTITY_NODE_HEIGHT[preset] };
|
|
25
|
+
}
|
|
26
|
+
/** Canonical entity-node sizes — used by every box-based notation preview. */
|
|
27
|
+
export const ENTITY_NODE_SIZE = {
|
|
28
|
+
compact: entityNodeSize('compact'),
|
|
29
|
+
normal: entityNodeSize('normal'),
|
|
30
|
+
wide: entityNodeSize('wide'),
|
|
22
31
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
33
|
+
export const GOALS_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
34
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
35
|
+
export const DGCA_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
36
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
37
|
+
export const BLOCKS_LEAF_SIZE = ENTITY_NODE_SIZE;
|
|
38
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
39
|
+
export const ACTION_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
40
|
+
export const CAPABILITY_MAP_NODE_SIZE = {
|
|
41
|
+
compact: { nodeWidth: ENTITY_NODE_WIDTH.compact, nodeHeight: ENTITY_NODE_HEIGHT.compact },
|
|
42
|
+
normal: { nodeWidth: ENTITY_NODE_WIDTH.normal, nodeHeight: ENTITY_NODE_HEIGHT.normal },
|
|
43
|
+
wide: { nodeWidth: ENTITY_NODE_WIDTH.wide, nodeHeight: ENTITY_NODE_HEIGHT.wide },
|
|
27
44
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
cellTextPadY: 10,
|
|
43
|
-
},
|
|
44
|
-
normal: {
|
|
45
|
-
legendColumnWidth: 140,
|
|
46
|
-
stageColumnWidth: 220,
|
|
47
|
-
stageHeaderHeight: 40,
|
|
48
|
-
goalRowHeight: 56,
|
|
49
|
-
resultRowHeight: 56,
|
|
50
|
-
aspectRowMinHeight: 60,
|
|
51
|
-
pillHeight: 40,
|
|
52
|
-
pillGap: 4,
|
|
53
|
-
cellPadding: 8,
|
|
54
|
-
textLineHeight: 17,
|
|
55
|
-
textCharWidth: 7.5,
|
|
56
|
-
cellTextPadX: 10,
|
|
57
|
-
cellTextPadY: 10,
|
|
58
|
-
},
|
|
59
|
-
wide: {
|
|
60
|
-
legendColumnWidth: 160,
|
|
61
|
-
stageColumnWidth: 280,
|
|
62
|
-
stageHeaderHeight: 44,
|
|
63
|
-
goalRowHeight: 60,
|
|
64
|
-
resultRowHeight: 60,
|
|
65
|
-
aspectRowMinHeight: 64,
|
|
66
|
-
pillHeight: 44,
|
|
67
|
-
pillGap: 4,
|
|
68
|
-
cellPadding: 8,
|
|
69
|
-
textLineHeight: 18,
|
|
70
|
-
textCharWidth: 7.5,
|
|
71
|
-
cellTextPadX: 10,
|
|
72
|
-
cellTextPadY: 10,
|
|
73
|
-
},
|
|
45
|
+
const PROCESS_BLUEPRINT_NORMAL = {
|
|
46
|
+
legendColumnWidth: 140,
|
|
47
|
+
stageColumnWidth: 220,
|
|
48
|
+
stageHeaderHeight: 40,
|
|
49
|
+
goalRowHeight: 56,
|
|
50
|
+
resultRowHeight: 56,
|
|
51
|
+
aspectRowMinHeight: 60,
|
|
52
|
+
pillHeight: 40,
|
|
53
|
+
pillGap: 4,
|
|
54
|
+
cellPadding: 8,
|
|
55
|
+
textLineHeight: 17,
|
|
56
|
+
textCharWidth: 7.5,
|
|
57
|
+
cellTextPadX: 10,
|
|
58
|
+
cellTextPadY: 10,
|
|
74
59
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
function scaleBlueprint(base, widthScale, heightScale) {
|
|
61
|
+
const w = (n) => Math.round(n * widthScale);
|
|
62
|
+
const h = (n) => Math.round(n * heightScale);
|
|
63
|
+
return {
|
|
64
|
+
legendColumnWidth: w(base.legendColumnWidth),
|
|
65
|
+
stageColumnWidth: w(base.stageColumnWidth),
|
|
66
|
+
stageHeaderHeight: h(base.stageHeaderHeight),
|
|
67
|
+
goalRowHeight: h(base.goalRowHeight),
|
|
68
|
+
resultRowHeight: h(base.resultRowHeight),
|
|
69
|
+
aspectRowMinHeight: h(base.aspectRowMinHeight),
|
|
70
|
+
pillHeight: h(base.pillHeight),
|
|
71
|
+
pillGap: base.pillGap,
|
|
72
|
+
cellPadding: base.cellPadding,
|
|
73
|
+
textLineHeight: h(base.textLineHeight),
|
|
74
|
+
textCharWidth: base.textCharWidth,
|
|
75
|
+
cellTextPadX: base.cellTextPadX,
|
|
76
|
+
cellTextPadY: base.cellTextPadY,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const PB_WIDTH_SCALE = {
|
|
80
|
+
compact: ENTITY_NODE_WIDTH.compact / ENTITY_NODE_WIDTH.normal,
|
|
81
|
+
normal: 1,
|
|
82
|
+
wide: ENTITY_NODE_WIDTH.wide / ENTITY_NODE_WIDTH.normal,
|
|
83
|
+
};
|
|
84
|
+
const PB_HEIGHT_SCALE = {
|
|
85
|
+
compact: ENTITY_NODE_HEIGHT.compact / ENTITY_NODE_HEIGHT.normal,
|
|
86
|
+
normal: 1,
|
|
87
|
+
wide: ENTITY_NODE_HEIGHT.wide / ENTITY_NODE_HEIGHT.normal,
|
|
88
|
+
};
|
|
89
|
+
export const PROCESS_BLUEPRINT_SIZE = {
|
|
90
|
+
compact: scaleBlueprint(PROCESS_BLUEPRINT_NORMAL, PB_WIDTH_SCALE.compact, PB_HEIGHT_SCALE.compact),
|
|
91
|
+
normal: PROCESS_BLUEPRINT_NORMAL,
|
|
92
|
+
wide: scaleBlueprint(PROCESS_BLUEPRINT_NORMAL, PB_WIDTH_SCALE.wide, PB_HEIGHT_SCALE.wide),
|
|
79
93
|
};
|
|
80
94
|
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
81
95
|
export function parseNodeSizePreset(value) {
|
|
@@ -84,16 +98,16 @@ export function parseNodeSizePreset(value) {
|
|
|
84
98
|
return 'normal';
|
|
85
99
|
}
|
|
86
100
|
export function resolveGoalsNodeSize(preset) {
|
|
87
|
-
return
|
|
101
|
+
return ENTITY_NODE_SIZE[preset];
|
|
88
102
|
}
|
|
89
103
|
export function resolveDgcaNodeSize(preset) {
|
|
90
|
-
return
|
|
104
|
+
return ENTITY_NODE_SIZE[preset];
|
|
91
105
|
}
|
|
92
106
|
export function resolveBlocksLeafSize(preset) {
|
|
93
|
-
return
|
|
107
|
+
return ENTITY_NODE_SIZE[preset];
|
|
94
108
|
}
|
|
95
109
|
export function resolveActionNodeSize(preset) {
|
|
96
|
-
return
|
|
110
|
+
return ENTITY_NODE_SIZE[preset];
|
|
97
111
|
}
|
|
98
112
|
export function resolveProcessBlueprintSize(preset) {
|
|
99
113
|
return PROCESS_BLUEPRINT_SIZE[preset];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { layoutCenteredEntityText, maxCharsForInnerWidth, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
|
|
2
|
+
import { layoutCenteredEntityText, LINE_H_PRIMARY, LINE_H_SECONDARY, maxCharsForInnerWidth, ROW_GROUP_GAP, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
|
|
3
3
|
describe('entity-text-layout', () => {
|
|
4
4
|
it('wrapWords respects max lines and truncates with ellipsis', () => {
|
|
5
5
|
const long = 'one two three four five six seven eight nine ten eleven twelve';
|
|
@@ -10,12 +10,29 @@ describe('entity-text-layout', () => {
|
|
|
10
10
|
it('maxCharsForInnerWidth scales with inner width', () => {
|
|
11
11
|
expect(maxCharsForInnerWidth(160, 7)).toBeGreaterThan(maxCharsForInnerWidth(120, 7));
|
|
12
12
|
});
|
|
13
|
+
it('layoutCenteredEntityText keeps name and type labels separated', () => {
|
|
14
|
+
const specs = layoutCenteredEntityText({
|
|
15
|
+
boxX: 0,
|
|
16
|
+
boxY: 0,
|
|
17
|
+
boxWidth: 250,
|
|
18
|
+
boxHeight: 80,
|
|
19
|
+
name: 'Grow enterprise value',
|
|
20
|
+
type: 'Strategic',
|
|
21
|
+
id: 'GOAL-VALUE-1',
|
|
22
|
+
nameMaxLines: 2,
|
|
23
|
+
idMaxLines: 1,
|
|
24
|
+
});
|
|
25
|
+
const nameY = specs.find((l) => l.cls === 'text-primary').y;
|
|
26
|
+
const typeY = specs.find((l) => l.cls === 'text-secondary').y;
|
|
27
|
+
const minGap = typeY - nameY - LINE_H_PRIMARY / 2 - LINE_H_SECONDARY / 2;
|
|
28
|
+
expect(minGap).toBeGreaterThanOrEqual(ROW_GROUP_GAP - 1);
|
|
29
|
+
});
|
|
13
30
|
it('layoutCenteredEntityText emits more name capacity on wide boxes', () => {
|
|
14
31
|
const narrow = layoutCenteredEntityText({
|
|
15
32
|
boxX: 0,
|
|
16
33
|
boxY: 0,
|
|
17
34
|
boxWidth: 200,
|
|
18
|
-
boxHeight:
|
|
35
|
+
boxHeight: 80,
|
|
19
36
|
name: 'Improve customer onboarding experience across regions',
|
|
20
37
|
id: 'GOAL-001',
|
|
21
38
|
nameMaxLines: 2,
|
|
@@ -25,7 +42,7 @@ describe('entity-text-layout', () => {
|
|
|
25
42
|
boxX: 0,
|
|
26
43
|
boxY: 0,
|
|
27
44
|
boxWidth: 320,
|
|
28
|
-
boxHeight:
|
|
45
|
+
boxHeight: 80,
|
|
29
46
|
name: 'Improve customer onboarding experience across regions',
|
|
30
47
|
id: 'GOAL-001',
|
|
31
48
|
nameMaxLines: 2,
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import { describe, expect, it } from 'vitest';
|
|
10
10
|
import { renderBlocksSvg } from '../render-blocks.js';
|
|
11
11
|
import { CHAR_W_PRIMARY as CHAR_W, CHAR_W_ID, TEXT_MARGIN_X } from '../entity-text-layout.js';
|
|
12
|
+
import { ENTITY_NODE_SIZE } from '../../node-size-presets.js';
|
|
13
|
+
const LEAF_W = ENTITY_NODE_SIZE.normal.width;
|
|
14
|
+
const LEAF_H = ENTITY_NODE_SIZE.normal.height;
|
|
12
15
|
function leafDoc(name, id) {
|
|
13
16
|
return {
|
|
14
17
|
notation: 'blocks',
|
|
@@ -69,7 +72,7 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
69
72
|
}
|
|
70
73
|
});
|
|
71
74
|
it('wraps a long ID to at most 2 lines and truncates with …', () => {
|
|
72
|
-
const longId = '
|
|
75
|
+
const longId = 'VERY_LONG_BLOCK_IDENTIFIER_WITH_MANY_SEGMENTS_AND_MORE_AND_MORE_EXTRA_TAIL_TO_FORCE_TRUNCATION';
|
|
73
76
|
const svg = renderBlocksSvg(leafDoc('Short name', longId));
|
|
74
77
|
const idLines = extractLabels(svg).filter((l) => l.cls === 'text-id');
|
|
75
78
|
expect(idLines.length).toBeGreaterThan(0);
|
|
@@ -78,10 +81,10 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
78
81
|
expect(idLines[1].text.endsWith('…')).toBe(true);
|
|
79
82
|
}
|
|
80
83
|
});
|
|
81
|
-
it(
|
|
84
|
+
it(`keeps every label inside the ${LEAF_W}px leaf width (approximated by CHAR_W * length)`, () => {
|
|
82
85
|
const svg = renderBlocksSvg(leafDoc('Personal data records that linger after consent withdrawal across many systems', 'PII_RECORDS_WITH_A_FAIRLY_LONG_IDENTIFIER_HERE'));
|
|
83
86
|
const labels = extractLabels(svg);
|
|
84
|
-
const inner =
|
|
87
|
+
const inner = LEAF_W - TEXT_MARGIN_X * 2;
|
|
85
88
|
for (const l of labels) {
|
|
86
89
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
87
90
|
expect(l.text.length * cw).toBeLessThanOrEqual(inner);
|
|
@@ -105,8 +108,8 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
105
108
|
it('truncates a long container ID so it does not overflow the block width', () => {
|
|
106
109
|
const svg = renderBlocksSvg(containerDoc('Short name', 'VERY_LONG_CONTAINER_IDENTIFIER_THAT_WOULD_OTHERWISE_OVERFLOW', 'CHILD', 'Child'));
|
|
107
110
|
const labels = extractLabels(svg);
|
|
108
|
-
//
|
|
109
|
-
const containerW =
|
|
111
|
+
// Container width derives from its single leaf child: leafW + 2×padding.
|
|
112
|
+
const containerW = LEAF_W + 24;
|
|
110
113
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
111
114
|
for (const l of labels) {
|
|
112
115
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
@@ -121,7 +124,7 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
121
124
|
const longName = 'Active systems that are erasure-reachable within the standard 30-day operational window';
|
|
122
125
|
const svg = renderBlocksSvg(containerDoc(longName, 'ACTIVE'));
|
|
123
126
|
const labels = extractLabels(svg);
|
|
124
|
-
const containerW =
|
|
127
|
+
const containerW = LEAF_W + 24;
|
|
125
128
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
126
129
|
const primaries = labels.filter((l) => l.cls === 'text-primary');
|
|
127
130
|
for (const l of primaries) {
|
|
@@ -58,6 +58,47 @@ describe('renderGoalsSvg', () => {
|
|
|
58
58
|
expect(svg).toContain('width="0"');
|
|
59
59
|
expect(svg).toContain('height="0"');
|
|
60
60
|
});
|
|
61
|
+
it('renders name, type, and id on separate lines without overlap', () => {
|
|
62
|
+
const tree = {
|
|
63
|
+
goal_types: [
|
|
64
|
+
{ name: 'Strategic', level: 0 },
|
|
65
|
+
{ name: 'Objective', level: 1 },
|
|
66
|
+
],
|
|
67
|
+
goals: [
|
|
68
|
+
{
|
|
69
|
+
id: 1,
|
|
70
|
+
canonical_id: 'GOAL-VALUE-1',
|
|
71
|
+
name: 'Grow enterprise value',
|
|
72
|
+
type: 'Strategic',
|
|
73
|
+
level: 0,
|
|
74
|
+
parent_id: 0,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 2,
|
|
78
|
+
canonical_id: 'GOAL-COST-1',
|
|
79
|
+
name: 'Reduce run cost 20%',
|
|
80
|
+
type: 'Objective',
|
|
81
|
+
level: 1,
|
|
82
|
+
parent_id: 1,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
const svg = renderGoalsSvg(tree);
|
|
87
|
+
expect(svg).toContain('Grow enterprise value');
|
|
88
|
+
expect(svg).toContain('Strategic');
|
|
89
|
+
expect(svg).toContain('GOAL-VALUE-1');
|
|
90
|
+
const typeMatches = [...svg.matchAll(/class="text-secondary"[^>]*y="(\d+)"/g)];
|
|
91
|
+
const nameMatches = [...svg.matchAll(/class="text-primary"[^>]*y="(\d+)"/g)];
|
|
92
|
+
expect(typeMatches.length).toBeGreaterThan(0);
|
|
93
|
+
for (const type of typeMatches) {
|
|
94
|
+
const typeY = Number(type[1]);
|
|
95
|
+
const nearestName = nameMatches
|
|
96
|
+
.map((m) => Number(m[1]))
|
|
97
|
+
.filter((y) => y < typeY)
|
|
98
|
+
.sort((a, b) => b - a)[0];
|
|
99
|
+
expect(typeY - nearestName).toBeGreaterThanOrEqual(18);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
61
102
|
it('shows canonical goal ids in node labels when present', () => {
|
|
62
103
|
const tree = {
|
|
63
104
|
goal_types: [{ name: 'Strategy', level: 0 }],
|
|
@@ -140,8 +140,7 @@ function totalVerticalSpan(groups) {
|
|
|
140
140
|
let span = 0;
|
|
141
141
|
for (let i = 0; i < groups.length; i++) {
|
|
142
142
|
const g = groups[i];
|
|
143
|
-
|
|
144
|
-
span += (g.lines.length - 1) * g.lineHeight;
|
|
143
|
+
span += g.lines.length * g.lineHeight;
|
|
145
144
|
if (i < groups.length - 1)
|
|
146
145
|
span += g.gapAfter;
|
|
147
146
|
}
|
|
@@ -192,8 +191,7 @@ export function layoutCenteredEntityText(opts) {
|
|
|
192
191
|
const g = groups[gi];
|
|
193
192
|
for (let li = 0; li < g.lines.length; li++) {
|
|
194
193
|
out.push({ cls: g.cls, text: g.lines[li], y });
|
|
195
|
-
|
|
196
|
-
y += g.lineHeight;
|
|
194
|
+
y += g.lineHeight;
|
|
197
195
|
}
|
|
198
196
|
if (gi < groups.length - 1)
|
|
199
197
|
y += g.gapAfter;
|
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACTION_NODE_SIZE,
|
|
5
|
+
BLOCKS_LEAF_SIZE,
|
|
6
|
+
CAPABILITY_MAP_NODE_SIZE,
|
|
7
|
+
DGCA_NODE_SIZE,
|
|
8
|
+
ENTITY_NODE_HEIGHT,
|
|
9
|
+
ENTITY_NODE_SIZE,
|
|
10
|
+
ENTITY_NODE_WIDTH,
|
|
11
|
+
GOALS_NODE_SIZE,
|
|
12
|
+
PROCESS_BLUEPRINT_SIZE,
|
|
13
|
+
resolveGoalsNodeSize,
|
|
14
|
+
} from '../node-size-presets.js';
|
|
15
|
+
|
|
16
|
+
describe('node-size-presets', () => {
|
|
17
|
+
it('uses shared width and height tiers for entity nodes', () => {
|
|
18
|
+
expect(ENTITY_NODE_WIDTH).toEqual({ compact: 200, normal: 250, wide: 320 });
|
|
19
|
+
expect(ENTITY_NODE_HEIGHT).toEqual({ compact: 72, normal: 80, wide: 96 });
|
|
20
|
+
expect(ENTITY_NODE_SIZE.normal).toEqual({ width: 250, height: 80 });
|
|
21
|
+
expect(ENTITY_NODE_SIZE.compact).toEqual({ width: 200, height: 72 });
|
|
22
|
+
expect(ENTITY_NODE_SIZE.wide).toEqual({ width: 320, height: 96 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('exposes the same entity sizes for every box-based notation', () => {
|
|
26
|
+
for (const table of [GOALS_NODE_SIZE, DGCA_NODE_SIZE, BLOCKS_LEAF_SIZE, ACTION_NODE_SIZE]) {
|
|
27
|
+
expect(table).toBe(ENTITY_NODE_SIZE);
|
|
28
|
+
expect(table.normal).toEqual(ENTITY_NODE_SIZE.normal);
|
|
29
|
+
}
|
|
30
|
+
expect(CAPABILITY_MAP_NODE_SIZE.normal).toEqual({
|
|
31
|
+
nodeWidth: ENTITY_NODE_WIDTH.normal,
|
|
32
|
+
nodeHeight: ENTITY_NODE_HEIGHT.normal,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('scales wide above normal and compact below', () => {
|
|
37
|
+
expect(resolveGoalsNodeSize('wide').height).toBeGreaterThan(resolveGoalsNodeSize('normal').height);
|
|
38
|
+
expect(resolveGoalsNodeSize('compact').height).toBeLessThan(resolveGoalsNodeSize('normal').height);
|
|
39
|
+
expect(resolveGoalsNodeSize('wide').width).toBeGreaterThan(resolveGoalsNodeSize('normal').width);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('scales process blueprint from the same tier ratios', () => {
|
|
43
|
+
expect(PROCESS_BLUEPRINT_SIZE.wide.stageColumnWidth).toBeGreaterThan(
|
|
44
|
+
PROCESS_BLUEPRINT_SIZE.normal.stageColumnWidth,
|
|
45
|
+
);
|
|
46
|
+
expect(PROCESS_BLUEPRINT_SIZE.compact.goalRowHeight).toBeLessThan(
|
|
47
|
+
PROCESS_BLUEPRINT_SIZE.normal.goalRowHeight,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/activities/layout.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ActivityDoc, Activity, ActivitiesLayout, ActivitiesLayoutOptions, LayoutNode, LayoutEdge } from './types.js';
|
|
2
2
|
import { computeCpm } from './cpm.js';
|
|
3
3
|
|
|
4
|
-
const DEFAULT_NODE_W =
|
|
4
|
+
const DEFAULT_NODE_W = 250;
|
|
5
5
|
const DEFAULT_NODE_H = 80;
|
|
6
6
|
const H_GAP = 80;
|
|
7
7
|
const V_GAP = 24;
|
|
@@ -118,8 +118,8 @@ describe('layoutNestedBlocks', () => {
|
|
|
118
118
|
},
|
|
119
119
|
});
|
|
120
120
|
const a = layout.blocks[0];
|
|
121
|
-
expect(a.width).toBe(
|
|
122
|
-
expect(a.height).toBe(
|
|
121
|
+
expect(a.width).toBe(250);
|
|
122
|
+
expect(a.height).toBe(80);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
it('honours custom layout options', () => {
|
package/src/blocks/layout.ts
CHANGED
|
@@ -5,10 +5,11 @@ import type {
|
|
|
5
5
|
BlocksLayoutOptions,
|
|
6
6
|
LaidOutBlock,
|
|
7
7
|
} from './types.js';
|
|
8
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULTS: Required<BlocksLayoutOptions> = {
|
|
10
|
-
leafWidth:
|
|
11
|
-
leafHeight:
|
|
11
|
+
leafWidth: ENTITY_NODE_SIZE.normal.width,
|
|
12
|
+
leafHeight: ENTITY_NODE_SIZE.normal.height,
|
|
12
13
|
padding: 12,
|
|
13
14
|
headerHeight: 28,
|
|
14
15
|
childGap: 12,
|
|
@@ -2,7 +2,7 @@ import type { CapabilityMap, LayoutOptions, CapabilityMapLayout, LaidOutNode, La
|
|
|
2
2
|
import { getLevel, getParentAddress } from './address.js';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_NODE_WIDTH = 250;
|
|
5
|
-
const DEFAULT_NODE_HEIGHT =
|
|
5
|
+
const DEFAULT_NODE_HEIGHT = 80;
|
|
6
6
|
const DEFAULT_RANK_SEP = 80;
|
|
7
7
|
const DEFAULT_NODE_SEP = 24;
|
|
8
8
|
/** Width of the virtual organisation-root node — narrower than a capability
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CapabilityNode, CapabilityMapHeader } from './types.js';
|
|
2
2
|
|
|
3
3
|
export const TREE_NODE_WIDTH = 250;
|
|
4
|
-
export const TREE_NODE_HEIGHT =
|
|
4
|
+
export const TREE_NODE_HEIGHT = 80;
|
|
5
5
|
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
6
6
|
export const TREE_RANK_SEP = 80;
|
|
7
7
|
/** Vertical gap between sibling nodes. */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// No React, no DOM, no vscode. Safe to call in Node.js or a test environment.
|
|
9
9
|
|
|
10
10
|
import type { Scope } from '../scope.js';
|
|
11
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
11
12
|
|
|
12
13
|
export type FGCAPreviewColumn = 'driver' | 'goal' | 'change' | 'activity';
|
|
13
14
|
|
|
@@ -125,8 +126,8 @@ export interface FGCAPreviewLayout {
|
|
|
125
126
|
|
|
126
127
|
// Default node + frame geometry. Inter-node gaps are user-configurable via
|
|
127
128
|
// Inter-node gaps are user-configurable via spacing settings; node size uses presets.
|
|
128
|
-
export const FGCA_NODE_W =
|
|
129
|
-
export const FGCA_NODE_H =
|
|
129
|
+
export const FGCA_NODE_W = ENTITY_NODE_SIZE.normal.width;
|
|
130
|
+
export const FGCA_NODE_H = ENTITY_NODE_SIZE.normal.height;
|
|
130
131
|
export const FGCA_HEADER_H = 32;
|
|
131
132
|
export const FGCA_PAD = 20;
|
|
132
133
|
export const FGCA_DEFAULT_COL_GAP = 160;
|
package/src/node-size-presets.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block/cell size presets for diagram renderers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
export type NodeSizePreset = 'compact' | 'normal' | 'wide';
|
|
@@ -39,82 +42,102 @@ export interface CapabilityMapDimensions {
|
|
|
39
42
|
|
|
40
43
|
type PresetTable<T> = Record<NodeSizePreset, T>;
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
46
50
|
};
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
52
57
|
};
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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'),
|
|
58
68
|
};
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 },
|
|
64
83
|
};
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
cellTextPadY: 10,
|
|
81
|
-
},
|
|
82
|
-
normal: {
|
|
83
|
-
legendColumnWidth: 140,
|
|
84
|
-
stageColumnWidth: 220,
|
|
85
|
-
stageHeaderHeight: 40,
|
|
86
|
-
goalRowHeight: 56,
|
|
87
|
-
resultRowHeight: 56,
|
|
88
|
-
aspectRowMinHeight: 60,
|
|
89
|
-
pillHeight: 40,
|
|
90
|
-
pillGap: 4,
|
|
91
|
-
cellPadding: 8,
|
|
92
|
-
textLineHeight: 17,
|
|
93
|
-
textCharWidth: 7.5,
|
|
94
|
-
cellTextPadX: 10,
|
|
95
|
-
cellTextPadY: 10,
|
|
96
|
-
},
|
|
97
|
-
wide: {
|
|
98
|
-
legendColumnWidth: 160,
|
|
99
|
-
stageColumnWidth: 280,
|
|
100
|
-
stageHeaderHeight: 44,
|
|
101
|
-
goalRowHeight: 60,
|
|
102
|
-
resultRowHeight: 60,
|
|
103
|
-
aspectRowMinHeight: 64,
|
|
104
|
-
pillHeight: 44,
|
|
105
|
-
pillGap: 4,
|
|
106
|
-
cellPadding: 8,
|
|
107
|
-
textLineHeight: 18,
|
|
108
|
-
textCharWidth: 7.5,
|
|
109
|
-
cellTextPadX: 10,
|
|
110
|
-
cellTextPadY: 10,
|
|
111
|
-
},
|
|
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,
|
|
112
99
|
};
|
|
113
100
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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),
|
|
118
141
|
};
|
|
119
142
|
|
|
120
143
|
export type NodeSizeNotation =
|
|
@@ -133,19 +156,19 @@ export function parseNodeSizePreset(value: string | undefined): NodeSizePreset {
|
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
export function resolveGoalsNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
136
|
-
return
|
|
159
|
+
return ENTITY_NODE_SIZE[preset];
|
|
137
160
|
}
|
|
138
161
|
|
|
139
162
|
export function resolveDgcaNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
140
|
-
return
|
|
163
|
+
return ENTITY_NODE_SIZE[preset];
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
export function resolveBlocksLeafSize(preset: NodeSizePreset): BlocksLeafDimensions {
|
|
144
|
-
return
|
|
167
|
+
return ENTITY_NODE_SIZE[preset];
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
export function resolveActionNodeSize(preset: NodeSizePreset): BoxDimensions {
|
|
148
|
-
return
|
|
171
|
+
return ENTITY_NODE_SIZE[preset];
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
export function resolveProcessBlueprintSize(preset: NodeSizePreset): ProcessBlueprintDimensions {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
layoutCenteredEntityText,
|
|
4
|
+
LINE_H_PRIMARY,
|
|
5
|
+
LINE_H_SECONDARY,
|
|
4
6
|
maxCharsForInnerWidth,
|
|
7
|
+
ROW_GROUP_GAP,
|
|
5
8
|
wrapWords,
|
|
6
9
|
wrapTextLines,
|
|
7
10
|
} from '../entity-text-layout.js';
|
|
@@ -18,12 +21,30 @@ describe('entity-text-layout', () => {
|
|
|
18
21
|
expect(maxCharsForInnerWidth(160, 7)).toBeGreaterThan(maxCharsForInnerWidth(120, 7));
|
|
19
22
|
});
|
|
20
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
|
+
|
|
21
42
|
it('layoutCenteredEntityText emits more name capacity on wide boxes', () => {
|
|
22
43
|
const narrow = layoutCenteredEntityText({
|
|
23
44
|
boxX: 0,
|
|
24
45
|
boxY: 0,
|
|
25
46
|
boxWidth: 200,
|
|
26
|
-
boxHeight:
|
|
47
|
+
boxHeight: 80,
|
|
27
48
|
name: 'Improve customer onboarding experience across regions',
|
|
28
49
|
id: 'GOAL-001',
|
|
29
50
|
nameMaxLines: 2,
|
|
@@ -33,7 +54,7 @@ describe('entity-text-layout', () => {
|
|
|
33
54
|
boxX: 0,
|
|
34
55
|
boxY: 0,
|
|
35
56
|
boxWidth: 320,
|
|
36
|
-
boxHeight:
|
|
57
|
+
boxHeight: 80,
|
|
37
58
|
name: 'Improve customer onboarding experience across regions',
|
|
38
59
|
id: 'GOAL-001',
|
|
39
60
|
nameMaxLines: 2,
|
|
@@ -11,6 +11,10 @@ import { describe, expect, it } from 'vitest';
|
|
|
11
11
|
import type { BlocksFile } from '../../blocks/types.js';
|
|
12
12
|
import { renderBlocksSvg } from '../render-blocks.js';
|
|
13
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';
|
|
15
|
+
|
|
16
|
+
const LEAF_W = ENTITY_NODE_SIZE.normal.width;
|
|
17
|
+
const LEAF_H = ENTITY_NODE_SIZE.normal.height;
|
|
14
18
|
|
|
15
19
|
function leafDoc(name: string, id: string): BlocksFile {
|
|
16
20
|
return {
|
|
@@ -76,7 +80,8 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
it('wraps a long ID to at most 2 lines and truncates with …', () => {
|
|
79
|
-
const longId =
|
|
83
|
+
const longId =
|
|
84
|
+
'VERY_LONG_BLOCK_IDENTIFIER_WITH_MANY_SEGMENTS_AND_MORE_AND_MORE_EXTRA_TAIL_TO_FORCE_TRUNCATION';
|
|
80
85
|
const svg = renderBlocksSvg(leafDoc('Short name', longId));
|
|
81
86
|
const idLines = extractLabels(svg).filter((l) => l.cls === 'text-id');
|
|
82
87
|
expect(idLines.length).toBeGreaterThan(0);
|
|
@@ -86,7 +91,7 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
86
91
|
}
|
|
87
92
|
});
|
|
88
93
|
|
|
89
|
-
it(
|
|
94
|
+
it(`keeps every label inside the ${LEAF_W}px leaf width (approximated by CHAR_W * length)`, () => {
|
|
90
95
|
const svg = renderBlocksSvg(
|
|
91
96
|
leafDoc(
|
|
92
97
|
'Personal data records that linger after consent withdrawal across many systems',
|
|
@@ -94,7 +99,7 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
94
99
|
),
|
|
95
100
|
);
|
|
96
101
|
const labels = extractLabels(svg);
|
|
97
|
-
const inner =
|
|
102
|
+
const inner = LEAF_W - TEXT_MARGIN_X * 2;
|
|
98
103
|
for (const l of labels) {
|
|
99
104
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
100
105
|
expect(l.text.length * cw).toBeLessThanOrEqual(inner);
|
|
@@ -126,8 +131,8 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
126
131
|
),
|
|
127
132
|
);
|
|
128
133
|
const labels = extractLabels(svg);
|
|
129
|
-
//
|
|
130
|
-
const containerW =
|
|
134
|
+
// Container width derives from its single leaf child: leafW + 2×padding.
|
|
135
|
+
const containerW = LEAF_W + 24;
|
|
131
136
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
132
137
|
for (const l of labels) {
|
|
133
138
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
@@ -144,7 +149,7 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
144
149
|
'Active systems that are erasure-reachable within the standard 30-day operational window';
|
|
145
150
|
const svg = renderBlocksSvg(containerDoc(longName, 'ACTIVE'));
|
|
146
151
|
const labels = extractLabels(svg);
|
|
147
|
-
const containerW =
|
|
152
|
+
const containerW = LEAF_W + 24;
|
|
148
153
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
149
154
|
const primaries = labels.filter((l) => l.cls === 'text-primary');
|
|
150
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 }],
|
|
@@ -169,7 +169,7 @@ function totalVerticalSpan(groups: TextRowGroup[]): number {
|
|
|
169
169
|
let span = 0;
|
|
170
170
|
for (let i = 0; i < groups.length; i++) {
|
|
171
171
|
const g = groups[i];
|
|
172
|
-
|
|
172
|
+
span += g.lines.length * g.lineHeight;
|
|
173
173
|
if (i < groups.length - 1) span += g.gapAfter;
|
|
174
174
|
}
|
|
175
175
|
return span;
|
|
@@ -223,7 +223,7 @@ export function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): Tex
|
|
|
223
223
|
const g = groups[gi];
|
|
224
224
|
for (let li = 0; li < g.lines.length; li++) {
|
|
225
225
|
out.push({ cls: g.cls, text: g.lines[li], y });
|
|
226
|
-
|
|
226
|
+
y += g.lineHeight;
|
|
227
227
|
}
|
|
228
228
|
if (gi < groups.length - 1) y += g.gapAfter;
|
|
229
229
|
}
|