@diagrammo/dgmo 0.46.0 → 0.48.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 (534) hide show
  1. package/dist/advanced.d.ts +3 -855
  2. package/dist/advanced.js +541 -70107
  3. package/dist/analyzer-UYKBXAKR.js +15 -0
  4. package/dist/auto.js +1 -1
  5. package/dist/auto.mjs +1 -1
  6. package/dist/block.js +83 -69417
  7. package/dist/calculator-LDYQCDDY.js +11 -0
  8. package/dist/charts-d3-QOZHKTCT.js +25 -0
  9. package/dist/chunk-2DXMZRWJ.js +800 -0
  10. package/dist/chunk-2LACINWN.js +786 -0
  11. package/dist/chunk-33LCUH4H.js +893 -0
  12. package/dist/chunk-3CIOJ5YJ.js +35 -0
  13. package/dist/chunk-3HDBAXF5.js +86 -0
  14. package/dist/chunk-3JLKYGBQ.js +290 -0
  15. package/dist/chunk-3MQQNXPD.js +415 -0
  16. package/dist/chunk-3R5POCTZ.js +967 -0
  17. package/dist/chunk-3SMGIJDD.js +112 -0
  18. package/dist/chunk-44CM72SQ.js +409 -0
  19. package/dist/chunk-4GRI7MEB.js +1604 -0
  20. package/dist/chunk-4OHACBGX.js +606 -0
  21. package/dist/chunk-5DH3KYFH.js +21 -0
  22. package/dist/chunk-5MLYVTYM.js +76 -0
  23. package/dist/chunk-5QXL4NYS.js +553 -0
  24. package/dist/chunk-5WRI5ZAA.js +31 -0
  25. package/dist/chunk-6EK55ART.js +117 -0
  26. package/dist/chunk-6FZYCKXB.js +2065 -0
  27. package/dist/chunk-6HO5ZTXK.js +277 -0
  28. package/dist/chunk-6KFWC4CO.js +7 -0
  29. package/dist/chunk-6MTWIQVO.js +121 -0
  30. package/dist/chunk-6NYWYNBI.js +329 -0
  31. package/dist/chunk-6SM5NJQH.js +620 -0
  32. package/dist/chunk-725LDASD.js +422 -0
  33. package/dist/chunk-76OWZ3BA.js +130 -0
  34. package/{src/block/parser.ts → dist/chunk-7JEGTRLD.js} +100 -189
  35. package/dist/chunk-A44USUV7.js +923 -0
  36. package/dist/chunk-A4GOY3PE.js +25 -0
  37. package/dist/chunk-A4VFQH5G.js +251 -0
  38. package/dist/chunk-AELOBFW5.js +375 -0
  39. package/dist/chunk-AN2MOO5T.js +522 -0
  40. package/dist/chunk-BMR4CIEO.js +41 -0
  41. package/dist/chunk-BS7SV7C3.js +51 -0
  42. package/dist/chunk-BUPDIHML.js +613 -0
  43. package/dist/chunk-BUVJFRPY.js +887 -0
  44. package/dist/chunk-C3Z4DX3V.js +112 -0
  45. package/dist/chunk-CI2G3OEB.js +1818 -0
  46. package/dist/chunk-CKNVO24D.js +54 -0
  47. package/dist/chunk-CUVHQHZ7.js +391 -0
  48. package/dist/chunk-CXK4DZH3.js +114 -0
  49. package/dist/{editor.cjs → chunk-DKEPS5LH.js} +14 -225
  50. package/dist/chunk-DU7W62N5.js +487 -0
  51. package/dist/chunk-DZAQBW35.js +751 -0
  52. package/dist/chunk-EB6FPQUP.js +316 -0
  53. package/dist/chunk-EMS2SK26.js +376 -0
  54. package/dist/chunk-ERLBPDGU.js +510 -0
  55. package/dist/chunk-EVCVDTXE.js +732 -0
  56. package/dist/chunk-FCIIHT24.js +384 -0
  57. package/dist/chunk-FFUN5D66.js +97 -0
  58. package/dist/chunk-FITOLXSX.js +714 -0
  59. package/dist/chunk-FMDSF3OV.js +68 -0
  60. package/dist/chunk-FNB3PNGF.js +410 -0
  61. package/dist/chunk-G2H74XJZ.js +2104 -0
  62. package/dist/chunk-GMR5MFHJ.js +339 -0
  63. package/dist/chunk-H6PI3KJT.js +305 -0
  64. package/dist/chunk-I46X5VUA.js +59 -0
  65. package/dist/chunk-I5IGB56C.js +255 -0
  66. package/dist/chunk-IH6ZMVQ5.js +3950 -0
  67. package/dist/chunk-IJZFN3HM.js +463 -0
  68. package/dist/chunk-IPWPWM6R.js +3079 -0
  69. package/dist/chunk-IYULMRBE.js +23 -0
  70. package/dist/chunk-IZMRON6C.js +167 -0
  71. package/dist/chunk-J5FIXQRT.js +813 -0
  72. package/dist/chunk-JED6ZDEE.js +73 -0
  73. package/dist/chunk-JEUJOX5X.js +136 -0
  74. package/dist/chunk-JM4LEPY3.js +436 -0
  75. package/dist/chunk-L3UHP6TD.js +101 -0
  76. package/dist/chunk-L66GEFB6.js +524 -0
  77. package/dist/chunk-LIOHW36Z.js +469 -0
  78. package/dist/chunk-LP7EC4MI.js +16 -0
  79. package/dist/chunk-MITI6NOL.js +548 -0
  80. package/dist/chunk-MO5YHHMQ.js +168 -0
  81. package/dist/chunk-MZSBFAPJ.js +885 -0
  82. package/dist/chunk-NNAAJVBU.js +9 -0
  83. package/dist/chunk-NOTA6TEI.js +47 -0
  84. package/dist/chunk-NURGB5Z4.js +335 -0
  85. package/dist/chunk-NX4EQS3U.js +1062 -0
  86. package/dist/chunk-NXMESY4J.js +1 -0
  87. package/dist/chunk-OJR3MQYV.js +1458 -0
  88. package/dist/chunk-OURHMODR.js +454 -0
  89. package/dist/chunk-P235SVAH.js +1057 -0
  90. package/dist/chunk-P4RH4OSP.js +1038 -0
  91. package/dist/chunk-P6T66CCZ.js +226 -0
  92. package/dist/chunk-PBAHEPLQ.js +223 -0
  93. package/dist/chunk-PBHE5UG6.js +1530 -0
  94. package/dist/chunk-PDKUANDZ.js +35 -0
  95. package/dist/chunk-PWIHZ6NJ.js +296 -0
  96. package/{src/c4/layout.ts → dist/chunk-QEBUY5NY.js} +384 -1140
  97. package/dist/chunk-QLCCJEJI.js +253 -0
  98. package/dist/chunk-QOOUC2J5.js +52 -0
  99. package/dist/chunk-QYTPPDAO.js +402 -0
  100. package/dist/chunk-R3XI2VC7.js +801 -0
  101. package/dist/chunk-RCERWOJR.js +154 -0
  102. package/dist/chunk-RJE5JTVK.js +362 -0
  103. package/dist/chunk-RTLBSQ3G.js +1155 -0
  104. package/dist/chunk-RUNNI6VE.js +448 -0
  105. package/dist/chunk-RVMRZAPB.js +354 -0
  106. package/dist/chunk-RYEGFMUO.js +955 -0
  107. package/dist/chunk-RZCF2VVM.js +528 -0
  108. package/dist/chunk-SML67VPV.js +1025 -0
  109. package/dist/chunk-SQ7IY53Q.js +44 -0
  110. package/dist/chunk-SQAG3UXH.js +161 -0
  111. package/dist/chunk-SRTU6HMQ.js +1851 -0
  112. package/dist/chunk-T2OE3YQB.js +550 -0
  113. package/dist/chunk-T3UBLACL.js +299 -0
  114. package/dist/chunk-TLVIHFRO.js +296 -0
  115. package/dist/chunk-TYS6CSS5.js +52 -0
  116. package/dist/chunk-U6EQI3U6.js +130 -0
  117. package/dist/chunk-UJ4FJLBF.js +86 -0
  118. package/dist/chunk-ULOEATBZ.js +347 -0
  119. package/dist/chunk-UNCCZ3NU.js +127 -0
  120. package/dist/chunk-USZW2HHU.js +197 -0
  121. package/dist/chunk-UWN33VNX.js +1610 -0
  122. package/dist/chunk-UZ7EZW3B.js +40 -0
  123. package/dist/chunk-VOKGH627.js +37 -0
  124. package/dist/chunk-WBRZJOEC.js +204 -0
  125. package/dist/chunk-WCSOJFU3.js +380 -0
  126. package/dist/chunk-WCT3NYCV.js +160 -0
  127. package/dist/chunk-WVAJFAIA.js +529 -0
  128. package/dist/chunk-X5LCYCL5.js +394 -0
  129. package/dist/chunk-XAKEUMEJ.js +1256 -0
  130. package/dist/chunk-Y2ZE3MSG.js +590 -0
  131. package/dist/chunk-Y3J7VR52.js +646 -0
  132. package/dist/chunk-Y5QGVTVC.js +33 -0
  133. package/dist/chunk-YAA6IID3.js +548 -0
  134. package/dist/chunk-YGJTDXVR.js +692 -0
  135. package/{src/utils/time-ticks.ts → dist/chunk-YRYKTYMQ.js} +45 -112
  136. package/dist/chunk-YYVTBZZC.js +61 -0
  137. package/dist/chunk-Z5CGSFJB.js +647 -0
  138. package/{src/gantt/calculator.ts → dist/chunk-Z72XRXD4.js} +236 -428
  139. package/dist/chunk-ZGR7ULFJ.js +978 -0
  140. package/dist/chunk-ZJEL4H2R.js +851 -0
  141. package/dist/chunk-ZO27FGJS.js +554 -0
  142. package/dist/collapse-CLPJD2RL.js +11 -0
  143. package/dist/collapse-SLINJQW6.js +9 -0
  144. package/dist/collapse-ZWUFFCK2.js +9 -0
  145. package/dist/compute-G55RG53N.js +8 -0
  146. package/dist/dgmo-router-DZCIDUO5.js +73 -0
  147. package/dist/dimensions-J3FHIMJ6.js +22 -0
  148. package/dist/element.js +1 -1
  149. package/dist/element.mjs +1 -1
  150. package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
  151. package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
  152. package/dist/index.d.ts +3 -469
  153. package/dist/index.js +137 -69685
  154. package/dist/layout-46N4JJYK.js +17 -0
  155. package/dist/layout-5RA6QWR6.js +26 -0
  156. package/dist/layout-5T6RH5M6.js +14 -0
  157. package/dist/layout-7QXCNOVS.js +13 -0
  158. package/dist/layout-EE6GE46Y.js +17 -0
  159. package/dist/layout-G23F7LDH.js +9 -0
  160. package/dist/layout-HRBZPFCN.js +23 -0
  161. package/dist/layout-HWDSRX5E.js +20 -0
  162. package/dist/layout-HYZPWAK5.js +18 -0
  163. package/dist/layout-N76NR4ZO.js +19 -0
  164. package/dist/layout-OB3RI63S.js +14 -0
  165. package/dist/layout-S2QBYSK2.js +15 -0
  166. package/dist/layout-UM5F4DJB.js +15 -0
  167. package/dist/layout-search-OYV77HHS.js +1655 -0
  168. package/dist/load-data-EHDDZ5ZS.js +8 -0
  169. package/dist/palettes-PRJ2RI6D.js +56 -0
  170. package/dist/parser-2MFP2H2E.js +11 -0
  171. package/dist/parser-3STE62GB.js +21 -0
  172. package/dist/parser-4FL2CMDG.js +18 -0
  173. package/dist/parser-4OGOBJJ4.js +13 -0
  174. package/dist/parser-6BTRLKRZ.js +15 -0
  175. package/dist/parser-AQETEVRW.js +21 -0
  176. package/dist/parser-CFZD5IBL.js +12 -0
  177. package/dist/parser-FUSKFGZN.js +16 -0
  178. package/dist/parser-I6KXEYFK.js +13 -0
  179. package/dist/parser-IXSZZQHY.js +18 -0
  180. package/dist/parser-JAL2HD4I.js +13 -0
  181. package/dist/parser-KNSQSKQO.js +20 -0
  182. package/dist/parser-N5O4IHUN.js +13 -0
  183. package/dist/parser-NGU3ETO6.js +16 -0
  184. package/dist/parser-NRAI6O25.js +19 -0
  185. package/dist/parser-O5J4VBII.js +11 -0
  186. package/dist/parser-OV7NOBW6.js +21 -0
  187. package/dist/parser-PQ3POX45.js +23 -0
  188. package/dist/parser-TFZFBVWN.js +11 -0
  189. package/dist/parser-TRTHDFPP.js +18 -0
  190. package/dist/parser-VQP2UJ6W.js +11 -0
  191. package/dist/parser-XDVELHRK.js +13 -0
  192. package/dist/parser-XKRP7QKP.js +11 -0
  193. package/dist/parser-Y6WTBU76.js +14 -0
  194. package/dist/parser-ZSDPZYQP.js +12 -0
  195. package/dist/renderer-2XQQQ6QR.js +28 -0
  196. package/dist/renderer-5NDVNDAZ.js +26 -0
  197. package/dist/renderer-6EEZHMH4.js +26 -0
  198. package/dist/renderer-6W5462RB.js +43 -0
  199. package/dist/renderer-7MVLWWQD.js +22 -0
  200. package/dist/renderer-AWWAF6XD.js +33 -0
  201. package/dist/renderer-BYCNMBF7.js +34 -0
  202. package/dist/renderer-CLJHQPBJ.js +29 -0
  203. package/dist/renderer-GHPZMFU2.js +15 -0
  204. package/dist/renderer-JGBKUCZD.js +16 -0
  205. package/dist/renderer-KQB7UGDS.js +19 -0
  206. package/dist/renderer-PGX2TKVS.js +24 -0
  207. package/dist/renderer-QIWVHWZO.js +15 -0
  208. package/dist/renderer-RPOA5YPZ.js +35 -0
  209. package/dist/renderer-SMIBCRVB.js +24 -0
  210. package/dist/renderer-T7YKC5VP.js +28 -0
  211. package/dist/renderer-TOCOVLHS.js +37 -0
  212. package/dist/renderer-UWA5XS74.js +39 -0
  213. package/dist/renderer-VACVQYML.js +25 -0
  214. package/dist/renderer-W4M6QLIJ.js +37 -0
  215. package/dist/renderer-WZRREQ2P.js +23 -0
  216. package/dist/renderer-YY2OSF6E.js +27 -0
  217. package/dist/renderer-Z7I3W2PP.js +24 -0
  218. package/dist/renderer-ZGNTUWC4.js +22 -0
  219. package/dist/renderer-ZTR3DER3.js +19 -0
  220. package/dist/renderer-radial-C32GPJQ7.js +21 -0
  221. package/dist/resolver-NOAA7ID7.js +13 -0
  222. package/dist/state-collapse-HCGD6IVJ.js +8 -0
  223. package/dist/state-parser-Z2V23ATR.js +18 -0
  224. package/dist/state-renderer-RFYHSP4K.js +34 -0
  225. package/dist/themes-1-CuKpeH.d.ts +908 -0
  226. package/package.json +19 -70
  227. package/dist/advanced.cjs +0 -71648
  228. package/dist/advanced.d.cts +0 -6571
  229. package/dist/auto.cjs +0 -70648
  230. package/dist/auto.d.cts +0 -54
  231. package/dist/block.cjs +0 -70208
  232. package/dist/block.d.cts +0 -115
  233. package/dist/editor.d.cts +0 -26
  234. package/dist/element.cjs +0 -70530
  235. package/dist/element.d.cts +0 -62
  236. package/dist/highlight.cjs +0 -852
  237. package/dist/highlight.d.cts +0 -49
  238. package/dist/index.cjs +0 -70491
  239. package/dist/index.d.cts +0 -626
  240. package/dist/pert.cjs +0 -325
  241. package/dist/pert.d.cts +0 -559
  242. package/src/advanced.ts +0 -873
  243. package/src/arc/parser.ts +0 -15
  244. package/src/arc/renderer.ts +0 -633
  245. package/src/auto/index.ts +0 -621
  246. package/src/auto/shared.ts +0 -370
  247. package/src/auto/styles.ts +0 -73
  248. package/src/block/layout.ts +0 -198
  249. package/src/block/renderer.ts +0 -484
  250. package/src/block/types.ts +0 -67
  251. package/src/boxes-and-lines/collapse.ts +0 -96
  252. package/src/boxes-and-lines/focus.ts +0 -257
  253. package/src/boxes-and-lines/label-placement.ts +0 -339
  254. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  255. package/src/boxes-and-lines/layout-layered.ts +0 -722
  256. package/src/boxes-and-lines/layout-search.ts +0 -1678
  257. package/src/boxes-and-lines/layout.ts +0 -398
  258. package/src/boxes-and-lines/parser.ts +0 -1181
  259. package/src/boxes-and-lines/renderer.ts +0 -1465
  260. package/src/boxes-and-lines/types.ts +0 -66
  261. package/src/c4/parser.ts +0 -1158
  262. package/src/c4/renderer.ts +0 -2008
  263. package/src/c4/types.ts +0 -89
  264. package/src/chart-type-registry.ts +0 -529
  265. package/src/chart-types.ts +0 -225
  266. package/src/chart.ts +0 -685
  267. package/src/charts-d3/bar.ts +0 -338
  268. package/src/charts-d3/chord.ts +0 -140
  269. package/src/charts-d3/function.ts +0 -147
  270. package/src/charts-d3/funnel.ts +0 -104
  271. package/src/charts-d3/heatmap.ts +0 -145
  272. package/src/charts-d3/index.ts +0 -337
  273. package/src/charts-d3/interactions.ts +0 -477
  274. package/src/charts-d3/line.ts +0 -344
  275. package/src/charts-d3/mount.ts +0 -151
  276. package/src/charts-d3/pie.ts +0 -117
  277. package/src/charts-d3/polar.ts +0 -89
  278. package/src/charts-d3/radar.ts +0 -0
  279. package/src/charts-d3/sankey.ts +0 -0
  280. package/src/charts-d3/scatter.ts +0 -185
  281. package/src/charts-d3/shared.ts +0 -208
  282. package/src/class/layout.ts +0 -326
  283. package/src/class/parser.ts +0 -635
  284. package/src/class/renderer.ts +0 -793
  285. package/src/class/types.ts +0 -58
  286. package/src/cli-banner.ts +0 -106
  287. package/src/cli.ts +0 -1080
  288. package/src/colors-diagnostics.ts +0 -35
  289. package/src/colors.ts +0 -379
  290. package/src/completion-types.ts +0 -27
  291. package/src/completion.ts +0 -2668
  292. package/src/cycle/layout.ts +0 -1026
  293. package/src/cycle/parser.ts +0 -387
  294. package/src/cycle/renderer.ts +0 -653
  295. package/src/cycle/types.ts +0 -101
  296. package/src/d3.ts +0 -1434
  297. package/src/data-chart-parser.ts +0 -1007
  298. package/src/dgmo-router.ts +0 -296
  299. package/src/diagnostics-registry.ts +0 -108
  300. package/src/diagnostics.ts +0 -0
  301. package/src/dimensions.ts +0 -20
  302. package/src/directives-registry.ts +0 -396
  303. package/src/editor/dgmo.grammar +0 -76
  304. package/src/editor/dgmo.grammar.d.ts +0 -2
  305. package/src/editor/dgmo.grammar.js +0 -18
  306. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  307. package/src/editor/dgmo.grammar.terms.js +0 -35
  308. package/src/editor/highlight-api.ts +0 -596
  309. package/src/editor/highlight.ts +0 -36
  310. package/src/editor/index.ts +0 -27
  311. package/src/editor/keywords.ts +0 -128
  312. package/src/editor/tokens.ts +0 -30
  313. package/src/element/index.ts +0 -432
  314. package/src/embed/css.ts +0 -312
  315. package/src/embed/escape.ts +0 -15
  316. package/src/embed/index.ts +0 -280
  317. package/src/er/classify.ts +0 -228
  318. package/src/er/layout.ts +0 -530
  319. package/src/er/parser.ts +0 -750
  320. package/src/er/renderer.ts +0 -725
  321. package/src/er/types.ts +0 -52
  322. package/src/error-card.ts +0 -269
  323. package/src/event-line/diagnostics.ts +0 -117
  324. package/src/event-line/parser.ts +0 -643
  325. package/src/event-line/renderer.ts +0 -1742
  326. package/src/event-line/types.ts +0 -74
  327. package/src/fonts.ts +0 -3
  328. package/src/gantt/parser.ts +0 -1641
  329. package/src/gantt/renderer.ts +0 -3787
  330. package/src/gantt/resolver.ts +0 -172
  331. package/src/gantt/types.ts +0 -206
  332. package/src/graph/diagnostics.ts +0 -64
  333. package/src/graph/edge-spline.ts +0 -29
  334. package/src/graph/flowchart-parser.ts +0 -759
  335. package/src/graph/flowchart-renderer.ts +0 -817
  336. package/src/graph/layout.ts +0 -472
  337. package/src/graph/notes.ts +0 -21
  338. package/src/graph/state-collapse.ts +0 -78
  339. package/src/graph/state-parser.ts +0 -574
  340. package/src/graph/state-renderer.ts +0 -653
  341. package/src/graph/types.ts +0 -65
  342. package/src/index.ts +0 -261
  343. package/src/infra/compute.ts +0 -1385
  344. package/src/infra/layout.ts +0 -888
  345. package/src/infra/parser.ts +0 -1234
  346. package/src/infra/renderer.ts +0 -2534
  347. package/src/infra/roles.ts +0 -75
  348. package/src/infra/types.ts +0 -216
  349. package/src/infra/validation.ts +0 -201
  350. package/src/journey-map/layout.ts +0 -490
  351. package/src/journey-map/parser.ts +0 -557
  352. package/src/journey-map/renderer.ts +0 -1580
  353. package/src/journey-map/types.ts +0 -47
  354. package/src/kanban/mutations.ts +0 -175
  355. package/src/kanban/parser.ts +0 -509
  356. package/src/kanban/renderer.ts +0 -1449
  357. package/src/kanban/types.ts +0 -39
  358. package/src/label-layout.ts +0 -368
  359. package/src/map/colorize.ts +0 -54
  360. package/src/map/completion.ts +0 -316
  361. package/src/map/context-labels.ts +0 -609
  362. package/src/map/data/PROVENANCE.json +0 -1
  363. package/src/map/data/README.md +0 -40
  364. package/src/map/data/airport-collisions.json +0 -1
  365. package/src/map/data/airports.json +0 -1
  366. package/src/map/data/gazetteer.json +0 -1
  367. package/src/map/data/lakes.json +0 -1
  368. package/src/map/data/mountain-ranges.json +0 -1
  369. package/src/map/data/na-lakes.json +0 -1
  370. package/src/map/data/na-land.json +0 -1
  371. package/src/map/data/region-names.json +0 -1
  372. package/src/map/data/rivers.json +0 -1
  373. package/src/map/data/types.ts +0 -121
  374. package/src/map/data/us-states.json +0 -1
  375. package/src/map/data/water-bodies.json +0 -1
  376. package/src/map/data/world-coarse.json +0 -1
  377. package/src/map/data/world-detail.json +0 -1
  378. package/src/map/diagnostics.ts +0 -134
  379. package/src/map/dimensions.ts +0 -150
  380. package/src/map/geo-query.ts +0 -295
  381. package/src/map/geo.ts +0 -438
  382. package/src/map/invert.ts +0 -111
  383. package/src/map/layout.ts +0 -5069
  384. package/src/map/legend-band.ts +0 -127
  385. package/src/map/load-data.ts +0 -186
  386. package/src/map/parser.ts +0 -783
  387. package/src/map/renderer.ts +0 -1445
  388. package/src/map/resolved-types.ts +0 -160
  389. package/src/map/resolver.ts +0 -1146
  390. package/src/map/types.ts +0 -194
  391. package/src/mindmap/collapse.ts +0 -67
  392. package/src/mindmap/layout.ts +0 -649
  393. package/src/mindmap/parser.ts +0 -430
  394. package/src/mindmap/renderer.ts +0 -637
  395. package/src/mindmap/text-wrap.ts +0 -218
  396. package/src/mindmap/types.ts +0 -56
  397. package/src/org/collapse.ts +0 -151
  398. package/src/org/layout.ts +0 -1262
  399. package/src/org/parser.ts +0 -519
  400. package/src/org/renderer.ts +0 -866
  401. package/src/org/resolver.ts +0 -636
  402. package/src/palettes/atlas.ts +0 -77
  403. package/src/palettes/blueprint.ts +0 -73
  404. package/src/palettes/catppuccin.ts +0 -84
  405. package/src/palettes/color-utils.ts +0 -443
  406. package/src/palettes/index.ts +0 -67
  407. package/src/palettes/nord.ts +0 -67
  408. package/src/palettes/registry.ts +0 -114
  409. package/src/palettes/slate.ts +0 -73
  410. package/src/palettes/tidewater.ts +0 -73
  411. package/src/palettes/tokyo-night.ts +0 -86
  412. package/src/palettes/types.ts +0 -88
  413. package/src/pert/analyzer.ts +0 -1183
  414. package/src/pert/diagnostics.ts +0 -110
  415. package/src/pert/index.ts +0 -43
  416. package/src/pert/internal.ts +0 -252
  417. package/src/pert/layout.ts +0 -971
  418. package/src/pert/monte-carlo.ts +0 -446
  419. package/src/pert/parser.ts +0 -1606
  420. package/src/pert/renderer.ts +0 -4009
  421. package/src/pert/share-normalize.ts +0 -62
  422. package/src/pert/types.ts +0 -604
  423. package/src/pyramid/parser.ts +0 -185
  424. package/src/pyramid/renderer.ts +0 -768
  425. package/src/pyramid/types.ts +0 -28
  426. package/src/quadrant/parser.ts +0 -18
  427. package/src/quadrant/renderer.ts +0 -638
  428. package/src/raci/diagnostics.ts +0 -213
  429. package/src/raci/index.ts +0 -23
  430. package/src/raci/mutations.ts +0 -321
  431. package/src/raci/parser.ts +0 -699
  432. package/src/raci/renderer.ts +0 -1627
  433. package/src/raci/types.ts +0 -82
  434. package/src/raci/variants.ts +0 -320
  435. package/src/render.ts +0 -268
  436. package/src/ring/parser.ts +0 -212
  437. package/src/ring/renderer.ts +0 -490
  438. package/src/ring/types.ts +0 -27
  439. package/src/sequence/collapse.ts +0 -174
  440. package/src/sequence/parser.ts +0 -1596
  441. package/src/sequence/participant-inference.ts +0 -152
  442. package/src/sequence/renderer.ts +0 -3168
  443. package/src/sequence/tag-resolution.ts +0 -164
  444. package/src/sharing.ts +0 -211
  445. package/src/sitemap/collapse.ts +0 -100
  446. package/src/sitemap/layout.ts +0 -925
  447. package/src/sitemap/parser.ts +0 -755
  448. package/src/sitemap/renderer.ts +0 -828
  449. package/src/sitemap/types.ts +0 -42
  450. package/src/slope/parser.ts +0 -18
  451. package/src/slope/renderer.ts +0 -378
  452. package/src/swimlane/diagnostics.ts +0 -94
  453. package/src/swimlane/layout.ts +0 -597
  454. package/src/swimlane/parser.ts +0 -613
  455. package/src/swimlane/renderer.ts +0 -681
  456. package/src/swimlane/types.ts +0 -151
  457. package/src/tech-radar/interactive.ts +0 -1129
  458. package/src/tech-radar/layout.ts +0 -197
  459. package/src/tech-radar/parser.ts +0 -436
  460. package/src/tech-radar/renderer.ts +0 -1239
  461. package/src/tech-radar/shared.ts +0 -151
  462. package/src/tech-radar/types.ts +0 -86
  463. package/src/themes.ts +0 -22
  464. package/src/timeline/parser.ts +0 -472
  465. package/src/timeline/renderer.ts +0 -3024
  466. package/src/timeline/types.ts +0 -33
  467. package/src/timeline/viz-parser.ts +0 -21
  468. package/src/treemap/diagnostics.ts +0 -62
  469. package/src/treemap/layout-radial.ts +0 -233
  470. package/src/treemap/layout.ts +0 -189
  471. package/src/treemap/parser.ts +0 -463
  472. package/src/treemap/renderer-radial.ts +0 -438
  473. package/src/treemap/renderer.ts +0 -572
  474. package/src/treemap/treemap-shared.ts +0 -309
  475. package/src/treemap/types.ts +0 -60
  476. package/src/utils/arrow-markers.ts +0 -51
  477. package/src/utils/arrows-diagnostics.ts +0 -62
  478. package/src/utils/arrows.ts +0 -227
  479. package/src/utils/brand.ts +0 -61
  480. package/src/utils/card.ts +0 -183
  481. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  482. package/src/utils/collapse-engine/tree.ts +0 -117
  483. package/src/utils/d3-helpers.ts +0 -219
  484. package/src/utils/d3-types.ts +0 -9
  485. package/src/utils/description-helpers.ts +0 -38
  486. package/src/utils/duration.ts +0 -312
  487. package/src/utils/export-container.ts +0 -41
  488. package/src/utils/expr-eval.ts +0 -27
  489. package/src/utils/fit-canvas.ts +0 -64
  490. package/src/utils/hover-styles.ts +0 -673
  491. package/src/utils/inline-markdown.ts +0 -96
  492. package/src/utils/legend-constants.ts +0 -48
  493. package/src/utils/legend-d3.ts +0 -618
  494. package/src/utils/legend-integration.ts +0 -107
  495. package/src/utils/legend-layout.ts +0 -776
  496. package/src/utils/legend-svg.ts +0 -214
  497. package/src/utils/legend-types.ts +0 -272
  498. package/src/utils/name-normalize.ts +0 -163
  499. package/src/utils/note-box/constants.ts +0 -25
  500. package/src/utils/note-box/index.ts +0 -11
  501. package/src/utils/note-box/metrics.ts +0 -90
  502. package/src/utils/note-box/svg.ts +0 -331
  503. package/src/utils/notes/bounds.ts +0 -30
  504. package/src/utils/notes/build.ts +0 -131
  505. package/src/utils/notes/index.ts +0 -18
  506. package/src/utils/notes/model.ts +0 -19
  507. package/src/utils/notes/parse.ts +0 -131
  508. package/src/utils/notes/place.ts +0 -177
  509. package/src/utils/notes/resolve.ts +0 -88
  510. package/src/utils/number-format.ts +0 -36
  511. package/src/utils/parsing.ts +0 -953
  512. package/src/utils/reserved-key-registry.ts +0 -239
  513. package/src/utils/safe-href.ts +0 -38
  514. package/src/utils/scaling.ts +0 -97
  515. package/src/utils/svg-embed.ts +0 -251
  516. package/src/utils/tag-groups.ts +0 -607
  517. package/src/utils/text-measure.ts +0 -122
  518. package/src/utils/title-constants.ts +0 -24
  519. package/src/utils/visual-conventions.ts +0 -61
  520. package/src/utils/wrapped-desc.ts +0 -87
  521. package/src/venn/parser.ts +0 -18
  522. package/src/venn/renderer.ts +0 -1047
  523. package/src/version-control/diagnostics.ts +0 -67
  524. package/src/version-control/parser.ts +0 -544
  525. package/src/version-control/renderer.ts +0 -724
  526. package/src/version-control/types.ts +0 -100
  527. package/src/visualizations/parse.ts +0 -1560
  528. package/src/visualizations/types.ts +0 -262
  529. package/src/wireframe/layout.ts +0 -479
  530. package/src/wireframe/parser.ts +0 -1114
  531. package/src/wireframe/renderer.ts +0 -1318
  532. package/src/wireframe/types.ts +0 -110
  533. package/src/wordcloud/parser.ts +0 -18
  534. package/src/wordcloud/renderer.ts +0 -357
