@elaraai/east-ui 1.0.68 → 1.0.69

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 (306) hide show
  1. package/README.md +2 -2
  2. package/dist/src/charts/chart/index.d.ts +39 -4
  3. package/dist/src/charts/chart/index.d.ts.map +1 -1
  4. package/dist/src/charts/chart/index.js +3 -3
  5. package/dist/src/charts/chart/index.js.map +1 -1
  6. package/dist/src/charts/spec/index.d.ts +61 -3
  7. package/dist/src/charts/spec/index.d.ts.map +1 -1
  8. package/dist/src/charts/spec/types.d.ts +1 -1
  9. package/dist/src/charts/spec/types.js +1 -1
  10. package/dist/src/collections/blend/index.d.ts +1 -1
  11. package/dist/src/collections/blend/index.d.ts.map +1 -1
  12. package/dist/src/collections/blend/index.js +1 -1
  13. package/dist/src/collections/blend/index.js.map +1 -1
  14. package/dist/src/collections/blend/types.js +1 -1
  15. package/dist/src/collections/blend/types.js.map +1 -1
  16. package/dist/src/collections/board/index.d.ts +4 -4
  17. package/dist/src/collections/board/index.d.ts.map +1 -1
  18. package/dist/src/collections/board/index.js +1 -1
  19. package/dist/src/collections/board/index.js.map +1 -1
  20. package/dist/src/collections/board/types.js +1 -1
  21. package/dist/src/collections/board/types.js.map +1 -1
  22. package/dist/src/collections/calendar/index.d.ts +1 -1
  23. package/dist/src/collections/calendar/index.d.ts.map +1 -1
  24. package/dist/src/collections/deck/index.d.ts +58 -0
  25. package/dist/src/collections/deck/index.d.ts.map +1 -1
  26. package/dist/src/collections/index.d.ts +1 -2
  27. package/dist/src/collections/index.d.ts.map +1 -1
  28. package/dist/src/collections/index.js +1 -2
  29. package/dist/src/collections/index.js.map +1 -1
  30. package/dist/src/collections/library/index.d.ts +29 -0
  31. package/dist/src/collections/library/index.d.ts.map +1 -1
  32. package/dist/src/collections/library/types.d.ts +29 -0
  33. package/dist/src/collections/library/types.d.ts.map +1 -1
  34. package/dist/src/collections/matrix/index.d.ts +1 -1
  35. package/dist/src/collections/matrix/index.d.ts.map +1 -1
  36. package/dist/src/collections/plan/assemble.d.ts +3732 -0
  37. package/dist/src/collections/plan/assemble.d.ts.map +1 -0
  38. package/dist/src/collections/plan/assemble.js +187 -0
  39. package/dist/src/collections/plan/assemble.js.map +1 -0
  40. package/dist/src/collections/plan/builders.d.ts +612 -0
  41. package/dist/src/collections/plan/builders.d.ts.map +1 -0
  42. package/dist/src/collections/plan/builders.js +543 -0
  43. package/dist/src/collections/plan/builders.js.map +1 -0
  44. package/dist/src/collections/plan/data-forms.d.ts +332 -0
  45. package/dist/src/collections/plan/data-forms.d.ts.map +1 -0
  46. package/dist/src/collections/plan/data-forms.js +212 -0
  47. package/dist/src/collections/plan/data-forms.js.map +1 -0
  48. package/dist/src/collections/plan/factories.d.ts +338 -0
  49. package/dist/src/collections/plan/factories.d.ts.map +1 -0
  50. package/dist/src/collections/plan/factories.js +414 -0
  51. package/dist/src/collections/plan/factories.js.map +1 -0
  52. package/dist/src/collections/plan/index.d.ts +271 -0
  53. package/dist/src/collections/plan/index.d.ts.map +1 -0
  54. package/dist/src/collections/plan/index.js +162 -0
  55. package/dist/src/collections/plan/index.js.map +1 -0
  56. package/dist/src/collections/plan/ir.d.ts +3496 -0
  57. package/dist/src/collections/plan/ir.d.ts.map +1 -0
  58. package/dist/src/collections/plan/ir.js +83 -0
  59. package/dist/src/collections/plan/ir.js.map +1 -0
  60. package/dist/src/collections/plan/pick.d.ts +115 -0
  61. package/dist/src/collections/plan/pick.d.ts.map +1 -0
  62. package/dist/src/collections/plan/pick.js +193 -0
  63. package/dist/src/collections/plan/pick.js.map +1 -0
  64. package/dist/src/collections/plan/root.d.ts +210 -0
  65. package/dist/src/collections/plan/root.d.ts.map +1 -0
  66. package/dist/src/collections/plan/root.js +146 -0
  67. package/dist/src/collections/plan/root.js.map +1 -0
  68. package/dist/src/collections/plan/series.d.ts +11890 -0
  69. package/dist/src/collections/plan/series.d.ts.map +1 -0
  70. package/dist/src/collections/plan/series.js +412 -0
  71. package/dist/src/collections/plan/series.js.map +1 -0
  72. package/dist/src/collections/plan/types.d.ts +9291 -0
  73. package/dist/src/collections/plan/types.d.ts.map +1 -0
  74. package/dist/src/collections/plan/types.js +940 -0
  75. package/dist/src/collections/plan/types.js.map +1 -0
  76. package/dist/src/collections/roster/index.d.ts +4 -4
  77. package/dist/src/collections/roster/index.d.ts.map +1 -1
  78. package/dist/src/collections/roster/index.js +1 -1
  79. package/dist/src/collections/roster/index.js.map +1 -1
  80. package/dist/src/collections/roster/types.d.ts +1 -1
  81. package/dist/src/collections/roster/types.d.ts.map +1 -1
  82. package/dist/src/collections/roster/types.js +4 -3
  83. package/dist/src/collections/roster/types.js.map +1 -1
  84. package/dist/src/collections/table/index.d.ts +70 -2
  85. package/dist/src/collections/table/index.d.ts.map +1 -1
  86. package/dist/src/collections/table/index.js +55 -11
  87. package/dist/src/collections/table/index.js.map +1 -1
  88. package/dist/src/collections/table/types.d.ts +1 -1
  89. package/dist/src/collections/table/types.d.ts.map +1 -1
  90. package/dist/src/component.d.ts +3898 -1339
  91. package/dist/src/component.d.ts.map +1 -1
  92. package/dist/src/component.js +83 -146
  93. package/dist/src/component.js.map +1 -1
  94. package/dist/src/container/card/types.d.ts +1 -1
  95. package/dist/src/container/card/types.d.ts.map +1 -1
  96. package/dist/src/contracts/approval.d.ts +117 -0
  97. package/dist/src/contracts/approval.d.ts.map +1 -0
  98. package/dist/src/contracts/approval.js +105 -0
  99. package/dist/src/contracts/approval.js.map +1 -0
  100. package/dist/src/contracts/drag.d.ts +3 -3
  101. package/dist/src/contracts/drag.d.ts.map +1 -1
  102. package/dist/src/contracts/drag.js +23 -10
  103. package/dist/src/contracts/drag.js.map +1 -1
  104. package/dist/src/contracts/index.d.ts +4 -1
  105. package/dist/src/contracts/index.d.ts.map +1 -1
  106. package/dist/src/contracts/index.js +4 -1
  107. package/dist/src/contracts/index.js.map +1 -1
  108. package/dist/src/contracts/pick.d.ts +705 -0
  109. package/dist/src/contracts/pick.d.ts.map +1 -0
  110. package/dist/src/contracts/pick.js +360 -0
  111. package/dist/src/contracts/pick.js.map +1 -0
  112. package/dist/src/contracts/review.d.ts +7 -99
  113. package/dist/src/contracts/review.d.ts.map +1 -1
  114. package/dist/src/contracts/review.js +11 -85
  115. package/dist/src/contracts/review.js.map +1 -1
  116. package/dist/src/contracts/slice-affordances.d.ts +13 -4
  117. package/dist/src/contracts/slice-affordances.d.ts.map +1 -1
  118. package/dist/src/contracts/slice-affordances.js +13 -3
  119. package/dist/src/contracts/slice-affordances.js.map +1 -1
  120. package/dist/src/contracts/source.d.ts +432 -0
  121. package/dist/src/contracts/source.d.ts.map +1 -0
  122. package/dist/src/contracts/source.js +505 -0
  123. package/dist/src/contracts/source.js.map +1 -0
  124. package/dist/src/contracts/states.d.ts +130 -0
  125. package/dist/src/contracts/states.d.ts.map +1 -1
  126. package/dist/src/contracts/states.js +122 -0
  127. package/dist/src/contracts/states.js.map +1 -1
  128. package/dist/src/contracts/time.d.ts +76 -0
  129. package/dist/src/contracts/time.d.ts.map +1 -0
  130. package/dist/src/contracts/time.js +78 -0
  131. package/dist/src/contracts/time.js.map +1 -0
  132. package/dist/src/display/trace/types.d.ts +1 -1
  133. package/dist/src/display/trace/types.d.ts.map +1 -1
  134. package/dist/src/index.d.ts +10 -2
  135. package/dist/src/index.d.ts.map +1 -1
  136. package/dist/src/index.js +22 -2
  137. package/dist/src/index.js.map +1 -1
  138. package/dist/src/internal.d.ts +5 -3
  139. package/dist/src/internal.d.ts.map +1 -1
  140. package/dist/src/internal.js +4 -3
  141. package/dist/src/internal.js.map +1 -1
  142. package/dist/src/layout/dock/index.d.ts +1 -1
  143. package/dist/src/layout/dock/index.js +1 -1
  144. package/dist/src/layout/index.d.ts +0 -1
  145. package/dist/src/layout/index.d.ts.map +1 -1
  146. package/dist/src/layout/index.js +0 -1
  147. package/dist/src/layout/index.js.map +1 -1
  148. package/dist/src/overlays/drawer/types.d.ts +1 -1
  149. package/dist/src/overlays/drawer/types.d.ts.map +1 -1
  150. package/dist/src/pick/index.d.ts +443 -0
  151. package/dist/src/pick/index.d.ts.map +1 -0
  152. package/dist/src/pick/index.js +64 -0
  153. package/dist/src/pick/index.js.map +1 -0
  154. package/dist/src/pick/panel/index.d.ts +64 -0
  155. package/dist/src/pick/panel/index.d.ts.map +1 -0
  156. package/dist/src/pick/panel/index.js +61 -0
  157. package/dist/src/pick/panel/index.js.map +1 -0
  158. package/dist/src/platform/slice/index.d.ts +291 -0
  159. package/dist/src/platform/slice/index.d.ts.map +1 -1
  160. package/dist/src/platform/slice/index.js +16 -0
  161. package/dist/src/platform/slice/index.js.map +1 -1
  162. package/dist/src/runtime/collections/deck.d.ts +29 -0
  163. package/dist/src/runtime/collections/deck.d.ts.map +1 -1
  164. package/dist/src/runtime/collections/index.d.ts +1 -2
  165. package/dist/src/runtime/collections/index.d.ts.map +1 -1
  166. package/dist/src/runtime/collections/index.js +1 -2
  167. package/dist/src/runtime/collections/index.js.map +1 -1
  168. package/dist/src/runtime/collections/library.d.ts +1 -1
  169. package/dist/src/runtime/collections/library.js +1 -1
  170. package/dist/src/runtime/collections/plan.d.ts +98 -0
  171. package/dist/src/runtime/collections/plan.d.ts.map +1 -0
  172. package/dist/src/runtime/collections/plan.js +98 -0
  173. package/dist/src/runtime/collections/plan.js.map +1 -0
  174. package/dist/src/runtime/collections/schematic.d.ts +1 -1
  175. package/dist/src/runtime/collections/schematic.js +1 -1
  176. package/dist/src/runtime/collections/table.d.ts +9 -1
  177. package/dist/src/runtime/collections/table.d.ts.map +1 -1
  178. package/dist/src/runtime/collections/table.js +1 -7
  179. package/dist/src/runtime/collections/table.js.map +1 -1
  180. package/dist/src/runtime/layout/dock.d.ts +2 -2
  181. package/dist/src/runtime/layout/dock.js +2 -2
  182. package/dist/src/runtime/layout/index.d.ts +0 -1
  183. package/dist/src/runtime/layout/index.d.ts.map +1 -1
  184. package/dist/src/runtime/layout/index.js +0 -1
  185. package/dist/src/runtime/layout/index.js.map +1 -1
  186. package/dist/src/shared/plot-gutter.d.ts +6 -23
  187. package/dist/src/shared/plot-gutter.d.ts.map +1 -1
  188. package/dist/src/shared/plot-gutter.js +6 -19
  189. package/dist/src/shared/plot-gutter.js.map +1 -1
  190. package/dist/src/shared/reify.d.ts +47 -4
  191. package/dist/src/shared/reify.d.ts.map +1 -1
  192. package/dist/src/shared/reify.js +62 -4
  193. package/dist/src/shared/reify.js.map +1 -1
  194. package/dist/src/slice/breakdown/index.d.ts +27 -0
  195. package/dist/src/slice/breakdown/index.d.ts.map +1 -1
  196. package/dist/src/slice/breakdown/types.d.ts +27 -0
  197. package/dist/src/slice/breakdown/types.d.ts.map +1 -1
  198. package/dist/src/slice/cohort/index.d.ts +54 -0
  199. package/dist/src/slice/cohort/index.d.ts.map +1 -1
  200. package/dist/src/slice/cohort/types.d.ts +27 -0
  201. package/dist/src/slice/cohort/types.d.ts.map +1 -1
  202. package/dist/src/slice/filter/index.d.ts +27 -0
  203. package/dist/src/slice/filter/index.d.ts.map +1 -1
  204. package/dist/src/slice/filter/types.d.ts +27 -0
  205. package/dist/src/slice/filter/types.d.ts.map +1 -1
  206. package/dist/src/slice/index.d.ts +405 -0
  207. package/dist/src/slice/index.d.ts.map +1 -1
  208. package/dist/src/slice/legend/index.d.ts +27 -0
  209. package/dist/src/slice/legend/index.d.ts.map +1 -1
  210. package/dist/src/slice/legend/types.d.ts +27 -0
  211. package/dist/src/slice/legend/types.d.ts.map +1 -1
  212. package/dist/src/slice/range/index.d.ts +60 -18
  213. package/dist/src/slice/range/index.d.ts.map +1 -1
  214. package/dist/src/slice/range/index.js +9 -1
  215. package/dist/src/slice/range/index.js.map +1 -1
  216. package/dist/src/slice/range/types.d.ts +63 -22
  217. package/dist/src/slice/range/types.d.ts.map +1 -1
  218. package/dist/src/slice/range/types.js +12 -5
  219. package/dist/src/slice/range/types.js.map +1 -1
  220. package/dist/src/slice/search/index.d.ts +27 -0
  221. package/dist/src/slice/search/index.d.ts.map +1 -1
  222. package/dist/src/slice/search/types.d.ts +27 -0
  223. package/dist/src/slice/search/types.d.ts.map +1 -1
  224. package/dist/src/slice/summary/index.d.ts +27 -0
  225. package/dist/src/slice/summary/index.d.ts.map +1 -1
  226. package/dist/src/slice/summary/types.d.ts +27 -0
  227. package/dist/src/slice/summary/types.d.ts.map +1 -1
  228. package/dist/src/style/content.d.ts +4 -6
  229. package/dist/src/style/content.d.ts.map +1 -1
  230. package/dist/src/style/content.js +4 -6
  231. package/dist/src/style/content.js.map +1 -1
  232. package/dist/src/style/scheme.js +1 -1
  233. package/dist/src/style/scheme.js.map +1 -1
  234. package/dist/test/collections/index.d.ts +2 -2
  235. package/dist/test/collections/index.d.ts.map +1 -1
  236. package/dist/test/collections/index.js +2 -2
  237. package/dist/test/collections/index.js.map +1 -1
  238. package/dist/test/collections/paged-source.examples.d.ts +9 -0
  239. package/dist/test/collections/paged-source.examples.d.ts.map +1 -0
  240. package/dist/test/collections/paged-source.examples.js +136 -0
  241. package/dist/test/collections/paged-source.examples.js.map +1 -0
  242. package/dist/test/collections/plan.examples.d.ts +125 -0
  243. package/dist/test/collections/plan.examples.d.ts.map +1 -0
  244. package/dist/test/collections/plan.examples.js +2939 -0
  245. package/dist/test/collections/plan.examples.js.map +1 -0
  246. package/dist/test/contracts/pick.examples.d.ts +6 -0
  247. package/dist/test/contracts/pick.examples.d.ts.map +1 -0
  248. package/dist/test/contracts/pick.examples.js +89 -0
  249. package/dist/test/contracts/pick.examples.js.map +1 -0
  250. package/dist/test/layout/dock.examples.d.ts +4 -4
  251. package/dist/test/layout/dock.examples.d.ts.map +1 -1
  252. package/dist/test/layout/dock.examples.js +34 -16
  253. package/dist/test/layout/dock.examples.js.map +1 -1
  254. package/dist/test/slice/slice.examples.d.ts +2 -1
  255. package/dist/test/slice/slice.examples.d.ts.map +1 -1
  256. package/dist/test/slice/slice.examples.js +67 -9
  257. package/dist/test/slice/slice.examples.js.map +1 -1
  258. package/package.json +3 -3
  259. package/dist/src/collections/gantt/index.d.ts +0 -377
  260. package/dist/src/collections/gantt/index.d.ts.map +0 -1
  261. package/dist/src/collections/gantt/index.js +0 -896
  262. package/dist/src/collections/gantt/index.js.map +0 -1
  263. package/dist/src/collections/gantt/types.d.ts +0 -353
  264. package/dist/src/collections/gantt/types.d.ts.map +0 -1
  265. package/dist/src/collections/gantt/types.js +0 -207
  266. package/dist/src/collections/gantt/types.js.map +0 -1
  267. package/dist/src/collections/planner/index.d.ts +0 -624
  268. package/dist/src/collections/planner/index.d.ts.map +0 -1
  269. package/dist/src/collections/planner/index.js +0 -941
  270. package/dist/src/collections/planner/index.js.map +0 -1
  271. package/dist/src/collections/planner/types.d.ts +0 -615
  272. package/dist/src/collections/planner/types.d.ts.map +0 -1
  273. package/dist/src/collections/planner/types.js +0 -423
  274. package/dist/src/collections/planner/types.js.map +0 -1
  275. package/dist/src/layout/aligned-stack/index.d.ts +0 -101
  276. package/dist/src/layout/aligned-stack/index.d.ts.map +0 -1
  277. package/dist/src/layout/aligned-stack/index.js +0 -105
  278. package/dist/src/layout/aligned-stack/index.js.map +0 -1
  279. package/dist/src/layout/aligned-stack/types.d.ts +0 -50
  280. package/dist/src/layout/aligned-stack/types.d.ts.map +0 -1
  281. package/dist/src/layout/aligned-stack/types.js +0 -24
  282. package/dist/src/layout/aligned-stack/types.js.map +0 -1
  283. package/dist/src/runtime/collections/gantt.d.ts +0 -67
  284. package/dist/src/runtime/collections/gantt.d.ts.map +0 -1
  285. package/dist/src/runtime/collections/gantt.js +0 -58
  286. package/dist/src/runtime/collections/gantt.js.map +0 -1
  287. package/dist/src/runtime/collections/planner.d.ts +0 -80
  288. package/dist/src/runtime/collections/planner.d.ts.map +0 -1
  289. package/dist/src/runtime/collections/planner.js +0 -76
  290. package/dist/src/runtime/collections/planner.js.map +0 -1
  291. package/dist/src/runtime/layout/aligned-stack.d.ts +0 -38
  292. package/dist/src/runtime/layout/aligned-stack.d.ts.map +0 -1
  293. package/dist/src/runtime/layout/aligned-stack.js +0 -36
  294. package/dist/src/runtime/layout/aligned-stack.js.map +0 -1
  295. package/dist/test/collections/gantt.examples.d.ts +0 -33
  296. package/dist/test/collections/gantt.examples.d.ts.map +0 -1
  297. package/dist/test/collections/gantt.examples.js +0 -359
  298. package/dist/test/collections/gantt.examples.js.map +0 -1
  299. package/dist/test/collections/planner.examples.d.ts +0 -47
  300. package/dist/test/collections/planner.examples.d.ts.map +0 -1
  301. package/dist/test/collections/planner.examples.js +0 -546
  302. package/dist/test/collections/planner.examples.js.map +0 -1
  303. package/dist/test/layout/aligned-stack.examples.d.ts +0 -25
  304. package/dist/test/layout/aligned-stack.examples.d.ts.map +0 -1
  305. package/dist/test/layout/aligned-stack.examples.js +0 -294
  306. package/dist/test/layout/aligned-stack.examples.js.map +0 -1
