@transitrix/diagrams 1.8.2 → 1.8.4
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/blocks/layout.js +1 -1
- package/dist/capability-map/render-capability-tree.js +2 -2
- package/dist/theme/__tests__/tokens.test.d.ts +1 -0
- package/dist/theme/__tests__/tokens.test.js +159 -0
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/themes.js +4 -1
- package/dist/theme/tokens.d.ts +107 -45
- package/dist/theme/tokens.js +82 -32
- package/dist/webview/__tests__/entity-text-layout.test.js +96 -1
- package/dist/webview/entity-text-layout.d.ts +31 -3
- package/dist/webview/entity-text-layout.js +123 -55
- package/dist/webview/render-activities.d.ts +1 -1
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-process.d.ts +1 -1
- package/dist/webview/render-process.js +20 -20
- package/package.json +1 -1
- package/src/blocks/layout.ts +1 -1
- package/src/capability-map/render-capability-tree.ts +2 -2
- package/src/theme/__tests__/tokens.test.ts +185 -0
- package/src/theme/index.ts +1 -1
- package/src/theme/themes.ts +4 -0
- package/src/theme/tokens.ts +83 -32
- package/src/webview/__tests__/entity-text-layout.test.ts +109 -0
- package/src/webview/entity-text-layout.ts +140 -54
- package/src/webview/render-activities.ts +2 -2
- package/src/webview/render-process.ts +20 -20
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { layoutCenteredEntityText, LINE_H_PRIMARY, LINE_H_SECONDARY, maxCharsForInnerWidth, ROW_GROUP_GAP, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
|
|
2
|
+
import { layoutCenteredEntityText, layoutLeafBlockText, layoutHeaderBlockText, LINE_H_PRIMARY, LINE_H_SECONDARY, TEXT_MARGIN_Y, maxCharsForInnerWidth, ROW_GROUP_GAP, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
|
|
3
|
+
import { ENTITY_NODE_SIZE } from '../../node-size-presets.js';
|
|
3
4
|
describe('entity-text-layout', () => {
|
|
4
5
|
it('wrapWords respects max lines and truncates with ellipsis', () => {
|
|
5
6
|
const long = 'one two three four five six seven eight nine ten eleven twelve';
|
|
@@ -58,3 +59,97 @@ describe('entity-text-layout', () => {
|
|
|
58
59
|
expect(lines.every((l) => l.length <= 10)).toBe(true);
|
|
59
60
|
});
|
|
60
61
|
});
|
|
62
|
+
// Approximate half-extent (px) of each text class's glyphs around its centre
|
|
63
|
+
// y — font-size / 2, the same heuristic the render-blocks overlap regression
|
|
64
|
+
// test (#419) uses. Used below to assert text never visually crosses the box
|
|
65
|
+
// border, not just that the *nominal* line-height budget fits.
|
|
66
|
+
const HALF_EXTENT = {
|
|
67
|
+
'text-primary': 6, // 12px font
|
|
68
|
+
'text-secondary': 6, // 11px font, rounded up
|
|
69
|
+
'text-id': 5, // 10px font
|
|
70
|
+
};
|
|
71
|
+
describe('entity text layout — never overflows the box, always keeps padding', () => {
|
|
72
|
+
const PRESETS = Object.entries(ENTITY_NODE_SIZE); // compact/normal/wide
|
|
73
|
+
const NAMES = [
|
|
74
|
+
'Short',
|
|
75
|
+
'Achieve full GDPR & NIS2 compliance',
|
|
76
|
+
'A name so long it would need three or more lines to render in full without any height limit at all',
|
|
77
|
+
];
|
|
78
|
+
function assertNoOverflowAndPadded(specs, boxTop, boxBottom) {
|
|
79
|
+
for (const s of specs) {
|
|
80
|
+
const half = HALF_EXTENT[s.cls] ?? 6;
|
|
81
|
+
expect(s.y - half).toBeGreaterThanOrEqual(boxTop);
|
|
82
|
+
expect(s.y + half).toBeLessThanOrEqual(boxBottom);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const [presetName, size] of PRESETS) {
|
|
86
|
+
for (const withType of [false, true]) {
|
|
87
|
+
for (const name of NAMES) {
|
|
88
|
+
it(`layoutCenteredEntityText: ${presetName} preset, type=${withType}, name="${name.slice(0, 20)}…" stays inside the box`, () => {
|
|
89
|
+
const boxY = 0;
|
|
90
|
+
const specs = layoutCenteredEntityText({
|
|
91
|
+
boxX: 0,
|
|
92
|
+
boxY,
|
|
93
|
+
boxWidth: size.width,
|
|
94
|
+
boxHeight: size.height,
|
|
95
|
+
name,
|
|
96
|
+
type: withType ? 'Strategic Goal' : undefined,
|
|
97
|
+
id: 'GOAL-EU-COMPLIANCE-1',
|
|
98
|
+
nameMaxLines: 2,
|
|
99
|
+
idMaxLines: 1,
|
|
100
|
+
});
|
|
101
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + size.height);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
it('reproduces the reported case (wide preset, 2-line name + type + long id) with real padding on all sides', () => {
|
|
107
|
+
const size = ENTITY_NODE_SIZE.normal;
|
|
108
|
+
const boxY = 100;
|
|
109
|
+
const specs = layoutCenteredEntityText({
|
|
110
|
+
boxX: 0,
|
|
111
|
+
boxY,
|
|
112
|
+
boxWidth: size.width,
|
|
113
|
+
boxHeight: size.height,
|
|
114
|
+
name: 'Achieve full GDPR & NIS2 compliance',
|
|
115
|
+
type: 'Project Goal',
|
|
116
|
+
id: 'GOAL-EU-COMPLIANCE-1',
|
|
117
|
+
nameMaxLines: 2,
|
|
118
|
+
idMaxLines: 1,
|
|
119
|
+
});
|
|
120
|
+
const nameLines = specs.filter((s) => s.cls === 'text-primary');
|
|
121
|
+
expect(nameLines.length).toBe(2); // normal preset is tall enough to keep both lines
|
|
122
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + size.height);
|
|
123
|
+
// Explicit padding, not just "doesn't cross the border".
|
|
124
|
+
const firstTop = Math.min(...specs.map((s) => s.y - (HALF_EXTENT[s.cls] ?? 6)));
|
|
125
|
+
const lastBottom = Math.max(...specs.map((s) => s.y + (HALF_EXTENT[s.cls] ?? 6)));
|
|
126
|
+
expect(firstTop - boxY).toBeGreaterThanOrEqual(TEXT_MARGIN_Y);
|
|
127
|
+
expect(boxY + size.height - lastBottom).toBeGreaterThanOrEqual(TEXT_MARGIN_Y);
|
|
128
|
+
});
|
|
129
|
+
it('layoutLeafBlockText stays inside the box for compact preset with a long name and id', () => {
|
|
130
|
+
const size = ENTITY_NODE_SIZE.compact;
|
|
131
|
+
const boxY = 0;
|
|
132
|
+
const specs = layoutLeafBlockText({
|
|
133
|
+
boxX: 0,
|
|
134
|
+
boxY,
|
|
135
|
+
boxWidth: size.width,
|
|
136
|
+
boxHeight: size.height,
|
|
137
|
+
name: 'Personal data records that linger after consent withdrawal across many systems and archives',
|
|
138
|
+
id: 'VERY_LONG_BLOCK_IDENTIFIER_WITH_MANY_SEGMENTS_AND_MORE',
|
|
139
|
+
});
|
|
140
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + size.height);
|
|
141
|
+
});
|
|
142
|
+
it('layoutHeaderBlockText stays inside a short header strip', () => {
|
|
143
|
+
const boxY = 0;
|
|
144
|
+
const headerHeight = 28;
|
|
145
|
+
const specs = layoutHeaderBlockText({
|
|
146
|
+
boxX: 0,
|
|
147
|
+
boxY,
|
|
148
|
+
boxWidth: 250,
|
|
149
|
+
headerHeight,
|
|
150
|
+
name: 'A fairly long container name here',
|
|
151
|
+
id: 'VERY_LONG_CONTAINER_IDENTIFIER',
|
|
152
|
+
});
|
|
153
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + headerHeight);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* Capability Map renderers.
|
|
7
7
|
*/
|
|
8
8
|
export declare const TEXT_MARGIN_X = 8;
|
|
9
|
+
/**
|
|
10
|
+
* Minimum reserved vertical padding (px) between the box's top/bottom edge and
|
|
11
|
+
* the nearest line's outer extent. Always enforced — see
|
|
12
|
+
* {@link layoutCenteredEntityText}'s adaptive line-count reduction, which
|
|
13
|
+
* trades name-line count for this padding rather than letting text touch or
|
|
14
|
+
* cross the box border.
|
|
15
|
+
*/
|
|
16
|
+
export declare const TEXT_MARGIN_Y = 4;
|
|
9
17
|
/** Approximate character width (px) at text-primary size (12px/600). */
|
|
10
18
|
export declare const CHAR_W_PRIMARY = 7;
|
|
11
19
|
/** Approximate character width (px) at text-id size (10px/600). */
|
|
@@ -18,10 +26,15 @@ export declare const LINE_H_PRIMARY = 14;
|
|
|
18
26
|
export declare const LINE_H_SECONDARY = 14;
|
|
19
27
|
/** Vertical advance between id line centres (text-id). */
|
|
20
28
|
export declare const LINE_H_ID = 12;
|
|
21
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Gap between the name block and the id block in leaf-style layouts — used as
|
|
31
|
+
* a direct centre-to-centre distance there, so keep it >= ~11px (half of
|
|
32
|
+
* text-primary's 12px font + half of text-id's 10px font) to avoid the
|
|
33
|
+
* overlap regressed in #419. Do not tighten without re-checking that test.
|
|
34
|
+
*/
|
|
22
35
|
export declare const NAME_ID_GAP = 14;
|
|
23
36
|
/** Gap between stacked row groups (name → type → id). */
|
|
24
|
-
export declare const ROW_GROUP_GAP =
|
|
37
|
+
export declare const ROW_GROUP_GAP = 4;
|
|
25
38
|
export declare function maxCharsForInnerWidth(innerWidth: number, charWidth: number): number;
|
|
26
39
|
/** Word-wrap `text` to at most `maxLines` lines of `maxChars` each. */
|
|
27
40
|
export declare function wrapWords(text: string, maxChars: number, maxLines: number): string[];
|
|
@@ -55,10 +68,20 @@ export interface LayoutCenteredEntityOptions {
|
|
|
55
68
|
nameMaxLines?: number;
|
|
56
69
|
idMaxLines?: number;
|
|
57
70
|
marginX?: number;
|
|
71
|
+
marginY?: number;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Layout centred entity text (name → optional type → id) inside a fixed box.
|
|
61
75
|
* Returns absolute SVG y-positions (line centres) for each rendered row.
|
|
76
|
+
*
|
|
77
|
+
* Guarantees, in order of priority:
|
|
78
|
+
* 1. Content never crosses the box's top/bottom edge.
|
|
79
|
+
* 2. At least `marginY` (default {@link TEXT_MARGIN_Y}) of padding is kept
|
|
80
|
+
* above the first line's glyphs and below the last line's glyphs.
|
|
81
|
+
* 3. The name gets as many lines as `nameMaxLines` allows (2 by default)
|
|
82
|
+
* when the box is tall enough; on short/compact boxes (e.g. with a type
|
|
83
|
+
* row present) it degrades to fewer name lines — trading line count,
|
|
84
|
+
* not padding — rather than letting text overflow.
|
|
62
85
|
*/
|
|
63
86
|
export declare function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): TextLineSpec[];
|
|
64
87
|
/** Emit centred `<text>` elements from {@link layoutCenteredEntityText} specs. */
|
|
@@ -73,7 +96,12 @@ export interface LayoutLeafBlockTextOptions {
|
|
|
73
96
|
nameMaxLines?: number;
|
|
74
97
|
marginX?: number;
|
|
75
98
|
}
|
|
76
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Leaf nested-block layout: name (up to 3 lines) + id (up to 2 lines), centred.
|
|
101
|
+
* Degrades name line count (never below 1) when the box is too short to hold
|
|
102
|
+
* the requested max plus {@link TEXT_MARGIN_Y} padding on both edges, so text
|
|
103
|
+
* never crosses the box border.
|
|
104
|
+
*/
|
|
77
105
|
export declare function layoutLeafBlockText(opts: LayoutLeafBlockTextOptions): TextLineSpec[];
|
|
78
106
|
export interface LayoutHeaderBlockTextOptions {
|
|
79
107
|
boxX: number;
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* Capability Map renderers.
|
|
7
7
|
*/
|
|
8
8
|
export const TEXT_MARGIN_X = 8;
|
|
9
|
+
/**
|
|
10
|
+
* Minimum reserved vertical padding (px) between the box's top/bottom edge and
|
|
11
|
+
* the nearest line's outer extent. Always enforced — see
|
|
12
|
+
* {@link layoutCenteredEntityText}'s adaptive line-count reduction, which
|
|
13
|
+
* trades name-line count for this padding rather than letting text touch or
|
|
14
|
+
* cross the box border.
|
|
15
|
+
*/
|
|
16
|
+
export const TEXT_MARGIN_Y = 4;
|
|
9
17
|
/** Approximate character width (px) at text-primary size (12px/600). */
|
|
10
18
|
export const CHAR_W_PRIMARY = 7;
|
|
11
19
|
/** Approximate character width (px) at text-id size (10px/600). */
|
|
@@ -18,10 +26,15 @@ export const LINE_H_PRIMARY = 14;
|
|
|
18
26
|
export const LINE_H_SECONDARY = 14;
|
|
19
27
|
/** Vertical advance between id line centres (text-id). */
|
|
20
28
|
export const LINE_H_ID = 12;
|
|
21
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Gap between the name block and the id block in leaf-style layouts — used as
|
|
31
|
+
* a direct centre-to-centre distance there, so keep it >= ~11px (half of
|
|
32
|
+
* text-primary's 12px font + half of text-id's 10px font) to avoid the
|
|
33
|
+
* overlap regressed in #419. Do not tighten without re-checking that test.
|
|
34
|
+
*/
|
|
22
35
|
export const NAME_ID_GAP = 14;
|
|
23
36
|
/** Gap between stacked row groups (name → type → id). */
|
|
24
|
-
export const ROW_GROUP_GAP =
|
|
37
|
+
export const ROW_GROUP_GAP = 4;
|
|
25
38
|
export function maxCharsForInnerWidth(innerWidth, charWidth) {
|
|
26
39
|
return Math.max(4, Math.floor(Math.max(0, innerWidth) / charWidth));
|
|
27
40
|
}
|
|
@@ -136,67 +149,100 @@ export function truncateId(id, maxChars) {
|
|
|
136
149
|
const sepIdx = Math.max(cut.lastIndexOf('_'), cut.lastIndexOf('-'));
|
|
137
150
|
return `${sepIdx > safeMax / 3 ? cut.slice(0, sepIdx) : cut}…`;
|
|
138
151
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Half-extent (px) of rendered glyphs around their centre y — approximately
|
|
154
|
+
* font-size / 2, the same heuristic the #419 non-overlap regression test
|
|
155
|
+
* uses. Used only to keep the *outermost* line's visual extent (not just its
|
|
156
|
+
* centre) inside the box when computing edge padding; inter-line spacing
|
|
157
|
+
* between adjacent rows is governed separately by the LINE_H_ constants,
|
|
158
|
+
* NAME_ID_GAP and ROW_GROUP_GAP, which already keep centres far enough apart
|
|
159
|
+
* to avoid overlap.
|
|
160
|
+
*/
|
|
161
|
+
const HALF_EXTENT_PRIMARY = 6; // text-primary, 12px font
|
|
162
|
+
const HALF_EXTENT_ID = 5; // text-id, 10px font
|
|
163
|
+
/** Place `groups`' lines starting at `startY`, advancing by each line's own height and gap. */
|
|
164
|
+
function placeLines(groups, startY) {
|
|
165
|
+
const out = [];
|
|
166
|
+
let y = startY;
|
|
167
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
168
|
+
const g = groups[gi];
|
|
169
|
+
for (let li = 0; li < g.lines.length; li++) {
|
|
170
|
+
out.push({ cls: g.cls, text: g.lines[li], y });
|
|
171
|
+
y += g.lineHeight;
|
|
172
|
+
}
|
|
173
|
+
if (gi < groups.length - 1)
|
|
174
|
+
y += g.gapAfter;
|
|
146
175
|
}
|
|
147
|
-
return
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** Centre-to-centre distance from the first to the last placed line. */
|
|
179
|
+
function centerSpan(groups) {
|
|
180
|
+
const ys = placeLines(groups, 0).map((s) => s.y);
|
|
181
|
+
return ys[ys.length - 1] - ys[0];
|
|
148
182
|
}
|
|
149
183
|
/**
|
|
150
184
|
* Layout centred entity text (name → optional type → id) inside a fixed box.
|
|
151
185
|
* Returns absolute SVG y-positions (line centres) for each rendered row.
|
|
186
|
+
*
|
|
187
|
+
* Guarantees, in order of priority:
|
|
188
|
+
* 1. Content never crosses the box's top/bottom edge.
|
|
189
|
+
* 2. At least `marginY` (default {@link TEXT_MARGIN_Y}) of padding is kept
|
|
190
|
+
* above the first line's glyphs and below the last line's glyphs.
|
|
191
|
+
* 3. The name gets as many lines as `nameMaxLines` allows (2 by default)
|
|
192
|
+
* when the box is tall enough; on short/compact boxes (e.g. with a type
|
|
193
|
+
* row present) it degrades to fewer name lines — trading line count,
|
|
194
|
+
* not padding — rather than letting text overflow.
|
|
152
195
|
*/
|
|
153
196
|
export function layoutCenteredEntityText(opts) {
|
|
154
197
|
const marginX = opts.marginX ?? TEXT_MARGIN_X;
|
|
198
|
+
const marginY = opts.marginY ?? TEXT_MARGIN_Y;
|
|
155
199
|
const innerW = Math.max(0, opts.boxWidth - marginX * 2);
|
|
156
|
-
const cx = opts.boxX + opts.boxWidth / 2;
|
|
157
200
|
const maxCharsName = maxCharsForInnerWidth(innerW, CHAR_W_PRIMARY);
|
|
158
201
|
const maxCharsType = maxCharsForInnerWidth(innerW, CHAR_W_SECONDARY);
|
|
159
202
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
160
|
-
const nameLines = wrapWords(opts.name, maxCharsName, opts.nameMaxLines ?? 2);
|
|
161
203
|
const typeLine = opts.type?.trim() ? truncateLine(opts.type, maxCharsType) : undefined;
|
|
162
204
|
const idMaxLines = opts.idMaxLines ?? 1;
|
|
163
205
|
const idLines = idMaxLines <= 1 ? [truncateId(opts.id, maxCharsId)] : wrapId(opts.id, maxCharsId, idMaxLines);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
206
|
+
function buildGroups(nameLines) {
|
|
207
|
+
const groups = [
|
|
208
|
+
{
|
|
209
|
+
cls: 'text-primary',
|
|
210
|
+
lines: nameLines,
|
|
211
|
+
lineHeight: LINE_H_PRIMARY,
|
|
212
|
+
gapAfter: typeLine ? ROW_GROUP_GAP : NAME_ID_GAP,
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
if (typeLine) {
|
|
216
|
+
groups.push({
|
|
217
|
+
cls: 'text-secondary',
|
|
218
|
+
lines: [typeLine],
|
|
219
|
+
lineHeight: LINE_H_SECONDARY,
|
|
220
|
+
gapAfter: NAME_ID_GAP,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
173
223
|
groups.push({
|
|
174
|
-
cls: 'text-
|
|
175
|
-
lines:
|
|
176
|
-
lineHeight:
|
|
177
|
-
gapAfter:
|
|
224
|
+
cls: 'text-id',
|
|
225
|
+
lines: idLines,
|
|
226
|
+
lineHeight: LINE_H_ID,
|
|
227
|
+
gapAfter: 0,
|
|
178
228
|
});
|
|
229
|
+
return groups;
|
|
179
230
|
}
|
|
180
|
-
groups
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const g = groups[gi];
|
|
192
|
-
for (let li = 0; li < g.lines.length; li++) {
|
|
193
|
-
out.push({ cls: g.cls, text: g.lines[li], y });
|
|
194
|
-
y += g.lineHeight;
|
|
195
|
-
}
|
|
196
|
-
if (gi < groups.length - 1)
|
|
197
|
-
y += g.gapAfter;
|
|
231
|
+
// groups[0] is always the name (text-primary) and the last group is always
|
|
232
|
+
// the id (text-id), so the edge-padding half-extents below are fixed.
|
|
233
|
+
const requestedNameMaxLines = Math.max(1, opts.nameMaxLines ?? 2);
|
|
234
|
+
const availableHeight = Math.max(0, opts.boxHeight - marginY * 2);
|
|
235
|
+
let nameLines = wrapWords(opts.name, maxCharsName, requestedNameMaxLines);
|
|
236
|
+
let groups = buildGroups(nameLines);
|
|
237
|
+
let contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
238
|
+
for (let cap = requestedNameMaxLines - 1; cap >= 1 && contentSpan > availableHeight; cap--) {
|
|
239
|
+
nameLines = wrapWords(opts.name, maxCharsName, cap);
|
|
240
|
+
groups = buildGroups(nameLines);
|
|
241
|
+
contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
198
242
|
}
|
|
199
|
-
|
|
243
|
+
const margin = Math.max(0, (opts.boxHeight - contentSpan) / 2);
|
|
244
|
+
const firstY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
245
|
+
return placeLines(groups, firstY);
|
|
200
246
|
}
|
|
201
247
|
/** Emit centred `<text>` elements from {@link layoutCenteredEntityText} specs. */
|
|
202
248
|
export function emitCenteredTextSvg(specs, centerX, esc) {
|
|
@@ -208,28 +254,48 @@ export function emitCenteredTextSvg(specs, centerX, esc) {
|
|
|
208
254
|
})
|
|
209
255
|
.join('\n');
|
|
210
256
|
}
|
|
211
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Leaf nested-block layout: name (up to 3 lines) + id (up to 2 lines), centred.
|
|
259
|
+
* Degrades name line count (never below 1) when the box is too short to hold
|
|
260
|
+
* the requested max plus {@link TEXT_MARGIN_Y} padding on both edges, so text
|
|
261
|
+
* never crosses the box border.
|
|
262
|
+
*/
|
|
212
263
|
export function layoutLeafBlockText(opts) {
|
|
213
264
|
const marginX = opts.marginX ?? TEXT_MARGIN_X;
|
|
214
265
|
const innerW = Math.max(0, opts.boxWidth - marginX * 2);
|
|
215
266
|
const maxCharsName = maxCharsForInnerWidth(innerW, CHAR_W_PRIMARY);
|
|
216
267
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
217
|
-
const nameLines = wrapWords(opts.name, maxCharsName, opts.nameMaxLines ?? 3);
|
|
218
268
|
const idLines = wrapId(opts.id, maxCharsId, 2);
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
269
|
+
const idCenterSpan = (idLines.length - 1) * LINE_H_ID;
|
|
270
|
+
const requestedNameMaxLines = Math.max(1, opts.nameMaxLines ?? 3);
|
|
271
|
+
const availableHeight = Math.max(0, opts.boxHeight - TEXT_MARGIN_Y * 2);
|
|
272
|
+
let nameLines = wrapWords(opts.name, maxCharsName, requestedNameMaxLines);
|
|
273
|
+
let nameCenterSpan = (nameLines.length - 1) * LINE_H_PRIMARY;
|
|
274
|
+
let contentSpan = nameCenterSpan + NAME_ID_GAP + idCenterSpan + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
275
|
+
for (let cap = requestedNameMaxLines - 1; cap >= 1 && contentSpan > availableHeight; cap--) {
|
|
276
|
+
nameLines = wrapWords(opts.name, maxCharsName, cap);
|
|
277
|
+
nameCenterSpan = (nameLines.length - 1) * LINE_H_PRIMARY;
|
|
278
|
+
contentSpan = nameCenterSpan + NAME_ID_GAP + idCenterSpan + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
279
|
+
}
|
|
280
|
+
const margin = Math.max(0, (opts.boxHeight - contentSpan) / 2);
|
|
281
|
+
const firstY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
223
282
|
const out = [];
|
|
224
283
|
for (let i = 0; i < nameLines.length; i++) {
|
|
225
284
|
out.push({ cls: 'text-primary', text: nameLines[i], y: firstY + i * LINE_H_PRIMARY });
|
|
226
285
|
}
|
|
227
|
-
const idFirstY = firstY +
|
|
286
|
+
const idFirstY = firstY + nameCenterSpan + NAME_ID_GAP;
|
|
228
287
|
for (let i = 0; i < idLines.length; i++) {
|
|
229
288
|
out.push({ cls: 'text-id', text: idLines[i], y: idFirstY + i * LINE_H_ID });
|
|
230
289
|
}
|
|
231
290
|
return out;
|
|
232
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Direct centre-to-centre gap between the header's name and id lines.
|
|
294
|
+
* Safely above the ~11px non-overlap floor (half of text-primary's 12px font
|
|
295
|
+
* + half of text-id's 10px font, per the #419 regression) but tighter than
|
|
296
|
+
* {@link NAME_ID_GAP} to fit the compact header strip.
|
|
297
|
+
*/
|
|
298
|
+
const HEADER_NAME_ID_GAP = 12;
|
|
233
299
|
/** Container header: single-line name + id stacked in the header strip. */
|
|
234
300
|
export function layoutHeaderBlockText(opts) {
|
|
235
301
|
const marginX = opts.marginX ?? TEXT_MARGIN_X;
|
|
@@ -238,11 +304,13 @@ export function layoutHeaderBlockText(opts) {
|
|
|
238
304
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
239
305
|
const nameText = truncateLine(opts.name, maxCharsName);
|
|
240
306
|
const idText = truncateId(opts.id, maxCharsId);
|
|
241
|
-
const
|
|
242
|
-
const
|
|
307
|
+
const contentSpan = HEADER_NAME_ID_GAP + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
308
|
+
const margin = Math.max(0, (opts.headerHeight - contentSpan) / 2);
|
|
309
|
+
const nameY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
310
|
+
const idY = nameY + HEADER_NAME_ID_GAP;
|
|
243
311
|
return [
|
|
244
|
-
{ cls: 'text-primary', text: nameText, y:
|
|
245
|
-
{ cls: 'text-id', text: idText, y:
|
|
312
|
+
{ cls: 'text-primary', text: nameText, y: nameY },
|
|
313
|
+
{ cls: 'text-id', text: idText, y: idY },
|
|
246
314
|
];
|
|
247
315
|
}
|
|
248
316
|
/**
|
|
@@ -7,7 +7,7 @@ import { type NodeSizePreset } from '../node-size-presets.js';
|
|
|
7
7
|
* network subset. Embedded alongside the shared theme CSS so the SVG is
|
|
8
8
|
* self-contained for the JCEF host.
|
|
9
9
|
*/
|
|
10
|
-
export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #
|
|
10
|
+
export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }\n .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
|
|
11
11
|
/** Arrowhead marker defs shared by both hosts for the network view. */
|
|
12
12
|
export declare const ACTIVITIES_NETWORK_DEFS = "<defs>\n <marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill\"/>\n </marker>\n <marker id=\"arrow-crit\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill-critical\"/>\n </marker>\n</defs>";
|
|
13
13
|
/**
|
|
@@ -31,8 +31,8 @@ const N_PAD = 24;
|
|
|
31
31
|
* self-contained for the JCEF host.
|
|
32
32
|
*/
|
|
33
33
|
export const ACTIVITIES_NETWORK_CSS = `
|
|
34
|
-
.act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #
|
|
35
|
-
.critical-node { fill: #
|
|
34
|
+
.act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }
|
|
35
|
+
.critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
|
|
36
36
|
.milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
|
|
37
37
|
.critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
|
|
38
38
|
.arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
|
|
@@ -70,7 +70,7 @@ export interface RenderProcessOptions {
|
|
|
70
70
|
/** Extra vertical space (px) reserved above the pool for a title block. */
|
|
71
71
|
topInset?: number;
|
|
72
72
|
}
|
|
73
|
-
export declare const BPMN_PROCESS_CSS = "\n.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#
|
|
73
|
+
export declare const BPMN_PROCESS_CSS = "\n.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 700; }\n.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 600; }\n.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; text-anchor: middle; }\n.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }\n.bpmn-event-start { stroke-width: 1.5; }\n.bpmn-event-end { stroke-width: 4; }\n.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }\n.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }\n.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }\n.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }\n.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }\n.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }\n";
|
|
74
74
|
/**
|
|
75
75
|
* Canonical body emitter, shared by every host (VS Code, JCEF, static export).
|
|
76
76
|
*
|
|
@@ -35,28 +35,28 @@ const LABEL_LINE_H = 13;
|
|
|
35
35
|
// BPMN-specific CSS — no font-style:italic per CLAUDE.md design rule
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
37
|
export const BPMN_PROCESS_CSS = `
|
|
38
|
-
.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#
|
|
39
|
-
.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#
|
|
40
|
-
.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#
|
|
41
|
-
.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#
|
|
42
|
-
.bpmn-pool-label { fill: var(--ts-text-primary,#
|
|
43
|
-
.bpmn-lane-label { fill: var(--ts-text-primary,#
|
|
44
|
-
.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#
|
|
45
|
-
.bpmn-task-name { fill: var(--ts-text-primary,#
|
|
46
|
-
.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#
|
|
38
|
+
.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
39
|
+
.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
40
|
+
.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
41
|
+
.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
42
|
+
.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 700; }
|
|
43
|
+
.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; font-weight: 600; }
|
|
44
|
+
.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
45
|
+
.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-size: 11px; text-anchor: middle; }
|
|
46
|
+
.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }
|
|
47
47
|
.bpmn-event-start { stroke-width: 1.5; }
|
|
48
48
|
.bpmn-event-end { stroke-width: 4; }
|
|
49
|
-
.bpmn-event-label { fill: var(--ts-text-secondary,#
|
|
50
|
-
.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#
|
|
51
|
-
.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#
|
|
52
|
-
.bpmn-gateway-label { fill: var(--ts-text-secondary,#
|
|
53
|
-
.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#
|
|
54
|
-
.bpmn-default-mark { stroke: var(--ts-edge-stroke,#
|
|
55
|
-
.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#
|
|
56
|
-
.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#
|
|
57
|
-
.bpmn-data-obj-label { fill: var(--ts-text-secondary,#
|
|
58
|
-
.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#
|
|
59
|
-
.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#
|
|
49
|
+
.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
|
|
50
|
+
.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
51
|
+
.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }
|
|
52
|
+
.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
|
|
53
|
+
.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }
|
|
54
|
+
.bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }
|
|
55
|
+
.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }
|
|
56
|
+
.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
|
|
57
|
+
.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
|
|
58
|
+
.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
|
|
59
|
+
.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
|
|
60
60
|
`;
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
// Internal helpers
|
package/package.json
CHANGED
package/src/blocks/layout.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function renderCapabilityTreeSvg(
|
|
|
82
82
|
: '';
|
|
83
83
|
|
|
84
84
|
return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
|
|
85
|
-
<rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#
|
|
85
|
+
<rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#004d67)" stroke-width="3"/>
|
|
86
86
|
<rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
|
|
87
87
|
<text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
|
|
88
88
|
<text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
|
|
@@ -102,7 +102,7 @@ export function renderCapabilityTreeSvg(
|
|
|
102
102
|
const bx = legendX;
|
|
103
103
|
legendX += bw;
|
|
104
104
|
if (count === 0) return '';
|
|
105
|
-
return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#
|
|
105
|
+
return `<rect class="tree-level-${i}" x="${bx}" y="${legendY}" width="${bw}" height="24" rx="4" stroke="var(--ts-node-stroke,#004d67)" stroke-width="1"/>
|
|
106
106
|
<text class="text-id" x="${bx + bw / 2}" y="${legendY + 12}" text-anchor="middle" dominant-baseline="central">${escXml(BAND_LABELS[i])}</text>`;
|
|
107
107
|
}).filter(Boolean).join('\n');
|
|
108
108
|
|