@transitrix/diagrams 1.0.0 → 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 (315) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +47 -47
  3. package/dist/__tests__/edge-path.test.js +8 -0
  4. package/dist/activities/cpm.js +11 -2
  5. package/dist/activities/gantt.js +6 -3
  6. package/dist/activities/types.d.ts +1 -0
  7. package/dist/activities/validate.js +3 -3
  8. package/dist/activity-card/__tests__/example.test.js +20 -1
  9. package/dist/activity-card/__tests__/layout.test.js +102 -11
  10. package/dist/activity-card/__tests__/resolver.test.js +161 -7
  11. package/dist/activity-card/index.d.ts +1 -1
  12. package/dist/activity-card/layout.d.ts +1 -1
  13. package/dist/activity-card/layout.js +220 -103
  14. package/dist/activity-card/resolver.js +115 -28
  15. package/dist/activity-card/types.d.ts +150 -27
  16. package/dist/activity-card/validate.js +3 -0
  17. package/dist/actor/__tests__/validate.test.js +63 -31
  18. package/dist/actor/index.d.ts +1 -1
  19. package/dist/actor/index.js +1 -1
  20. package/dist/actor/types.d.ts +7 -3
  21. package/dist/actor/types.js +17 -3
  22. package/dist/actor/validate.js +55 -26
  23. package/dist/assertion/types.d.ts +3 -1
  24. package/dist/assertion/types.js +1 -1
  25. package/dist/change/__tests__/validate.test.js +40 -34
  26. package/dist/change/index.d.ts +1 -0
  27. package/dist/change/types.d.ts +1 -1
  28. package/dist/change/types.js +2 -2
  29. package/dist/change/validate.d.ts +6 -1
  30. package/dist/change/validate.js +68 -41
  31. package/dist/compliance/__tests__/classify.test.js +34 -0
  32. package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
  33. package/dist/compliance/__tests__/html.test.js +2 -2
  34. package/dist/compliance/__tests__/impact.test.js +509 -6
  35. package/dist/compliance/classify.d.ts +6 -0
  36. package/dist/compliance/classify.js +9 -1
  37. package/dist/compliance/coverage-metric.d.ts +31 -11
  38. package/dist/compliance/coverage-metric.js +149 -39
  39. package/dist/compliance/html.js +78 -81
  40. package/dist/compliance/impact.d.ts +100 -16
  41. package/dist/compliance/impact.js +272 -28
  42. package/dist/compliance/index.d.ts +2 -2
  43. package/dist/compliance/index.js +1 -1
  44. package/dist/compliance/markdown.js +1 -0
  45. package/dist/compliance/types.d.ts +25 -6
  46. package/dist/edge-path.d.ts +8 -4
  47. package/dist/edge-path.js +11 -6
  48. package/dist/factor/__tests__/validate.test.js +75 -46
  49. package/dist/factor/index.d.ts +3 -2
  50. package/dist/factor/index.js +1 -1
  51. package/dist/factor/types.d.ts +5 -8
  52. package/dist/factor/types.js +10 -5
  53. package/dist/factor/validate.d.ts +6 -1
  54. package/dist/factor/validate.js +68 -50
  55. package/dist/fgca/__tests__/chain-table.test.js +2 -2
  56. package/dist/fgca/__tests__/layout.test.js +11 -11
  57. package/dist/fgca/__tests__/preview-layout.test.js +7 -7
  58. package/dist/fgca/__tests__/resolver.test.js +185 -0
  59. package/dist/fgca/chain-table.d.ts +1 -1
  60. package/dist/fgca/chain-table.js +4 -4
  61. package/dist/fgca/index.d.ts +4 -2
  62. package/dist/fgca/index.js +2 -1
  63. package/dist/fgca/layout.d.ts +2 -2
  64. package/dist/fgca/layout.js +9 -9
  65. package/dist/fgca/nodeTypes.d.ts +1 -1
  66. package/dist/fgca/nodeTypes.js +2 -2
  67. package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
  68. package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
  69. package/dist/fgca/parse-canonical.js +15 -10
  70. package/dist/fgca/preview-layout.d.ts +1 -1
  71. package/dist/fgca/preview-layout.js +4 -4
  72. package/dist/fgca/resolver.d.ts +49 -0
  73. package/dist/fgca/resolver.js +126 -0
  74. package/dist/fgca/types.d.ts +11 -11
  75. package/dist/fgca/types.js +2 -2
  76. package/dist/fgca/validate.d.ts +2 -2
  77. package/dist/fgca/validate.js +17 -14
  78. package/dist/goals/index.d.ts +1 -1
  79. package/dist/index.d.ts +5 -0
  80. package/dist/index.js +5 -0
  81. package/dist/process-blueprint/__tests__/layout.test.js +37 -0
  82. package/dist/process-blueprint/index.d.ts +1 -1
  83. package/dist/process-blueprint/layout.js +66 -35
  84. package/dist/process-blueprint/types.d.ts +36 -1
  85. package/dist/scenarios/index.d.ts +1 -1
  86. package/dist/scenarios/types.d.ts +4 -4
  87. package/dist/stakeholder/__tests__/validate.test.js +49 -47
  88. package/dist/stakeholder/index.d.ts +1 -0
  89. package/dist/stakeholder/types.d.ts +3 -4
  90. package/dist/stakeholder/types.js +2 -3
  91. package/dist/stakeholder/validate.d.ts +6 -1
  92. package/dist/stakeholder/validate.js +71 -50
  93. package/dist/target-state/__tests__/validate.test.js +43 -27
  94. package/dist/target-state/index.d.ts +2 -0
  95. package/dist/target-state/index.js +1 -0
  96. package/dist/target-state/types.d.ts +6 -1
  97. package/dist/target-state/types.js +8 -7
  98. package/dist/target-state/validate.d.ts +6 -1
  99. package/dist/target-state/validate.js +69 -45
  100. package/dist/theme/themes.js +71 -69
  101. package/dist/theme/tokens.d.ts +7 -1
  102. package/dist/theme/tokens.js +3 -1
  103. package/dist/webview/__tests__/entry-notations.test.js +2 -0
  104. package/dist/webview/__tests__/entry.test.js +11 -11
  105. package/dist/webview/__tests__/render-process.test.js +463 -0
  106. package/dist/webview/__tests__/render-util.test.d.ts +1 -0
  107. package/dist/webview/__tests__/render-util.test.js +54 -0
  108. package/dist/webview/entry.d.ts +1 -1
  109. package/dist/webview/entry.js +9 -5
  110. package/dist/webview/render-activities.d.ts +24 -2
  111. package/dist/webview/render-activities.js +92 -57
  112. package/dist/webview/render-activity-card.d.ts +11 -1
  113. package/dist/webview/render-activity-card.js +115 -79
  114. package/dist/webview/render-blocks.d.ts +24 -1
  115. package/dist/webview/render-blocks.js +58 -24
  116. package/dist/webview/render-fgca.d.ts +31 -0
  117. package/dist/webview/render-fgca.js +30 -19
  118. package/dist/webview/render-goals.d.ts +27 -1
  119. package/dist/webview/render-goals.js +42 -32
  120. package/dist/webview/render-process-blueprint.d.ts +11 -1
  121. package/dist/webview/render-process-blueprint.js +118 -53
  122. package/dist/webview/render-process.d.ts +95 -0
  123. package/dist/webview/render-process.js +392 -0
  124. package/dist/webview/render-scenarios.js +2 -2
  125. package/dist/webview/render-util.d.ts +19 -6
  126. package/dist/webview/render-util.js +23 -15
  127. package/package.json +54 -40
  128. package/src/__tests__/edge-path.test.ts +58 -46
  129. package/src/__tests__/scope.test.ts +27 -27
  130. package/src/__tests__/yaml-normalize.test.ts +158 -158
  131. package/src/activities/__tests__/cpm.test.ts +164 -164
  132. package/src/activities/__tests__/gantt.test.ts +245 -245
  133. package/src/activities/__tests__/layout.test.ts +112 -112
  134. package/src/activities/__tests__/validate.test.ts +409 -409
  135. package/src/activities/cpm.ts +107 -95
  136. package/src/activities/gantt.ts +277 -276
  137. package/src/activities/index.ts +28 -28
  138. package/src/activities/layout.ts +112 -112
  139. package/src/activities/types.ts +149 -148
  140. package/src/activities/validate.ts +357 -357
  141. package/src/activity-card/__tests__/example.test.ts +85 -66
  142. package/src/activity-card/__tests__/layout.test.ts +169 -63
  143. package/src/activity-card/__tests__/resolver.test.ts +330 -161
  144. package/src/activity-card/__tests__/validate.test.ts +131 -131
  145. package/src/activity-card/index.ts +38 -32
  146. package/src/activity-card/layout.ts +324 -194
  147. package/src/activity-card/resolver.ts +422 -337
  148. package/src/activity-card/types.ts +376 -237
  149. package/src/activity-card/validate.ts +162 -158
  150. package/src/actor/__tests__/validate.test.ts +70 -31
  151. package/src/actor/index.ts +1 -1
  152. package/src/actor/types.ts +21 -7
  153. package/src/actor/validate.ts +59 -27
  154. package/src/applications/__tests__/validate.test.ts +256 -256
  155. package/src/applications/index.ts +11 -11
  156. package/src/applications/types.ts +43 -43
  157. package/src/applications/validate.ts +131 -131
  158. package/src/assertion/__tests__/example.test.ts +37 -37
  159. package/src/assertion/__tests__/validate.test.ts +142 -142
  160. package/src/assertion/index.ts +12 -12
  161. package/src/assertion/types.ts +60 -58
  162. package/src/assertion/validate.ts +145 -145
  163. package/src/blocks/__tests__/layout.test.ts +169 -169
  164. package/src/blocks/__tests__/validate.test.ts +260 -260
  165. package/src/blocks/index.ts +17 -17
  166. package/src/blocks/layout.ts +186 -186
  167. package/src/blocks/types.ts +62 -62
  168. package/src/blocks/validate.ts +206 -206
  169. package/src/capability-map/__tests__/validate.test.ts +238 -238
  170. package/src/capability-map/index.ts +8 -8
  171. package/src/capability-map/types.ts +29 -29
  172. package/src/capability-map/validate.ts +127 -127
  173. package/src/change/__tests__/validate.test.ts +43 -34
  174. package/src/change/index.ts +1 -0
  175. package/src/change/types.ts +5 -3
  176. package/src/change/validate.ts +75 -43
  177. package/src/compliance/__tests__/classify.test.ts +64 -27
  178. package/src/compliance/__tests__/confidence.test.ts +98 -98
  179. package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
  180. package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
  181. package/src/compliance/__tests__/example.test.ts +57 -57
  182. package/src/compliance/__tests__/gap-report.test.ts +128 -128
  183. package/src/compliance/__tests__/html.test.ts +141 -141
  184. package/src/compliance/__tests__/impact.test.ts +1059 -481
  185. package/src/compliance/__tests__/markdown.test.ts +94 -94
  186. package/src/compliance/__tests__/views.test.ts +64 -64
  187. package/src/compliance/classify.ts +100 -84
  188. package/src/compliance/confidence.ts +69 -69
  189. package/src/compliance/coverage-metric.ts +404 -263
  190. package/src/compliance/gap-report.ts +94 -94
  191. package/src/compliance/html.ts +291 -295
  192. package/src/compliance/impact.ts +979 -677
  193. package/src/compliance/index.ts +57 -56
  194. package/src/compliance/markdown.ts +136 -135
  195. package/src/compliance/reverse-index.ts +33 -33
  196. package/src/compliance/types.ts +136 -120
  197. package/src/compliance/views.ts +46 -46
  198. package/src/compliance-matrix/__tests__/build.test.ts +200 -200
  199. package/src/compliance-matrix/__tests__/example.test.ts +57 -57
  200. package/src/compliance-matrix/index.ts +11 -11
  201. package/src/compliance-matrix/types.ts +102 -102
  202. package/src/edge-path.ts +50 -44
  203. package/src/factor/__tests__/validate.test.ts +81 -46
  204. package/src/factor/index.ts +3 -7
  205. package/src/factor/types.ts +15 -19
  206. package/src/factor/validate.ts +77 -51
  207. package/src/fgca/__tests__/chain-table.test.ts +172 -172
  208. package/src/fgca/__tests__/layout.test.ts +179 -179
  209. package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
  210. package/src/fgca/__tests__/preview-layout.test.ts +124 -124
  211. package/src/fgca/__tests__/resolver.test.ts +209 -0
  212. package/src/fgca/__tests__/validate.test.ts +142 -142
  213. package/src/fgca/chain-table.ts +225 -225
  214. package/src/fgca/index.ts +47 -45
  215. package/src/fgca/layout.ts +204 -204
  216. package/src/fgca/nodeTypes.ts +11 -11
  217. package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
  218. package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
  219. package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
  220. package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
  221. package/src/fgca/parse-canonical.ts +405 -400
  222. package/src/fgca/preview-layout.ts +217 -217
  223. package/src/fgca/resolver.ts +159 -0
  224. package/src/fgca/types.ts +50 -50
  225. package/src/fgca/validate.ts +129 -126
  226. package/src/geometry.ts +9 -9
  227. package/src/goals/__tests__/layout.test.ts +175 -175
  228. package/src/goals/__tests__/mutations.test.ts +111 -111
  229. package/src/goals/__tests__/parse-canonical.test.ts +145 -145
  230. package/src/goals/__tests__/validate.test.ts +202 -202
  231. package/src/goals/index.ts +20 -21
  232. package/src/goals/layout.ts +172 -172
  233. package/src/goals/mutations.ts +123 -123
  234. package/src/goals/parse-canonical.ts +286 -286
  235. package/src/goals/types.ts +85 -85
  236. package/src/goals/validate.ts +136 -136
  237. package/src/index.ts +28 -23
  238. package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
  239. package/src/process-blueprint/__tests__/layout.test.ts +240 -195
  240. package/src/process-blueprint/__tests__/validate.test.ts +310 -310
  241. package/src/process-blueprint/index.ts +32 -31
  242. package/src/process-blueprint/layout.ts +564 -531
  243. package/src/process-blueprint/types.ts +278 -242
  244. package/src/process-blueprint/validate.ts +181 -181
  245. package/src/process-map/__tests__/validate.test.ts +223 -223
  246. package/src/process-map/index.ts +10 -10
  247. package/src/process-map/types.ts +36 -36
  248. package/src/process-map/validate.ts +135 -135
  249. package/src/products/__tests__/validate.test.ts +239 -239
  250. package/src/products/index.ts +9 -9
  251. package/src/products/types.ts +31 -31
  252. package/src/products/validate.ts +112 -112
  253. package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
  254. package/src/repo-validate/index.ts +2 -2
  255. package/src/repo-validate/types.ts +50 -50
  256. package/src/repo-validate/validate-repo.ts +196 -196
  257. package/src/requirement/__tests__/example.test.ts +24 -24
  258. package/src/requirement/__tests__/validate.test.ts +96 -96
  259. package/src/requirement/index.ts +4 -4
  260. package/src/requirement/types.ts +35 -35
  261. package/src/requirement/validate.ts +92 -92
  262. package/src/scenarios/__tests__/validate.test.ts +209 -209
  263. package/src/scenarios/index.ts +15 -15
  264. package/src/scenarios/types.ts +37 -37
  265. package/src/scenarios/validate.ts +122 -122
  266. package/src/schema-version.ts +16 -16
  267. package/src/scope.ts +46 -46
  268. package/src/stakeholder/__tests__/validate.test.ts +51 -47
  269. package/src/stakeholder/index.ts +1 -0
  270. package/src/stakeholder/types.ts +7 -7
  271. package/src/stakeholder/validate.ts +79 -52
  272. package/src/target-state/__tests__/validate.test.ts +48 -27
  273. package/src/target-state/index.ts +2 -0
  274. package/src/target-state/types.ts +11 -7
  275. package/src/target-state/validate.ts +76 -46
  276. package/src/theme/index.ts +4 -4
  277. package/src/theme/themes.ts +239 -237
  278. package/src/theme/tokens.ts +135 -133
  279. package/src/typed-id.ts +56 -56
  280. package/src/validation-types.ts +36 -36
  281. package/src/webview/__tests__/entry-notations.test.ts +79 -77
  282. package/src/webview/__tests__/entry.test.ts +64 -64
  283. package/src/webview/__tests__/render-goals.test.ts +72 -72
  284. package/src/webview/__tests__/render-process.test.ts +538 -0
  285. package/src/webview/__tests__/render-util.test.ts +66 -0
  286. package/src/webview/entry.ts +298 -292
  287. package/src/webview/render-activities.ts +111 -69
  288. package/src/webview/render-activity-card.ts +275 -234
  289. package/src/webview/render-blocks.ts +78 -28
  290. package/src/webview/render-fgca.ts +53 -28
  291. package/src/webview/render-goals.ts +133 -110
  292. package/src/webview/render-process-blueprint.ts +281 -207
  293. package/src/webview/render-process.ts +507 -0
  294. package/src/webview/render-scenarios.ts +3 -3
  295. package/src/webview/render-util.ts +27 -14
  296. package/src/webview/styles.css +322 -323
  297. package/src/yaml-normalize.ts +56 -56
  298. package/dist/element-envelope.d.ts +0 -15
  299. package/dist/element-envelope.js +0 -55
  300. package/dist/issues/__tests__/layout.test.js +0 -62
  301. package/dist/issues/__tests__/validate.test.js +0 -82
  302. package/dist/issues/index.d.ts +0 -4
  303. package/dist/issues/index.js +0 -2
  304. package/dist/issues/layout.d.ts +0 -10
  305. package/dist/issues/layout.js +0 -82
  306. package/dist/issues/types.d.ts +0 -61
  307. package/dist/issues/types.js +0 -6
  308. package/dist/issues/validate.d.ts +0 -3
  309. package/dist/issues/validate.js +0 -143
  310. package/dist/webview/transitrix-render.css +0 -323
  311. package/dist/webview/transitrix-render.js +0 -6452
  312. package/dist/webview/transitrix-render.js.map +0 -7
  313. package/src/element-envelope.ts +0 -76
  314. /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
  315. /package/dist/{issues/__tests__/validate.test.d.ts → webview/__tests__/render-process.test.d.ts} +0 -0