@@ -1,1627 +0,0 @@
1
- // ============================================================
2
- // RACI / RASCI / DACI — D3 SVG Renderer
3
- // ============================================================
4
- //
5
- // Treatment: color-blocked letters (cell fill = palette tint per
6
- // marker; letter on top with palette-aware contrast text). One of
7
- // the three Task-7 prototype options; chosen for v1 because:
8
- //
9
- // - At gallery thumbnail scale, the marker palette reads as the
10
- // dominant signal (matches conventional RACI visual language).
11
- // - Multi-marker cells (`A R`) stay legible — letters don't pile up.
12
- // - Empty cells are visually distinct (subtle hairline border) per
13
- // AC 13's "ready to author" requirement.
14
- //
15
- // Locked in `docs/architecture/adr-NNN-raci-renderer-visual.md`
16
- // (Phase 6 deliverable). Switching to dot-markers or plain letters
17
- // in a future iteration only changes the cell-paint code path.
18
- //
19
- // SVG attribute conventions (per `sequence/renderer.ts:2310-2317`):
20
- // - Each phase `<g>` carries `data-section`, `data-section-toggle`,
21
- // `data-line-number`. Children carry `data-line-number` only.
22
- // - Task rows carry `data-line-number` for editor highlighting.
23
- // - Cells carry `data-line-number` to the role-assignment line so
24
- // click → editor cursor jumps to the source.
25
-
26
- import * as d3Selection from 'd3-selection';
27
- import { FONT_FAMILY } from '../fonts';
28
- import {
29
- TITLE_FONT_SIZE,
30
- TITLE_FONT_WEIGHT,
31
- TITLE_Y,
32
- } from '../utils/title-constants';
33
- import { contrastText, mix, themeBaseBg } from '../palettes/color-utils';
34
- import type { PaletteColors } from '../palettes';
35
- import type { D3ExportDimensions } from '../utils/d3-types';
36
- import type {
37
- ParsedRaci,
38
- RaciMarker,
39
- RaciPhase,
40
- RaciTask,
41
- RaciVariant,
42
- } from './types';
43
- import { allTasks } from './parser';
44
- import { VARIANTS } from './variants';
45
- import { ScaleContext } from '../utils/scaling';
46
- import {
47
- measureText,
48
- truncateText,
49
- wrapTextToWidth,
50
- } from '../utils/text-measure';
51
-
52
- /**
53
- * Group `parsed.diagnostics` per task, so the renderer can paint a
54
- * violation indicator next to each task's label. Diagnostics whose
55
- * line falls inside a task's [lineNumber, endLineNumber] range are
56
- * attributed to that task. Out-of-range diagnostics are dropped (chart-
57
- * level concerns are surfaced by the diagnostics panel, not the canvas).
58
- */
59
- interface DiagnosticEntry {
60
- line: number;
61
- message: string;
62
- }
63
-
64
- function groupDiagnosticsByTask(parsed: ParsedRaci): Map<
65
- string,
66
- {
67
- errors: DiagnosticEntry[];
68
- warnings: DiagnosticEntry[];
69
- /** First diagnostic line for click-to-navigate. */
70
- firstLine: number;
71
- }
72
- > {
73
- const map = new Map<
74
- string,
75
- {
76
- errors: DiagnosticEntry[];
77
- warnings: DiagnosticEntry[];
78
- firstLine: number;
79
- }
80
- >();
81
- if (parsed.diagnostics.length === 0) return map;
82
-
83
- const tasks: RaciTask[] = [];
84
- for (const t of parsed.tasksWithoutPhase) tasks.push(t);
85
- for (const p of parsed.phases) for (const t of p.tasks) tasks.push(t);
86
-
87
- for (const d of parsed.diagnostics) {
88
- if (d.line <= 0) continue;
89
- const owner = tasks.find(
90
- (t) => d.line >= t.lineNumber && d.line <= t.endLineNumber
91
- );
92
- if (!owner) continue;
93
- let bucket = map.get(owner.id);
94
- if (!bucket) {
95
- bucket = { errors: [], warnings: [], firstLine: d.line };
96
- map.set(owner.id, bucket);
97
- } else if (d.line < bucket.firstLine) {
98
- bucket.firstLine = d.line;
99
- }
100
- const entry = { line: d.line, message: d.message };
101
- if (d.severity === 'error') bucket.errors.push(entry);
102
- else bucket.warnings.push(entry);
103
- }
104
- return map;
105
- }
106
-
107
- /**
108
- * Constraint-rule messages name the task they're about — either as a
109
- * `Task 'X' …` prefix (most rules) or an `… on task 'X' …` mid-clause
110
- * (conflicting-markers rule). When rendered right next to the task
111
- * label, that naming is redundant — strip it so the inline preview
112
- * reads cleanly (`Has no Responsible assigned.` instead of
113
- * `Task 'Stand the watch' has no Responsible assigned.`).
114
- *
115
- * The full form stays in `parsed.diagnostics` for the bottom
116
- * diagnostics panel, which lists messages outside their task row.
117
- */
118
- function trimTaskPrefix(message: string): string {
119
- const prefix = message.match(/^Task '[^']*'\s+(.+)$/);
120
- if (prefix) {
121
- // Capture group [1] guaranteed by regex match.
122
- const rest = prefix[1]!;
123
- return rest.charAt(0).toUpperCase() + rest.slice(1);
124
- }
125
- return message.replace(/\s+on task '[^']*'/, '');
126
- }
127
-
128
- /**
129
- * Build an SVG path for a rectangle with only the top corners rounded —
130
- * matches the kanban-style "column" silhouette where the bottom runs
131
- * straight to the edge so the column reads as a continuing container.
132
- */
133
- function topRoundedRectPath(
134
- x: number,
135
- y: number,
136
- w: number,
137
- h: number,
138
- r: number
139
- ): string {
140
- const rr = Math.min(r, w / 2, h / 2);
141
- return (
142
- `M ${x} ${y + rr}` +
143
- ` A ${rr} ${rr} 0 0 1 ${x + rr} ${y}` +
144
- ` H ${x + w - rr}` +
145
- ` A ${rr} ${rr} 0 0 1 ${x + w} ${y + rr}` +
146
- ` V ${y + h}` +
147
- ` H ${x}` +
148
- ` Z`
149
- );
150
- }
151
-
152
- /** Sort markers into canonical alphabet order for display. */
153
- function sortByAlphabet(
154
- markers: ReadonlyArray<RaciMarker>,
155
- alphabet: ReadonlyArray<RaciMarker>
156
- ): RaciMarker[] {
157
- const order = new Map<RaciMarker, number>(
158
- alphabet.map((m, i) => [m, i] as const)
159
- );
160
- return [...markers].sort(
161
- (a, b) => (order.get(a) ?? 99) - (order.get(b) ?? 99)
162
- );
163
- }
164
-
165
- // ── Marker semantic labels (variant-aware) ───────────────────
166
- //
167
- // The legend chip shows both the letter and its full meaning.
168
- // Variant differences are deliberate: in DACI the `A` is "Approver"
169
- // (not "Accountable") and `C` is "Contributor" (not "Consulted") —
170
- // teaching the meaning is part of the legend's job.
171
- export const MARKER_LABELS: Readonly<
172
- Record<RaciVariant, Readonly<Partial<Record<RaciMarker, string>>>>
173
- > = {
174
- raci: { R: 'Responsible', A: 'Accountable', C: 'Consulted', I: 'Informed' },
175
- rasci: {
176
- R: 'Responsible',
177
- A: 'Accountable',
178
- S: 'Support',
179
- C: 'Consulted',
180
- I: 'Informed',
181
- },
182
- daci: { D: 'Driver', A: 'Approver', C: 'Contributor', I: 'Informed' },
183
- };
184
-
185
- // ── Layout constants ─────────────────────────────────────────
186
-
187
- const TITLE_AREA_HEIGHT = 50;
188
- /** Left/right page margin. */
189
- const H_MARGIN = 24;
190
- /** Top breathing room below title. */
191
- const V_MARGIN = 16;
192
- /**
193
- * Marker palette / legend chip height. Matches the in-cell marker
194
- * slice height (`ROW_HEIGHT - 2 * CELL_PAD = 28`) so legend chips
195
- * read as the same primitive as the slices inside the matrix.
196
- */
197
- const LEGEND_HEIGHT = 28;
198
- /** Gap between adjacent legend chips. */
199
- const LEGEND_CHIP_GAP = 8;
200
- /** Min chip width when rendering with full marker labels. */
201
- const LEGEND_CHIP_LABEL_MIN = 96;
202
- /** Chip width when only the marker letter is shown (compact mode). Square = same as cell height. */
203
- const LEGEND_LETTER_CHIP_W = 28;
204
- /** Min horizontal gap between title and right-aligned legend. */
205
- const TITLE_LEGEND_GAP = 16;
206
- /** Label font in legend chips. */
207
- const LEGEND_LABEL_FONT = 12;
208
- /** Letter font in legend chips. */
209
- const LEGEND_LETTER_FONT = 14;
210
- /** Header row that holds the role names. Matches kanban COLUMN_HEADER_HEIGHT. */
211
- const HEADER_HEIGHT = 36;
212
- /** Each task row. */
213
- const ROW_HEIGHT = 36;
214
- /** Phase divider band height (full-width above the phase's tasks).
215
- * Sized with breathing room so the per-column collapsed-phase
216
- * summary chips can match the regular cell geometry without crowding
217
- * the phase title. */
218
- const PHASE_HEIGHT = 40;
219
- /** Width allocated to the task-label column on the left. */
220
- const TASK_LABEL_MIN = 200;
221
- const TASK_LABEL_MAX = 360;
222
- /** Min/max role column width — clamped if too few or too many roles. */
223
- const ROLE_COL_MIN = 64;
224
- const ROLE_COL_MAX = 120;
225
- /** Inter-cell padding. */
226
- const CELL_PAD = 4;
227
- /** Extra vertical space added to a task row when it has a description. */
228
- /** Vertical space per stacked violation message line on a task row. */
229
- const VIOLATION_LINE_HEIGHT = 14;
230
- /**
231
- * Breathing room between the task label and the first line of stacked
232
- * content (description or violation). Without this the secondary lines
233
- * sit visually crammed against the task name.
234
- */
235
- const STACK_TOP_GAP = 8;
236
- /** Description font size — smaller than LABEL_FONT for visual hierarchy. */
237
- const DESC_FONT = 11;
238
- /** Per-role column corner radius (matches kanban COLUMN_RADIUS). */
239
- const COLUMN_RADIUS = 8;
240
- /**
241
- * Inset on each side of a role column, producing a 16-px visual gap
242
- * between columns — matches kanban COLUMN_GAP exactly.
243
- */
244
- const COLUMN_INSET = 8;
245
- /**
246
- * Extra space below the last task row, kept inside the column body so
247
- * the rounded bottom corners read clearly. Mirrors kanban COLUMN_PADDING.
248
- */
249
- const COLUMN_BOTTOM_PAD = 14;
250
- /** Marker letter font size. */
251
- const MARKER_FONT = 14;
252
- const MARKER_FONT_WEIGHT = 600;
253
- /** Task-label font size. */
254
- const LABEL_FONT = 13;
255
- /** Role-header font size. */
256
- const ROLE_HEADER_FONT = 12;
257
- /** Phase-bar font size. */
258
- const PHASE_FONT = 13;
259
-
260
- // Cell-fill tint percentage of the marker color over surface bg.
261
- // 25 = the canonical `shapeFill` tint used by every other chart type.
262
- const TINT_PCT = 25;
263
- /**
264
- * Marker rect stroke + radius — matches the codebase's node styling
265
- * convention (flowchart `NODE_STROKE_WIDTH = 1.5`, kanban
266
- * `CARD_STROKE_WIDTH = 1.5`, c4 `NODE_STROKE_WIDTH = 1.5`,
267
- * kanban `CARD_RADIUS = 6`). Keeps RACI markers visually consistent
268
- * with nodes elsewhere in the diagram language.
269
- */
270
- import { NODE_STROKE_WIDTH } from '../utils/visual-conventions'; // shared (Story 111.1)
271
- const NODE_RADIUS = 6;
272
-
273
- /**
274
- * Map each marker letter to a semantic color drawn from the palette's
275
- * named accents. The mapping follows the most common convention seen
276
- * across Atlassian / Smartsheet / Excel template defaults:
277
- * - R Responsible → green (active doer / "go" signal)
278
- * - A Accountable → red (owns it / "buck stops here")
279
- * - C Consulted → yellow
280
- * - I Informed → gray
281
- * - S Support → teal (RASCI; close to R-green but cooler)
282
- * - D Driver → purple (DACI; distinct from A's red)
283
- *
284
- * Palette swap → markers swap; semantics stay constant.
285
- */
286
- /**
287
- * Auto-accent rotation used when the user hasn't given a phase / role
288
- * an explicit `| color: …`. Cycles through every named accent (gray
289
- * excluded — reads as "disabled") so users can refer to phases and
290
- * columns by color name. Marker fills sit on top at full saturation
291
- * inside small chips, so even a "red column" + Accountable marker
292
- * read as separate elements.
293
- *
294
- * Order alternates warm / cool so adjacent phases / columns don't
295
- * blur into one another.
296
- */
297
- const AUTO_ACCENTS: ReadonlyArray<keyof PaletteColors['colors']> = [
298
- 'blue',
299
- 'orange',
300
- 'green',
301
- 'purple',
302
- 'yellow',
303
- 'cyan',
304
- 'red',
305
- 'teal',
306
- ];
307
-
308
- function autoAccent(index: number, palette: PaletteColors): string {
309
- // In-bounds: (index % AUTO_ACCENTS.length) is always within bounds (AUTO_ACCENTS is non-empty).
310
- return palette.colors[AUTO_ACCENTS[index % AUTO_ACCENTS.length]!];
311
- }
312
-
313
- function markerColor(marker: RaciMarker, palette: PaletteColors): string {
314
- switch (marker) {
315
- case 'R':
316
- return palette.colors.green;
317
- case 'A':
318
- return palette.colors.red;
319
- case 'S':
320
- return palette.colors.teal;
321
- case 'C':
322
- return palette.colors.yellow;
323
- case 'I':
324
- return palette.colors.gray;
325
- case 'D':
326
- return palette.colors.purple;
327
- }
328
- }
329
-
330
- // ── Public render entry points ───────────────────────────────
331
-
332
- /** Drag-source identity passed to `onMarkerDragStart`. */
333
- export type RaciDragSource =
334
- | { kind: 'legend'; marker: RaciMarker }
335
- | {
336
- kind: 'cell';
337
- marker: RaciMarker;
338
- taskId: string;
339
- roleId: string;
340
- };
341
-
342
- export interface RaciInteractionHandlers {
343
- onClickLine?: (lineNumber: number) => void;
344
- /** Fires on `pointerdown` of a legend chip OR an in-cell marker slice. */
345
- onMarkerDragStart?: (source: RaciDragSource, event: PointerEvent) => void;
346
- /**
347
- * Suppress the in-SVG legend strip. The app overlays an HTML legend
348
- * for native HTML5 drag — SVG `<g> draggable=true` is unreliable.
349
- * PNG/SVG export keeps the legend by default.
350
- */
351
- hideLegend?: boolean;
352
- /**
353
- * Phase ids whose tasks should be hidden. The phase header still
354
- * renders with a "collapsed" chevron so the user can re-expand.
355
- */
356
- collapsedPhases?: ReadonlySet<string>;
357
- /**
358
- * Suppress the in-SVG title. The app paints its own HTML title
359
- * above the HTML legend bar so the visual order is title → legend
360
- * → matrix. PNG/SVG export keeps the title by default.
361
- */
362
- hideTitle?: boolean;
363
- }
364
-
365
- /**
366
- * Render a RACI / RASCI / DACI matrix into the given DOM container.
367
- * Layout is computed once and laid out as SVG; no animation.
368
- */
369
- export function renderRaci(
370
- container: HTMLDivElement,
371
- parsed: ParsedRaci,
372
- palette: PaletteColors,
373
- isDark: boolean,
374
- handlers?: RaciInteractionHandlers | ((lineNumber: number) => void),
375
- exportDims?: D3ExportDimensions
376
- ): void {
377
- // Backwards-compat: previous signature accepted `onClickLine` directly.
378
- const opts: RaciInteractionHandlers =
379
- typeof handlers === 'function'
380
- ? { onClickLine: handlers }
381
- : (handlers ?? {});
382
- const onClickLine = opts.onClickLine;
383
- const onMarkerDragStart = opts.onMarkerDragStart;
384
- const hideLegend = opts.hideLegend ?? false;
385
- const collapsedPhases = opts.collapsedPhases ?? new Set<string>();
386
- const hideTitle =
387
- (opts.hideTitle ?? false) || parsed.options['no-title'] === 'on';
388
- d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
389
- const width = exportDims?.width ?? container.clientWidth;
390
- const height = exportDims?.height ?? container.clientHeight;
391
- if (width <= 0 || height <= 0) return;
392
-
393
- const tasksAll = [...allTasks(parsed)];
394
- if (tasksAll.length === 0 && parsed.phases.length === 0) return;
395
-
396
- const solid = parsed.options['solid-fill'] === 'on';
397
- const surfaceBg = themeBaseBg(palette, isDark);
398
-
399
- // --- ScaleContext: differential scaling ---
400
- const roleCount = Math.max(1, parsed.roles.length);
401
- const idealWidth = roleCount * ROLE_COL_MAX + TASK_LABEL_MAX + 2 * H_MARGIN;
402
- const ctx = exportDims
403
- ? ScaleContext.identity()
404
- : ScaleContext.from(width, idealWidth);
405
-
406
- const sTitleFontSize = ctx.text(TITLE_FONT_SIZE);
407
- const sTitleY = ctx.structural(TITLE_Y);
408
- const sHMargin = ctx.aesthetic(H_MARGIN);
409
- const sVMargin = ctx.aesthetic(V_MARGIN);
410
- const sTitleAreaHeight = ctx.structural(TITLE_AREA_HEIGHT);
411
- const sHeaderHeight = ctx.structural(HEADER_HEIGHT);
412
- const sRowHeight = ctx.structural(ROW_HEIGHT);
413
- const sPhaseHeight = ctx.structural(PHASE_HEIGHT);
414
- const sTaskLabelMin = ctx.structural(TASK_LABEL_MIN);
415
- const sTaskLabelMax = ctx.structural(TASK_LABEL_MAX);
416
- const sRoleColMin = ctx.structural(ROLE_COL_MIN);
417
- const sRoleColMax = ctx.structural(ROLE_COL_MAX);
418
- const sCellPad = ctx.structural(CELL_PAD);
419
- const sLabelFont = ctx.text(LABEL_FONT);
420
- const sRoleHeaderFont = ctx.text(ROLE_HEADER_FONT);
421
- const sPhaseFont = ctx.text(PHASE_FONT);
422
- const sMarkerFont = ctx.text(MARKER_FONT);
423
- const sDescFont = ctx.text(DESC_FONT);
424
- const sColumnRadius = ctx.structural(COLUMN_RADIUS);
425
- const sColumnInset = ctx.structural(COLUMN_INSET);
426
- const sColumnBottomPad = ctx.structural(COLUMN_BOTTOM_PAD);
427
- const sLegendHeight = ctx.structural(LEGEND_HEIGHT);
428
- const sLegendChipGap = ctx.structural(LEGEND_CHIP_GAP);
429
- const sLegendChipLabelMin = ctx.structural(LEGEND_CHIP_LABEL_MIN);
430
- const sLegendLetterChipW = ctx.structural(LEGEND_LETTER_CHIP_W);
431
- const sTitleLegendGap = ctx.aesthetic(TITLE_LEGEND_GAP);
432
- const sLegendLabelFont = ctx.text(LEGEND_LABEL_FONT);
433
- const sLegendLetterFont = ctx.text(LEGEND_LETTER_FONT);
434
- const sViolationLineHeight = ctx.structural(VIOLATION_LINE_HEIGHT);
435
- const sStackTopGap = ctx.structural(STACK_TOP_GAP);
436
- const sNodeStrokeWidth = ctx.structural(NODE_STROKE_WIDTH);
437
- const sNodeRadius = ctx.structural(NODE_RADIUS);
438
-
439
- // ── Column widths ──────────────────────────────────────────
440
-
441
- const innerWidth = Math.max(0, width - 2 * sHMargin);
442
-
443
- // Greedy split: prefer sRoleColMin..sRoleColMax, give the rest to
444
- // the task-label column (clamped to a reasonable range).
445
- let roleColW = Math.max(
446
- sRoleColMin,
447
- Math.min(sRoleColMax, Math.floor((innerWidth - sTaskLabelMin) / roleCount))
448
- );
449
- let labelW = innerWidth - roleColW * roleCount;
450
- if (labelW > sTaskLabelMax) {
451
- labelW = sTaskLabelMax;
452
- roleColW = Math.max(
453
- sRoleColMin,
454
- Math.floor((innerWidth - labelW) / roleCount)
455
- );
456
- }
457
- if (labelW < sTaskLabelMin) labelW = sTaskLabelMin;
458
-
459
- // ── Vertical layout ────────────────────────────────────────
460
-
461
- // Pre-compute per-task diagnostic buckets so the row-height pass can
462
- // grow rows that need to fit multiple stacked violation messages.
463
- const taskDiagnostics = groupDiagnosticsByTask(parsed);
464
-
465
- const phasedRows: Array<
466
- | { kind: 'phase'; phase: RaciPhase; collapsed: boolean }
467
- | { kind: 'task'; task: RaciTask }
468
- > = [];
469
- for (const t of parsed.tasksWithoutPhase)
470
- phasedRows.push({ kind: 'task', task: t });
471
- for (const p of parsed.phases) {
472
- const collapsed = collapsedPhases.has(p.id);
473
- phasedRows.push({ kind: 'phase', phase: p, collapsed });
474
- if (!collapsed) {
475
- for (const t of p.tasks) phasedRows.push({ kind: 'task', task: t });
476
- }
477
- }
478
-
479
- // Vertical stack: [title row + inline legend] → role headers → body.
480
- // Title and legend share a single row at the top (kanban-style):
481
- // title left-aligned, legend right-aligned, both vertically centered.
482
- // When both are hidden (app preview with HTML overlays), the row collapses.
483
- const hasTopRow = !hideTitle || !hideLegend;
484
- const titleArea = hasTopRow ? sTitleAreaHeight : sVMargin;
485
- const legendY = (sTitleAreaHeight - sLegendHeight) / 2;
486
- const headerY = titleArea + sVMargin;
487
- const bodyTop = headerY + sHeaderHeight;
488
- let cursorY = bodyTop;
489
- const rowYs: number[] = [];
490
- // Pre-compute wrapped task content once: both the y-cursor pass and
491
- // the renderer need it, and word-wrapping isn't free.
492
- const labelMaxW = labelW - 16;
493
- const taskRowContent = new Map<string, RowContent>();
494
- for (const row of phasedRows) {
495
- if (row.kind === 'task') {
496
- const bucket = taskDiagnostics.get(row.task.id);
497
- taskRowContent.set(
498
- row.task.id,
499
- prepareRowContent(
500
- row.task,
501
- bucket,
502
- labelMaxW,
503
- sLabelFont,
504
- sDescFont,
505
- sRowHeight,
506
- sStackTopGap,
507
- sViolationLineHeight
508
- )
509
- );
510
- }
511
- }
512
- for (const row of phasedRows) {
513
- rowYs.push(cursorY);
514
- if (row.kind === 'phase') {
515
- cursorY += sPhaseHeight;
516
- } else {
517
- const content = taskRowContent.get(row.task.id);
518
- cursorY += content?.rowHeight ?? sRowHeight;
519
- }
520
- }
521
- const colBottomY = cursorY + sColumnBottomPad;
522
- const totalHeight = colBottomY + sVMargin;
523
-
524
- // Export renders a fixed canvas (e.g. 1200×800); a short matrix would
525
- // otherwise reserve a tall band of dead space below the last row. Size the
526
- // export canvas to the content height. The interactive preview keeps the
527
- // `max(pane, content)` behaviour so a short matrix still fills the pane.
528
- const svgHeight = exportDims ? totalHeight : Math.max(height, totalHeight);
529
-
530
- // ── SVG root ───────────────────────────────────────────────
531
-
532
- const svg = d3Selection
533
- .select(container)
534
- .append('svg')
535
- .attr('xmlns', 'http://www.w3.org/2000/svg')
536
- .attr('width', width)
537
- .attr('height', svgHeight)
538
- .attr('viewBox', `0 0 ${width} ${svgHeight}`)
539
- .attr('preserveAspectRatio', 'xMidYMin meet')
540
- .attr('font-family', FONT_FAMILY)
541
- .style('background', 'transparent');
542
-
543
- if (ctx.isBelowFloor) {
544
- svg.attr('width', '100%');
545
- }
546
-
547
- // ── Top row: title (left) + legend (right), inline like kanban ──
548
-
549
- // Decide legend geometry first so we know how much room is left for the title.
550
- const variantLabels = MARKER_LABELS[parsed.variant];
551
- const legendMarkers = (Object.keys(variantLabels) as RaciMarker[]).filter(
552
- (m) => variantLabels[m] !== undefined
553
- );
554
- const numChips = legendMarkers.length;
555
- const chipGapTotal = sLegendChipGap * Math.max(0, numChips - 1);
556
-
557
- // Size full-mode chips to actually fit the longest label without truncation.
558
- // chip = 4 pad + 24 slab + 8 gap + label + 8 right pad.
559
- const longestLabelPx = legendMarkers.reduce(
560
- (n, m) =>
561
- Math.max(n, measureText(variantLabels[m] ?? '', sLegendLabelFont)),
562
- 0
563
- );
564
- const fullChipW = Math.max(
565
- sLegendChipLabelMin,
566
- Math.ceil(4 + 24 + 8 + longestLabelPx + 8)
567
- );
568
- const fullLegendW = numChips * fullChipW + chipGapTotal;
569
- const letterLegendW = numChips * sLegendLetterChipW + chipGapTotal;
570
-
571
- // Estimate title pixel width using the shared glyph measurer.
572
- const titleEstW =
573
- parsed.title && !hideTitle ? measureText(parsed.title, sTitleFontSize) : 0;
574
-
575
- // Pick legend mode: full chips if everything fits, else letter-only.
576
- // If even letters won't fit beside the title, the title is truncated below.
577
- let legendMode: 'full' | 'letters' = 'full';
578
- let legendTotalW = fullLegendW;
579
- let legendChipW = fullChipW;
580
- if (!hideLegend) {
581
- const remaining = innerWidth - titleEstW - sTitleLegendGap;
582
- if (remaining < fullLegendW) {
583
- legendMode = 'letters';
584
- legendTotalW = letterLegendW;
585
- legendChipW = sLegendLetterChipW;
586
- }
587
- } else {
588
- legendTotalW = 0;
589
- }
590
-
591
- const legendX = sHMargin + innerWidth - legendTotalW;
592
- const titleMaxW = hideLegend
593
- ? innerWidth
594
- : legendX - sHMargin - sTitleLegendGap;
595
-
596
- // Title — left-aligned, truncated to whatever room the legend leaves.
597
- if (parsed.title && !hideTitle) {
598
- svg
599
- .append('text')
600
- .attr('x', sHMargin)
601
- .attr('y', sTitleY)
602
- .attr('text-anchor', 'start')
603
- .attr('font-size', sTitleFontSize)
604
- .attr('font-weight', TITLE_FONT_WEIGHT)
605
- .attr('fill', palette.text)
606
- .text(truncateText(parsed.title, sTitleFontSize, titleMaxW));
607
- }
608
-
609
- // Legend — right-aligned in the same row.
610
- if (!hideLegend && numChips > 0) {
611
- renderLegend(
612
- svg,
613
- legendMarkers,
614
- parsed.variant,
615
- legendX,
616
- legendY,
617
- legendMode,
618
- legendChipW,
619
- palette,
620
- surfaceBg,
621
- solid,
622
- onMarkerDragStart,
623
- sLegendChipGap,
624
- sLegendHeight,
625
- sNodeRadius,
626
- sNodeStrokeWidth,
627
- sLegendLetterFont,
628
- sLegendLabelFont
629
- );
630
- }
631
-
632
- // ── Per-role columns (kanban-style: rounded body bg + header overlay) ──
633
-
634
- const roleX = (i: number): number => sHMargin + labelW + i * roleColW;
635
-
636
- // Column treatment: each role gets its own rounded rect spanning the full
637
- // body height, with a clearly darker header overlay on top. Drawn before
638
- // any body content so phase bars and marker cells render above it.
639
- const colTotalHeight = colBottomY - headerY;
640
-
641
- // No-phase row bands: when the chart has no [Section] bars carrying
642
- // visual rhythm, paint a full-width tinted band per task row so each
643
- // row reads as its own micro-section. Drawn BEFORE columns/cells so
644
- // column tints stack on top without muddying. A 4-px y-margin keeps
645
- // adjacent bands from touching.
646
- if (parsed.phases.length === 0) {
647
- const bandsG = svg.append('g').attr('class', 'raci-row-bands');
648
- phasedRows.forEach((row, i) => {
649
- if (row.kind !== 'task') return;
650
- const taskIdx = parsed.tasksWithoutPhase.indexOf(row.task);
651
- if (taskIdx < 0) return;
652
- // In-bounds: forEach index i is < rowYs.length (rowYs aligned with phasedRows).
653
- const yTop = rowYs[i]!;
654
- const rh = taskRowContent.get(row.task.id)?.rowHeight ?? sRowHeight;
655
- bandsG
656
- .append('rect')
657
- .attr('class', 'raci-row-band')
658
- .attr('data-task-id', row.task.id)
659
- .attr('x', sHMargin)
660
- .attr('y', yTop + 2)
661
- .attr('width', innerWidth)
662
- .attr('height', rh - 4)
663
- .attr('rx', 6)
664
- .attr('fill', mix(autoAccent(taskIdx, palette), surfaceBg, 12));
665
- });
666
- }
667
-
668
- // Each role gets its own <g class="raci-column" data-role-id> wrapping
669
- // body + header + name text. This lets the app's sync CSS dim other
670
- // columns when one is hovered: any descendant getting :hover bubbles
671
- // up so the wrapper's :hover state fires for the whole column area.
672
- const columnsG = svg.append('g').attr('class', 'raci-columns');
673
- parsed.roles.forEach((roleId, i) => {
674
- const cx = roleX(i) + sColumnInset;
675
- const cw = roleColW - 2 * sColumnInset;
676
- // Per-role color from `Cap blue` trailing-token (or `Cap | color: blue`)
677
- // syntax. When the user provides
678
- // one, it wins; otherwise rotate through marker-safe accents so
679
- // each column has a subtle visual identity instead of every column
680
- // reading as the same neutral gray.
681
- const roleColor = parsed.roleColors[i] ?? autoAccent(i, palette);
682
- const bodyFill = mix(roleColor, themeBaseBg(palette, isDark), 16);
683
- const headerFill = mix(
684
- roleColor,
685
- themeBaseBg(palette, isDark),
686
- 30
687
- );
688
- const colG = columnsG
689
- .append('g')
690
- .attr('class', 'raci-column')
691
- .attr('data-role-id', roleId);
692
- colG
693
- .append('rect')
694
- .attr('class', 'raci-column-body')
695
- .attr('x', cx)
696
- .attr('y', headerY)
697
- .attr('width', cw)
698
- .attr('height', colTotalHeight)
699
- .attr('rx', sColumnRadius)
700
- .attr('fill', bodyFill);
701
- // Top-rounded path (flat bottom) — matches kanban COLUMN_HEADER. The
702
- // header sits flush against the body content below; rounded bottoms
703
- // would leave a visible seam between header and the first task row.
704
- colG
705
- .append('path')
706
- .attr('class', 'raci-column-header')
707
- .attr(
708
- 'd',
709
- topRoundedRectPath(cx, headerY, cw, sHeaderHeight, sColumnRadius)
710
- )
711
- .attr('fill', headerFill);
712
- const cxText = roleX(i) + roleColW / 2;
713
- colG
714
- .append('text')
715
- .attr('class', 'raci-column-label')
716
- .attr('x', cxText)
717
- .attr('y', headerY + sHeaderHeight / 2)
718
- .attr('text-anchor', 'middle')
719
- .attr('dominant-baseline', 'central')
720
- .attr('font-size', sRoleHeaderFont)
721
- .attr('font-weight', 600)
722
- .attr('fill', palette.text)
723
- .text(
724
- truncateText(
725
- parsed.roleDisplayNames[i] ?? '',
726
- sRoleHeaderFont,
727
- roleColW - 2 * sCellPad
728
- )
729
- );
730
- });
731
-
732
- // ── Body: phases + task rows ───────────────────────────────
733
-
734
- const hasAnyDiagnostic = taskDiagnostics.size > 0;
735
-
736
- // Chart-wide collapsed-summary decision. If ANY collapsed phase would
737
- // need a column whose chip-slice width drops below the readable
738
- // threshold, suppress summary chips on EVERY collapsed bar — so the
739
- // user never wonders "why does Filing have data but Settlement
740
- // doesn't?". Either every collapsed bar shows chips or none do.
741
- const SUMMARY_MIN_SLICE_W = 22;
742
- const summaryInsetForCheck = sColumnInset + 4;
743
- const summaryColBodyWForCheck = roleColW - 2 * summaryInsetForCheck;
744
- let chartCanShowSummaries = true;
745
- for (const row of phasedRows) {
746
- if (row.kind !== 'phase' || !row.collapsed) continue;
747
- for (const roleId of parsed.roles) {
748
- const used = new Set<RaciMarker>();
749
- for (const t of row.phase.tasks) {
750
- const a = t.roleAssignments.find((r) => r.id === roleId);
751
- if (!a) continue;
752
- for (const m of a.markers) used.add(m);
753
- }
754
- if (used.size === 0) continue;
755
- const totalGap = SLICE_GAP * Math.max(0, used.size - 1);
756
- const sliceW = (summaryColBodyWForCheck - totalGap) / used.size;
757
- if (sliceW < SUMMARY_MIN_SLICE_W) {
758
- chartCanShowSummaries = false;
759
- break;
760
- }
761
- }
762
- if (!chartCanShowSummaries) break;
763
- }
764
-
765
- for (let i = 0; i < phasedRows.length; i++) {
766
- // In-bounds by loop guard (i < phasedRows.length); rowYs aligned with phasedRows.
767
- const row = phasedRows[i]!;
768
- const y = rowYs[i]!;
769
- if (row.kind === 'phase') {
770
- const phaseIdx = parsed.phases.indexOf(row.phase);
771
- renderPhaseBar(
772
- svg,
773
- row.phase,
774
- sHMargin,
775
- y,
776
- innerWidth,
777
- palette,
778
- row.collapsed,
779
- autoAccent(Math.max(0, phaseIdx), palette),
780
- parsed.roles,
781
- roleX,
782
- roleColW,
783
- parsed.variant,
784
- surfaceBg,
785
- solid,
786
- chartCanShowSummaries,
787
- sPhaseHeight,
788
- sPhaseFont,
789
- sColumnInset,
790
- sNodeRadius
791
- );
792
- } else {
793
- renderTaskRow(
794
- svg,
795
- row.task,
796
- parsed,
797
- sHMargin,
798
- y,
799
- labelW,
800
- roleX,
801
- roleColW,
802
- palette,
803
- surfaceBg,
804
- solid,
805
- taskDiagnostics,
806
- hasAnyDiagnostic,
807
- taskRowContent.get(row.task.id),
808
- onClickLine,
809
- onMarkerDragStart,
810
- sLabelFont,
811
- sDescFont,
812
- sCellPad,
813
- sRowHeight,
814
- sColumnInset,
815
- sNodeRadius,
816
- sNodeStrokeWidth,
817
- sMarkerFont,
818
- sLegendLabelFont,
819
- sStackTopGap,
820
- sViolationLineHeight
821
- );
822
- }
823
- }
824
-
825
- // ── Empty-state hint when all cells are blank ──────────────
826
- const anyMarker = tasksAll.some((t) =>
827
- t.roleAssignments.some((a) => a.markers.length > 0)
828
- );
829
- if (!anyMarker) {
830
- svg
831
- .append('text')
832
- .attr('x', width / 2)
833
- .attr('y', totalHeight - sVMargin - 2)
834
- .attr('text-anchor', 'middle')
835
- .attr('font-size', 12)
836
- .attr('fill', palette.textMuted)
837
- .text(
838
- 'Drag a marker from the legend onto any cell. Drag a marker out to remove.'
839
- );
840
- }
841
-
842
- // ── Column hover wiring ──────────────────────────────────────
843
- //
844
- // Toggle `data-hover-col` on the SVG and tag the matching column +
845
- // its cells with `raci-col-active` whenever the pointer is over any
846
- // element with a `data-role-id` ancestor. The app's sync CSS uses
847
- // those hooks to dim everything else, so a single role's row of
848
- // cells pops on hover. Pure-CSS doesn't reach because cells are not
849
- // descendants of the column group.
850
- setupColumnHover(svg);
851
- }
852
-
853
- function setupColumnHover(
854
- svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>
855
- ): void {
856
- const node = svg.node();
857
- if (!node) return;
858
- let current: string | null = null;
859
-
860
- const findRole = (target: EventTarget | null): string | null => {
861
- let el = target as Element | null;
862
- while (el && el !== node) {
863
- const r = el.getAttribute && el.getAttribute('data-role-id');
864
- if (r) return r;
865
- el = el.parentElement;
866
- }
867
- return null;
868
- };
869
-
870
- const set = (role: string | null): void => {
871
- if (role === current) return;
872
- if (current) {
873
- svg
874
- .selectAll(
875
- `.raci-cell[data-role-id="${current}"], .raci-column[data-role-id="${current}"]`
876
- )
877
- .classed('raci-col-active', false);
878
- }
879
- current = role;
880
- if (role) {
881
- svg.attr('data-hover-col', role);
882
- svg
883
- .selectAll(
884
- `.raci-cell[data-role-id="${role}"], .raci-column[data-role-id="${role}"]`
885
- )
886
- .classed('raci-col-active', true);
887
- } else {
888
- svg.attr('data-hover-col', null);
889
- }
890
- };
891
-
892
- svg.on('mouseover', (event: MouseEvent) => set(findRole(event.target)));
893
- svg.on('mouseleave', () => set(null));
894
- }
895
-
896
- // ── Legend / palette ────────────────────────────────────────
897
-
898
- function renderLegend(
899
- svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
900
- markers: ReadonlyArray<RaciMarker>,
901
- variant: RaciVariant,
902
- x: number,
903
- y: number,
904
- mode: 'full' | 'letters',
905
- chipW: number,
906
- palette: PaletteColors,
907
- surfaceBg: string,
908
- solid: boolean,
909
- _onMarkerDragStart:
910
- | ((source: RaciDragSource, e: PointerEvent) => void)
911
- | undefined,
912
- sLegendChipGap: number,
913
- sLegendHeight: number,
914
- sNodeRadius: number,
915
- sNodeStrokeWidth: number,
916
- sLegendLetterFont: number,
917
- sLegendLabelFont: number
918
- ): void {
919
- if (markers.length === 0) return;
920
- const labels = MARKER_LABELS[variant];
921
-
922
- const legendG = svg
923
- .append('g')
924
- .attr('class', 'raci-legend')
925
- .attr('aria-label', `${variant.toUpperCase()} marker legend`);
926
-
927
- markers.forEach((marker, i) => {
928
- const cx = x + i * (chipW + sLegendChipGap);
929
- const rawColor = markerColor(marker, palette);
930
- const labelText = labels[marker]!;
931
-
932
- const chipG = legendG
933
- .append('g')
934
- .attr('class', 'raci-legend-chip')
935
- .attr('data-marker', marker)
936
- .style('cursor', 'grab');
937
-
938
- if (mode === 'letters') {
939
- // Compact: a single colored pill with just the marker letter.
940
- // The full label moves to a native tooltip so hover still teaches it.
941
- const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
942
- const stroke = solid ? mix(rawColor, surfaceBg, 70) : rawColor;
943
- chipG
944
- .append('rect')
945
- .attr('x', cx)
946
- .attr('y', y)
947
- .attr('width', chipW)
948
- .attr('height', sLegendHeight)
949
- .attr('rx', sNodeRadius)
950
- .attr('fill', fill)
951
- .attr('stroke', stroke)
952
- .attr('stroke-width', sNodeStrokeWidth);
953
- chipG
954
- .append('text')
955
- .attr('x', cx + chipW / 2)
956
- .attr('y', y + sLegendHeight / 2)
957
- .attr('text-anchor', 'middle')
958
- .attr('dominant-baseline', 'central')
959
- .attr('font-size', sLegendLetterFont)
960
- .attr('font-weight', 700)
961
- .attr(
962
- 'fill',
963
- solid
964
- ? contrastText(
965
- fill,
966
- palette.textOnFillLight,
967
- palette.textOnFillDark
968
- )
969
- : palette.text
970
- )
971
- .text(marker);
972
- return;
973
- }
974
-
975
- // Full mode: bordered chip with a letter slab on the left and label text.
976
- const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
977
- const stroke = mix(rawColor, surfaceBg, 70);
978
-
979
- chipG
980
- .append('rect')
981
- .attr('x', cx)
982
- .attr('y', y)
983
- .attr('width', chipW)
984
- .attr('height', sLegendHeight)
985
- .attr('rx', sNodeRadius)
986
- .attr('fill', fill)
987
- .attr('stroke', stroke)
988
- .attr('stroke-width', sNodeStrokeWidth);
989
-
990
- // Letter slab on the left — tinted (not solid) so it sits in
991
- // the same visual register as the diagram's marker cells.
992
- const slabPad = 4;
993
- const slabW = 24;
994
- const slabFill = solid
995
- ? mix(rawColor, surfaceBg, 70)
996
- : mix(rawColor, surfaceBg, 55);
997
- chipG
998
- .append('rect')
999
- .attr('x', cx + slabPad)
1000
- .attr('y', y + slabPad)
1001
- .attr('width', slabW)
1002
- .attr('height', sLegendHeight - 2 * slabPad)
1003
- .attr('rx', 4)
1004
- .attr('fill', slabFill);
1005
-
1006
- chipG
1007
- .append('text')
1008
- .attr('x', cx + slabPad + slabW / 2)
1009
- .attr('y', y + sLegendHeight / 2)
1010
- .attr('text-anchor', 'middle')
1011
- .attr('dominant-baseline', 'central')
1012
- .attr('font-size', sLegendLetterFont)
1013
- .attr('font-weight', 700)
1014
- .attr(
1015
- 'fill',
1016
- solid
1017
- ? contrastText(
1018
- slabFill,
1019
- palette.textOnFillLight,
1020
- palette.textOnFillDark
1021
- )
1022
- : palette.text
1023
- )
1024
- .text(marker);
1025
-
1026
- // Full label to the right of the slab
1027
- chipG
1028
- .append('text')
1029
- .attr('x', cx + slabPad + slabW + 8)
1030
- .attr('y', y + sLegendHeight / 2)
1031
- .attr('dominant-baseline', 'central')
1032
- .attr('font-size', sLegendLabelFont)
1033
- .attr('font-weight', 600)
1034
- .attr('fill', palette.text)
1035
- .text(
1036
- truncateText(
1037
- labelText,
1038
- sLegendLabelFont,
1039
- chipW - slabW - slabPad * 2 - 12
1040
- )
1041
- );
1042
- });
1043
- }
1044
-
1045
- // Drop-in export wrapper used by `d3.ts` renderForExport dispatch.
1046
- export function renderRaciForExport(
1047
- container: HTMLDivElement,
1048
- parsed: ParsedRaci,
1049
- palette: PaletteColors,
1050
- isDark: boolean,
1051
- exportDims?: D3ExportDimensions
1052
- ): void {
1053
- renderRaci(container, parsed, palette, isDark, {}, exportDims);
1054
- }
1055
-
1056
- // ── Phase bar ────────────────────────────────────────────────
1057
-
1058
- function renderPhaseBar(
1059
- svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
1060
- phase: RaciPhase,
1061
- x: number,
1062
- y: number,
1063
- width: number,
1064
- palette: PaletteColors,
1065
- collapsed: boolean,
1066
- autoColor: string,
1067
- // Role columns + variant. Supplied so a collapsed phase bar can paint
1068
- // a per-column marker-union summary in each role column's slot.
1069
- roles: ReadonlyArray<string>,
1070
- roleX: (i: number) => number,
1071
- roleColW: number,
1072
- variant: RaciVariant,
1073
- surfaceBg: string,
1074
- solid: boolean,
1075
- showSummary: boolean,
1076
- sPhaseHeight: number,
1077
- sPhaseFont: number,
1078
- sColumnInset: number,
1079
- sNodeRadius: number
1080
- ): void {
1081
- const phaseG = svg
1082
- .append('g')
1083
- .attr('class', 'raci-phase')
1084
- .attr('data-section', `phase-${phase.id}`)
1085
- .attr('data-section-toggle', '')
1086
- .attr('data-line-number', String(phase.lineNumber))
1087
- .attr('data-phase-id', phase.id)
1088
- .attr('data-collapsed', collapsed ? '1' : '0')
1089
- .style('cursor', 'pointer');
1090
-
1091
- // Phase color rules: explicit `[Voyage] | color: blue` wins; absent
1092
- // that, fall back to the marker-safe auto-accent so each phase bar
1093
- // has its own identity.
1094
- const phaseFill = mix(phase.color ?? autoColor, palette.bg, 30);
1095
- phaseG
1096
- .append('rect')
1097
- .attr('x', x)
1098
- .attr('y', y + 4)
1099
- .attr('width', width)
1100
- .attr('height', sPhaseHeight - 8)
1101
- .attr('fill', phaseFill)
1102
- .attr('rx', 4);
1103
-
1104
- const chevX = x + 12;
1105
- const chevY = y + sPhaseHeight / 2;
1106
- phaseG
1107
- .append('text')
1108
- .attr('x', chevX)
1109
- .attr('y', chevY)
1110
- .attr('text-anchor', 'middle')
1111
- .attr('dominant-baseline', 'central')
1112
- .attr('font-family', FONT_FAMILY)
1113
- .attr('font-size', 9)
1114
- .attr('fill', palette.text)
1115
- .attr('opacity', 0.6)
1116
- .text(collapsed ? '▶' : '▼');
1117
-
1118
- phaseG
1119
- .append('text')
1120
- .attr('x', x + 26)
1121
- .attr('y', y + (sPhaseHeight - 4) / 2 + 2)
1122
- .attr('dominant-baseline', 'central')
1123
- .attr('font-size', sPhaseFont)
1124
- .attr('font-weight', 600)
1125
- .attr('fill', palette.text)
1126
- .text(phase.displayName);
1127
-
1128
- if (collapsed) {
1129
- // Show the hidden-task count to the right of the label so users
1130
- // can see at a glance what's in the rolled-up phase.
1131
- const taskCount = phase.tasks.length;
1132
- if (taskCount > 0) {
1133
- const labelTextWidth = measureText(phase.displayName, sPhaseFont);
1134
- phaseG
1135
- .append('text')
1136
- .attr('x', x + 26 + labelTextWidth + 10)
1137
- .attr('y', y + (sPhaseHeight - 4) / 2 + 2)
1138
- .attr('dominant-baseline', 'central')
1139
- .attr('font-size', sPhaseFont - 1)
1140
- .attr('font-weight', 500)
1141
- .attr('fill', palette.textMuted)
1142
- .text(`${taskCount} ${taskCount === 1 ? 'task' : 'tasks'}`);
1143
- }
1144
-
1145
- // Chart-wide gate: if any collapsed phase on this chart would have
1146
- // a column too narrow to render readable chips, suppress on every
1147
- // collapsed bar — coverage stays consistent.
1148
- if (!showSummary) return;
1149
-
1150
- // Per-column marker-union summary. For each role column, walk every
1151
- // task in this phase, union the markers used in that column, and
1152
- // paint chips that match the regular cell geometry — same column
1153
- // inset, same SLICE_GAP, so chips fill the column body and read
1154
- // as a compressed cell rather than a separate primitive.
1155
- const alphabet = VARIANTS[variant].alphabet;
1156
- const summaryInset = sColumnInset + 4;
1157
- const summaryColBodyW = roleColW - 2 * summaryInset;
1158
- const SUMMARY_CHIP_H = 24;
1159
- const SUMMARY_LETTER_FONT = 13;
1160
- const summaryY = y + (sPhaseHeight - SUMMARY_CHIP_H) / 2;
1161
-
1162
- roles.forEach((roleId, colIdx) => {
1163
- const used = new Set<RaciMarker>();
1164
- for (const t of phase.tasks) {
1165
- const a = t.roleAssignments.find((r) => r.id === roleId);
1166
- if (!a) continue;
1167
- for (const m of a.markers) used.add(m);
1168
- }
1169
- if (used.size === 0) return;
1170
- const ordered = sortByAlphabet([...used], alphabet);
1171
- const totalGap = SLICE_GAP * Math.max(0, ordered.length - 1);
1172
- const sliceW = (summaryColBodyW - totalGap) / ordered.length;
1173
- const startX = roleX(colIdx) + summaryInset;
1174
-
1175
- ordered.forEach((marker, i) => {
1176
- const cx = startX + i * (sliceW + SLICE_GAP);
1177
- const rawColor = markerColor(marker, palette);
1178
- // Match the cell-marker / legend-chip fill+stroke colors and
1179
- // corner radius. Stroke width is a touch thinner than
1180
- // NODE_STROKE_WIDTH because at the smaller summary scale the
1181
- // full 1.5 reads as too heavy.
1182
- const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
1183
- const stroke = solid ? mix(rawColor, surfaceBg, 70) : rawColor;
1184
- const chipG = phaseG.append('g').attr('class', 'raci-phase-summary');
1185
- chipG
1186
- .append('rect')
1187
- .attr('x', cx)
1188
- .attr('y', summaryY)
1189
- .attr('width', sliceW)
1190
- .attr('height', SUMMARY_CHIP_H)
1191
- .attr('rx', sNodeRadius)
1192
- .attr('fill', fill)
1193
- .attr('stroke', stroke)
1194
- .attr('stroke-width', 1.25);
1195
- chipG
1196
- .append('text')
1197
- .attr('x', cx + sliceW / 2)
1198
- .attr('y', summaryY + SUMMARY_CHIP_H / 2)
1199
- .attr('text-anchor', 'middle')
1200
- .attr('dominant-baseline', 'central')
1201
- .attr('font-size', SUMMARY_LETTER_FONT)
1202
- .attr('font-weight', 700)
1203
- .attr(
1204
- 'fill',
1205
- solid
1206
- ? contrastText(
1207
- fill,
1208
- palette.textOnFillLight,
1209
- palette.textOnFillDark
1210
- )
1211
- : palette.text
1212
- )
1213
- .text(marker);
1214
- });
1215
- });
1216
- }
1217
- }
1218
-
1219
- // ── Task row ─────────────────────────────────────────────────
1220
-
1221
- /** Gap (in px) between adjacent marker slices in a multi-marker cell. */
1222
- const SLICE_GAP = 4;
1223
-
1224
- interface TaskDiagnosticBucket {
1225
- errors: DiagnosticEntry[];
1226
- warnings: DiagnosticEntry[];
1227
- firstLine: number;
1228
- }
1229
-
1230
- function renderTaskRow(
1231
- svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
1232
- task: RaciTask,
1233
- parsed: ParsedRaci,
1234
- x: number,
1235
- y: number,
1236
- labelW: number,
1237
- roleX: (i: number) => number,
1238
- roleColW: number,
1239
- palette: PaletteColors,
1240
- surfaceBg: string,
1241
- solid: boolean,
1242
- taskDiagnostics: Map<string, TaskDiagnosticBucket> | null,
1243
- _hasAnyDiagnostic: boolean,
1244
- rowContent: RowContent | undefined,
1245
- onClickLine: ((lineNumber: number) => void) | undefined,
1246
- _onMarkerDragStart:
1247
- | ((source: RaciDragSource, e: PointerEvent) => void)
1248
- | undefined,
1249
- sLabelFont: number,
1250
- sDescFont: number,
1251
- sCellPad: number,
1252
- sRowHeight: number,
1253
- sColumnInset: number,
1254
- sNodeRadius: number,
1255
- sNodeStrokeWidth: number,
1256
- sMarkerFont: number,
1257
- sLegendLabelFont: number,
1258
- sStackTopGap: number,
1259
- sViolationLineHeight: number
1260
- ): void {
1261
- const rowG = svg
1262
- .append('g')
1263
- .attr('class', 'raci-task-row')
1264
- .attr('data-task-id', task.id)
1265
- .attr('data-line-number', String(task.lineNumber));
1266
-
1267
- // No gutter chip — violations now stack on their own lines below the
1268
- // task label, each colored by severity (red = error, amber = warning).
1269
- const labelX = x + 8;
1270
- const labelMaxW = labelW - 16;
1271
- const content =
1272
- rowContent ??
1273
- prepareRowContent(
1274
- task,
1275
- taskDiagnostics?.get(task.id),
1276
- labelMaxW,
1277
- sLabelFont,
1278
- sDescFont,
1279
- sRowHeight,
1280
- sStackTopGap,
1281
- sViolationLineHeight
1282
- );
1283
- const { nameLines, descLines, violations, rowHeight } = content;
1284
-
1285
- // Group everything that lives in the label column under a single
1286
- // wrapper so the sync adapter's hover-dim CSS can target the wrapper
1287
- // and fire whether the user is over the rect, the name text, the
1288
- // description, or a violation. Cells stay siblings of this group on
1289
- // the rowG so they DON'T trigger the row-hover dim.
1290
- const labelG = rowG.append('g').attr('class', 'raci-task-label-area');
1291
-
1292
- // Transparent hit-zone over the task-label column, sized to match
1293
- // the wrapped row height so any pointer drift inside the label
1294
- // column is captured by the wrapper :hover.
1295
- labelG
1296
- .append('rect')
1297
- .attr('class', 'raci-task-label-zone')
1298
- .attr('x', x)
1299
- .attr('y', y)
1300
- .attr('width', labelW)
1301
- .attr('height', rowHeight)
1302
- .attr('fill', 'transparent')
1303
- .attr('pointer-events', 'all');
1304
-
1305
- // Task name — wrapped to as many lines as fit without truncating.
1306
- const nameTopY = y + sLabelFont / 2 + 6;
1307
- const nameEl = labelG
1308
- .append('text')
1309
- .attr('class', 'raci-task-label')
1310
- .attr('x', labelX)
1311
- .attr('y', nameTopY)
1312
- .attr('dominant-baseline', 'central')
1313
- .attr('font-size', sLabelFont)
1314
- .attr('fill', palette.text);
1315
- nameLines.forEach((line, i) => {
1316
- const tspan = nameEl.append('tspan').attr('x', labelX).text(line);
1317
- if (i > 0) tspan.attr('dy', NAME_LINE_HEIGHT);
1318
- });
1319
-
1320
- let stackY =
1321
- nameTopY +
1322
- Math.max(0, nameLines.length - 1) * NAME_LINE_HEIGHT +
1323
- sLabelFont / 2 +
1324
- 4 +
1325
- sStackTopGap;
1326
-
1327
- if (descLines.length > 0) {
1328
- const descEl = labelG
1329
- .append('text')
1330
- .attr('class', 'raci-task-description')
1331
- .attr('x', labelX)
1332
- .attr('y', stackY)
1333
- .attr('dominant-baseline', 'central')
1334
- .attr('font-size', sDescFont)
1335
- .attr('fill', palette.textMuted);
1336
- descLines.forEach((line, i) => {
1337
- const tspan = descEl.append('tspan').attr('x', labelX).text(line);
1338
- if (i > 0) tspan.attr('dy', DESC_LINE_HEIGHT);
1339
- });
1340
- stackY += descLines.length * DESC_LINE_HEIGHT;
1341
- }
1342
-
1343
- for (const v of violations) {
1344
- const lineColor =
1345
- v.severity === 'error' ? palette.colors.red : palette.colors.orange;
1346
- const lineG = labelG
1347
- .append('g')
1348
- .attr('class', 'raci-violation-line')
1349
- .attr('data-severity', v.severity)
1350
- .style('cursor', onClickLine ? 'pointer' : 'default');
1351
-
1352
- const violationFont = sLabelFont - 2;
1353
- const textEl = lineG
1354
- .append('text')
1355
- .attr('x', labelX)
1356
- .attr('y', stackY)
1357
- .attr('dominant-baseline', 'central')
1358
- .attr('font-size', violationFont)
1359
- .attr('fill', lineColor);
1360
- v.lines.forEach((line, i) => {
1361
- // Render bold for any quoted spans within this wrapped line.
1362
- const segs = parseQuotedSegments(line);
1363
- segs.forEach((seg, segIdx) => {
1364
- const tspan = textEl.append('tspan').text(seg.text);
1365
- if (i > 0 && segIdx === 0)
1366
- tspan.attr('x', labelX).attr('dy', sViolationLineHeight);
1367
- if (seg.bold) tspan.attr('font-weight', 700);
1368
- });
1369
- });
1370
-
1371
- if (onClickLine) {
1372
- const targetLine = v.sourceLine;
1373
- lineG.on('click', (e: Event) => {
1374
- e.stopPropagation();
1375
- onClickLine(targetLine);
1376
- });
1377
- }
1378
- stackY += v.lines.length * sViolationLineHeight;
1379
- }
1380
-
1381
- // Build a quick lookup: roleId → markers[]
1382
- const cellByRole = new Map<string, readonly RaciMarker[]>();
1383
- for (const a of task.roleAssignments) {
1384
- cellByRole.set(a.id, a.markers);
1385
- }
1386
-
1387
- const alphabet = VARIANTS[parsed.variant].alphabet;
1388
-
1389
- // For each role column, paint a cell
1390
- parsed.roles.forEach((roleId, idx) => {
1391
- // Cells sit inside the column body. The column body uses COLUMN_INSET
1392
- // on each side; add 4 px more so the cell doesn't kiss the column edge.
1393
- const cellInset = sColumnInset + 4;
1394
- const cx = roleX(idx) + cellInset;
1395
- const cy = y + sCellPad;
1396
- const cw = roleColW - 2 * cellInset;
1397
- const ch = sRowHeight - 2 * sCellPad;
1398
- const markersSource = cellByRole.get(roleId) ?? [];
1399
- // Display markers in canonical alphabet order — RACI cells always
1400
- // read R A C I left-to-right regardless of source order. Source
1401
- // ordering still drives the round-trip (mutations preserve it).
1402
- const markers = sortByAlphabet(markersSource, alphabet);
1403
-
1404
- const assignment = task.roleAssignments.find((a) => a.id === roleId);
1405
- const cellG = rowG
1406
- .append('g')
1407
- .attr('class', 'raci-cell')
1408
- .attr('data-task-id', task.id)
1409
- .attr('data-role-id', roleId);
1410
- // Cells with an explicit role assignment carry the assignment's line
1411
- // number so click → cursor jumps to that exact source line. Empty
1412
- // cells leave the attribute off; walk-up resolution falls back to
1413
- // the parent task row.
1414
- if (assignment) {
1415
- cellG.attr('data-line-number', String(assignment.lineNumber));
1416
- }
1417
-
1418
- // Full-bounds transparent hit layer covering the column-body area
1419
- // (NOT the inter-column gap). Without this, empty cells only hit-test
1420
- // on the marker stroke — drag-and-drop misses the cell body.
1421
- cellG
1422
- .append('rect')
1423
- .attr('class', 'raci-cell-hit')
1424
- .attr('x', roleX(idx) + sColumnInset)
1425
- .attr('y', y)
1426
- .attr('width', roleColW - 2 * sColumnInset)
1427
- .attr('height', sRowHeight)
1428
- .attr('fill', 'transparent')
1429
- .attr('pointer-events', 'all');
1430
-
1431
- // Build a horizontally-segmented fill: each marker gets its own
1432
- // tinted slice in canonical order with a small gap between slices.
1433
- // Each slice is its own draggable handle (`<g>` with data-marker).
1434
- //
1435
- // Border styling matches the codebase's node convention
1436
- // (flowchart NODE_STROKE_WIDTH = 1.5, kanban CARD_STROKE_WIDTH = 1.5,
1437
- // c4 NODE_STROKE_WIDTH = 1.5; corner radius 6 = kanban CARD_RADIUS).
1438
- const totalGap = SLICE_GAP * (markers.length - 1);
1439
- const sliceW = markers.length > 0 ? (cw - totalGap) / markers.length : 0;
1440
- const variantLabels = MARKER_LABELS[parsed.variant];
1441
- markers.forEach((m, i) => {
1442
- const rawColor = markerColor(m, palette);
1443
- const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
1444
- const stroke = rawColor;
1445
- const sliceX = cx + i * (sliceW + SLICE_GAP);
1446
-
1447
- const sliceG = cellG
1448
- .append('g')
1449
- .attr('class', 'raci-marker-slice')
1450
- .attr('data-marker', m)
1451
- .style('cursor', 'grab');
1452
-
1453
- sliceG
1454
- .append('rect')
1455
- .attr('x', sliceX)
1456
- .attr('y', cy)
1457
- .attr('width', sliceW)
1458
- .attr('height', ch)
1459
- .attr('rx', sNodeRadius)
1460
- .attr('fill', fill)
1461
- .attr('stroke', stroke)
1462
- .attr('stroke-width', sNodeStrokeWidth);
1463
-
1464
- // When the slice is wide enough, spell out the full marker label
1465
- // ("Responsible") instead of the bare letter. Same primitive as the
1466
- // legend chip, so cells and legend read as the same UI element.
1467
- const fullLabel = variantLabels[m] ?? m;
1468
- const labelPx = measureText(fullLabel, sLegendLabelFont);
1469
- const showFullLabel = labelPx + 16 <= sliceW;
1470
- const textContent = showFullLabel ? fullLabel : m;
1471
- const textFont = showFullLabel ? sLegendLabelFont : sMarkerFont;
1472
-
1473
- sliceG
1474
- .append('text')
1475
- .attr('x', sliceX + sliceW / 2)
1476
- .attr('y', cy + ch / 2)
1477
- .attr('text-anchor', 'middle')
1478
- .attr('dominant-baseline', 'central')
1479
- .attr('font-size', textFont)
1480
- .attr('font-weight', MARKER_FONT_WEIGHT)
1481
- .attr(
1482
- 'fill',
1483
- solid
1484
- ? contrastText(
1485
- fill,
1486
- palette.textOnFillLight,
1487
- palette.textOnFillDark
1488
- )
1489
- : palette.text
1490
- )
1491
- .text(textContent);
1492
- });
1493
-
1494
- // Drop-preview rect: sized + rounded to match where a dropped marker
1495
- // would land. Appended LAST so it paints on top of any marker slices —
1496
- // otherwise the slice rects in a filled cell fully cover the highlight
1497
- // and the user gets no visual feedback when dragging onto a cell that
1498
- // already has assignments. `pointer-events: none` keeps it clear of
1499
- // hit-testing. Hidden by default; CSS paints it when the cell carries
1500
- // [data-drop-active="1"].
1501
- cellG
1502
- .append('rect')
1503
- .attr('class', 'raci-cell-drop-preview')
1504
- .attr('x', cx)
1505
- .attr('y', cy)
1506
- .attr('width', cw)
1507
- .attr('height', ch)
1508
- .attr('rx', sNodeRadius)
1509
- .attr('fill', 'transparent')
1510
- .attr('stroke', 'transparent')
1511
- .attr('pointer-events', 'none');
1512
-
1513
- if (onClickLine && assignment) {
1514
- cellG.on('click', () => onClickLine(assignment.lineNumber));
1515
- }
1516
- });
1517
- }
1518
-
1519
- // ── Helpers ──────────────────────────────────────────────────
1520
-
1521
- /**
1522
- * Wrapped text content for a task row plus the height that fits it.
1523
- * Pre-computed once per task and reused by both the y-cursor layout
1524
- * pass (which needs the height) and the renderer (which paints the
1525
- * wrapped lines), so the two stay in sync without duplicating logic.
1526
- */
1527
- interface RowContent {
1528
- nameLines: string[];
1529
- descLines: string[];
1530
- violations: Array<{
1531
- severity: 'error' | 'warning';
1532
- sourceLine: number;
1533
- text: string;
1534
- lines: string[];
1535
- }>;
1536
- rowHeight: number;
1537
- }
1538
-
1539
- const NAME_LINE_HEIGHT = 16;
1540
- const DESC_LINE_HEIGHT = 16;
1541
-
1542
- function prepareRowContent(
1543
- task: RaciTask,
1544
- bucket: TaskDiagnosticBucket | undefined,
1545
- labelMaxW: number,
1546
- sLabelFont = LABEL_FONT,
1547
- sDescFont = DESC_FONT,
1548
- sRowHeight = ROW_HEIGHT,
1549
- sStackTopGap = STACK_TOP_GAP,
1550
- sViolationLineHeight = VIOLATION_LINE_HEIGHT
1551
- ): RowContent {
1552
- const nameLines = wrapTextToWidth(task.displayName, sLabelFont, labelMaxW, {
1553
- hardBreak: true,
1554
- });
1555
- const description = task.description?.trim() ?? '';
1556
- const descLines =
1557
- description.length > 0
1558
- ? description
1559
- .split('\n')
1560
- .flatMap((line) =>
1561
- wrapTextToWidth(line, sDescFont, labelMaxW, { hardBreak: true })
1562
- )
1563
- : [];
1564
- const violations: RowContent['violations'] = [];
1565
- if (bucket) {
1566
- const collect = (
1567
- arr: DiagnosticEntry[],
1568
- severity: 'error' | 'warning'
1569
- ): void => {
1570
- for (const e of arr) {
1571
- const text = trimTaskPrefix(e.message);
1572
- violations.push({
1573
- severity,
1574
- sourceLine: e.line,
1575
- text,
1576
- lines: wrapTextToWidth(text, sLabelFont - 2, labelMaxW, {
1577
- hardBreak: true,
1578
- }),
1579
- });
1580
- }
1581
- };
1582
- collect(bucket.errors, 'error');
1583
- collect(bucket.warnings, 'warning');
1584
- }
1585
-
1586
- const totalViolationLines = violations.reduce(
1587
- (n, v) => n + v.lines.length,
1588
- 0
1589
- );
1590
- const hasStacked = descLines.length > 0 || violations.length > 0;
1591
- // ROW_HEIGHT was sized for a one-line label; extra label lines push
1592
- // every stacked block down by NAME_LINE_HEIGHT.
1593
- const labelExtra = Math.max(0, nameLines.length - 1) * NAME_LINE_HEIGHT;
1594
- const rowHeight =
1595
- sRowHeight +
1596
- labelExtra +
1597
- (hasStacked ? sStackTopGap : 0) +
1598
- descLines.length * DESC_LINE_HEIGHT +
1599
- totalViolationLines * sViolationLineHeight;
1600
-
1601
- return { nameLines, descLines, violations, rowHeight };
1602
- }
1603
-
1604
- /**
1605
- * Split a diagnostic message at single-quoted spans so the renderer can
1606
- * paint the quoted name (role / task) as a bold tspan with no quotes.
1607
- * Plain-text contexts (tooltip, bottom panel) keep the quotes — they're
1608
- * the only emphasis available there.
1609
- */
1610
- type Segment = { text: string; bold: boolean };
1611
-
1612
- function parseQuotedSegments(message: string): Segment[] {
1613
- const out: Segment[] = [];
1614
- const re = /'([^']+)'/g;
1615
- let last = 0;
1616
- let m: RegExpExecArray | null;
1617
- while ((m = re.exec(message)) !== null) {
1618
- if (m.index > last)
1619
- out.push({ text: message.slice(last, m.index), bold: false });
1620
- // Capture group [1] guaranteed by the regex pattern.
1621
- out.push({ text: m[1]!, bold: true });
1622
- last = m.index + m[0].length;
1623
- }
1624
- if (last < message.length)
1625
- out.push({ text: message.slice(last), bold: false });
1626
- return out;
1627
- }