@nowline/layout 0.8.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/dist/axis-columns.d.ts +70 -0
  2. package/dist/axis-columns.d.ts.map +1 -0
  3. package/dist/axis-columns.js +104 -0
  4. package/dist/axis-columns.js.map +1 -0
  5. package/dist/band-scale.d.ts.map +1 -1
  6. package/dist/calendar-resolver.d.ts +27 -0
  7. package/dist/calendar-resolver.d.ts.map +1 -0
  8. package/dist/calendar-resolver.js +132 -0
  9. package/dist/calendar-resolver.js.map +1 -0
  10. package/dist/calendar.d.ts +0 -19
  11. package/dist/calendar.d.ts.map +1 -1
  12. package/dist/calendar.js +4 -44
  13. package/dist/calendar.js.map +1 -1
  14. package/dist/canvas-width.d.ts +23 -0
  15. package/dist/canvas-width.d.ts.map +1 -0
  16. package/dist/canvas-width.js +26 -0
  17. package/dist/canvas-width.js.map +1 -0
  18. package/dist/capacity.d.ts +28 -0
  19. package/dist/capacity.d.ts.map +1 -1
  20. package/dist/capacity.js +64 -29
  21. package/dist/capacity.js.map +1 -1
  22. package/dist/chart-translation.d.ts +22 -0
  23. package/dist/chart-translation.d.ts.map +1 -0
  24. package/dist/chart-translation.js +83 -0
  25. package/dist/chart-translation.js.map +1 -0
  26. package/dist/container-header-geometry.d.ts +64 -0
  27. package/dist/container-header-geometry.d.ts.map +1 -0
  28. package/dist/container-header-geometry.js +89 -0
  29. package/dist/container-header-geometry.js.map +1 -0
  30. package/dist/dsl-utils.d.ts +3 -4
  31. package/dist/dsl-utils.d.ts.map +1 -1
  32. package/dist/dsl-utils.js +8 -25
  33. package/dist/dsl-utils.js.map +1 -1
  34. package/dist/edge-routing.d.ts +69 -78
  35. package/dist/edge-routing.d.ts.map +1 -1
  36. package/dist/edge-routing.js +271 -401
  37. package/dist/edge-routing.js.map +1 -1
  38. package/dist/frame-tab-geometry.d.ts +24 -14
  39. package/dist/frame-tab-geometry.d.ts.map +1 -1
  40. package/dist/frame-tab-geometry.js +69 -23
  41. package/dist/frame-tab-geometry.js.map +1 -1
  42. package/dist/header-card-geometry.d.ts +12 -0
  43. package/dist/header-card-geometry.d.ts.map +1 -1
  44. package/dist/header-card-geometry.js +14 -2
  45. package/dist/header-card-geometry.js.map +1 -1
  46. package/dist/header-logo.d.ts +28 -0
  47. package/dist/header-logo.d.ts.map +1 -0
  48. package/dist/header-logo.js +51 -0
  49. package/dist/header-logo.js.map +1 -0
  50. package/dist/i18n.d.ts +11 -0
  51. package/dist/i18n.d.ts.map +1 -1
  52. package/dist/i18n.js +11 -1
  53. package/dist/i18n.js.map +1 -1
  54. package/dist/index.d.ts +20 -8
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +17 -7
  57. package/dist/index.js.map +1 -1
  58. package/dist/inline-date-pin-geometry.d.ts +19 -11
  59. package/dist/inline-date-pin-geometry.d.ts.map +1 -1
  60. package/dist/inline-date-pin-geometry.js +45 -46
  61. package/dist/inline-date-pin-geometry.js.map +1 -1
  62. package/dist/item-bar-geometry.d.ts +192 -4
  63. package/dist/item-bar-geometry.d.ts.map +1 -1
  64. package/dist/item-bar-geometry.js +258 -5
  65. package/dist/item-bar-geometry.js.map +1 -1
  66. package/dist/item-port-geometry.d.ts +13 -0
  67. package/dist/item-port-geometry.d.ts.map +1 -0
  68. package/dist/item-port-geometry.js +63 -0
  69. package/dist/item-port-geometry.js.map +1 -0
  70. package/dist/layout-context.d.ts +115 -51
  71. package/dist/layout-context.d.ts.map +1 -1
  72. package/dist/layout-context.js +96 -5
  73. package/dist/layout-context.js.map +1 -1
  74. package/dist/layout-insights.d.ts.map +1 -1
  75. package/dist/layout-insights.js +73 -1
  76. package/dist/layout-insights.js.map +1 -1
  77. package/dist/layout.d.ts +35 -0
  78. package/dist/layout.d.ts.map +1 -1
  79. package/dist/layout.js +18 -1315
  80. package/dist/layout.js.map +1 -1
  81. package/dist/nodes/anchor-node.d.ts.map +1 -1
  82. package/dist/nodes/anchor-node.js +11 -4
  83. package/dist/nodes/anchor-node.js.map +1 -1
  84. package/dist/nodes/content-extent.d.ts +24 -0
  85. package/dist/nodes/content-extent.d.ts.map +1 -0
  86. package/dist/nodes/content-extent.js +100 -0
  87. package/dist/nodes/content-extent.js.map +1 -0
  88. package/dist/nodes/dependency-edges.d.ts +6 -0
  89. package/dist/nodes/dependency-edges.d.ts.map +1 -0
  90. package/dist/nodes/dependency-edges.js +148 -0
  91. package/dist/nodes/dependency-edges.js.map +1 -0
  92. package/dist/nodes/footnote-node.d.ts +24 -3
  93. package/dist/nodes/footnote-node.d.ts.map +1 -1
  94. package/dist/nodes/footnote-node.js +129 -25
  95. package/dist/nodes/footnote-node.js.map +1 -1
  96. package/dist/nodes/group-node.d.ts +10 -21
  97. package/dist/nodes/group-node.d.ts.map +1 -1
  98. package/dist/nodes/group-node.js +79 -154
  99. package/dist/nodes/group-node.js.map +1 -1
  100. package/dist/nodes/header-node.d.ts +56 -0
  101. package/dist/nodes/header-node.d.ts.map +1 -0
  102. package/dist/nodes/header-node.js +163 -0
  103. package/dist/nodes/header-node.js.map +1 -0
  104. package/dist/nodes/include-node.d.ts +22 -12
  105. package/dist/nodes/include-node.d.ts.map +1 -1
  106. package/dist/nodes/include-node.js +95 -62
  107. package/dist/nodes/include-node.js.map +1 -1
  108. package/dist/nodes/item-measure.d.ts +135 -0
  109. package/dist/nodes/item-measure.d.ts.map +1 -0
  110. package/dist/nodes/item-measure.js +514 -0
  111. package/dist/nodes/item-measure.js.map +1 -0
  112. package/dist/nodes/item-node.d.ts +162 -9
  113. package/dist/nodes/item-node.d.ts.map +1 -1
  114. package/dist/nodes/item-node.js +256 -69
  115. package/dist/nodes/item-node.js.map +1 -1
  116. package/dist/nodes/marker-band.d.ts +118 -0
  117. package/dist/nodes/marker-band.d.ts.map +1 -0
  118. package/dist/nodes/marker-band.js +303 -0
  119. package/dist/nodes/marker-band.js.map +1 -0
  120. package/dist/nodes/marker-geometry.js +1 -1
  121. package/dist/nodes/marker-geometry.js.map +1 -1
  122. package/dist/nodes/milestone-node.d.ts +112 -15
  123. package/dist/nodes/milestone-node.d.ts.map +1 -1
  124. package/dist/nodes/milestone-node.js +246 -95
  125. package/dist/nodes/milestone-node.js.map +1 -1
  126. package/dist/nodes/nowline-node.d.ts +8 -0
  127. package/dist/nodes/nowline-node.d.ts.map +1 -0
  128. package/dist/nodes/nowline-node.js +74 -0
  129. package/dist/nodes/nowline-node.js.map +1 -0
  130. package/dist/nodes/parallel-node.d.ts +7 -12
  131. package/dist/nodes/parallel-node.d.ts.map +1 -1
  132. package/dist/nodes/parallel-node.js +66 -32
  133. package/dist/nodes/parallel-node.js.map +1 -1
  134. package/dist/nodes/region-markers.d.ts +41 -0
  135. package/dist/nodes/region-markers.d.ts.map +1 -0
  136. package/dist/nodes/region-markers.js +130 -0
  137. package/dist/nodes/region-markers.js.map +1 -0
  138. package/dist/nodes/roadmap-node.d.ts +9 -72
  139. package/dist/nodes/roadmap-node.d.ts.map +1 -1
  140. package/dist/nodes/roadmap-node.js +38 -788
  141. package/dist/nodes/roadmap-node.js.map +1 -1
  142. package/dist/nodes/row-track.d.ts +33 -0
  143. package/dist/nodes/row-track.d.ts.map +1 -0
  144. package/dist/nodes/row-track.js +170 -0
  145. package/dist/nodes/row-track.js.map +1 -0
  146. package/dist/nodes/sequence-item.d.ts +25 -0
  147. package/dist/nodes/sequence-item.d.ts.map +1 -0
  148. package/dist/nodes/sequence-item.js +396 -0
  149. package/dist/nodes/sequence-item.js.map +1 -0
  150. package/dist/nodes/sequence-one.d.ts +5 -0
  151. package/dist/nodes/sequence-one.d.ts.map +1 -0
  152. package/dist/nodes/sequence-one.js +26 -0
  153. package/dist/nodes/sequence-one.js.map +1 -0
  154. package/dist/nodes/swimlane-node.d.ts +5 -17
  155. package/dist/nodes/swimlane-node.d.ts.map +1 -1
  156. package/dist/nodes/swimlane-node.js +62 -147
  157. package/dist/nodes/swimlane-node.js.map +1 -1
  158. package/dist/nodes/wave-node.d.ts +52 -0
  159. package/dist/nodes/wave-node.d.ts.map +1 -0
  160. package/dist/nodes/wave-node.js +483 -0
  161. package/dist/nodes/wave-node.js.map +1 -0
  162. package/dist/non-working-display.d.ts +14 -0
  163. package/dist/non-working-display.d.ts.map +1 -0
  164. package/dist/non-working-display.js +28 -0
  165. package/dist/non-working-display.js.map +1 -0
  166. package/dist/phases/date-window.d.ts +48 -0
  167. package/dist/phases/date-window.d.ts.map +1 -0
  168. package/dist/phases/date-window.js +130 -0
  169. package/dist/phases/date-window.js.map +1 -0
  170. package/dist/phases/finalize.d.ts +198 -0
  171. package/dist/phases/finalize.d.ts.map +1 -0
  172. package/dist/phases/finalize.js +1058 -0
  173. package/dist/phases/finalize.js.map +1 -0
  174. package/dist/phases/measure.d.ts +77 -0
  175. package/dist/phases/measure.d.ts.map +1 -0
  176. package/dist/phases/measure.js +222 -0
  177. package/dist/phases/measure.js.map +1 -0
  178. package/dist/phases/place.d.ts +28 -0
  179. package/dist/phases/place.d.ts.map +1 -0
  180. package/dist/phases/place.js +225 -0
  181. package/dist/phases/place.js.map +1 -0
  182. package/dist/renderable.d.ts.map +1 -1
  183. package/dist/renderable.js +23 -3
  184. package/dist/renderable.js.map +1 -1
  185. package/dist/resolve-today.d.ts +11 -1
  186. package/dist/resolve-today.d.ts.map +1 -1
  187. package/dist/resolve-today.js +26 -12
  188. package/dist/resolve-today.js.map +1 -1
  189. package/dist/row-packer.d.ts +42 -16
  190. package/dist/row-packer.d.ts.map +1 -1
  191. package/dist/row-packer.js +51 -78
  192. package/dist/row-packer.js.map +1 -1
  193. package/dist/schedule.d.ts +58 -1
  194. package/dist/schedule.d.ts.map +1 -1
  195. package/dist/schedule.js +280 -106
  196. package/dist/schedule.js.map +1 -1
  197. package/dist/start-rules.d.ts +27 -0
  198. package/dist/start-rules.d.ts.map +1 -0
  199. package/dist/start-rules.js +91 -0
  200. package/dist/start-rules.js.map +1 -0
  201. package/dist/start-terms.d.ts +23 -0
  202. package/dist/start-terms.d.ts.map +1 -0
  203. package/dist/start-terms.js +27 -0
  204. package/dist/start-terms.js.map +1 -0
  205. package/dist/status-dot.d.ts +46 -0
  206. package/dist/status-dot.d.ts.map +1 -0
  207. package/dist/status-dot.js +78 -0
  208. package/dist/status-dot.js.map +1 -0
  209. package/dist/style-resolution.d.ts.map +1 -1
  210. package/dist/style-resolution.js +23 -43
  211. package/dist/style-resolution.js.map +1 -1
  212. package/dist/text-measure.d.ts +28 -0
  213. package/dist/text-measure.d.ts.map +1 -0
  214. package/dist/text-measure.js +70 -0
  215. package/dist/text-measure.js.map +1 -0
  216. package/dist/themes/contrast.d.ts +4 -0
  217. package/dist/themes/contrast.d.ts.map +1 -0
  218. package/dist/themes/contrast.js +32 -0
  219. package/dist/themes/contrast.js.map +1 -0
  220. package/dist/themes/dark.d.ts.map +1 -1
  221. package/dist/themes/dark.js +28 -1
  222. package/dist/themes/dark.js.map +1 -1
  223. package/dist/themes/grayscale.d.ts.map +1 -1
  224. package/dist/themes/grayscale.js +28 -1
  225. package/dist/themes/grayscale.js.map +1 -1
  226. package/dist/themes/index.d.ts +4 -11
  227. package/dist/themes/index.d.ts.map +1 -1
  228. package/dist/themes/index.js +6 -4
  229. package/dist/themes/index.js.map +1 -1
  230. package/dist/themes/light.d.ts.map +1 -1
  231. package/dist/themes/light.js +28 -1
  232. package/dist/themes/light.js.map +1 -1
  233. package/dist/themes/shape.d.ts +31 -0
  234. package/dist/themes/shape.d.ts.map +1 -1
  235. package/dist/themes/shared.d.ts +44 -50
  236. package/dist/themes/shared.d.ts.map +1 -1
  237. package/dist/themes/shared.js +79 -83
  238. package/dist/themes/shared.js.map +1 -1
  239. package/dist/time-scale.d.ts +42 -6
  240. package/dist/time-scale.d.ts.map +1 -1
  241. package/dist/time-scale.js +109 -17
  242. package/dist/time-scale.js.map +1 -1
  243. package/dist/types.d.ts +528 -11
  244. package/dist/types.d.ts.map +1 -1
  245. package/dist/view-preset.d.ts +61 -6
  246. package/dist/view-preset.d.ts.map +1 -1
  247. package/dist/view-preset.js +439 -24
  248. package/dist/view-preset.js.map +1 -1
  249. package/dist/wave-barrier.d.ts +107 -0
  250. package/dist/wave-barrier.d.ts.map +1 -0
  251. package/dist/wave-barrier.js +174 -0
  252. package/dist/wave-barrier.js.map +1 -0
  253. package/dist/wave-hatch.d.ts +4 -0
  254. package/dist/wave-hatch.d.ts.map +1 -0
  255. package/dist/wave-hatch.js +21 -0
  256. package/dist/wave-hatch.js.map +1 -0
  257. package/dist/wave-layout.d.ts +99 -0
  258. package/dist/wave-layout.d.ts.map +1 -0
  259. package/dist/wave-layout.js +130 -0
  260. package/dist/wave-layout.js.map +1 -0
  261. package/dist/working-calendar.d.ts +128 -4
  262. package/dist/working-calendar.d.ts.map +1 -1
  263. package/dist/working-calendar.js +725 -51
  264. package/dist/working-calendar.js.map +1 -1
  265. package/dist/working-span.d.ts +11 -0
  266. package/dist/working-span.d.ts.map +1 -0
  267. package/dist/working-span.js +18 -0
  268. package/dist/working-span.js.map +1 -0
  269. package/package.json +7 -7
  270. package/src/axis-columns.ts +140 -0
  271. package/src/calendar-resolver.ts +161 -0
  272. package/src/calendar.ts +4 -45
  273. package/src/canvas-width.ts +36 -0
  274. package/src/capacity.ts +71 -30
  275. package/src/chart-translation.ts +88 -0
  276. package/src/container-header-geometry.ts +116 -0
  277. package/src/dsl-utils.ts +8 -26
  278. package/src/edge-routing.ts +347 -491
  279. package/src/frame-tab-geometry.ts +78 -25
  280. package/src/header-card-geometry.ts +17 -2
  281. package/src/header-logo.ts +67 -0
  282. package/src/i18n.ts +22 -1
  283. package/src/index.ts +97 -12
  284. package/src/inline-date-pin-geometry.ts +55 -58
  285. package/src/item-bar-geometry.ts +341 -5
  286. package/src/item-port-geometry.ts +69 -0
  287. package/src/layout-context.ts +210 -72
  288. package/src/layout-insights.ts +109 -2
  289. package/src/layout.ts +57 -1530
  290. package/src/nodes/anchor-node.ts +11 -4
  291. package/src/nodes/content-extent.ts +107 -0
  292. package/src/nodes/dependency-edges.ts +171 -0
  293. package/src/nodes/footnote-node.ts +171 -31
  294. package/src/nodes/group-node.ts +92 -215
  295. package/src/nodes/header-node.ts +255 -0
  296. package/src/nodes/include-node.ts +124 -110
  297. package/src/nodes/item-measure.ts +723 -0
  298. package/src/nodes/item-node.ts +358 -80
  299. package/src/nodes/marker-band.ts +406 -0
  300. package/src/nodes/marker-geometry.ts +1 -1
  301. package/src/nodes/milestone-node.ts +321 -136
  302. package/src/nodes/nowline-node.ts +87 -0
  303. package/src/nodes/parallel-node.ts +69 -39
  304. package/src/nodes/region-markers.ts +194 -0
  305. package/src/nodes/roadmap-node.ts +46 -950
  306. package/src/nodes/row-track.ts +217 -0
  307. package/src/nodes/sequence-item.ts +433 -0
  308. package/src/nodes/sequence-one.ts +40 -0
  309. package/src/nodes/swimlane-node.ts +66 -206
  310. package/src/nodes/wave-node.ts +609 -0
  311. package/src/non-working-display.ts +32 -0
  312. package/src/phases/date-window.ts +191 -0
  313. package/src/phases/finalize.ts +1303 -0
  314. package/src/phases/measure.ts +329 -0
  315. package/src/phases/place.ts +318 -0
  316. package/src/renderable.ts +23 -3
  317. package/src/resolve-today.ts +28 -25
  318. package/src/row-packer.ts +85 -84
  319. package/src/schedule.ts +362 -114
  320. package/src/start-rules.ts +113 -0
  321. package/src/start-terms.ts +38 -0
  322. package/src/status-dot.ts +81 -0
  323. package/src/style-resolution.ts +27 -57
  324. package/src/text-measure.ts +71 -0
  325. package/src/themes/contrast.ts +34 -0
  326. package/src/themes/dark.ts +28 -1
  327. package/src/themes/grayscale.ts +28 -1
  328. package/src/themes/index.ts +10 -7
  329. package/src/themes/light.ts +28 -1
  330. package/src/themes/shape.ts +38 -0
  331. package/src/themes/shared.ts +85 -93
  332. package/src/time-scale.ts +117 -19
  333. package/src/types.ts +529 -20
  334. package/src/view-preset.ts +520 -27
  335. package/src/wave-barrier.ts +242 -0
  336. package/src/wave-hatch.ts +25 -0
  337. package/src/wave-layout.ts +203 -0
  338. package/src/working-calendar.ts +875 -57
  339. package/src/working-span.ts +19 -0