@@ -0,0 +1,2939 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@elaraai/east-ui/jsx-runtime";
2
+ /**
3
+ * Copyright (c) 2025 Elara AI Pty Ltd
4
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
5
+ */
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";
10
+ // The corpus — every canvas is DEFINED the one way (`Plan Data Interface.md`
11
+ // §3.5): `data` (RAW domain rows — batches, tonnes, lifecycle states; row
12
+ // series discriminated by a field; no factory-built values in the data) +
13
+ // `series` (one `Plan.series.*` value per row series, `$.const`-bound and typed
14
+ // by the `Plan.Types.Series(Row)` constructor — its accessors DERIVE the
15
+ // canvas vocabulary from the raw fields client-side: labels, quantity
16
+ // displays, element values via the `Plan.run`/`chip`/`event` expression
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.
20
+ // Fixtures are defined INLINE in each example's fn body (the automated docs
21
+ // extract only the fn body) and DERIVED with East expressions — a `week(n)`
22
+ // ISO-week function plus `East.Array.generate` — never hand-written per
23
+ // element; only individually-meaningful mock records stay literal, and
24
+ // lifecycle states are plain `EventStateType` variants (the shared contracts
25
+ // vocabulary a plan dataset stores).
26
+ // ============================================================================
27
+ // planTargetState — the §1 flagship (every row kind, ONE source, series)
28
+ // ============================================================================
29
+ export const planTargetState = example({
30
+ keywords: [
31
+ "Plan", "canvas", "data", "series", "match", "axis", "window", "resolution", "now",
32
+ "span", "run", "group", "heat", "buckets", "cards", "chip", "events", "mark", "chart",
33
+ "layers", "rollup", "bands", "review", "footer", "milestone",
34
+ "decision", "exception", "pinned", "port", "hovercard", "popover",
35
+ "slice", "brush", "horizon", "toolbar", "affordances", "expand",
36
+ "expandRender", "resolver", "data-driven", "accessor", "raw", "target state",
37
+ ],
38
+ description: "Every row kind on one axis from a single raw ops source, with slice chrome, expand, review and a status footer",
39
+ fn: East.function([], UIComponentType, (_$) => {
40
+ // The horizon fixture — the despatch orders the slice narrows (the
41
+ // canvas rows are the ops source; the slice is chrome over THESE):
42
+ // when, which line, whether the order is at risk of running late,
43
+ // and the tonnage booked so far (0 = nothing yet).
44
+ const HorizonRow = StructType({ key: StringType, at: DateTimeType, line: StringType, risk: StringType, tonnes: FloatType });
45
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
46
+ // The RAW job record — what an ops dataset stores: phase + optional
47
+ // batch, the window, optional tonnage, the lifecycle state, an alert.
48
+ const JobRow = StructType({
49
+ key: StringType, phase: StringType, batch: OptionType(StringType),
50
+ start: DateTimeType, end: DateTimeType,
51
+ tonnes: OptionType(FloatType), state: EventStateType,
52
+ alert: OptionType(StatusValueType),
53
+ });
54
+ const ShiftRow = StructType({
55
+ key: StringType, from: DateTimeType, to: DateTimeType,
56
+ hours: FloatType, state: EventStateType,
57
+ });
58
+ const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
59
+ // ONE source — row series discriminated by the kind variant (the
60
+ // natural ops-dataset shape; the same rows page from a dataset). The
61
+ // arms carry RAW fields; presence (status / expand declaration) is
62
+ // per-row Option DATA the accessors pass through.
63
+ const OpsRow = StructType({
64
+ kind: VariantType({
65
+ kpi: StructType({ name: StringType, headline: StringType, pinned: BooleanType,
66
+ points: ArrayType(MeasureRow) }),
67
+ machine: StructType({ cap: FloatType,
68
+ status: OptionType(StatusValueType),
69
+ detail: OptionType(Plan.Types.Expand),
70
+ jobs: ArrayType(JobRow),
71
+ decisions: ArrayType(Plan.Types.DecisionMark),
72
+ ports: ArrayType(Plan.Types.Port) }),
73
+ load: StructType({ name: StringType, sub: StringType, cells: ArrayType(Plan.Types.HeatCell) }),
74
+ dock: StructType({ name: StringType, allocations: ArrayType(AllocRow),
75
+ markers: ArrayType(Plan.Types.CellMarker) }),
76
+ crew: StructType({ name: StringType, hours: StringType, shifts: ArrayType(ShiftRow) }),
77
+ stream: StructType({ name: StringType, marks: ArrayType(Plan.Types.EventMark) }),
78
+ }),
79
+ });
80
+ const cfg = Slice.config(HorizonRow, {
81
+ fields: {
82
+ at: { label: "Despatched", format: { date: "MMM D" } },
83
+ line: { label: "Line" },
84
+ risk: { label: "Risk", hints: ["late", "on-time"] },
85
+ tonnes: { label: "Tonnes" },
86
+ },
87
+ rangeFieldId: "at",
88
+ searchFieldIds: ["line"],
89
+ });
90
+ return (_jsx(Reactive, { children: $ => {
91
+ // Monday of ISO week n, 2026 (W1 Monday = 2025-12-29). The §1
92
+ // window is W27–W38 (half-open at W39); now = W31.
93
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
94
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
95
+ return w1.addWeeks(n.subtract(1n));
96
+ }));
97
+ const covPcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
98
+ 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));
99
+ const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: covPcts.get(i) })));
100
+ const loadCells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
101
+ at: Plan.at.time(week(i.add(27n))), value: some(loadPcts.get(i)),
102
+ label: some(East.Float.printFixed(loadPcts.get(i), 0n)),
103
+ })));
104
+ // 36 despatch orders spread over W21–W47 — the horizon fixture.
105
+ // Every third order is at risk of running late (the §1 `Late
106
+ // risk` cohort counts 12); every seventh has no tonnage booked
107
+ // yet (the `Empty` cohort).
108
+ const horizon = $.let(East.Array.generate(36n, HorizonRow, (_$, i) => ({
109
+ key: East.str `h${East.print(i.add(1n))}`,
110
+ at: week(i.multiply(27n).divide(36n).add(21n)),
111
+ line: i.remainder(2n).equal(0n).ifElse(() => "Line 1", () => "Line 2"),
112
+ risk: i.remainder(3n).equal(0n).ifElse(() => "late", () => "on-time"),
113
+ tonnes: i.remainder(7n).equal(6n).ifElse(() => 0.0, () => i.multiply(11n).remainder(40n).toFloat().add(20.0)),
114
+ })));
115
+ // 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
+ const jobRuns = $.const(East.function([ArrayType(JobRow)], ArrayType(Plan.Types.Run), (_$, jobs) => jobs.map(($, j) => {
122
+ const noQuantity = $.const(none, OptionType(StringType));
123
+ const quantity = $.let(j.tonnes.match({
124
+ some: (_$, t) => East.value(some(East.str `${East.Float.printFixed(t, 0n)} t`), OptionType(StringType)),
125
+ none: (_$) => noQuantity,
126
+ }), OptionType(StringType));
127
+ const label = $.let(j.batch.match({
128
+ some: (_$, b) => East.str `${j.phase} · ${b}`,
129
+ none: (_$) => j.phase,
130
+ }), StringType);
131
+ const run = $.let({
132
+ 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,
134
+ moved: none, icon: none,
135
+ }, Plan.Types.Run);
136
+ return run;
137
+ })));
138
+ // Raw shifts → chips: hours print as the chip label, an ADDED
139
+ // proposal wearing the `+` prefix — display derives from the
140
+ // lifecycle, down to the proposal's flavour (a `removed` shift is
141
+ // a proposal too, and `+` would read as its opposite).
142
+ const shiftChips = $.const(East.function([ArrayType(ShiftRow)], ArrayType(Plan.Types.Chip), (_$, shifts) => shifts.map(($, s) => {
143
+ const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
144
+ const label = $.let(s.state.match({
145
+ proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
146
+ }, _$ => East.str `${hrs}h`), StringType);
147
+ return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
148
+ })));
149
+ // The ONE ops source — every series' rows in one KEYED collection,
150
+ // 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.
155
+ const ops = $.const(new Map([
156
+ ["coverage", { kind: variant("kpi", { name: "COVERAGE", headline: "94.2%", pinned: true, points: coverage }) }],
157
+ ["L1-M03", { kind: variant("machine", { cap: 120.0, status: some(variant("success", null)), detail: none,
158
+ jobs: [
159
+ { key: "set", phase: "SET", batch: none, start: week(27n), end: week(28n), tonnes: none, state: variant("actual", null), alert: none },
160
+ { key: "b214", phase: "RUN", batch: some("B-214"), start: week(28n), end: week(31n), tonnes: some(96.0), state: variant("in-progress", null), alert: none },
161
+ { key: "cln", phase: "CLN", batch: none, start: week(31n), end: week(32n), tonnes: none, state: variant("confirmed", null), alert: none },
162
+ { key: "b221", phase: "RUN", batch: some("B-221"), start: week(32n), end: week(35n), tonnes: some(88.0), state: variant("proposed", variant("recommended", null)), alert: none },
163
+ ],
164
+ decisions: [{ key: "d1", at: Plan.at.time(week(32n)), applied: false }],
165
+ ports: [{ at: Plan.at.time(week(31n)), label: some("−24 t") }] }) }],
166
+ ["L1-M04", { kind: variant("machine", { cap: 120.0, status: none,
167
+ // The expand declaration — a stored plain-data record
168
+ // (§3.2); presence is a per-row fact and the ROOT's
169
+ // expandRender mounts the body.
170
+ detail: some({ height: some("152px"), axis: variant("keep", null) }),
171
+ jobs: [
172
+ { key: "b208", phase: "RUN", batch: some("B-208"), start: week(27n), end: week(30n), tonnes: some(112.0), state: variant("actual", null), alert: none },
173
+ { key: "hld", phase: "HLD", batch: none, start: week(30n), end: week(31n), tonnes: none, state: variant("confirmed", null), alert: none },
174
+ { key: "qc", phase: "QC", batch: none, start: week(31n), end: week(33n), tonnes: none, state: variant("confirmed", null), alert: none },
175
+ { key: "b231", phase: "RUN", batch: some("B-231"), start: week(33n), end: week(41n), tonnes: some(104.0), state: variant("proposed", variant("recommended", null)), alert: none },
176
+ ],
177
+ decisions: [], ports: [] }) }],
178
+ ["L1-M07", { kind: variant("machine", { cap: 80.0, status: some(variant("warning", null)), detail: none,
179
+ jobs: [
180
+ { key: "b197", phase: "HLD", batch: some("B-197"), start: week(27n), end: week(31n), tonnes: none, state: variant("actual", null), alert: some(variant("warning", null)) },
181
+ { key: "cln", phase: "CLN", batch: none, start: week(34n), end: week(36n), tonnes: none, state: variant("proposed", variant("recommended", null)), alert: none },
182
+ ],
183
+ decisions: [], ports: [] }) }],
184
+ ["l2-load", { kind: variant("load", { name: "L2 load", sub: "%/wk", cells: loadCells }) }],
185
+ ["dock2", { kind: variant("dock", { name: "Dock 2",
186
+ allocations: [
187
+ { key: "a1", at: week(27n), state: variant("confirmed", null) },
188
+ { key: "a2", at: week(28n), state: variant("confirmed", null) },
189
+ { key: "a3", at: week(29n), state: variant("confirmed", null) },
190
+ { key: "a4", at: week(30n), state: variant("confirmed", null) },
191
+ { key: "a5", at: week(31n), state: variant("proposed", variant("recommended", null)) },
192
+ { key: "a6", at: week(33n), state: variant("proposed", variant("recommended", null)) },
193
+ { key: "a7", at: week(35n), state: variant("confirmed", null) },
194
+ { key: "a8", at: week(35n), state: variant("proposed", variant("recommended", null)) },
195
+ ],
196
+ 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: [
198
+ { key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
199
+ { key: "s2", from: week(29n), to: week(31n), hours: 72.0, state: variant("confirmed", null) },
200
+ { key: "s3", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
201
+ { key: "s4", from: week(34n), to: week(35n), hours: 48.0, state: variant("estimated", null) },
202
+ { key: "s5", from: week(36n), to: week(38n), hours: 56.0, state: variant("proposed", variant("recommended", null)) },
203
+ ] }) }],
204
+ ["50-milestones", { kind: variant("stream", { name: "MILESTONES", marks: [
205
+ { key: "kick", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
206
+ { key: "d1", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
207
+ { key: "rel", at: Plan.at.time(week(33n)), kind: variant("milestone", null), icon: none, label: some("REL 2.4") },
208
+ { key: "audit", at: Plan.at.time(week(35n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
209
+ { key: "d2", at: Plan.at.time(week(37n)), kind: variant("decision", { applied: false }), icon: none, label: some("×3") },
210
+ ] }) }],
211
+ ]), 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.
214
+ const series = $.const([
215
+ Plan.series.chart(OpsRow, {
216
+ key: "chart", title: "Chart",
217
+ match: r => r.kind.hasTag("kpi"),
218
+ label: r => r.kind.unwrap("kpi").name, id: true,
219
+ pinned: r => r.kind.unwrap("kpi").pinned,
220
+ value: r => some(r.kind.unwrap("kpi").headline),
221
+ status: _r => some(variant("warning", null)),
222
+ height: "spark", expandable: true,
223
+ layers: r => [
224
+ Plan.layer(Chart.Line(r.kind.unwrap("kpi").points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
225
+ Chart.refLine({ y: 100, label: "TARGET 100" }),
226
+ ],
227
+ }),
228
+ Plan.series.group(OpsRow, { key: "10-line1", label: "Line 1", meta: "8 rs · 82%" }, [
229
+ Plan.series.span(OpsRow, {
230
+ key: "span", title: "Span",
231
+ match: r => r.kind.hasTag("machine"),
232
+ label: (_r, k) => k, id: true,
233
+ value: r => some(East.str `${East.Float.printFixed(r.kind.unwrap("machine").cap, 0n)} t`),
234
+ status: r => r.kind.unwrap("machine").status,
235
+ expand: r => r.kind.unwrap("machine").detail,
236
+ runs: r => jobRuns(r.kind.unwrap("machine").jobs),
237
+ decisions: r => r.kind.unwrap("machine").decisions,
238
+ ports: r => r.kind.unwrap("machine").ports,
239
+ }),
240
+ ]),
241
+ Plan.series.group(OpsRow, { key: "20-line2", label: "Line 2", value: "98%", status: "warning", collapsed: true, summaryAggregate: "mean" }, [
242
+ Plan.series.heat(OpsRow, {
243
+ key: "heat", title: "Heat",
244
+ match: r => r.kind.hasTag("load"),
245
+ label: r => r.kind.unwrap("load").name,
246
+ sub: r => some(r.kind.unwrap("load").sub),
247
+ cells: r => Plan.heatCells(r.kind.unwrap("load").cells, { min: 0, max: 100, warnAt: 95 }),
248
+ }),
249
+ ]),
250
+ Plan.series.group(OpsRow, { key: "30-docks", label: "Docks · In", meta: "3 rs" }, [
251
+ Plan.series.buckets(OpsRow, {
252
+ key: "buckets", title: "Buckets",
253
+ match: r => r.kind.hasTag("dock"),
254
+ label: r => r.kind.unwrap("dock").name,
255
+ value: _r => some("load/wk"),
256
+ events: r => r.kind.unwrap("dock").allocations.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
257
+ markers: r => r.kind.unwrap("dock").markers,
258
+ }),
259
+ ]),
260
+ Plan.series.cards(OpsRow, {
261
+ key: "cards", title: "Cards",
262
+ match: r => r.kind.hasTag("crew"),
263
+ label: r => r.kind.unwrap("crew").name, stacked: true,
264
+ sub: r => some(r.kind.unwrap("crew").hours),
265
+ chips: r => shiftChips(r.kind.unwrap("crew").shifts),
266
+ }),
267
+ Plan.series.events(OpsRow, {
268
+ key: "events", title: "Events",
269
+ match: r => r.kind.hasTag("stream"),
270
+ label: r => r.kind.unwrap("stream").name, id: true,
271
+ value: r => some(East.print(r.kind.unwrap("stream").marks.length())),
272
+ marks: r => r.kind.unwrap("stream").marks,
273
+ }),
274
+ ], ArrayType(Plan.Types.Series(OpsRow)));
275
+ const axis = $.const(Plan.axis({
276
+ window: { min: week(27n), max: week(39n) },
277
+ resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
278
+ }));
279
+ // 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
281
+ // beside the filter builder with `Late risk` active — their
282
+ // counts are live over the horizon fixture, never printed.
283
+ const slice = $.let(Slice.bind([HorizonRow], "ex.plan.target", cfg, Slice.state({
284
+ range: some(variant("datetime", { from: week(27n), to: week(39n) })),
285
+ cohorts: [
286
+ { id: "late", name: "Late risk", filters: [variant("string", { fieldId: "risk", op: variant("eq", "late") })] },
287
+ { id: "empty", name: "Empty", filters: [variant("float", { fieldId: "tonnes", op: variant("lte", 0.0) })] },
288
+ ],
289
+ activeCohorts: new Set(["late"]),
290
+ }), horizon, none));
291
+ // The R2 developer render — the ROOT's resolver, called with the
292
+ // focused row's ref; ONE function serves every row whose `expand`
293
+ // accessor returned some(...). The machine series declares it.
294
+ 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 }))));
296
+ // 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.
300
+ const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
301
+ const noBody = $.const(none, OptionType(UIComponentType));
302
+ return ref.match({
303
+ 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),
305
+ }, _$ => noBody);
306
+ }));
307
+ const hover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
308
+ const noBody = $.const(none, OptionType(UIComponentType));
309
+ return ref.match({
310
+ run: (_$, ev) => ev.run.equal("b197").ifElse(() => some(_jsx(Text, { children: "Waiting on QC gate 4 \u2014 2.6\u00D7 median dwell." })), () => noBody),
311
+ }, _$ => noBody);
312
+ }));
313
+ // 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));
316
+ const onGroupToggle = $.const(East.function([Plan.Types.GroupToggleEvent], NullType, (_$, _e) => null));
317
+ const onBatch = $.const(East.function([], NullType, (_$) => null));
318
+ 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.
321
+ 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: {
324
+ 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
+ onRerun: onBatch,
330
+ }, expandRender: expandRender, popover: popover, hover: hover, onSelect: onRowRef, onRunClick: onRunClick, onGroupToggle: onGroupToggle, footer: [
331
+ { text: "512 RESOURCES · 12 GROUPS · 3 IN VIEW" },
332
+ { text: "318 OBSERVED · 966 PLANNED" },
333
+ { text: "3 EXCEPTIONS", tone: "warning" },
334
+ { text: "RUN 412 · W27–W38", end: true },
335
+ ] }));
336
+ } }));
337
+ }),
338
+ inputs: [],
339
+ });
340
+ // ============================================================================
341
+ // planVariants — THE Plan configurator (#571): axis presets × style sweeps
342
+ // ============================================================================
343
+ /**
344
+ * THE Plan configurator — the slot-2 variant-space surface the retired Gantt /
345
+ * Planner configurators held (#571). ONE live canvas; every axis feeds it as
346
+ * an expression. The window presets are whole `Plan.axis` VALUES riding a
347
+ * typed preset struct: the ops week window, the year roadmap at month
348
+ * resolution, and the 14-day sprint at day resolution with `ddd DD` labels
349
+ * (the #309 pinned-day-columns contract the Planner's `day` preset and the
350
+ * AlignedStack date-axis panel guarded). The style sweeps ride `style` —
351
+ * density rhythm and gutter width — and every callback (select / run click /
352
+ * group toggle / grain change) logs to the aside, the retired configurators'
353
+ * pattern. Fill sizing stays `planFill`'s; the per-kind visual grammars stay
354
+ * the static per-kind panels.
355
+ */
356
+ export const planVariants = example({
357
+ keywords: [
358
+ "Plan", "configurator", "Configurator", "variants", "preset", "axis", "window",
359
+ "resolution", "month", "week", "day", "roadmap", "sprint", "ops", "format",
360
+ "ddd", "#309", "density", "condensed", "compact", "comfortable", "gutterWidth",
361
+ "gutter", "style", "onSelect", "onRunClick", "onGroupToggle", "onGrainChange",
362
+ "callback", "aside", "Reactive", "State", "SegmentGroup", "Select", "getTag",
363
+ ],
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",
365
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
366
+ // Monday of ISO week n, 2026 — the ops window is W27–W38
367
+ // (half-open at W39), now = W31; the roadmap preset walks the
368
+ // whole ISO year and the sprint preset a 14-day slice of it.
369
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
370
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
371
+ return w1.addWeeks(n.subtract(1n));
372
+ }));
373
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
374
+ const JobRow = StructType({
375
+ key: StringType, label: StringType,
376
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
377
+ });
378
+ const OpsRow = StructType({
379
+ series: StringType, label: StringType,
380
+ sub: OptionType(StringType),
381
+ jobs: ArrayType(JobRow),
382
+ points: ArrayType(MeasureRow),
383
+ cells: ArrayType(Plan.Types.HeatCell),
384
+ });
385
+ const noJobs = $.const([], ArrayType(JobRow));
386
+ const noPoints = $.const([], ArrayType(MeasureRow));
387
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
388
+ const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
389
+ const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
390
+ const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
391
+ at: Plan.at.time(week(i.multiply(2n).add(27n))),
392
+ value: some(pcts.get(i)),
393
+ label: some(East.Float.printFixed(pcts.get(i), 0n)),
394
+ })));
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.
397
+ 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"),
400
+ jobs: [
401
+ { key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) },
402
+ { 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,
405
+ jobs: [
406
+ { 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,
410
+ jobs: [
411
+ { key: "b301", label: "RUN · B-301", start: week(29n), end: week(33n), state: variant("confirmed", null) },
412
+ ], points: noPoints, cells: noCells }],
413
+ ]), DictType(StringType, OpsRow));
414
+ const series = $.const([
415
+ Plan.series.chart(OpsRow, {
416
+ key: "util", title: "Utilisation",
417
+ match: r => r.series.equal("util"),
418
+ label: r => r.label, id: true, height: "spark",
419
+ layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
420
+ }),
421
+ Plan.series.span(OpsRow, {
422
+ key: "mach", title: "Machine jobs",
423
+ match: r => r.series.equal("mach"),
424
+ label: r => r.label, id: true, sub: r => r.sub,
425
+ runs: r => r.jobs.map((_$, j) => Plan.run({
426
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
427
+ })),
428
+ }),
429
+ Plan.series.heat(OpsRow, {
430
+ key: "load", title: "Line load",
431
+ match: r => r.series.equal("load"),
432
+ label: r => r.label, sub: r => r.sub,
433
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
434
+ }),
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" }, [
437
+ Plan.series.span(OpsRow, {
438
+ key: "gmach", title: "Grouped jobs",
439
+ match: r => r.series.equal("gmach"),
440
+ label: r => r.label, id: true,
441
+ runs: r => r.jobs.map((_$, j) => Plan.run({
442
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
443
+ })),
444
+ }),
445
+ ]),
446
+ ], ArrayType(Plan.Types.Series(OpsRow)));
447
+ // Every preset is DATA — a whole axis value in a typed struct, so
448
+ // switching presets swaps window, resolution, format and now
449
+ // through ONE expression-fed prop.
450
+ const presets = $.const([
451
+ { key: "ops", axis: Plan.axis({
452
+ window: { min: week(27n), max: week(39n) },
453
+ resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
454
+ }) },
455
+ { key: "roadmap", axis: Plan.axis({
456
+ window: { min: week(1n), max: week(53n) },
457
+ resolution: "month", format: "MMM", now: week(31n),
458
+ }) },
459
+ { key: "sprint", axis: Plan.axis({
460
+ window: { min: week(30n), max: week(32n) },
461
+ resolution: "day", format: "ddd DD", now: week(31n),
462
+ }) },
463
+ ], ArrayType(StructType({ key: StringType, axis: Plan.Types.Axis })));
464
+ const presetKeys = $.const(["ops", "roadmap", "sprint"], ArrayType(StringType));
465
+ const densities = $.const([
466
+ variant("condensed", null), variant("compact", null), variant("comfortable", null),
467
+ ], ArrayType(Style.Types.Density));
468
+ const gutters = $.const(["168px", "200px", "240px"], ArrayType(StringType));
469
+ const presetBind = $.let(State.bind([StringType], "plan_variants_preset", "ops"));
470
+ const densityBind = $.let(State.bind([StringType], "plan_variants_density", "compact"));
471
+ const gutterBind = $.let(State.bind([StringType], "plan_variants_gutter", "168px"));
472
+ const lastEventBind = $.let(State.bind([StringType], "plan_variants_last_event", ""));
473
+ const pKey = $.let(presetBind.read());
474
+ const dKey = $.let(densityBind.read());
475
+ const gKey = $.let(gutterBind.read());
476
+ const lastEvent = $.let(lastEventBind.read());
477
+ const onPreset = $.const(East.function([StringType], NullType, ($, next) => { $(presetBind.write(next)); }));
478
+ const onDensity = $.const(East.function([StringType], NullType, ($, next) => { $(densityBind.write(next)); }));
479
+ 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}`));
483
+ }));
484
+ const onRunClick = $.const(East.function([Plan.Types.RunClickEvent], NullType, ($, ev) => {
485
+ $(lastEventBind.write(East.str `onRunClick · ${ev.row} / ${ev.run}`));
486
+ }));
487
+ const onGroupToggle = $.const(East.function([Plan.Types.GroupToggleEvent], NullType, ($, ev) => {
488
+ const state = $.let(ev.expanded.ifElse(() => "expanded", () => "collapsed"), StringType);
489
+ $(lastEventBind.write(East.str `onGroupToggle · ${ev.row} → ${state}`));
490
+ }));
491
+ const onGrainChange = $.const(East.function([Plan.Types.Grain], NullType, ($, grain) => {
492
+ $(lastEventBind.write(East.str `onGrainChange · ${grain.getTag()}`));
493
+ }));
494
+ const sel = $.let(presets.filter((_$, o) => o.key.equal(pKey)).get(0n, _$ => presets.get(0n)));
495
+ const densitySel = $.let(densities.filter((_$, v) => v.getTag().equal(dKey)).get(0n));
496
+ return (_jsx(Configurator, { controls: [
497
+ Configurator.Control("Preset", pKey, _jsx(Select, { value: pKey, onChange: onPreset, size: "sm", items: presetKeys.map((_$, s) => Select.Item(s, s)) })),
498
+ 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
+ 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: {
501
+ label: "Events · Reactive",
502
+ body: (_jsx(Badge, { colorPalette: "brand", variant: "outline", children: East.equal(lastEvent.length(), 0n).ifElse(_$ => "Interact with the canvas", _$ => lastEvent) })),
503
+ }, spec: [
504
+ Configurator.Spec("Resolution", sel.axis.unwrap("time").resolution.getTag()),
505
+ Configurator.Spec("Rows", East.print(ops.size())),
506
+ ] }));
507
+ } }))),
508
+ inputs: [],
509
+ });
510
+ // ============================================================================
511
+ // Per-kind examples — one canvas per row kind; the config sweep is DATA
512
+ // ============================================================================
513
+ 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",
516
+ fn: East.function([], UIComponentType, ($) => {
517
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
518
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
519
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
520
+ return w1.addWeeks(n.subtract(1n));
521
+ }));
522
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
523
+ // The RAW job record; `series` picks the series, everything else —
524
+ // including the expand declaration — is per-row data.
525
+ const JobRow = StructType({
526
+ key: StringType, phase: StringType, batch: OptionType(StringType),
527
+ start: DateTimeType, end: DateTimeType,
528
+ tonnes: OptionType(FloatType), state: EventStateType,
529
+ });
530
+ const MachineRow = StructType({
531
+ series: StringType, program: StringType,
532
+ sub: OptionType(StringType), value: OptionType(StringType),
533
+ expand: OptionType(Plan.Types.Expand),
534
+ jobs: ArrayType(JobRow),
535
+ decisions: ArrayType(Plan.Types.DecisionMark),
536
+ ports: ArrayType(Plan.Types.Port),
537
+ });
538
+ const machines = $.const(new Map([
539
+ // Proposal flavours: forecast ghost · proposed cut · declined.
540
+ ["L1-M07", { series: "flavours", program: "", sub: none, value: some("80 t"), expand: none,
541
+ jobs: [
542
+ { key: "run", phase: "RUN", batch: some("B-197"), start: week(27n), end: week(30n), tonnes: some(64.0), state: variant("in-progress", null) },
543
+ { key: "gho", phase: "FORECAST", batch: none, start: week(30n), end: week(32n), tonnes: none, state: variant("estimated", null) },
544
+ { key: "rem", phase: "CUT", batch: none, start: week(33n), end: week(35n), tonnes: none, state: variant("proposed", variant("removed", null)) },
545
+ { key: "rej", phase: "DECLINED", batch: none, start: week(36n), end: week(38n), tonnes: none, state: variant("rejected", null) },
546
+ ], decisions: [], ports: [] }],
547
+ // Tonnage + an applied decision + a port on a stacked two-line
548
+ // gutter; the EXPAND DECLARATION is row data (R2) — the render is
549
+ // the root's expandRender resolver.
550
+ ["L1-M09", { series: "detail", program: "", sub: some("cap 120 t"), value: none,
551
+ expand: some({ height: some("152px"), axis: variant("dim", null) }),
552
+ jobs: [
553
+ { key: "a", phase: "RUN", batch: some("B-208"), start: week(27n), end: week(31n), tonnes: some(112.0), state: variant("actual", null) },
554
+ { 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
+ ],
556
+ 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: [] }],
567
+ // Linked despatch whose run starts BEYOND the window — in links
568
+ // focus its landing renders as the edge fade.
569
+ ["dsp", { series: "despatch", program: "", sub: none, value: none, expand: none,
570
+ 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: [] }],
578
+ ]), DictType(StringType, MachineRow));
579
+ // Raw jobs → runs, once — every span series shares the mapping.
580
+ const jobRuns = $.const(East.function([ArrayType(JobRow)], ArrayType(Plan.Types.Run), (_$, jobs) => jobs.map(($, j) => {
581
+ const noQuantity = $.const(none, OptionType(StringType));
582
+ const quantity = $.let(j.tonnes.match({
583
+ some: (_$, t) => East.value(some(East.str `${East.Float.printFixed(t, 0n)} t`), OptionType(StringType)),
584
+ none: (_$) => noQuantity,
585
+ }), OptionType(StringType));
586
+ const label = $.let(j.batch.match({
587
+ some: (_$, b) => East.str `${j.phase} · ${b}`,
588
+ none: (_$) => j.phase,
589
+ }), StringType);
590
+ const run = $.let({
591
+ key: j.key, start: Plan.at.time(j.start), end: Plan.at.time(j.end), label,
592
+ quantity, qty: j.tonnes, state: j.state,
593
+ status: none, moved: none, icon: none,
594
+ }, Plan.Types.Run);
595
+ return run;
596
+ })));
597
+ const series = $.const([
598
+ Plan.series.span(MachineRow, {
599
+ key: "flavours", title: "Flavours",
600
+ match: r => r.series.equal("flavours"),
601
+ label: (_r, k) => k, id: true,
602
+ value: r => r.value,
603
+ runs: r => jobRuns(r.jobs),
604
+ }),
605
+ Plan.series.span(MachineRow, {
606
+ key: "detail", title: "Detail",
607
+ match: r => r.series.equal("detail"),
608
+ label: (_r, k) => k, id: true, stacked: true,
609
+ sub: r => r.sub, expand: r => r.expand,
610
+ runs: r => jobRuns(r.jobs), decisions: r => r.decisions, ports: r => r.ports,
611
+ }),
612
+ Plan.series.span(MachineRow, {
613
+ key: "rollup", title: "Rollup",
614
+ match: r => r.series.equal("rollup"),
615
+ label: (_r, k) => k, id: true,
616
+ runs: r => jobRuns(r.jobs),
617
+ groupBy: [r => r.program], rollup: "union", unit: "t",
618
+ }),
619
+ Plan.series.span(MachineRow, {
620
+ key: "despatch", title: "Despatch",
621
+ match: r => r.series.equal("despatch"),
622
+ label: (_r, k) => k, id: true,
623
+ runs: r => jobRuns(r.jobs),
624
+ }),
625
+ Plan.series.group(MachineRow, { key: "line2", label: "Line 2", meta: "1 rs" }, [
626
+ Plan.series.span(MachineRow, {
627
+ key: "line2", title: "Line2",
628
+ match: r => r.series.equal("line2"),
629
+ label: (_r, k) => k, id: true,
630
+ runs: r => jobRuns(r.jobs),
631
+ groupBy: [r => r.program], rollup: "byStatus", unit: "t",
632
+ }),
633
+ ]),
634
+ ], ArrayType(Plan.Types.Series(MachineRow)));
635
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
636
+ // The R2 developer render — the ROOT's resolver, called with the
637
+ // focused row's ref; ONE function serves every declaring row.
638
+ 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 }))));
640
+ // The generalized popover resolver — decision diamonds ride the mark
641
+ // arm of the element ref.
642
+ const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
643
+ const noBody = $.const(none, OptionType(UIComponentType));
644
+ return ref.match({
645
+ mark: (_$, ev) => ev.mark.equal("d1").ifElse(() => some(_jsx(Text, { children: "Approved by run 411." })), () => noBody),
646
+ }, _$ => noBody);
647
+ }));
648
+ return (_jsx(Plan, { expandRender: expandRender, popover: popover,
649
+ // A denser gutter (value + carets) — widen it (the shared
650
+ // CSS-px height/width vocabulary).
651
+ style: { gutterWidth: "200px" }, axis: axis,
652
+ // The link graph (R1) — hover a linked row for the ⌁ control.
653
+ // The edges deliberately cover the routing permutations:
654
+ // forward, a same-row seam feed, loopbacks, a rising loop,
655
+ // and an off-window landing.
656
+ 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" }),
663
+ ], data: machines, series: series }));
664
+ }),
665
+ inputs: [],
666
+ });
667
+ 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"],
669
+ description: "Bucket rows over one dock source — tiles derived in the accessor, and stored tile records with lanes, tones, colours and markers",
670
+ fn: East.function([], UIComponentType, ($) => {
671
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
672
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
673
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
674
+ return w1.addWeeks(n.subtract(1n));
675
+ }));
676
+ const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
677
+ const DockRow = StructType({
678
+ series: StringType, label: StringType,
679
+ sub: OptionType(StringType), value: OptionType(StringType),
680
+ lanes: ArrayType(Plan.Types.Lane),
681
+ allocations: ArrayType(AllocRow),
682
+ tiles: ArrayType(Plan.Types.BucketEvent),
683
+ markers: ArrayType(Plan.Types.CellMarker),
684
+ });
685
+ // Raw weekly allocations; every third one is a proposal.
686
+ const inbound = $.let(East.Array.generate(6n, AllocRow, ($, i) => {
687
+ const confirmed = $.const(variant("confirmed", null), EventStateType);
688
+ const recommended = $.const(variant("proposed", variant("recommended", null)), EventStateType);
689
+ return {
690
+ key: East.str `a${East.print(i)}`,
691
+ at: week(i.add(27n)),
692
+ state: i.remainder(3n).equal(2n).ifElse(() => recommended, () => confirmed),
693
+ };
694
+ }));
695
+ const docks = $.const(new Map([
696
+ ["dock2", { series: "inbound", label: "Dock 2", sub: none, value: some("load/wk"),
697
+ lanes: [], allocations: inbound, tiles: [],
698
+ markers: [{ at: Plan.at.time(week(29n)), lane: none, status: variant("warning", null), message: "capacity 90%" }] }],
699
+ // The grammar showcase — tiles stored IN the element vocabulary
700
+ // (plain `PlanBucketEventType` records; no builders in data).
701
+ ["dock5", { series: "outbound", label: "Dock 5", sub: some("day · am/pm"), value: none,
702
+ lanes: [{ key: "am", label: some("AM") }, { key: "pm", label: some("PM") }],
703
+ allocations: [],
704
+ tiles: [
705
+ { key: "m1", at: Plan.at.time(week(27n)), lane: some("am"), label: none, icon: none, state: variant("confirmed", null),
706
+ tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: none },
707
+ { key: "m2", at: Plan.at.time(week(27n)), lane: some("pm"), label: none, icon: none, state: variant("confirmed", null),
708
+ tone: some(variant("warning", null)), color: none, colorPalette: none, stretch: none, content: none, animation: none },
709
+ { key: "m3", at: Plan.at.time(week(28n)), lane: some("am"), label: none, icon: none, state: variant("proposed", variant("recommended", null)),
710
+ tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: some(variant("pulse", null)) },
711
+ { key: "m4", at: Plan.at.time(week(29n)), lane: none, label: some("MIXED"), icon: none, state: variant("confirmed", null),
712
+ tone: none, color: none, colorPalette: none, stretch: some(variant("horizontal", null)),
713
+ content: some({ horizontal: some(variant("center", null)), vertical: none }), animation: none },
714
+ { key: "m5", at: Plan.at.time(week(30n)), lane: some("pm"), label: none,
715
+ icon: some({ prefix: "fas", name: "truck", label: none, style: none }),
716
+ state: variant("proposed", variant("recommended", null)),
717
+ tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: none },
718
+ { key: "m6", at: Plan.at.time(week(31n)), lane: some("am"), label: some("QC"), icon: none, state: variant("estimated", null),
719
+ tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: none },
720
+ // The colour channels (#571, from the Planner's colors
721
+ // preset): `color` is a raw token override, `colorPalette`
722
+ // recolours the whole lifecycle treatment.
723
+ { key: "m7", at: Plan.at.time(week(32n)), lane: some("am"), label: some("S-A"), icon: none, state: variant("confirmed", null),
724
+ tone: none, color: some("teal.solid"), colorPalette: none, stretch: none, content: none, animation: none },
725
+ { key: "m8", at: Plan.at.time(week(32n)), lane: some("pm"), label: some("S-B"), icon: none, state: variant("confirmed", null),
726
+ tone: none, color: none, colorPalette: some(variant("brand", null)), stretch: none, content: none, animation: none },
727
+ ],
728
+ markers: [{ at: Plan.at.time(week(29n)), lane: none, status: variant("danger", null), message: "capacity breach" }] }],
729
+ ]), DictType(StringType, DockRow));
730
+ const series = $.const([
731
+ // Raw allocations → resting tiles, in the accessor.
732
+ Plan.series.buckets(DockRow, {
733
+ key: "inbound", title: "Inbound",
734
+ match: r => r.series.equal("inbound"),
735
+ label: r => r.label,
736
+ value: r => r.value,
737
+ events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
738
+ markers: r => r.markers,
739
+ }),
740
+ Plan.series.group(DockRow, { key: "outbound", label: "Docks · Out", meta: "1 rs" }, [
741
+ // Stored vocabulary records pass straight through.
742
+ Plan.series.buckets(DockRow, {
743
+ key: "outbound", title: "Outbound",
744
+ match: r => r.series.equal("outbound"),
745
+ label: r => r.label,
746
+ sub: r => r.sub,
747
+ lanes: r => r.lanes, events: r => r.tiles, markers: r => r.markers,
748
+ }),
749
+ ]),
750
+ ], ArrayType(Plan.Types.Series(DockRow)));
751
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
752
+ // The generalized resolvers — tiles ride the event arm of the ref.
753
+ const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
754
+ const noBody = $.const(none, OptionType(UIComponentType));
755
+ return ref.match({
756
+ event: (_$, ev) => ev.event.equal("m5").ifElse(() => some(_jsx(Text, { children: "Load 41 \u00B7 8 pallets" })), () => noBody),
757
+ }, _$ => noBody);
758
+ }));
759
+ const hover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
760
+ const noBody = $.const(none, OptionType(UIComponentType));
761
+ return ref.match({
762
+ event: (_$, ev) => ev.event.equal("m3").ifElse(() => some(_jsx(Text, { children: "Urgent \u2014 overtime window" })), () => noBody),
763
+ }, _$ => noBody);
764
+ }));
765
+ return (_jsx(Plan, { popover: popover, hover: hover, axis: axis, data: docks, series: series }));
766
+ }),
767
+ inputs: [],
768
+ });
769
+ 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"],
771
+ description: "Chart rows over one measure source — one series per mark kind, plus annotations and a fixed dual-axis composition",
772
+ fn: East.function([], UIComponentType, ($) => {
773
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
774
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
775
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
776
+ return w1.addWeeks(n.subtract(1n));
777
+ }));
778
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
779
+ const BandRow = StructType({ week: DateTimeType, lo: FloatType, hi: FloatType });
780
+ // ONE raw source — each series's points ride the row (`extra` carries
781
+ // the second set for stacked / dual compositions).
782
+ const ChartRow = StructType({
783
+ series: StringType, label: StringType,
784
+ sub: OptionType(StringType), value: OptionType(StringType),
785
+ points: ArrayType(MeasureRow),
786
+ extra: ArrayType(MeasureRow),
787
+ band: ArrayType(BandRow),
788
+ });
789
+ const pcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
790
+ const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: pcts.get(i) })));
791
+ const cum = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.toFloat().multiply(14.0).add(40.0) })));
792
+ const out1 = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(23n).remainder(17n).toFloat().add(28.0) })));
793
+ const out2 = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(31n).remainder(13n).toFloat().add(14.0) })));
794
+ 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
+ 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
+ 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 }],
803
+ ]), DictType(StringType, ChartRow));
804
+ const series = $.const([
805
+ // Line — the KPI spark with a breach threshold; the caret opens
806
+ // it to a custom 120px (expandedHeight, default 88).
807
+ Plan.series.chart(ChartRow, {
808
+ key: "spark", title: "Spark",
809
+ match: r => r.series.equal("spark"),
810
+ label: r => r.label, id: true,
811
+ value: r => r.value, status: _r => some(variant("warning", null)),
812
+ height: "spark", expandable: true, expandedHeight: "120px",
813
+ layers: r => [Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } })],
814
+ }),
815
+ // Area — the cumulative fill.
816
+ Plan.series.chart(ChartRow, {
817
+ key: "cum", title: "Cumulative",
818
+ match: r => r.series.equal("cum"),
819
+ label: r => r.label, id: true, value: r => r.value,
820
+ layers: r => [Chart.Area(r.points, { x: p => p.week, y: p => p.pct })],
821
+ }),
822
+ // Columns — the row's two point sets stacked by one series id,
823
+ // on a two-line gutter (label over sub).
824
+ Plan.series.chart(ChartRow, {
825
+ key: "stacked", title: "Stacked",
826
+ match: r => r.series.equal("stacked"),
827
+ label: r => r.label, id: true, stacked: true, sub: r => r.sub,
828
+ layers: r => [
829
+ Plan.layer(Chart.Column(r.points, { x: p => p.week, y: p => p.pct }), { series: "L1" }),
830
+ Plan.layer(Chart.Column(r.extra, { x: p => p.week, y: p => p.pct }), { series: "L2" }),
831
+ ],
832
+ }),
833
+ // Scatter — the defect cloud.
834
+ Plan.series.chart(ChartRow, {
835
+ key: "ppm", title: "Ppm",
836
+ match: r => r.series.equal("ppm"),
837
+ label: r => r.label, id: true, value: r => r.value,
838
+ layers: r => [Chart.Scatter(r.points, { x: p => p.week, y: p => p.pct })],
839
+ }),
840
+ // Line + every annotation kind, at expanded density.
841
+ Plan.series.chart(ChartRow, {
842
+ key: "refs", title: "Refs",
843
+ match: r => r.series.equal("refs"),
844
+ label: r => r.label, id: true,
845
+ height: "expanded",
846
+ layers: r => [
847
+ Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
848
+ Chart.refLine({ y: 100, label: "TARGET 100" }),
849
+ Chart.refBand({ x: [week(34n), week(36n)], label: "CRUNCH" }),
850
+ Chart.refDot({ x: week(36n), y: 91.4, label: "LOW" }),
851
+ ],
852
+ }),
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" }, [
857
+ Plan.series.chart(ChartRow, {
858
+ key: "dual", title: "Dual",
859
+ match: r => r.series.equal("dual"),
860
+ label: r => r.label, id: true,
861
+ height: Plan.fixed("120px"),
862
+ left: { domain: [0, 60], tickValues: [0, 25, 50] },
863
+ right: { domain: [80, 105], tickValues: [85, 95, 105] },
864
+ swatches: [{ color: "ink.3", label: "out" }, { color: "brand.d", label: "cov · rh" }],
865
+ layers: r => [
866
+ Chart.Column(r.points, { x: p => p.week, y: p => p.pct }),
867
+ Plan.layer(Chart.Line(r.extra, { x: p => p.week, y: p => p.pct }), { axis: "right" }),
868
+ Plan.layer(Chart.Band(r.band, { x: p => p.week, low: p => p.lo, high: p => p.hi }), { axis: "right" }),
869
+ ],
870
+ }),
871
+ ]),
872
+ ], ArrayType(Plan.Types.Series(ChartRow)));
873
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
874
+ return (_jsx(Plan, { axis: axis, data: measures, series: series }));
875
+ }),
876
+ inputs: [],
877
+ });
878
+ 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",
881
+ fn: East.function([], UIComponentType, ($) => {
882
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
883
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
884
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
885
+ return w1.addWeeks(n.subtract(1n));
886
+ }));
887
+ const HeatRow = StructType({
888
+ series: StringType, line: StringType, label: StringType,
889
+ sub: OptionType(StringType),
890
+ cells: ArrayType(Plan.Types.HeatCell),
891
+ weights: ArrayType(Plan.Types.WeightCell),
892
+ segs: ArrayType(Plan.Types.SegmentCell),
893
+ });
894
+ 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
+ const cells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
896
+ at: Plan.at.time(week(i.add(27n))),
897
+ value: i.equal(4n).ifElse(() => none, () => some(pcts.get(i))), // W31 = no data
898
+ label: i.equal(4n).ifElse(() => none, () => some(East.Float.printFixed(pcts.get(i), 0n))),
899
+ })));
900
+ // Booked-vs-free fractions; the back half is the planned pale tail.
901
+ const weights = $.let(East.Array.generate(6n, Plan.Types.WeightCell, (_$, i) => ({
902
+ at: Plan.at.time(week(i.multiply(2n).add(27n))),
903
+ fraction: i.toFloat().multiply(-0.11).add(0.9),
904
+ planned: i.greaterEqual(3n),
905
+ })));
906
+ 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: [] }],
910
+ // Segment compositions — plain `{ fill, weight, label }` records.
911
+ ["pack", { series: "segments", line: "", label: "Pack line", sub: some("capacity"), cells: [], weights: [],
912
+ segs: [
913
+ { at: Plan.at.time(week(27n)), segments: [
914
+ { fill: variant("success", null), weight: 60.0, label: some("60%") },
915
+ { fill: variant("warning", null), weight: 25.0, label: some("25%") },
916
+ { fill: variant("slack", null), weight: 15.0, label: none },
917
+ ] },
918
+ { at: Plan.at.time(week(28n)), segments: [
919
+ { fill: variant("success", null), weight: 70.0, label: some("70%") },
920
+ { fill: variant("slack", null), weight: 30.0, label: none },
921
+ ] },
922
+ { at: Plan.at.time(week(29n)), segments: [
923
+ { fill: variant("danger", null), weight: 40.0, label: some("40%") },
924
+ { fill: variant("free", null), weight: 60.0, label: none },
925
+ ] },
926
+ ] }],
927
+ ]), DictType(StringType, HeatRow));
928
+ const series = $.const([
929
+ // The aggregate-mean parent derives per discovered line value.
930
+ Plan.series.heat(HeatRow, {
931
+ key: "depth", title: "Depth",
932
+ match: r => r.series.equal("depth"),
933
+ label: r => r.label, id: true,
934
+ 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 },
936
+ }),
937
+ Plan.series.heat(HeatRow, {
938
+ key: "booked", title: "Booked",
939
+ match: r => r.series.equal("booked"),
940
+ label: r => r.label,
941
+ sub: r => r.sub,
942
+ cells: r => Plan.weightCells(r.weights),
943
+ }),
944
+ Plan.series.group(HeatRow, { key: "packing", label: "Packing", meta: "1 rs" }, [
945
+ Plan.series.heat(HeatRow, {
946
+ key: "segments", title: "Segments",
947
+ match: r => r.series.equal("segments"),
948
+ label: r => r.label,
949
+ sub: r => r.sub,
950
+ cells: r => Plan.segmentCells(r.segs),
951
+ }),
952
+ ]),
953
+ ], ArrayType(Plan.Types.Series(HeatRow)));
954
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
955
+ return (_jsx(Plan, { axis: axis, data: lines, series: series }));
956
+ }),
957
+ inputs: [],
958
+ });
959
+ 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",
962
+ fn: East.function([], UIComponentType, ($) => {
963
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
964
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
965
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
966
+ return w1.addWeeks(n.subtract(1n));
967
+ }));
968
+ const RawCell = StructType({ at: DateTimeType, value: OptionType(FloatType) });
969
+ // 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,
973
+ sub: OptionType(StringType),
974
+ act: ArrayType(RawCell),
975
+ plan: ArrayType(RawCell),
976
+ });
977
+ const act = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
978
+ at: week(i.add(27n)),
979
+ value: i.equal(9n).ifElse(() => none, () => some(i.toFloat().multiply(7.0).add(40.0))),
980
+ })));
981
+ const deltas = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
982
+ at: week(i.add(27n)),
983
+ value: i.remainder(4n).equal(3n).ifElse(() => none, () => some(i.toFloat().multiply(1.5).subtract(8.0))),
984
+ })));
985
+ const outflow = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
986
+ at: week(i.add(27n)),
987
+ value: some(i.toFloat().multiply(-3.0).subtract(12.0)),
988
+ })));
989
+ 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: [] }],
995
+ // Footer emphasis + negative tone + the muted em-dash.
996
+ ["net", { series: "net", name: "Net flow", top: "", program: "", sub: none, plan: [],
997
+ act: [
998
+ { at: week(27n), value: some(22.0) }, { at: week(28n), value: some(-26.0) },
999
+ { at: week(29n), value: none },
1000
+ ] }],
1001
+ // Multi-value series — raw act + plan arrays per row. The SPLIT
1002
+ // (how the positions sit against each other) and the GUTTER (one
1003
+ // line or two) are independent choices, so all four combinations
1004
+ // are here: the pair that reads well depends on the numbers, not
1005
+ // 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 }],
1008
+ // Horizontal, on a ONE-line gutter — the pair reads as a single
1009
+ // 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 }],
1011
+ // Vertical, on a TWO-line gutter — the stack needs the unit spelled
1012
+ // 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,
1015
+ // 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 }],
1018
+ // The same, STACKED: members and their subtotal both put the two
1019
+ // 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 }],
1022
+ ]), DictType(StringType, OrderRow));
1023
+ const series = $.const([
1024
+ Plan.series.table(OrderRow, {
1025
+ key: "orders", title: "Orders",
1026
+ match: r => r.series.equal("orders"),
1027
+ label: r => r.name,
1028
+ cells: r => Plan.tableCells(r.act),
1029
+ groupBy: [r => r.top, r => r.program], aggregate: "sum",
1030
+ format: Format.Number({ maximumFractionDigits: 0n }),
1031
+ }),
1032
+ Plan.series.table(OrderRow, {
1033
+ key: "net", title: "Net",
1034
+ match: r => r.series.equal("net"),
1035
+ label: r => r.name, emphasis: "footer",
1036
+ cells: r => Plan.tableCells(r.act),
1037
+ format: Format.Number({ maximumFractionDigits: 0n }),
1038
+ }),
1039
+ // Per-POSITION style declared ONCE, in the CONFIG — a strong
1040
+ // rolled-up actual beside its muted, always-signed plan Δ.
1041
+ Plan.series.table(OrderRow, {
1042
+ key: "actplan", title: "Actual vs plan",
1043
+ match: r => r.series.equal("actplan"),
1044
+ label: r => r.name, stacked: true, sub: r => r.sub,
1045
+ series: r => [
1046
+ Plan.tableSeries({ strong: true, rollup: true, cells: Plan.tableCells(r.act) }),
1047
+ Plan.tableSeries({
1048
+ tone: "muted",
1049
+ format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
1050
+ cells: Plan.tableCells(r.plan),
1051
+ }),
1052
+ ],
1053
+ format: Format.Number({ maximumFractionDigits: 0n }),
1054
+ }),
1055
+ // The VERTICAL split stacks the positions; the row grows.
1056
+ Plan.series.table(OrderRow, {
1057
+ key: "inout", title: "Inout",
1058
+ match: r => r.series.equal("inout"),
1059
+ label: r => r.name, split: "vertical",
1060
+ series: r => [
1061
+ Plan.tableSeries({ cells: Plan.tableCells(r.act) }),
1062
+ Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
1063
+ ],
1064
+ format: Format.Number({ maximumFractionDigits: 0n }),
1065
+ }),
1066
+ // HORIZONTAL on a ONE-line gutter — the other half of the pair
1067
+ // above: the split is a cell-layout choice and the gutter a label
1068
+ // choice, so neither implies the other.
1069
+ Plan.series.table(OrderRow, {
1070
+ key: "sidebyside", title: "Side by side",
1071
+ match: r => r.series.equal("sidebyside"),
1072
+ label: r => r.name, split: "horizontal",
1073
+ series: r => [
1074
+ Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
1075
+ Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
1076
+ ],
1077
+ format: Format.Number({ maximumFractionDigits: 0n }),
1078
+ }),
1079
+ // A MULTI-VALUE series under a subtotal parent — every position
1080
+ // rolls up, so the parent shows an act subtotal beside a Δ subtotal
1081
+ // instead of collapsing to one number and looking complete. Flag a
1082
+ // position `rollup: true` to narrow it back to that one.
1083
+ Plan.series.table(OrderRow, {
1084
+ key: "flow", title: "Flow",
1085
+ match: r => r.series.equal("flow"),
1086
+ label: r => r.name,
1087
+ series: r => [
1088
+ Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
1089
+ Plan.tableSeries({
1090
+ tone: "muted",
1091
+ format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
1092
+ cells: Plan.tableCells(r.plan),
1093
+ }),
1094
+ ],
1095
+ groupBy: [r => r.top], aggregate: "sum",
1096
+ format: Format.Number({ maximumFractionDigits: 0n }),
1097
+ }),
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.
1103
+ Plan.series.table(OrderRow, {
1104
+ key: "stack", title: "Stack",
1105
+ match: r => r.series.equal("stack"),
1106
+ label: r => r.name, split: "vertical",
1107
+ series: r => [
1108
+ Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
1109
+ Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
1110
+ ],
1111
+ groupBy: [r => r.top], aggregate: "sum",
1112
+ format: Format.Number({ maximumFractionDigits: 0n }),
1113
+ }),
1114
+ // VERTICAL on a TWO-line gutter — the remaining combination, and
1115
+ // the one that grows the row in BOTH directions at once.
1116
+ Plan.series.table(OrderRow, {
1117
+ key: "overunder", title: "Over / under",
1118
+ match: r => r.series.equal("overunder"),
1119
+ label: r => r.name, split: "vertical", stacked: true, sub: r => r.sub,
1120
+ series: r => [
1121
+ Plan.tableSeries({ strong: true, rollup: true, cells: Plan.tableCells(r.act) }),
1122
+ Plan.tableSeries({
1123
+ tone: "muted",
1124
+ format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
1125
+ cells: Plan.tableCells(r.plan),
1126
+ }),
1127
+ ],
1128
+ format: Format.Number({ maximumFractionDigits: 0n }),
1129
+ }),
1130
+ ], ArrayType(Plan.Types.Series(OrderRow)));
1131
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1132
+ return (_jsx(Plan, { axis: axis, data: orders, series: series }));
1133
+ }),
1134
+ inputs: [],
1135
+ });
1136
+ 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"],
1138
+ description: "Cards rows over one crew source — chip labels derived from hours × lifecycle, plus stored chip records",
1139
+ fn: East.function([], UIComponentType, ($) => {
1140
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1141
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1142
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1143
+ return w1.addWeeks(n.subtract(1n));
1144
+ }));
1145
+ const ShiftRow = StructType({
1146
+ key: StringType, from: DateTimeType, to: DateTimeType,
1147
+ hours: FloatType, state: EventStateType,
1148
+ });
1149
+ const CrewRow = StructType({
1150
+ series: StringType, name: StringType,
1151
+ sub: OptionType(StringType), value: OptionType(StringType),
1152
+ shifts: ArrayType(ShiftRow),
1153
+ chips: ArrayType(Plan.Types.Chip),
1154
+ });
1155
+ const crews = $.const(new Map([
1156
+ // RAW shifts — hours + lifecycle; chip labels derive client-side.
1157
+ ["crewA", { series: "main", name: "Crew A", sub: some("152h → 168h"), value: none, chips: [], shifts: [
1158
+ { key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
1159
+ { key: "s2", from: week(29n), to: week(31n), hours: 56.0, state: variant("proposed", variant("removed", null)) },
1160
+ { key: "s3", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
1161
+ { key: "s4", from: week(34n), to: week(35n), hours: 48.0, state: variant("estimated", null) },
1162
+ ] }],
1163
+ // STORED vocabulary — plain chip records (the §3.2 element
1164
+ // shapes), here carrying the shift-type icon.
1165
+ ["crewB", { series: "pool", name: "Crew B", sub: none, value: some("128h"), shifts: [], chips: [
1166
+ { key: "b1", from: Plan.at.time(week(28n)), to: Plan.at.time(week(31n)), label: "96h", state: variant("confirmed", null),
1167
+ icon: some({ prefix: "fas", name: "user-group", label: none, style: none }) },
1168
+ { key: "b2", from: Plan.at.time(week(33n)), to: Plan.at.time(week(36n)), label: "+32h", state: variant("proposed", variant("recommended", null)), icon: none },
1169
+ ] }],
1170
+ ]), DictType(StringType, CrewRow));
1171
+ const series = $.const([
1172
+ Plan.series.cards(CrewRow, {
1173
+ key: "main", title: "Main",
1174
+ match: r => r.series.equal("main"),
1175
+ label: r => r.name, stacked: true,
1176
+ sub: r => r.sub,
1177
+ chips: r => r.shifts.map(($, s) => {
1178
+ const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
1179
+ // The `+` means ADDED hours, so it rides the proposal's
1180
+ // flavour, not the mere fact of being a proposal — a
1181
+ // `removed` shift is a proposal too, and prefixing it `+`
1182
+ // would read as the opposite of what it does.
1183
+ const label = $.let(s.state.match({
1184
+ proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
1185
+ }, _$ => East.str `${hrs}h`), StringType);
1186
+ return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
1187
+ }),
1188
+ }),
1189
+ Plan.series.group(CrewRow, { key: "pool", label: "Relief pool", meta: "1 rs" }, [
1190
+ Plan.series.cards(CrewRow, {
1191
+ key: "pool", title: "Pool",
1192
+ match: r => r.series.equal("pool"),
1193
+ label: r => r.name,
1194
+ value: r => r.value,
1195
+ chips: r => r.chips,
1196
+ }),
1197
+ ]),
1198
+ ], ArrayType(Plan.Types.Series(CrewRow)));
1199
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n), format: "D MMM" }));
1200
+ // The generalized popover resolver — chips ride the chip arm.
1201
+ const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
1202
+ const noBody = $.const(none, OptionType(UIComponentType));
1203
+ return ref.match({
1204
+ chip: (_$, ev) => ev.chip.equal("s3").ifElse(() => some(_jsx(Text, { children: "Overtime proposal." })), () => noBody),
1205
+ }, _$ => noBody);
1206
+ }));
1207
+ return (_jsx(Plan, { popover: popover, axis: axis, data: crews, series: series }));
1208
+ }),
1209
+ inputs: [],
1210
+ });
1211
+ export const planEventRows = example({
1212
+ keywords: ["Plan", "data", "series", "events", "mark", "milestone", "decision", "exception", "markKind", "applied", "icon", "label", "popover", "group", "match", "stacked", "gutter", "raw"],
1213
+ description: "Event rows over one stream source — milestone dots, decision diamonds, an exception, and a custom glyph",
1214
+ fn: East.function([], UIComponentType, ($) => {
1215
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1216
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1217
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1218
+ return w1.addWeeks(n.subtract(1n));
1219
+ }));
1220
+ const StreamRow = StructType({
1221
+ series: StringType, name: StringType,
1222
+ sub: OptionType(StringType), value: OptionType(StringType),
1223
+ marks: ArrayType(Plan.Types.EventMark),
1224
+ });
1225
+ const streams = $.const(new Map([
1226
+ ["ms", { series: "main", name: "MILESTONES", sub: none, value: some("5"), marks: [
1227
+ { key: "kick", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
1228
+ { key: "d1", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
1229
+ { key: "rel", at: Plan.at.time(week(33n)), kind: variant("milestone", null),
1230
+ icon: some({ prefix: "fas", name: "rocket", label: none, style: none }), label: some("REL 2.4") },
1231
+ { key: "audit", at: Plan.at.time(week(35n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
1232
+ { key: "d2", at: Plan.at.time(week(37n)), kind: variant("decision", { applied: false }), icon: none, label: some("×3") },
1233
+ ] }],
1234
+ ["release", { series: "programs", name: "RELEASES", sub: some("6-wk cadence"), value: none, marks: [
1235
+ { key: "r1", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("2.3") },
1236
+ { key: "r2", at: Plan.at.time(week(36n)), kind: variant("milestone", null), icon: none, label: some("2.4") },
1237
+ ] }],
1238
+ ]), DictType(StringType, StreamRow));
1239
+ const series = $.const([
1240
+ Plan.series.events(StreamRow, {
1241
+ key: "main-2", title: "Main",
1242
+ match: r => r.series.equal("main"),
1243
+ label: r => r.name, id: true,
1244
+ value: r => r.value,
1245
+ marks: r => r.marks,
1246
+ }),
1247
+ Plan.series.group(StreamRow, { key: "programs", label: "Programs", meta: "1 rs" }, [
1248
+ Plan.series.events(StreamRow, {
1249
+ key: "programs", title: "Programs",
1250
+ match: r => r.series.equal("programs"),
1251
+ label: r => r.name, id: true, stacked: true,
1252
+ sub: r => r.sub,
1253
+ marks: r => r.marks,
1254
+ }),
1255
+ ]),
1256
+ ], ArrayType(Plan.Types.Series(StreamRow)));
1257
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1258
+ // The generalized popover resolver — event marks ride the mark arm.
1259
+ const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
1260
+ const noBody = $.const(none, OptionType(UIComponentType));
1261
+ return ref.match({
1262
+ mark: (_$, ev) => ev.mark.equal("rel").ifElse(() => some(_jsx(Text, { children: "Go/no-go review." })), () => noBody),
1263
+ }, _$ => noBody);
1264
+ }));
1265
+ return (_jsx(Plan, { popover: popover, axis: axis, data: streams, series: series }));
1266
+ }),
1267
+ inputs: [],
1268
+ });
1269
+ 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",
1272
+ fn: East.function([], UIComponentType, ($) => {
1273
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1274
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1275
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1276
+ return w1.addWeeks(n.subtract(1n));
1277
+ }));
1278
+ const JobRow = StructType({
1279
+ key: StringType, batch: StringType,
1280
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
1281
+ });
1282
+ const LineRow = StructType({
1283
+ series: StringType, line: StringType, label: StringType,
1284
+ jobs: ArrayType(JobRow),
1285
+ cells: ArrayType(Plan.Types.HeatCell),
1286
+ });
1287
+ 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));
1288
+ const cells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
1289
+ at: Plan.at.time(week(i.add(27n))),
1290
+ value: some(pcts.get(i)),
1291
+ label: some(East.Float.printFixed(pcts.get(i), 0n)),
1292
+ })));
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",
1296
+ jobs: [{ key: "r", batch: "B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) }],
1297
+ 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 }],
1307
+ ]), DictType(StringType, LineRow));
1308
+ 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)));
1349
+ const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
1350
+ return (_jsx(Plan, { axis: axis, data: lines, series: series }));
1351
+ }),
1352
+ inputs: [],
1353
+ });
1354
+ // ============================================================================
1355
+ // planSeriesData — the minimal data + series introduction
1356
+ // ============================================================================
1357
+ 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",
1360
+ fn: East.function([], UIComponentType, ($) => {
1361
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1362
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1363
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1364
+ return w1.addWeeks(n.subtract(1n));
1365
+ }));
1366
+ // The RAW domain shape — series discriminated by a variant field
1367
+ // (the natural ops-dataset form; the same rows page from a dataset).
1368
+ const JobRow = StructType({
1369
+ batch: StringType, start: DateTimeType, end: DateTimeType,
1370
+ tonnes: FloatType, state: EventStateType,
1371
+ });
1372
+ const ShiftRow = StructType({
1373
+ key: StringType, from: DateTimeType, to: DateTimeType,
1374
+ hours: FloatType, state: EventStateType,
1375
+ });
1376
+ const OpsRow = StructType({
1377
+ line: StringType,
1378
+ kind: VariantType({
1379
+ machine: StructType({ jobs: ArrayType(JobRow) }),
1380
+ crew: StructType({ shifts: ArrayType(ShiftRow) }),
1381
+ }),
1382
+ });
1383
+ const ops = $.const(new Map([
1384
+ ["L1-M03", { line: "Line 1", kind: variant("machine", { jobs: [
1385
+ { batch: "B-214", start: week(28n), end: week(31n), tonnes: 96.0, state: variant("in-progress", null) },
1386
+ { batch: "B-221", start: week(32n), end: week(35n), tonnes: 88.0, state: variant("proposed", variant("recommended", null)) },
1387
+ ] }) }],
1388
+ ["L1-M04", { line: "Line 1", kind: variant("machine", { jobs: [
1389
+ { batch: "B-208", start: week(27n), end: week(30n), tonnes: 112.0, state: variant("actual", null) },
1390
+ ] }) }],
1391
+ ["L2-M11", { line: "Line 2", kind: variant("machine", { jobs: [
1392
+ { batch: "B-241", start: week(29n), end: week(33n), tonnes: 92.0, state: variant("confirmed", null) },
1393
+ ] }) }],
1394
+ ["crewA", { line: "Line 1", kind: variant("crew", { shifts: [
1395
+ { key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
1396
+ { key: "s2", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
1397
+ ] }) }],
1398
+ ]), DictType(StringType, OpsRow));
1399
+ // 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.
1403
+ 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: [
1405
+ Plan.mark({ key: "kick", at: week(28n), kind: "milestone", label: "KICKOFF" }),
1406
+ Plan.mark({ key: "rel", at: week(33n), kind: "milestone", label: "REL 2.4" }),
1407
+ ] })]),
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)));
1437
+ 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 }));
1439
+ }),
1440
+ inputs: [],
1441
+ });
1442
+ // ============================================================================
1443
+ // planSeriesLibrary — the pickable series library (the DnD behavioral isolate)
1444
+ // ============================================================================
1445
+ export const planLibraryDnd = example({
1446
+ keywords: [
1447
+ "Plan", "data", "series", "library", "Pick", "pick", "Panel", "pickItems", "hidden",
1448
+ "toggle", "eye", "kind icon", "count", "group", "grouped", "discovered", "by", "rows",
1449
+ "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",
1452
+ ],
1453
+ description: "The series library across every row kind — duplicate kinds, groups, and a `keySuffix` pair showing one asset three ways",
1454
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
1455
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1456
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1457
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1458
+ return w1.addWeeks(n.subtract(1n));
1459
+ }));
1460
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
1461
+ const JobRow = StructType({
1462
+ key: StringType, label: StringType,
1463
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
1464
+ });
1465
+ const ShiftRow = StructType({
1466
+ key: StringType, from: DateTimeType, to: DateTimeType, label: StringType, state: EventStateType,
1467
+ });
1468
+ const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
1469
+ // ONE flat source (the `planExpand` shape): `pick` names the series
1470
+ // 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,
1476
+ jobs: ArrayType(JobRow),
1477
+ points: ArrayType(MeasureRow),
1478
+ cells: ArrayType(Plan.Types.HeatCell),
1479
+ allocs: ArrayType(AllocRow),
1480
+ nums: ArrayType(Plan.Types.TableCell),
1481
+ shifts: ArrayType(ShiftRow),
1482
+ marks: ArrayType(Plan.Types.EventMark),
1483
+ });
1484
+ const noJobs = $.const([], ArrayType(JobRow));
1485
+ const noPoints = $.const([], ArrayType(MeasureRow));
1486
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
1487
+ const noAllocs = $.const([], ArrayType(AllocRow));
1488
+ const noNums = $.const([], ArrayType(Plan.Types.TableCell));
1489
+ const noShifts = $.const([], ArrayType(ShiftRow));
1490
+ const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
1491
+ const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
1492
+ const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
1493
+ const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
1494
+ at: Plan.at.time(week(i.multiply(2n).add(27n))),
1495
+ value: some(pcts.get(i)),
1496
+ label: some(East.Float.printFixed(pcts.get(i), 0n)),
1497
+ })));
1498
+ // The lifecycle states as consts, so each tag stays a LITERAL —
1499
+ // `variant(someString, null)` widens to `variant<string, null>` and
1500
+ // stops satisfying `EventStateType`.
1501
+ const ACTUAL = variant("actual", null);
1502
+ const CONFIRMED = variant("confirmed", null);
1503
+ const RUNNING = variant("in-progress", null);
1504
+ const PROPOSED = variant("proposed", variant("recommended", null));
1505
+ const base = {
1506
+ line: "", jobs: noJobs, points: noPoints, cells: noCells,
1507
+ allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks,
1508
+ };
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.
1511
+ 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",
1518
+ jobs: [{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: RUNNING }],
1519
+ points,
1520
+ nums: [
1521
+ { at: Plan.at.time(week(28n)), value: some(96.0), text: none, tone: none },
1522
+ { at: Plan.at.time(week(31n)), value: some(88.0), text: none, tone: none },
1523
+ ] }],
1524
+ ["20-m04", { ...base, pick: "machines", label: "L1-M04",
1525
+ jobs: [{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: ACTUAL }],
1526
+ points,
1527
+ nums: [
1528
+ { at: Plan.at.time(week(28n)), value: some(112.0), text: none, tone: none },
1529
+ { at: Plan.at.time(week(31n)), value: some(-24.0), text: none, tone: none },
1530
+ ] }],
1531
+ // SAME KIND as the machines series, different entry — a kind is
1532
+ // not an identity, which is why the library keys on `key`.
1533
+ ["25-c01", { ...base, pick: "contract", label: "CON-01",
1534
+ 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",
1538
+ allocs: [
1539
+ { key: "a1", at: week(28n), state: variant("confirmed", null) },
1540
+ { key: "a2", at: week(31n), state: variant("proposed", variant("recommended", null)) },
1541
+ ] }],
1542
+ ["50-desp", { ...base, pick: "table", label: "Despatch t",
1543
+ nums: [
1544
+ { at: Plan.at.time(week(28n)), value: some(128.0), text: none, tone: none },
1545
+ { at: Plan.at.time(week(30n)), value: some(-96.0), text: none, tone: none },
1546
+ ] }],
1547
+ ["60-crewA", { ...base, pick: "cards", label: "Crew A",
1548
+ shifts: [
1549
+ { key: "s1", from: week(27n), to: week(29n), label: "80h", state: variant("confirmed", null) },
1550
+ { key: "s2", from: week(31n), to: week(33n), label: "+64h", state: variant("proposed", variant("recommended", null)) },
1551
+ ] }],
1552
+ ["70-ms", { ...base, pick: "events", label: "MILESTONES",
1553
+ marks: [
1554
+ { key: "k", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
1555
+ { key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
1556
+ ] }],
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",
1560
+ jobs: [{ key: "b301", label: "RUN · B-301", start: week(29n), end: week(33n), state: CONFIRMED }] }],
1561
+ ["g1-m12", { ...base, pick: "gspan", label: "L3-M12",
1562
+ 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",
1565
+ 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 }] }],
1573
+ ]), 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.
1577
+ const all = $.const([
1578
+ // Literal one-off chrome — it names itself, so it can be
1579
+ // 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 })]),
1581
+ Plan.series.chart(OpsRow, {
1582
+ key: "util", title: "Utilisation", subtitle: "% per fortnight",
1583
+ match: r => r.pick.equal("util"),
1584
+ label: r => r.label, id: true, height: "spark",
1585
+ layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.pct })],
1586
+ }),
1587
+ Plan.series.span(OpsRow, {
1588
+ key: "machines", title: "Machine jobs", subtitle: "one row per machine",
1589
+ 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",
1611
+ // The label stays the ASSET — all three rows are the same
1612
+ // machine, and pretending otherwise would hide that. What
1613
+ // distinguishes them is the VIEW, which is what the gutter
1614
+ // 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
+ }),
1626
+ Plan.series.span(OpsRow, {
1627
+ key: "contract", title: "Contractor jobs", subtitle: "same KIND, own entry",
1628
+ match: r => r.pick.equal("contract"),
1629
+ label: r => r.label, id: true,
1630
+ runs: r => r.jobs.map((_$, j) => Plan.run({
1631
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
1632
+ })),
1633
+ }),
1634
+ Plan.series.heat(OpsRow, {
1635
+ key: "load", title: "Line load", subtitle: "% per fortnight",
1636
+ match: r => r.pick.equal("load"),
1637
+ label: r => r.label,
1638
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1639
+ }),
1640
+ Plan.series.heat(OpsRow, {
1641
+ key: "quality", title: "Quality index", subtitle: "same KIND, own entry",
1642
+ match: r => r.pick.equal("quality"),
1643
+ label: r => r.label,
1644
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1645
+ }),
1646
+ Plan.series.buckets(OpsRow, {
1647
+ key: "docks", title: "Dock allocations", subtitle: "tiles per bucket",
1648
+ match: r => r.pick.equal("docks"),
1649
+ label: r => r.label,
1650
+ events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
1651
+ }),
1652
+ Plan.series.table(OpsRow, {
1653
+ key: "table", title: "Despatch tonnes", subtitle: "per bucket",
1654
+ match: r => r.pick.equal("table"),
1655
+ label: r => r.label,
1656
+ cells: r => r.nums,
1657
+ format: Format.Number({ maximumFractionDigits: 0n }),
1658
+ }),
1659
+ Plan.series.cards(OpsRow, {
1660
+ key: "cards", title: "Crew shifts", subtitle: "assignments",
1661
+ match: r => r.pick.equal("cards"),
1662
+ label: r => r.label,
1663
+ chips: r => r.shifts.map((_$, s) => Plan.chip({ key: s.key, from: s.from, to: s.to, label: s.label, state: s.state })),
1664
+ }),
1665
+ Plan.series.events(OpsRow, {
1666
+ key: "events", title: "Milestones", subtitle: "instant marks",
1667
+ match: r => r.pick.equal("events"),
1668
+ label: r => r.label, id: true,
1669
+ marks: r => r.marks,
1670
+ }),
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.
1675
+ //
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" }, [
1682
+ Plan.series.span(OpsRow, {
1683
+ key: "gspan", title: "Grouped span", subtitle: "member",
1684
+ match: r => r.pick.equal("gspan"),
1685
+ label: r => r.label, id: true,
1686
+ runs: r => r.jobs.map((_$, j) => Plan.run({
1687
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
1688
+ })),
1689
+ }),
1690
+ ]),
1691
+ Plan.series.group(OpsRow, { key: "g2-lines", label: "Load", meta: "heat" }, [
1692
+ Plan.series.heat(OpsRow, {
1693
+ key: "gheat", title: "Grouped heat", subtitle: "member",
1694
+ match: r => r.pick.equal("gheat"),
1695
+ label: r => r.label,
1696
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1697
+ }),
1698
+ ]),
1699
+ Plan.series.group(OpsRow, { key: "g3-docks", label: "Docks", meta: "buckets" }, [
1700
+ Plan.series.buckets(OpsRow, {
1701
+ key: "gbuckets", title: "Grouped buckets", subtitle: "member",
1702
+ match: r => r.pick.equal("gbuckets"),
1703
+ label: r => r.label,
1704
+ events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
1705
+ }),
1706
+ ]),
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.
1710
+ 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
+ ]),
1723
+ ], 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.
1726
+ const shown = $.let(Plan.pick("ex.plan.library", all, {
1727
+ data: ops,
1728
+ hidden: ["quality", "cards", "g3-docks"],
1729
+ }));
1730
+ 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
+ // `pick` REPLACES `series`: the handle already carries the list, so
1734
+ // the canvas feeds itself the picked ones and mounts the library.
1735
+ // 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" } }));
1737
+ } }))),
1738
+ inputs: [],
1739
+ });
1740
+ // ============================================================================
1741
+ // planRowDrop — the canvas as a drag TARGET, and which rows can receive
1742
+ // ============================================================================
1743
+ /**
1744
+ * A Plan is a drag target, and a heterogeneous one — which is what makes it
1745
+ * different from every other target in the grammar.
1746
+ *
1747
+ * Roster, Board and Blend have ONE kind of cell, so "can you drop here" is a
1748
+ * 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:
1750
+ *
1751
+ * 1. **Structurally, by kind.** Only rows holding discrete scheduled objects
1752
+ * register a drop cell at all — `span` (runs), `buckets` (tiles), `events`
1753
+ * (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.
1758
+ * 2. **By policy, with `canDrop`.** Of the rows that can receive, this canvas
1759
+ * admits only the matching FAMILY: a job goes on a machine, a delivery on
1760
+ * a dock, a shift on a crew, a milestone on a stream. The `PALLET` card
1761
+ * belongs to no family and is therefore refused everywhere — the ⊘ stage
1762
+ * on every row, which is what a card with nowhere to go should look like.
1763
+ *
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.
1769
+ */
1770
+ export const planRowDrop = example({
1771
+ keywords: [
1772
+ "Plan", "Library", "DnD", "drag", "drop", "onDrag", "canDrop", "sources", "id",
1773
+ "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",
1776
+ ],
1777
+ description: "Library + Plan DnD — only the kinds holding discrete objects receive, and `canDrop` admits only the matching family",
1778
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
1779
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
1780
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
1781
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
1782
+ return w1.addWeeks(n.subtract(1n));
1783
+ }));
1784
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
1785
+ const JobRow = StructType({
1786
+ key: StringType, label: StringType,
1787
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
1788
+ });
1789
+ const ShiftRow = StructType({
1790
+ key: StringType, from: DateTimeType, to: DateTimeType, label: StringType, state: EventStateType,
1791
+ });
1792
+ 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
+ const OpsRow = StructType({
1798
+ series: StringType, label: StringType, line: StringType,
1799
+ jobs: ArrayType(JobRow),
1800
+ points: ArrayType(MeasureRow),
1801
+ cells: ArrayType(Plan.Types.HeatCell),
1802
+ allocs: ArrayType(AllocRow),
1803
+ nums: ArrayType(Plan.Types.TableCell),
1804
+ shifts: ArrayType(ShiftRow),
1805
+ marks: ArrayType(Plan.Types.EventMark),
1806
+ });
1807
+ const noJobs = $.const([], ArrayType(JobRow));
1808
+ const noPoints = $.const([], ArrayType(MeasureRow));
1809
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
1810
+ const noAllocs = $.const([], ArrayType(AllocRow));
1811
+ const noNums = $.const([], ArrayType(Plan.Types.TableCell));
1812
+ const noShifts = $.const([], ArrayType(ShiftRow));
1813
+ const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
1814
+ const noDrops = $.const([], ArrayType(DropRow));
1815
+ const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
1816
+ const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
1817
+ const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
1818
+ at: Plan.at.time(week(i.multiply(2n).add(27n))),
1819
+ value: some(pcts.get(i)),
1820
+ label: some(East.Float.printFixed(pcts.get(i), 0n)),
1821
+ })));
1822
+ const CONFIRMED = variant("confirmed", null);
1823
+ const RUNNING = variant("in-progress", null);
1824
+ // Everything a drop creates is a PROPOSAL — it is a suggestion the
1825
+ // host has not committed, and the lifecycle is how the canvas says so.
1826
+ const ADDED = variant("proposed", variant("added", null));
1827
+ const base = {
1828
+ line: "", jobs: noJobs, points: noPoints, cells: noCells,
1829
+ allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks,
1830
+ };
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",
1837
+ jobs: [{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: RUNNING }] }],
1838
+ ["20-m04", { ...base, series: "mach", label: "L1-M04",
1839
+ 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",
1842
+ allocs: [{ key: "a1", at: week(29n), state: CONFIRMED }] }],
1843
+ ["50-desp", { ...base, series: "table", label: "Despatch t",
1844
+ nums: [
1845
+ { at: Plan.at.time(week(28n)), value: some(128.0), text: none, tone: none },
1846
+ { at: Plan.at.time(week(31n)), value: some(-96.0), text: none, tone: none },
1847
+ ] }],
1848
+ ["60-crewA", { ...base, series: "crew", label: "Crew A",
1849
+ shifts: [{ key: "s1", from: week(27n), to: week(29n), label: "80h", state: CONFIRMED }] }],
1850
+ ["70-ms", { ...base, series: "strm", label: "MILESTONES",
1851
+ 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",
1855
+ jobs: [{ key: "b301", label: "RUN · B-301", start: week(30n), end: week(34n), state: CONFIRMED }] }],
1856
+ ]), DictType(StringType, OpsRow));
1857
+ // ── 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.
1861
+ 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"],
1866
+ ]), DictType(StringType, StringType));
1867
+ const CardRow = StructType({
1868
+ key: StringType, name: StringType, family: StringType, note: StringType, icon: StringType,
1869
+ });
1870
+ const cards = $.const([
1871
+ { key: "job-weld", name: "Weld cell", family: "job", note: "job · machines", icon: "gear" },
1872
+ { key: "job-cure", name: "Cure oven", family: "job", note: "job · machines", icon: "fire" },
1873
+ { key: "dlv-truck", name: "Truck 12", family: "delivery", note: "delivery · docks", icon: "truck" },
1874
+ { key: "shf-night", name: "Night shift", family: "shift", note: "shift · crews", icon: "moon" },
1875
+ { key: "mst-audit", name: "Audit gate", family: "milestone", note: "milestone · streams", icon: "flag" },
1876
+ // Belongs to no family, so no row accepts it — the ⊘ stage
1877
+ // everywhere, which is what a card with nowhere to go looks like.
1878
+ { key: "pallet", name: "PALLET", family: "none", note: "fits nowhere", icon: "box" },
1879
+ ], ArrayType(CardRow));
1880
+ const cardFamily = $.const(cards.toDict((_$, c) => c.key, (_$, c) => c.family));
1881
+ const cardName = $.const(cards.toDict((_$, c) => c.key, (_$, c) => c.name));
1882
+ // ── The drop veto ────────────────────────────────────────────
1883
+ // 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.
1888
+ const canDrop = $.const(East.function([DragEventType], BooleanType, ($, event) => {
1889
+ const no = $.const(false, BooleanType);
1890
+ return event.match({
1891
+ add: ($, add) => {
1892
+ const row = $.let(add.into.row);
1893
+ const card = $.let(add.from.key);
1894
+ return rowAccepts.has(row)
1895
+ .and(_$ => cardFamily.has(card))
1896
+ .and(_$ => rowAccepts.get(row).equal(cardFamily.get(card)));
1897
+ },
1898
+ }, _$ => no);
1899
+ }));
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()));
1905
+ 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
+ });
1925
+ }));
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
+ const last = $.let(lastBind.read());
1935
+ const axis = $.const(Plan.axis({
1936
+ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
1937
+ }));
1938
+ 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
+ // The DnD target role: `id` names this surface in every
1940
+ // 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: [
1946
+ // INERT — a chart plots a derived series, so there
1947
+ // is nothing a card could become here.
1948
+ Plan.series.chart(OpsRow, {
1949
+ key: "util", title: "Utilisation",
1950
+ match: r => r.series.equal("util"),
1951
+ label: r => r.label, id: true, height: "spark",
1952
+ layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
1953
+ }),
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.
1958
+ Plan.series.span(OpsRow, {
1959
+ key: "mach", title: "Machine jobs",
1960
+ match: r => r.series.equal("mach"),
1961
+ label: r => r.label, id: true,
1962
+ runs: (r, k) => r.jobs.map((_$, j) => Plan.run({
1963
+ 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
+ }))),
1969
+ }),
1970
+ // INERT — an intensity field has no object to add to.
1971
+ Plan.series.heat(OpsRow, {
1972
+ key: "load", title: "Line load",
1973
+ match: r => r.series.equal("load"),
1974
+ label: r => r.label,
1975
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
1976
+ }),
1977
+ // RECEIVES — a dropped delivery becomes a tile in
1978
+ // the bucket under the pointer.
1979
+ Plan.series.buckets(OpsRow, {
1980
+ key: "dock", title: "Dock allocations",
1981
+ match: r => r.series.equal("dock"),
1982
+ 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 }))),
1986
+ }),
1987
+ // INERT — the cells are computed numbers.
1988
+ Plan.series.table(OpsRow, {
1989
+ key: "table", title: "Despatch tonnes",
1990
+ match: r => r.series.equal("table"),
1991
+ label: r => r.label,
1992
+ cells: r => r.nums,
1993
+ format: Format.Number({ maximumFractionDigits: 0n }),
1994
+ }),
1995
+ // RECEIVES — a dropped shift becomes a chip.
1996
+ Plan.series.cards(OpsRow, {
1997
+ key: "crew", title: "Crew shifts",
1998
+ match: r => r.series.equal("crew"),
1999
+ label: r => r.label,
2000
+ chips: (r, k) => r.shifts.map((_$, s) => Plan.chip({
2001
+ 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
+ }))),
2007
+ }),
2008
+ // RECEIVES — a dropped milestone becomes a mark at
2009
+ // the instant, the one kind with no duration.
2010
+ Plan.series.events(OpsRow, {
2011
+ key: "strm", title: "Milestones",
2012
+ match: r => r.series.equal("strm"),
2013
+ 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
+ }))),
2018
+ }),
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" }, [
2021
+ Plan.series.span(OpsRow, {
2022
+ key: "gmach", title: "Grouped machine jobs",
2023
+ match: r => r.series.equal("gmach"),
2024
+ label: r => r.label, id: true,
2025
+ runs: (r, k) => r.jobs.map((_$, j) => Plan.run({
2026
+ 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
+ }))),
2032
+ }),
2033
+ ]),
2034
+ ], style: { height: "420px" } }), _jsx(Text.MonoLabel, { children: East.str `LAST DROP · ${last}` })] }));
2035
+ } }))),
2036
+ inputs: [],
2037
+ });
2038
+ // ============================================================================
2039
+ // planFill — the bounded sizing isolate (#320 / #567 D1)
2040
+ // ============================================================================
2041
+ /** 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).
2046
+ *
2047
+ * The grouping is here for what it does to VIRTUALIZATION, not for the
2048
+ * chrome. Collapsing a strip removes its 25 children from the virtualizer's
2049
+ * item list, so `count` and the total size change while the scroll offset does
2050
+ * 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. */
2054
+ 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",
2057
+ fn: East.function([], UIComponentType, ($) => {
2058
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open).
2059
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2060
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2061
+ return w1.addWeeks(n.subtract(1n));
2062
+ }));
2063
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
2064
+ const UnitRow = StructType({
2065
+ line: StringType, series: StringType,
2066
+ sub: OptionType(StringType),
2067
+ start: DateTimeType, end: DateTimeType, tonnes: FloatType,
2068
+ points: ArrayType(MeasureRow),
2069
+ cells: ArrayType(Plan.Types.HeatCell),
2070
+ });
2071
+ // 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`, …).
2075
+ //
2076
+ // `line` is the grouping level: 8 strips of 25, so a single collapse
2077
+ // takes an eighth of the list out of the virtualizer at once.
2078
+ //
2079
+ // Everything else here exists to make the row heights DISAGREE, which
2080
+ // is the case a virtualizer gets wrong. `series` cycles span / chart /
2081
+ // 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`.
2085
+ const units = $.const(East.Array.range(0n, 200n).toDict((_$, i) => East.str `UNIT-${East.print(i.add(1000n))}`, ($, i) => {
2086
+ const m = $.let(i.modulo(10n), IntegerType);
2087
+ return {
2088
+ line: East.str `LINE ${East.print(i.modulo(8n).add(1n))}`,
2089
+ // The kind cycles with the KEY, so every stretch of the canvas
2090
+ // holds the same mix — clustering the tall rows at one end
2091
+ // would leave most of the scroll a single height again.
2092
+ series: m.equal(4n).ifElse(() => "heat", () => m.equal(5n).ifElse(() => "spark", () => m.equal(6n).ifElse(() => "chartM", () => m.equal(7n).ifElse(() => "chartL", () => m.equal(8n).ifElse(() => "chartXL", () => "span"))))),
2093
+ // Alternating one-line / two-line gutters.
2094
+ sub: i.modulo(2n).equal(0n).ifElse(() => some(East.str `cap ${East.print(i.modulo(9n).add(40n))} t`), () => $.const(none, OptionType(StringType))),
2095
+ start: week(i.modulo(9n).add(27n)),
2096
+ end: week(i.modulo(9n).add(30n)),
2097
+ tonnes: i.toFloat().multiply(1.5).add(40.0),
2098
+ points: East.Array.generate(6n, MeasureRow, (_$, j) => ({
2099
+ week: week(j.multiply(2n).add(27n)),
2100
+ pct: j.multiply(17n).add(i).remainder(60n).toFloat().add(40.0),
2101
+ })),
2102
+ cells: East.Array.generate(6n, Plan.Types.HeatCell, (_$, j) => ({
2103
+ at: Plan.at.time(week(j.multiply(2n).add(27n))),
2104
+ value: some(j.multiply(13n).add(i).remainder(100n).toFloat()),
2105
+ label: none,
2106
+ })),
2107
+ };
2108
+ }), 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.
2116
+ 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)));
2170
+ 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" } }) }));
2172
+ }),
2173
+ inputs: [],
2174
+ });
2175
+ // ============================================================================
2176
+ // planReview — the review chrome, and what a verdict is FOR (#569)
2177
+ // ============================================================================
2178
+ export const planReview = example({
2179
+ 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",
2184
+ ],
2185
+ description: "Live review chrome — verdicts live in bound `State`, and the canvas re-derives buttons, bar and dot",
2186
+ fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
2187
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
2188
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2189
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2190
+ return w1.addWeeks(n.subtract(1n));
2191
+ }));
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.
2195
+ const JobRow = StructType({
2196
+ start: DateTimeType, end: DateTimeType, tonnes: FloatType, flagged: BooleanType,
2197
+ });
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"],
2209
+ ])));
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));
2223
+ }));
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
+ const series = $.const([
2230
+ Plan.series.span(JobRow, {
2231
+ key: "span-5", title: "Span",
2232
+ label: (_r, k) => k, id: true,
2233
+ 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
2239
+ // 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)),
2242
+ runs: (r, k) => [Plan.run({
2243
+ key: k, start: r.start, end: r.end,
2244
+ 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)))),
2248
+ })],
2249
+ }),
2250
+ ], ArrayType(Plan.Types.Series(JobRow)));
2251
+ const axis = $.const(Plan.axis({
2252
+ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
2253
+ }));
2254
+ const counts = $.let(live.filter((_$, v) => v.equal("pending")).size());
2255
+ const rejected = $.let(live.filter((_$, v) => v.equal("rejected")).size());
2256
+ 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
+ } }));
2260
+ } }))),
2261
+ inputs: [],
2262
+ });
2263
+ /**
2264
+ * R2 expand-in-place — the one example that shows the whole gesture.
2265
+ *
2266
+ * Expand is two halves and needs BOTH: a per-row `expand` declaration (data,
2267
+ * 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.
2270
+ *
2271
+ * The rows here are deliberately heterogeneous — span, chart, heat, table,
2272
+ * events — because focusing one is what makes the other five collapse, and
2273
+ * each row KIND collapses differently (#591): geometry shrinks, values
2274
+ * re-encode as a tone strip, shapes keep their silhouette.
2275
+ */
2276
+ 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"],
2278
+ description: "Expand-in-place — a row declares `expand`, the root renders plot and gutter, and unfocused rows collapse to strips",
2279
+ fn: East.function([], UIComponentType, ($) => {
2280
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2281
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2282
+ return w1.addWeeks(n.subtract(1n));
2283
+ }));
2284
+ // ONE raw source; `series` picks the series and `expand` is per-row
2285
+ // DATA — presence is what grows the ⤢ control on that row.
2286
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
2287
+ const OpsRow = StructType({
2288
+ series: StringType,
2289
+ label: StringType,
2290
+ expand: OptionType(Plan.Types.Expand),
2291
+ jobs: ArrayType(StructType({
2292
+ key: StringType, label: StringType,
2293
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
2294
+ })),
2295
+ points: ArrayType(MeasureRow),
2296
+ cells: ArrayType(Plan.Types.HeatCell),
2297
+ nums: ArrayType(Plan.Types.TableCell),
2298
+ marks: ArrayType(Plan.Types.EventMark),
2299
+ });
2300
+ const noJobs = $.const([], ArrayType(StructType({
2301
+ key: StringType, label: StringType,
2302
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
2303
+ })));
2304
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
2305
+ const noNums = $.const([], ArrayType(Plan.Types.TableCell));
2306
+ const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
2307
+ const noPoints = $.const([], ArrayType(MeasureRow));
2308
+ const covPcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
2309
+ const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: covPcts.get(i) })));
2310
+ const ops = $.const(new Map([
2311
+ // A CHART row — the one kind whose marks are a VALUE scale. Its
2312
+ // plot, its gutter ticks and its ref-label gate answer to the band
2313
+ // the marks keep at the top, not to the grown row (#591).
2314
+ ["COVERAGE", { series: "chart", label: "COVERAGE",
2315
+ expand: some({ height: some("150px"), axis: variant("keep", null) }),
2316
+ jobs: noJobs, points: coverage, cells: noCells, nums: noNums, marks: noMarks }],
2317
+ // axis: keep — the grid and now-line run THROUGH the render.
2318
+ ["L1-M03", { series: "span", label: "L1-M03",
2319
+ expand: some({ height: some("168px"), axis: variant("keep", null) }),
2320
+ jobs: [
2321
+ { key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
2322
+ { key: "qc", label: "QC", start: week(30n), end: week(32n), state: variant("confirmed", null) },
2323
+ { key: "b231", label: "RUN · B-231", start: week(33n), end: week(38n), state: variant("proposed", variant("recommended", null)) },
2324
+ ], points: noPoints, cells: noCells, nums: noNums, marks: noMarks }],
2325
+ // axis: dim — washed to 40% behind a dense render.
2326
+ ["L1-M04", { series: "span", label: "L1-M04",
2327
+ expand: some({ height: some("140px"), axis: variant("dim", null) }),
2328
+ jobs: [
2329
+ { key: "b214", label: "RUN · B-214", start: week(28n), end: week(33n), state: variant("in-progress", null) },
2330
+ ], points: noPoints, cells: noCells, nums: noNums, marks: noMarks }],
2331
+ // No declaration — no control. The contrast is the point: one row
2332
+ // that cannot be expanded beside five that can.
2333
+ ["L1-M07", { series: "span", label: "L1-M07", expand: none,
2334
+ jobs: [
2335
+ { key: "hld", label: "HLD · B-197", start: week(27n), end: week(31n), state: variant("actual", null) },
2336
+ ], points: noPoints, cells: noCells, nums: noNums, marks: noMarks }],
2337
+ // The kinds that COLLAPSE differently — heat keeps its ramp, the
2338
+ // table re-encodes its numerals, the marks keep their silhouettes.
2339
+ ["LOAD", { series: "heat", label: "Line load",
2340
+ expand: some({ height: some("132px"), axis: variant("keep", null) }),
2341
+ jobs: noJobs, points: noPoints, nums: noNums, marks: noMarks,
2342
+ cells: [
2343
+ { at: Plan.at.time(week(27n)), value: some(46.0), label: some("46") },
2344
+ { at: Plan.at.time(week(28n)), value: some(58.0), label: some("58") },
2345
+ { at: Plan.at.time(week(29n)), value: some(66.0), label: some("66") },
2346
+ { at: Plan.at.time(week(30n)), value: some(72.0), label: some("72") },
2347
+ { at: Plan.at.time(week(31n)), value: some(84.0), label: some("84") },
2348
+ { at: Plan.at.time(week(32n)), value: some(90.0), label: some("90") },
2349
+ { at: Plan.at.time(week(33n)), value: some(96.0), label: some("96") },
2350
+ { at: Plan.at.time(week(34n)), value: none, label: none },
2351
+ { at: Plan.at.time(week(35n)), value: some(92.0), label: some("92") },
2352
+ ] }],
2353
+ // axis: off — the render draws its own canvas, so the shared lines
2354
+ // are suppressed INSIDE this row only (the ruler never moves).
2355
+ ["DESPATCH", { series: "table", label: "Despatch t",
2356
+ expand: some({ height: some("120px"), axis: variant("off", null) }),
2357
+ jobs: noJobs, points: noPoints, cells: noCells, marks: noMarks,
2358
+ nums: [
2359
+ { at: Plan.at.time(week(27n)), value: some(128.0), text: none, tone: none },
2360
+ { at: Plan.at.time(week(28n)), value: some(134.0), text: none, tone: none },
2361
+ { at: Plan.at.time(week(29n)), value: some(119.0), text: none, tone: none },
2362
+ { at: Plan.at.time(week(30n)), value: some(-96.0), text: none, tone: none },
2363
+ { at: Plan.at.time(week(31n)), value: some(-88.0), text: none, tone: none },
2364
+ { at: Plan.at.time(week(32n)), value: none, text: none, tone: none },
2365
+ { at: Plan.at.time(week(33n)), value: some(151.0), text: none, tone: none },
2366
+ { at: Plan.at.time(week(34n)), value: some(162.0), text: none, tone: none },
2367
+ { at: Plan.at.time(week(35n)), value: some(144.0), text: none, tone: none },
2368
+ ] }],
2369
+ ["MILESTONES", { series: "events", label: "MILESTONES",
2370
+ expand: some({ height: some("112px"), axis: variant("dim", null) }),
2371
+ jobs: noJobs, points: noPoints, cells: noCells, nums: noNums,
2372
+ marks: [
2373
+ { key: "k", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
2374
+ { key: "d", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
2375
+ { key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
2376
+ ] }],
2377
+ ]), DictType(StringType, OpsRow));
2378
+ // ONE resolver serves every declaring row, called with the row ref at
2379
+ // interaction time. The canvas hands it the PLOT column with the
2380
+ // shared grid + now-line drawn behind it — so a component that fills
2381
+ // that column edge to edge shares the canvas's x-space and lines up
2382
+ // with the buckets above. `Sparkline` is the axis-free chart, which is
2383
+ // what makes the alignment visible; a `Chart` would draw its own axes
2384
+ // and margins inside the column and align to those instead.
2385
+ 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%" }))));
2387
+ // The GUTTER half. An expanded row's gutter cell grows with the row,
2388
+ // and what fills the space it opens up is the author's — the identity
2389
+ // 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.
2391
+ // The old spec's drilled-row card, which is what the grown gutter is
2392
+ // for: identity lines then a fill meter. The lines need no styling —
2393
+ // the gutter body already carries the sub-line vocabulary — so the
2394
+ // author writes content, not typography.
2395
+ const GutterFacts = StructType({ a: StringType, b: StringType, fill: FloatType });
2396
+ const expandGutter = $.const(East.function([Plan.Types.RowRef], UIComponentType, ($, ref) => {
2397
+ const facts = $.const(new Map([
2398
+ ["COVERAGE", { a: "TARGET 100 · MIN 92", b: "BREACH W34–W36 · 3 wk", fill: 0.94 }],
2399
+ ["L1-M03", { a: "120 t · FILL", b: "B-208 · 88 t · 73%", fill: 0.73 }],
2400
+ ["L1-M04", { a: "120 t · FILL", b: "B-214 · 89 t · 74%", fill: 0.74 }],
2401
+ ["LOAD", { a: "MEAN 74 · PEAK 96", b: "BREACH W33 · 1 wk", fill: 0.96 }],
2402
+ ["DESPATCH", { a: "NET 1 629 t", b: "2 SHORT WEEKS", fill: 0.55 }],
2403
+ ["MILESTONES", { a: "5 MARKS", b: "1 EXCEPTION · W34", fill: 0.2 }],
2404
+ ]), DictType(StringType, GutterFacts));
2405
+ const f = $.let(facts.get(ref.key));
2406
+ 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
+ }));
2408
+ return (_jsx(Plan, { axis: Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }), data: ops, series: [
2409
+ Plan.series.span(OpsRow, {
2410
+ key: "span-6", title: "Span",
2411
+ match: r => r.series.equal("span"),
2412
+ label: r => r.label, id: true, expand: r => r.expand,
2413
+ runs: r => r.jobs.map((_$, j) => Plan.run({
2414
+ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
2415
+ })),
2416
+ }),
2417
+ Plan.series.chart(OpsRow, {
2418
+ key: "chart-2", title: "Chart",
2419
+ match: r => r.series.equal("chart"),
2420
+ label: r => r.label, id: true, expand: r => r.expand,
2421
+ height: "spark",
2422
+ left: { domain: [80, 110], tickValues: [80, 100] },
2423
+ layers: r => [
2424
+ Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
2425
+ Chart.refLine({ y: 100, label: "TARGET 100" }),
2426
+ ],
2427
+ }),
2428
+ Plan.series.heat(OpsRow, {
2429
+ key: "heat-3", title: "Heat",
2430
+ match: r => r.series.equal("heat"),
2431
+ label: r => r.label, expand: r => r.expand,
2432
+ cells: r => Plan.heatCells(r.cells, { min: 40.0, max: 100.0 }),
2433
+ }),
2434
+ Plan.series.table(OpsRow, {
2435
+ key: "table", title: "Table",
2436
+ match: r => r.series.equal("table"),
2437
+ label: r => r.label, expand: r => r.expand,
2438
+ cells: r => r.nums,
2439
+ }),
2440
+ Plan.series.events(OpsRow, {
2441
+ key: "events-2", title: "Events",
2442
+ match: r => r.series.equal("events"),
2443
+ label: r => r.label, id: true, expand: r => r.expand,
2444
+ marks: r => r.marks,
2445
+ }),
2446
+ ], expandRender: expandRender, expandGutter: expandGutter, style: { height: "460px" } }));
2447
+ }),
2448
+ inputs: [],
2449
+ });
2450
+ // ============================================================================
2451
+ // planNarrow — the §10 narrow layout: a phone-width box makes the Plan a
2452
+ // review tool, not a canvas (#570)
2453
+ // ============================================================================
2454
+ /**
2455
+ * Below 480px of CONTAINER width — not the viewport: this example is a 360px
2456
+ * `<Box>` on a desktop page — the Plan reflows to the §10 layout: three tabs
2457
+ * over ONE slice (Groups · Rows · Measures), the group grain as hottest-first
2458
+ * strip cards, one group's rows as cards whose head is the row's gutter
2459
+ * identity and whose body is its plot on the shared window, chart rows
2460
+ * full-width at expanded density. A row declaring `expand` drills in place on
2461
+ * a second tap; horizontal pan is two-finger. The DEFINITION is the desktop
2462
+ * one — the same `data` + `series`, the same slice — only the box changed.
2463
+ */
2464
+ 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"],
2466
+ 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
+ fn: East.function([], UIComponentType, (_$) => {
2468
+ const HorizonRow = StructType({ key: StringType, at: DateTimeType, risk: StringType });
2469
+ const cfg = Slice.config(HorizonRow, {
2470
+ fields: { at: { label: "Despatched", format: { date: "MMM D" } }, risk: { label: "Risk", hints: ["late", "on-time"] } },
2471
+ rangeFieldId: "at",
2472
+ });
2473
+ return (_jsx(Reactive, { children: $ => {
2474
+ // Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
2475
+ const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
2476
+ const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
2477
+ return w1.addWeeks(n.subtract(1n));
2478
+ }));
2479
+ const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
2480
+ const JobRow = StructType({
2481
+ key: StringType, label: StringType,
2482
+ start: DateTimeType, end: DateTimeType, state: EventStateType,
2483
+ });
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.
2487
+ const OpsRow = StructType({
2488
+ series: StringType, line: StringType, label: StringType,
2489
+ value: OptionType(StringType), status: OptionType(StatusValueType),
2490
+ expand: OptionType(Plan.Types.Expand),
2491
+ jobs: ArrayType(JobRow),
2492
+ cells: ArrayType(Plan.Types.HeatCell),
2493
+ points: ArrayType(MeasureRow),
2494
+ });
2495
+ const noJobs = $.const([], ArrayType(JobRow));
2496
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
2497
+ const noPoints = $.const([], ArrayType(MeasureRow));
2498
+ 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
+ // Line 2 runs hotter than Line 1 — the Groups tab sorts it first.
2500
+ const loadA = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
2501
+ at: Plan.at.time(week(i.add(27n))), value: some(loadPcts.get(i).multiply(0.85)),
2502
+ label: some(East.Float.printFixed(loadPcts.get(i).multiply(0.85), 0n)),
2503
+ })));
2504
+ const loadB = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
2505
+ at: Plan.at.time(week(i.add(27n))), value: some(loadPcts.get(i)),
2506
+ label: some(East.Float.printFixed(loadPcts.get(i), 0n)),
2507
+ })));
2508
+ const covPcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
2509
+ const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: covPcts.get(i) })));
2510
+ const ops = $.const(new Map([
2511
+ ["L1-M03", { series: "mach", line: "Line 1 · Fill", label: "L1-M03",
2512
+ value: some("120 t"), status: some(variant("success", null)),
2513
+ expand: some({ height: some("140px"), axis: variant("keep", null) }),
2514
+ jobs: [
2515
+ { key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
2516
+ { key: "qc", label: "QC", start: week(30n), end: week(32n), state: variant("confirmed", null) },
2517
+ { key: "b231", label: "RUN · B-231", start: week(33n), end: week(38n), state: variant("proposed", variant("recommended", null)) },
2518
+ ], cells: noCells, points: noPoints }],
2519
+ ["L1-M04", { series: "mach", line: "Line 1 · Fill", label: "L1-M04",
2520
+ value: some("120 t"), status: some(variant("warning", null)), expand: none,
2521
+ jobs: [
2522
+ { key: "b214", label: "RUN · B-214", start: week(28n), end: week(33n), state: variant("in-progress", null) },
2523
+ ], cells: noCells, points: noPoints }],
2524
+ ["l1-load", { series: "load", line: "Line 1 · Fill", label: "Line load",
2525
+ value: none, status: none, expand: none,
2526
+ jobs: noJobs, cells: loadA, points: noPoints }],
2527
+ ["L2-M11", { series: "mach", line: "Line 2 · Assy", label: "L2-M11",
2528
+ value: some("80 t"), status: none, expand: none,
2529
+ jobs: [
2530
+ { key: "b241", label: "RUN · B-241", start: week(29n), end: week(34n), state: variant("confirmed", null) },
2531
+ ], cells: noCells, points: noPoints }],
2532
+ ["l2-load", { series: "load", line: "Line 2 · Assy", label: "Line load",
2533
+ 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 }],
2538
+ ]), DictType(StringType, OpsRow));
2539
+ // The slice's own rows — despatch orders with a late-risk cohort
2540
+ // seeded active, so the chips row has something to say.
2541
+ const horizon = $.let(East.Array.generate(24n, HorizonRow, (_$, i) => ({
2542
+ key: East.str `h${East.print(i.add(1n))}`,
2543
+ at: week(i.divide(2n).add(27n)),
2544
+ risk: i.remainder(3n).equal(0n).ifElse(() => "late", () => "on-time"),
2545
+ })));
2546
+ const slice = $.let(Slice.bind([HorizonRow], "ex.plan.narrow", cfg, Slice.state({
2547
+ range: some(variant("datetime", { from: week(27n), to: week(39n) })),
2548
+ cohorts: [{ id: "late", name: "Late risk", filters: [variant("string", { fieldId: "risk", op: variant("eq", "late") })] }],
2549
+ activeCohorts: new Set(["late"]),
2550
+ }), horizon, none));
2551
+ 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 }),
2573
+ }),
2574
+ ]),
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
+ ],
2585
+ }),
2586
+ ], ArrayType(Plan.Types.Series(OpsRow)));
2587
+ 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%" }))));
2589
+ const axis = $.const(Plan.axis({
2590
+ window: { min: week(27n), max: week(39n) },
2591
+ resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
2592
+ }));
2593
+ // The 360px box is the whole point: the reflow is a property of
2594
+ // 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: [
2596
+ { text: "6 ROWS · 2 LINES" },
2597
+ { text: "RUN 412 · W27–W38", end: true },
2598
+ ], style: { height: "560px" } }) }));
2599
+ } }));
2600
+ }),
2601
+ inputs: [],
2602
+ });
2603
+ // ============================================================================
2604
+ // planNumberAxis — the `number` axis (#631): day 1..8 with AM/PM lanes
2605
+ // ============================================================================
2606
+ /**
2607
+ * The `number` axis — the retired Planner's `plannerPoint` canvas on the
2608
+ * Plan: eight days at step 1, AM/PM lanes in the bucket rows, `now` at day
2609
+ * 5. Every kind positions on the one numeric scale, and every instant in the
2610
+ * raw data is a plain number: a `FloatType` field wraps to the `number` arm
2611
+ * through the element builders (`Plan.run({ start: j.start })`), a chart
2612
+ * layer's numeric x accessor lands its columns on the same arm, `Plan.tableCells`
2613
+ * 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.
2618
+ */
2619
+ export const planNumberAxis = example({
2620
+ keywords: [
2621
+ "Plan", "axis", "number", "numeric", "step", "Plan.axis.number", "Plan.at", "instant",
2622
+ "day", "AM", "PM", "lanes", "buckets", "Planner", "plannerPoint", "brush", "float", "range",
2623
+ "format", "now", "typed axis", "#631", "raw",
2624
+ ],
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",
2626
+ fn: East.function([], UIComponentType, (_$) => {
2627
+ const HorizonRow = StructType({ key: StringType, day: FloatType, line: StringType });
2628
+ const cfg = Slice.config(HorizonRow, {
2629
+ fields: { day: { label: "Day" }, line: { label: "Line" } },
2630
+ rangeFieldId: "day",
2631
+ });
2632
+ return (_jsx(Reactive, { children: $ => {
2633
+ // RAW rows — every instant is a plain number (a day index).
2634
+ const AllocRow = StructType({ key: StringType, day: FloatType, lane: StringType, state: EventStateType });
2635
+ const JobRow = StructType({ key: StringType, label: StringType, start: FloatType, end: FloatType, state: EventStateType });
2636
+ const ShiftRow = StructType({ key: StringType, from: FloatType, to: FloatType, hours: FloatType, state: EventStateType });
2637
+ const MarkRow = StructType({ key: StringType, day: FloatType, label: StringType });
2638
+ const PointRow = StructType({ day: FloatType, t: FloatType });
2639
+ const RawCell = StructType({ at: FloatType, value: OptionType(FloatType) });
2640
+ const OpsRow = StructType({
2641
+ series: StringType, label: StringType, value: OptionType(StringType),
2642
+ allocations: ArrayType(AllocRow), jobs: ArrayType(JobRow), shifts: ArrayType(ShiftRow),
2643
+ marks: ArrayType(MarkRow), points: ArrayType(PointRow), tonnes: ArrayType(RawCell),
2644
+ cells: ArrayType(Plan.Types.HeatCell),
2645
+ });
2646
+ const noAllocs = $.const([], ArrayType(AllocRow));
2647
+ const noJobs = $.const([], ArrayType(JobRow));
2648
+ const noShifts = $.const([], ArrayType(ShiftRow));
2649
+ const noMarks = $.const([], ArrayType(MarkRow));
2650
+ const noPoints = $.const([], ArrayType(PointRow));
2651
+ const noTonnes = $.const([], ArrayType(RawCell));
2652
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
2653
+ const CONFIRMED = variant("confirmed", null);
2654
+ const ACTUAL = variant("actual", null);
2655
+ const RUNNING = variant("in-progress", null);
2656
+ const PROPOSED = variant("proposed", variant("recommended", null));
2657
+ // Per-day series over days 1..8 — derived, never hand-written.
2658
+ const points = $.let(East.Array.generate(8n, PointRow, (_$, i) => ({ day: i.toFloat().add(1.0), t: i.multiply(17n).remainder(40n).toFloat().add(60.0) })));
2659
+ const tonnes = $.let(East.Array.generate(8n, RawCell, (_$, i) => ({ at: i.toFloat().add(1.0), value: some(i.multiply(23n).remainder(70n).toFloat().add(80.0)) })));
2660
+ // Heat cells as STORED records — the instant spelled explicitly.
2661
+ const cells = $.let(East.Array.generate(8n, Plan.Types.HeatCell, ($, i) => {
2662
+ const load = $.let(i.multiply(29n).remainder(55n).toFloat().add(40.0), FloatType);
2663
+ return { at: Plan.at.number(i.toFloat().add(1.0)), value: some(load), label: some(East.Float.printFixed(load, 0n)) };
2664
+ }));
2665
+ const base = {
2666
+ value: none, allocations: noAllocs, jobs: noJobs, shifts: noShifts,
2667
+ marks: noMarks, points: noPoints, tonnes: noTonnes, cells: noCells,
2668
+ };
2669
+ // Keys order the canvas (#568) — numbered so each series lands where the
2670
+ // §1 layout puts it.
2671
+ const ops = $.const(new Map([
2672
+ ["10-dock2", { ...base, series: "dock", label: "Dock 2", value: some("load/day"), allocations: [
2673
+ { key: "a1", day: 1.0, lane: "am", state: CONFIRMED },
2674
+ { key: "a2", day: 1.0, lane: "pm", state: CONFIRMED },
2675
+ { key: "a3", day: 2.0, lane: "am", state: CONFIRMED },
2676
+ { key: "a4", day: 3.0, lane: "pm", state: PROPOSED },
2677
+ { key: "a5", day: 5.0, lane: "am", state: PROPOSED },
2678
+ { key: "a6", day: 6.0, lane: "pm", state: CONFIRMED },
2679
+ { key: "a7", day: 8.0, lane: "am", state: PROPOSED },
2680
+ ] }],
2681
+ ["10-dock5", { ...base, series: "dock", label: "Dock 5", value: some("load/day"), allocations: [
2682
+ { key: "b1", day: 2.0, lane: "pm", state: CONFIRMED },
2683
+ { key: "b2", day: 4.0, lane: "am", state: CONFIRMED },
2684
+ { key: "b3", day: 4.0, lane: "pm", state: PROPOSED },
2685
+ { key: "b4", day: 7.0, lane: "am", state: PROPOSED },
2686
+ ] }],
2687
+ ["20-m03", { ...base, series: "mach", label: "L1-M03", value: some("120 t"), jobs: [
2688
+ { key: "set", label: "SET", start: 1.0, end: 2.0, state: ACTUAL },
2689
+ { key: "b214", label: "RUN · B-214", start: 2.0, end: 5.0, state: RUNNING },
2690
+ { key: "b221", label: "RUN · B-221", start: 6.0, end: 8.0, state: PROPOSED },
2691
+ ] }],
2692
+ ["20-m04", { ...base, series: "mach", label: "L1-M04", value: some("80 t"), jobs: [
2693
+ { key: "b208", label: "RUN · B-208", start: 1.0, end: 4.0, state: ACTUAL },
2694
+ { key: "qc", label: "QC", start: 4.0, end: 5.0, state: CONFIRMED },
2695
+ { key: "b231", label: "RUN · B-231", start: 5.0, end: 9.0, state: PROPOSED },
2696
+ ] }],
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: [
2701
+ { key: "s1", from: 1.0, to: 3.0, hours: 24.0, state: CONFIRMED },
2702
+ { key: "s2", from: 3.0, to: 6.0, hours: 36.0, state: CONFIRMED },
2703
+ { key: "s3", from: 6.0, to: 8.0, hours: 24.0, state: PROPOSED },
2704
+ ] }],
2705
+ ["70-ms", { ...base, series: "ms", label: "MILESTONES", marks: [
2706
+ { key: "kick", day: 2.0, label: "KICKOFF" },
2707
+ { key: "rel", day: 6.0, label: "REL" },
2708
+ ] }],
2709
+ ]), DictType(StringType, OpsRow));
2710
+ const series = $.const([
2711
+ Plan.series.group(OpsRow, { key: "10-docks", label: "Docks · In", meta: "2 rs" }, [
2712
+ Plan.series.buckets(OpsRow, {
2713
+ key: "dock", title: "Docks",
2714
+ match: r => r.series.equal("dock"),
2715
+ label: r => r.label, value: r => r.value,
2716
+ // The Planner's AM/PM lanes — sub-slots of each day column.
2717
+ lanes: _r => [Plan.lane({ key: "am", label: "AM" }), Plan.lane({ key: "pm", label: "PM" })],
2718
+ // `a.day` is a FloatType field — the builder wraps it to the number arm.
2719
+ events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.day, lane: a.lane, state: a.state })),
2720
+ }),
2721
+ ]),
2722
+ Plan.series.span(OpsRow, {
2723
+ key: "mach", title: "Machines",
2724
+ match: r => r.series.equal("mach"),
2725
+ label: r => r.label, id: true, value: r => r.value,
2726
+ runs: r => r.jobs.map((_$, j) => Plan.run({ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state })),
2727
+ }),
2728
+ Plan.series.heat(OpsRow, {
2729
+ key: "load", title: "Line load",
2730
+ match: r => r.series.equal("load"),
2731
+ label: r => r.label,
2732
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 90 }),
2733
+ }),
2734
+ Plan.series.chart(OpsRow, {
2735
+ key: "out", title: "Output",
2736
+ match: r => r.series.equal("out"),
2737
+ label: r => r.label, id: true, value: r => r.value, height: "expanded",
2738
+ // A numeric x accessor lands the columns on the number arm.
2739
+ layers: r => [Chart.Column(r.points, { x: p => p.day, y: p => p.t })],
2740
+ }),
2741
+ Plan.series.table(OpsRow, {
2742
+ key: "table", title: "Despatch",
2743
+ match: r => r.series.equal("table"),
2744
+ label: r => r.label,
2745
+ // A `{ at: FloatType, value }` record wraps by its field type.
2746
+ cells: r => Plan.tableCells(r.tonnes),
2747
+ format: Format.Number({ maximumFractionDigits: 0n }),
2748
+ }),
2749
+ Plan.series.cards(OpsRow, {
2750
+ key: "crew", title: "Crews",
2751
+ match: r => r.series.equal("crew"),
2752
+ label: r => r.label,
2753
+ chips: r => r.shifts.map((_$, s) => Plan.chip({
2754
+ key: s.key, from: s.from, to: s.to,
2755
+ label: East.str `${East.Float.printFixed(s.hours, 0n)}h`, state: s.state,
2756
+ })),
2757
+ }),
2758
+ Plan.series.events(OpsRow, {
2759
+ key: "ms", title: "Milestones",
2760
+ match: r => r.series.equal("ms"),
2761
+ label: r => r.label, id: true,
2762
+ marks: r => r.marks.map((_$, m) => Plan.mark({ key: m.key, at: m.day, kind: "milestone", label: m.label })),
2763
+ }),
2764
+ ], ArrayType(Plan.Types.Series(OpsRow)));
2765
+ // 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.
2768
+ const horizon = $.let(East.Array.generate(24n, HorizonRow, (_$, i) => ({
2769
+ key: East.str `o${East.print(i.add(1n))}`,
2770
+ day: i.divide(2n).toFloat().add(1.0),
2771
+ line: i.remainder(2n).equal(0n).ifElse(() => "Line 1", () => "Line 2"),
2772
+ })));
2773
+ const slice = $.let(Slice.bind([HorizonRow], "ex.plan.number", cfg, Slice.state({
2774
+ range: some(variant("float", { from: 1.0, to: 9.0 })),
2775
+ }), horizon, none));
2776
+ // The declaration: `[1, 9)` ÷ 1 = eight day columns, the divider at 5.
2777
+ const axis = $.const(Plan.axis.number({
2778
+ window: { min: 1, max: 9 }, step: 1, now: 5, format: Chart.format.number(),
2779
+ }));
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
+ ] }));
2784
+ } }));
2785
+ }),
2786
+ inputs: [],
2787
+ });
2788
+ // ============================================================================
2789
+ // planOrdinalAxis — the `ordinal` axis (#631): workflow phases
2790
+ // ============================================================================
2791
+ /**
2792
+ * The `ordinal` axis — a workflow of six phases, each a bucket, in declared
2793
+ * order. Instants are the phase VALUES: a run's `start` / `end` are phase
2794
+ * names (an interval covers `[start, end]` in phase order — on an ordinal
2795
+ * axis the end names the LAST bucket covered, since values are buckets, not
2796
+ * edges), a tile sits in a phase, a chart's string x accessor lands its
2797
+ * columns on the phase arm, `Plan.tableCells` reads a string `at`, and
2798
+ * `Plan.at.ordinal` spells one out where a record is written as data. There
2799
+ * is no slice range for an ordinal axis — the list IS the window, so the
2800
+ * horizon brush does not mount and the window keys idle; `now` names a phase.
2801
+ */
2802
+ export const planOrdinalAxis = example({
2803
+ keywords: [
2804
+ "Plan", "axis", "ordinal", "phase", "phases", "workflow", "stage", "Plan.axis.ordinal",
2805
+ "Plan.at", "instant", "values", "list", "span", "buckets", "heat", "chart", "table",
2806
+ "cards", "events", "Planner", "typed axis", "#631", "raw",
2807
+ ],
2808
+ description: "The ordinal axis — six workflow phases as the buckets, in declared order; orders span phases, tiles and cells sit in them, a string x accessor lands a chart on them",
2809
+ fn: East.function([], UIComponentType, ($) => {
2810
+ const PHASES = $.const(["INTAKE", "PREP", "BUILD", "QC", "PACK", "SHIP"], ArrayType(StringType));
2811
+ // RAW rows — every instant is a phase NAME.
2812
+ const JobRow = StructType({ key: StringType, label: StringType, start: StringType, end: StringType, state: EventStateType });
2813
+ const AllocRow = StructType({ key: StringType, phase: StringType, state: EventStateType });
2814
+ const ShiftRow = StructType({ key: StringType, from: StringType, to: StringType, label: StringType, state: EventStateType });
2815
+ const MarkRow = StructType({ key: StringType, phase: StringType, label: StringType, exception: BooleanType });
2816
+ const PointRow = StructType({ phase: StringType, n: FloatType });
2817
+ const RawCell = StructType({ at: StringType, value: OptionType(FloatType) });
2818
+ const OrderRow = StructType({
2819
+ series: StringType, label: StringType, value: OptionType(StringType),
2820
+ jobs: ArrayType(JobRow), allocations: ArrayType(AllocRow), shifts: ArrayType(ShiftRow),
2821
+ marks: ArrayType(MarkRow), points: ArrayType(PointRow), counts: ArrayType(RawCell),
2822
+ cells: ArrayType(Plan.Types.HeatCell),
2823
+ });
2824
+ const noJobs = $.const([], ArrayType(JobRow));
2825
+ const noAllocs = $.const([], ArrayType(AllocRow));
2826
+ const noShifts = $.const([], ArrayType(ShiftRow));
2827
+ const noMarks = $.const([], ArrayType(MarkRow));
2828
+ const noPoints = $.const([], ArrayType(PointRow));
2829
+ const noCounts = $.const([], ArrayType(RawCell));
2830
+ const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
2831
+ const CONFIRMED = variant("confirmed", null);
2832
+ const ACTUAL = variant("actual", null);
2833
+ const RUNNING = variant("in-progress", null);
2834
+ const PROPOSED = variant("proposed", variant("recommended", null));
2835
+ // Per-phase series derived over the declared list — the index picks the value.
2836
+ const points = $.let(East.Array.generate(6n, PointRow, (_$, i) => ({ phase: PHASES.get(i), n: i.multiply(7n).remainder(20n).toFloat().add(4.0) })));
2837
+ const counts = $.let(East.Array.generate(6n, RawCell, (_$, i) => ({
2838
+ at: PHASES.get(i),
2839
+ value: i.equal(4n).ifElse(() => none, () => some(i.multiply(11n).remainder(30n).toFloat().add(12.0))),
2840
+ })));
2841
+ // Heat cells as STORED records — the phase spelled with `Plan.at.ordinal`.
2842
+ const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, ($, i) => {
2843
+ const load = $.let(i.multiply(31n).remainder(60n).toFloat().add(35.0), FloatType);
2844
+ return { at: Plan.at.ordinal(PHASES.get(i)), value: some(load), label: some(East.Float.printFixed(load, 0n)) };
2845
+ }));
2846
+ const base = {
2847
+ value: none, jobs: noJobs, allocations: noAllocs, shifts: noShifts,
2848
+ marks: noMarks, points: noPoints, counts: noCounts, cells: noCells,
2849
+ };
2850
+ const orders = $.const(new Map([
2851
+ ["10-or-1188", { ...base, series: "order", label: "OR-1188", value: some("96 t"), jobs: [
2852
+ { key: "prep", label: "PREP", start: "INTAKE", end: "PREP", state: ACTUAL },
2853
+ { key: "build", label: "BUILD · B-214", start: "BUILD", end: "QC", state: RUNNING },
2854
+ { key: "ship", label: "PACK + SHIP", start: "PACK", end: "SHIP", state: PROPOSED },
2855
+ ] }],
2856
+ ["10-or-1204", { ...base, series: "order", label: "OR-1204", value: some("54 t"), jobs: [
2857
+ { key: "intake", label: "INTAKE", start: "INTAKE", end: "INTAKE", state: ACTUAL },
2858
+ { key: "build", label: "BUILD · B-221", start: "PREP", end: "PACK", state: PROPOSED },
2859
+ ] }],
2860
+ ["20-bench", { ...base, series: "bench", label: "Bench 2", value: some("slots"), allocations: [
2861
+ { key: "a1", phase: "PREP", state: CONFIRMED }, { key: "a2", phase: "BUILD", state: CONFIRMED },
2862
+ { key: "a3", phase: "BUILD", state: PROPOSED }, { key: "a4", phase: "PACK", state: PROPOSED },
2863
+ ] }],
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: [
2868
+ { key: "s1", from: "INTAKE", to: "PREP", label: "prep crew", state: CONFIRMED },
2869
+ { key: "s2", from: "BUILD", to: "SHIP", label: "+ finish crew", state: PROPOSED },
2870
+ ] }],
2871
+ ["70-gates", { ...base, series: "gates", label: "GATES", marks: [
2872
+ { key: "g1", phase: "QC", label: "HOLD", exception: true },
2873
+ { key: "g2", phase: "SHIP", label: "RELEASE", exception: false },
2874
+ ] }],
2875
+ ]), DictType(StringType, OrderRow));
2876
+ const EXCEPTION = $.const(variant("exception", null), Plan.Types.EventMarkKind);
2877
+ const MILESTONE = $.const(variant("milestone", null), Plan.Types.EventMarkKind);
2878
+ const series = $.const([
2879
+ Plan.series.group(OrderRow, { key: "10-orders", label: "Orders", meta: "2 rs" }, [
2880
+ Plan.series.span(OrderRow, {
2881
+ key: "order", title: "Orders",
2882
+ match: r => r.series.equal("order"),
2883
+ label: r => r.label, id: true, value: r => r.value,
2884
+ // `j.start` / `j.end` are StringType fields — the builder wraps them to the ordinal arm.
2885
+ runs: r => r.jobs.map((_$, j) => Plan.run({ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state })),
2886
+ }),
2887
+ ]),
2888
+ Plan.series.buckets(OrderRow, {
2889
+ key: "bench", title: "Benches",
2890
+ match: r => r.series.equal("bench"),
2891
+ label: r => r.label, value: r => r.value,
2892
+ events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.phase, state: a.state })),
2893
+ }),
2894
+ Plan.series.heat(OrderRow, {
2895
+ key: "load", title: "Phase load",
2896
+ match: r => r.series.equal("load"),
2897
+ label: r => r.label,
2898
+ cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 90 }),
2899
+ }),
2900
+ Plan.series.chart(OrderRow, {
2901
+ key: "wip", title: "WIP",
2902
+ match: r => r.series.equal("wip"),
2903
+ label: r => r.label, id: true, value: r => r.value, height: "expanded",
2904
+ // A string x accessor lands the columns on the ordinal arm.
2905
+ layers: r => [Chart.Column(r.points, { x: p => p.phase, y: p => p.n })],
2906
+ }),
2907
+ Plan.series.table(OrderRow, {
2908
+ key: "count", title: "Counts",
2909
+ match: r => r.series.equal("count"),
2910
+ label: r => r.label,
2911
+ cells: r => Plan.tableCells(r.counts),
2912
+ format: Format.Number({ maximumFractionDigits: 0n }),
2913
+ }),
2914
+ Plan.series.cards(OrderRow, {
2915
+ key: "crew", title: "Crews",
2916
+ match: r => r.series.equal("crew"),
2917
+ label: r => r.label,
2918
+ chips: r => r.shifts.map((_$, s) => Plan.chip({ key: s.key, from: s.from, to: s.to, label: s.label, state: s.state })),
2919
+ }),
2920
+ Plan.series.events(OrderRow, {
2921
+ key: "gates", title: "Gates",
2922
+ match: r => r.series.equal("gates"),
2923
+ label: r => r.label, id: true,
2924
+ marks: r => r.marks.map((_$, m) => Plan.mark({
2925
+ key: m.key, at: m.phase, label: m.label,
2926
+ kind: m.exception.ifElse(() => EXCEPTION, () => MILESTONE),
2927
+ })),
2928
+ }),
2929
+ ], ArrayType(Plan.Types.Series(OrderRow)));
2930
+ // The declaration: the list IS the axis — one bucket per phase, `now` at BUILD.
2931
+ 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
+ ] }));
2936
+ }),
2937
+ inputs: [],
2938
+ });
2939
+ //# sourceMappingURL=plan.examples.js.map