@elaraai/east-ui 1.0.80 → 1.0.82

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 (353) hide show
  1. package/dist/src/charts/chart/index.d.ts +4 -0
  2. package/dist/src/charts/chart/index.d.ts.map +1 -1
  3. package/dist/src/charts/spec/index.d.ts +8 -0
  4. package/dist/src/charts/spec/index.d.ts.map +1 -1
  5. package/dist/src/collections/deck/index.d.ts +8 -0
  6. package/dist/src/collections/deck/index.d.ts.map +1 -1
  7. package/dist/src/collections/index.d.ts +2 -2
  8. package/dist/src/collections/index.d.ts.map +1 -1
  9. package/dist/src/collections/index.js.map +1 -1
  10. package/dist/src/collections/library/index.d.ts +4 -0
  11. package/dist/src/collections/library/index.d.ts.map +1 -1
  12. package/dist/src/collections/library/types.d.ts +4 -0
  13. package/dist/src/collections/library/types.d.ts.map +1 -1
  14. package/dist/src/collections/matrix/index.d.ts +56 -19
  15. package/dist/src/collections/matrix/index.d.ts.map +1 -1
  16. package/dist/src/collections/matrix/index.js +38 -24
  17. package/dist/src/collections/matrix/index.js.map +1 -1
  18. package/dist/src/collections/plan/assemble.d.ts +655 -2113
  19. package/dist/src/collections/plan/assemble.d.ts.map +1 -1
  20. package/dist/src/collections/plan/assemble.js +159 -136
  21. package/dist/src/collections/plan/assemble.js.map +1 -1
  22. package/dist/src/collections/plan/builders.d.ts +193 -40
  23. package/dist/src/collections/plan/builders.d.ts.map +1 -1
  24. package/dist/src/collections/plan/builders.js +162 -32
  25. package/dist/src/collections/plan/builders.js.map +1 -1
  26. package/dist/src/collections/plan/factories.d.ts +290 -116
  27. package/dist/src/collections/plan/factories.d.ts.map +1 -1
  28. package/dist/src/collections/plan/factories.js +281 -156
  29. package/dist/src/collections/plan/factories.js.map +1 -1
  30. package/dist/src/collections/plan/index.d.ts +111 -58
  31. package/dist/src/collections/plan/index.d.ts.map +1 -1
  32. package/dist/src/collections/plan/index.js +62 -31
  33. package/dist/src/collections/plan/index.js.map +1 -1
  34. package/dist/src/collections/plan/ir.d.ts +759 -1769
  35. package/dist/src/collections/plan/ir.d.ts.map +1 -1
  36. package/dist/src/collections/plan/ir.js +51 -27
  37. package/dist/src/collections/plan/ir.js.map +1 -1
  38. package/dist/src/collections/plan/pick.d.ts +89 -71
  39. package/dist/src/collections/plan/pick.d.ts.map +1 -1
  40. package/dist/src/collections/plan/pick.js +101 -86
  41. package/dist/src/collections/plan/pick.js.map +1 -1
  42. package/dist/src/collections/plan/root.d.ts +164 -77
  43. package/dist/src/collections/plan/root.d.ts.map +1 -1
  44. package/dist/src/collections/plan/root.js +350 -42
  45. package/dist/src/collections/plan/root.js.map +1 -1
  46. package/dist/src/collections/plan/series.d.ts +12524 -10527
  47. package/dist/src/collections/plan/series.d.ts.map +1 -1
  48. package/dist/src/collections/plan/series.js +1596 -298
  49. package/dist/src/collections/plan/series.js.map +1 -1
  50. package/dist/src/collections/plan/types.d.ts +6115 -2802
  51. package/dist/src/collections/plan/types.d.ts.map +1 -1
  52. package/dist/src/collections/plan/types.js +575 -170
  53. package/dist/src/collections/plan/types.js.map +1 -1
  54. package/dist/src/collections/sheet/bridge.d.ts +154 -43
  55. package/dist/src/collections/sheet/bridge.d.ts.map +1 -1
  56. package/dist/src/collections/sheet/bridge.js +383 -240
  57. package/dist/src/collections/sheet/bridge.js.map +1 -1
  58. package/dist/src/collections/sheet/columns.d.ts +66 -16
  59. package/dist/src/collections/sheet/columns.d.ts.map +1 -1
  60. package/dist/src/collections/sheet/columns.js +25 -25
  61. package/dist/src/collections/sheet/columns.js.map +1 -1
  62. package/dist/src/collections/sheet/context-bridge.d.ts +48 -0
  63. package/dist/src/collections/sheet/context-bridge.d.ts.map +1 -0
  64. package/dist/src/collections/sheet/context-bridge.js +216 -0
  65. package/dist/src/collections/sheet/context-bridge.js.map +1 -0
  66. package/dist/src/collections/sheet/draft-bridge.d.ts +37 -0
  67. package/dist/src/collections/sheet/draft-bridge.d.ts.map +1 -0
  68. package/dist/src/collections/sheet/draft-bridge.js +153 -0
  69. package/dist/src/collections/sheet/draft-bridge.js.map +1 -0
  70. package/dist/src/collections/sheet/drafts.d.ts +14 -0
  71. package/dist/src/collections/sheet/drafts.d.ts.map +1 -0
  72. package/dist/src/collections/sheet/drafts.js +14 -0
  73. package/dist/src/collections/sheet/drafts.js.map +1 -0
  74. package/dist/src/collections/sheet/editing-bridge.d.ts +36 -0
  75. package/dist/src/collections/sheet/editing-bridge.d.ts.map +1 -0
  76. package/dist/src/collections/sheet/editing-bridge.js +196 -0
  77. package/dist/src/collections/sheet/editing-bridge.js.map +1 -0
  78. package/dist/src/collections/sheet/editing-types.d.ts +268 -0
  79. package/dist/src/collections/sheet/editing-types.d.ts.map +1 -0
  80. package/dist/src/collections/sheet/editing-types.js +34 -0
  81. package/dist/src/collections/sheet/editing-types.js.map +1 -0
  82. package/dist/src/collections/sheet/edits.d.ts +37 -0
  83. package/dist/src/collections/sheet/edits.d.ts.map +1 -0
  84. package/dist/src/collections/sheet/edits.js +31 -0
  85. package/dist/src/collections/sheet/edits.js.map +1 -0
  86. package/dist/src/collections/sheet/group.d.ts +165 -0
  87. package/dist/src/collections/sheet/group.d.ts.map +1 -0
  88. package/dist/src/collections/sheet/group.js +52 -0
  89. package/dist/src/collections/sheet/group.js.map +1 -0
  90. package/dist/src/collections/sheet/index.d.ts +171 -27
  91. package/dist/src/collections/sheet/index.d.ts.map +1 -1
  92. package/dist/src/collections/sheet/index.js +99 -17
  93. package/dist/src/collections/sheet/index.js.map +1 -1
  94. package/dist/src/collections/sheet/link.d.ts +22 -16
  95. package/dist/src/collections/sheet/link.d.ts.map +1 -1
  96. package/dist/src/collections/sheet/link.js +18 -12
  97. package/dist/src/collections/sheet/link.js.map +1 -1
  98. package/dist/src/collections/sheet/registers.d.ts +152 -5
  99. package/dist/src/collections/sheet/registers.d.ts.map +1 -1
  100. package/dist/src/collections/sheet/registers.js +154 -5
  101. package/dist/src/collections/sheet/registers.js.map +1 -1
  102. package/dist/src/collections/sheet/root.d.ts +175 -26
  103. package/dist/src/collections/sheet/root.d.ts.map +1 -1
  104. package/dist/src/collections/sheet/root.js +150 -26
  105. package/dist/src/collections/sheet/root.js.map +1 -1
  106. package/dist/src/collections/sheet/seed-bridge.d.ts +14 -0
  107. package/dist/src/collections/sheet/seed-bridge.d.ts.map +1 -0
  108. package/dist/src/collections/sheet/seed-bridge.js +51 -0
  109. package/dist/src/collections/sheet/seed-bridge.js.map +1 -0
  110. package/dist/src/collections/sheet/sub-rows.d.ts +249 -0
  111. package/dist/src/collections/sheet/sub-rows.d.ts.map +1 -0
  112. package/dist/src/collections/sheet/sub-rows.js +222 -0
  113. package/dist/src/collections/sheet/sub-rows.js.map +1 -0
  114. package/dist/src/collections/sheet/transactions.d.ts +76 -0
  115. package/dist/src/collections/sheet/transactions.d.ts.map +1 -0
  116. package/dist/src/collections/sheet/transactions.js +40 -0
  117. package/dist/src/collections/sheet/transactions.js.map +1 -0
  118. package/dist/src/collections/sheet/types.d.ts +12034 -3651
  119. package/dist/src/collections/sheet/types.d.ts.map +1 -1
  120. package/dist/src/collections/sheet/types.js +365 -48
  121. package/dist/src/collections/sheet/types.js.map +1 -1
  122. package/dist/src/collections/table/index.d.ts +98 -123
  123. package/dist/src/collections/table/index.d.ts.map +1 -1
  124. package/dist/src/collections/table/index.js +97 -111
  125. package/dist/src/collections/table/index.js.map +1 -1
  126. package/dist/src/collections/table/types.d.ts +87 -31
  127. package/dist/src/collections/table/types.d.ts.map +1 -1
  128. package/dist/src/collections/table/types.js +51 -26
  129. package/dist/src/collections/table/types.js.map +1 -1
  130. package/dist/src/collections/value-tree/flatten.d.ts.map +1 -1
  131. package/dist/src/collections/value-tree/flatten.js +4 -1
  132. package/dist/src/collections/value-tree/flatten.js.map +1 -1
  133. package/dist/src/component.d.ts +5075 -4079
  134. package/dist/src/component.d.ts.map +1 -1
  135. package/dist/src/component.js +46 -46
  136. package/dist/src/component.js.map +1 -1
  137. package/dist/src/contracts/approval.d.ts +10 -8
  138. package/dist/src/contracts/approval.d.ts.map +1 -1
  139. package/dist/src/contracts/approval.js +10 -8
  140. package/dist/src/contracts/approval.js.map +1 -1
  141. package/dist/src/contracts/drag.d.ts +11 -9
  142. package/dist/src/contracts/drag.d.ts.map +1 -1
  143. package/dist/src/contracts/drag.js +28 -21
  144. package/dist/src/contracts/drag.js.map +1 -1
  145. package/dist/src/contracts/editing.d.ts +1283 -0
  146. package/dist/src/contracts/editing.d.ts.map +1 -0
  147. package/dist/src/contracts/editing.js +711 -0
  148. package/dist/src/contracts/editing.js.map +1 -0
  149. package/dist/src/contracts/index.d.ts +2 -1
  150. package/dist/src/contracts/index.d.ts.map +1 -1
  151. package/dist/src/contracts/index.js +2 -1
  152. package/dist/src/contracts/index.js.map +1 -1
  153. package/dist/src/contracts/pick.d.ts +6 -7
  154. package/dist/src/contracts/pick.d.ts.map +1 -1
  155. package/dist/src/contracts/pick.js +6 -7
  156. package/dist/src/contracts/pick.js.map +1 -1
  157. package/dist/src/contracts/review.d.ts +3 -1
  158. package/dist/src/contracts/review.d.ts.map +1 -1
  159. package/dist/src/contracts/review.js +3 -1
  160. package/dist/src/contracts/review.js.map +1 -1
  161. package/dist/src/contracts/source.d.ts +270 -80
  162. package/dist/src/contracts/source.d.ts.map +1 -1
  163. package/dist/src/contracts/source.js +357 -58
  164. package/dist/src/contracts/source.js.map +1 -1
  165. package/dist/src/display/chip-rail/index.d.ts.map +1 -1
  166. package/dist/src/display/chip-rail/index.js +10 -10
  167. package/dist/src/display/chip-rail/index.js.map +1 -1
  168. package/dist/src/extension.d.ts +2 -1
  169. package/dist/src/extension.d.ts.map +1 -1
  170. package/dist/src/extension.js +2 -1
  171. package/dist/src/extension.js.map +1 -1
  172. package/dist/src/format/index.d.ts +1 -154
  173. package/dist/src/format/index.d.ts.map +1 -1
  174. package/dist/src/format/types.d.ts +35 -23
  175. package/dist/src/format/types.d.ts.map +1 -1
  176. package/dist/src/format/types.js +16 -14
  177. package/dist/src/format/types.js.map +1 -1
  178. package/dist/src/forms/field/index.js +4 -4
  179. package/dist/src/forms/field/index.js.map +1 -1
  180. package/dist/src/forms/file-upload/index.js +16 -16
  181. package/dist/src/forms/file-upload/index.js.map +1 -1
  182. package/dist/src/index.d.ts +4 -2
  183. package/dist/src/index.d.ts.map +1 -1
  184. package/dist/src/index.js +12 -5
  185. package/dist/src/index.js.map +1 -1
  186. package/dist/src/internal.d.ts +8 -3
  187. package/dist/src/internal.d.ts.map +1 -1
  188. package/dist/src/internal.js +11 -3
  189. package/dist/src/internal.js.map +1 -1
  190. package/dist/src/layout/box/index.d.ts.map +1 -1
  191. package/dist/src/layout/box/index.js +47 -47
  192. package/dist/src/layout/box/index.js.map +1 -1
  193. package/dist/src/layout/flex/index.d.ts.map +1 -1
  194. package/dist/src/layout/flex/index.js +47 -47
  195. package/dist/src/layout/flex/index.js.map +1 -1
  196. package/dist/src/layout/grid/index.js +11 -11
  197. package/dist/src/layout/grid/index.js.map +1 -1
  198. package/dist/src/layout/scroll-area/index.d.ts.map +1 -1
  199. package/dist/src/layout/scroll-area/index.js +8 -8
  200. package/dist/src/layout/scroll-area/index.js.map +1 -1
  201. package/dist/src/layout/separator/index.d.ts.map +1 -1
  202. package/dist/src/layout/separator/index.js +6 -6
  203. package/dist/src/layout/separator/index.js.map +1 -1
  204. package/dist/src/layout/stack/index.d.ts.map +1 -1
  205. package/dist/src/layout/stack/index.js +46 -46
  206. package/dist/src/layout/stack/index.js.map +1 -1
  207. package/dist/src/layout/sticky/index.d.ts.map +1 -1
  208. package/dist/src/layout/sticky/index.js +8 -8
  209. package/dist/src/layout/sticky/index.js.map +1 -1
  210. package/dist/src/overlays/action-bar/index.d.ts.map +1 -1
  211. package/dist/src/overlays/action-bar/index.js +8 -8
  212. package/dist/src/overlays/action-bar/index.js.map +1 -1
  213. package/dist/src/platform/index.d.ts +1 -1
  214. package/dist/src/platform/index.d.ts.map +1 -1
  215. package/dist/src/platform/index.js +1 -1
  216. package/dist/src/platform/index.js.map +1 -1
  217. package/dist/src/platform/slice/impl.d.ts +62 -51
  218. package/dist/src/platform/slice/impl.d.ts.map +1 -1
  219. package/dist/src/platform/slice/impl.js +155 -72
  220. package/dist/src/platform/slice/impl.js.map +1 -1
  221. package/dist/src/platform/slice/index.d.ts +3744 -2226
  222. package/dist/src/platform/slice/index.d.ts.map +1 -1
  223. package/dist/src/platform/slice/index.js +43 -1
  224. package/dist/src/platform/slice/index.js.map +1 -1
  225. package/dist/src/runtime/collections/deck.d.ts +4 -0
  226. package/dist/src/runtime/collections/deck.d.ts.map +1 -1
  227. package/dist/src/runtime/collections/matrix.d.ts +10 -9
  228. package/dist/src/runtime/collections/matrix.d.ts.map +1 -1
  229. package/dist/src/runtime/collections/matrix.js +11 -6
  230. package/dist/src/runtime/collections/matrix.js.map +1 -1
  231. package/dist/src/runtime/collections/plan.d.ts +148 -65
  232. package/dist/src/runtime/collections/plan.d.ts.map +1 -1
  233. package/dist/src/runtime/collections/plan.js +155 -67
  234. package/dist/src/runtime/collections/plan.js.map +1 -1
  235. package/dist/src/runtime/collections/sheet.d.ts +56 -23
  236. package/dist/src/runtime/collections/sheet.d.ts.map +1 -1
  237. package/dist/src/runtime/collections/sheet.js +30 -22
  238. package/dist/src/runtime/collections/sheet.js.map +1 -1
  239. package/dist/src/runtime/collections/table.d.ts +18 -11
  240. package/dist/src/runtime/collections/table.d.ts.map +1 -1
  241. package/dist/src/runtime/collections/table.js +7 -4
  242. package/dist/src/runtime/collections/table.js.map +1 -1
  243. package/dist/src/shared/reify.d.ts +4 -46
  244. package/dist/src/shared/reify.d.ts.map +1 -1
  245. package/dist/src/shared/reify.js +4 -61
  246. package/dist/src/shared/reify.js.map +1 -1
  247. package/dist/src/shared/tree.d.ts +88 -0
  248. package/dist/src/shared/tree.d.ts.map +1 -0
  249. package/dist/src/shared/tree.js +134 -0
  250. package/dist/src/shared/tree.js.map +1 -0
  251. package/dist/src/slice/breakdown/index.d.ts +4 -0
  252. package/dist/src/slice/breakdown/index.d.ts.map +1 -1
  253. package/dist/src/slice/breakdown/types.d.ts +4 -0
  254. package/dist/src/slice/breakdown/types.d.ts.map +1 -1
  255. package/dist/src/slice/cohort/index.d.ts +24 -4
  256. package/dist/src/slice/cohort/index.d.ts.map +1 -1
  257. package/dist/src/slice/cohort/index.js +8 -4
  258. package/dist/src/slice/cohort/index.js.map +1 -1
  259. package/dist/src/slice/cohort/types.d.ts +11 -0
  260. package/dist/src/slice/cohort/types.d.ts.map +1 -1
  261. package/dist/src/slice/cohort/types.js +7 -0
  262. package/dist/src/slice/cohort/types.js.map +1 -1
  263. package/dist/src/slice/filter/index.d.ts +4 -0
  264. package/dist/src/slice/filter/index.d.ts.map +1 -1
  265. package/dist/src/slice/filter/types.d.ts +4 -0
  266. package/dist/src/slice/filter/types.d.ts.map +1 -1
  267. package/dist/src/slice/index.d.ts +736 -0
  268. package/dist/src/slice/index.d.ts.map +1 -1
  269. package/dist/src/slice/legend/index.d.ts +4 -0
  270. package/dist/src/slice/legend/index.d.ts.map +1 -1
  271. package/dist/src/slice/legend/types.d.ts +4 -0
  272. package/dist/src/slice/legend/types.d.ts.map +1 -1
  273. package/dist/src/slice/rail/index.d.ts +8 -4
  274. package/dist/src/slice/rail/index.d.ts.map +1 -1
  275. package/dist/src/slice/rail/index.js.map +1 -1
  276. package/dist/src/slice/range/index.d.ts +4 -0
  277. package/dist/src/slice/range/index.d.ts.map +1 -1
  278. package/dist/src/slice/range/types.d.ts +4 -0
  279. package/dist/src/slice/range/types.d.ts.map +1 -1
  280. package/dist/src/slice/search/index.d.ts +4 -0
  281. package/dist/src/slice/search/index.d.ts.map +1 -1
  282. package/dist/src/slice/search/types.d.ts +4 -0
  283. package/dist/src/slice/search/types.d.ts.map +1 -1
  284. package/dist/src/slice/summary/index.d.ts +4 -0
  285. package/dist/src/slice/summary/index.d.ts.map +1 -1
  286. package/dist/src/slice/summary/types.d.ts +4 -0
  287. package/dist/src/slice/summary/types.d.ts.map +1 -1
  288. package/dist/src/typography/code/index.js +1 -1
  289. package/dist/src/typography/code/index.js.map +1 -1
  290. package/dist/src/typography/code-block/index.js +7 -7
  291. package/dist/src/typography/code-block/index.js.map +1 -1
  292. package/dist/src/typography/heading/index.js +2 -2
  293. package/dist/src/typography/heading/index.js.map +1 -1
  294. package/dist/src/typography/list/index.js +1 -1
  295. package/dist/src/typography/list/index.js.map +1 -1
  296. package/dist/src/typography/mark/index.js +1 -1
  297. package/dist/src/typography/mark/index.js.map +1 -1
  298. package/dist/src/typography/note/index.js +1 -1
  299. package/dist/src/typography/note/index.js.map +1 -1
  300. package/dist/src/typography/numeric/index.d.ts +3 -308
  301. package/dist/src/typography/numeric/index.d.ts.map +1 -1
  302. package/dist/src/typography/numeric/index.js +4 -4
  303. package/dist/src/typography/numeric/index.js.map +1 -1
  304. package/dist/src/typography/numeric/types.d.ts +1 -154
  305. package/dist/src/typography/numeric/types.d.ts.map +1 -1
  306. package/dist/src/typography/text/index.js +1 -1
  307. package/dist/src/typography/text/index.js.map +1 -1
  308. package/dist/test/collections/matrix.examples.d.ts +7 -3
  309. package/dist/test/collections/matrix.examples.d.ts.map +1 -1
  310. package/dist/test/collections/matrix.examples.js +55 -24
  311. package/dist/test/collections/matrix.examples.js.map +1 -1
  312. package/dist/test/collections/paged-source.examples.d.ts +4 -0
  313. package/dist/test/collections/paged-source.examples.d.ts.map +1 -1
  314. package/dist/test/collections/paged-source.examples.js +98 -13
  315. package/dist/test/collections/paged-source.examples.js.map +1 -1
  316. package/dist/test/collections/plan.examples.d.ts +101 -26
  317. package/dist/test/collections/plan.examples.d.ts.map +1 -1
  318. package/dist/test/collections/plan.examples.js +1481 -755
  319. package/dist/test/collections/plan.examples.js.map +1 -1
  320. package/dist/test/collections/sheet-link.examples.d.ts +7 -0
  321. package/dist/test/collections/sheet-link.examples.d.ts.map +1 -0
  322. package/dist/test/collections/sheet-link.examples.js +41 -0
  323. package/dist/test/collections/sheet-link.examples.js.map +1 -0
  324. package/dist/test/collections/sheet-transactions.examples.d.ts +7 -0
  325. package/dist/test/collections/sheet-transactions.examples.d.ts.map +1 -0
  326. package/dist/test/collections/sheet-transactions.examples.js +58 -0
  327. package/dist/test/collections/sheet-transactions.examples.js.map +1 -0
  328. package/dist/test/collections/sheet.examples.d.ts +52 -5
  329. package/dist/test/collections/sheet.examples.d.ts.map +1 -1
  330. package/dist/test/collections/sheet.examples.js +500 -158
  331. package/dist/test/collections/sheet.examples.js.map +1 -1
  332. package/dist/test/collections/table.examples.d.ts +31 -3
  333. package/dist/test/collections/table.examples.d.ts.map +1 -1
  334. package/dist/test/collections/table.examples.js +188 -28
  335. package/dist/test/collections/table.examples.js.map +1 -1
  336. package/dist/test/contracts/editing.examples.d.ts +7 -0
  337. package/dist/test/contracts/editing.examples.d.ts.map +1 -0
  338. package/dist/test/contracts/editing.examples.js +58 -0
  339. package/dist/test/contracts/editing.examples.js.map +1 -0
  340. package/dist/test/layout/splitter.examples.d.ts.map +1 -1
  341. package/dist/test/layout/splitter.examples.js +3 -1
  342. package/dist/test/layout/splitter.examples.js.map +1 -1
  343. package/dist/test/platform/slice.examples.d.ts.map +1 -1
  344. package/dist/test/platform/slice.examples.js +1 -0
  345. package/dist/test/platform/slice.examples.js.map +1 -1
  346. package/dist/test/slice/slice.examples.d.ts.map +1 -1
  347. package/dist/test/slice/slice.examples.js +5 -2
  348. package/dist/test/slice/slice.examples.js.map +1 -1
  349. package/package.json +4 -3
  350. package/dist/src/collections/plan/data-forms.d.ts +0 -332
  351. package/dist/src/collections/plan/data-forms.d.ts.map +0 -1
  352. package/dist/src/collections/plan/data-forms.js +0 -212
  353. package/dist/src/collections/plan/data-forms.js.map +0 -1
@@ -4,9 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "@elaraai/east-ui/jsx-runtime";
4
4
  * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
5
5
  */
6
6
  /** @jsxImportSource @elaraai/east-ui */
7
- import { ArrayType, BooleanType, DateTimeType, DictType, East, FloatType, IntegerType, NullType, OptionType, StringType, StructType, VariantType, example, none, some, variant, } from "@elaraai/east";
8
- import { DragEventType, EventStateType, State, StatusValueType, Style, UIComponentType } from "@elaraai/east-ui";
9
- import { Badge, Box, Chart, Configurator, Format, Library, Plan, Progress, Reactive, SegmentGroup, Select, Slice, Sparkline, Text, VStack, deriveApproval } from "@elaraai/east-ui";
7
+ import { ArrayType, BooleanType, DateTimeType, DictType, East, FloatType, IntegerType, NullType, OptionType, RecursiveType, StringType, StructType, VariantType, example, none, some, variant, } from "@elaraai/east";
8
+ import { ApprovalStateType, DragEventType, Editing, EventStateType, State, StatusValueType, Style, UIComponentType } from "@elaraai/east-ui";
9
+ import { Badge, Box, Button, Chart, Configurator, Format, HStack, Library, Plan, Progress, Reactive, SegmentGroup, Select, Slice, Sparkline, Text, VStack, deriveApproval } from "@elaraai/east-ui";
10
10
  // The corpus — every canvas is DEFINED the one way (`Plan Data Interface.md`
11
11
  // §3.5): `data` (RAW domain rows — batches, tonnes, lifecycle states; row
12
12
  // series discriminated by a field; no factory-built values in the data) +
