@transitrix/diagrams 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/activities/cpm.js +11 -2
  2. package/dist/activities/gantt.js +6 -3
  3. package/dist/activities/types.d.ts +1 -0
  4. package/dist/activities/validate.js +3 -3
  5. package/dist/activity-card/__tests__/resolver.test.js +22 -1
  6. package/dist/activity-card/resolver.js +11 -5
  7. package/dist/compliance/__tests__/impact.test.js +198 -0
  8. package/dist/compliance/html.js +1 -7
  9. package/dist/compliance/impact.d.ts +24 -1
  10. package/dist/compliance/impact.js +97 -21
  11. package/dist/factor/__tests__/validate.test.js +3 -3
  12. package/dist/factor/types.d.ts +1 -1
  13. package/dist/factor/validate.js +2 -2
  14. package/dist/fgca/__tests__/chain-table.test.js +2 -2
  15. package/dist/fgca/__tests__/layout.test.js +11 -11
  16. package/dist/fgca/__tests__/preview-layout.test.js +7 -7
  17. package/dist/fgca/__tests__/resolver.test.js +2 -2
  18. package/dist/fgca/chain-table.d.ts +1 -1
  19. package/dist/fgca/chain-table.js +4 -4
  20. package/dist/fgca/index.d.ts +2 -2
  21. package/dist/fgca/index.js +1 -1
  22. package/dist/fgca/layout.d.ts +2 -2
  23. package/dist/fgca/layout.js +9 -9
  24. package/dist/fgca/nodeTypes.d.ts +1 -1
  25. package/dist/fgca/nodeTypes.js +2 -2
  26. package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
  27. package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
  28. package/dist/fgca/parse-canonical.js +15 -10
  29. package/dist/fgca/preview-layout.d.ts +1 -1
  30. package/dist/fgca/preview-layout.js +4 -4
  31. package/dist/fgca/resolver.js +3 -1
  32. package/dist/fgca/types.d.ts +11 -11
  33. package/dist/fgca/types.js +2 -2
  34. package/dist/fgca/validate.d.ts +2 -2
  35. package/dist/fgca/validate.js +17 -14
  36. package/dist/process-blueprint/index.d.ts +1 -1
  37. package/dist/process-blueprint/layout.js +64 -37
  38. package/dist/process-blueprint/types.d.ts +14 -0
  39. package/dist/scenarios/index.d.ts +1 -1
  40. package/dist/scenarios/types.d.ts +4 -4
  41. package/dist/theme/themes.js +2 -0
  42. package/dist/theme/tokens.d.ts +7 -1
  43. package/dist/theme/tokens.js +3 -1
  44. package/dist/webview/__tests__/entry-notations.test.js +2 -0
  45. package/dist/webview/__tests__/render-process.test.d.ts +1 -0
  46. package/dist/webview/__tests__/render-process.test.js +463 -0
  47. package/dist/webview/__tests__/render-util.test.d.ts +1 -0
  48. package/dist/webview/__tests__/render-util.test.js +54 -0
  49. package/dist/webview/entry.d.ts +1 -1
  50. package/dist/webview/entry.js +9 -5
  51. package/dist/webview/render-activities.d.ts +21 -1
  52. package/dist/webview/render-activities.js +91 -56
  53. package/dist/webview/render-activity-card.d.ts +11 -1
  54. package/dist/webview/render-activity-card.js +11 -2
  55. package/dist/webview/render-blocks.d.ts +24 -1
  56. package/dist/webview/render-blocks.js +58 -24
  57. package/dist/webview/render-fgca.d.ts +27 -0
  58. package/dist/webview/render-fgca.js +29 -15
  59. package/dist/webview/render-goals.d.ts +26 -1
  60. package/dist/webview/render-goals.js +30 -20
  61. package/dist/webview/render-process-blueprint.d.ts +11 -1
  62. package/dist/webview/render-process-blueprint.js +118 -53
  63. package/dist/webview/render-process.d.ts +95 -0
  64. package/dist/webview/render-process.js +392 -0
  65. package/dist/webview/render-scenarios.js +2 -2
  66. package/dist/webview/render-util.d.ts +19 -6
  67. package/dist/webview/render-util.js +23 -15
  68. package/package.json +15 -1
  69. package/src/activities/cpm.ts +14 -2
  70. package/src/activities/gantt.ts +6 -5
  71. package/src/activities/types.ts +1 -0
  72. package/src/activities/validate.ts +3 -3
  73. package/src/activity-card/__tests__/resolver.test.ts +24 -1
  74. package/src/activity-card/resolver.ts +12 -5
  75. package/src/compliance/__tests__/impact.test.ts +219 -0
  76. package/src/compliance/html.ts +1 -8
  77. package/src/compliance/impact.ts +141 -23
  78. package/src/factor/__tests__/validate.test.ts +3 -3
  79. package/src/factor/types.ts +1 -1
  80. package/src/factor/validate.ts +2 -2
  81. package/src/fgca/__tests__/chain-table.test.ts +2 -2
  82. package/src/fgca/__tests__/layout.test.ts +13 -13
  83. package/src/fgca/__tests__/preview-layout.test.ts +7 -7
  84. package/src/fgca/__tests__/resolver.test.ts +2 -2
  85. package/src/fgca/chain-table.ts +5 -5
  86. package/src/fgca/index.ts +3 -3
  87. package/src/fgca/layout.ts +11 -11
  88. package/src/fgca/nodeTypes.ts +3 -3
  89. package/src/fgca/nodes/FGCAFactorNode.tsx +6 -6
  90. package/src/fgca/parse-canonical.ts +17 -12
  91. package/src/fgca/preview-layout.ts +5 -5
  92. package/src/fgca/resolver.ts +3 -1
  93. package/src/fgca/types.ts +13 -13
  94. package/src/fgca/validate.ts +17 -14
  95. package/src/process-blueprint/index.ts +1 -0
  96. package/src/process-blueprint/layout.ts +71 -42
  97. package/src/process-blueprint/types.ts +15 -0
  98. package/src/scenarios/index.ts +2 -2
  99. package/src/scenarios/types.ts +4 -4
  100. package/src/scenarios/validate.ts +3 -3
  101. package/src/theme/themes.ts +2 -0
  102. package/src/theme/tokens.ts +3 -1
  103. package/src/webview/__tests__/entry-notations.test.ts +2 -0
  104. package/src/webview/__tests__/render-process.test.ts +538 -0
  105. package/src/webview/__tests__/render-util.test.ts +66 -0
  106. package/src/webview/entry.ts +11 -5
  107. package/src/webview/render-activities.ts +107 -67
  108. package/src/webview/render-activity-card.ts +11 -2
  109. package/src/webview/render-blocks.ts +78 -28
  110. package/src/webview/render-fgca.ts +52 -28
  111. package/src/webview/render-goals.ts +45 -23
  112. package/src/webview/render-process-blueprint.ts +156 -82
  113. package/src/webview/render-process.ts +507 -0
  114. package/src/webview/render-scenarios.ts +3 -3
  115. package/src/webview/render-util.ts +27 -14
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Host-neutral SVG renderer for the Process Blueprint notation.
3
3
  *