@@ -20,6 +20,8 @@
20
20
  // --timezone is ignored. --timezone only governs the clock-based default
21
21
  // (when --now is omitted).
22
22
 
23
+ import { ISO_DATE_RE, isRealIsoDate, parseIsoDate } from '@nowline/core/internal';
24
+
23
25
  // ---- Types and errors -------------------------------------------------------
24
26
 
25
27
  /**
@@ -145,17 +147,25 @@ export function civilDateInZone(instant: Date, zone: NormalizedZone): Date {
145
147
 
146
148
  // ---- Now-value parsing ------------------------------------------------------
147
149
 
148
- // Bare YYYY-MM-DD (floating date, zone-independent)
149
- const BARE_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
150
-
151
150
  // ISO 8601 instant with explicit Z or ±HH / ±HH:MM / ±HHMM offset.
152
151
  // Captures: [1] datetime part, [2] sign or Z, [3] hours, [4] minutes (optional)
153
152
  const ISO_WITH_OFFSET_RE =
154
153
  /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|([+-])(\d{2})(?::?(\d{2}))?)$/;
155
154
 
156
155
  // ISO 8601 with time but no offset (floating local date-time).
157
- // Captures: [1] year, [2] month, [3] day
158
- const ISO_FLOATING_RE = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
156
+ const ISO_FLOATING_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
157
+
158
+ /**
159
+ * True when `raw` has one of the `now` string shapes but its leading
160
+ * `YYYY-MM-DD` is not a real calendar day (`2026-02-30`). {@link resolveToday}
161
+ * returns undefined for such a value, as for any unrecognised string; this
162
+ * lets an option parser say why.
163
+ */
164
+ export function hasImpossibleNowDate(raw: string): boolean {
165
+ const isNowShape =
166
+ ISO_DATE_RE.test(raw) || ISO_WITH_OFFSET_RE.test(raw) || ISO_FLOATING_RE.test(raw);
167
+ return isNowShape && !isRealIsoDate(raw.slice(0, 10));
168
+ }
159
169
 