@@ -14,11 +14,16 @@
14
14
  */
15
15
  import { layoutNestedBlocks } from '../blocks/layout.js';
16
16
  import type { BlocksFile, BlocksLayout, LaidOutBlock } from '../blocks/types.js';
17
- import { generateSvgEmbedCss } from '../theme/index.js';
17
+ import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
18
18
  import { escXml } from './render-util.js';
19
19
 
20
20
  const PAD = 24;
21
21
 
22
+ // Approximate character width (px) at text-primary size (12px/600).
23
+ const CHAR_W = 7;
24
+ // Approximate character width (px) at text-id size (10px/600) — used for ID suffix.
25
+ const CHAR_W_ID = 6;
26
+
22
27
  export interface RenderBlocksOptions {
23
28
  title?: string;
24
29
  }
@@ -36,27 +41,89 @@ function levelClassForDepth(depth: number): string {
36
41
  return `level-${idx}`;
37
42
  }
38
43
 
39
- function truncate(text: string, maxChars: number): string {
44
+ /** Truncate at a word boundary so labels never break in the middle of a word. */
45
+ function wordTruncate(text: string, maxChars: number): string {
40
46
  if (text.length <= maxChars) return text;
41
- return text.slice(0, Math.max(0, maxChars - 1)) + '…';
47
+ const cut = text.slice(0, maxChars - 1);
48
+ const lastSpace = cut.lastIndexOf(' ');
49
+ return (lastSpace > maxChars / 2 ? cut.slice(0, lastSpace) : cut) + '…';
42
50
  }
43
51
 
44
52
  function emitBlockSvg(b: LaidOutBlock, ox: number, oy: number, parts: string[]): void {
45
53
  const cls = levelClassForDepth(b.depth);
54
+ const cx = b.x + ox + b.width / 2;
46
55
  parts.push(
47
56
  `<rect class="diagram-node ${cls}" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="6"/>`,
48
57
  );
49
58
 
50
- // Header label, centred horizontally within the block's header strip.
51
- const headerY = b.y + oy + b.headerHeight / 2;
52
- const maxChars = Math.max(4, Math.floor(b.width / 8));
53
- parts.push(
54
- `<text class="text-header" x="${b.x + ox + b.width / 2}" y="${headerY}" text-anchor="middle" dominant-baseline="central">${escXml(truncate(b.name, maxChars))}</text>`,
55
- );
59
+ const isLeaf = b.children.length === 0;
60
+ if (isLeaf) {
61
+ // Leaf block: name on upper line, ID on lower line — both centred.
62
+ const nameY = b.y + oy + Math.round(b.height * 0.38);
63
+ const idY = b.y + oy + Math.round(b.height * 0.66);
64
+ const maxChars = Math.max(4, Math.floor(b.width / CHAR_W));
65
+ parts.push(
66
+ `<text class="text-primary" x="${cx}" y="${nameY}" text-anchor="middle" dominant-baseline="central">${escXml(wordTruncate(b.name, maxChars))}</text>`,
67
+ `<text class="text-id" x="${cx}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(b.id)}</text>`,
68
+ );
69
+ } else {
70
+ // Container block: name + (ID) on the single header line; ID in grey.
71
+ const headerY = b.y + oy + b.headerHeight / 2;
72
+ const idSuffix = ` (${b.id})`;
73
+ const idSuffixW = idSuffix.length * CHAR_W_ID;
74
+ const nameMaxChars = Math.max(4, Math.floor((b.width - idSuffixW) / CHAR_W));
75
+ const nameText = wordTruncate(b.name, nameMaxChars);
76
+ parts.push(
77
+ `<text class="text-primary" x="${cx}" y="${headerY}" text-anchor="middle" dominant-baseline="central">${escXml(nameText)}<tspan fill="var(--ts-text-secondary, #64748b)" font-size="10" font-weight="600">${escXml(idSuffix)}</tspan></text>`,
78
+ );
79
+ for (const c of b.children) emitBlockSvg(c, ox, oy, parts);
80
+ }
81
+ }
56
82
 
57
- for (const c of b.children) emitBlockSvg(c, ox, oy, parts);
83
+ export interface RenderBlocksLayoutOptions {
84
+ /** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
85
+ topInset?: number;
86
+ /** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
87
+ title?: string;
88
+ /** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
89
+ embedCssTheme?: ThemeId;
58
90
  }
59
91
 
92
+ /**
93
+ * The single Nested Blocks SVG emitter shared by every host. Takes an
94
+ * already-computed {@link BlocksLayout} (callers decide the layout options) and
95
+ * produces the `<svg>`. Hosts wrap it with their own chrome:
96
+ * - IntelliJ/UI via {@link renderBlocksSvg} (embedded CSS + simple header);
97
+ * - VS Code's blocks preview (rich title block, no embedded CSS — the webview
98
+ * and the export path own styling).
99
+ */
100
+ export function renderBlocksLayoutSvg(
101
+ layout: BlocksLayout,
102
+ options: RenderBlocksLayoutOptions = {},
103
+ ): string {
104
+ const { topInset = 0, title = '', embedCssTheme } = options;
105
+
106
+ const w = layout.bounds.width + PAD * 2;
107
+ const h = layout.bounds.height + PAD * 2 + topInset;
108
+ const ox = -layout.bounds.x + PAD;
109
+ const oy = -layout.bounds.y + PAD + topInset;
110
+
111
+ const parts: string[] = [];
112
+ for (const top of layout.blocks) emitBlockSvg(top, ox, oy, parts);
113
+
114
+ const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
115
+
116
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
117
+ ${title}
118
+ ${parts.join('\n')}
119
+ </svg>`;
120
+ }
121
+
122
+ /**
123
+ * Host-neutral blocks renderer (IntelliJ/UI). Lays the doc out with the default
124
+ * spacing, then delegates the actual SVG emission to {@link renderBlocksLayoutSvg}
125
+ * with the shared theme CSS embedded so the output is self-contained.
126
+ */
60
127
  export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions = {}): string {
61
128
  const { title = '' } = options;
62
129
 
@@ -66,26 +133,9 @@ export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions =
66
133
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
67
134
  }
68
135
 
69
- const w = layout.bounds.width + PAD * 2;
70
- const h = layout.bounds.height + PAD * 2;
71
- const ox = -layout.bounds.x + PAD;
72
- const oy = -layout.bounds.y + PAD;
73
-
74
- const parts: string[] = [];
75
- for (const top of layout.blocks) emitBlockSvg(top, ox, oy, parts);
76
-
77
136
  const titleSvg = title
78
137
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
79
138
  : '';
80
139
 
81
- // Embed the shared theme CSS inside the SVG so the rendered output is
82
- // self-contained — the JCEF host page only needs to drop the SVG into the
83
- // DOM and styling resolves without any cooperation from the host stylesheet.
84
- const embedCss = generateSvgEmbedCss('transitrix');
85
-
86
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
87
- <style>${embedCss}</style>
88
- ${titleSvg}
89
- ${parts.join('\n')}
90
- </svg>`;
140
+ return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
91
141
  }
@@ -26,29 +26,30 @@ import { generateSvgEmbedCss } from '../theme/index.js';
26
26
  import { escXml } from './render-util.js';
27
27
 
28
28
  const COL_LABELS: Record<FGCAPreviewColumn, string> = {
29
- factor: 'Factors (F)',
29
+ driver: 'Drivers (D)',
30
30
  goal: 'Goals (G)',
31
31
  change: 'Changes (C)',
32
32
  activity: 'Activities (A)',
33
33
  };
34
34
 
35
- export interface RenderFgcaOptions {
36
- /** `'fga'` hides the Changes column; defaults to `'fgca'`. */
37
- variant?: 'fgca' | 'fga';
38
- /** Optional heading rendered as a left-anchored `text-header` line. */
39
- title?: string;
40
- }
41
-
42
- export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): string {
43
- const { variant = 'fgca', title = '' } = options;
44
- const hideChanges = variant === 'fga';
45
-
46
- const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, { hideChanges });
47
-
48
- if (nodes.length === 0) {
49
- return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
50
- }
35
+ type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
51
36
 
37
+ /**
38
+ * Canonical FGCA/FGA body — the column headers, node rects and edge paths that
39
+ * go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
40
+ * and the host-neutral wrapper below. Excludes the host-specific title block
41
+ * and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
42
+ * layout's own); the VS Code path wraps this in a `translate(0, titleH)` group
43
+ * to make room for its title block, while the host-neutral wrapper renders it
44
+ * untranslated.
45
+ */
46
+ export function renderFgcaBody(
47
+ columns: FgcaLayout['columns'],
48
+ nodes: FgcaLayout['nodes'],
49
+ edges: FgcaLayout['edges'],
50
+ curvature: number,
51
+ entryCurvature: number | undefined,
52
+ ): string {
52
53
  const headerSvg = columns
53
54
  .map(({ col, x }) =>
54
55
  [
@@ -58,13 +59,10 @@ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): st
58
59
  )
59
60
  .join('\n');
60
61
 
61
- // Cubic bezier with horizontal control handles (shared geometry in
62
- // @transitrix/diagrams). Uses the package default curvature — the
63
- // interactive curvature control is a VS Code-only concern.
64
62
  const edgeSvg = edges
65
63
  .map(
66
64
  (e) =>
67
- `<path d="${horizontalCubicEdgePath(e.sx, e.sy, e.tx, e.ty, DEFAULT_EDGE_CURVATURE)}" class="diagram-edge" marker-end="url(#arrow)"/>`,
65
+ `<path d="${horizontalCubicEdgePath(e.sx, e.sy, e.tx, e.ty, curvature, entryCurvature)}" class="diagram-edge" marker-end="url(#arrow)"/>`,
68
66
  )
69
67
  .join('\n');
70
68
 
@@ -84,20 +82,49 @@ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): st
84
82
  }