4
- * Step 4 of the IntelliJ epic (ADR 0001): ported from the pure SVG core of
5
- * `extension/src/process-blueprint-preview.ts` (`layoutToSvg`). The VS Code
6
- * title block (`svg-title-block.ts`) is intentionally dropped — JCEF has no
7
- * VS Code APIs — and replaced with `render-goals.ts`' simple optional
8
- * `<text class="text-header">` heading. No `vscode`, no `node:*`, no host APIs;
9
- * the shared theme CSS is embedded inside the `<svg>` so the output is
10
- * self-contained.
4
+ * Single-emitter unification (review C): the canonical blueprint body lives
5
+ * here in `renderProcessBlueprintBody` and is shared by both hosts. The VS Code
6
+ * preview (`extension/src/process-blueprint-preview.ts`) wraps it with the VS
7
+ * Code title block; this module wraps it with `render-goals.ts`' simple
8
+ * optional `<text class="text-header">` heading and embeds the shared theme CSS
9
+ * (plus the blueprint-specific `bp-*` rules) so the JCEF output is
10
+ * self-contained. No `vscode`, no `node:*`, no host APIs.
11
11
  */
12
12
  import { layoutProcessBlueprint } from '../process-blueprint/layout.js';
13
13
  import type {
@@ -20,6 +20,15 @@ import { escXml } from './render-util.js';
20
20
 
21
21
  const PAD = 24;
22
22
 
23
+ // Blueprint-specific rules not carried by the shared theme CSS. The outer
24
+ // border is drawn last on top of the clipped inner content; the row
25
+ // backgrounds are transparent (pills/chips carry the colour). Mirrors
26
+ // `BLUEPRINT_CSS` from the VS Code preview so the self-contained JCEF output
27
+ // matches the editor.
28
+ const BP_EMBED_CSS = `
29
+ .bp-row-bg { fill: none; }
30
+ .bp-border { fill: none; stroke: var(--ts-border, #cbd5e1); stroke-width: 1; }`;
31
+
23
32
  function truncate(text: string, maxChars: number): string {
24
33
  if (text.length <= maxChars) return text;
25
34
  return text.slice(0, Math.max(0, maxChars - 1)) + '…';
@@ -42,11 +51,13 @@ function textCellSvg(
42
51
  const tspans = ls
43
52
  .map((ln, i) => `<tspan x="${x}" y="${first + i * lineHeight}">${escXml(ln)}</tspan>`)
44
53
  .join('');
45
- return `<text class="${cls}" style="dominant-baseline:central">${tspans}</text>`;
54
+ return `<text class="${cls}" dominant-baseline="central">${tspans}</text>`;
46
55
  }
47
56
 
48
57
  /**
49
- * Render a single compliance law chip.
58
+ * Render a single compliance law chip wrapped in a `<g data-chip-law…>` so the
59
+ * VS Code host can wire click-to-drill-down. Other hosts (JCEF) simply ignore
60
+ * the data attributes.
50
61
  *
51
62
  * Three orthogonal decorations are expressed as SVG overrides:
52
63
  * - `new` — dashed stroke border (`stroke-dasharray="4 2"`)
@@ -54,26 +65,28 @@ function textCellSvg(
54
65
  * - `deadline` — urgent fill override (`class="…compliance-deadline"`) + a small
55
66
  * badge circle in the top-right corner
56
67
  */
57
- function complianceChipSvg(chip: ComplianceChip, ox: number, oy: number): string {
68
+ function complianceChipSvg(
69
+ chip: ComplianceChip,
70
+ ox: number,
71
+ oy: number,
72
+ stageId: string,
73
+ ): string {
58
74
  const { x, y, width, height, lawId, decorations } = chip;
59
75
  const ax = x + ox;
60
76
  const ay = y + oy;
61
- const rx = 6;
62
77
  const hasNew = decorations.includes('new');
63
78
  const hasGap = decorations.includes('gap');
64
79
  const hasDeadline = decorations.includes('deadline');
65
-
66
80
  let rectClass = 'diagram-node level-5 compliance-chip';
67
81
  if (hasDeadline) rectClass += ' compliance-deadline';
68
82
  else if (hasGap) rectClass += ' compliance-gap';
69
-
70
83
  const strokeDash = hasNew ? ' stroke-dasharray="4 2"' : '';
71
84
  const parts: string[] = [];
72
85
  parts.push(
73
- `<rect class="${rectClass}" x="${ax}" y="${ay}" width="${width}" height="${height}" rx="${rx}"${strokeDash}/>`,
86
+ `<rect class="${rectClass}" x="${ax}" y="${ay}" width="${width}" height="${height}" rx="6"${strokeDash}/>`,
74
87
  );
75
88
  parts.push(
76
- `<text class="text-pill" x="${ax + width / 2}" y="${ay + height / 2}" text-anchor="middle" style="dominant-baseline:central">${escXml(truncate(lawId, Math.floor(width / 8)))}</text>`,
89
+ `<text class="text-pill" x="${ax + width / 2}" y="${ay + height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncate(lawId, Math.floor(width / 8)))}</text>`,
77
90
  );
78
91
  if (hasDeadline) {
79
92
  const br = 5;
@@ -81,127 +94,188 @@ function complianceChipSvg(chip: ComplianceChip, ox: number, oy: number): string
81
94
  const by = ay + br + 3;
82
95
  parts.push(`<circle class="compliance-badge" cx="${bx}" cy="${by}" r="${br}"/>`);
83
96
  parts.push(
84
- `<text class="compliance-badge-text" x="${bx}" y="${by}" text-anchor="middle" style="dominant-baseline:central">!</text>`,
97
+ `<text class="compliance-badge-text" x="${bx}" y="${by}" text-anchor="middle" dominant-baseline="central">!</text>`,
85
98
  );
86
99
  }
87
- return parts.join('\n');
100
+ return `<g data-chip-law="${escXml(lawId)}" data-chip-stage="${escXml(stageId)}">\n${parts.join('\n')}\n</g>`;
88
101
  }
89
102
 
90
- export interface RenderProcessBlueprintOptions {
91
- title?: string;
92
- }
93
-
94
- export function renderProcessBlueprintSvg(
95
- doc: ProcessBlueprintFile,
96
- options: RenderProcessBlueprintOptions = {},
103
+ /**
104
+ * Canonical Process Blueprint body — everything inside the `<svg>` except the
105
+ * host-specific title block. Shared verbatim by the VS Code preview and the
106
+ * host-neutral wrapper below so both surfaces stay pixel-identical.
107
+ *
108
+ * Layering: a clipped group holds the row fills and grid lines; pills and
109
+ * compliance chips render on top (so grid lines sit under pill content); the
110
+ * outer rounded border is drawn last so corner cells never overflow it.
111
+ */
112
+ export function renderProcessBlueprintBody(
113
+ layout: ProcessBlueprintLayout,
114
+ ox: number,
115
+ oy: number,
116
+ clipId = 'bp-clip',
97
117
  ): string {
98
- const { title = '' } = options;
99
-
100
- const layout: ProcessBlueprintLayout = layoutProcessBlueprint(doc);
101
-
102
- if (layout.bounds.width === 0 || layout.bounds.height === 0) {
103
- return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
104
- }
105
-
106
- const titleH = title ? PAD : 0;
107
- const w = layout.bounds.width + PAD * 2;
108
- const h = layout.bounds.height + PAD * 2 + titleH;
109
- const ox = PAD;
110
- const oy = PAD + titleH;
118
+ const tw = layout.bounds.width;
119
+ const th = layout.bounds.height;
111
120
 
112
121
  const parts: string[] = [];
122
+ // Pills and chips collected separately so they render after (on top of) grid lines.
123
+ const topParts: string[] = [];
113
124
 
125
+ // Outer background (fill only; border drawn last on top).
114
126
  parts.push(
115
- `<rect class="diagram-node level-0" x="${ox}" y="${oy}" width="${layout.bounds.width}" height="${layout.bounds.height}" rx="6"/>`,
127
+ `<rect class="diagram-node level-0 bp-row-bg" x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6" stroke="none"/>`,
116
128
  );
117
129
 
130
+ // ClipPath — clips all inner content to the outer rounded rect so corner cells
131
+ // don't visually overflow the rx=6 boundary.
132
+ parts.push(`<defs><clipPath id="${clipId}"><rect x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6"/></clipPath></defs>`);
133
+
134
+ const inner: string[] = [];
135
+
136
+ // Stage headers (top row).
118
137
  for (const s of layout.stageHeaders) {
119
- parts.push(
120
- `<rect class="diagram-node level-1" x="${s.x + ox}" y="${s.y + oy}" width="${s.width}" height="${s.height}"/>`,
138
+ inner.push(
139
+ `<rect class="diagram-node level-1" x="${s.x + ox}" y="${s.y + oy}" width="${s.width}" height="${s.height}" stroke="none"/>`,
121
140
  );
122
- parts.push(
123
- `<text class="text-header" x="${s.x + ox + s.width / 2}" y="${s.y + oy + s.height / 2}" text-anchor="middle" style="dominant-baseline:central">${escXml(truncate(s.name, 28))}</text>`,
141
+ inner.push(
142
+ `<text class="text-header" x="${s.x + ox + s.width / 2}" y="${s.y + oy + s.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncate(s.name, 28))}</text>`,
124
143
  );
125
144
  }
126
145
 
146
+ // Legend column (row labels).
127
147
  for (const l of layout.legend) {
128
- parts.push(
129
- `<rect class="diagram-node level-2" x="${ox}" y="${l.y + oy}" width="${layout.legendColumnWidth}" height="${l.height}"/>`,
148
+ inner.push(
149
+ `<rect class="diagram-node level-2 bp-row-bg" x="${ox}" y="${l.y + oy}" width="${layout.legendColumnWidth}" height="${l.height}" stroke="none"/>`,
130
150
  );
131
- parts.push(
132
- `<text class="text-primary" x="${ox + 12}" y="${l.y + oy + l.height / 2}" style="dominant-baseline:central">${escXml(l.label)}</text>`,
151
+ inner.push(
152
+ `<text class="text-primary" x="${ox + 12}" y="${l.y + oy + l.height / 2}" dominant-baseline="central">${escXml(l.label)}</text>`,
133
153
  );
134
154
  }
135
155
 
156
+ // Goal and result cells.
136
157
  const textX = layout.cellTextPadX;
137
158
  for (const c of layout.goalCells) {
138
- parts.push(
139
- `<rect class="diagram-node level-3" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}"/>`,
140
- );
141
- parts.push(
142
- textCellSvg(c.lines, 'text-secondary', c.x + ox + textX, c.y + oy, c.height, layout.textLineHeight),
159
+ inner.push(
160
+ `<rect class="diagram-node level-3 bp-row-bg" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}" stroke="none"/>`,
143
161
  );
162
+ inner.push(textCellSvg(c.lines, 'text-secondary', c.x + ox + textX, c.y + oy, c.height, layout.textLineHeight));
144
163
  }
145
164
  for (const c of layout.resultCells) {
146
- parts.push(
147
- `<rect class="diagram-node level-4" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}"/>`,
148
- );
149
- parts.push(
150
- textCellSvg(c.lines, 'text-secondary', c.x + ox + textX, c.y + oy, c.height, layout.textLineHeight),
165
+ inner.push(
166
+ `<rect class="diagram-node level-4 bp-row-bg" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}" stroke="none"/>`,
151
167
  );
168
+ inner.push(textCellSvg(c.lines, 'text-secondary', c.x + ox + textX, c.y + oy, c.height, layout.textLineHeight));
152
169
  }
153
170
 
171
+ // Aspect rows — transparent background; pills carry the colour.
154
172
  for (let r = 0; r < layout.aspectRows.length; r++) {
155
173
  const row = layout.aspectRows[r];
156
174
  const level = 5 + (r % 3);
157
- parts.push(
158
- `<rect class="diagram-node level-${level}" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${layout.bounds.width - layout.legendColumnWidth}" height="${row.height}" opacity="0.15"/>`,
175
+ inner.push(
176
+ `<rect class="diagram-node level-${level} bp-row-bg" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${tw - layout.legendColumnWidth}" height="${row.height}" stroke="none"/>`,
159
177
  );
160
- for (let i = 1; i < layout.stageHeaders.length; i++) {
161
- const x = layout.legendColumnWidth + i * layout.stageColumnWidth + ox;
162
- parts.push(
163
- `<line class="diagram-edge" x1="${x}" y1="${row.y + oy}" x2="${x}" y2="${row.y + row.height + oy}" opacity="0.3"/>`,
164
- );
165
- }
166
178
  for (const p of row.pills) {
167
- parts.push(
179
+ const cx = p.x + ox + p.width / 2;
180
+ const pillLH = 15;
181
+ // p.lines: name lines (possibly wrapped), optionally with id as last line.
182
+ const hasIdLine = !!(p.id && p.lines.length > 0 && p.lines[p.lines.length - 1] === p.id);
183
+ const nameLines = hasIdLine ? p.lines.slice(0, -1) : p.lines;
184
+ const idLine = hasIdLine ? p.id : undefined;
185
+ const pillFirstY = p.y + oy + p.height / 2 - ((p.lines.length - 1) / 2) * pillLH;
186
+ topParts.push(
168
187
  `<rect class="diagram-node level-${level}" x="${p.x + ox}" y="${p.y + oy}" width="${p.width}" height="${p.height}" rx="6"/>`,
169
188
  );
170
- const label = p.id ? `${p.name} · ${p.id}` : p.name;
171
- parts.push(
172
- `<text class="text-pill" x="${p.x + ox + p.width / 2}" y="${p.y + oy + p.height / 2}" text-anchor="middle" style="dominant-baseline:central">${escXml(truncate(label, Math.floor(p.width / 8)))}</text>`,
173
- );
189
+ if (nameLines.length > 0) {
190
+ const tspans = nameLines
191
+ .map((ln, i) => `<tspan x="${cx}" y="${pillFirstY + i * pillLH}">${escXml(ln)}</tspan>`)
192
+ .join('');
193
+ topParts.push(`<text class="text-pill" text-anchor="middle" dominant-baseline="central">${tspans}</text>`);
194
+ }
195
+ if (idLine) {
196
+ topParts.push(
197
+ `<text class="text-secondary" x="${cx}" y="${pillFirstY + nameLines.length * pillLH}" text-anchor="middle" dominant-baseline="central">${escXml(idLine)}</text>`,
198
+ );
199
+ }
174
200
  }
175
201
  }
176
202
 
177
203
  // Compliance row (optional).
178
204
  if (layout.complianceRow) {
179
205
  const row = layout.complianceRow;
180
- parts.push(
181
- `<rect class="diagram-node level-5" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${layout.bounds.width - layout.legendColumnWidth}" height="${row.height}" opacity="0.10"/>`,
206
+ inner.push(
207
+ `<rect class="diagram-node level-5 bp-row-bg" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${tw - layout.legendColumnWidth}" height="${row.height}" stroke="none"/>`,
182
208
  );
183
- for (let i = 1; i < layout.stageHeaders.length; i++) {
184
- const x = layout.legendColumnWidth + i * layout.stageColumnWidth + ox;
185
- parts.push(
186
- `<line class="diagram-edge" x1="${x}" y1="${row.y + oy}" x2="${x}" y2="${row.y + row.height + oy}" opacity="0.3"/>`,
187
- );
188
- }
189
209
  for (const chip of row.chips) {
190
- parts.push(complianceChipSvg(chip, ox, oy));
210
+ const stageId = layout.stageHeaders[chip.stageIndex]?.id ?? '';
211
+ topParts.push(complianceChipSvg(chip, ox, oy, stageId));
212
+ }
213
+ }
214
+
215
+ // Grid lines — drawn before pills so they appear under pill content.
216
+ const gridX1 = ox;
217
+ const gridX2 = ox + tw;
218
+ const gridY1 = oy;
219
+ const gridY2 = oy + th;
220
+ if (layout.legend.length > 0) {
221
+ const headerBottomY = oy + layout.legend[0].y;
222
+ inner.push(`<line class="diagram-edge" x1="${gridX1}" y1="${headerBottomY}" x2="${gridX2}" y2="${headerBottomY}"/>`);
223
+ for (let i = 0; i < layout.legend.length - 1; i++) {
224
+ const rowBottomY = oy + layout.legend[i].y + layout.legend[i].height;
225
+ inner.push(`<line class="diagram-edge" x1="${gridX1}" y1="${rowBottomY}" x2="${gridX2}" y2="${rowBottomY}"/>`);
191
226
  }
192
227
  }
228
+ const legLineX = ox + layout.legendColumnWidth;
229
+ inner.push(`<line class="diagram-edge" x1="${legLineX}" y1="${gridY1}" x2="${legLineX}" y2="${gridY2}"/>`);
230
+ for (let i = 1; i < layout.stageHeaders.length; i++) {
231
+ const stageLineX = ox + layout.legendColumnWidth + i * layout.stageColumnWidth;
232
+ inner.push(`<line class="diagram-edge" x1="${stageLineX}" y1="${gridY1}" x2="${stageLineX}" y2="${gridY2}"/>`);
233
+ }
234
+
235
+ // Inner content (fills + grid lines) clipped to rounded outer rect.
236
+ parts.push(`<g clip-path="url(#${clipId})">${inner.join('\n')}${topParts.join('\n')}</g>`);
237
+
238
+ // Outer border on top — fill="none" as attribute so PNG export (no external CSS) never shows black.
239
+ parts.push(`<rect class="bp-border" x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6" fill="none"/>`);
240
+
241
+ return parts.join('\n');
242
+ }
243
+
244
+ export interface RenderProcessBlueprintOptions {
245
+ title?: string;
246
+ }
247
+
248
+ export function renderProcessBlueprintSvg(
249
+ doc: ProcessBlueprintFile,
250
+ options: RenderProcessBlueprintOptions = {},
251
+ ): string {
252
+ const { title = '' } = options;
253
+
254
+ const layout: ProcessBlueprintLayout = layoutProcessBlueprint(doc);
255
+
256
+ if (layout.bounds.width === 0 || layout.bounds.height === 0) {
257
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
258
+ }
259
+
260
+ const titleH = title ? PAD : 0;
261
+ const w = layout.bounds.width + PAD * 2;
262
+ const h = layout.bounds.height + PAD * 2 + titleH;
263
+ const ox = PAD;
264
+ const oy = PAD + titleH;
265
+
266
+ const body = renderProcessBlueprintBody(layout, ox, oy);
193
267
 
194
268
  const titleSvg = title
195
269
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
196
270
  : '';
197
271
 
198
- // Embed the shared theme CSS inside the SVG so the rendered output is
199
- // self-contained for the JCEF host — matches render-goals.ts.
200
- const embedCss = generateSvgEmbedCss('transitrix');
272
+ // Embed the shared theme CSS plus the blueprint-specific rules inside the SVG
273
+ // so the rendered output is self-contained for the JCEF host.
274
+ const embedCss = generateSvgEmbedCss('transitrix') + BP_EMBED_CSS;
201
275
 
202
276
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
203
277
  <style>${embedCss}</style>
204
278
  ${titleSvg}
205
- ${parts.join('\n')}
279
+ ${body}
206
280
  </svg>`;
207
281
  }