@@ -15,8 +15,12 @@ import { Badge, Box, Chart, Configurator, Format, Library, Plan, Progress, React
15
15
  // canvas vocabulary from the raw fields client-side: labels, quantity
16
16
  // displays, element values via the `Plan.run`/`chip`/`event` expression
17
17
  // builders) + the root RESOLVER functions (`popover` / `hover` /
18
- // `expandRender`). The kind factories are subtree vocabulary only —
19
- // library-template `make` bodies and `Plan.series.rows` one-off chrome.
18
+ // `expandRender`). The series list IS the layout — one block per series, top
19
+ // to bottom — and hierarchy comes only from the data's own nesting (#822): a
20
+ // series' `children` walk what an entry holds, and a flat source is grouped in
21
+ // a data step first (`groupToDicts`). A row's id is its series and the path of
22
+ // entry keys to it (`Plan.ref`). The kind factories are subtree vocabulary only
23
+ // — the one-off chrome a `Plan.series.rows` entry places (`planLiteralRows`).
20
24
  // Fixtures are defined INLINE in each example's fn body (the automated docs
21
25
  // extract only the fn body) and DERIVED with East expressions — a `week(n)`
22
26
  // ISO-week function plus `East.Array.generate` — never hand-written per
@@ -29,11 +33,12 @@ import { Badge, Box, Chart, Configurator, Format, Library, Plan, Progress, React
29
33
  export const planTargetState = example({
30
34
  keywords: [
31
35
  "Plan", "canvas", "data", "series", "match", "axis", "window", "resolution", "now",
32
- "span", "run", "group", "heat", "buckets", "cards", "chip", "events", "mark", "chart",
36
+ "span", "run", "group", "section", "heat", "buckets", "cards", "chip", "events", "mark", "chart",
33
37
  "layers", "rollup", "bands", "review", "footer", "milestone",
34
38
  "decision", "exception", "pinned", "port", "hovercard", "popover",
35
39
  "slice", "brush", "horizon", "toolbar", "affordances", "expand",
36
40
  "expandRender", "resolver", "data-driven", "accessor", "raw", "target state",
41
+ "layout", "row id", "Plan.ref", "links", "quantity", "Plan.quantity", "onElementClick",
37
42
  ],
38
43
  description: "Every row kind on one axis from a single raw ops source, with slice chrome, expand, review and a status footer",
39
44
  fn: East.function([], UIComponentType, (_$) => {
@@ -113,24 +118,26 @@ export const planTargetState = example({
113
118
  tonnes: i.remainder(7n).equal(6n).ifElse(() => 0.0, () => i.multiply(11n).remainder(40n).toFloat().add(20.0)),
114
119
  })));
115
120
  // Raw jobs → runs: ONE bound mapping function — the bar label and
116
- // the quantity display/number pair derive CLIENT-SIDE from the
117
- // raw phase/batch/tonnes fields (the series-make application). The
118
- // run is written as a RECORD, so its instants are spelled with
119
- // `Plan.at.time` — the `Plan.run` builder would wrap a DateTime
120
- // field by itself.
121
+ // the quantity derive CLIENT-SIDE from the raw phase/batch/tonnes
122
+ // fields (the series-make application). A quantity is ONE value —
123
+ // the number, its unit and how it prints (#824) — so the bar's
124
+ // caption and a rollup's sum can never disagree. The run is written
125
+ // as a RECORD, so its instants are spelled with `Plan.at.time` —
126
+ // the `Plan.run` builder would wrap a DateTime field by itself.
127
+ const tonnesFormat = $.const(Format.Number({ maximumFractionDigits: 0n }));
121
128
  const jobRuns = $.const(East.function([ArrayType(JobRow)], ArrayType(Plan.Types.Run), (_$, jobs) => jobs.map(($, j) => {
122
- const noQuantity = $.const(none, OptionType(StringType));
129
+ const noQuantity = $.const(none, OptionType(Plan.Types.Quantity));
123
130
  const quantity = $.let(j.tonnes.match({
124
- some: (_$, t) => East.value(some(East.str `${East.Float.printFixed(t, 0n)} t`), OptionType(StringType)),
131
+ some: (_$, t) => East.value(some(Plan.quantity(t, { unit: "t", format: tonnesFormat })), OptionType(Plan.Types.Quantity)),
125
132
  none: (_$) => noQuantity,
126
- }), OptionType(StringType));
133
+ }), OptionType(Plan.Types.Quantity));
127
134
  const label = $.let(j.batch.match({
128
135
  some: (_$, b) => East.str `${j.phase} · ${b}`,
129
136
  none: (_$) => j.phase,
130
137
  }), StringType);
131
138
  const run = $.let({
132
139
  key: j.key, start: Plan.at.time(j.start), end: Plan.at.time(j.end), label,
133
- quantity, qty: j.tonnes, state: j.state, status: j.alert,
140
+ quantity, state: j.state, status: j.alert,
134
141
  moved: none, icon: none,
135
142
  }, Plan.Types.Run);
136
143
  return run;
@@ -148,12 +155,12 @@ export const planTargetState = example({
148
155
  })));
149
156
  // The ONE ops source — every series' rows in one KEYED collection,
150
157
  // RAW: no display strings the accessors can derive, no built
151
- // elements. The keys are the canvas's order as well as its
152
- // identity (#568), so the §1 layout is expressed by naming them:
153
- // the ordering segment places each series, the rest is the row's
154
- // real id — which is what `links`, `popover` and `onSelect` speak.
158
+ // elements. A key is its entry's identity, never its place: a row's
159
+ // id is the series that made it and this key
160
+ // (`Plan.ref("machines", "L1-M03")`), which is what `links`,
161
+ // `popover` and `onSelect` speak — the §1 layout is the series list.
155
162
  const ops = $.const(new Map([
156
- ["coverage", { kind: variant("kpi", { name: "COVERAGE", headline: "94.2%", pinned: true, points: coverage }) }],
163
+ ["coverage", { kind: variant("kpi", { name: "Coverage", headline: "94.2%", pinned: true, points: coverage }) }],
157
164
  ["L1-M03", { kind: variant("machine", { cap: 120.0, status: some(variant("success", null)), detail: none,
158
165
  jobs: [
159
166
  { key: "set", phase: "SET", batch: none, start: week(27n), end: week(28n), tonnes: none, state: variant("actual", null), alert: none },
@@ -194,14 +201,14 @@ export const planTargetState = example({
194
201
  { key: "a8", at: week(35n), state: variant("proposed", variant("recommended", null)) },
195
202
  ],
196
203
  markers: [{ at: Plan.at.time(week(35n)), lane: none, status: variant("warning", null), message: "capacity breach — 2 allocations" }] }) }],
197
- ["40-crewA", { kind: variant("crew", { name: "Crew A", hours: "152h → 168h", shifts: [
204
+ ["crewA", { kind: variant("crew", { name: "Crew A", hours: "152h → 168h", shifts: [
198
205
  { key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
199
206
  { key: "s2", from: week(29n), to: week(31n), hours: 72.0, state: variant("confirmed", null) },
200
207
  { key: "s3", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
201
208
  { key: "s4", from: week(34n), to: week(35n), hours: 48.0, state: variant("estimated", null) },
202
209
  { key: "s5", from: week(36n), to: week(38n), hours: 56.0, state: variant("proposed", variant("recommended", null)) },
203
210
  ] }) }],
204
- ["50-milestones", { kind: variant("stream", { name: "MILESTONES", marks: [
211
+ ["milestones", { kind: variant("stream", { name: "Milestones", marks: [
205
212
  { key: "kick", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
206
213
  { key: "d1", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
207
214
  { key: "rel", at: Plan.at.time(week(33n)), kind: variant("milestone", null), icon: none, label: some("REL 2.4") },
@@ -209,11 +216,13 @@ export const planTargetState = example({
209
216
  { key: "d2", at: Plan.at.time(week(37n)), kind: variant("decision", { applied: false }), icon: none, label: some("×3") },
210
217
  ] }) }],
211
218
  ]), DictType(StringType, OpsRow));
212
- // The series — one entry per row series, canvas order = series
213
- // order; the whole list is an East value typed by the constructor.
219
+ // The series — the list IS the layout (#822): one block per
220
+ // series, top to bottom in this order, a section titling the
221
+ // blocks beneath it. The whole list is an East value typed by the
222
+ // constructor.
214
223
  const series = $.const([
215
224
  Plan.series.chart(OpsRow, {
216
- key: "chart", title: "Chart",
225
+ key: "coverage", title: "Coverage",
217
226
  match: r => r.kind.hasTag("kpi"),
218
227
  label: r => r.kind.unwrap("kpi").name, id: true,
219
228
  pinned: r => r.kind.unwrap("kpi").pinned,
@@ -225,9 +234,9 @@ export const planTargetState = example({
225
234
  Chart.refLine({ y: 100, label: "TARGET 100" }),
226
235
  ],
227
236
  }),
228
- Plan.series.group(OpsRow, { key: "10-line1", label: "Line 1", meta: "8 rs · 82%" }, [
237
+ Plan.series.section(OpsRow, { key: "line1", title: "Line 1", meta: "3 rows · 82%" }, [
229
238
  Plan.series.span(OpsRow, {
230
- key: "span", title: "Span",
239
+ key: "machines", title: "Machines",
231
240
  match: r => r.kind.hasTag("machine"),
232
241
  label: (_r, k) => k, id: true,
233
242
  value: r => some(East.str `${East.Float.printFixed(r.kind.unwrap("machine").cap, 0n)} t`),
@@ -238,34 +247,34 @@ export const planTargetState = example({
238
247
  ports: r => r.kind.unwrap("machine").ports,
239
248
  }),
240
249
  ]),
241
- Plan.series.group(OpsRow, { key: "20-line2", label: "Line 2", value: "98%", status: "warning", collapsed: true, summaryAggregate: "mean" }, [
250
+ Plan.series.section(OpsRow, { key: "line2", title: "Line 2", value: "98%", status: "warning", collapsed: true, summaryAggregate: "mean" }, [
242
251
  Plan.series.heat(OpsRow, {
243
- key: "heat", title: "Heat",
252
+ key: "load", title: "Load",
244
253
  match: r => r.kind.hasTag("load"),
245
254
  label: r => r.kind.unwrap("load").name,
246
255
  sub: r => some(r.kind.unwrap("load").sub),
247
256
  cells: r => Plan.heatCells(r.kind.unwrap("load").cells, { min: 0, max: 100, warnAt: 95 }),
248
257
  }),
249
258
  ]),
250
- Plan.series.group(OpsRow, { key: "30-docks", label: "Docks · In", meta: "3 rs" }, [
259
+ Plan.series.section(OpsRow, { key: "docks-in", title: "Docks · In", meta: "1 row" }, [
251
260
  Plan.series.buckets(OpsRow, {
252
- key: "buckets", title: "Buckets",
261
+ key: "docks", title: "Docks",
253
262
  match: r => r.kind.hasTag("dock"),
254
263
  label: r => r.kind.unwrap("dock").name,
255
- value: _r => some("load/wk"),
264
+ sub: _r => some("load/wk"),
256
265
  events: r => r.kind.unwrap("dock").allocations.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
257
266
  markers: r => r.kind.unwrap("dock").markers,
258
267
  }),
259
268
  ]),
260
269
  Plan.series.cards(OpsRow, {
261
- key: "cards", title: "Cards",
270
+ key: "crews", title: "Crews",
262
271
  match: r => r.kind.hasTag("crew"),
263
272
  label: r => r.kind.unwrap("crew").name, stacked: true,
264
273
  sub: r => some(r.kind.unwrap("crew").hours),
265
274
  chips: r => shiftChips(r.kind.unwrap("crew").shifts),
266
275
  }),
267
276
  Plan.series.events(OpsRow, {
268
- key: "events", title: "Events",
277
+ key: "milestones", title: "Milestones",
269
278
  match: r => r.kind.hasTag("stream"),
270
279
  label: r => r.kind.unwrap("stream").name, id: true,
271
280
  value: r => some(East.print(r.kind.unwrap("stream").marks.length())),
@@ -277,11 +286,13 @@ export const planTargetState = example({
277
286
  resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
278
287
  }));
279
288
  // The §1 toolbar is SEEDED slice state: the applied window is the
280
- // range chip (`JUN 29 → SEP 21 · 84d`), and two saved cohorts sit
289
+ // range chip (`JUN 29 – SEP 20 · 84d`), and two saved cohorts sit
281
290
  // beside the filter builder with `Late risk` active — their
282
- // counts are live over the horizon fixture, never printed.
291
+ // counts are live over the horizon fixture, never printed. A slice
292
+ // range is CLOSED — both ends inclusive — so the twelve weeks
293
+ // W27–W38 end the millisecond before W39.
283
294
  const slice = $.let(Slice.bind([HorizonRow], "ex.plan.target", cfg, Slice.state({
284
- range: some(variant("datetime", { from: week(27n), to: week(39n) })),
295
+ range: some(variant("datetime", { from: week(27n), to: week(39n).addMilliseconds(-1n) })),
285
296
  cohorts: [
286
297
  { id: "late", name: "Late risk", filters: [variant("string", { fieldId: "risk", op: variant("eq", "late") })] },
287
298
  { id: "empty", name: "Empty", filters: [variant("float", { fieldId: "tonnes", op: variant("lte", 0.0) })] },
@@ -289,19 +300,20 @@ export const planTargetState = example({
289
300
  activeCohorts: new Set(["late"]),
290
301
  }), horizon, none));
291
302
  // The R2 developer render — the ROOT's resolver, called with the
292
- // focused row's ref; ONE function serves every row whose `expand`
303
+ // focused row's id; ONE function serves every row whose `expand`
293
304
  // accessor returned some(...). The machine series declares it.
294
305
  const util = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(17n).remainder(45n).toFloat().add(52.0) })));
295
- const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Chart, { layers: [Chart.Line(util, { x: r => r.week, y: r => r.pct })], height: 120, grid: false }))));
306
+ const expandRender = $.const(East.function([Plan.Types.RowId], UIComponentType, (_$, _id) => (_jsx(Chart, { layers: [Chart.Line(util, { x: r => r.week, y: r => r.pct })], height: 120, grid: false }))));
296
307
  // The generalized element resolvers — ONE stored popover / hover
297
- // function each over Plan.Types.ElementRef (refs carry the row
298
- // key on every arm); rich bodies build lazily at interaction
299
- // time, and a none result opens no surface.
308
+ // function each over Plan.Types.ElementRef (every arm carries the
309
+ // row's id); rich bodies build lazily at interaction time, and a
310
+ // none result opens no surface.
300
311
  const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
301
312
  const noBody = $.const(none, OptionType(UIComponentType));
313
+ const m03 = $.const(Plan.ref("machines", "L1-M03"));
302
314
  return ref.match({
303
315
  run: (_$, ev) => ev.run.equal("b221").ifElse(() => some(_jsx(Text, { children: "Proposed by run 412 \u2014 fills the W32 idle window." })), () => noBody),
304
- mark: (_$, ev) => ev.row.equal("L1-M03").and(() => ev.mark.equal("d1")).ifElse(() => some(_jsx(Text, { children: "Schedule B-221" })), () => noBody),
316
+ mark: (_$, ev) => East.equal(ev.row, m03).and(() => ev.mark.equal("d1")).ifElse(() => some(_jsx(Text, { children: "Schedule B-221" })), () => noBody),
305
317
  }, _$ => noBody);
306
318
  }));
307
319
  const hover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
@@ -311,23 +323,31 @@ export const planTargetState = example({
311
323
  }, _$ => noBody);
312
324
  }));
313
325
  // Behavior props — bound once so memoized renderers keep identity.
314
- const onRowRef = $.const(East.function([Plan.Types.RowRef], NullType, (_$, _r) => null));
315
- const onRunClick = $.const(East.function([Plan.Types.RunClickEvent], NullType, (_$, _e) => null));
326
+ const onRow = $.const(East.function([Plan.Types.RowId], NullType, (_$, _id) => null));
327
+ // ONE element callback (#824) — a run, tile, mark, chip, cell or
328
+ // link ribbon, by the same ref the popover resolver receives.
329
+ const onElementClick = $.const(East.function([Plan.Types.ElementRef], NullType, (_$, _ref) => null));
316
330
  const onGroupToggle = $.const(East.function([Plan.Types.GroupToggleEvent], NullType, (_$, _e) => null));
317
331
  const onBatch = $.const(East.function([], NullType, (_$) => null));
318
332
  return (_jsx(Plan, { slice: { slice, affordances: ["cohort", "filter", "search", "range", "resolution", "brush", "summary"] }, axis: axis,
319
- // The link graph (R1) — the W31 −24 t transfer: hover a
320
- // linked machine for the links-focus control.
333
+ // The link graph (R1) — the W31 −24 t transfer, its ends
334
+ // named by row id: hover a linked machine for the
335
+ // links-focus control. Its quantity weighs the ribbon, and
336
+ // `text` says it the author's way.
321
337
  links: [
322
- Plan.link({ from: "L1-M03", fromRun: "b214", to: "L1-M04", toRun: "qc", quantity: 24, label: "−24 t" }),
323
- ], data: ops, series: series, review: {
338
+ Plan.link({
339
+ key: "t-w31",
340
+ from: Plan.ref("machines", "L1-M03"), fromRun: "b214",
341
+ to: Plan.ref("machines", "L1-M04"), toRun: "qc",
342
+ quantity: Plan.quantity(24, { unit: "t", text: "−24 t" }),
343
+ }),
344
+ ], data: ops, series: series,
345
+ // The review chrome — its foot and Rerun. A verdict is a
346
+ // draft of an editing session (`planReview`, `planEditing`).
347
+ review: {
324
348
  summary: _jsx(Text, { children: "4 JOBS \u00B7 2 FLAGGED NEED A CALL \u00B7 +6H FLOAT" }),
325
- onApprove: onRowRef,
326
- onReject: onRowRef,
327
- onApproveAll: onBatch,
328
- onRejectAll: onBatch,
329
349
  onRerun: onBatch,
330
- }, expandRender: expandRender, popover: popover, hover: hover, onSelect: onRowRef, onRunClick: onRunClick, onGroupToggle: onGroupToggle, footer: [
350
+ }, expandRender: expandRender, popover: popover, hover: hover, onSelect: onRow, onElementClick: onElementClick, onGroupToggle: onGroupToggle, footer: [
331
351
  { text: "512 RESOURCES · 12 GROUPS · 3 IN VIEW" },
332
352
  { text: "318 OBSERVED · 966 PLANNED" },
333
353
  { text: "3 EXCEPTIONS", tone: "warning" },
@@ -348,7 +368,8 @@ export const planTargetState = example({
348
368
  * resolution, and the 14-day sprint at day resolution with `ddd DD` labels
349
369
  * (the #309 pinned-day-columns contract the Planner's `day` preset and the
350
370
  * AlignedStack date-axis panel guarded). The style sweeps ride `style` —
351
- * density rhythm and gutter width — and every callback (select / run click /
371
+ * density rhythm and gutter width — over a canvas holding every row kind, so
372
+ * one click re-rhythms them all; every callback (select / element click /
352
373
  * group toggle / grain change) logs to the aside, the retired configurators'
353
374
  * pattern. Fill sizing stays `planFill`'s; the per-kind visual grammars stay
354
375
  * the static per-kind panels.
@@ -358,10 +379,12 @@ export const planVariants = example({
358
379
  "Plan", "configurator", "Configurator", "variants", "preset", "axis", "window",
359
380
  "resolution", "month", "week", "day", "roadmap", "sprint", "ops", "format",
360
381
  "ddd", "#309", "density", "condensed", "compact", "comfortable", "gutterWidth",
361
- "gutter", "style", "onSelect", "onRunClick", "onGroupToggle", "onGrainChange",
382
+ "gutter", "style", "onSelect", "onElementClick", "ElementRef", "onGroupToggle", "onGrainChange",
362
383
  "callback", "aside", "Reactive", "State", "SegmentGroup", "Select", "getTag",
384
+ "every row kind", "span", "chart", "expandable", "heat", "buckets", "table",
385
+ "tableSeries", "vertical", "cards", "events", "group", "section", "row id", "print",
363
386
  ],
364
- description: "Plan configurator — axis window presets (ops week / year roadmap / 14-day sprint) with density and gutter-width sweeps on one live canvas; every callback logs to the aside",
387
+ description: "Plan configurator — axis window presets (ops week / year roadmap / 14-day sprint) with density and gutter-width sweeps over every row kind on one live canvas; every callback logs to the aside",
365
388
  fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
366
389
  // Monday of ISO week n, 2026 — the ops window is W27–W38
367
390
  // (half-open at W39), now = W31; the roadmap preset walks the
@@ -375,16 +398,33 @@ export const planVariants = example({
375
398
  key: StringType, label: StringType,
376
399
  start: DateTimeType, end: DateTimeType, state: EventStateType,
377
400
  });
401
+ const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
402
+ const ShiftRow = StructType({
403
+ key: StringType, from: DateTimeType, to: DateTimeType, label: StringType, state: EventStateType,
404
+ });
405
+ const RawCell = StructType({ at: DateTimeType, value: OptionType(FloatType) });
406
+ // ONE flat source (the `planExpand` shape): `series` names the
407
+ // series that claims the row, and every channel a row's kind does
408
+ // not read stays empty.
378
409
  const OpsRow = StructType({
379
410
  series: StringType, label: StringType,
380
411
  sub: OptionType(StringType),
381
412
  jobs: ArrayType(JobRow),
382
413
  points: ArrayType(MeasureRow),
383
414
  cells: ArrayType(Plan.Types.HeatCell),
415
+ allocs: ArrayType(AllocRow),
416
+ act: ArrayType(RawCell),
417
+ plan: ArrayType(RawCell),
418
+ shifts: ArrayType(ShiftRow),
419
+ marks: ArrayType(Plan.Types.EventMark),
384
420
  });
385
421
  const noJobs = $.const([], ArrayType(JobRow));
386
422
  const noPoints = $.const([], ArrayType(MeasureRow));
387
423
  const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
424
+ const noAllocs = $.const([], ArrayType(AllocRow));
425
+ const noRaw = $.const([], ArrayType(RawCell));
426
+ const noShifts = $.const([], ArrayType(ShiftRow));
427
+ const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
388
428
  const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
389
429
  const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
390
430
  const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
@@ -392,30 +432,62 @@ export const planVariants = example({
392
432
  value: some(pcts.get(i)),
393
433
  label: some(East.Float.printFixed(pcts.get(i), 0n)),
394
434
  })));
395
- // Mixed kinds on purpose: the density sweep re-rhythms the span /
396
- // heat rows and the gutter sweep re-widths every label at once.
435
+ // Despatched tonnes and their signed Δ against plan, fortnightly —
436
+ // the table row's two positions.
437
+ const act = $.let(East.Array.generate(6n, RawCell, (_$, i) => ({
438
+ at: week(i.multiply(2n).add(27n)), value: some(pcts.get(i).multiply(1.5)),
439
+ })));
440
+ const delta = $.let(East.Array.generate(6n, RawCell, (_$, i) => ({
441
+ at: week(i.multiply(2n).add(27n)), value: some(pcts.get(i).subtract(70.0)),
442
+ })));
443
+ const base = {
444
+ sub: none, jobs: noJobs, points: noPoints, cells: noCells,
445
+ allocs: noAllocs, act: noRaw, plan: noRaw, shifts: noShifts, marks: noMarks,
446
+ };
447
+ // Every row kind on purpose: the density sweep re-rhythms them all
448
+ // at once — compact tightens the shared row and the bars in it,
449
+ // while a two-line gutter keeps its floor and a stacked table a
450
+ // line per position — and the gutter sweep re-widths every label.
397
451
  const ops = $.const(new Map([
398
- ["10-util", { series: "util", label: "UTIL %", sub: none, jobs: noJobs, points, cells: noCells }],
399
- ["20-m03", { series: "mach", label: "L1-M03", sub: some("cap 120 t"),
452
+ ["util", { ...base, series: "util", label: "Util %", points }],
453
+ ["m03", { ...base, series: "mach", label: "L1-M03", sub: some("cap 120 t"),
400
454
  jobs: [
401
455
  { key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) },
402
456
  { key: "b221", label: "RUN · B-221", start: week(32n), end: week(35n), state: variant("proposed", variant("recommended", null)) },
403
- ], points: noPoints, cells: noCells }],
404
- ["20-m04", { series: "mach", label: "L1-M04", sub: none,
457
+ ] }],
458
+ ["m04", { ...base, series: "mach", label: "L1-M04",
405
459
  jobs: [
406
460
  { key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
407
- ], points: noPoints, cells: noCells }],
408
- ["30-load", { series: "load", label: "L2 load", sub: some("%/wk"), jobs: noJobs, points: noPoints, cells }],
409
- ["g1-m11", { series: "gmach", label: "L3-M11", sub: none,
461
+ ] }],
462
+ ["load", { ...base, series: "load", label: "L2 load", sub: some("%/wk"), cells }],
463
+ ["dock2", { ...base, series: "dock", label: "Dock 2",
464
+ allocs: [
465
+ { key: "a1", at: week(28n), state: variant("confirmed", null) },
466
+ { key: "a2", at: week(31n), state: variant("proposed", variant("recommended", null)) },
467
+ { key: "a3", at: week(33n), state: variant("confirmed", null) },
468
+ ] }],
469
+ ["desp", { ...base, series: "desp", label: "Despatch t", act, plan: delta }],
470
+ ["crewA", { ...base, series: "crew", label: "Crew A",
471
+ shifts: [
472
+ { key: "s1", from: week(27n), to: week(29n), label: "80h", state: variant("confirmed", null) },
473
+ { key: "s2", from: week(31n), to: week(33n), label: "+64h", state: variant("proposed", variant("recommended", null)) },
474
+ ] }],
475
+ ["ms", { ...base, series: "ms", label: "Milestones",
476
+ marks: [
477
+ { key: "k", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
478
+ { key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
479
+ ] }],
480
+ ["m11", { ...base, series: "gmach", label: "L3-M11",
410
481
  jobs: [
411
482
  { key: "b301", label: "RUN · B-301", start: week(29n), end: week(33n), state: variant("confirmed", null) },
412
- ], points: noPoints, cells: noCells }],
483
+ ] }],
413
484
  ]), DictType(StringType, OpsRow));
414
485
  const series = $.const([
486
+ // The spark's gutter opens it to the expanded chart, at any density.
415
487
  Plan.series.chart(OpsRow, {
416
488
  key: "util", title: "Utilisation",
417
489
  match: r => r.series.equal("util"),
418
- label: r => r.label, id: true, height: "spark",
490
+ label: r => r.label, id: true, height: "spark", expandable: true,
419
491
  layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
420
492
  }),
421
493
  Plan.series.span(OpsRow, {
@@ -432,8 +504,41 @@ export const planVariants = example({
432
504
  label: r => r.label, sub: r => r.sub,
433
505
  cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
434
506
  }),
435
- // A strip so the group-toggle callback has something to fire on.
436
- Plan.series.group(OpsRow, { key: "g1-line3", label: "Line 3", meta: "1 rs" }, [
507
+ Plan.series.buckets(OpsRow, {
508
+ key: "dock", title: "Dock allocations",
509
+ match: r => r.series.equal("dock"),
510
+ label: r => r.label,
511
+ events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
512
+ }),
513
+ // The positions STACKED — the row grows a line per position.
514
+ Plan.series.table(OpsRow, {
515
+ key: "desp", title: "Despatch",
516
+ match: r => r.series.equal("desp"),
517
+ label: r => r.label, split: "vertical",
518
+ series: r => [
519
+ Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
520
+ Plan.tableSeries({
521
+ tone: "muted",
522
+ format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
523
+ cells: Plan.tableCells(r.plan),
524
+ }),
525
+ ],
526
+ format: Format.Number({ maximumFractionDigits: 0n }),
527
+ }),
528
+ Plan.series.cards(OpsRow, {
529
+ key: "crew", title: "Crew shifts",
530
+ match: r => r.series.equal("crew"),
531
+ label: r => r.label,
532
+ chips: r => r.shifts.map((_$, s) => Plan.chip({ key: s.key, from: s.from, to: s.to, label: s.label, state: s.state })),
533
+ }),
534
+ Plan.series.events(OpsRow, {
535
+ key: "ms", title: "Milestones",
536
+ match: r => r.series.equal("ms"),
537
+ label: r => r.label, id: true,
538
+ marks: r => r.marks,
539
+ }),
540
+ // A section header, so the group-toggle callback has something to fire on.
541
+ Plan.series.section(OpsRow, { key: "line3", title: "Line 3", meta: "1 row" }, [
437
542
  Plan.series.span(OpsRow, {
438
543
  key: "gmach", title: "Grouped jobs",
439
544
  match: r => r.series.equal("gmach"),
@@ -477,16 +582,20 @@ export const planVariants = example({
477
582
  const onPreset = $.const(East.function([StringType], NullType, ($, next) => { $(presetBind.write(next)); }));
478
583
  const onDensity = $.const(East.function([StringType], NullType, ($, next) => { $(densityBind.write(next)); }));
479
584
  const onGutter = $.const(East.function([StringType], NullType, ($, next) => { $(gutterBind.write(next)); }));
480
- // The interactive surface — every callback writes the aside line.
481
- const onSelect = $.const(East.function([Plan.Types.RowRef], NullType, ($, ref) => {
482
- $(lastEventBind.write(East.str `onSelect · ${ref.key}`));
585
+ // The interactive surface — every callback writes the aside line,
586
+ // naming the row by its id (the series that made it and the path
587
+ // of keys to it) in its `.east` text.
588
+ const onSelect = $.const(East.function([Plan.Types.RowId], NullType, ($, id) => {
589
+ $(lastEventBind.write(East.str `onSelect · ${East.print(id)}`));
483
590
  }));
484
- const onRunClick = $.const(East.function([Plan.Types.RunClickEvent], NullType, ($, ev) => {
485
- $(lastEventBind.write(East.str `onRunClick · ${ev.row} / ${ev.run}`));
591
+ // ONE callback for every element (#824) — the ref's arm says which
592
+ // kind was clicked, and each arm names its row and element.
593
+ const onElementClick = $.const(East.function([Plan.Types.ElementRef], NullType, ($, ref) => {
594
+ $(lastEventBind.write(East.str `onElementClick · ${ref.getTag()} · ${East.print(ref)}`));
486
595
  }));
487
596
  const onGroupToggle = $.const(East.function([Plan.Types.GroupToggleEvent], NullType, ($, ev) => {
488
597
  const state = $.let(ev.expanded.ifElse(() => "expanded", () => "collapsed"), StringType);
489
- $(lastEventBind.write(East.str `onGroupToggle · ${ev.row} → ${state}`));
598
+ $(lastEventBind.write(East.str `onGroupToggle · ${East.print(ev.row)} → ${state}`));
490
599
  }));
491
600
  const onGrainChange = $.const(East.function([Plan.Types.Grain], NullType, ($, grain) => {
492
601
  $(lastEventBind.write(East.str `onGrainChange · ${grain.getTag()}`));
@@ -497,7 +606,7 @@ export const planVariants = example({
497
606
  Configurator.Control("Preset", pKey, _jsx(Select, { value: pKey, onChange: onPreset, size: "sm", items: presetKeys.map((_$, s) => Select.Item(s, s)) })),
498
607
  Configurator.Control("Density", dKey, _jsx(SegmentGroup, { value: dKey, onChange: onDensity, size: "sm", items: densities.map((_$, v) => SegmentGroup.Item(v.getTag(), _jsx(Text, { children: v.getTag().upperCase() }))) })),
499
608
  Configurator.Control("Gutter", gKey, _jsx(SegmentGroup, { value: gKey, onChange: onGutter, size: "sm", items: gutters.map((_$, g) => SegmentGroup.Item(g, _jsx(Text, { children: g }))) })),
500
- ], preview: _jsx(Plan, { axis: sel.axis, data: ops, series: series, onSelect: onSelect, onRunClick: onRunClick, onGroupToggle: onGroupToggle, onGrainChange: onGrainChange, style: { density: densitySel, gutterWidth: gKey } }), live: true, aside: {
609
+ ], preview: _jsx(Plan, { axis: sel.axis, data: ops, series: series, onSelect: onSelect, onElementClick: onElementClick, onGroupToggle: onGroupToggle, onGrainChange: onGrainChange, style: { density: densitySel, gutterWidth: gKey } }), live: true, aside: {
501
610
  label: "Events · Reactive",
502
611
  body: (_jsx(Badge, { colorPalette: "brand", variant: "outline", children: East.equal(lastEvent.length(), 0n).ifElse(_$ => "Interact with the canvas", _$ => lastEvent) })),
503
612
  }, spec: [
@@ -511,8 +620,8 @@ export const planVariants = example({
511
620
  // Per-kind examples — one canvas per row kind; the config sweep is DATA
512
621
  // ============================================================================
513
622
  export const planSpanRows = example({
514
- keywords: ["Plan", "data", "series", "span", "run", "state", "estimated", "removed", "rejected", "decision", "port", "rollup", "union", "byStatus", "groupBy", "bands", "group", "stacked", "gutter", "links", "link", "focus", "expand", "expandRender", "match", "raw"],
515
- description: "Span rows over one raw machine source — proposal flavours, decision diamonds and ports, rollup parents, and a link graph",
623
+ keywords: ["Plan", "data", "series", "span", "run", "state", "estimated", "removed", "rejected", "decision", "port", "rollup", "union", "byStatus", "children", "nested", "recursive", "RecursiveType", "bands", "section", "stacked", "gutter", "links", "link", "Plan.ref", "row id", "focus", "expand", "expandRender", "match", "raw", "quantity", "Plan.quantity", "unit", "sum per unit"],
624
+ description: "Span rows over one raw machine source — proposal flavours, decision diamonds and ports, programs rolling their machines up into bands, and a link graph",
516
625
  fn: East.function([], UIComponentType, ($) => {
517
626
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
518
627
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -527,69 +636,84 @@ export const planSpanRows = example({
527
636
  start: DateTimeType, end: DateTimeType,
528
637
  tonnes: OptionType(FloatType), state: EventStateType,
529
638
  });
530
- const MachineRow = StructType({
531
- series: StringType, program: StringType,
639
+ // A PROGRAM is a record too, holding its machines (`machines`): the
640
+ // hierarchy is the data's own (#822), so a program's row nests them —
641
+ // and the entry type is recursive, to whatever depth the data has.
642
+ const MachineRow = RecursiveType((self) => StructType({
643
+ series: StringType,
532
644
  sub: OptionType(StringType), value: OptionType(StringType),
533
645
  expand: OptionType(Plan.Types.Expand),
534
646
  jobs: ArrayType(JobRow),
535
647
  decisions: ArrayType(Plan.Types.DecisionMark),
536
648
  ports: ArrayType(Plan.Types.Port),
537
- });
649
+ machines: DictType(StringType, self),
650
+ }));
651
+ const noMachines = $.const(new Map(), DictType(StringType, MachineRow));
538
652
  const machines = $.const(new Map([
539
653
  // Proposal flavours: forecast ghost · proposed cut · declined.
540
- ["L1-M07", { series: "flavours", program: "", sub: none, value: some("80 t"), expand: none,
654
+ ["L1-M07", { series: "flavours", sub: none, value: some("80 t"), expand: none,
541
655
  jobs: [
542
656
  { key: "run", phase: "RUN", batch: some("B-197"), start: week(27n), end: week(30n), tonnes: some(64.0), state: variant("in-progress", null) },
543
657
  { key: "gho", phase: "FORECAST", batch: none, start: week(30n), end: week(32n), tonnes: none, state: variant("estimated", null) },
544
658
  { key: "rem", phase: "CUT", batch: none, start: week(33n), end: week(35n), tonnes: none, state: variant("proposed", variant("removed", null)) },
545
659
  { key: "rej", phase: "DECLINED", batch: none, start: week(36n), end: week(38n), tonnes: none, state: variant("rejected", null) },
546
- ], decisions: [], ports: [] }],
660
+ ], decisions: [], ports: [], machines: noMachines }],
547
661
  // Tonnage + an applied decision + a port on a stacked two-line
548
662
  // gutter; the EXPAND DECLARATION is row data (R2) — the render is
549
663
  // the root's expandRender resolver.
550
- ["L1-M09", { series: "detail", program: "", sub: some("cap 120 t"), value: none,
664
+ ["L1-M09", { series: "detail", sub: some("cap 120 t"), value: none,
551
665
  expand: some({ height: some("152px"), axis: variant("dim", null) }),
552
666
  jobs: [
553
667
  { key: "a", phase: "RUN", batch: some("B-208"), start: week(27n), end: week(31n), tonnes: some(112.0), state: variant("actual", null) },
554
668
  { key: "b", phase: "RUN", batch: some("B-231"), start: week(31n), end: week(34n), tonnes: some(104.0), state: variant("proposed", variant("recommended", null)) },
555
669
  ],
556
670
  decisions: [{ key: "d1", at: Plan.at.time(week(31n)), applied: true }],
557
- ports: [{ at: Plan.at.time(week(31n)), label: some("−24 t") }] }],
558
- // The rollup series — one union parent per program (renderer-derived bands).
559
- ["L1-M03", { series: "rollup", program: "Program A", sub: none, value: none, expand: none,
560
- jobs: [
561
- { key: "b214", phase: "RUN", batch: some("B-214"), start: week(28n), end: week(31n), tonnes: some(96.0), state: variant("actual", null) },
562
- { key: "b221", phase: "RUN", batch: some("B-221"), start: week(32n), end: week(35n), tonnes: some(88.0), state: variant("proposed", variant("recommended", null)) },
563
- ], decisions: [], ports: [] }],
564
- ["L2-M11", { series: "rollup", program: "Program A", sub: none, value: none, expand: none,
565
- jobs: [{ key: "b241", phase: "RUN", batch: some("B-241"), start: week(29n), end: week(33n), tonnes: some(92.0), state: variant("confirmed", null) }],
566
- decisions: [], ports: [] }],
671
+ ports: [{ at: Plan.at.time(week(31n)), label: some("−24 t") }], machines: noMachines }],
672
+ // A program and its machines — the program's row rolls their runs
673
+ // up into union bands (renderer-derived).
674
+ ["Program A", { series: "rollup", sub: none, value: none, expand: none, jobs: [], decisions: [], ports: [],
675
+ machines: new Map([
676
+ ["L1-M03", { series: "rollup", sub: none, value: none, expand: none,
677
+ jobs: [
678
+ { key: "b214", phase: "RUN", batch: some("B-214"), start: week(28n), end: week(31n), tonnes: some(96.0), state: variant("actual", null) },
679
+ { key: "b221", phase: "RUN", batch: some("B-221"), start: week(32n), end: week(35n), tonnes: some(88.0), state: variant("proposed", variant("recommended", null)) },
680
+ ], decisions: [], ports: [], machines: noMachines }],
681
+ ["L2-M11", { series: "rollup", sub: none, value: none, expand: none,
682
+ jobs: [{ key: "b241", phase: "RUN", batch: some("B-241"), start: week(29n), end: week(33n), tonnes: some(92.0), state: variant("confirmed", null) }],
683
+ decisions: [], ports: [], machines: noMachines }],
684
+ ]) }],
567
685
  // Linked despatch whose run starts BEYOND the window — in links
568
686
  // focus its landing renders as the edge fade.
569
- ["dsp", { series: "despatch", program: "", sub: none, value: none, expand: none,
687
+ ["dsp", { series: "despatch", sub: none, value: none, expand: none,
570
688
  jobs: [{ key: "d1", phase: "DSP", batch: none, start: week(39n), end: week(42n), tonnes: some(91.0), state: variant("proposed", variant("recommended", null)) }],
571
- decisions: [], ports: [] }],
572
- // The byStatus series inside the Line 2 strip.
573
- ["L2-M12", { series: "line2", program: "Program B", sub: none, value: none, expand: none,
574
- jobs: [
575
- { key: "r1", phase: "RUN", batch: some("B-198"), start: week(28n), end: week(32n), tonnes: some(64.0), state: variant("actual", null) },
576
- { key: "r2", phase: "RUN", batch: some("B-202"), start: week(30n), end: week(34n), tonnes: some(40.0), state: variant("proposed", variant("recommended", null)) },
577
- ], decisions: [], ports: [] }],
689
+ decisions: [], ports: [], machines: noMachines }],
690
+ // The program under the Line 2 section, rolled up byStatus.
691
+ ["Program B", { series: "line2", sub: none, value: none, expand: none, jobs: [], decisions: [], ports: [],
692
+ machines: new Map([
693
+ ["L2-M12", { series: "line2", sub: none, value: none, expand: none,
694
+ jobs: [
695
+ { key: "r1", phase: "RUN", batch: some("B-198"), start: week(28n), end: week(32n), tonnes: some(64.0), state: variant("actual", null) },
696
+ { key: "r2", phase: "RUN", batch: some("B-202"), start: week(30n), end: week(34n), tonnes: some(40.0), state: variant("proposed", variant("recommended", null)) },
697
+ ], decisions: [], ports: [], machines: noMachines }],
698
+ ]) }],
578
699
  ]), DictType(StringType, MachineRow));
579
- // Raw jobs → runs, once — every span series shares the mapping.
700
+ // Raw jobs → runs, once — every span series shares the mapping. A
701
+ // job's tonnage is its run's QUANTITY: the bar prints it, and a
702
+ // program's bands sum its machines' tonnes unit by unit (#824).
703
+ const tonnesFormat = $.const(Format.Number({ maximumFractionDigits: 0n }));
580
704
  const jobRuns = $.const(East.function([ArrayType(JobRow)], ArrayType(Plan.Types.Run), (_$, jobs) => jobs.map(($, j) => {
581
- const noQuantity = $.const(none, OptionType(StringType));
705
+ const noQuantity = $.const(none, OptionType(Plan.Types.Quantity));
582
706
  const quantity = $.let(j.tonnes.match({
583
- some: (_$, t) => East.value(some(East.str `${East.Float.printFixed(t, 0n)} t`), OptionType(StringType)),
707
+ some: (_$, t) => East.value(some(Plan.quantity(t, { unit: "t", format: tonnesFormat })), OptionType(Plan.Types.Quantity)),
584
708
  none: (_$) => noQuantity,
585
- }), OptionType(StringType));
709
+ }), OptionType(Plan.Types.Quantity));
586
710
  const label = $.let(j.batch.match({
587
711
  some: (_$, b) => East.str `${j.phase} · ${b}`,
588
712
  none: (_$) => j.phase,
589
713
  }), StringType);
590
714
  const run = $.let({
591
715
  key: j.key, start: Plan.at.time(j.start), end: Plan.at.time(j.end), label,
592
- quantity, qty: j.tonnes, state: j.state,
716
+ quantity, state: j.state,
593
717
  status: none, moved: none, icon: none,
594
718
  }, Plan.Types.Run);
595
719
  return run;
@@ -609,12 +733,14 @@ export const planSpanRows = example({
609
733
  sub: r => r.sub, expand: r => r.expand,
610
734
  runs: r => jobRuns(r.jobs), decisions: r => r.decisions, ports: r => r.ports,
611
735
  }),
736
+ // A program's machines nest under it (`children` — more of this
737
+ // series, to any depth), and its row rolls their runs up.
612
738
  Plan.series.span(MachineRow, {
613
739
  key: "rollup", title: "Rollup",
614
740
  match: r => r.series.equal("rollup"),
615
741
  label: (_r, k) => k, id: true,
616
742
  runs: r => jobRuns(r.jobs),
617
- groupBy: [r => r.program], rollup: "union", unit: "t",
743
+ children: r => r.machines, rollup: "union",
618
744
  }),
619
745
  Plan.series.span(MachineRow, {
620
746
  key: "despatch", title: "Despatch",
@@ -622,21 +748,21 @@ export const planSpanRows = example({
622
748
  label: (_r, k) => k, id: true,
623
749
  runs: r => jobRuns(r.jobs),
624
750
  }),
625
- Plan.series.group(MachineRow, { key: "line2", label: "Line 2", meta: "1 rs" }, [
751
+ Plan.series.section(MachineRow, { key: "line2", title: "Line 2", meta: "1 row" }, [
626
752
  Plan.series.span(MachineRow, {
627
- key: "line2", title: "Line2",
753
+ key: "programs", title: "Programs",
628
754
  match: r => r.series.equal("line2"),
629
755
  label: (_r, k) => k, id: true,
630
756
  runs: r => jobRuns(r.jobs),
631
- groupBy: [r => r.program], rollup: "byStatus", unit: "t",
757
+ children: r => r.machines, rollup: "byStatus",
632
758
  }),
633
759
  ]),
634
760
  ], ArrayType(Plan.Types.Series(MachineRow)));
635
761
  const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
636
762
  // The R2 developer render — the ROOT's resolver, called with the
637
- // focused row's ref; ONE function serves every declaring row.
763
+ // focused row's id; ONE function serves every declaring row.
638
764
  const util = $.let(East.Array.generate(8n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(13n).remainder(40n).toFloat().add(55.0) })));
639
- const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Chart, { layers: [Chart.Column(util, { x: r => r.week, y: r => r.pct })], height: 100, grid: false }))));
765
+ const expandRender = $.const(East.function([Plan.Types.RowId], UIComponentType, (_$, _id) => (_jsx(Chart, { layers: [Chart.Column(util, { x: r => r.week, y: r => r.pct })], height: 100, grid: false }))));
640
766
  // The generalized popover resolver — decision diamonds ride the mark
641
767
  // arm of the element ref.
642
768
  const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
@@ -645,6 +771,13 @@ export const planSpanRows = example({
645
771
  mark: (_$, ev) => ev.mark.equal("d1").ifElse(() => some(_jsx(Text, { children: "Approved by run 411." })), () => noBody),
646
772
  }, _$ => noBody);
647
773
  }));
774
+ // The linked rows, by id — the series that made each and the path of
775
+ // entry keys to it (a program's machine sits under the program).
776
+ const m07 = $.const(Plan.ref("flavours", "L1-M07"));
777
+ const m09 = $.const(Plan.ref("detail", "L1-M09"));
778
+ const m03 = $.const(Plan.ref("rollup", "Program A", "L1-M03"));
779
+ const m11 = $.const(Plan.ref("rollup", "Program A", "L2-M11"));
780
+ const dsp = $.const(Plan.ref("despatch", "dsp"));
648
781
  return (_jsx(Plan, { expandRender: expandRender, popover: popover,
649
782
  // A denser gutter (value + carets) — widen it (the shared
650
783
  // CSS-px height/width vocabulary).
@@ -652,20 +785,21 @@ export const planSpanRows = example({
652
785
  // The link graph (R1) — hover a linked row for the ⌁ control.
653
786
  // The edges deliberately cover the routing permutations:
654
787
  // forward, a same-row seam feed, loopbacks, a rising loop,
655
- // and an off-window landing.
788
+ // and an off-window landing. Each moves a QUANTITY (#824):
789
+ // its value weighs the ribbon and its caption prints on it.
656
790
  links: [
657
- Plan.link({ from: "L1-M07", fromRun: "run", to: "L1-M09", toRun: "a", quantity: 24, label: "24 t" }),
658
- Plan.link({ from: "L1-M09", fromRun: "a", to: "L1-M09", toRun: "b", quantity: 40, label: "40 t" }),
659
- Plan.link({ from: "L1-M09", fromRun: "b", to: "L1-M03", toRun: "b221", quantity: 88, label: "88 t" }),
660
- Plan.link({ from: "L1-M03", fromRun: "b214", to: "L2-M11", toRun: "b241", quantity: 32, label: "32 t" }),
661
- Plan.link({ from: "L2-M11", fromRun: "b241", to: "L1-M09", toRun: "b", quantity: 18, label: "18 t" }),
662
- Plan.link({ from: "L1-M09", fromRun: "b", to: "dsp", toRun: "d1", quantity: 91, label: "91 t" }),
791
+ Plan.link({ key: "l1", from: m07, fromRun: "run", to: m09, toRun: "a", quantity: Plan.quantity(24, { unit: "t" }) }),
792
+ Plan.link({ key: "l2", from: m09, fromRun: "a", to: m09, toRun: "b", quantity: Plan.quantity(40, { unit: "t" }) }),
793
+ Plan.link({ key: "l3", from: m09, fromRun: "b", to: m03, toRun: "b221", quantity: Plan.quantity(88, { unit: "t" }) }),
794
+ Plan.link({ key: "l4", from: m03, fromRun: "b214", to: m11, toRun: "b241", quantity: Plan.quantity(32, { unit: "t" }) }),
795
+ Plan.link({ key: "l5", from: m11, fromRun: "b241", to: m09, toRun: "b", quantity: Plan.quantity(18, { unit: "t" }) }),
796
+ Plan.link({ key: "l6", from: m09, fromRun: "b", to: dsp, toRun: "d1", quantity: Plan.quantity(91, { unit: "t" }) }),
663
797
  ], data: machines, series: series }));
664
798
  }),
665
799
  inputs: [],
666
800
  });
667
801
  export const planBucketRows = example({
668
- keywords: ["Plan", "data", "series", "buckets", "Planner", "lane", "lanes", "AM", "PM", "event", "tile", "marker", "tone", "color", "colorPalette", "stretch", "pulse", "icon", "hovercard", "popover", "mixed", "unbucketed", "group", "match", "gutter", "raw"],
802
+ keywords: ["Plan", "data", "series", "buckets", "Planner", "lane", "lanes", "AM", "PM", "event", "tile", "marker", "tone", "color", "colorPalette", "stretch", "pulse", "icon", "hovercard", "popover", "mixed", "unbucketed", "section", "match", "gutter", "raw"],
669
803
  description: "Bucket rows over one dock source — tiles derived in the accessor, and stored tile records with lanes, tones, colours and markers",
670
804
  fn: East.function([], UIComponentType, ($) => {
671
805
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
@@ -676,7 +810,7 @@ export const planBucketRows = example({
676
810
  const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
677
811
  const DockRow = StructType({
678
812
  series: StringType, label: StringType,
679
- sub: OptionType(StringType), value: OptionType(StringType),
813
+ sub: OptionType(StringType),
680
814
  lanes: ArrayType(Plan.Types.Lane),
681
815
  allocations: ArrayType(AllocRow),
682
816
  tiles: ArrayType(Plan.Types.BucketEvent),
@@ -693,12 +827,12 @@ export const planBucketRows = example({
693
827
  };
694
828
  }));
695
829
  const docks = $.const(new Map([
696
- ["dock2", { series: "inbound", label: "Dock 2", sub: none, value: some("load/wk"),
830
+ ["dock2", { series: "inbound", label: "Dock 2", sub: some("load/wk"),
697
831
  lanes: [], allocations: inbound, tiles: [],
698
832
  markers: [{ at: Plan.at.time(week(29n)), lane: none, status: variant("warning", null), message: "capacity 90%" }] }],
699
833
  // The grammar showcase — tiles stored IN the element vocabulary
700
834
  // (plain `PlanBucketEventType` records; no builders in data).
701
- ["dock5", { series: "outbound", label: "Dock 5", sub: some("day · am/pm"), value: none,
835
+ ["dock5", { series: "outbound", label: "Dock 5", sub: some("day · am/pm"),
702
836
  lanes: [{ key: "am", label: some("AM") }, { key: "pm", label: some("PM") }],
703
837
  allocations: [],
704
838
  tiles: [
@@ -733,11 +867,11 @@ export const planBucketRows = example({
733
867
  key: "inbound", title: "Inbound",
734
868
  match: r => r.series.equal("inbound"),
735
869
  label: r => r.label,
736
- value: r => r.value,
870
+ sub: r => r.sub,
737
871
  events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
738
872
  markers: r => r.markers,
739
873
  }),
740
- Plan.series.group(DockRow, { key: "outbound", label: "Docks · Out", meta: "1 rs" }, [
874
+ Plan.series.section(DockRow, { key: "docks-out", title: "Docks · Out", meta: "1 row" }, [
741
875
  // Stored vocabulary records pass straight through.
742
876
  Plan.series.buckets(DockRow, {
743
877
  key: "outbound", title: "Outbound",
@@ -767,7 +901,7 @@ export const planBucketRows = example({
767
901
  inputs: [],
768
902
  });
769
903
  export const planChartRows = example({
770
- keywords: ["Plan", "data", "series", "chart", "layers", "spark", "expanded", "fixed", "refLine", "refBand", "refDot", "breach", "stacked", "dual-axis", "swatches", "Area", "Band", "Scatter", "Column", "Line", "domain", "tickValues", "group", "match", "gutter", "raw"],
904
+ keywords: ["Plan", "data", "series", "chart", "layers", "spark", "expanded", "fixed", "refLine", "refBand", "refDot", "breach", "stacked", "dual-axis", "swatches", "Area", "Band", "Scatter", "Column", "Line", "domain", "tickValues", "section", "match", "gutter", "raw"],
771
905
  description: "Chart rows over one measure source — one series per mark kind, plus annotations and a fixed dual-axis composition",
772
906
  fn: East.function([], UIComponentType, ($) => {
773
907
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
@@ -794,12 +928,12 @@ export const planChartRows = example({
794
928
  const ppm = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(37n).remainder(60n).toFloat().add(120.0) })));
795
929
  const band = $.let(East.Array.generate(12n, BandRow, (_$, i) => ({ week: week(i.add(27n)), lo: pcts.get(i).subtract(3.0), hi: pcts.get(i).add(3.0) })));
796
930
  const measures = $.const(new Map([
797
- ["spark", { series: "spark", label: "COVERAGE", sub: none, value: some("94.2%"), points: coverage, extra: [], band: [] }],
798
- ["cum", { series: "cum", label: "CUMULATIVE · t", sub: none, value: some("194 t"), points: cum, extra: [], band: [] }],
799
- ["stacked", { series: "stacked", label: "OUTPUT · t", sub: some("t/wk"), value: none, points: out1, extra: out2, band: [] }],
800
- ["ppm", { series: "ppm", label: "DEFECTS · ppm", sub: none, value: some("161"), points: ppm, extra: [], band: [] }],
801
- ["refs", { series: "refs", label: "COVERAGE + REFS", sub: none, value: none, points: coverage, extra: [], band: [] }],
802
- ["dual", { series: "dual", label: "OUT + COVERAGE", sub: none, value: none, points: out1, extra: coverage, band }],
931
+ ["spark", { series: "spark", label: "Coverage", sub: none, value: some("94.2%"), points: coverage, extra: [], band: [] }],
932
+ ["cum", { series: "cum", label: "Cumulative · t", sub: none, value: some("194 t"), points: cum, extra: [], band: [] }],
933
+ ["stacked", { series: "stacked", label: "Output · t", sub: some("t/wk"), value: none, points: out1, extra: out2, band: [] }],
934
+ ["ppm", { series: "ppm", label: "Defects · ppm", sub: none, value: some("161"), points: ppm, extra: [], band: [] }],
935
+ ["refs", { series: "refs", label: "Coverage + refs", sub: none, value: none, points: coverage, extra: [], band: [] }],
936
+ ["dual", { series: "dual", label: "Out + coverage", sub: none, value: none, points: out1, extra: coverage, band }],
803
937
  ]), DictType(StringType, ChartRow));
804
938
  const series = $.const([
805
939
  // Line — the KPI spark with a breach threshold; the caret opens
@@ -850,10 +984,10 @@ export const planChartRows = example({
850
984
  Chart.refDot({ x: week(36n), y: 91.4, label: "LOW" }),
851
985
  ],
852
986
  }),
853
- // The composed dual-axis chart inside a static group strip; axes
854
- // take Chart.Root's vocabulary — domain / tickValues. Output
855
- // columns scale left; the coverage line + its band scale right.
856
- Plan.series.group(ChartRow, { key: "quality", label: "Quality", meta: "1 rs" }, [
987
+ // The composed dual-axis chart under a section header; axes take
988
+ // Chart.Root's vocabulary — domain / tickValues. Output columns
989
+ // scale left; the coverage line + its band scale right.
990
+ Plan.series.section(ChartRow, { key: "quality", title: "Quality", meta: "1 row" }, [
857
991
  Plan.series.chart(ChartRow, {
858
992
  key: "dual", title: "Dual",
859
993
  match: r => r.series.equal("dual"),
@@ -876,21 +1010,26 @@ export const planChartRows = example({
876
1010
  inputs: [],
877
1011
  });
878
1012
  export const planHeatRows = example({
879
- keywords: ["Plan", "data", "series", "heat", "Matrix", "cells", "depth", "aggregate", "mean", "groupBy", "scale", "warnAt", "weightCells", "segmentCells", "segment", "no-data", "hatch", "group", "match", "gutter", "raw"],
880
- description: "Heat rows over one line source — colour-depth cells under an aggregate parent, weight bars, and status segments",
1013
+ keywords: ["Plan", "data", "series", "heat", "Matrix", "cells", "depth", "aggregate", "mean", "children", "nested", "recursive", "RecursiveType", "scale", "warnAt", "weightCells", "segmentCells", "segment", "no-data", "hatch", "section", "match", "gutter", "raw"],
1014
+ description: "Heat rows over one line source — colour-depth cells under a line that averages them, weight bars, and status segments",
881
1015
  fn: East.function([], UIComponentType, ($) => {
882
1016
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
883
1017
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
884
1018
  const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
885
1019
  return w1.addWeeks(n.subtract(1n));
886
1020
  }));
887
- const HeatRow = StructType({
888
- series: StringType, line: StringType, label: StringType,
1021
+ // A LINE holds its machines' load rows (`children`): the hierarchy is
1022
+ // the data's (#822), so the line's row derives its own cells from
1023
+ // theirs.
1024
+ const HeatRow = RecursiveType((self) => StructType({
1025
+ series: StringType, label: StringType,
889
1026
  sub: OptionType(StringType),
890
1027
  cells: ArrayType(Plan.Types.HeatCell),
891
1028
  weights: ArrayType(Plan.Types.WeightCell),
892
1029
  segs: ArrayType(Plan.Types.SegmentCell),
893
- });
1030
+ children: DictType(StringType, self),
1031
+ }));
1032
+ const noChildren = $.const(new Map(), DictType(StringType, HeatRow));
894
1033
  const pcts = $.const([46.0, 52.0, 58.0, 61.0, 66.0, 72.0, 78.0, 84.0, 90.0, 96.0, 98.0, 92.0], ArrayType(FloatType));
895
1034
  const cells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
896
1035
  at: Plan.at.time(week(i.add(27n))),
@@ -904,11 +1043,16 @@ export const planHeatRows = example({
904
1043
  planned: i.greaterEqual(3n),
905
1044
  })));
906
1045
  const lines = $.const(new Map([
907
- ["m03h", { series: "depth", line: "Line 1", label: "L1-M03", sub: none, cells, weights: [], segs: [] }],
908
- ["m04h", { series: "depth", line: "Line 1", label: "L1-M04", sub: none, cells, weights: [], segs: [] }],
909
- ["booked", { series: "booked", line: "", label: "Crew A", sub: some("booked h"), cells: [], weights, segs: [] }],
1046
+ // A line with no cells of its own — its row shows the per-bucket
1047
+ // mean of its machines' (the series declares `aggregate`).
1048
+ ["line1", { series: "depth", label: "Line 1", sub: none, cells: [], weights: [], segs: [],
1049
+ children: new Map([
1050
+ ["m03h", { series: "depth", label: "L1-M03", sub: none, cells, weights: [], segs: [], children: noChildren }],
1051
+ ["m04h", { series: "depth", label: "L1-M04", sub: none, cells, weights: [], segs: [], children: noChildren }],
1052
+ ]) }],
1053
+ ["booked", { series: "booked", label: "Crew A", sub: some("booked h"), cells: [], weights, segs: [], children: noChildren }],
910
1054
  // Segment compositions — plain `{ fill, weight, label }` records.
911
- ["pack", { series: "segments", line: "", label: "Pack line", sub: some("capacity"), cells: [], weights: [],
1055
+ ["pack", { series: "segments", label: "Pack line", sub: some("capacity"), cells: [], weights: [], children: noChildren,
912
1056
  segs: [
913
1057
  { at: Plan.at.time(week(27n)), segments: [
914
1058
  { fill: variant("success", null), weight: 60.0, label: some("60%") },
@@ -926,13 +1070,16 @@ export const planHeatRows = example({
926
1070
  ] }],
927
1071
  ]), DictType(StringType, HeatRow));
928
1072
  const series = $.const([
929
- // The aggregate-mean parent derives per discovered line value.
1073
+ // A line's machines nest under it, and its row is their per-bucket
1074
+ // mean — painted on `scale`, the scale a parent's DERIVED cells
1075
+ // take (#824; a mean of rows on 0–100 would inherit it anyway).
930
1076
  Plan.series.heat(HeatRow, {
931
1077
  key: "depth", title: "Depth",
932
1078
  match: r => r.series.equal("depth"),
933
1079
  label: r => r.label, id: true,
934
1080
  cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
935
- groupBy: [r => r.line], aggregate: "mean", scale: { min: 0, max: 100, warnAt: 95 },
1081
+ children: r => r.children, aggregate: "mean",
1082
+ scale: { min: 0, max: 100, warnAt: 95 },
936
1083
  }),
937
1084
  Plan.series.heat(HeatRow, {
938
1085
  key: "booked", title: "Booked",
@@ -941,7 +1088,7 @@ export const planHeatRows = example({
941
1088
  sub: r => r.sub,
942
1089
  cells: r => Plan.weightCells(r.weights),
943
1090
  }),
944
- Plan.series.group(HeatRow, { key: "packing", label: "Packing", meta: "1 rs" }, [
1091
+ Plan.series.section(HeatRow, { key: "packing", title: "Packing", meta: "1 row" }, [
945
1092
  Plan.series.heat(HeatRow, {
946
1093
  key: "segments", title: "Segments",
947
1094
  match: r => r.series.equal("segments"),
@@ -957,8 +1104,8 @@ export const planHeatRows = example({
957
1104
  inputs: [],
958
1105
  });
959
1106
  export const planTableRows = example({
960
- keywords: ["Plan", "data", "series", "table", "cells", "tableCells", "subtotal", "aggregate", "sum", "format", "emphasis", "footer", "groupBy", "nested", "depth", "em-dash", "neg", "match", "gutter", "tableSeries", "split", "horizontal", "vertical", "multi-value", "multi-cell", "stacked", "two-line", "strong", "muted", "rollup", "mirror", "position", "raw"],
961
- description: "Table rows over one order source — nested groupBy subtotals, footer emphasis, and every split × gutter combination",
1107
+ keywords: ["Plan", "data", "series", "table", "cells", "tableCells", "subtotal", "aggregate", "sum", "format", "emphasis", "footer", "children", "nested", "recursive", "RecursiveType", "depth", "em-dash", "neg", "match", "gutter", "tableSeries", "split", "horizontal", "vertical", "multi-value", "multi-cell", "stacked", "two-line", "strong", "muted", "rollup", "mirror", "position", "raw"],
1108
+ description: "Table rows over one order source — subtotals at every level of the data's nesting, footer emphasis, and every split × gutter combination",
962
1109
  fn: East.function([], UIComponentType, ($) => {
963
1110
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
964
1111
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -967,13 +1114,17 @@ export const planTableRows = example({
967
1114
  }));
968
1115
  const RawCell = StructType({ at: DateTimeType, value: OptionType(FloatType) });
969
1116
  // The RAW order record — actuals and the plan Δ as per-bucket value
970
- // arrays; every display decision lives in the series configs.
971
- const OrderRow = StructType({
972
- series: StringType, name: StringType, top: StringType, program: StringType,
1117
+ // arrays; every display decision lives in the series configs. Orders
1118
+ // nest (`children`): a top holds its programs, a program its orders —
1119
+ // the hierarchy is the data's own (#822), to whatever depth it has.
1120
+ const OrderRow = RecursiveType((self) => StructType({
1121
+ series: StringType, name: StringType,
973
1122
  sub: OptionType(StringType),
974
1123
  act: ArrayType(RawCell),
975
1124
  plan: ArrayType(RawCell),
976
- });
1125
+ children: DictType(StringType, self),
1126
+ }));
1127
+ const noChildren = $.const(new Map(), DictType(StringType, OrderRow));
977
1128
  const act = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
978
1129
  at: week(i.add(27n)),
979
1130
  value: i.equal(9n).ifElse(() => none, () => some(i.toFloat().multiply(7.0).add(40.0))),
@@ -987,13 +1138,29 @@ export const planTableRows = example({
987
1138
  value: some(i.toFloat().multiply(-3.0).subtract(12.0)),
988
1139
  })));
989
1140
  const orders = $.const(new Map([
990
- // The nested series — two groupBy levels derive their subtotals.
991
- ["or-1188", { series: "orders", name: "OR-1188", top: "Despatches", program: "Program A", sub: none, act, plan: [] }],
992
- ["or-1204", { series: "orders", name: "OR-1204", top: "Despatches", program: "Program A", sub: none, act, plan: [] }],
993
- ["or-1219", { series: "orders", name: "OR-1219", top: "Despatches", program: "Program B", sub: none, act, plan: [] }],
994
- ["rt-0031", { series: "orders", name: "RT-0031", top: "Returns", program: "Program B", sub: none, act, plan: [] }],
1141
+ // Two levels of nesting — a top holds its programs, a program its
1142
+ // orders; every level with no values of its own is a subtotal.
1143
+ ["despatches", { series: "orders", name: "Despatches", sub: none, act: [], plan: [],
1144
+ children: new Map([
1145
+ ["program-a", { series: "orders", name: "Program A", sub: none, act: [], plan: [],
1146
+ children: new Map([
1147
+ ["or-1188", { series: "orders", name: "OR-1188", sub: none, act, plan: [], children: noChildren }],
1148
+ ["or-1204", { series: "orders", name: "OR-1204", sub: none, act, plan: [], children: noChildren }],
1149
+ ]) }],
1150
+ ["program-b", { series: "orders", name: "Program B", sub: none, act: [], plan: [],
1151
+ children: new Map([
1152
+ ["or-1219", { series: "orders", name: "OR-1219", sub: none, act, plan: [], children: noChildren }],
1153
+ ]) }],
1154
+ ]) }],
1155
+ ["returns", { series: "orders", name: "Returns", sub: none, act: [], plan: [],
1156
+ children: new Map([
1157
+ ["program-b", { series: "orders", name: "Program B", sub: none, act: [], plan: [],
1158
+ children: new Map([
1159
+ ["rt-0031", { series: "orders", name: "RT-0031", sub: none, act, plan: [], children: noChildren }],
1160
+ ]) }],
1161
+ ]) }],
995
1162
  // Footer emphasis + negative tone + the muted em-dash.
996
- ["net", { series: "net", name: "Net flow", top: "", program: "", sub: none, plan: [],
1163
+ ["net", { series: "net", name: "Net flow", sub: none, plan: [], children: noChildren,
997
1164
  act: [
998
1165
  { at: week(27n), value: some(22.0) }, { at: week(28n), value: some(-26.0) },
999
1166
  { at: week(29n), value: none },
@@ -1003,30 +1170,38 @@ export const planTableRows = example({
1003
1170
  // line or two) are independent choices, so all four combinations
1004
1171
  // are here: the pair that reads well depends on the numbers, not
1005
1172
  // on the split.
1006
- ["actplan", { series: "actplan", name: "Act · Δ plan", top: "", program: "", sub: some("t/wk"), act, plan: deltas }],
1007
- ["inout", { series: "inout", name: "In / out", top: "", program: "", sub: none, act, plan: outflow }],
1173
+ ["actplan", { series: "actplan", name: "Act · Δ plan", sub: some("t/wk"), act, plan: deltas, children: noChildren }],
1174
+ ["inout", { series: "inout", name: "In / out", sub: none, act, plan: outflow, children: noChildren }],
1008
1175
  // Horizontal, on a ONE-line gutter — the pair reads as a single
1009
1176
  // fact ("booked beside free"), so a sub label would only repeat it.
1010
- ["sidebyside", { series: "sidebyside", name: "Booked · free", top: "", program: "", sub: none, act, plan: outflow }],
1177
+ ["sidebyside", { series: "sidebyside", name: "Booked · free", sub: none, act, plan: outflow, children: noChildren }],
1011
1178
  // Vertical, on a TWO-line gutter — the stack needs the unit spelled
1012
1179
  // out, because the positions are the same measure at two times.
1013
- ["overunder", { series: "overunder", name: "Act / plan", top: "", program: "", sub: some("t/wk"), act, plan: deltas }],
1014
- // GROUPED and multi-value: the subtotal parent mirrors its members,
1180
+ ["overunder", { series: "overunder", name: "Act / plan", sub: some("t/wk"), act, plan: deltas, children: noChildren }],
1181
+ // NESTED and multi-value: the subtotal parent mirrors its members,
1015
1182
  // an act subtotal beside a Δ subtotal.
1016
- ["fl-1", { series: "flow", name: "FL-2201", top: "Flows", program: "", sub: none, act, plan: deltas }],
1017
- ["fl-2", { series: "flow", name: "FL-2202", top: "Flows", program: "", sub: none, act, plan: deltas }],
1183
+ ["flows", { series: "flow", name: "Flows", sub: none, act: [], plan: [],
1184
+ children: new Map([
1185
+ ["fl-1", { series: "flow", name: "FL-2201", sub: none, act, plan: deltas, children: noChildren }],
1186
+ ["fl-2", { series: "flow", name: "FL-2202", sub: none, act, plan: deltas, children: noChildren }],
1187
+ ]) }],
1018
1188
  // The same, STACKED: members and their subtotal both put the two
1019
1189
  // positions on their own lines, so the parent has to grow too.
1020
- ["st-1", { series: "stack", name: "ST-3301", top: "Stacks", program: "", sub: none, act, plan: outflow }],
1021
- ["st-2", { series: "stack", name: "ST-3302", top: "Stacks", program: "", sub: none, act, plan: outflow }],
1190
+ ["stacks", { series: "stack", name: "Stacks", sub: none, act: [], plan: [],
1191
+ children: new Map([
1192
+ ["st-1", { series: "stack", name: "ST-3301", sub: none, act, plan: outflow, children: noChildren }],
1193
+ ["st-2", { series: "stack", name: "ST-3302", sub: none, act, plan: outflow, children: noChildren }],
1194
+ ]) }],
1022
1195
  ]), DictType(StringType, OrderRow));
1023
1196
  const series = $.const([
1197
+ // Each order nests under its program, each program under its top
1198
+ // (`children`, to any depth), and every parent sums its children.
1024
1199
  Plan.series.table(OrderRow, {
1025
1200
  key: "orders", title: "Orders",
1026
1201
  match: r => r.series.equal("orders"),
1027
1202
  label: r => r.name,
1028
1203
  cells: r => Plan.tableCells(r.act),
1029
- groupBy: [r => r.top, r => r.program], aggregate: "sum",
1204
+ children: r => r.children, aggregate: "sum",
1030
1205
  format: Format.Number({ maximumFractionDigits: 0n }),
1031
1206
  }),
1032
1207
  Plan.series.table(OrderRow, {
@@ -1092,14 +1267,14 @@ export const planTableRows = example({
1092
1267
  cells: Plan.tableCells(r.plan),
1093
1268
  }),
1094
1269
  ],
1095
- groupBy: [r => r.top], aggregate: "sum",
1270
+ children: r => r.children, aggregate: "sum",
1096
1271
  format: Format.Number({ maximumFractionDigits: 0n }),
1097
1272
  }),
1098
- // GROUPED and VERTICAL — the subtotal stacks its positions the way
1099
- // its members do. The parent carries no series of its own (they are
1100
- // derived), so its height comes from the DERIVED count: estimating
1101
- // from its own empty list would call this a one-line row and render
1102
- // it as two.
1273
+ // NESTED and VERTICAL — the subtotal stacks its positions the way
1274
+ // its members do. The parent's positions carry no values of their
1275
+ // own (they are derived), so its height comes from its members'
1276
+ // count: a parent that read its own empty cells as one line would
1277
+ // render as two.
1103
1278
  Plan.series.table(OrderRow, {
1104
1279
  key: "stack", title: "Stack",
1105
1280
  match: r => r.series.equal("stack"),
@@ -1108,7 +1283,7 @@ export const planTableRows = example({
1108
1283
  Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
1109
1284
  Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
1110
1285
  ],
1111
- groupBy: [r => r.top], aggregate: "sum",
1286
+ children: r => r.children, aggregate: "sum",
1112
1287
  format: Format.Number({ maximumFractionDigits: 0n }),
1113
1288
  }),
1114
1289
  // VERTICAL on a TWO-line gutter — the remaining combination, and
@@ -1133,8 +1308,141 @@ export const planTableRows = example({
1133
1308
  }),
1134
1309
  inputs: [],
1135
1310
  });
1311
+ // ============================================================================
1312
+ // planFold — a coarser resolution FOLDS each bucket's values (#824)
1313
+ // ============================================================================
1314
+ /**
1315
+ * Temporal fold (#824). The data is weekly; the canvas shows it at whatever
1316
+ * resolution the axis states, and at MONTH every row shows ONE value per
1317
+ * month — the fold of its weeks there — where it used to stack four or five
1318
+ * on top of each other. Each cell builder and chart layer declares its fold,
1319
+ * defaulting to what its values mean: a heat level and a line fold by `mean`,
1320
+ * a table numeral and a column by `sum`, a weight fraction by `mean`. A row
1321
+ * overrides it where the meaning differs — peak load by `max`, closing stock
1322
+ * by `last`. The switch is the resolution; nothing in the data changes, and a
1323
+ * row whose weeks were never folded (WEEK) shows them as they are.
1324
+ */
1325
+ export const planFold = example({
1326
+ keywords: [
1327
+ "Plan", "fold", "temporal fold", "resolution", "week", "month", "rebucket", "bucket",
1328
+ "sum", "mean", "max", "last", "count", "default", "override", "heatCells", "weightCells",
1329
+ "tableCells", "tableSeries", "layer", "Plan.layer", "column", "line", "format",
1330
+ "Reactive", "State", "SegmentGroup", "#824",
1331
+ ],
1332
+ description: "Temporal fold — weekly data at MONTH resolution shows one folded cell per month per row: heat and lines by mean, tables and columns by sum, with per-row overrides (peak load by max, closing stock by last)",
1333
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
1334
+ // Monday of ISO week n, 2026 — twelve weeks, W27–W38.
1335
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1336
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1337
+ return w1.addWeeks(n.subtract(1n));
1338
+ }));
1339
+ const Weekly = StructType({ at: DateTimeType, value: OptionType(FloatType) });
1340
+ const MeasureRow = StructType({ week: DateTimeType, v: FloatType });
1341
+ // ONE flat weekly source; `series` picks the row's series.
1342
+ const OpsRow = StructType({
1343
+ series: StringType, label: StringType,
1344
+ heat: ArrayType(Plan.Types.HeatCell),
1345
+ weights: ArrayType(Plan.Types.WeightCell),
1346
+ weekly: ArrayType(Weekly),
1347
+ points: ArrayType(MeasureRow),
1348
+ });
1349
+ const noHeat = $.const([], ArrayType(Plan.Types.HeatCell));
1350
+ const noWeights = $.const([], ArrayType(Plan.Types.WeightCell));
1351
+ const noWeekly = $.const([], ArrayType(Weekly));
1352
+ const noPoints = $.const([], ArrayType(MeasureRow));
1353
+ // Twelve weeks of each measure, derived — never hand-written.
1354
+ const load = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
1355
+ at: Plan.at.time(week(i.add(27n))),
1356
+ value: some(i.multiply(17n).remainder(50n).toFloat().add(45.0)),
1357
+ label: none,
1358
+ })));
1359
+ const booked = $.let(East.Array.generate(12n, Plan.Types.WeightCell, (_$, i) => ({
1360
+ at: Plan.at.time(week(i.add(27n))),
1361
+ fraction: i.multiply(7n).remainder(10n).toFloat().multiply(0.06).add(0.4),
1362
+ planned: i.greaterEqual(5n),
1363
+ })));
1364
+ const tonnes = $.let(East.Array.generate(12n, Weekly, (_$, i) => ({
1365
+ at: week(i.add(27n)), value: some(i.multiply(29n).remainder(60n).toFloat().add(80.0)),
1366
+ })));
1367
+ const stock = $.let(East.Array.generate(12n, Weekly, (_$, i) => ({
1368
+ at: week(i.add(27n)), value: some(i.toFloat().multiply(-22.0).add(420.0)),
1369
+ })));
1370
+ const output = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({
1371
+ week: week(i.add(27n)), v: i.multiply(23n).remainder(40n).toFloat().add(60.0),
1372
+ })));
1373
+ const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({
1374
+ week: week(i.add(27n)), v: i.multiply(13n).remainder(9n).toFloat().add(88.0),
1375
+ })));
1376
+ const base = { heat: noHeat, weights: noWeights, weekly: noWeekly, points: noPoints };
1377
+ const ops = $.const(new Map([
1378
+ ["load", { ...base, series: "load", label: "Line load %", heat: load }],
1379
+ ["peak", { ...base, series: "peak", label: "Peak load %", heat: load }],
1380
+ ["book", { ...base, series: "book", label: "Booked", weights: booked }],
1381
+ ["desp", { ...base, series: "desp", label: "Despatch t", weekly: tonnes }],
1382
+ ["stock", { ...base, series: "stock", label: "Closing stock t", weekly: stock }],
1383
+ ["out", { ...base, series: "out", label: "Output · t", points: output }],
1384
+ ["cov", { ...base, series: "cov", label: "Coverage %", points: coverage }],
1385
+ ]), DictType(StringType, OpsRow));
1386
+ const whole = $.const(Format.Number({ maximumFractionDigits: 0n }));
1387
+ const series = $.const([
1388
+ // A level — a month shows its weeks' MEAN (the default). The
1389
+ // declared format prints the folded values.
1390
+ Plan.series.heat(OpsRow, {
1391
+ key: "load", title: "Line load",
1392
+ match: r => r.series.equal("load"), label: r => r.label,
1393
+ cells: r => Plan.heatCells(r.heat, { min: 0, max: 100, format: whole }),
1394
+ }),
1395
+ // The same weeks, folded by their MAX — the peak a month hit.
1396
+ Plan.series.heat(OpsRow, {
1397
+ key: "peak", title: "Peak load",
1398
+ match: r => r.series.equal("peak"), label: r => r.label,
1399
+ cells: r => Plan.heatCells(r.heat, { min: 0, max: 100, fold: "max", format: whole }),
1400
+ }),
1401
+ // A fraction of each bucket booked — a month's is its weeks' mean.
1402
+ Plan.series.heat(OpsRow, {
1403
+ key: "book", title: "Booked",
1404
+ match: r => r.series.equal("book"), label: r => r.label,
1405
+ cells: r => Plan.weightCells(r.weights),
1406
+ }),
1407
+ // An amount — a month shows its weeks' SUM (the default).
1408
+ Plan.series.table(OpsRow, {
1409
+ key: "desp", title: "Despatch",
1410
+ match: r => r.series.equal("desp"), label: r => r.label,
1411
+ cells: r => Plan.tableCells(r.weekly), format: whole,
1412
+ }),
1413
+ // A balance — a month shows where it CLOSED, its last week.
1414
+ Plan.series.table(OpsRow, {
1415
+ key: "stock", title: "Closing stock",
1416
+ match: r => r.series.equal("stock"), label: r => r.label,
1417
+ cells: r => Plan.tableCells(r.weekly), fold: "last", format: whole,
1418
+ }),
1419
+ // Columns sum; a line averages.
1420
+ Plan.series.chart(OpsRow, {
1421
+ key: "out", title: "Output",
1422
+ match: r => r.series.equal("out"), label: r => r.label, id: true,
1423
+ layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.v })],
1424
+ }),
1425
+ Plan.series.chart(OpsRow, {
1426
+ key: "cov", title: "Coverage",
1427
+ match: r => r.series.equal("cov"), label: r => r.label, id: true,
1428
+ layers: r => [Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.v }), { fold: "mean" })],
1429
+ }),
1430
+ ], ArrayType(Plan.Types.Series(OpsRow)));
1431
+ // The resolution is the switch — two whole axis values, picked by key.
1432
+ const axes = $.const([
1433
+ { key: "month", axis: Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "month", now: week(31n) }) },
1434
+ { key: "week", axis: Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }) },
1435
+ ], ArrayType(StructType({ key: StringType, axis: Plan.Types.Axis })));
1436
+ const resBind = $.let(State.bind([StringType], "plan_fold_resolution", "month"));
1437
+ const resKey = $.let(resBind.read());
1438
+ const onRes = $.const(East.function([StringType], NullType, ($, next) => { $(resBind.write(next)); }));
1439
+ const sel = $.let(axes.filter((_$, a) => a.key.equal(resKey)).get(0n, _$ => axes.get(0n)));
1440
+ return (_jsxs(VStack, { gap: "2", align: "stretch", children: [_jsx(SegmentGroup, { value: resKey, onChange: onRes, size: "sm", items: [SegmentGroup.Item("month", _jsx(Text, { children: "MONTH" })), SegmentGroup.Item("week", _jsx(Text, { children: "WEEK" }))] }), _jsx(Plan, { axis: sel.axis, data: ops, series: series })] }));
1441
+ } }))),
1442
+ inputs: [],
1443
+ });
1136
1444
  export const planCardRows = example({
1137
- keywords: ["Plan", "data", "series", "cards", "Roster", "chip", "lifecycle", "confirmed", "recommended", "removed", "estimated", "icon", "popover", "stacked", "format", "axis", "group", "match", "gutter", "raw"],
1445
+ keywords: ["Plan", "data", "series", "cards", "Roster", "chip", "lifecycle", "confirmed", "recommended", "removed", "estimated", "icon", "popover", "stacked", "format", "axis", "section", "match", "gutter", "raw"],
1138
1446
  description: "Cards rows over one crew source — chip labels derived from hours × lifecycle, plus stored chip records",
1139
1447
  fn: East.function([], UIComponentType, ($) => {
1140
1448
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
@@ -1186,7 +1494,7 @@ export const planCardRows = example({
1186
1494
  return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
1187
1495
  }),
1188
1496
  }),
1189
- Plan.series.group(CrewRow, { key: "pool", label: "Relief pool", meta: "1 rs" }, [
1497
+ Plan.series.section(CrewRow, { key: "relief", title: "Relief pool", meta: "1 row" }, [
1190
1498
  Plan.series.cards(CrewRow, {
1191
1499
  key: "pool", title: "Pool",
1192
1500
  match: r => r.series.equal("pool"),
@@ -1209,7 +1517,7 @@ export const planCardRows = example({
1209
1517
  inputs: [],
1210
1518
  });
1211
1519
  export const planEventRows = example({
1212
- keywords: ["Plan", "data", "series", "events", "mark", "milestone", "decision", "exception", "markKind", "applied", "icon", "label", "popover", "group", "match", "stacked", "gutter", "raw"],
1520
+ keywords: ["Plan", "data", "series", "events", "mark", "milestone", "decision", "exception", "markKind", "applied", "icon", "label", "popover", "section", "match", "stacked", "gutter", "raw"],
1213
1521
  description: "Event rows over one stream source — milestone dots, decision diamonds, an exception, and a custom glyph",
1214
1522
  fn: East.function([], UIComponentType, ($) => {
1215
1523
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
@@ -1223,7 +1531,7 @@ export const planEventRows = example({
1223
1531
  marks: ArrayType(Plan.Types.EventMark),
1224
1532
  });
1225
1533
  const streams = $.const(new Map([
1226
- ["ms", { series: "main", name: "MILESTONES", sub: none, value: some("5"), marks: [
1534
+ ["ms", { series: "main", name: "Milestones", sub: none, value: some("5"), marks: [
1227
1535
  { key: "kick", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
1228
1536
  { key: "d1", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
1229
1537
  { key: "rel", at: Plan.at.time(week(33n)), kind: variant("milestone", null),
@@ -1231,22 +1539,22 @@ export const planEventRows = example({
1231
1539
  { key: "audit", at: Plan.at.time(week(35n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
1232
1540
  { key: "d2", at: Plan.at.time(week(37n)), kind: variant("decision", { applied: false }), icon: none, label: some("×3") },
1233
1541
  ] }],
1234
- ["release", { series: "programs", name: "RELEASES", sub: some("6-wk cadence"), value: none, marks: [
1542
+ ["release", { series: "programs", name: "Releases", sub: some("6-wk cadence"), value: none, marks: [
1235
1543
  { key: "r1", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("2.3") },
1236
1544
  { key: "r2", at: Plan.at.time(week(36n)), kind: variant("milestone", null), icon: none, label: some("2.4") },
1237
1545
  ] }],
1238
1546
  ]), DictType(StringType, StreamRow));
1239
1547
  const series = $.const([
1240
1548
  Plan.series.events(StreamRow, {
1241
- key: "main-2", title: "Main",
1549
+ key: "milestones", title: "Milestones",
1242
1550
  match: r => r.series.equal("main"),
1243
1551
  label: r => r.name, id: true,
1244
1552
  value: r => r.value,
1245
1553
  marks: r => r.marks,
1246
1554
  }),
1247
- Plan.series.group(StreamRow, { key: "programs", label: "Programs", meta: "1 rs" }, [
1555
+ Plan.series.section(StreamRow, { key: "programs", title: "Programs", meta: "1 row" }, [
1248
1556
  Plan.series.events(StreamRow, {
1249
- key: "programs", title: "Programs",
1557
+ key: "releases", title: "Releases",
1250
1558
  match: r => r.series.equal("programs"),
1251
1559
  label: r => r.name, id: true, stacked: true,
1252
1560
  sub: r => r.sub,
@@ -1267,8 +1575,8 @@ export const planEventRows = example({
1267
1575
  inputs: [],
1268
1576
  });
1269
1577
  export const planGroupedRows = example({
1270
- keywords: ["Plan", "data", "series", "group", "groups", "strip", "by", "discovered", "summary", "summaryAggregate", "collapsed", "groupBy", "heterogeneous", "match", "meta", "nesting", "raw"],
1271
- description: "Group strips in all three forms — static expanded, static collapsed, and discovered by a `by` accessor",
1578
+ keywords: ["Plan", "data", "series", "group", "groups", "strip", "summary", "summaryAggregate", "collapsed", "groupToDicts", "grouping", "data step", "children", "Plan.children", "step down", "member count", "heterogeneous", "match", "nesting", "raw"],
1579
+ description: "Group strips over grouped data — one `groupToDicts` makes each line an entry holding its rows, and each line's strip nests them, collapsed strips resting as their mean",
1272
1580
  fn: East.function([], UIComponentType, ($) => {
1273
1581
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1274
1582
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -1279,8 +1587,10 @@ export const planGroupedRows = example({
1279
1587
  key: StringType, batch: StringType,
1280
1588
  start: DateTimeType, end: DateTimeType, state: EventStateType,
1281
1589
  });
1590
+ // The RAW rows, flat — each names its line, and carries jobs, load
1591
+ // cells, or both.
1282
1592
  const LineRow = StructType({
1283
- series: StringType, line: StringType, label: StringType,
1593
+ line: StringType, label: StringType,
1284
1594
  jobs: ArrayType(JobRow),
1285
1595
  cells: ArrayType(Plan.Types.HeatCell),
1286
1596
  });
@@ -1290,62 +1600,51 @@ export const planGroupedRows = example({
1290
1600
  value: some(pcts.get(i)),
1291
1601
  label: some(East.Float.printFixed(pcts.get(i), 0n)),
1292
1602
  })));
1293
- const lines = $.const(new Map([
1294
- // Static Line 1 — mixed kinds (a span child + a heat child).
1295
- ["m03", { series: "l1span", line: "", label: "L1-M03",
1603
+ const rows = $.const(new Map([
1604
+ // Line 1 — mixed kinds: a machine's jobs and its load.
1605
+ ["m03", { line: "Line 1", label: "L1-M03",
1296
1606
  jobs: [{ key: "r", batch: "B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) }],
1297
1607
  cells: [] }],
1298
- ["m03h", { series: "l1heat", line: "", label: "L1-M03 load", jobs: [], cells }],
1299
- // Static collapsed Line 2 — rests as its DECLARED mean strip.
1300
- ["l2", { series: "l2", line: "", label: "L2 load", jobs: [], cells }],
1301
- // The DISCOVERED form — one strip per distinct line value. Named
1302
- // apart from the static strips above so the panel reads as three
1303
- // distinct forms rather than two repeated ones.
1304
- ["d-m21", { series: "byline", line: "Line 3", label: "L3-M21", jobs: [], cells }],
1305
- ["d-m22", { series: "byline", line: "Line 3", label: "L3-M22", jobs: [], cells }],
1306
- ["d-m31", { series: "byline", line: "Line 4", label: "L4-M31", jobs: [], cells }],
1608
+ ["m03h", { line: "Line 1", label: "L1-M03 load", jobs: [], cells }],
1609
+ ["l2", { line: "Line 2", label: "L2 load", jobs: [], cells }],
1610
+ ["m21", { line: "Line 3", label: "L3-M21", jobs: [], cells }],
1611
+ ["m22", { line: "Line 3", label: "L3-M22", jobs: [], cells }],
1612
+ ["m31", { line: "Line 4", label: "L4-M31", jobs: [], cells }],
1307
1613
  ]), DictType(StringType, LineRow));
1614
+ // Grouping is a DATA step (#822): one `groupToDicts` makes each line an
1615
+ // entry holding its rows, keyed as the source keys them. A strip nests
1616
+ // exactly what its entry holds, so it reads the same inline or paged.
1617
+ const lines = $.let(rows.groupToDicts(($, r) => r.line, ($, _r, k) => k));
1618
+ const LineGroup = DictType(StringType, LineRow);
1308
1619
  const series = $.const([
1309
- Plan.series.group(LineRow, { key: "line1", label: "Line 1", meta: "2 rs · 82%" }, [
1310
- Plan.series.span(LineRow, {
1311
- key: "l1span", title: "L1span",
1312
- match: r => r.series.equal("l1span"),
1313
- label: r => r.label, id: true,
1314
- runs: r => r.jobs.map((_$, j) => Plan.run({
1315
- key: j.key, start: j.start, end: j.end,
1316
- label: East.str `RUN · ${j.batch}`, state: j.state,
1317
- })),
1318
- }),
1319
- Plan.series.heat(LineRow, {
1320
- key: "l1heat", title: "L1heat",
1321
- match: r => r.series.equal("l1heat"),
1322
- label: r => r.label,
1323
- cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1324
- }),
1325
- ]),
1326
- Plan.series.group(LineRow, { key: "line2", label: "Line 2", value: "98%", status: "warning", collapsed: true, summaryAggregate: "mean" }, [
1327
- Plan.series.heat(LineRow, {
1328
- key: "l2", title: "L2",
1329
- match: r => r.series.equal("l2"),
1330
- label: r => r.label,
1331
- cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
1332
- }),
1333
- ]),
1334
- // DISCOVERED strips — one collapsed group per distinct `by`
1335
- // value, wearing the member-count meta.
1336
- Plan.series.group(LineRow, {
1337
- key: "discovered", title: "Discovered lines",
1338
- by: r => r.line, match: r => r.series.equal("byline"),
1339
- collapsed: true, summaryAggregate: "mean",
1340
- }, [
1341
- Plan.series.heat(LineRow, {
1342
- key: "byline", title: "By line",
1343
- match: r => r.series.equal("byline"),
1344
- label: r => r.label, id: true,
1345
- cells: r => Plan.heatCells(r.cells),
1346
- }),
1347
- ]),
1348
- ], ArrayType(Plan.Types.Series(LineRow)));
1620
+ // One strip PER LINE, its members the line's rows — stepped down
1621
+ // into (`Plan.children`) and laid out like a top-level list: the
1622
+ // jobs block, then the load block. Line 1 rests open; the others
1623
+ // rest as their DECLARED mean strip, wearing their member count.
1624
+ Plan.series.group(LineGroup, {
1625
+ key: "lines", title: "Lines",
1626
+ label: (_g, line) => line,
1627
+ collapsed: (_g, line) => line.equal("Line 1").not(),
1628
+ summaryAggregate: "mean",
1629
+ children: Plan.children((g) => g, [
1630
+ Plan.series.span(LineRow, {
1631
+ key: "line-jobs", title: "Jobs",
1632
+ match: r => r.jobs.size().greater(0n),
1633
+ label: r => r.label, id: true,
1634
+ runs: r => r.jobs.map((_$, j) => Plan.run({
1635
+ key: j.key, start: j.start, end: j.end,
1636
+ label: East.str `RUN · ${j.batch}`, state: j.state,
1637
+ })),
1638
+ }),
1639
+ Plan.series.heat(LineRow, {
1640
+ key: "line-load", title: "Load",
1641
+ match: r => r.cells.size().greater(0n),
1642
+ label: r => r.label,
1643
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1644
+ }),
1645
+ ]),
1646
+ }),
1647
+ ], ArrayType(Plan.Types.Series(LineGroup)));
1349
1648
  const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1350
1649
  return (_jsx(Plan, { axis: axis, data: lines, series: series }));
1351
1650
  }),
@@ -1355,8 +1654,8 @@ export const planGroupedRows = example({
1355
1654
  // planSeriesData — the minimal data + series introduction
1356
1655
  // ============================================================================
1357
1656
  export const planSeriesData = example({
1358
- keywords: ["Plan", "data", "series", "match", "series", "variant", "span", "cards", "group", "rows", "groupBy", "rollup", "Series", "data-driven", "accessor", "raw", "one source"],
1359
- description: "The data + series canvas, minimally — one raw source and one `Plan.series.*` entry per row series",
1657
+ keywords: ["Plan", "data", "series", "match", "variant", "span", "cards", "group", "rows", "groupToDicts", "grouping", "data step", "children", "Plan.children", "step down", "nesting", "rollup", "Series", "data-driven", "accessor", "raw", "one source", "layout"],
1658
+ description: "The data + series canvas, minimally — one raw source grouped into blocks in one data step, and one `Plan.series.*` entry per block, the list the layout",
1360
1659
  fn: East.function([], UIComponentType, ($) => {
1361
1660
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1362
1661
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -1396,61 +1695,203 @@ export const planSeriesData = example({
1396
1695
  { key: "s2", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
1397
1696
  ] }) }],
1398
1697
  ]), DictType(StringType, OpsRow));
1698
+ // Hierarchy is the DATA's (#822): one `groupToDicts` groups the rows
1699
+ // into the canvas's blocks — each machine under its line, the crews
1700
+ // under one "Crews" block. An entry of the result holds its rows.
1701
+ const blocks = $.let(ops.groupToDicts(($, r) => r.kind.hasTag("crew").ifElse(() => "Crews", () => r.line), ($, _r, k) => k));
1702
+ const Block = DictType(StringType, OpsRow);
1399
1703
  // The series — real East values bound in the body, typed by the
1400
- // constructor; canvas order = series order. The accessors are where
1401
- // raw fields become canvas vocabulary: labels, quantity displays and
1402
- // chip text all derive CLIENT-SIDE inside each series's stored make.
1704
+ // constructor. The list IS the layout: one block per series, top to
1705
+ // bottom. The accessors are where raw fields become canvas vocabulary:
1706
+ // labels, quantity displays and chip text all derive CLIENT-SIDE,
1707
+ // inside each series' `derive`.
1403
1708
  const series = $.const([
1404
- Plan.series.rows(OpsRow, { key: "chrome", title: "Milestones", subtitle: "one-off chrome" }, [Plan.events({ key: "ms", label: "MILESTONES", id: true, marks: [
1709
+ // One row per line, its machines stepped down into
1710
+ // (`Plan.children`) and their runs rolled up into its bands —
1711
+ // which sum the runs' quantities, unit by unit.
1712
+ Plan.series.span(Block, {
1713
+ key: "lines", title: "Lines",
1714
+ match: (_b, name) => name.equal("Crews").not(),
1715
+ label: (_b, name) => name,
1716
+ runs: _b => [],
1717
+ rollup: "union",
1718
+ children: Plan.children((b) => b, [
1719
+ Plan.series.span(OpsRow, {
1720
+ key: "machines", title: "Machines",
1721
+ match: r => r.kind.hasTag("machine"),
1722
+ label: (_r, k) => k, id: true,
1723
+ runs: r => r.kind.unwrap("machine").jobs.map((_$, j) => Plan.run({
1724
+ key: j.batch, start: j.start, end: j.end,
1725
+ label: East.str `RUN · ${j.batch}`,
1726
+ // A quantity is one value: the bar prints `96 t`,
1727
+ // and the line's band sums the tonnes.
1728
+ quantity: Plan.quantity(j.tonnes, { unit: "t", format: Format.Number({ maximumFractionDigits: 0n }) }),
1729
+ state: j.state,
1730
+ })),
1731
+ }),
1732
+ ]),
1733
+ }),
1734
+ // One strip per matching block — here the one "Crews" block,
1735
+ // wearing its member count.
1736
+ Plan.series.group(Block, {
1737
+ key: "crews", title: "Crews",
1738
+ match: (_b, name) => name.equal("Crews"),
1739
+ label: (_b, name) => name,
1740
+ children: Plan.children((b) => b, [
1741
+ Plan.series.cards(OpsRow, {
1742
+ key: "crew-shifts", title: "Crew shifts",
1743
+ match: r => r.kind.hasTag("crew"),
1744
+ label: (_r, k) => k,
1745
+ chips: r => r.kind.unwrap("crew").shifts.map(($, s) => {
1746
+ const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
1747
+ // `+` marks ADDED hours — a removed proposal keeps the
1748
+ // plain figure (see planCardRows for the full ladder).
1749
+ const label = $.let(s.state.match({
1750
+ proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
1751
+ }, _$ => East.str `${hrs}h`), StringType);
1752
+ return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
1753
+ }),
1754
+ }),
1755
+ ]),
1756
+ }),
1757
+ Plan.series.rows(Block, { key: "chrome", title: "Milestones", subtitle: "one-off chrome" }, [Plan.events({ key: "ms", label: "Milestones", id: true, marks: [
1405
1758
  Plan.mark({ key: "kick", at: week(28n), kind: "milestone", label: "KICKOFF" }),
1406
1759
  Plan.mark({ key: "rel", at: week(33n), kind: "milestone", label: "REL 2.4" }),
1407
1760
  ] })]),
1408
- Plan.series.span(OpsRow, {
1409
- key: "span-2", title: "Span",
1410
- match: r => r.kind.hasTag("machine"),
1411
- label: (_r, k) => k, id: true,
1412
- runs: r => r.kind.unwrap("machine").jobs.map((_$, j) => Plan.run({
1413
- key: j.batch, start: j.start, end: j.end,
1414
- label: East.str `RUN · ${j.batch}`,
1415
- quantity: East.str `${East.Float.printFixed(j.tonnes, 0n)} t`,
1416
- qty: j.tonnes, state: j.state,
1417
- })),
1418
- groupBy: [r => r.line], rollup: "union", unit: "t",
1419
- }),
1420
- Plan.series.group(OpsRow, { key: "crews", label: "Crews", meta: "1 rs" }, [
1421
- Plan.series.cards(OpsRow, {
1422
- key: "cards-2", title: "Cards",
1423
- match: r => r.kind.hasTag("crew"),
1424
- label: (_r, k) => k,
1425
- chips: r => r.kind.unwrap("crew").shifts.map(($, s) => {
1426
- const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
1427
- // `+` marks ADDED hours — a removed proposal keeps the
1428
- // plain figure (see planCardRows for the full ladder).
1429
- const label = $.let(s.state.match({
1430
- proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
1431
- }, _$ => East.str `${hrs}h`), StringType);
1432
- return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
1433
- }),
1434
- }),
1435
- ]),
1436
- ], ArrayType(Plan.Types.Series(OpsRow)));
1761
+ ], ArrayType(Plan.Types.Series(Block)));
1437
1762
  const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1438
- return (_jsx(Plan, { axis: axis, data: ops, series: series }));
1763
+ return (_jsx(Plan, { axis: axis, data: blocks, series: series }));
1439
1764
  }),
1440
1765
  inputs: [],
1441
1766
  });
1442
1767
  // ============================================================================
1443
- // planSeriesLibrary — the pickable series library (the DnD behavioral isolate)
1768
+ // planLiteralRows — the kind factories: rows no dataset holds
1769
+ // ============================================================================
1770
+ export const planLiteralRows = example({
1771
+ keywords: [
1772
+ "Plan", "span", "Plan.span", "literal", "rows", "series.rows", "chrome", "one-off",
1773
+ "kind factory", "subtree", "nested", "parent", "rollup", "union", "bands", "run", "layout",
1774
+ ],
1775
+ description: "Literal rows — `Plan.span` builds a one-off subtree (a shutdown parent rolling up two trades' runs) that `Plan.series.rows` places beside the data-driven series",
1776
+ fn: East.function([], UIComponentType, ($) => {
1777
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1778
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1779
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1780
+ return w1.addWeeks(n.subtract(1n));
1781
+ }));
1782
+ const JobRow = StructType({
1783
+ batch: StringType, start: DateTimeType, end: DateTimeType, state: EventStateType,
1784
+ });
1785
+ const MachineRow = StructType({ jobs: ArrayType(JobRow) });
1786
+ const machines = $.const(new Map([
1787
+ ["L1-M03", { jobs: [{ batch: "B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) }] }],
1788
+ ["L1-M04", { jobs: [{ batch: "B-208", start: week(27n), end: week(30n), state: variant("actual", null) }] }],
1789
+ ]), DictType(StringType, MachineRow));
1790
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1791
+ return (_jsx(Plan, { axis: axis, data: machines, series: [
1792
+ Plan.series.span(MachineRow, {
1793
+ key: "machines", title: "Machines",
1794
+ label: (_r, k) => k, id: true,
1795
+ runs: r => r.jobs.map((_$, j) => Plan.run({
1796
+ key: j.batch, start: j.start, end: j.end,
1797
+ label: East.str `RUN · ${j.batch}`, state: j.state,
1798
+ })),
1799
+ }),
1800
+ // Rows no dataset holds — the planned shutdown, written out once.
1801
+ // `Plan.span` nests: the parent DECLARES its rollup and the canvas
1802
+ // derives the band from its two rows' runs. The series list is the
1803
+ // layout, so this block sits below the machines.
1804
+ Plan.series.rows(MachineRow, { key: "works", title: "Planned works", subtitle: "literal rows" }, [
1805
+ Plan.span({
1806
+ key: "shutdown", label: "Shutdown", rollup: "union", rows: [
1807
+ Plan.span({ key: "elec", label: "Electrical", runs: [
1808
+ Plan.run({ key: "iso", start: week(33n), end: week(34n), label: "ISOLATE", state: "confirmed" }),
1809
+ ] }),
1810
+ Plan.span({ key: "mech", label: "Mechanical", runs: [
1811
+ Plan.run({ key: "reline", start: week(34n), end: week(36n), label: "RELINE", state: "recommended" }),
1812
+ ] }),
1813
+ ],
1814
+ }),
1815
+ ]),
1816
+ ] }));
1817
+ }),
1818
+ inputs: [],
1819
+ });
1820
+ // ============================================================================
1821
+ // planPick — the series library, minimally (#590)
1822
+ // ============================================================================
1823
+ export const planPick = example({
1824
+ keywords: [
1825
+ "Plan", "pick", "Plan.pick", "Pick", "library", "panel", "series", "hidden",
1826
+ "toggle", "eye", "show", "hide", "choose", "persisted", "Reactive", "State", "#590",
1827
+ ],
1828
+ description: "The series library, minimally — `Plan.pick` binds which series show, and `<Plan pick>` mounts the library beside the canvas",
1829
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
1830
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1831
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1832
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1833
+ return w1.addWeeks(n.subtract(1n));
1834
+ }));
1835
+ const JobRow = StructType({
1836
+ batch: StringType, start: DateTimeType, end: DateTimeType, state: EventStateType,
1837
+ });
1838
+ const OpsRow = StructType({
1839
+ series: StringType, jobs: ArrayType(JobRow), cells: ArrayType(Plan.Types.HeatCell),
1840
+ });
1841
+ const noJobs = $.const([], ArrayType(JobRow));
1842
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
1843
+ const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
1844
+ const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
1845
+ at: Plan.at.time(week(i.multiply(2n).add(27n))), value: some(pcts.get(i)), label: none,
1846
+ })));
1847
+ const ops = $.const(new Map([
1848
+ ["L1-M03", { series: "machines", cells: noCells,
1849
+ jobs: [{ batch: "B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) }] }],
1850
+ ["L1-M04", { series: "machines", cells: noCells,
1851
+ jobs: [{ batch: "B-208", start: week(27n), end: week(30n), state: variant("actual", null) }] }],
1852
+ ["L2-load", { series: "load", cells, jobs: noJobs }],
1853
+ ]), DictType(StringType, OpsRow));
1854
+ // Every series that COULD show — the library lists these, and the
1855
+ // canvas shows the ones switched on in this order.
1856
+ const all = $.const([
1857
+ Plan.series.span(OpsRow, {
1858
+ key: "machines", title: "Machine jobs", subtitle: "one row per machine",
1859
+ match: r => r.series.equal("machines"),
1860
+ label: (_r, k) => k, id: true,
1861
+ runs: r => r.jobs.map((_$, j) => Plan.run({
1862
+ key: j.batch, start: j.start, end: j.end,
1863
+ label: East.str `RUN · ${j.batch}`, state: j.state,
1864
+ })),
1865
+ }),
1866
+ Plan.series.heat(OpsRow, {
1867
+ key: "load", title: "Line load", subtitle: "% per fortnight",
1868
+ match: r => r.series.equal("load"),
1869
+ label: (_r, k) => k,
1870
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1871
+ }),
1872
+ ], ArrayType(Plan.Types.Series(OpsRow)));
1873
+ // The handle is STATE — which series are switched off, persisted
1874
+ // under its key — so it lives inside the Reactive. "load" starts off.
1875
+ const shown = $.let(Plan.pick("ex.plan.pick", all, { hidden: ["load"] }));
1876
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1877
+ // `pick` REPLACES `series`: the canvas shows the picked series and
1878
+ // mounts the library itself, so nothing else is wired.
1879
+ return (_jsx(Plan, { axis: axis, data: ops, pick: shown }));
1880
+ } }))),
1881
+ inputs: [],
1882
+ });
1883
+ // ============================================================================
1884
+ // planLibraryDnd — the pickable series library over every row kind
1444
1885
  // ============================================================================
1445
1886
  export const planLibraryDnd = example({
1446
1887
  keywords: [
1447
1888
  "Plan", "data", "series", "library", "Pick", "pick", "Panel", "pickItems", "hidden",
1448
- "toggle", "eye", "kind icon", "count", "group", "grouped", "discovered", "by", "rows",
1889
+ "toggle", "eye", "kind icon", "group", "section", "views", "nested", "children", "rows",
1449
1890
  "chrome", "span", "buckets", "chart", "heat", "table", "cards", "events", "duplicate",
1450
- "keySuffix", "keyPrefix", "same entity", "multiple views", "adjacent", "seek",
1451
- "DnD", "drag", "drop", "onDrag", "canDrop", "sources", "add", "Reactive", "State", "#590",
1891
+ "same entity", "multiple views", "adjacent", "seek", "layout", "order",
1892
+ "Reactive", "State", "#590",
1452
1893
  ],
1453
- description: "The series library across every row kind — duplicate kinds, groups, and a `keySuffix` pair showing one asset three ways",
1894
+ description: "The series library across every row kind — duplicate kinds, sections, a group per entry, and a `views` series showing one asset three ways",
1454
1895
  fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
1455
1896
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1456
1897
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -1468,11 +1909,10 @@ export const planLibraryDnd = example({
1468
1909
  const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
1469
1910
  // ONE flat source (the `planExpand` shape): `pick` names the series
1470
1911
  // that claims the row, and every other channel is empty for the
1471
- // series that do not use it. Each series therefore matches a
1472
- // DISJOINT slice — two series over the same rows would emit the same
1473
- // keys and collapse last-wins, which is a different demo.
1474
- const OpsRow = StructType({
1475
- pick: StringType, label: StringType, line: StringType,
1912
+ // series that do not use it. A program holds its members
1913
+ // (`members`), so the entry type is recursive.
1914
+ const OpsRow = RecursiveType((self) => StructType({
1915
+ pick: StringType, label: StringType,
1476
1916
  jobs: ArrayType(JobRow),
1477
1917
  points: ArrayType(MeasureRow),
1478
1918
  cells: ArrayType(Plan.Types.HeatCell),
@@ -1480,7 +1920,8 @@ export const planLibraryDnd = example({
1480
1920
  nums: ArrayType(Plan.Types.TableCell),
1481
1921
  shifts: ArrayType(ShiftRow),
1482
1922
  marks: ArrayType(Plan.Types.EventMark),
1483
- });
1923
+ members: DictType(StringType, self),
1924
+ }));
1484
1925
  const noJobs = $.const([], ArrayType(JobRow));
1485
1926
  const noPoints = $.const([], ArrayType(MeasureRow));
1486
1927
  const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
@@ -1488,6 +1929,7 @@ export const planLibraryDnd = example({
1488
1929
  const noNums = $.const([], ArrayType(Plan.Types.TableCell));
1489
1930
  const noShifts = $.const([], ArrayType(ShiftRow));
1490
1931
  const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
1932
+ const noMembers = $.const(new Map(), DictType(StringType, OpsRow));
1491
1933
  const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
1492
1934
  const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
1493
1935
  const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
@@ -1503,126 +1945,127 @@ export const planLibraryDnd = example({
1503
1945
  const RUNNING = variant("in-progress", null);
1504
1946
  const PROPOSED = variant("proposed", variant("recommended", null));
1505
1947
  const base = {
1506
- line: "", jobs: noJobs, points: noPoints, cells: noCells,
1507
- allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks,
1948
+ jobs: noJobs, points: noPoints, cells: noCells,
1949
+ allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks, members: noMembers,
1508
1950
  };
1509
- // Keys ORDER the canvas (#568), so they are numbered to place each
1510
- // series' rows; group strips sort among the ROOTS by their own key.
1951
+ // The keys are the entries' identities; the LAYOUT is the series
1952
+ // list below (#822).
1511
1953
  const ops = $.const(new Map([
1512
- ["10-util", { ...base, pick: "util", label: "UTIL %", points }],
1513
- // ONE asset, THREE views — the span the machines series draws,
1514
- // plus a chart and a table over these same two rows (see the
1515
- // `keySuffix` pair below). The extra channels are what those
1516
- // two read; nothing about the row is duplicated.
1517
- ["20-m03", { ...base, pick: "machines", label: "L1-M03",
1954
+ ["util", { ...base, pick: "util", label: "Util %", points }],
1955
+ // ONE asset, THREE views — the `views` series below gives each
1956
+ // machine a jobs row, a utilisation chart and a tonnes table,
1957
+ // from these channels; nothing about the row is duplicated.
1958
+ ["m03", { ...base, pick: "machines", label: "L1-M03",
1518
1959
  jobs: [{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: RUNNING }],
1519
1960
  points,
1520
1961
  nums: [
1521
1962
  { at: Plan.at.time(week(28n)), value: some(96.0), text: none, tone: none },
1522
1963
  { at: Plan.at.time(week(31n)), value: some(88.0), text: none, tone: none },
1523
1964
  ] }],
1524
- ["20-m04", { ...base, pick: "machines", label: "L1-M04",
1965
+ ["m04", { ...base, pick: "machines", label: "L1-M04",
1525
1966
  jobs: [{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: ACTUAL }],
1526
1967
  points,
1527
1968
  nums: [
1528
1969
  { at: Plan.at.time(week(28n)), value: some(112.0), text: none, tone: none },
1529
1970
  { at: Plan.at.time(week(31n)), value: some(-24.0), text: none, tone: none },
1530
1971
  ] }],
1531
- // SAME KIND as the machines series, different entry — a kind is
1972
+ // SAME KIND as the machines' jobs, different entry — a kind is
1532
1973
  // not an identity, which is why the library keys on `key`.
1533
- ["25-c01", { ...base, pick: "contract", label: "CON-01",
1974
+ ["c01", { ...base, pick: "contract", label: "CON-01",
1534
1975
  jobs: [{ key: "c1", label: "RUN · C-1", start: week(30n), end: week(34n), state: CONFIRMED }] }],
1535
- ["30-load", { ...base, pick: "load", label: "L2 load", cells }],
1536
- ["35-qual", { ...base, pick: "quality", label: "Quality", cells }],
1537
- ["40-dock2", { ...base, pick: "docks", label: "Dock 2",
1976
+ ["load", { ...base, pick: "load", label: "L2 load", cells }],
1977
+ ["qual", { ...base, pick: "quality", label: "Quality", cells }],
1978
+ ["dock2", { ...base, pick: "docks", label: "Dock 2",
1538
1979
  allocs: [
1539
1980
  { key: "a1", at: week(28n), state: variant("confirmed", null) },
1540
1981
  { key: "a2", at: week(31n), state: variant("proposed", variant("recommended", null)) },
1541
1982
  ] }],
1542
- ["50-desp", { ...base, pick: "table", label: "Despatch t",
1983
+ ["desp", { ...base, pick: "table", label: "Despatch t",
1543
1984
  nums: [
1544
1985
  { at: Plan.at.time(week(28n)), value: some(128.0), text: none, tone: none },
1545
1986
  { at: Plan.at.time(week(30n)), value: some(-96.0), text: none, tone: none },
1546
1987
  ] }],
1547
- ["60-crewA", { ...base, pick: "cards", label: "Crew A",
1988
+ ["crewA", { ...base, pick: "cards", label: "Crew A",
1548
1989
  shifts: [
1549
1990
  { key: "s1", from: week(27n), to: week(29n), label: "80h", state: variant("confirmed", null) },
1550
1991
  { key: "s2", from: week(31n), to: week(33n), label: "+64h", state: variant("proposed", variant("recommended", null)) },
1551
1992
  ] }],
1552
- ["70-ms", { ...base, pick: "events", label: "MILESTONES",
1993
+ ["ms", { ...base, pick: "events", label: "Milestones",
1553
1994
  marks: [
1554
1995
  { key: "k", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
1555
1996
  { key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
1556
1997
  ] }],
1557
- // Members of the three STATIC groups — they hang under their
1558
- // strip, so their keys only order within that subtree.
1559
- ["g1-m11", { ...base, pick: "gspan", label: "L3-M11",
1998
+ // Members of the three sections — each section lays its series
1999
+ // out under its header.
2000
+ ["m11", { ...base, pick: "gspan", label: "L3-M11",
1560
2001
  jobs: [{ key: "b301", label: "RUN · B-301", start: week(29n), end: week(33n), state: CONFIRMED }] }],
1561
- ["g1-m12", { ...base, pick: "gspan", label: "L3-M12",
2002
+ ["m12", { ...base, pick: "gspan", label: "L3-M12",
1562
2003
  jobs: [{ key: "b302", label: "RUN · B-302", start: week(31n), end: week(36n), state: ACTUAL }] }],
1563
- ["g2-l1", { ...base, pick: "gheat", label: "L4 load", cells }],
1564
- ["g3-d5", { ...base, pick: "gbuckets", label: "Dock 5",
2004
+ ["l4", { ...base, pick: "gheat", label: "L4 load", cells }],
2005
+ ["d5", { ...base, pick: "gbuckets", label: "Dock 5",
1565
2006
  allocs: [{ key: "a3", at: week(33n), state: variant("confirmed", null) }] }],
1566
- // The DISCOVERED group's members — one strip per `line`.
1567
- ["p1-a", { ...base, pick: "programs", label: "PR-A1", line: "Program A",
1568
- jobs: [{ key: "p1", label: "RUN · P-1", start: week(28n), end: week(32n), state: CONFIRMED }] }],
1569
- ["p2-a", { ...base, pick: "programs", label: "PR-A2", line: "Program A",
1570
- jobs: [{ key: "p2", label: "RUN · P-2", start: week(33n), end: week(37n), state: PROPOSED }] }],
1571
- ["p3-b", { ...base, pick: "programs", label: "PR-B1", line: "Program B",
1572
- jobs: [{ key: "p3", label: "RUN · P-3", start: week(30n), end: week(35n), state: ACTUAL }] }],
2007
+ // Two PROGRAMS, each holding its runs — one strip per program,
2008
+ // its members nested in it.
2009
+ ["Program A", { ...base, pick: "programs", label: "Program A",
2010
+ members: new Map([
2011
+ ["p1", { ...base, pick: "programs", label: "PR-A1",
2012
+ jobs: [{ key: "p1", label: "RUN · P-1", start: week(28n), end: week(32n), state: CONFIRMED }] }],
2013
+ ["p2", { ...base, pick: "programs", label: "PR-A2",
2014
+ jobs: [{ key: "p2", label: "RUN · P-2", start: week(33n), end: week(37n), state: PROPOSED }] }],
2015
+ ]) }],
2016
+ ["Program B", { ...base, pick: "programs", label: "Program B",
2017
+ members: new Map([
2018
+ ["p3", { ...base, pick: "programs", label: "PR-B1",
2019
+ jobs: [{ key: "p3", label: "RUN · P-3", start: week(30n), end: week(35n), state: ACTUAL }] }],
2020
+ ]) }],
1573
2021
  ]), DictType(StringType, OpsRow));
1574
- // The whole library: every kind once, two kinds TWICE, each of three
1575
- // static groups wrapping a different kind, the discovered form, and
1576
- // literal chrome. Fourteen entries, nine distinct arms.
2022
+ // The whole library, in layout order: every kind once, two kinds
2023
+ // TWICE, a views series, three sections each wrapping a different
2024
+ // kind, a group per entry, and literal chrome. Fourteen entries,
2025
+ // eleven distinct arms.
1577
2026
  const all = $.const([
1578
2027
  // Literal one-off chrome — it names itself, so it can be
1579
2028
  // switched off like anything else.
1580
- Plan.series.rows(OpsRow, { key: "chrome", title: "Section header", subtitle: "literal chrome" }, [Plan.events({ key: "00-hdr", label: "PLAN", id: true })]),
2029
+ Plan.series.rows(OpsRow, { key: "chrome", title: "Section header", subtitle: "literal chrome" }, [Plan.events({ key: "hdr", label: "Plan", id: true })]),
1581
2030
  Plan.series.chart(OpsRow, {
1582
2031
  key: "util", title: "Utilisation", subtitle: "% per fortnight",
1583
2032
  match: r => r.pick.equal("util"),
1584
2033
  label: r => r.label, id: true, height: "spark",
1585
2034
  layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.pct })],
1586
2035
  }),
1587
- Plan.series.span(OpsRow, {
1588
- key: "machines", title: "Machine jobs", subtitle: "one row per machine",
2036
+ // ── THE SAME ASSET, SEEN THREE WAYS ───────────────────────
2037
+ // A `views` series gives each machine one row per member
2038
+ // series, ADJACENT and in this order — its jobs, its
2039
+ // utilisation, its tonnes — and a seek on a machine lands on
2040
+ // its first view row. Each row's id is its member series and
2041
+ // the machine's key, so all three are addressable apart.
2042
+ Plan.series.views(OpsRow, {
2043
+ key: "machines", title: "Machines", subtitle: "one asset, three views",
1589
2044
  match: r => r.pick.equal("machines"),
1590
- label: r => r.label, id: true,
1591
- runs: r => r.jobs.map((_$, j) => Plan.run({
1592
- key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
1593
- })),
1594
- }),
1595
- // ── THE SAME ASSET, SEEN TWO MORE WAYS ────────────────────
1596
- // Both of these match the machines slice the span series
1597
- // already claims, so all three emit a row per machine. Without
1598
- // an affix they would land on one key and the last would
1599
- // silently replace the others (#568's union is last-wins).
1600
- //
1601
- // `keySuffix` separates them while keeping the DATA key first,
1602
- // which is the whole point: the canvas is in key order, so an
1603
- // asset's three views sit ADJACENT (`20-m03`, `20-m03/chart`,
1604
- // `20-m03/table`) instead of the series banking together — and
1605
- // seek still lands, because it positions on the first row
1606
- // at-or-after the sought key. A `keyPrefix` would do neither.
1607
- Plan.series.chart(OpsRow, {
1608
- key: "machine-util", title: "Machine · utilisation", subtitle: "same asset, 2nd view",
1609
- match: r => r.pick.equal("machines"),
1610
- keySuffix: "/chart",
2045
+ }, [
2046
+ Plan.series.span(OpsRow, {
2047
+ key: "machine-jobs", title: "Machine jobs",
2048
+ label: r => r.label, id: true,
2049
+ runs: r => r.jobs.map((_$, j) => Plan.run({
2050
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
2051
+ })),
2052
+ }),
1611
2053
  // The label stays the ASSET — all three rows are the same
1612
2054
  // machine, and pretending otherwise would hide that. What
1613
2055
  // distinguishes them is the VIEW, which is what the gutter
1614
2056
  // sub-line is for.
1615
- label: r => r.label, stacked: true, sub: _r => some("utilisation %"), height: "spark",
1616
- layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
1617
- }),
1618
- Plan.series.table(OpsRow, {
1619
- key: "machine-tonnes", title: "Machine · tonnes", subtitle: "same asset, 3rd view",
1620
- match: r => r.pick.equal("machines"),
1621
- keySuffix: "/table",
1622
- label: r => r.label, stacked: true, sub: _r => some("tonnes · plan Δ"),
1623
- cells: r => r.nums,
1624
- format: Format.Number({ maximumFractionDigits: 0n }),
1625
- }),
2057
+ Plan.series.chart(OpsRow, {
2058
+ key: "machine-util", title: "Machine · utilisation",
2059
+ label: r => r.label, stacked: true, sub: _r => some("utilisation %"), height: "spark",
2060
+ layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
2061
+ }),
2062
+ Plan.series.table(OpsRow, {
2063
+ key: "machine-tonnes", title: "Machine · tonnes",
2064
+ label: r => r.label, stacked: true, sub: _r => some("tonnes · plan Δ"),
2065
+ cells: r => r.nums,
2066
+ format: Format.Number({ maximumFractionDigits: 0n }),
2067
+ }),
2068
+ ]),
1626
2069
  Plan.series.span(OpsRow, {
1627
2070
  key: "contract", title: "Contractor jobs", subtitle: "same KIND, own entry",
1628
2071
  match: r => r.pick.equal("contract"),
@@ -1668,19 +2111,16 @@ export const planLibraryDnd = example({
1668
2111
  label: r => r.label, id: true,
1669
2112
  marks: r => r.marks,
1670
2113
  }),
1671
- // STATIC groups — each wrapping a DIFFERENT kind. The strip's
1672
- // own key / label / meta ARE the library entry's identity, so
1673
- // "Line 3" is the thing a person picks and switching it off
1674
- // takes its whole subtree with it.
2114
+ // SECTIONS — each wrapping a DIFFERENT kind. A section is the
2115
+ // unit a person picks — "Line 3" — and switching it off takes
2116
+ // its whole block with it, because its members are built by its
2117
+ // own `derive`.
1675
2118
  //
1676
- // The metas are terse because they are read TWICE: as the
1677
- // strip's meta line in the gutter (which is 168px and truncates
1678
- // the label if the meta crowds it) and as the entry's subtitle
1679
- // in the panel. Borrowing identity means one string serves both
1680
- // — the cost of the group needing nothing extra written.
1681
- Plan.series.group(OpsRow, { key: "g1-line3", label: "Line 3", meta: "span" }, [
2119
+ // The metas are terse because the gutter is 168px and truncates
2120
+ // the title if the meta crowds it.
2121
+ Plan.series.section(OpsRow, { key: "line3", title: "Line 3", subtitle: "a section of spans", meta: "span" }, [
1682
2122
  Plan.series.span(OpsRow, {
1683
- key: "gspan", title: "Grouped span", subtitle: "member",
2123
+ key: "gspan", title: "Line 3 jobs", subtitle: "member",
1684
2124
  match: r => r.pick.equal("gspan"),
1685
2125
  label: r => r.label, id: true,
1686
2126
  runs: r => r.jobs.map((_$, j) => Plan.run({
@@ -1688,52 +2128,50 @@ export const planLibraryDnd = example({
1688
2128
  })),
1689
2129
  }),
1690
2130
  ]),
1691
- Plan.series.group(OpsRow, { key: "g2-lines", label: "Load", meta: "heat" }, [
2131
+ Plan.series.section(OpsRow, { key: "loads", title: "Load", subtitle: "a section of heat", meta: "heat" }, [
1692
2132
  Plan.series.heat(OpsRow, {
1693
- key: "gheat", title: "Grouped heat", subtitle: "member",
2133
+ key: "gheat", title: "Load rows", subtitle: "member",
1694
2134
  match: r => r.pick.equal("gheat"),
1695
2135
  label: r => r.label,
1696
2136
  cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1697
2137
  }),
1698
2138
  ]),
1699
- Plan.series.group(OpsRow, { key: "g3-docks", label: "Docks", meta: "buckets" }, [
2139
+ Plan.series.section(OpsRow, { key: "dock-group", title: "Docks", subtitle: "a section of buckets", meta: "buckets" }, [
1700
2140
  Plan.series.buckets(OpsRow, {
1701
- key: "gbuckets", title: "Grouped buckets", subtitle: "member",
2141
+ key: "gbuckets", title: "Dock rows", subtitle: "member",
1702
2142
  match: r => r.pick.equal("gbuckets"),
1703
2143
  label: r => r.label,
1704
2144
  events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
1705
2145
  }),
1706
2146
  ]),
1707
- // The DISCOVERED form — one strip per distinct `line`, and ONE
1708
- // library entry for all of them, which is why it declares its
1709
- // own identity instead of borrowing a strip's.
2147
+ // One strip PER PROGRAM, its runs nested in the program's entry
2148
+ // — and ONE library entry for all of them.
1710
2149
  Plan.series.group(OpsRow, {
1711
- key: "programs", title: "Programs", subtitle: "one strip per line",
1712
- by: r => r.line, match: r => r.pick.equal("programs"), keyPrefix: "p-",
1713
- }, [
1714
- Plan.series.span(OpsRow, {
1715
- key: "programs", title: "Program runs", subtitle: "member",
1716
- match: r => r.pick.equal("programs"),
1717
- label: r => r.label, id: true,
1718
- runs: r => r.jobs.map((_$, j) => Plan.run({
1719
- key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
1720
- })),
1721
- }),
1722
- ]),
2150
+ key: "programs", title: "Programs", subtitle: "one strip per program",
2151
+ match: r => r.pick.equal("programs"),
2152
+ label: r => r.label,
2153
+ children: Plan.children((r) => r.members, [
2154
+ Plan.series.span(OpsRow, {
2155
+ key: "program-runs", title: "Program runs", subtitle: "member",
2156
+ label: r => r.label, id: true,
2157
+ runs: r => r.jobs.map((_$, j) => Plan.run({
2158
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
2159
+ })),
2160
+ }),
2161
+ ]),
2162
+ }),
1723
2163
  ], ArrayType(Plan.Types.Series(OpsRow)));
1724
- // `data` gives each entry its row count, so a series that selects
1725
- // nothing says `0` rather than switching on to no visible effect.
2164
+ // The library lists every series by title, subtitle and kind; the
2165
+ // only option is which start switched off. There are no row counts
2166
+ // (#822): a count means something only when every entry is in hand.
1726
2167
  const shown = $.let(Plan.pick("ex.plan.library", all, {
1727
- data: ops,
1728
- hidden: ["quality", "cards", "g3-docks"],
2168
+ hidden: ["quality", "cards", "dock-group"],
1729
2169
  }));
1730
2170
  const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1731
- const onDrag = $.const(East.function([DragEventType], NullType, (_$, _e) => null));
1732
- const canDrop = $.const(East.function([DragEventType], BooleanType, (_$, _e) => true));
1733
2171
  // `pick` REPLACES `series`: the handle already carries the list, so
1734
2172
  // the canvas feeds itself the picked ones and mounts the library.
1735
2173
  // Nothing here wires the panel to the canvas.
1736
- return (_jsx(Plan, { axis: axis, data: ops, pick: shown, id: "plan", sources: ["row-library"], onDrag: onDrag, canDrop: canDrop, style: { height: "620px" } }));
2174
+ return (_jsx(Plan, { axis: axis, data: ops, pick: shown, style: { height: "620px" } }));
1737
2175
  } }))),
1738
2176
  inputs: [],
1739
2177
  });
@@ -1746,35 +2184,43 @@ export const planLibraryDnd = example({
1746
2184
  *
1747
2185
  * Roster, Board and Blend have ONE kind of cell, so "can you drop here" is a
1748
2186
  * question about the cell's contents. A Plan's rows are nine different things,
1749
- * so the question is answered TWICE, at two different levels:
2187
+ * so the question is answered at three levels:
1750
2188
  *
1751
- * 1. **Structurally, by kind.** Only rows holding discrete scheduled objects
1752
- * register a drop cell at all — `span` (runs), `buckets` (tiles), `events`
2189
+ * 1. **Structurally, by series.** A card lands only on a row whose series
2190
+ * declares `edit` — WHERE it lands (`items`, the entry's list the row's
2191
+ * elements come from) and HOW it becomes one (`create`, from the drop: the
2192
+ * card, the row and the bucket's instant). Only the kinds holding discrete
2193
+ * scheduled objects can — `span` (runs), `buckets` (tiles), `events`
1753
2194
  * (marks), `cards` (chips). A `chart` / `heat` / `table` row renders
1754
- * DERIVED values, so there is nothing for a card to become; it registers
1755
- * no cell, never lights up during a drag, and is not reachable by any
1756
- * predicate. Group strips are wayfinding, so they are inert too — their
1757
- * MEMBERS receive.
2195
+ * DERIVED values, and section headers and group strips are wayfinding:
2196
+ * they register no cell and never light up during a drag.
1758
2197
  * 2. **By policy, with `canDrop`.** Of the rows that can receive, this canvas
1759
2198
  * admits only the matching FAMILY: a job goes on a machine, a delivery on
1760
2199
  * a dock, a shift on a crew, a milestone on a stream. The `PALLET` card
1761
2200
  * belongs to no family and is therefore refused everywhere — the ⊘ stage
1762
2201
  * on every row, which is what a card with nowhere to go should look like.
2202
+ * 3. **As a draft (#880).** A drop is a gesture of the editing session: the
2203
+ * entry is drafted with the new item in its list and its rows derived
2204
+ * again — drawn at once with the pending mark, undone with ⌘Z — and Apply
2205
+ * writes every draft as ONE checked batch, here through the live handle's
2206
+ * inline adapter (`editing.onUpdate`).
1763
2207
  *
1764
- * The drop is not cosmetic. `onDrag` writes to bound state, the series
1765
- * accessors read that state back, and the row re-derives — the documented flow
1766
- * for every target ("no drop writes state directly"). Nothing is painted
1767
- * optimistically, so what appears on the canvas is only ever what the data
1768
- * says.
2208
+ * The canvas is bounded shorter than its rows, so the Line 3 machine starts
2209
+ * below the fold: a card held at the canvas's bottom edge scrolls it there
2210
+ * (#608). A card is also a keyboard control — Space picks it up, the arrows
2211
+ * carry it from row to row and along a row's buckets, Space drops it and
2212
+ * Escape cancels — and every step is said to a screen reader.
1769
2213
  */
1770
2214
  export const planRowDrop = example({
1771
2215
  keywords: [
1772
- "Plan", "Library", "DnD", "drag", "drop", "onDrag", "canDrop", "sources", "id",
2216
+ "Plan", "Library", "DnD", "drag", "drop", "canDrop", "sources", "id", "edit", "items", "create",
1773
2217
  "add", "target", "surface", "cell", "slot", "row kind", "selective", "veto",
1774
- "invalid", "span", "buckets", "events", "cards", "chart", "heat", "table", "group",
1775
- "droppable", "inert", "bucket instant", "Reactive", "State", "commit", "re-derive",
2218
+ "invalid", "span", "buckets", "events", "cards", "chart", "heat", "table", "section", "row id", "row text",
2219
+ "droppable", "inert", "bucket instant", "editing", "onUpdate", "onPatch", "draft", "Apply", "undo",
2220
+ "Plan.Types.PatchEvent", "Reactive", "State", "live handle", "re-derive", "#880",
2221
+ "auto-scroll", "keyboard", "screen reader", "announcements", "#608",
1776
2222
  ],
1777
- description: "Library + Plan DnD — only the kinds holding discrete objects receive, and `canDrop` admits only the matching family",
2223
+ description: "Library + Plan DnD — a card lands only on a series that declares `edit`, `canDrop` admits only the matching family, and every drop is a draft applied as one checked batch",
1778
2224
  fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
1779
2225
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1780
2226
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -1790,12 +2236,8 @@ export const planRowDrop = example({
1790
2236
  key: StringType, from: DateTimeType, to: DateTimeType, label: StringType, state: EventStateType,
1791
2237
  });
1792
2238
  const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
1793
- // What a completed drop leaves behind: an instant and a name. The
1794
- // KIND it becomes is the receiving series' business, which is why
1795
- // one shape serves all four.
1796
- const DropRow = StructType({ key: StringType, at: DateTimeType, label: StringType });
1797
2239
  const OpsRow = StructType({
1798
- series: StringType, label: StringType, line: StringType,
2240
+ series: StringType, label: StringType,
1799
2241
  jobs: ArrayType(JobRow),
1800
2242
  points: ArrayType(MeasureRow),
1801
2243
  cells: ArrayType(Plan.Types.HeatCell),
@@ -1811,7 +2253,6 @@ export const planRowDrop = example({
1811
2253
  const noNums = $.const([], ArrayType(Plan.Types.TableCell));
1812
2254
  const noShifts = $.const([], ArrayType(ShiftRow));
1813
2255
  const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
1814
- const noDrops = $.const([], ArrayType(DropRow));
1815
2256
  const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
1816
2257
  const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
1817
2258
  const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
@@ -1825,44 +2266,49 @@ export const planRowDrop = example({
1825
2266
  // host has not committed, and the lifecycle is how the canvas says so.
1826
2267
  const ADDED = variant("proposed", variant("added", null));
1827
2268
  const base = {
1828
- line: "", jobs: noJobs, points: noPoints, cells: noCells,
2269
+ jobs: noJobs, points: noPoints, cells: noCells,
1829
2270
  allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks,
1830
2271
  };
1831
- // Keys order the canvas (#568), numbered so the droppable and inert
1832
- // kinds INTERLEAVE — a solid block of receiving rows would not show
1833
- // that the line is drawn per kind.
1834
- const ops = $.const(new Map([
1835
- ["10-util", { ...base, series: "util", label: "UTIL %", points }],
1836
- ["20-m03", { ...base, series: "mach", label: "L1-M03",
2272
+ // The series list below orders the canvas, interleaving the
2273
+ // droppable and inert kinds — a solid block of receiving rows would
2274
+ // not show that the line is drawn per kind. The source is a LIVE
2275
+ // handle: the canvas reads it, and Apply writes the drafts back.
2276
+ const ops = $.let(State.bind([DictType(StringType, OpsRow)], "ex.plan.ops", new Map([
2277
+ ["util", { ...base, series: "util", label: "Util %", points }],
2278
+ ["m03", { ...base, series: "mach", label: "L1-M03",
1837
2279
  jobs: [{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: RUNNING }] }],
1838
- ["20-m04", { ...base, series: "mach", label: "L1-M04",
2280
+ ["m04", { ...base, series: "mach", label: "L1-M04",
1839
2281
  jobs: [{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: CONFIRMED }] }],
1840
- ["30-load", { ...base, series: "load", label: "L2 load", cells }],
1841
- ["40-dock2", { ...base, series: "dock", label: "Dock 2",
2282
+ ["load", { ...base, series: "load", label: "L2 load", cells }],
2283
+ ["dock2", { ...base, series: "dock", label: "Dock 2",
1842
2284
  allocs: [{ key: "a1", at: week(29n), state: CONFIRMED }] }],
1843
- ["50-desp", { ...base, series: "table", label: "Despatch t",
2285
+ ["desp", { ...base, series: "table", label: "Despatch t",
1844
2286
  nums: [
1845
2287
  { at: Plan.at.time(week(28n)), value: some(128.0), text: none, tone: none },
1846
2288
  { at: Plan.at.time(week(31n)), value: some(-96.0), text: none, tone: none },
1847
2289
  ] }],
1848
- ["60-crewA", { ...base, series: "crew", label: "Crew A",
2290
+ ["crewA", { ...base, series: "crew", label: "Crew A",
1849
2291
  shifts: [{ key: "s1", from: week(27n), to: week(29n), label: "80h", state: CONFIRMED }] }],
1850
- ["70-ms", { ...base, series: "strm", label: "MILESTONES",
2292
+ ["ms", { ...base, series: "strm", label: "Milestones",
1851
2293
  marks: [{ key: "k", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") }] }],
1852
- // A group MEMBER — the strip itself takes no drops, but the
1853
- // span row inside it receives like any other span row.
1854
- ["g1-m11", { ...base, series: "gmach", label: "L3-M11", line: "Line 3",
2294
+ // A section MEMBER — the header itself takes no drops, but the
2295
+ // span row under it receives like any other span row.
2296
+ ["m11", { ...base, series: "gmach", label: "L3-M11",
1855
2297
  jobs: [{ key: "b301", label: "RUN · B-301", start: week(30n), end: week(34n), state: CONFIRMED }] }],
1856
- ]), DictType(StringType, OpsRow));
2298
+ ])));
1857
2299
  // ── The two policy tables the host owns ───────────────────────
1858
- // Which FAMILY of card each row will take. A row absent from this
1859
- // map takes nothing — which is how the inert kinds would behave
1860
- // even if they did register a cell.
2300
+ // Which FAMILY of card each row will take. A drop cell names its
2301
+ // row by the canonical TEXT of the row's id — the series that made
2302
+ // it and the path of keys to it — so the table is keyed by that
2303
+ // text. A row absent from this map takes nothing — which is how
2304
+ // the inert kinds would behave even if they did register a cell.
1861
2305
  const rowAccepts = $.const(new Map([
1862
- ["20-m03", "job"], ["20-m04", "job"], ["g1-m11", "job"],
1863
- ["40-dock2", "delivery"],
1864
- ["60-crewA", "shift"],
1865
- ["70-ms", "milestone"],
2306
+ [East.print(Plan.ref("mach", "m03")), "job"],
2307
+ [East.print(Plan.ref("mach", "m04")), "job"],
2308
+ [East.print(Plan.ref("gmach", "m11")), "job"],
2309
+ [East.print(Plan.ref("dock", "dock2")), "delivery"],
2310
+ [East.print(Plan.ref("crew", "crewA")), "shift"],
2311
+ [East.print(Plan.ref("strm", "ms")), "milestone"],
1866
2312
  ]), DictType(StringType, StringType));
1867
2313
  const CardRow = StructType({
1868
2314
  key: StringType, name: StringType, family: StringType, note: StringType, icon: StringType,
@@ -1881,10 +2327,9 @@ export const planRowDrop = example({
1881
2327
  const cardName = $.const(cards.toDict((_$, c) => c.key, (_$, c) => c.name));
1882
2328
  // ── The drop veto ────────────────────────────────────────────
1883
2329
  // Consulted with the candidate event the pointer's CURRENT bucket
1884
- // would produce, so the ⊘ appears while dragging rather than after.
1885
- // Only `add` can reach a Plan (nothing on the canvas starts a drag,
1886
- // so the surface declares no move / resize), and refusing the rest
1887
- // says that rather than pretending they are permitted.
2330
+ // would produce, so the ⊘ appears while dragging rather than after,
2331
+ // and once more before the drop becomes a draft. Only `add` reaches
2332
+ // a Plan from a library, and refusing the rest says so.
1888
2333
  const canDrop = $.const(East.function([DragEventType], BooleanType, ($, event) => {
1889
2334
  const no = $.const(false, BooleanType);
1890
2335
  return event.match({
@@ -1897,40 +2342,16 @@ export const planRowDrop = example({
1897
2342
  },
1898
2343
  }, _$ => no);
1899
2344
  }));
1900
- // ── The commit ───────────────────────────────────────────────
1901
- // The drop reports; the HOST writes. Nothing is painted
1902
- // optimistically — this state is what the series read back, so a
1903
- // dropped card only appears because the data now says it is there.
1904
- const dropBind = $.let(State.bind([DictType(StringType, ArrayType(DropRow))], "ex.plan.drops", new Map()));
2345
+ // ── The session ──────────────────────────────────────────────
2346
+ // Every drop is a DRAFT of the entry it landed on: the canvas
2347
+ // draws the new item at once, marked pending, and the history bar
2348
+ // undoes, redoes, discards and applies it. Apply writes every draft
2349
+ // through the live handle as one checked batch; `onPatch` hears each
2350
+ // gesture as it is made.
1905
2351
  const lastBind = $.let(State.bind([StringType], "ex.plan.lastdrop", "none yet"));
1906
- const onDrag = $.const(East.function([DragEventType], NullType, ($, event) => {
1907
- $.match(event, {
1908
- add: ($, add) => {
1909
- const next = $.let(dropBind.read());
1910
- const row = $.let(add.into.row);
1911
- const card = $.let(add.from.key);
1912
- // The slot IS an East DateTime in text — that is the
1913
- // contract the Z-less ISO spelling exists to keep.
1914
- const at = $.let(add.into.slot.parse(DateTimeType));
1915
- const one = $.let([{
1916
- key: East.str `drop-${card}-${add.into.slot}`,
1917
- at, label: cardName.get(card),
1918
- }], ArrayType(DropRow));
1919
- $(next.insertOrUpdate(row, one, (_$, existing, incoming) => existing.concat(incoming)));
1920
- $(dropBind.write(next));
1921
- $(lastBind.write(East.str `add ${card} → ${row} · ${add.into.slot}`));
1922
- },
1923
- move: (_$) => { }, remove: (_$) => { }, resize: (_$) => { },
1924
- });
2352
+ const onPatch = $.const(East.function([Plan.Types.PatchEvent(OpsRow)], NullType, ($, event) => {
2353
+ $(lastBind.write(East.str `${event.origin.getTag()} · ${event.label}`));
1925
2354
  }));
1926
- // Read DIRECTLY in the accessors below — never through a helper
1927
- // East.function. A series accessor is reified into a standalone
1928
- // function, so whatever it captures crosses that boundary: a
1929
- // captured DICT travels as the value it is, but a captured
1930
- // FUNCTION drags the enclosing block along with it, once per
1931
- // accessor, and four accessors was enough to take this example's
1932
- // render payload past 100MB. `planReview` is the shape to copy.
1933
- const drops = $.let(dropBind.read());
1934
2355
  const last = $.let(lastBind.read());
1935
2356
  const axis = $.const(Plan.axis({
1936
2357
  window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
@@ -1938,11 +2359,10 @@ export const planRowDrop = example({
1938
2359
  return (_jsxs(VStack, { gap: "4", align: "stretch", children: [_jsx(Library, { id: "plan-library", data: cards, item: c => ({ key: c.key, label: c.name, sublabel: c.note, icon: c.icon }) }), _jsx(Plan, { axis: axis, data: ops,
1939
2360
  // The DnD target role: `id` names this surface in every
1940
2361
  // cell ref, `sources` says which palettes it will take
1941
- // from, and the two callbacks are the policy and the
1942
- // commit. Omitting either `id` or `onDrag` registers no
1943
- // target at all — a drop with nowhere to report is a
1944
- // gesture that silently loses work.
1945
- id: "ops-plan", sources: ["plan-library"], canDrop: canDrop, onDrag: onDrag, series: [
2362
+ // from, and `canDrop` is the policy. A drop becomes a
2363
+ // draft of the session — without `editing` no card
2364
+ // lands, since nothing could hold it.
2365
+ id: "ops-plan", sources: ["plan-library"], canDrop: canDrop, editing: { onUpdate: ops.write, onPatch }, series: [
1946
2366
  // INERT — a chart plots a derived series, so there
1947
2367
  // is nothing a card could become here.
1948
2368
  Plan.series.chart(OpsRow, {
@@ -1951,21 +2371,25 @@ export const planRowDrop = example({
1951
2371
  label: r => r.label, id: true, height: "spark",
1952
2372
  layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
1953
2373
  }),
1954
- // RECEIVES — runs are discrete scheduled objects.
1955
- // The row's own runs, plus one proposed run per
1956
- // card dropped on it — a fortnight long, starting
1957
- // at the bucket the pointer named.
2374
+ // RECEIVES — runs are discrete scheduled objects. A
2375
+ // dropped job joins the machine's `jobs`: a
2376
+ // fortnight long, from the bucket the pointer named.
1958
2377
  Plan.series.span(OpsRow, {
1959
2378
  key: "mach", title: "Machine jobs",
1960
2379
  match: r => r.series.equal("mach"),
1961
2380
  label: r => r.label, id: true,
1962
- runs: (r, k) => r.jobs.map((_$, j) => Plan.run({
2381
+ runs: r => r.jobs.map((_$, j) => Plan.run({
1963
2382
  key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
1964
- })).concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
1965
- .map((_$, d) => Plan.run({
1966
- key: d.key, start: d.at, end: d.at.addWeeks(2n),
1967
- label: d.label, state: ADDED,
1968
- }))),
2383
+ })),
2384
+ edit: {
2385
+ items: "jobs",
2386
+ create: (drop, r) => ({
2387
+ key: East.str `drop-${drop.from.key}-${East.print(r.jobs.size())}`,
2388
+ label: cardName.get(drop.from.key),
2389
+ start: drop.at.unwrap("time"), end: drop.at.unwrap("time").addWeeks(2n),
2390
+ state: ADDED,
2391
+ }),
2392
+ },
1969
2393
  }),
1970
2394
  // INERT — an intensity field has no object to add to.
1971
2395
  Plan.series.heat(OpsRow, {
@@ -1980,9 +2404,14 @@ export const planRowDrop = example({
1980
2404
  key: "dock", title: "Dock allocations",
1981
2405
  match: r => r.series.equal("dock"),
1982
2406
  label: r => r.label,
1983
- events: (r, k) => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state }))
1984
- .concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
1985
- .map((_$, d) => Plan.event({ key: d.key, at: d.at, state: ADDED }))),
2407
+ events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
2408
+ edit: {
2409
+ items: "allocs",
2410
+ create: (drop, r) => ({
2411
+ key: East.str `drop-${drop.from.key}-${East.print(r.allocs.size())}`,
2412
+ at: drop.at.unwrap("time"), state: ADDED,
2413
+ }),
2414
+ },
1986
2415
  }),
1987
2416
  // INERT — the cells are computed numbers.
1988
2417
  Plan.series.table(OpsRow, {
@@ -1997,13 +2426,17 @@ export const planRowDrop = example({
1997
2426
  key: "crew", title: "Crew shifts",
1998
2427
  match: r => r.series.equal("crew"),
1999
2428
  label: r => r.label,
2000
- chips: (r, k) => r.shifts.map((_$, s) => Plan.chip({
2429
+ chips: r => r.shifts.map((_$, s) => Plan.chip({
2001
2430
  key: s.key, from: s.from, to: s.to, label: s.label, state: s.state,
2002
- })).concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
2003
- .map((_$, d) => Plan.chip({
2004
- key: d.key, from: d.at, to: d.at.addWeeks(2n),
2005
- label: d.label, state: ADDED,
2006
- }))),
2431
+ })),
2432
+ edit: {
2433
+ items: "shifts",
2434
+ create: (drop, r) => ({
2435
+ key: East.str `drop-${drop.from.key}-${East.print(r.shifts.size())}`,
2436
+ from: drop.at.unwrap("time"), to: drop.at.unwrap("time").addWeeks(2n),
2437
+ label: cardName.get(drop.from.key), state: ADDED,
2438
+ }),
2439
+ },
2007
2440
  }),
2008
2441
  // RECEIVES — a dropped milestone becomes a mark at
2009
2442
  // the instant, the one kind with no duration.
@@ -2011,27 +2444,40 @@ export const planRowDrop = example({
2011
2444
  key: "strm", title: "Milestones",
2012
2445
  match: r => r.series.equal("strm"),
2013
2446
  label: r => r.label, id: true,
2014
- marks: (r, k) => r.marks.concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
2015
- .map((_$, d) => Plan.mark({
2016
- key: d.key, at: d.at, kind: "milestone", label: d.label,
2017
- }))),
2447
+ marks: r => r.marks,
2448
+ edit: {
2449
+ items: "marks",
2450
+ create: (drop, r) => ({
2451
+ key: East.str `drop-${drop.from.key}-${East.print(r.marks.size())}`,
2452
+ at: drop.at, kind: variant("milestone", null), icon: none,
2453
+ label: some(cardName.get(drop.from.key)),
2454
+ }),
2455
+ },
2018
2456
  }),
2019
- // The STRIP is inert; the span row inside it is not.
2020
- Plan.series.group(OpsRow, { key: "g1-line3", label: "Line 3", meta: "span" }, [
2457
+ // The HEADER is inert; the span row under it is not.
2458
+ Plan.series.section(OpsRow, { key: "line3", title: "Line 3", meta: "span" }, [
2021
2459
  Plan.series.span(OpsRow, {
2022
- key: "gmach", title: "Grouped machine jobs",
2460
+ key: "gmach", title: "Line 3 machine jobs",
2023
2461
  match: r => r.series.equal("gmach"),
2024
2462
  label: r => r.label, id: true,
2025
- runs: (r, k) => r.jobs.map((_$, j) => Plan.run({
2463
+ runs: r => r.jobs.map((_$, j) => Plan.run({
2026
2464
  key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
2027
- })).concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
2028
- .map((_$, d) => Plan.run({
2029
- key: d.key, start: d.at, end: d.at.addWeeks(2n),
2030
- label: d.label, state: ADDED,
2031
- }))),
2465
+ })),
2466
+ edit: {
2467
+ items: "jobs",
2468
+ create: (drop, r) => ({
2469
+ key: East.str `drop-${drop.from.key}-${East.print(r.jobs.size())}`,
2470
+ label: cardName.get(drop.from.key),
2471
+ start: drop.at.unwrap("time"), end: drop.at.unwrap("time").addWeeks(2n),
2472
+ state: ADDED,
2473
+ }),
2474
+ },
2032
2475
  }),
2033
2476
  ]),
2034
- ], style: { height: "420px" } }), _jsx(Text.MonoLabel, { children: East.str `LAST DROP · ${last}` })] }));
2477
+ ],
2478
+ // Shorter than its rows: the last ones start below the
2479
+ // fold, and a card held at the bottom edge scrolls there.
2480
+ style: { height: "300px" } }), _jsx(Text.MonoLabel, { children: East.str `LAST GESTURE · ${last}` })] }));
2035
2481
  } }))),
2036
2482
  inputs: [],
2037
2483
  });
@@ -2039,21 +2485,21 @@ export const planRowDrop = example({
2039
2485
  // planFill — the bounded sizing isolate (#320 / #567 D1)
2040
2486
  // ============================================================================
2041
2487
  /** Fill (#320) — `height="fill"` resolves against the bounded Box and
2042
- * virtualizes 200 span rows under 8 rollup parents. The bound must land on the
2043
- * canvas WRAPPER: a percentage passed inward resolves against an auto-height
2044
- * parent, computes to `auto`, and silently unbinds — the frame reports bounded,
2045
- * renders its spacer, and never scrolls (#567 D1).
2488
+ * virtualizes 200 rows of mixed kinds under 8 line strips. The bound must land
2489
+ * on the canvas WRAPPER: a percentage passed inward resolves against an
2490
+ * auto-height parent, computes to `auto`, and silently unbinds — the frame
2491
+ * reports bounded, renders its spacer, and never scrolls (#567 D1).
2046
2492
  *
2047
2493
  * The grouping is here for what it does to VIRTUALIZATION, not for the
2048
2494
  * chrome. Collapsing a strip removes its 25 children from the virtualizer's
2049
2495
  * item list, so `count` and the total size change while the scroll offset does
2050
2496
  * not — the case where an estimate that disagrees with the rendered height
2051
- * shows up as drift or a jumping scrollbar. The parents also give the list two
2052
- * different row heights (a rollup band is taller than a leaf), so the
2053
- * `estimateSize` path is exercised rather than a single constant. */
2497
+ * shows up as drift or a jumping scrollbar. The strips also give the list
2498
+ * another row height again, so the `estimateSize` path is exercised rather
2499
+ * than a single constant. */
2054
2500
  export const planFill = example({
2055
- keywords: ["Plan", "fill", "height", "maxHeight", "#320", "virtual", "virtualization", "bounded", "Box", "scroll", "sizing", "data", "series", "span", "chart", "heat", "spark", "sub", "two-line", "mixed", "variable", "estimateSize", "group", "groupBy", "rollup", "union", "collapse", "parent"],
2056
- description: "Fill sizing over variable row heights — 200 virtualized rows of mixed kinds under 8 rollup parents",
2501
+ keywords: ["Plan", "fill", "height", "maxHeight", "#320", "virtual", "virtualization", "bounded", "Box", "scroll", "sizing", "data", "series", "span", "chart", "heat", "spark", "sub", "two-line", "mixed", "variable", "estimateSize", "group", "groupToDicts", "grouping", "data step", "views", "interleave", "Plan.children", "collapse", "parent"],
2502
+ description: "Fill sizing over variable row heights — 200 virtualized rows of mixed kinds under 8 line strips",
2057
2503
  fn: East.function([], UIComponentType, ($) => {
2058
2504
  // Monday of ISO week n, 2026 — window W27–W38 (half-open).
2059
2505
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -2069,9 +2515,9 @@ export const planFill = example({
2069
2515
  cells: ArrayType(Plan.Types.HeatCell),
2070
2516
  });
2071
2517
  // 200 raw rows, generated in East — the row count is the point. The
2072
- // KEYS are the point too: they order the canvas, so they are numbered
2073
- // to sort as written (`UNIT-1000` … `UNIT-1199`) rather than
2074
- // lexicographically (`UNIT-1`, `UNIT-10`, `UNIT-100`, …).
2518
+ // KEYS sort as written (`UNIT-1000` … `UNIT-1199`) rather than
2519
+ // lexicographically (`UNIT-1`, `UNIT-10`, `UNIT-100`, …), and a line's
2520
+ // rows keep that order.
2075
2521
  //
2076
2522
  // `line` is the grouping level: 8 strips of 25, so a single collapse
2077
2523
  // takes an eighth of the list out of the virtualizer at once.
@@ -2079,9 +2525,9 @@ export const planFill = example({
2079
2525
  // Everything else here exists to make the row heights DISAGREE, which
2080
2526
  // is the case a virtualizer gets wrong. `series` cycles span / chart /
2081
2527
  // heat (32 / 32 / 28px), `sub` alternates so every other row floors at
2082
- // the 42px two-line gutter, and the rollup parents add a fourth height
2083
- // again — so consecutive estimates differ in both directions and no
2084
- // constant can stand in for `estimateSize`.
2528
+ // the 42px two-line gutter, and the strips add another height again —
2529
+ // so consecutive estimates differ in both directions and no constant
2530
+ // can stand in for `estimateSize`.
2085
2531
  const units = $.const(East.Array.range(0n, 200n).toDict((_$, i) => East.str `UNIT-${East.print(i.add(1000n))}`, ($, i) => {
2086
2532
  const m = $.let(i.modulo(10n), IntegerType);
2087
2533
  return {
@@ -2106,157 +2552,435 @@ export const planFill = example({
2106
2552
  })),
2107
2553
  };
2108
2554
  }), DictType(StringType, UnitRow));
2109
- // ONE discovered group per line, wrapping EVERY kind — the strip form
2110
- // takes child series, so each line's members are all its rows whatever
2111
- // kind they are. (`groupBy` on the span series alone would nest only
2112
- // the spans and leave every chart / heat row a root, which sorts them
2113
- // as a block after all eight subtrees; the chart series has no
2114
- // `groupBy` at all.) Inside a strip the members are in KEY order, so
2115
- // the kinds interleave the way the keys do.
2555
+ // Grouping is a DATA step (#822): one `groupToDicts` makes each line an
2556
+ // entry holding its 25 units.
2557
+ const lines = $.let(units.groupToDicts(($, u) => u.line, ($, _u, k) => k));
2558
+ const LineGroup = DictType(StringType, UnitRow);
2559
+ // ONE strip per line, its units stepped down into. A `views` series
2560
+ // gives each unit the ONE row its kind's member draws, so within a
2561
+ // strip the kinds interleave in the units' order — a block per kind
2562
+ // would bank the tall rows together again.
2116
2563
  const series = $.const([
2117
- Plan.series.group(UnitRow, { key: "lines", title: "Lines", by: r => r.line }, [
2118
- Plan.series.span(UnitRow, {
2119
- key: "span-4", title: "Span",
2120
- match: r => r.series.equal("span"),
2121
- label: (_r, k) => k, id: true, sub: r => r.sub,
2122
- runs: (r, k) => [Plan.run({
2123
- key: k, start: r.start, end: r.end,
2124
- label: East.str `RUN · ${k}`,
2125
- quantity: East.str `${East.Float.printFixed(r.tonnes, 0n)} t`,
2126
- qty: r.tonnes, state: variant("confirmed", null),
2127
- })],
2128
- }),
2129
- // Heat rows are 28px — shorter than everything around them.
2130
- Plan.series.heat(UnitRow, {
2131
- key: "heat-2", title: "Heat",
2132
- match: r => r.series.equal("heat"),
2133
- label: (_r, k) => k, id: true, sub: r => r.sub,
2134
- cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
2135
- }),
2136
- // Four chart heights. `height` is a SERIES declaration, not a
2137
- // per-row accessor, so distinct heights mean distinct series —
2138
- // which is the point here: a spark, then three EXPANDED rows
2139
- // several times taller, scattered through the same key order.
2140
- Plan.series.chart(UnitRow, {
2141
- key: "spark-2", title: "Spark",
2142
- match: r => r.series.equal("spark"),
2143
- label: (_r, k) => k, id: true, sub: r => r.sub,
2144
- height: "spark", expandable: true,
2145
- layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
2146
- }),
2147
- Plan.series.chart(UnitRow, {
2148
- key: "chartM", title: "Chart · medium",
2149
- match: r => r.series.equal("chartM"),
2150
- label: (_r, k) => k, id: true, sub: r => r.sub,
2151
- height: Plan.fixed("72px"),
2152
- layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
2153
- }),
2154
- Plan.series.chart(UnitRow, {
2155
- key: "chartL", title: "Chart · large",
2156
- match: r => r.series.equal("chartL"),
2157
- label: (_r, k) => k, id: true, sub: r => r.sub,
2158
- height: "expanded",
2159
- layers: r => [Chart.Area(r.points, { x: p => p.week, y: p => p.pct })],
2160
- }),
2161
- Plan.series.chart(UnitRow, {
2162
- key: "chartXL", title: "Chart · x-large",
2163
- match: r => r.series.equal("chartXL"),
2164
- label: (_r, k) => k, id: true, sub: r => r.sub,
2165
- height: "expanded", expandedHeight: "140px",
2166
- layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.pct })],
2167
- }),
2168
- ]),
2169
- ], ArrayType(Plan.Types.Series(UnitRow)));
2564
+ Plan.series.group(LineGroup, {
2565
+ key: "lines", title: "Lines",
2566
+ label: (_g, line) => line,
2567
+ children: Plan.children((g) => g, [
2568
+ Plan.series.views(UnitRow, { key: "units", title: "Units" }, [
2569
+ Plan.series.span(UnitRow, {
2570
+ key: "unit-span", title: "Span",
2571
+ match: r => r.series.equal("span"),
2572
+ label: (_r, k) => k, id: true, sub: r => r.sub,
2573
+ runs: (r, k) => [Plan.run({
2574
+ key: k, start: r.start, end: r.end,
2575
+ label: East.str `RUN · ${k}`,
2576
+ quantity: Plan.quantity(r.tonnes, { unit: "t", format: Format.Number({ maximumFractionDigits: 0n }) }),
2577
+ state: variant("confirmed", null),
2578
+ })],
2579
+ }),
2580
+ // Heat rows are 28px — shorter than everything around them.
2581
+ Plan.series.heat(UnitRow, {
2582
+ key: "unit-heat", title: "Heat",
2583
+ match: r => r.series.equal("heat"),
2584
+ label: (_r, k) => k, id: true, sub: r => r.sub,
2585
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
2586
+ }),
2587
+ // Four chart heights. `height` is a SERIES declaration,
2588
+ // not a per-row accessor, so distinct heights mean
2589
+ // distinct members — which is the point here: a spark,
2590
+ // then three EXPANDED rows several times taller,
2591
+ // scattered through the same unit order.
2592
+ Plan.series.chart(UnitRow, {
2593
+ key: "unit-spark", title: "Spark",
2594
+ match: r => r.series.equal("spark"),
2595
+ label: (_r, k) => k, id: true, sub: r => r.sub,
2596
+ height: "spark", expandable: true,
2597
+ layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
2598
+ }),
2599
+ Plan.series.chart(UnitRow, {
2600
+ key: "unit-chart-m", title: "Chart · medium",
2601
+ match: r => r.series.equal("chartM"),
2602
+ label: (_r, k) => k, id: true, sub: r => r.sub,
2603
+ height: Plan.fixed("72px"),
2604
+ layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
2605
+ }),
2606
+ Plan.series.chart(UnitRow, {
2607
+ key: "unit-chart-l", title: "Chart · large",
2608
+ match: r => r.series.equal("chartL"),
2609
+ label: (_r, k) => k, id: true, sub: r => r.sub,
2610
+ height: "expanded",
2611
+ layers: r => [Chart.Area(r.points, { x: p => p.week, y: p => p.pct })],
2612
+ }),
2613
+ Plan.series.chart(UnitRow, {
2614
+ key: "unit-chart-xl", title: "Chart · x-large",
2615
+ match: r => r.series.equal("chartXL"),
2616
+ label: (_r, k) => k, id: true, sub: r => r.sub,
2617
+ height: "expanded", expandedHeight: "140px",
2618
+ layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.pct })],
2619
+ }),
2620
+ ]),
2621
+ ]),
2622
+ }),
2623
+ ], ArrayType(Plan.Types.Series(LineGroup)));
2170
2624
  const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
2171
- return (_jsx(Box, { height: "240px", children: _jsx(Plan, { axis: axis, data: units, series: series, style: { height: "fill" } }) }));
2625
+ return (_jsx(Box, { height: "240px", children: _jsx(Plan, { axis: axis, data: lines, series: series, style: { height: "fill" } }) }));
2172
2626
  }),
2173
2627
  inputs: [],
2174
2628
  });
2175
2629
  // ============================================================================
2176
2630
  // planReview — the review chrome, and what a verdict is FOR (#569)
2177
2631
  // ============================================================================
2632
+ /**
2633
+ * Review as drafts (#880): a verdict is a FIELD of the record the reviewer
2634
+ * decides about — `approval`, an `ApprovalStateType` — and the series whose
2635
+ * rows are reviewed names it (`review: { verdict: "approval" }`). Approve or
2636
+ * Reject on a row, and Approve all / Reject all at the foot, draft the entries
2637
+ * with the field changed: the canvas derives the drafted rows again at once —
2638
+ * buttons, bar, dot and run — marked pending, the history bar undoes, redoes
2639
+ * and discards them, and Apply writes them back through the live handle as one
2640
+ * checked batch. The foot's summary is the HOST's, over what it holds: the
2641
+ * saved state, which moves only when Apply lands.
2642
+ */
2178
2643
  export const planReview = example({
2179
2644
  keywords: [
2180
- "Plan", "review", "approval", "approve", "reject", "verdict", "decision",
2181
- "deriveApproval", "flagged", "chrome", "batch", "foot", "onApprove",
2182
- "onReject", "onApproveAll", "onRejectAll", "Reactive", "State", "bind",
2183
- "write", "live", "derived", "accessor", "data",
2645
+ "Plan", "review", "approval", "approve", "reject", "verdict", "decision", "ApprovalStateType",
2646
+ "deriveApproval", "flagged", "chrome", "batch", "foot", "Approve all", "Reject all", "onRerun",
2647
+ "editing", "onUpdate", "draft", "pending", "Apply", "undo", "redo", "discard", "Reactive", "State", "bind",
2648
+ "live handle", "derived", "accessor", "data", "row id", "#880",
2184
2649
  ],
2185
- description: "Live review chrome — verdicts live in bound `State`, and the canvas re-derives buttons, bar and dot",
2650
+ description: "Review as drafts — a verdict drafts the entry's `approval` field, the canvas re-derives buttons, bar and dot from the draft, and Apply writes the batch back through the live handle",
2186
2651
  fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
2187
2652
  // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
2188
2653
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2189
2654
  const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2190
2655
  return w1.addWeeks(n.subtract(1n));
2191
2656
  }));
2192
- // The RAW record carries NO verdict — deciding is not a property of
2193
- // the job, it is something the reviewer does to it. So the verdicts
2194
- // live in their own bound state and the job rows stay untouched.
2657
+ // The verdict IS a field of the job — what the reviewer decides
2658
+ // about it — so a verdict drafts the job with `approval` changed,
2659
+ // and Apply writes the job back. Nothing is held anywhere else.
2195
2660
  const JobRow = StructType({
2196
2661
  start: DateTimeType, end: DateTimeType, tonnes: FloatType, flagged: BooleanType,
2662
+ approval: ApprovalStateType,
2197
2663
  });
2198
- const jobs = $.const(new Map([
2199
- ["L1-M03", { start: week(28n), end: week(31n), tonnes: 96.0, flagged: true }],
2200
- ["L1-M04", { start: week(29n), end: week(33n), tonnes: 112.0, flagged: true }],
2201
- ["L1-M07", { start: week(30n), end: week(34n), tonnes: 64.0, flagged: true }],
2202
- ["L2-M11", { start: week(27n), end: week(30n), tonnes: 88.0, flagged: false }],
2203
- ]), DictType(StringType, JobRow));
2204
- // Every key is seeded, so a read is always a hit and the accessors
2205
- // never carry a "missing" branch.
2206
- const verdicts = $.let(State.bind([DictType(StringType, StringType)], "plan_review_verdicts", new Map([
2207
- ["L1-M03", "pending"], ["L1-M04", "approved"],
2208
- ["L1-M07", "rejected"], ["L2-M11", "approved"],
2664
+ // A LIVE handle: the canvas reads it, and Apply writes it.
2665
+ const jobs = $.let(State.bind([DictType(StringType, JobRow)], "plan_review_jobs", new Map([
2666
+ ["L1-M03", { start: week(28n), end: week(31n), tonnes: 96.0, flagged: true, approval: variant("pending", null) }],
2667
+ ["L1-M04", { start: week(29n), end: week(33n), tonnes: 112.0, flagged: true, approval: variant("approved", null) }],
2668
+ ["L1-M07", { start: week(30n), end: week(34n), tonnes: 64.0, flagged: true, approval: variant("rejected", null) }],
2669
+ ["L2-M11", { start: week(27n), end: week(30n), tonnes: 88.0, flagged: false, approval: variant("approved", null) }],
2209
2670
  ])));
2210
- const live = $.let(verdicts.read());
2211
- // The callbacks WRITE. `onApprove` fires with the row ref, so one
2212
- // key changes; the batch verbs rewrite every key. Nothing else in
2213
- // the canvas is told — it re-derives because `Reactive` re-runs.
2214
- const setOne = $.const(East.function([StringType, StringType], NullType, ($, key, verdict) => {
2215
- const next = $.let(verdicts.read());
2216
- $(next.insertOrUpdate(key, verdict));
2217
- $(verdicts.write(next));
2218
- }));
2219
- const setAll = $.const(East.function([StringType], NullType, ($, verdict) => {
2220
- const next = $.let(new Map(), DictType(StringType, StringType));
2221
- $(jobs.forEach((_$, _r, k) => next.insertOrUpdate(k, verdict)));
2222
- $(verdicts.write(next));
2671
+ // Rerun is not a verdict — it asks the host for a fresh proposal,
2672
+ // so it stays a callback and writes the SOURCE: here every job
2673
+ // returns to what its flag derives ("clean rests pre-approved,
2674
+ // flagged awaits an explicit call"). Drafts made before it sit over
2675
+ // a source that moved, and say so.
2676
+ const onRerun = $.const(East.function([], NullType, ($) => {
2677
+ const fresh = $.let(jobs.read().map((_$, j) => ({
2678
+ start: j.start, end: j.end, tonnes: j.tonnes, flagged: j.flagged,
2679
+ approval: deriveApproval(j.flagged).unwrap("some"),
2680
+ })));
2681
+ $(jobs.write(fresh));
2223
2682
  }));
2224
- const onApprove = $.const(East.function([Plan.Types.RowRef], NullType, ($, ref) => { $(setOne(ref.key, "approved")); }));
2225
- const onReject = $.const(East.function([Plan.Types.RowRef], NullType, ($, ref) => { $(setOne(ref.key, "rejected")); }));
2226
- const onApproveAll = $.const(East.function([], NullType, ($) => { $(setAll("approved")); }));
2227
- const onRejectAll = $.const(East.function([], NullType, ($) => { $(setAll("rejected")); }));
2228
- const onRerun = $.const(East.function([], NullType, ($) => { $(setAll("pending")); }));
2229
2683
  const series = $.const([
2230
2684
  Plan.series.span(JobRow, {
2231
- key: "span-5", title: "Span",
2685
+ key: "jobs", title: "Jobs",
2232
2686
  label: (_r, k) => k, id: true,
2233
2687
  value: r => some(East.str `${East.Float.printFixed(r.tonnes, 0n)} t`),
2234
- // Seeds the BUTTONS from the live verdict. `deriveApproval`
2235
- // covers the undecided case — "clean rests pre-approved,
2236
- // flagged awaits an explicit call".
2237
- approval: (r, k) => live.get(k).equal("approved").ifElse(() => some(variant("approved", null)), () => live.get(k).equal("rejected").ifElse(() => some(variant("rejected", null)), () => deriveApproval(r.flagged))),
2238
- // ...and the SAME fact drives the bar and the dot, because
2688
+ // The verdict's FIELD: the decision buttons show it, and a
2689
+ // verdict drafts the job with it changed.
2690
+ review: { verdict: "approval" },
2691
+ // ...and the SAME field drives the bar and the dot, because
2239
2692
  // appearance is derived like everything else. Click Approve
2240
- // and this repaints — the chrome never touched it.
2241
- status: (r, k) => live.get(k).equal("rejected").ifElse(() => some(variant("danger", null)), () => r.flagged.and(_$ => live.get(k).equal("pending")).ifElse(() => some(variant("warning", null)), () => none)),
2693
+ // and the draft repaints — the chrome never touched it.
2694
+ status: r => r.approval.hasTag("rejected").ifElse(() => some(variant("danger", null)), () => r.flagged.and(_$ => r.approval.hasTag("pending")).ifElse(() => some(variant("warning", null)), () => none)),
2242
2695
  runs: (r, k) => [Plan.run({
2243
2696
  key: k, start: r.start, end: r.end,
2244
2697
  label: East.str `RUN · ${k}`,
2245
- quantity: East.str `${East.Float.printFixed(r.tonnes, 0n)} t`,
2246
- qty: r.tonnes,
2247
- state: live.get(k).equal("approved").ifElse(() => variant("confirmed", null), () => live.get(k).equal("rejected").ifElse(() => variant("rejected", null), () => variant("proposed", variant("recommended", null)))),
2698
+ quantity: Plan.quantity(r.tonnes, { unit: "t", format: Format.Number({ maximumFractionDigits: 0n }) }),
2699
+ state: r.approval.hasTag("approved").ifElse(() => variant("confirmed", null), () => r.approval.hasTag("rejected").ifElse(() => variant("rejected", null), () => variant("proposed", variant("recommended", null)))),
2248
2700
  })],
2249
2701
  }),
2250
2702
  ], ArrayType(Plan.Types.Series(JobRow)));
2251
2703
  const axis = $.const(Plan.axis({
2252
2704
  window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
2253
2705
  }));
2254
- const counts = $.let(live.filter((_$, v) => v.equal("pending")).size());
2255
- const rejected = $.let(live.filter((_$, v) => v.equal("rejected")).size());
2706
+ // The host's summary reads what it holds — the saved jobs.
2707
+ const saved = $.let(jobs.read());
2708
+ const pending = $.let(saved.filter((_$, j) => j.approval.hasTag("pending")).size());
2709
+ const rejected = $.let(saved.filter((_$, j) => j.approval.hasTag("rejected")).size());
2256
2710
  return (_jsx(Plan, { axis: axis, data: jobs, series: series, review: {
2257
- summary: _jsx(Text, { children: East.str `${East.Float.printFixed(counts.toFloat(), 0n)} PENDING · ${East.Float.printFixed(rejected.toFloat(), 0n)} REJECTED` }),
2258
- onApprove, onReject, onApproveAll, onRejectAll, onRerun,
2259
- } }));
2711
+ summary: _jsx(Text, { children: East.str `SAVED · ${East.Float.printFixed(pending.toFloat(), 0n)} PENDING · ${East.Float.printFixed(rejected.toFloat(), 0n)} REJECTED` }),
2712
+ onRerun,
2713
+ }, editing: { onUpdate: jobs.write } }));
2714
+ } }))),
2715
+ inputs: [],
2716
+ });
2717
+ // ============================================================================
2718
+ // planEditing — every change a draft, one checked Apply (#880, #825)
2719
+ // ============================================================================
2720
+ /**
2721
+ * The Plan's editing session (#880) — the Sheet's, over the canvas's entries.
2722
+ *
2723
+ * The source holds LINES, each holding its machines. A machine is reviewed
2724
+ * (its `approval` field), receives dropped jobs (its `jobs` list) and has its
2725
+ * jobs moved and resized, and every gesture lands on the MACHINE rows, one
2726
+ * level down — yet each drafts the LINE, the source's top-level entry, which
2727
+ * the whole subtree rides in:
2728
+ *
2729
+ * - Approve / Reject on a machine, and Approve all / Reject all at the foot,
2730
+ * draft its `approval` (`review: { verdict: "approval" }` on the machines).
2731
+ * - A job card dropped on a machine drafts a new job in its `jobs` at the
2732
+ * bucket's instant (`edit: { items: "jobs", create }`).
2733
+ * - A job's run moves along its machine or onto another machine, and resizes
2734
+ * by either end (#825): the series names the job's key and the instant
2735
+ * fields a move writes (`key`, `start`, `end`). It moves in whole weeks, or
2736
+ * in days with Shift held. A move onto a machine of the other line takes
2737
+ * the job out of one line and puts it into the other, as one gesture over
2738
+ * both. From the keyboard, Space on a focused run picks it up: ←/→ move it
2739
+ * a week, Shift+←/→ its end, Alt+←/→ its start, ↑/↓ carry it to another
2740
+ * machine, and Space drops it.
2741
+ *
2742
+ * Every gesture is one transaction, drawn at once with the pending mark; the
2743
+ * toolbar's history bar undoes, redoes and discards it (so do ⌘Z and ⌘⇧Z).
2744
+ * `ready` is the author's check over a drafted line: a machine holding more
2745
+ * than four jobs is refused, by name, and Apply waits until it is fixed — M11
2746
+ * already holds four, so a job moved onto it holds Apply until one leaves.
2747
+ * Apply writes the batch back through the live handle (`onUpdate`), and
2748
+ * `onPatch` hears each gesture as it is made.
2749
+ */
2750
+ export const planEditing = example({
2751
+ keywords: [
2752
+ "Plan", "editing", "session", "draft", "drafts", "transaction", "onUpdate", "onPatch", "ready",
2753
+ "Readiness", "Editing.Types.Readiness", "invalid", "review", "verdict", "approval", "ApprovalStateType",
2754
+ "Approve all", "Reject all", "edit", "items", "create", "drop", "Library", "DnD", "id", "sources",
2755
+ "undo", "redo", "discard", "Apply", "history bar", "pending", "nested", "children", "Plan.children",
2756
+ "top-level entry", "live handle", "Plan.Types.PatchEvent", "Reactive", "State", "bind", "#880",
2757
+ "move", "resize", "drag", "run", "key", "start", "end", "Shift", "snap", "keyboard", "Space",
2758
+ "cross-row", "Plan.Types.Move", "#825",
2759
+ ],
2760
+ description: "The Plan's editing session — verdicts, dropped jobs and moved or resized runs on nested machine rows draft their line (both lines when a job changes line), the history bar undoes and applies them, and `ready` refuses a crowded machine",
2761
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
2762
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
2763
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2764
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2765
+ return w1.addWeeks(n.subtract(1n));
2766
+ }));
2767
+ const Job = StructType({
2768
+ key: StringType, label: StringType, start: DateTimeType, end: DateTimeType, state: EventStateType,
2769
+ });
2770
+ const Machine = StructType({ approval: ApprovalStateType, jobs: ArrayType(Job) });
2771
+ const Line = StructType({ name: StringType, machines: DictType(StringType, Machine) });
2772
+ const CONFIRMED = variant("confirmed", null);
2773
+ const RECOMMENDED = variant("proposed", variant("recommended", null));
2774
+ // What a drop creates is a proposal the host has not committed.
2775
+ const ADDED = variant("proposed", variant("added", null));
2776
+ // A LIVE handle over the lines: the canvas reads it, and Apply
2777
+ // writes it. M11 already holds four jobs, so one more is refused.
2778
+ const lines = $.let(State.bind([DictType(StringType, Line)], "ex.plan.editing.lines", new Map([
2779
+ ["L1", { name: "Line 1", machines: new Map([
2780
+ ["M03", { approval: variant("pending", null), jobs: [
2781
+ { key: "b214", label: "B-214", start: week(28n), end: week(31n), state: RECOMMENDED },
2782
+ ] }],
2783
+ ["M04", { approval: variant("approved", null), jobs: [
2784
+ { key: "b208", label: "B-208", start: week(27n), end: week(30n), state: CONFIRMED },
2785
+ { key: "b219", label: "B-219", start: week(31n), end: week(34n), state: RECOMMENDED },
2786
+ ] }],
2787
+ ]) }],
2788
+ ["L2", { name: "Line 2", machines: new Map([
2789
+ ["M11", { approval: variant("pending", null), jobs: [
2790
+ { key: "b241", label: "B-241", start: week(27n), end: week(29n), state: CONFIRMED },
2791
+ { key: "b244", label: "B-244", start: week(29n), end: week(31n), state: RECOMMENDED },
2792
+ { key: "b247", label: "B-247", start: week(31n), end: week(33n), state: RECOMMENDED },
2793
+ { key: "b250", label: "B-250", start: week(33n), end: week(35n), state: RECOMMENDED },
2794
+ ] }],
2795
+ ]) }],
2796
+ ])));
2797
+ // The palette the jobs come from.
2798
+ const CardRow = StructType({ key: StringType, name: StringType, note: StringType, icon: StringType });
2799
+ const cards = $.const([
2800
+ { key: "weld", name: "Weld run", note: "two weeks", icon: "gear" },
2801
+ { key: "cure", name: "Cure run", note: "two weeks", icon: "fire" },
2802
+ ], ArrayType(CardRow));
2803
+ const cardName = $.const(cards.toDict((_$, c) => c.key, (_$, c) => c.name));
2804
+ // The author's check over one drafted LINE — every check of the
2805
+ // batch runs in one call. A refusal names the machine.
2806
+ const ready = $.const(East.function([Line, StringType], Editing.Types.Readiness, ($, line, _key) => {
2807
+ const crowded = $.let(line.machines.filter((_$, m) => m.jobs.size().greater(4n)));
2808
+ const result = $.let(variant("ready", null), Editing.Types.Readiness);
2809
+ $.if(crowded.size().greater(0n), ($) => {
2810
+ $.assign(result, variant("invalid", crowded.toArray((_$, m, k) => ({
2811
+ field: "jobs", message: East.str `${k} holds ${East.print(m.jobs.size())} jobs — at most 4`,
2812
+ }))));
2813
+ });
2814
+ return result;
2815
+ }));
2816
+ // Every gesture, as it is made — a verdict, a drop, an undo.
2817
+ const lastBind = $.let(State.bind([StringType], "ex.plan.editing.last", "none yet"));
2818
+ const onPatch = $.const(East.function([Plan.Types.PatchEvent(Line)], NullType, ($, event) => {
2819
+ $(lastBind.write(East.str `${event.origin.getTag()} · ${event.label}`));
2820
+ }));
2821
+ const last = $.let(lastBind.read());
2822
+ // The host's summary reads what it holds — the saved lines.
2823
+ const saved = $.let(lines.read());
2824
+ const pending = $.let(saved.toArray((_$, l) => l.machines.filter((_$, m) => m.approval.hasTag("pending")).size()).sum());
2825
+ const axis = $.const(Plan.axis({
2826
+ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
2827
+ }));
2828
+ return (_jsxs(VStack, { gap: "4", align: "stretch", children: [_jsx(Library, { id: "plan-editing-jobs", data: cards, item: c => ({ key: c.key, label: c.name, sublabel: c.note, icon: c.icon }) }), _jsx(Plan, { axis: axis, data: lines, id: "plan-editing", sources: ["plan-editing-jobs"], series: [
2829
+ // One row per line, its machines stepped down into
2830
+ // through a plain field — which is what lets a
2831
+ // gesture on a machine write back into its line.
2832
+ Plan.series.span(Line, {
2833
+ key: "lines", title: "Lines",
2834
+ label: l => l.name,
2835
+ runs: _l => [],
2836
+ rollup: "union",
2837
+ children: Plan.children(l => l.machines, [
2838
+ Plan.series.span(Machine, {
2839
+ key: "machines", title: "Machines",
2840
+ label: (_m, k) => k, id: true,
2841
+ review: { verdict: "approval" },
2842
+ status: m => m.approval.hasTag("rejected").ifElse(() => some(variant("danger", null)), () => none),
2843
+ runs: m => m.jobs.map((_$, j) => Plan.run({
2844
+ key: j.key, start: j.start, end: j.end,
2845
+ label: East.str `RUN · ${j.label}`, state: j.state,
2846
+ })),
2847
+ edit: {
2848
+ items: "jobs",
2849
+ // A run moves and resizes: the job is
2850
+ // found by `key` — the run's own key —
2851
+ // and a move writes `start` and `end`.
2852
+ // Keys stay unique on a machine: a drop
2853
+ // or a move that would repeat one is
2854
+ // refused, so the new job's key names
2855
+ // its card, bucket and place.
2856
+ key: "key", start: "start", end: "end",
2857
+ create: (drop, m) => ({
2858
+ key: East.str `${drop.from.key}-${East.print(drop.at.unwrap("time"))}-${East.print(m.jobs.size())}`,
2859
+ label: cardName.get(drop.from.key),
2860
+ start: drop.at.unwrap("time"), end: drop.at.unwrap("time").addWeeks(2n),
2861
+ state: ADDED,
2862
+ }),
2863
+ },
2864
+ }),
2865
+ ]),
2866
+ }),
2867
+ ], review: {
2868
+ summary: _jsx(Text, { children: East.str `SAVED · ${East.print(pending)} PENDING` }),
2869
+ }, editing: { onUpdate: lines.write, onPatch, ready }, style: { height: "360px" } }), _jsx(Text.MonoLabel, { children: East.str `LAST GESTURE · ${last}` })] }));
2870
+ } }))),
2871
+ inputs: [],
2872
+ });
2873
+ // ============================================================================
2874
+ // planUiState — the interaction state, held by the host (#824)
2875
+ // ============================================================================
2876
+ /**
2877
+ * A bound `ui` state (#824): the canvas's selection, the rows folded or opened
2878
+ * against what they declare, the expanded charts, and a row to bring into
2879
+ * view — held by the HOST, in `State.bind` at `Plan.Types.UiState`, seeded by
2880
+ * `Plan.uiState(…)`. The canvas reads it and writes the user's actions back;
2881
+ * anything else may write it too. Here a picker beside the canvas brings a
2882
+ * machine into view (`focus` — the canvas opens the line it sits in, scrolls
2883
+ * to it and spends the request) and selects it, three buttons fold, open and
2884
+ * expand from outside, and a readout says what the state holds — including
2885
+ * whatever the user clicked on the canvas itself.
2886
+ */
2887
+ export const planUiState = example({
2888
+ keywords: [
2889
+ "Plan", "ui", "UiState", "Plan.uiState", "state", "bound", "State", "bind", "controlled",
2890
+ "selected", "selection", "collapsed", "expanded", "fold", "open", "charts", "focus",
2891
+ "scroll to row", "deep link", "external", "write back", "Reactive", "Select", "Button", "#824",
2892
+ ],
2893
+ description: "A bound ui state — the host selects a machine and brings it into view, folds and opens lines and expands a chart from outside, and reads back what the user did on the canvas",
2894
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
2895
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
2896
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2897
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2898
+ return w1.addWeeks(n.subtract(1n));
2899
+ }));
2900
+ const JobRow = StructType({
2901
+ key: StringType, start: DateTimeType, end: DateTimeType, state: EventStateType,
2902
+ });
2903
+ const MachineRow = StructType({ line: StringType, jobs: ArrayType(JobRow) });
2904
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
2905
+ const machines = $.const(new Map([
2906
+ ["L1-M03", { line: "Line 1", jobs: [{ key: "b214", start: week(28n), end: week(31n), state: variant("in-progress", null) }] }],
2907
+ ["L1-M04", { line: "Line 1", jobs: [{ key: "b208", start: week(27n), end: week(30n), state: variant("actual", null) }] }],
2908
+ ["L2-M11", { line: "Line 2", jobs: [{ key: "b241", start: week(29n), end: week(33n), state: variant("confirmed", null) }] }],
2909
+ ["L2-M12", { line: "Line 2", jobs: [{ key: "b198", start: week(30n), end: week(34n), state: variant("proposed", variant("recommended", null)) }] }],
2910
+ ["L3-M21", { line: "Line 3", jobs: [{ key: "b301", start: week(31n), end: week(35n), state: variant("confirmed", null) }] }],
2911
+ ["L3-M22", { line: "Line 3", jobs: [{ key: "b302", start: week(33n), end: week(37n), state: variant("proposed", variant("recommended", null)) }] }],
2912
+ ]), DictType(StringType, MachineRow));
2913
+ const lines = $.let(machines.groupToDicts(($, m) => m.line, ($, _m, k) => k));
2914
+ const LineGroup = DictType(StringType, MachineRow);
2915
+ const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({
2916
+ week: week(i.add(27n)), pct: i.multiply(13n).remainder(9n).toFloat().add(88.0),
2917
+ })));
2918
+ const series = $.const([
2919
+ Plan.series.rows(LineGroup, { key: "kpi", title: "Coverage" }, [
2920
+ Plan.chart({
2921
+ key: "coverage", label: "Coverage", id: true, height: "spark", expandable: true,
2922
+ layers: [Chart.Line(coverage, { x: p => p.week, y: p => p.pct })],
2923
+ }),
2924
+ ]),
2925
+ Plan.series.group(LineGroup, {
2926
+ key: "lines", title: "Lines",
2927
+ label: (_g, line) => line,
2928
+ children: Plan.children((g) => g, [
2929
+ Plan.series.span(MachineRow, {
2930
+ key: "machines", title: "Machines",
2931
+ label: (_m, k) => k, id: true,
2932
+ runs: m => m.jobs.map((_$, j) => Plan.run({
2933
+ key: j.key, start: j.start, end: j.end, label: East.str `RUN · ${j.key}`, state: j.state,
2934
+ })),
2935
+ }),
2936
+ ]),
2937
+ }),
2938
+ ], ArrayType(Plan.Types.Series(LineGroup)));
2939
+ // The STATE — Line 3 starts folded. Every row is named by its id:
2940
+ // a line is `Plan.ref("lines", line)`, a machine
2941
+ // `Plan.ref("machines", line, machine)`.
2942
+ const ui = $.let(State.bind([Plan.Types.UiState], "ex.plan.ui", Plan.uiState({ collapsed: [Plan.ref("lines", "Line 3")] })));
2943
+ const now = $.let(ui.read());
2944
+ const lineIds = $.const([Plan.ref("lines", "Line 1"), Plan.ref("lines", "Line 2"), Plan.ref("lines", "Line 3")], ArrayType(Plan.Types.RowId));
2945
+ const kpi = $.const(Plan.ref("kpi", "coverage"));
2946
+ // Bring a machine into view: select it, and REQUEST its focus — the
2947
+ // canvas opens its line if it is folded, scrolls to it, and clears
2948
+ // the request once it has.
2949
+ const goTo = $.const(East.function([StringType], NullType, ($, key) => {
2950
+ const s = $.let(ui.read());
2951
+ const id = $.let(Plan.ref("machines", machines.get(key).line, key));
2952
+ $(ui.write(East.value({
2953
+ selected: some(id), collapsed: s.collapsed, expanded: s.expanded, charts: s.charts, focus: some(id),
2954
+ }, Plan.Types.UiState)));
2955
+ }));
2956
+ const foldAll = $.const(East.function([], NullType, ($) => {
2957
+ const s = $.let(ui.read());
2958
+ $(ui.write(East.value({
2959
+ selected: s.selected, collapsed: lineIds, expanded: [], charts: s.charts, focus: s.focus,
2960
+ }, Plan.Types.UiState)));
2961
+ }));
2962
+ const openAll = $.const(East.function([], NullType, ($) => {
2963
+ const s = $.let(ui.read());
2964
+ $(ui.write(East.value({
2965
+ selected: s.selected, collapsed: [], expanded: lineIds, charts: s.charts, focus: s.focus,
2966
+ }, Plan.Types.UiState)));
2967
+ }));
2968
+ const noCharts = $.const([], ArrayType(Plan.Types.RowId));
2969
+ const kpiChart = $.const([kpi], ArrayType(Plan.Types.RowId));
2970
+ const chart = $.const(East.function([], NullType, ($) => {
2971
+ const s = $.let(ui.read());
2972
+ const charts = $.let(s.charts.size().greater(0n).ifElse(() => noCharts, () => kpiChart));
2973
+ $(ui.write(East.value({
2974
+ selected: s.selected, collapsed: s.collapsed, expanded: s.expanded, charts, focus: s.focus,
2975
+ }, Plan.Types.UiState)));
2976
+ }));
2977
+ const keys = $.const(["L1-M03", "L1-M04", "L2-M11", "L2-M12", "L3-M21", "L3-M22"], ArrayType(StringType));
2978
+ const picked = $.let(now.selected.match({
2979
+ some: (_$, id) => East.print(id),
2980
+ none: (_$) => "nothing",
2981
+ }), StringType);
2982
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
2983
+ return (_jsxs(VStack, { gap: "2", align: "stretch", children: [_jsxs(HStack, { gap: "2", children: [_jsx(Select, { value: "L3-M22", onChange: goTo, size: "sm", items: keys.map((_$, k) => Select.Item(k, East.str `Go to ${k}`)) }), _jsx(Button, { size: "xs", onClick: foldAll, children: "Fold lines" }), _jsx(Button, { size: "xs", onClick: openAll, children: "Open lines" }), _jsx(Button, { size: "xs", onClick: chart, children: "Coverage chart" })] }), _jsx(Plan, { axis: axis, data: lines, series: series, ui: ui, style: { height: "300px" } }), _jsx(Text.MonoLabel, { children: East.str `SELECTED · ${picked} · ${East.print(now.collapsed.size())} FOLDED · ${East.print(now.expanded.size())} OPENED` })] }));
2260
2984
  } }))),
2261
2985
  inputs: [],
2262
2986
  });
@@ -2265,8 +2989,8 @@ export const planReview = example({
2265
2989
  *
2266
2990
  * Expand is two halves and needs BOTH: a per-row `expand` declaration (data,
2267
2991
  * so it flows through an accessor like every other envelope field) and the
2268
- * ROOT's `expandRender` resolver, which builds the mounted body from the row
2269
- * ref. Neither alone shows the control.
2992
+ * ROOT's `expandRender` resolver, which builds the mounted body from the row's
2993
+ * id. Neither alone shows the control.
2270
2994
  *
2271
2995
  * The rows here are deliberately heterogeneous — span, chart, heat, table,
2272
2996
  * events — because focusing one is what makes the other five collapse, and
@@ -2274,7 +2998,7 @@ export const planReview = example({
2274
2998
  * re-encode as a tone strip, shapes keep their silhouette.
2275
2999
  */
2276
3000
  export const planExpand = example({
2277
- keywords: ["Plan", "expand", "expandRender", "axis", "keep", "dim", "off", "focus", "R2", "collapse", "context strip", "row", "data", "series", "chart", "heat", "table", "events", "span", "raw"],
3001
+ keywords: ["Plan", "expand", "expandRender", "expandGutter", "axis", "keep", "dim", "off", "focus", "R2", "collapse", "context strip", "row", "row id", "data", "series", "chart", "heat", "table", "events", "span", "raw"],
2278
3002
  description: "Expand-in-place — a row declares `expand`, the root renders plot and gutter, and unfocused rows collapse to strips",
2279
3003
  fn: East.function([], UIComponentType, ($) => {
2280
3004
  const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
@@ -2311,7 +3035,7 @@ export const planExpand = example({
2311
3035
  // A CHART row — the one kind whose marks are a VALUE scale. Its
2312
3036
  // plot, its gutter ticks and its ref-label gate answer to the band
2313
3037
  // the marks keep at the top, not to the grown row (#591).
2314
- ["COVERAGE", { series: "chart", label: "COVERAGE",
3038
+ ["COVERAGE", { series: "chart", label: "Coverage",
2315
3039
  expand: some({ height: some("150px"), axis: variant("keep", null) }),
2316
3040
  jobs: noJobs, points: coverage, cells: noCells, nums: noNums, marks: noMarks }],
2317
3041
  // axis: keep — the grid and now-line run THROUGH the render.
@@ -2366,7 +3090,7 @@ export const planExpand = example({
2366
3090
  { at: Plan.at.time(week(34n)), value: some(162.0), text: none, tone: none },
2367
3091
  { at: Plan.at.time(week(35n)), value: some(144.0), text: none, tone: none },
2368
3092
  ] }],
2369
- ["MILESTONES", { series: "events", label: "MILESTONES",
3093
+ ["MILESTONES", { series: "events", label: "Milestones",
2370
3094
  expand: some({ height: some("112px"), axis: variant("dim", null) }),
2371
3095
  jobs: noJobs, points: noPoints, cells: noCells, nums: noNums,
2372
3096
  marks: [
@@ -2375,7 +3099,7 @@ export const planExpand = example({
2375
3099
  { key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
2376
3100
  ] }],
2377
3101
  ]), DictType(StringType, OpsRow));
2378
- // ONE resolver serves every declaring row, called with the row ref at
3102
+ // ONE resolver serves every declaring row, called with the row's id at
2379
3103
  // interaction time. The canvas hands it the PLOT column with the
2380
3104
  // shared grid + now-line drawn behind it — so a component that fills
2381
3105
  // that column edge to edge shares the canvas's x-space and lines up
@@ -2383,17 +3107,17 @@ export const planExpand = example({
2383
3107
  // what makes the alignment visible; a `Chart` would draw its own axes
2384
3108
  // and margins inside the column and align to those instead.
2385
3109
  const util = $.let(East.Array.generate(12n, FloatType, (_$, i) => i.multiply(19n).remainder(48n).toFloat().add(50.0)));
2386
- const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Sparkline, { data: util, type: "area", color: "link", width: "100%", height: "100%" }))));
3110
+ const expandRender = $.const(East.function([Plan.Types.RowId], UIComponentType, (_$, _id) => (_jsx(Sparkline, { data: util, type: "area", color: "link", width: "100%", height: "100%" }))));
2387
3111
  // The GUTTER half. An expanded row's gutter cell grows with the row,
2388
3112
  // and what fills the space it opens up is the author's — the identity
2389
3113
  // and measures that only earn their place once the row has the canvas.
2390
- // Same row ref as `expandRender`, so it can differ per row.
3114
+ // Same row id as `expandRender`, so it can differ per row.
2391
3115
  // The old spec's drilled-row card, which is what the grown gutter is
2392
3116
  // for: identity lines then a fill meter. The lines need no styling —
2393
3117
  // the gutter body already carries the sub-line vocabulary — so the
2394
3118
  // author writes content, not typography.
2395
3119
  const GutterFacts = StructType({ a: StringType, b: StringType, fill: FloatType });
2396
- const expandGutter = $.const(East.function([Plan.Types.RowRef], UIComponentType, ($, ref) => {
3120
+ const expandGutter = $.const(East.function([Plan.Types.RowId], UIComponentType, ($, id) => {
2397
3121
  const facts = $.const(new Map([
2398
3122
  ["COVERAGE", { a: "TARGET 100 · MIN 92", b: "BREACH W34–W36 · 3 wk", fill: 0.94 }],
2399
3123
  ["L1-M03", { a: "120 t · FILL", b: "B-208 · 88 t · 73%", fill: 0.73 }],
@@ -2402,12 +3126,14 @@ export const planExpand = example({
2402
3126
  ["DESPATCH", { a: "NET 1 629 t", b: "2 SHORT WEEKS", fill: 0.55 }],
2403
3127
  ["MILESTONES", { a: "5 MARKS", b: "1 EXCEPTION · W34", fill: 0.2 }],
2404
3128
  ]), DictType(StringType, GutterFacts));
2405
- const f = $.let(facts.get(ref.key));
3129
+ // A row's id is its series and the path of keys to it — here the
3130
+ // entry's one key, which is what the facts are keyed by.
3131
+ const f = $.let(facts.get(id.unwrap("entry").path.get(0n)));
2406
3132
  return (_jsxs(Box, { children: [_jsx(Text, { children: f.a }), _jsx(Text, { children: f.b }), _jsx(Box, { width: "108px", children: _jsx(Progress, { value: f.fill.multiply(100.0), size: "xs", tone: "brand" }) })] }));
2407
3133
  }));
2408
3134
  return (_jsx(Plan, { axis: Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }), data: ops, series: [
2409
3135
  Plan.series.span(OpsRow, {
2410
- key: "span-6", title: "Span",
3136
+ key: "machines", title: "Machines",
2411
3137
  match: r => r.series.equal("span"),
2412
3138
  label: r => r.label, id: true, expand: r => r.expand,
2413
3139
  runs: r => r.jobs.map((_$, j) => Plan.run({
@@ -2415,7 +3141,7 @@ export const planExpand = example({
2415
3141
  })),
2416
3142
  }),
2417
3143
  Plan.series.chart(OpsRow, {
2418
- key: "chart-2", title: "Chart",
3144
+ key: "coverage", title: "Coverage",
2419
3145
  match: r => r.series.equal("chart"),
2420
3146
  label: r => r.label, id: true, expand: r => r.expand,
2421
3147
  height: "spark",
@@ -2426,19 +3152,19 @@ export const planExpand = example({
2426
3152
  ],
2427
3153
  }),
2428
3154
  Plan.series.heat(OpsRow, {
2429
- key: "heat-3", title: "Heat",
3155
+ key: "load", title: "Line load",
2430
3156
  match: r => r.series.equal("heat"),
2431
3157
  label: r => r.label, expand: r => r.expand,
2432
3158
  cells: r => Plan.heatCells(r.cells, { min: 40.0, max: 100.0 }),
2433
3159
  }),
2434
3160
  Plan.series.table(OpsRow, {
2435
- key: "table", title: "Table",
3161
+ key: "despatch", title: "Despatch",
2436
3162
  match: r => r.series.equal("table"),
2437
3163
  label: r => r.label, expand: r => r.expand,
2438
3164
  cells: r => r.nums,
2439
3165
  }),
2440
3166
  Plan.series.events(OpsRow, {
2441
- key: "events-2", title: "Events",
3167
+ key: "milestones", title: "Milestones",
2442
3168
  match: r => r.series.equal("events"),
2443
3169
  label: r => r.label, id: true, expand: r => r.expand,
2444
3170
  marks: r => r.marks,
@@ -2462,7 +3188,7 @@ export const planExpand = example({
2462
3188
  * one — the same `data` + `series`, the same slice — only the box changed.
2463
3189
  */
2464
3190
  export const planNarrow = example({
2465
- keywords: ["Plan", "narrow", "mobile", "phone", "responsive", "compact", "container", "breakpoint", "tabs", "Groups", "Rows", "Measures", "cards", "strip", "hottest", "two-finger", "pan", "review", "cohort", "slice", "§10", "raw"],
3191
+ keywords: ["Plan", "narrow", "mobile", "phone", "responsive", "compact", "container", "breakpoint", "tabs", "Groups", "Rows", "Measures", "cards", "strip", "hottest", "two-finger", "pan", "review", "cohort", "slice", "§10", "groupToDicts", "raw"],
2466
3192
  description: "The narrow layout — a phone-width box turns the same canvas into a review tool: Groups · Rows · Measures tabs, hottest-first strip cards, rows as cards, charts at expanded density",
2467
3193
  fn: East.function([], UIComponentType, (_$) => {
2468
3194
  const HorizonRow = StructType({ key: StringType, at: DateTimeType, risk: StringType });
@@ -2481,20 +3207,17 @@ export const planNarrow = example({
2481
3207
  key: StringType, label: StringType,
2482
3208
  start: DateTimeType, end: DateTimeType, state: EventStateType,
2483
3209
  });
2484
- // ONE raw source: `series` picks the series, `line` the group a row
2485
- // belongs to (the KPI belongs to none — it rides the Measures tab
2486
- // and an "Other rows" card), and every envelope field is per-row DATA.
3210
+ // The raw rows: `series` picks the series, `line` the line a row
3211
+ // belongs to, and every envelope field is per-row DATA.
2487
3212
  const OpsRow = StructType({
2488
3213
  series: StringType, line: StringType, label: StringType,
2489
3214
  value: OptionType(StringType), status: OptionType(StatusValueType),
2490
3215
  expand: OptionType(Plan.Types.Expand),
2491
3216
  jobs: ArrayType(JobRow),
2492
3217
  cells: ArrayType(Plan.Types.HeatCell),
2493
- points: ArrayType(MeasureRow),
2494
3218
  });
2495
3219
  const noJobs = $.const([], ArrayType(JobRow));
2496
3220
  const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
2497
- const noPoints = $.const([], ArrayType(MeasureRow));
2498
3221
  const loadPcts = $.const([46.0, 52.0, 58.0, 61.0, 66.0, 72.0, 78.0, 84.0, 90.0, 96.0, 98.0, 92.0], ArrayType(FloatType));
2499
3222
  // Line 2 runs hotter than Line 1 — the Groups tab sorts it first.
2500
3223
  const loadA = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
@@ -2515,27 +3238,28 @@ export const planNarrow = example({
2515
3238
  { key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
2516
3239
  { key: "qc", label: "QC", start: week(30n), end: week(32n), state: variant("confirmed", null) },
2517
3240
  { key: "b231", label: "RUN · B-231", start: week(33n), end: week(38n), state: variant("proposed", variant("recommended", null)) },
2518
- ], cells: noCells, points: noPoints }],
3241
+ ], cells: noCells }],
2519
3242
  ["L1-M04", { series: "mach", line: "Line 1 · Fill", label: "L1-M04",
2520
3243
  value: some("120 t"), status: some(variant("warning", null)), expand: none,
2521
3244
  jobs: [
2522
3245
  { key: "b214", label: "RUN · B-214", start: week(28n), end: week(33n), state: variant("in-progress", null) },
2523
- ], cells: noCells, points: noPoints }],
3246
+ ], cells: noCells }],
2524
3247
  ["l1-load", { series: "load", line: "Line 1 · Fill", label: "Line load",
2525
3248
  value: none, status: none, expand: none,
2526
- jobs: noJobs, cells: loadA, points: noPoints }],
3249
+ jobs: noJobs, cells: loadA }],
2527
3250
  ["L2-M11", { series: "mach", line: "Line 2 · Assy", label: "L2-M11",
2528
3251
  value: some("80 t"), status: none, expand: none,
2529
3252
  jobs: [
2530
3253
  { key: "b241", label: "RUN · B-241", start: week(29n), end: week(34n), state: variant("confirmed", null) },
2531
- ], cells: noCells, points: noPoints }],
3254
+ ], cells: noCells }],
2532
3255
  ["l2-load", { series: "load", line: "Line 2 · Assy", label: "Line load",
2533
3256
  value: none, status: some(variant("warning", null)), expand: none,
2534
- jobs: noJobs, cells: loadB, points: noPoints }],
2535
- ["coverage", { series: "kpi", line: "", label: "COVERAGE",
2536
- value: some("94.2%"), status: none, expand: none,
2537
- jobs: noJobs, cells: noCells, points: coverage }],
3257
+ jobs: noJobs, cells: loadB }],
2538
3258
  ]), DictType(StringType, OpsRow));
3259
+ // Grouping is a DATA step (#822): one `groupToDicts` makes each
3260
+ // line an entry holding its rows — the strips nest exactly those.
3261
+ const lines = $.let(ops.groupToDicts(($, r) => r.line, ($, _r, k) => k));
3262
+ const LineGroup = DictType(StringType, OpsRow);
2539
3263
  // The slice's own rows — despatch orders with a late-risk cohort
2540
3264
  // seeded active, so the chips row has something to say.
2541
3265
  const horizon = $.let(East.Array.generate(24n, HorizonRow, (_$, i) => ({
@@ -2543,56 +3267,62 @@ export const planNarrow = example({
2543
3267
  at: week(i.divide(2n).add(27n)),
2544
3268
  risk: i.remainder(3n).equal(0n).ifElse(() => "late", () => "on-time"),
2545
3269
  })));
3270
+ // A slice range is CLOSED — both ends inclusive — so the twelve
3271
+ // weeks W27–W38 end the millisecond before W39.
2546
3272
  const slice = $.let(Slice.bind([HorizonRow], "ex.plan.narrow", cfg, Slice.state({
2547
- range: some(variant("datetime", { from: week(27n), to: week(39n) })),
3273
+ range: some(variant("datetime", { from: week(27n), to: week(39n).addMilliseconds(-1n) })),
2548
3274
  cohorts: [{ id: "late", name: "Late risk", filters: [variant("string", { fieldId: "risk", op: variant("eq", "late") })] }],
2549
3275
  activeCohorts: new Set(["late"]),
2550
3276
  }), horizon, none));
2551
3277
  const series = $.const([
2552
- // One strip per line, DISCOVERED from the data; its summary
2553
- // strip is the max of its heat rows — what "hottest first" reads.
2554
- Plan.series.group(OpsRow, {
2555
- key: "lines", title: "Lines",
2556
- by: r => r.line, match: r => r.line.equal("").not(),
2557
- keyPrefix: "g-", summaryAggregate: "max",
2558
- }, [
2559
- Plan.series.span(OpsRow, {
2560
- key: "mach", title: "Machines",
2561
- match: r => r.series.equal("mach"),
2562
- label: r => r.label, id: true,
2563
- value: r => r.value, status: r => r.status, expand: r => r.expand,
2564
- runs: r => r.jobs.map((_$, j) => Plan.run({
2565
- key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
2566
- })),
2567
- }),
2568
- Plan.series.heat(OpsRow, {
2569
- key: "load", title: "Line load",
2570
- match: r => r.series.equal("load"),
2571
- label: r => r.label, status: r => r.status,
2572
- cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
3278
+ // The KPI no line holds — a hand-built chart row over the
3279
+ // coverage points: it rides the Measures tab and an "Other
3280
+ // rows" card.
3281
+ Plan.series.rows(LineGroup, { key: "kpi", title: "Coverage" }, [
3282
+ Plan.chart({
3283
+ key: "coverage", label: "Coverage", id: true, value: "94.2%",
3284
+ height: "spark",
3285
+ left: { domain: [80, 110], tickValues: [80, 100] },
3286
+ layers: [
3287
+ Plan.layer(Chart.Line(coverage, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
3288
+ Chart.refLine({ y: 100, label: "TARGET 100" }),
3289
+ ],
2573
3290
  }),
2574
3291
  ]),
2575
- Plan.series.chart(OpsRow, {
2576
- key: "kpi", title: "Coverage",
2577
- match: r => r.series.equal("kpi"),
2578
- label: r => r.label, id: true, value: r => r.value,
2579
- height: "spark",
2580
- left: { domain: [80, 110], tickValues: [80, 100] },
2581
- layers: r => [
2582
- Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
2583
- Chart.refLine({ y: 100, label: "TARGET 100" }),
2584
- ],
3292
+ // One strip per line, its rows stepped down into; its summary
3293
+ // strip is the max of its heat rows — what "hottest first" reads.
3294
+ Plan.series.group(LineGroup, {
3295
+ key: "lines", title: "Lines",
3296
+ label: (_g, line) => line,
3297
+ summaryAggregate: "max",
3298
+ children: Plan.children((g) => g, [
3299
+ Plan.series.span(OpsRow, {
3300
+ key: "mach", title: "Machines",
3301
+ match: r => r.series.equal("mach"),
3302
+ label: r => r.label, id: true,
3303
+ value: r => r.value, status: r => r.status, expand: r => r.expand,
3304
+ runs: r => r.jobs.map((_$, j) => Plan.run({
3305
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
3306
+ })),
3307
+ }),
3308
+ Plan.series.heat(OpsRow, {
3309
+ key: "load", title: "Line load",
3310
+ match: r => r.series.equal("load"),
3311
+ label: r => r.label, status: r => r.status,
3312
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
3313
+ }),
3314
+ ]),
2585
3315
  }),
2586
- ], ArrayType(Plan.Types.Series(OpsRow)));
3316
+ ], ArrayType(Plan.Types.Series(LineGroup)));
2587
3317
  const util = $.let(East.Array.generate(12n, FloatType, (_$, i) => i.multiply(19n).remainder(48n).toFloat().add(50.0)));
2588
- const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Sparkline, { data: util, type: "area", color: "link", width: "100%", height: "100%" }))));
3318
+ const expandRender = $.const(East.function([Plan.Types.RowId], UIComponentType, (_$, _id) => (_jsx(Sparkline, { data: util, type: "area", color: "link", width: "100%", height: "100%" }))));
2589
3319
  const axis = $.const(Plan.axis({
2590
3320
  window: { min: week(27n), max: week(39n) },
2591
3321
  resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
2592
3322
  }));
2593
3323
  // The 360px box is the whole point: the reflow is a property of
2594
3324
  // the CONTAINER, so a phone, a splitter pane and this box agree.
2595
- return (_jsx(Box, { width: "360px", children: _jsx(Plan, { axis: axis, data: ops, series: series, slice: { slice, affordances: ["cohort", "filter", "range", "resolution", "summary"] }, expandRender: expandRender, footer: [
3325
+ return (_jsx(Box, { width: "360px", children: _jsx(Plan, { axis: axis, data: lines, series: series, slice: { slice, affordances: ["cohort", "filter", "range", "resolution", "summary"] }, expandRender: expandRender, footer: [
2596
3326
  { text: "6 ROWS · 2 LINES" },
2597
3327
  { text: "RUN 412 · W27–W38", end: true },
2598
3328
  ], style: { height: "560px" } }) }));
@@ -2611,20 +3341,20 @@ export const planNarrow = example({
2611
3341
  * through the element builders (`Plan.run({ start: j.start })`), a chart
2612
3342
  * layer's numeric x accessor lands its columns on the same arm, `Plan.tableCells`
2613
3343
  * reads a numeric `at`, and the heat cells stored as RECORDS spell it out with
2614
- * `Plan.at.number`. The slice's range field is a float, so the horizon brush
2615
- * and the range chip ride the slice's `float` arm exactly as they ride
2616
- * `datetime` on a time axis; there is no resolution segment — `step` is the
2617
- * declaration.
3344
+ * `Plan.at.number`. The slice's range field is an integer day, so the horizon
3345
+ * brush and the range chip ride the slice's `integer` arm exactly as they ride
3346
+ * `datetime` on a time axis — a closed range, so the eight days `[1, 9)` read
3347
+ * `1–8`; there is no resolution segment — `step` is the declaration.
2618
3348
  */
2619
3349
  export const planNumberAxis = example({
2620
3350
  keywords: [
2621
3351
  "Plan", "axis", "number", "numeric", "step", "Plan.axis.number", "Plan.at", "instant",
2622
- "day", "AM", "PM", "lanes", "buckets", "Planner", "plannerPoint", "brush", "float", "range",
3352
+ "day", "AM", "PM", "lanes", "buckets", "Planner", "plannerPoint", "brush", "integer", "range", "inclusive",
2623
3353
  "format", "now", "typed axis", "#631", "raw",
2624
3354
  ],
2625
- description: "The number axis — day 1..8 at step 1 with AM/PM lanes (the retired Planner's plannerPoint), every row kind on the numeric scale, the horizon brush over the slice's float range",
3355
+ description: "The number axis — day 1..8 at step 1 with AM/PM lanes (the retired Planner's plannerPoint), every row kind on the numeric scale, the horizon brush over the slice's integer range, read inclusive",
2626
3356
  fn: East.function([], UIComponentType, (_$) => {
2627
- const HorizonRow = StructType({ key: StringType, day: FloatType, line: StringType });
3357
+ const HorizonRow = StructType({ key: StringType, day: IntegerType, line: StringType });
2628
3358
  const cfg = Slice.config(HorizonRow, {
2629
3359
  fields: { day: { label: "Day" }, line: { label: "Line" } },
2630
3360
  rangeFieldId: "day",
@@ -2638,7 +3368,7 @@ export const planNumberAxis = example({
2638
3368
  const PointRow = StructType({ day: FloatType, t: FloatType });
2639
3369
  const RawCell = StructType({ at: FloatType, value: OptionType(FloatType) });
2640
3370
  const OpsRow = StructType({
2641
- series: StringType, label: StringType, value: OptionType(StringType),
3371
+ series: StringType, label: StringType, value: OptionType(StringType), sub: OptionType(StringType),
2642
3372
  allocations: ArrayType(AllocRow), jobs: ArrayType(JobRow), shifts: ArrayType(ShiftRow),
2643
3373
  marks: ArrayType(MarkRow), points: ArrayType(PointRow), tonnes: ArrayType(RawCell),
2644
3374
  cells: ArrayType(Plan.Types.HeatCell),
@@ -2663,13 +3393,13 @@ export const planNumberAxis = example({
2663
3393
  return { at: Plan.at.number(i.toFloat().add(1.0)), value: some(load), label: some(East.Float.printFixed(load, 0n)) };
2664
3394
  }));
2665
3395
  const base = {
2666
- value: none, allocations: noAllocs, jobs: noJobs, shifts: noShifts,
3396
+ value: none, sub: none, allocations: noAllocs, jobs: noJobs, shifts: noShifts,
2667
3397
  marks: noMarks, points: noPoints, tonnes: noTonnes, cells: noCells,
2668
3398
  };
2669
- // Keys order the canvas (#568) — numbered so each series lands where the
2670
- // §1 layout puts it.
3399
+ // The keys are the entries' identities; the series list below is
3400
+ // the layout.
2671
3401
  const ops = $.const(new Map([
2672
- ["10-dock2", { ...base, series: "dock", label: "Dock 2", value: some("load/day"), allocations: [
3402
+ ["dock2", { ...base, series: "dock", label: "Dock 2", sub: some("load/day"), allocations: [
2673
3403
  { key: "a1", day: 1.0, lane: "am", state: CONFIRMED },
2674
3404
  { key: "a2", day: 1.0, lane: "pm", state: CONFIRMED },
2675
3405
  { key: "a3", day: 2.0, lane: "am", state: CONFIRMED },
@@ -2678,41 +3408,41 @@ export const planNumberAxis = example({
2678
3408
  { key: "a6", day: 6.0, lane: "pm", state: CONFIRMED },
2679
3409
  { key: "a7", day: 8.0, lane: "am", state: PROPOSED },
2680
3410
  ] }],
2681
- ["10-dock5", { ...base, series: "dock", label: "Dock 5", value: some("load/day"), allocations: [
3411
+ ["dock5", { ...base, series: "dock", label: "Dock 5", sub: some("load/day"), allocations: [
2682
3412
  { key: "b1", day: 2.0, lane: "pm", state: CONFIRMED },
2683
3413
  { key: "b2", day: 4.0, lane: "am", state: CONFIRMED },
2684
3414
  { key: "b3", day: 4.0, lane: "pm", state: PROPOSED },
2685
3415
  { key: "b4", day: 7.0, lane: "am", state: PROPOSED },
2686
3416
  ] }],
2687
- ["20-m03", { ...base, series: "mach", label: "L1-M03", value: some("120 t"), jobs: [
3417
+ ["m03", { ...base, series: "mach", label: "L1-M03", value: some("120 t"), jobs: [
2688
3418
  { key: "set", label: "SET", start: 1.0, end: 2.0, state: ACTUAL },
2689
3419
  { key: "b214", label: "RUN · B-214", start: 2.0, end: 5.0, state: RUNNING },
2690
3420
  { key: "b221", label: "RUN · B-221", start: 6.0, end: 8.0, state: PROPOSED },
2691
3421
  ] }],
2692
- ["20-m04", { ...base, series: "mach", label: "L1-M04", value: some("80 t"), jobs: [
3422
+ ["m04", { ...base, series: "mach", label: "L1-M04", value: some("80 t"), jobs: [
2693
3423
  { key: "b208", label: "RUN · B-208", start: 1.0, end: 4.0, state: ACTUAL },
2694
3424
  { key: "qc", label: "QC", start: 4.0, end: 5.0, state: CONFIRMED },
2695
3425
  { key: "b231", label: "RUN · B-231", start: 5.0, end: 9.0, state: PROPOSED },
2696
3426
  ] }],
2697
- ["30-load", { ...base, series: "load", label: "Line load", cells }],
2698
- ["40-out", { ...base, series: "out", label: "OUTPUT · t", value: some("612 t"), points }],
2699
- ["50-desp", { ...base, series: "table", label: "Despatch t", tonnes }],
2700
- ["60-crewA", { ...base, series: "crew", label: "Crew A", shifts: [
3427
+ ["load", { ...base, series: "load", label: "Line load", cells }],
3428
+ ["out", { ...base, series: "out", label: "Output · t", value: some("612 t"), points }],
3429
+ ["desp", { ...base, series: "table", label: "Despatch t", tonnes }],
3430
+ ["crewA", { ...base, series: "crew", label: "Crew A", shifts: [
2701
3431
  { key: "s1", from: 1.0, to: 3.0, hours: 24.0, state: CONFIRMED },
2702
3432
  { key: "s2", from: 3.0, to: 6.0, hours: 36.0, state: CONFIRMED },
2703
3433
  { key: "s3", from: 6.0, to: 8.0, hours: 24.0, state: PROPOSED },
2704
3434
  ] }],
2705
- ["70-ms", { ...base, series: "ms", label: "MILESTONES", marks: [
3435
+ ["ms", { ...base, series: "ms", label: "Milestones", marks: [
2706
3436
  { key: "kick", day: 2.0, label: "KICKOFF" },
2707
3437
  { key: "rel", day: 6.0, label: "REL" },
2708
3438
  ] }],
2709
3439
  ]), DictType(StringType, OpsRow));
2710
3440
  const series = $.const([
2711
- Plan.series.group(OpsRow, { key: "10-docks", label: "Docks · In", meta: "2 rs" }, [
3441
+ Plan.series.section(OpsRow, { key: "docks-in", title: "Docks · In", meta: "2 rows" }, [
2712
3442
  Plan.series.buckets(OpsRow, {
2713
3443
  key: "dock", title: "Docks",
2714
3444
  match: r => r.series.equal("dock"),
2715
- label: r => r.label, value: r => r.value,
3445
+ label: r => r.label, sub: r => r.sub,
2716
3446
  // The Planner's AM/PM lanes — sub-slots of each day column.
2717
3447
  lanes: _r => [Plan.lane({ key: "am", label: "AM" }), Plan.lane({ key: "pm", label: "PM" })],
2718
3448
  // `a.day` is a FloatType field — the builder wraps it to the number arm.
@@ -2763,24 +3493,23 @@ export const planNumberAxis = example({
2763
3493
  }),
2764
3494
  ], ArrayType(Plan.Types.Series(OpsRow)));
2765
3495
  // The slice's horizon — twelve days of orders behind an eight-day
2766
- // window. Its range field is a FLOAT, so the brush, the range chip
2767
- // and the window keys write the slice's `float` arm.
3496
+ // window. Its range field is an INTEGER day, so the brush, the
3497
+ // range chip and the window keys write the slice's `integer` arm:
3498
+ // a closed range, both ends inclusive — the eight days `[1, 9)`
3499
+ // are `1–8`.
2768
3500
  const horizon = $.let(East.Array.generate(24n, HorizonRow, (_$, i) => ({
2769
3501
  key: East.str `o${East.print(i.add(1n))}`,
2770
- day: i.divide(2n).toFloat().add(1.0),
3502
+ day: i.divide(2n).add(1n),
2771
3503
  line: i.remainder(2n).equal(0n).ifElse(() => "Line 1", () => "Line 2"),
2772
3504
  })));
2773
3505
  const slice = $.let(Slice.bind([HorizonRow], "ex.plan.number", cfg, Slice.state({
2774
- range: some(variant("float", { from: 1.0, to: 9.0 })),
3506
+ range: some(variant("integer", { from: 1n, to: 8n })),
2775
3507
  }), horizon, none));
2776
3508
  // The declaration: `[1, 9)` ÷ 1 = eight day columns, the divider at 5.
2777
3509
  const axis = $.const(Plan.axis.number({
2778
3510
  window: { min: 1, max: 9 }, step: 1, now: 5, format: Chart.format.number(),
2779
3511
  }));
2780
- return (_jsx(Plan, { axis: axis, data: ops, series: series, slice: { slice, affordances: ["filter", "range", "brush", "summary"] }, footer: [
2781
- { text: "8 DAYS · STEP 1 · NOW 5" },
2782
- { text: "NUMBER AXIS", end: true },
2783
- ] }));
3512
+ return (_jsx(Plan, { axis: axis, data: ops, series: series, slice: { slice, affordances: ["filter", "range", "brush", "summary"] }, footer: [{ text: "8 STEPS · NOW 5" }] }));
2784
3513
  } }));
2785
3514
  }),
2786
3515
  inputs: [],
@@ -2816,7 +3545,7 @@ export const planOrdinalAxis = example({
2816
3545
  const PointRow = StructType({ phase: StringType, n: FloatType });
2817
3546
  const RawCell = StructType({ at: StringType, value: OptionType(FloatType) });
2818
3547
  const OrderRow = StructType({
2819
- series: StringType, label: StringType, value: OptionType(StringType),
3548
+ series: StringType, label: StringType, value: OptionType(StringType), sub: OptionType(StringType),
2820
3549
  jobs: ArrayType(JobRow), allocations: ArrayType(AllocRow), shifts: ArrayType(ShiftRow),
2821
3550
  marks: ArrayType(MarkRow), points: ArrayType(PointRow), counts: ArrayType(RawCell),
2822
3551
  cells: ArrayType(Plan.Types.HeatCell),
@@ -2844,31 +3573,31 @@ export const planOrdinalAxis = example({
2844
3573
  return { at: Plan.at.ordinal(PHASES.get(i)), value: some(load), label: some(East.Float.printFixed(load, 0n)) };
2845
3574
  }));
2846
3575
  const base = {
2847
- value: none, jobs: noJobs, allocations: noAllocs, shifts: noShifts,
3576
+ value: none, sub: none, jobs: noJobs, allocations: noAllocs, shifts: noShifts,
2848
3577
  marks: noMarks, points: noPoints, counts: noCounts, cells: noCells,
2849
3578
  };
2850
3579
  const orders = $.const(new Map([
2851
- ["10-or-1188", { ...base, series: "order", label: "OR-1188", value: some("96 t"), jobs: [
3580
+ ["or-1188", { ...base, series: "order", label: "OR-1188", value: some("96 t"), jobs: [
2852
3581
  { key: "prep", label: "PREP", start: "INTAKE", end: "PREP", state: ACTUAL },
2853
3582
  { key: "build", label: "BUILD · B-214", start: "BUILD", end: "QC", state: RUNNING },
2854
3583
  { key: "ship", label: "PACK + SHIP", start: "PACK", end: "SHIP", state: PROPOSED },
2855
3584
  ] }],
2856
- ["10-or-1204", { ...base, series: "order", label: "OR-1204", value: some("54 t"), jobs: [
3585
+ ["or-1204", { ...base, series: "order", label: "OR-1204", value: some("54 t"), jobs: [
2857
3586
  { key: "intake", label: "INTAKE", start: "INTAKE", end: "INTAKE", state: ACTUAL },
2858
3587
  { key: "build", label: "BUILD · B-221", start: "PREP", end: "PACK", state: PROPOSED },
2859
3588
  ] }],
2860
- ["20-bench", { ...base, series: "bench", label: "Bench 2", value: some("slots"), allocations: [
3589
+ ["bench", { ...base, series: "bench", label: "Bench 2", sub: some("slots"), allocations: [
2861
3590
  { key: "a1", phase: "PREP", state: CONFIRMED }, { key: "a2", phase: "BUILD", state: CONFIRMED },
2862
3591
  { key: "a3", phase: "BUILD", state: PROPOSED }, { key: "a4", phase: "PACK", state: PROPOSED },
2863
3592
  ] }],
2864
- ["30-load", { ...base, series: "load", label: "Phase load", cells }],
2865
- ["40-wip", { ...base, series: "wip", label: "WIP · orders", value: some("31"), points }],
2866
- ["50-count", { ...base, series: "count", label: "Orders in phase", counts }],
2867
- ["60-crew", { ...base, series: "crew", label: "Crew B", shifts: [
3593
+ ["load", { ...base, series: "load", label: "Phase load", cells }],
3594
+ ["wip", { ...base, series: "wip", label: "WIP · orders", value: some("31"), points }],
3595
+ ["count", { ...base, series: "count", label: "Orders in phase", counts }],
3596
+ ["crew", { ...base, series: "crew", label: "Crew B", shifts: [
2868
3597
  { key: "s1", from: "INTAKE", to: "PREP", label: "prep crew", state: CONFIRMED },
2869
3598
  { key: "s2", from: "BUILD", to: "SHIP", label: "+ finish crew", state: PROPOSED },
2870
3599
  ] }],
2871
- ["70-gates", { ...base, series: "gates", label: "GATES", marks: [
3600
+ ["gates", { ...base, series: "gates", label: "Gates", marks: [
2872
3601
  { key: "g1", phase: "QC", label: "HOLD", exception: true },
2873
3602
  { key: "g2", phase: "SHIP", label: "RELEASE", exception: false },
2874
3603
  ] }],
@@ -2876,7 +3605,7 @@ export const planOrdinalAxis = example({
2876
3605
  const EXCEPTION = $.const(variant("exception", null), Plan.Types.EventMarkKind);
2877
3606
  const MILESTONE = $.const(variant("milestone", null), Plan.Types.EventMarkKind);
2878
3607
  const series = $.const([
2879
- Plan.series.group(OrderRow, { key: "10-orders", label: "Orders", meta: "2 rs" }, [
3608
+ Plan.series.section(OrderRow, { key: "order-block", title: "Orders", meta: "2 rows" }, [
2880
3609
  Plan.series.span(OrderRow, {
2881
3610
  key: "order", title: "Orders",
2882
3611
  match: r => r.series.equal("order"),
@@ -2888,7 +3617,7 @@ export const planOrdinalAxis = example({
2888
3617
  Plan.series.buckets(OrderRow, {
2889
3618
  key: "bench", title: "Benches",
2890
3619
  match: r => r.series.equal("bench"),
2891
- label: r => r.label, value: r => r.value,
3620
+ label: r => r.label, sub: r => r.sub,
2892
3621
  events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.phase, state: a.state })),
2893
3622
  }),
2894
3623
  Plan.series.heat(OrderRow, {
@@ -2929,10 +3658,7 @@ export const planOrdinalAxis = example({
2929
3658
  ], ArrayType(Plan.Types.Series(OrderRow)));
2930
3659
  // The declaration: the list IS the axis — one bucket per phase, `now` at BUILD.
2931
3660
  const axis = $.const(Plan.axis.ordinal({ values: PHASES, now: "BUILD" }));
2932
- return (_jsx(Plan, { axis: axis, data: orders, series: series, footer: [
2933
- { text: "6 PHASES · ORDINAL AXIS" },
2934
- { text: "NOW · BUILD", end: true },
2935
- ] }));
3661
+ return (_jsx(Plan, { axis: axis, data: orders, series: series, footer: [{ text: "6 PHASES · NOW BUILD" }] }));
2936
3662
  }),
2937
3663
  inputs: [],
2938
3664
  });