85
83
  }
86
84
  const twoLines = line2.length > 0;
87
- const y1 = twoLines ? n.y + NODE_H / 2 - 8 : n.y + NODE_H / 2 + 5;
88
- const y2 = y1 + 18;
85
+ const y1 = twoLines ? n.y + 16 : n.y + 26;
86
+ const y2 = n.y + 32;
87
+ const idY = twoLines ? n.y + 54 : n.y + 50;
88
+ const entityId = n.id.slice(n.id.indexOf('_') + 1);
89
89
  return [
90
90
  `<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${NODE_W}" height="${NODE_H}" rx="8"/>`,
91
- `<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle">${escXml(line1)}</text>`,
91
+ `<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
92
92
  twoLines
93
- ? `<text class="text-secondary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle">${escXml(line2)}</text>`
93
+ ? `<text class="text-secondary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>`
94
94
  : '',
95
+ `<text class="text-id" x="${n.x + NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(entityId)}</text>`,
95
96
  ]
96
97
  .filter(Boolean)
97
98
  .join('\n');
98
99
  })
99
100
  .join('\n');
100
101
 
102
+ return `${headerSvg}\n${nodeSvg}\n${edgeSvg}`;
103
+ }
104
+
105
+ export interface RenderFgcaOptions {
106
+ /** `'fga'` hides the Changes column; defaults to `'fgca'`. */
107
+ variant?: 'fgca' | 'fga';
108
+ /** Optional heading rendered as a left-anchored `text-header` line. */
109
+ title?: string;
110
+ /** Exit edge curvature; 1 = default, 0 = straight, higher = stronger arc. */
111
+ curvature?: number;
112
+ /** Entry curvature at the target node; defaults to `curvature` when omitted. */
113
+ entryCurvature?: number;
114
+ }
115
+
116
+ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): string {
117
+ const { variant = 'fgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
118
+ const hideChanges = variant === 'fga';
119
+
120
+ const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, { hideChanges });
121
+
122
+ if (nodes.length === 0) {
123
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
124
+ }
125
+
126
+ const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature);
127
+
101
128
  const titleSvg = title