160
170
  // ---- Public API -------------------------------------------------------------
161
171
 
@@ -193,11 +203,14 @@ export interface ResolveTodayOptions {
193
203
  * Precedence (from highest to lowest):
194
204
  * 1. `now === '-'` / `null` → `undefined` (suppress)
195
205
  * 2. `now` is a `Date` object → returned as-is (caller must supply UTC midnight)
196
- * 3. `now` bare `YYYY-MM-DD` → floating; `Date.UTC(y, m, d)` regardless of zone
206
+ * 3. `now` bare `YYYY-MM-DD` → floating; UTC midnight of that day regardless of zone
197
207
  * 4. `now` ISO 8601 + Z/offset → embedded offset wins; zone ignored
198
208
  * 5. `now` ISO 8601 without offset→ floating; written date part; zone ignored
199
209
  * 6. `now` omitted → civil date of `clock()` in effective `zone`
200
210
  *
211
+ * A string whose `YYYY-MM-DD` is not a real calendar day (`2026-02-30`) is
212
+ * rejected like an unrecognised one, never rolled over to the next month.
213
+ *
201
214
  * The `zone` is ONLY consulted for case 6 (clock-based default). Authored
202
215
  * dates — item bars, milestones, anchors, the axis ticks — are floating and
203
216
  * are never affected by this function.
@@ -214,17 +227,14 @@ export function resolveToday(opts: ResolveTodayOptions = {}): Date | undefined {
214
227
  if (now instanceof Date) return now;
215
228
 
216
229
  if (now !== undefined) {
230
+ // Every string form leads with a YYYY-MM-DD. `Date.UTC` and `new Date`
231
+ // would both roll an impossible one over (`2026-02-30` to March 2), so
232
+ // reject it here and let the option layer report it.
233
+ if (hasImpossibleNowDate(now)) return undefined;
234
+
217
235
  // 3. Bare YYYY-MM-DD — floating; zone ignored
218
- const bareMatch = BARE_DATE_RE.exec(now);
219
- if (bareMatch) {
220
- return new Date(
221
- Date.UTC(
222
- parseInt(bareMatch[1], 10),
223
- parseInt(bareMatch[2], 10) - 1,
224
- parseInt(bareMatch[3], 10),
225
- ),
226
- );
227
- }
236
+ const bare = parseIsoDate(now);
237
+ if (bare) return bare;
228
238
 
229
239
  // 4. ISO 8601 instant with explicit Z or offset — embedded offset WINS
230
240
  const withOffset = ISO_WITH_OFFSET_RE.exec(now);
@@ -245,15 +255,8 @@ export function resolveToday(opts: ResolveTodayOptions = {}): Date | undefined {
245
255
  }
246
256
 
247
257
  // 5. ISO 8601 without offset — floating; use written date part; zone ignored
248
- const floating = ISO_FLOATING_RE.exec(now);
249
- if (floating) {
250
- return new Date(
251
- Date.UTC(
252
- parseInt(floating[1], 10),
253
- parseInt(floating[2], 10) - 1,
254
- parseInt(floating[3], 10),
255
- ),
256
- );
258
+ if (ISO_FLOATING_RE.test(now)) {
259
+ return parseIsoDate(now.slice(0, 10)) ?? undefined;
257
260
  }
258
261
 
259
262
  // Unrecognised string — caller should have validated before calling.
package/src/row-packer.ts CHANGED
@@ -1,8 +1,10 @@
1
- // RowPacker — the topmost-fit row-pack engine shared by `SwimlaneNode`
2
- // and `GroupNode`. Tracks rows of variable height (each row's height is
3
- // the max of its placed children's heights, with a per-packer minimum)
4
- // and bumps a child to a new row when it would collide with a sibling's
5
- // `rightEdge`, caption `spillX`, or a slack-arrow corridor.
1
+ // RowPacker — the row-pack engine shared by `SwimlaneNode` and
2
+ // `GroupNode`. A child never lands above the one before it
3
+ // (specs/true-width-layout.md §3.2), so it either joins the last row or
4
+ // opens a new one below it: it drops a row when it would collide with the
5
+ // last row's `rightEdge`, caption `spillX`, or a slack-arrow corridor.
6
+ // Each row's height is the max of its placed children's heights, with a
7
+ // per-packer minimum.
6
8
  //
7
9
  // Two-phase API per child:
8
10
  // 1) `placeItem(...)` / `placeBlock(...)` decides which row the child
@@ -12,12 +14,8 @@
12
14
  // back into the packer so the row's `rightEdge` / `spillX` advance
13
15
  // and the row's height grows to accommodate the new child.
14
16
  //
15
- // When a row's height grows after later rows already exist, the packer
16
- // shifts every subsequent row (and every positioned child placed in it)
17
- // downward by the delta. Items keep their row-anchored top edge; the
18
- // row simply gets taller. This keeps the contract "all children in a
19
- // row share that row's top y" intact while letting individual children
20
- // grow vertically (e.g. wrapped label-chiclet stacks in m3).
17
+ // A child only ever commits to the last row, so a row that grows has no
18
+ // rows below it to move.
21
19
 
22
20
  import type { PositionedItem, PositionedTrackChild, SlackCorridor } from './types.js';
23
21
 
@@ -31,10 +29,11 @@ export interface PackedRow {
31
29
  /** Reserved x for caption spill. Items whose `desiredStart < spillX`
32
30
  * bump to a new row even if `rightEdge` would let them in. */
33
31
  spillX: number;
34
- /** Children placed in this row. Used when the row's height grows
35
- * after later rows exist — every child in every later row shifts
36
- * down by the delta. */
37
- placedChildren: PositionedTrackChild[];
32
+ /** How far below `y` an item joining this row sits: 0 for a row of items,
33
+ * and for a row a parallel or group opened, the distance from its top to
34
+ * its first track, so the item beside it aligns with that track rather
35
+ * than with the block's header band. */
36
+ contentOffsetY: number;
38
37
  }
39
38
 
40
39
  export interface RowPackerOptions {
@@ -64,6 +63,10 @@ export interface ItemPlacementInput {
64
63
  * initial height when this is the first item in a fresh row, or
65
64
  * to grow the row when a taller item lands in an existing row. */
66
65
  predictedHeight: number;
66
+ /** Ids of the items still to come in this track. A slack corridor
67
+ * whose arrow starts at one of them never bumps this item: rows
68
+ * never move up, so that item would follow it down. */
69
+ pendingIds?: ReadonlySet<string>;
67
70
  }
68
71
 
69
72
  /** Result of a `placeItem` query. */
@@ -76,10 +79,9 @@ export interface ItemPlacement {
76
79
  export interface ItemCommitInput {
77
80
  rowIndex: number;
78
81
  placed: PositionedItem;
79
- /** Logical right edge of the placed item. Includes the trailing
80
- * `ITEM_INSET_PX` so the next chained item butts edge-to-edge in
81
- * logical space (with a 2 × ITEM_INSET_PX visible gutter between
82
- * bars). */
82
+ /** Logical right edge of the placed item: its span in time, so the
83
+ * next chained item butts edge-to-edge in logical space (each bar
84
+ * keeps its seams inside its own dates). */
83
85
  logicalEnd: number;
84
86
  /** Reserved spill x for captions that overflow the bar's right
85
87
  * edge. `null` resets the row's spill reservation to `laneLeftX`. */
@@ -111,40 +113,58 @@ export interface BlockCommitInput {
111
113
  blockHeight: number;
112
114
  /** Logical right edge of the block. */
113
115
  blockEnd: number;
116
+ /** Distance from the block's top to its first track (header band plus
117
+ * body pad); 0 when it has no header or children. An item that joins
118
+ * the block's row lands this far down. */
119
+ contentOffsetY?: number;
120
+ /** Reserved spill x for what the block paints past its right edge
121
+ * (a header title, a caption its children spill). Omitted or
122
+ * `null` resets the row's spill reservation to `laneLeftX`. */
123
+ spillReservation?: number | null;
124
+ /** Rightmost x anything inside the block paints, when it can pass
125
+ * `blockEnd` (see `trackChildPaintedRightX`): a container's box spans
126
+ * only its dates, so a caption its children spill is not in it. It widens
127
+ * `usedRightX()` (the lane's extent, and so the canvas); the
128
+ * caller reserves it on the row through `spillReservation`. */
129
+ paintedRight?: number;
114
130
  }
115
131
 
116
132
  export class RowPacker {
117
133
  public readonly rows: PackedRow[];
118
134
  private readonly opts: RowPackerOptions;
135
+ /** Rightmost `BlockCommitInput.paintedRight` committed so far. */
136
+ private paintedRightX: number;
119
137
 
120
138
  constructor(opts: RowPackerOptions) {
121
139
  this.opts = opts;
140
+ this.paintedRightX = opts.laneLeftX;
122
141
  this.rows = [
123
142
  {
124
143
  y: opts.originY,
125
144
  height: opts.minRowHeight,
126
145
  rightEdge: opts.laneLeftX,
127
146
  spillX: opts.laneLeftX,
128
- placedChildren: [],
147
+ contentOffsetY: 0,
129
148
  },
130
149
  ];
131
150
  }
132
151
 
133
152
  /**
134
- * Find the topmost row where the item's natural extent fits without
135
- * colliding with sibling content or a slack corridor; appending a
136
- * fresh row at the bottom when none of the existing rows fit.
153
+ * Join the last row (the last committed child's) when the item's natural
154
+ * extent fits there without colliding with sibling content or a slack
155
+ * corridor; otherwise append a fresh row below it.
137
156
  */
138
157
  placeItem(input: ItemPlacementInput): ItemPlacement {
139
- for (let i = 0; i < this.rows.length; i += 1) {
140
- const r = this.rows[i];
141
- if (input.desiredStart < r.rightEdge) continue;
142
- if (input.desiredStart < r.spillX) continue;
143
- if (this.rowIntersectsCorridor(r, input)) continue;
144
- return { rowIndex: i, y: r.y };
145
- }
146
- const fresh = this.appendRow(Math.max(this.opts.minRowHeight, input.predictedHeight));
147
- return { rowIndex: this.rows.length - 1, y: fresh.y };
158
+ const last = this.rows[this.rows.length - 1];
159
+ const fits =
160
+ input.desiredStart >= last.rightEdge &&
161
+ input.desiredStart >= last.spillX &&
162
+ !this.rowIntersectsCorridor(last, input) &&
163
+ !this.rowCrossesOwnCorridor(last, input);
164
+ const row = fits
165
+ ? last
166
+ : this.appendRow(Math.max(this.opts.minRowHeight, input.predictedHeight));
167
+ return { rowIndex: this.rows.length - 1, y: row.y + row.contentOffsetY };
148
168
  }
149
169
 
150
170
  /**
@@ -164,16 +184,16 @@ export class RowPacker {
164
184
 
165
185
  /**
166
186
  * Record a placed item back into its row. Grows the row's height to
167
- * fit the item, advances `rightEdge`, sets the caption spill, and
168
- * shifts later rows down if this row got taller.
187
+ * fit the item, advances `rightEdge` and sets the caption spill.
169
188
  */
170
189
  commitItem(input: ItemCommitInput): void {
171
190
  const row = this.rows[input.rowIndex];
172
- const target = Math.max(input.placed.box.height, input.rowHeight ?? 0);
173
- this.growRowHeight(input.rowIndex, target);
191
+ row.height = Math.max(
192
+ row.height,
193
+ row.contentOffsetY + Math.max(input.placed.box.height, input.rowHeight ?? 0),
194
+ );
174
195
  row.rightEdge = input.logicalEnd;
175
196
  row.spillX = input.spillReservation ?? this.opts.laneLeftX;
176
- row.placedChildren.push(input.placed);
177
197
  }
178
198
 
179
199
  /**
@@ -182,10 +202,13 @@ export class RowPacker {
182
202
  */
183
203
  commitBlock(input: BlockCommitInput): void {
184
204
  const row = this.rows[input.rowIndex];
185
- this.growRowHeight(input.rowIndex, input.blockHeight);
205
+ row.height = Math.max(row.height, input.blockHeight);
206
+ row.contentOffsetY = input.contentOffsetY ?? 0;
186
207
  row.rightEdge = input.blockEnd;
187
- row.spillX = this.opts.laneLeftX;
188
- row.placedChildren.push(input.placed);
208
+ row.spillX = input.spillReservation ?? this.opts.laneLeftX;
209
+ if (input.paintedRight !== undefined && input.paintedRight > this.paintedRightX) {
210
+ this.paintedRightX = input.paintedRight;
211
+ }
189
212
  }
190
213
 
191
214
  /** Total vertical extent reached (last row's bottom - originY). */
@@ -195,10 +218,11 @@ export class RowPacker {
195
218
  }
196
219
 
197
220
  /** Rightmost extent reached — bar logical end OR caption spill,
198
- * whichever is wider, across every row. Empty rows contribute
199
- * `laneLeftX`. */
221
+ * whichever is wider, across every row, and anything a committed
222
+ * block paints past its own box (`BlockCommitInput.paintedRight`).
223
+ * Empty rows contribute `laneLeftX`. */
200
224
  usedRightX(): number {
201
- let x = this.opts.laneLeftX;
225
+ let x = this.paintedRightX;
202
226
  for (const r of this.rows) {
203
227
  if (r.rightEdge > x) x = r.rightEdge;
204
228
  if (r.spillX > x) x = r.spillX;
@@ -213,7 +237,23 @@ export class RowPacker {
213
237
  c.y < r.y + r.height &&
214
238
  p.desiredStart < c.xEnd &&
215
239
  p.desiredEnd > c.xStart &&
216
- c.slackPredId !== p.childId,
240
+ c.slackPredId !== p.childId &&
241
+ !p.pendingIds?.has(c.slackPredId),
242
+ );
243
+ }
244
+
245
+ /**
246
+ * An overrun slack arrow runs left, from its source back to a milestone
247
+ * dated before the source ends. Rows never move up, so the source cannot
248
+ * push the work before it out of that path; it drops to a row where the
249
+ * path is clear instead.
250
+ */
251
+ private rowCrossesOwnCorridor(r: PackedRow, p: ItemPlacementInput): boolean {
252
+ return this.opts.slackCorridors.some(
253
+ (c) =>
254
+ c.slackPredId === p.childId &&
255
+ c.xStart < p.desiredStart &&
256
+ r.rightEdge > Math.max(c.xStart, this.opts.laneLeftX),
217
257
  );
218
258
  }
219
259
 
@@ -224,48 +264,9 @@ export class RowPacker {
224
264
  height,
225
265
  rightEdge: this.opts.laneLeftX,
226
266
  spillX: this.opts.laneLeftX,
227
- placedChildren: [],
267
+ contentOffsetY: 0,
228
268
  };
229
269
  this.rows.push(r);
230
270
  return r;
231
271
  }
232
-
233
- private growRowHeight(rowIndex: number, newHeight: number): void {
234
- const row = this.rows[rowIndex];
235
- const delta = newHeight - row.height;
236
- if (delta <= 0) return;
237
- row.height = newHeight;
238
- for (let i = rowIndex + 1; i < this.rows.length; i += 1) {
239
- const r = this.rows[i];
240
- r.y += delta;
241
- for (const child of r.placedChildren) {
242
- shiftPositionedY(child, delta);
243
- }
244
- }
245
- }
246
- }
247
-
248
- /**
249
- * Walk a positioned subtree and shift every absolute y by `dy`. Used
250
- * when an earlier row in the packer grows, retroactively pushing every
251
- * later row (and its placed subtrees) down to keep them clear.
252
- *
253
- * Items: shift the bar box plus label chips and any overflow box.
254
- * Groups / parallels: shift the container box and recurse into
255
- * children. The caller's `box.y` already moved with the parent.
256
- */
257
- function shiftPositionedY(p: PositionedTrackChild, dy: number): void {
258
- p.box.y += dy;
259
- if (p.kind === 'item') {
260
- for (const chip of p.labelChips) {
261
- chip.box.y += dy;
262
- }
263
- if (p.overflowBox) {
264
- p.overflowBox.y += dy;
265
- }
266
- return;
267
- }
268
- for (const child of p.children) {
269
- shiftPositionedY(child, dy);
270
- }
271
272
  }