102
129
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
103
130
  : '';
@@ -115,8 +142,6 @@ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): st
115
142
  </marker>
116
143
  </defs>
117
144
  ${titleSvg}
118
- ${headerSvg}
119
- ${nodeSvg}
120
- ${edgeSvg}
145
+ ${body}
121
146
  </svg>`;
122
147
  }
@@ -1,110 +1,133 @@
1
- /**
2
- * Browser-safe SVG renderer for the canonical Goals notation.
3
- *
4
- * Step 3 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
5
- * validated GoalTree into renderable SVG so JCEF can drop it into the preview
6
- * panel. The VS Code path lives in `extension/src/goals-preview.ts` and pulls
7
- * in VS Code-specific concerns (themes, title block, save dialogs); this
8
- * module is the host-neutral subset — pure layout → SVG with no VS Code APIs,
9
- * no `node:fs`, no `node:path`.
10
- *
11
- * Step 4 will follow the same shape for the remaining ten notations.
12
- */
13
- import { layoutGoalTree } from '../goals/layout.js';
14
- import type { GoalTree, GoalTreeLayout, LaidOutEdge } from '../goals/types.js';
15
- import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
16
- import { generateSvgEmbedCss } from '../theme/index.js';
17
-
18
- const NODE_W = 250;
19
- const NODE_H = 60;
20
- const RANK_SEP = 100;
21
- const NODE_SEP = 24;
22
- const PAD = 24;
23
- const LABEL_MAX = 36;
24
- const LABEL_TRIM = 34;
25
-
26
- function escXml(s: string): string {
27
- return s
28
- .replace(/&/g, '&amp;')
29
- .replace(/</g, '&lt;')
30
- .replace(/>/g, '&gt;')
31
- .replace(/"/g, '&quot;');
32
- }
33
-
34
- export interface RenderGoalsOptions {
35
- treeName?: string;
36
- curvature?: number;
37
- }
38
-
39
- export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {}): string {
40
- const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE } = options;
41
-
42
- const layout: GoalTreeLayout = layoutGoalTree(tree, {
43
- nodeWidth: NODE_W,
44
- nodeHeight: NODE_H,
45
- rankSep: RANK_SEP,
46
- nodeSep: NODE_SEP,
47
- });
48
-
49
- if (layout.nodes.length === 0) {
50
- return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
51
- }
52
-
53
- const w = layout.bounds.width + PAD * 2;
54
- const h = layout.bounds.height + PAD * 2;
55
- const ox = -layout.bounds.x + PAD;
56
- const oy = -layout.bounds.y + PAD;
57
-
58
- const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
59
-
60
- function edgePath(e: LaidOutEdge): string {
61
- const s = nodeMap.get(e.source);
62
- const t = nodeMap.get(e.target);
63
- if (!s || !t) return '';
64
- const sx = s.x + ox + s.width;
65
- const sy = s.y + oy + s.height / 2;
66
- const tx = t.x + ox;
67
- const ty = t.y + oy + t.height / 2;
68
- return horizontalCubicEdgePath(sx, sy, tx, ty, curvature);
69
- }
70
-
71
- const edgeSvg = layout.edges
72
- .map((e) => `<path d="${edgePath(e)}" class="diagram-edge" marker-end="url(#arrow)"/>`)
73
- .join('\n');
74
-
75
- const nodeSvg = layout.nodes
76
- .map((n) => {
77
- const x = n.x + ox;
78
- const y = n.y + oy;
79
- const level = n.data.level % 8;
80
- const labelText = n.data.name ?? String(n.id);
81
- const label = labelText.length > LABEL_MAX ? labelText.slice(0, LABEL_TRIM) + '…' : labelText;
82
- return `<g>
83
- <rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>
84
- <text class="text-primary" x="${x + n.width / 2}" y="${y + n.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(label)}</text>
85
- </g>`;
86
- })
87
- .join('\n');
88
-
89
- const titleSvg = treeName
90
- ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
91
- : '';
92
-
93
- // Embed the shared theme CSS inside the SVG so the rendered output is
94
- // self-contained — the JCEF host page only needs to drop the SVG into the
95
- // DOM and styling resolves without any cooperation from the host stylesheet.
96
- // Matches what the VS Code path produces via `prepareSvgForExport`.
97
- const embedCss = generateSvgEmbedCss('transitrix');
98
-
99
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
100
- <style>${embedCss}</style>
101
- <defs>
102
- <marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
103
- <path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
104
- </marker>
105
- </defs>
106
- ${titleSvg}
107
- ${nodeSvg}
108
- ${edgeSvg}
109
- </svg>`;
110
- }
1
+ /**
2
+ * Browser-safe SVG renderer for the canonical Goals notation.
3
+ *
4
+ * Step 3 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
5
+ * validated GoalTree into renderable SVG so JCEF can drop it into the preview
6
+ * panel. The VS Code path lives in `extension/src/goals-preview.ts` and pulls
7
+ * in VS Code-specific concerns (themes, title block, save dialogs); this
8
+ * module is the host-neutral subset — pure layout → SVG with no VS Code APIs,
9
+ * no `node:fs`, no `node:path`.
10
+ *
11
+ * Step 4 will follow the same shape for the remaining ten notations.
12
+ */
13
+ import { layoutGoalTree } from '../goals/layout.js';
14
+ import type { GoalTree, GoalTreeLayout, LaidOutEdge } from '../goals/types.js';
15
+ import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
16
+ import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
17
+ import { escXml } from './render-util.js';
18
+
19
+ const NODE_W = 250;
20
+ const NODE_H = 60;
21
+ const RANK_SEP = 100;
22
+ const NODE_SEP = 24;
23
+ const PAD = 24;
24
+ const LABEL_MAX = 36;
25
+ const LABEL_TRIM = 34;
26
+
27
+ export interface RenderGoalsOptions {
28
+ treeName?: string;
29
+ curvature?: number;
30
+ entryCurvature?: number;
31
+ }
32
+
33
+ /**
34
+ * Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
35
+ * spacing, then delegates the actual SVG emission to {@link renderGoalsLayoutSvg}
36
+ * with the shared theme CSS embedded so the output is self-contained.
37
+ */
38
+ export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {}): string {
39
+ const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
40
+
41
+ const layout: GoalTreeLayout = layoutGoalTree(tree, {
42
+ nodeWidth: NODE_W,
43
+ nodeHeight: NODE_H,
44
+ rankSep: RANK_SEP,
45
+ nodeSep: NODE_SEP,
46
+ });
47
+
48
+ if (layout.nodes.length === 0) {
49
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
50
+ }
51
+
52
+ const title = treeName
53
+ ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
54
+ : '';
55
+
56
+ // Embed the shared theme CSS so the JCEF host page only needs to drop the SVG
57
+ // into the DOM — styling resolves without help from the host stylesheet. The
58
+ // VS Code preview omits this (its webview supplies the CSS) and embeds it only
59
+ // on export via `prepareSvgForExport`.
60
+ return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, title, embedCssTheme: 'transitrix' });
61
+ }
62
+
63
+ export interface RenderGoalsLayoutOptions {
64
+ curvature?: number;
65
+ entryCurvature?: number;
66
+ /** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
67
+ topInset?: number;
68
+ /** Raw SVG injected immediately after `<defs>` — a header line or a full title block. */
69
+ title?: string;
70
+ /** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
71
+ embedCssTheme?: ThemeId;
72
+ }
73
+
74
+ /**
75
+ * The single goals SVG emitter shared by every host. Takes an already-computed
76
+ * {@link GoalTreeLayout} (callers decide spacing/scope) and produces the `<svg>`.
77
+ * Hosts wrap it with their own chrome:
78
+ * - IntelliJ/UI via {@link renderGoalsSvg} (embedded CSS + simple header);
79
+ * - VS Code's goals preview (rich title block, no embedded CSS — the webview
80
+ * and the export path own styling).
81
+ */
82
+ export function renderGoalsLayoutSvg(layout: GoalTreeLayout, options: RenderGoalsLayoutOptions = {}): string {
83
+ const { curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, topInset = 0, title = '', embedCssTheme } = options;
84
+
85
+ const w = layout.bounds.width + PAD * 2;
86
+ const h = layout.bounds.height + PAD * 2 + topInset;
87
+ const ox = -layout.bounds.x + PAD;
88
+ const oy = -layout.bounds.y + PAD + topInset;
89
+
90
+ const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
91
+
92
+ function edgePath(e: LaidOutEdge): string {
93
+ const s = nodeMap.get(e.source);
94
+ const t = nodeMap.get(e.target);
95
+ if (!s || !t) return '';
96
+ const sx = s.x + ox + s.width;
97
+ const sy = s.y + oy + s.height / 2;
98
+ const tx = t.x + ox;
99
+ const ty = t.y + oy + t.height / 2;
100
+ return horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature);
101
+ }
102
+
103
+ const edgeSvg = layout.edges
104
+ .map((e) => `<path d="${edgePath(e)}" class="diagram-edge" marker-end="url(#arrow)"/>`)
105
+ .join('\n');
106
+
107
+ const nodeSvg = layout.nodes
108
+ .map((n) => {
109
+ const x = n.x + ox;
110
+ const y = n.y + oy;
111
+ const level = n.data.level % 8;
112
+ const labelText = n.data.name ?? String(n.id);
113
+ const label = labelText.length > LABEL_MAX ? labelText.slice(0, LABEL_TRIM) + '…' : labelText;
114
+ return `<g>
115
+ <rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>
116
+ <text class="text-primary" x="${x + n.width / 2}" y="${y + n.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(label)}</text>
117
+ </g>`;
118
+ })
119
+ .join('\n');
120
+
121
+ const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
122
+
123
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
124
+ <defs>
125
+ <marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
126
+ <path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
127
+ </marker>
128
+ </defs>
129
+ ${title}
130
+ ${nodeSvg}
131
+ ${edgeSvg}
132
+ </svg>`;
133
+ }