@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
@@ -3,47 +3,74 @@
3
3
  // and either pins to a fixed `date:` or floats to the rightmost `after:`
4
4
  // predecessor. A non-binding (second-latest) predecessor drives the
5
5
  // "slack" arrow when present. Date-pinned milestones whose predecessors
6
- // would push past the date are flagged `isOverrun`.
6
+ // would push past the date are flagged `isOverrun` and slip: the diamond
7
+ // stays on the date, and a second one sits where the latest predecessor
8
+ // ends (`slipX`).
7
9
  //
8
- // Marker-row placement (row + label box + left/right side) is decided
9
- // once by `roadmap-node.ts::packMarkerRow`. Date-pinned entities are
10
- // pre-packed before swimlanes run; after-only milestones get a
11
- // PROVISIONAL row=0 placement here at build time which the unified
12
- // re-pack in `RoadmapNode.place` overwrites with the final tick-order
13
- // bottom-first slot once every centerX is known.
10
+ // Placement decides x, slip, slack arrows and overrun only (`MilestonePlacement`).
11
+ // The marker row and label box come from the final marker pack in
12
+ // finalize (`packMarkers`, f5, through `marker-band.ts::packMarkerRow`),
13
+ // which runs once every centerX is known and the canvas is final;
14
+ // `positionMilestones` then builds each `PositionedMilestone` from its
15
+ // placement and its row (`placeMarkers`, f6). An arrow that starts on a
16
+ // marker row (from an anchor or milestone predecessor) waits for those
17
+ // rows too: placement records the marker, `positionMilestones` reads its
18
+ // row.
14
19
 
15
20
  import type { MilestoneDeclaration } from '@nowline/core';
16
- import { parseDate, propValue, propValues } from '../dsl-utils.js';
21
+ import { formatIsoDate, parseIsoDate, propValue, propValues } from '../dsl-utils.js';
22
+ import { itemSlackAttachY } from '../item-port-geometry.js';
17
23
  import type { LayoutContext } from '../layout-context.js';
18
24
  import { resolveStyle } from '../style-resolution.js';
19
- import type { BoundingBox, Point, PositionedMilestone } from '../types.js';
20
- import {
21
- MARKER_BOLD_WIDTH_FACTOR,
22
- MARKER_DIAMOND_RADIUS_PX,
23
- MARKER_LABEL_GAP_PX,
24
- MARKER_LABEL_HEIGHT_PX,
25
- } from './marker-geometry.js';
25
+ import type {
26
+ MarkerRowPlacement,
27
+ Point,
28
+ PositionedMilestone,
29
+ ResolvedStyle,
30
+ SlackCorridor,
31
+ } from '../types.js';
32
+ import { isOnWaveBoundary, WAVE_EDGE_TOLERANCE_PX } from '../wave-layout.js';
33
+ import { MARKER_DIAMOND_RADIUS_PX } from './marker-geometry.js';
26
34
 
27
35
  /**
28
- * One predecessor of a milestone with everything the slack-arrow
29
- * pipeline needs: its source x (visual right edge for items, marker
30
- * centerX for anchors / other milestones), its attach y (bar bottom
31
- * strip when text spills, row mid otherwise), and its flow key (used
32
- * to dedupe so a chained-flow's siblings collapse to the last entry).
36
+ * One predecessor of a milestone with everything placement and the
37
+ * slack-arrow pipeline need: its end in time (`endX`, also where its slack
38
+ * or overrun line starts, specs/true-width-layout.md §3.5), an item's
39
+ * attach y in the chart's local frame (bar bottom strip when text spills,
40
+ * nominal row midline otherwise), and its flow key (used to dedupe so a
41
+ * chained-flow's siblings collapse to the last entry).
33
42
  */
34
43
  export interface MilestonePredecessor {
35
44
  ref: string;
36
- x: number;
37
- y: number;
45
+ /**
46
+ * Where the predecessor ends in time: an item's duration end, a
47
+ * marker's x, a wave's `E_k`. The milestone sits on the latest one and
48
+ * overruns a date by it (specs/scheduler-unification.md §8 decision 6);
49
+ * its slack or overrun line starts here.
50
+ */
51
+ endX: number;
52
+ /**
53
+ * An item's attach y. Undefined for every other reference: a marker
54
+ * (its arrow starts on its marker row, known only after the final
55
+ * pack), a wave, a group or a parallel.
56
+ */
57
+ y: number | undefined;
58
+ /** Set when `ref` names an anchor or milestone of this file. */
59
+ marker?: true;
38
60
  flowKey: string;
61
+ /**
62
+ * Set when `ref` names a wave: `endX` is the seeded wave end `E_k`.
63
+ * Wave references draw no slack arrows (specs/waves.md §5.3).
64
+ */
65
+ wave?: string;
39
66
  }
40
67
 
41
68
  /**
42
- * Resolve each `after:` reference into a `MilestonePredecessor`.
43
- * Items use their VISUAL right edge as the slack source so the
44
- * arrow leaves the painted bar instead of landing in the inter-
45
- * column gutter; markers (anchors / other milestones) fall back
46
- * to their cut-line centerX. Refs whose target is unknown drop
69
+ * Resolve each `after:` reference into a `MilestonePredecessor`. Its
70
+ * `endX` is the reference's right edge in time (`entityRightEdges`, an
71
+ * item's duration end, a marker's cut-line x), where its slack line
72
+ * starts: a bar spans exactly its dates, so its end date is its edge
73
+ * (specs/true-width-layout.md §3.5). Refs whose target is unknown drop
47
74
  * silently — matches the legacy continue path.
48
75
  */
49
76
  export function collectMilestonePredecessors(
@@ -52,14 +79,26 @@ export function collectMilestonePredecessors(
52
79
  ): MilestonePredecessor[] {
53
80
  const out: MilestonePredecessor[] = [];
54
81
  for (const ref of refs) {
55
- const visualRight = ctx.entityVisualRightX.get(ref);
56
- const x = visualRight ?? ctx.entityRightEdges.get(ref);
57
- if (x === undefined) continue;
58
- const y = ctx.itemSlackAttachY.get(ref) ?? ctx.entityMidpoints.get(ref)?.y ?? 0;
82
+ // Items attach off their FINAL box (see `item-port-geometry.ts`),
83
+ // so a bar that a row growth moved down still sources its arrow.
84
+ const item = ctx.placedItems.get(ref);
85
+ const endX = ctx.entityRightEdges.get(ref);
86
+ if (endX === undefined) continue;
87
+ const y = item ? itemSlackAttachY(item, ctx.bandScale.bandwidth()) : undefined;
88
+ // A wave reads its seeded end `E_k` (logical, not inset), so a
89
+ // milestone bound by it sits exactly on the boundary.
90
+ if (!item && ctx.waves?.plan.index.has(ref)) {
91
+ out.push({ ref, endX, y, flowKey: `wave:${ref}`, wave: ref });
92
+ continue;
93
+ }
59
94
  // Markers don't share a flow with anything, so use their id
60
95
  // as a unique flow key — every marker stands on its own.
61
96
  const flowKey = ctx.itemFlowKey.get(ref) ?? `marker:${ref}`;
62
- out.push({ ref, x, y, flowKey });
97
+ if (!item && ctx.markerIds.has(ref)) {
98
+ out.push({ ref, endX, y, marker: true, flowKey });
99
+ continue;
100
+ }
101
+ out.push({ ref, endX, y, flowKey });
63
102
  }
64
103
  return out;
65
104
  }
@@ -77,32 +116,101 @@ export function lastPredecessorPerFlow(preds: MilestonePredecessor[]): Milestone
77
116
  const m = new Map<string, MilestonePredecessor>();
78
117
  for (const p of preds) {
79
118
  const existing = m.get(p.flowKey);
80
- if (!existing || p.x > existing.x) m.set(p.flowKey, p);
119
+ if (!existing || p.endX > existing.endX) m.set(p.flowKey, p);
81
120
  }
82
121
  return Array.from(m.values());
83
122
  }
84
123
 
85
- function decideLabelBoxForCanvas(
86
- centerX: number,
87
- centerY: number,
88
- radius: number,
89
- title: string,
90
- fontSize: number,
91
- bold: boolean,
92
- chartLeftX: number,
93
- chartRightX: number,
94
- ): { box: BoundingBox; side: 'left' | 'right' } {
95
- const labelWidth = title.length * fontSize * 0.58 * (bold ? MARKER_BOLD_WIDTH_FACTOR : 1);
96
- const naturalRightX = centerX + radius + MARKER_LABEL_GAP_PX;
97
- const naturalLeftX = centerX - radius - MARKER_LABEL_GAP_PX - labelWidth;
98
- const fitsRight = naturalRightX + labelWidth <= chartRightX;
99
- const fitsLeft = naturalLeftX >= chartLeftX;
100
- const side: 'left' | 'right' = fitsRight ? 'right' : fitsLeft ? 'left' : 'right';
101
- const xLeft = side === 'right' ? naturalRightX : naturalLeftX;
102
- return {
103
- box: { x: xLeft, y: centerY - 4, width: labelWidth, height: MARKER_LABEL_HEIGHT_PX },
104
- side,
105
- };
124
+ /**
125
+ * Where a milestone's slack or overrun arrow starts: at `x` on a chart y
126
+ * (an item predecessor's attach y, in the chart's local frame), or at `x`
127
+ * on the row of `marker` in the marker band, which `positionMilestones`
128
+ * reads from the final pack.
129
+ */
130
+ export type MilestoneArrowStart = { x: number; y: number } | { x: number; marker: string };
131
+
132
+ /**
133
+ * Whether a marker predecessor sits on top of the milestone at `x`: their
134
+ * diamonds touch or overlap. It draws no arrow then, since the arrow would
135
+ * run between two diamonds that already meet.
136
+ */
137
+ function isOnTopOf(p: MilestonePredecessor, x: number): boolean {
138
+ return Math.abs(x - p.endX) <= 2 * MARKER_DIAMOND_RADIUS_PX;
139
+ }
140
+
141
+ /**
142
+ * The slack arrow a non-binding predecessor of an after-only milestone at
143
+ * `maxEnd` draws, or null: it must end before the milestone and be an
144
+ * item or a marker clear of the milestone's diamond. A wave, a group and
145
+ * a parallel draw none.
146
+ */
147
+ function slackArrowStart(p: MilestonePredecessor, maxEnd: number): MilestoneArrowStart | null {
148
+ if (p.endX >= maxEnd || p.wave !== undefined) return null;
149
+ if (p.y !== undefined) return { x: p.endX, y: p.y };
150
+ if (p.marker && !isOnTopOf(p, maxEnd)) return { x: p.endX, marker: p.ref };
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * The predecessor a dated milestone slips to: the one that ends latest in
156
+ * time and is not a wave (an overrunning wave is taken separately, by
157
+ * `overrunningWave`).
158
+ */
159
+ export function latestArrowPredecessor(preds: MilestonePredecessor[]): MilestonePredecessor | null {
160
+ let maxPred: MilestonePredecessor | null = null;
161
+ for (const p of preds) {
162
+ if (p.wave !== undefined) continue;
163
+ if (!maxPred || p.endX > maxPred.endX) maxPred = p;
164
+ }
165
+ return maxPred;
166
+ }
167
+
168
+ /**
169
+ * The wave predecessor that overruns a dated milestone at `x`: the
170
+ * latest-ending one whose end lies past `x` (by more than the boundary
171
+ * tolerance, so a wave ending on the date does not count). Ties keep the
172
+ * first listed.
173
+ */
174
+ function overrunningWave(
175
+ preds: MilestonePredecessor[],
176
+ x: number,
177
+ ): MilestonePredecessor | undefined {
178
+ let latest: MilestonePredecessor | undefined;
179
+ for (const p of preds) {
180
+ if (p.wave === undefined || p.endX - x < WAVE_EDGE_TOLERANCE_PX) continue;
181
+ if (!latest || p.endX > latest.endX) latest = p;
182
+ }
183
+ return latest;
184
+ }
185
+
186
+ /**
187
+ * A milestone as placement leaves it: its x, slack arrows, overrun and cut
188
+ * lines, but no marker row or label box. Those come from the unified
189
+ * marker re-pack, which needs every milestone's x and the final canvas;
190
+ * `positionMilestones` combines the two.
191
+ */
192
+ export interface MilestonePlacement {
193
+ id: string;
194
+ title: string;
195
+ x: number;
196
+ radius: number;
197
+ /** True for a `date:` milestone, false for an after-only one. */
198
+ fixed: boolean;
199
+ slackArrows: MilestoneArrowStart[] | undefined;
200
+ isOverrun: boolean;
201
+ /**
202
+ * Where an overrun dated milestone actually lands: the end of its
203
+ * latest predecessor (or overrunning wave), past `x`. Omitted when it
204
+ * does not overrun, or when the two diamonds would meet.
205
+ */
206
+ slipX?: number;
207
+ style: ResolvedStyle;
208
+ cutTopY: number;
209
+ cutBottomY: number;
210
+ onWaveBoundary?: true;
211
+ overrunByWave?: string;
212
+ overrunDate?: string;
213
+ hiddenDate?: string;
106
214
  }
107
215
 
108
216
  export class MilestoneNode {
@@ -115,56 +223,59 @@ export class MilestoneNode {
115
223
  * Returns null when the milestone is neither date-pinned nor has a
116
224
  * resolvable `after:` predecessor (skipped silently — matches the
117
225
  * legacy `buildMilestones` continue-paths).
226
+ *
227
+ * Writes the milestone's entity edges and midpoint; later milestones'
228
+ * `after:` references read the edges. The midpoint's y is an
229
+ * estimate: the pre-pack row of a dated milestone, the band's first
230
+ * row for an after-only one. Nothing reads it before f6 replaces it
231
+ * with the final pack's row, except f6's coverage trace, which
232
+ * reports a milestone whose row differs from it.
118
233
  */
119
- place(ctx: LayoutContext): PositionedMilestone | null {
234
+ place(ctx: LayoutContext): MilestonePlacement | null {
120
235
  const m = this.milestone;
121
236
  const style = resolveStyle('milestone', m.properties, ctx.styleCtx);
122
237
  const dateRaw = propValue(m.properties, 'date');
123
238
  const afterRaw = propValues(m.properties, 'after');
124
- const date = parseDate(dateRaw);
239
+ const date = parseIsoDate(dateRaw);
125
240
  const inRowY = ctx.timeline.markerRow.y;
126
241
  const radius = MARKER_DIAMOND_RADIUS_PX;
127
242
  const title = m.title ?? this.id;
128
243
 
129
244
  let centerX: number | null = null;
130
- let centerY: number = inRowY;
131
- let labelBox: BoundingBox | null = null;
132
- let labelSide: 'left' | 'right' = 'right';
245
+ let estimatedCenterY: number = inRowY;
133
246
  let fixed = false;
134
- let slackArrows: Array<{ x: number; y: number }> | undefined;
247
+ let slackArrows: MilestoneArrowStart[] | undefined;
135
248
  let isOverrun = false;
249
+ let slipX: number | undefined;
250
+ let overrunByWave: string | undefined;
136
251
 
137
252
  if (date) {
138
253
  const x = ctx.scale.forwardWithinDomain(date);
139
254
  if (x === null) return null;
140
255
  centerX = x;
141
256
  fixed = true;
142
- const placement = ctx.markerRowPlacements.get(this.id);
143
- if (placement) {
144
- centerY = placement.centerY;
145
- labelBox = placement.labelBox;
146
- labelSide = placement.labelSide;
147
- }
257
+ const prePacked = ctx.markerRowPlacements.get(this.id);
258
+ if (prePacked) estimatedCenterY = prePacked.centerY;
148
259
  // Date-pinned milestones whose `after:` predecessors finish
149
- // past the pinned date are flagged `isOverrun`; the latest
150
- // last-of-flow predecessor draws a single overrun arrow
151
- // pointing back from its visual right edge to the
152
- // milestone's column. Flow-dedupe avoids stacking redundant
153
- // arrows from sibling items in one chained flow.
260
+ // past the pinned date are flagged `isOverrun` and slip to the
261
+ // latest one's end: the diamond stays on the date, a second
262
+ // one and the cut line move to `slipX` (specs/rendering.md,
263
+ // Milestones). The wave-deadline idiom (specs/waves.md §5.3,
264
+ // Example 11) counts too: a wave listed in `after:` that ends
265
+ // past the date.
154
266
  const preds = collectMilestonePredecessors(afterRaw, ctx);
155
267
  const dedupedPreds = lastPredecessorPerFlow(preds);
156
- let maxPred: MilestonePredecessor | null = null;
157
- for (const p of dedupedPreds) {
158
- if (!maxPred || p.x > maxPred.x) maxPred = p;
159
- }
160
- if (maxPred && maxPred.x > x) {
268
+ const maxPred = latestArrowPredecessor(dedupedPreds);
269
+ const wave = overrunningWave(dedupedPreds, x);
270
+ overrunByWave = wave?.wave;
271
+ const landsAt = Math.max(
272
+ maxPred && maxPred.endX > x ? maxPred.endX : x,
273
+ wave?.endX ?? x,
274
+ );
275
+ if (landsAt > x) {
161
276
  isOverrun = true;
162
- slackArrows = [
163
- {
164
- x: maxPred.x,
165
- y: maxPred.y > 0 ? maxPred.y : centerY,
166
- },
167
- ];
277
+ // Two diamonds that would meet draw as one, on the date.
278
+ if (landsAt - x > 2 * MARKER_DIAMOND_RADIUS_PX) slipX = landsAt;
168
279
  }
169
280
  } else if (afterRaw.length > 0) {
170
281
  // Float to the rightmost (binding) predecessor; every
@@ -178,65 +289,27 @@ export class MilestoneNode {
178
289
  // last-of-flow arrow.
179
290
  const preds = collectMilestonePredecessors(afterRaw, ctx);
180
291
  const dedupedPreds = lastPredecessorPerFlow(preds);
181
- dedupedPreds.sort((a, b) => b.x - a.x);
182
- const maxEnd = dedupedPreds[0]?.x ?? ctx.timeline.originX;
292
+ dedupedPreds.sort((a, b) => b.endX - a.endX);
293
+ const maxEnd = dedupedPreds[0]?.endX ?? ctx.timeline.originX;
183
294
  centerX = maxEnd;
184
295
  fixed = false;
185
- // Provisional placement — RoadmapNode runs a unified
186
- // tick-order pack across all markers (date-pinned anchors,
187
- // date-pinned milestones, after-only milestones) once
188
- // everyone's centerX is known. That pass overwrites the
189
- // rowIndex / centerY / labelBox / labelSide we set here.
190
- const provisional = decideLabelBoxForCanvas(
191
- centerX,
192
- inRowY,
193
- radius,
194
- title,
195
- 10,
196
- true,
197
- ctx.timeline.box.x,
198
- ctx.chartRightX,
199
- );
200
- centerY = inRowY;
201
- labelBox = provisional.box;
202
- labelSide = provisional.side;
203
- ctx.markerRowPlacements.set(this.id, {
204
- rowIndex: 0,
205
- centerY,
206
- labelBox,
207
- labelSide,
208
- });
209
- const arrows: Array<{ x: number; y: number }> = [];
296
+ const arrows: MilestoneArrowStart[] = [];
210
297
  for (let i = 1; i < dedupedPreds.length; i++) {
211
- const p = dedupedPreds[i];
212
- if (p.x < maxEnd && p.y > 0) arrows.push({ x: p.x, y: p.y });
298
+ const arrow = slackArrowStart(dedupedPreds[i], maxEnd);
299
+ if (arrow) arrows.push(arrow);
213
300
  }
214
301
  if (arrows.length > 0) slackArrows = arrows;
215
302
  }
216
303
  if (centerX === null) return null;
217
- if (!labelBox) {
218
- const fallback = decideLabelBoxForCanvas(
219
- centerX,
220
- centerY,
221
- radius,
222
- title,
223
- 10,
224
- true,
225
- ctx.timeline.box.x,
226
- ctx.chartRightX,
227
- );
228
- labelBox = fallback.box;
229
- labelSide = fallback.side;
230
- }
231
304
 
232
- const center: Point = { x: centerX, y: centerY };
233
- ctx.entityLeftEdges.set(this.id, center.x);
234
- ctx.entityRightEdges.set(this.id, center.x);
235
- ctx.entityMidpoints.set(this.id, center);
236
- return {
305
+ const midpoint: Point = { x: centerX, y: estimatedCenterY };
306
+ ctx.entityLeftEdges.set(this.id, midpoint.x);
307
+ ctx.entityRightEdges.set(this.id, midpoint.x);
308
+ ctx.entityMidpoints.set(this.id, midpoint);
309
+ const placement: MilestonePlacement = {
237
310
  id: this.id,
238
311
  title,
239
- center,
312
+ x: centerX,
240
313
  radius,
241
314
  fixed,
242
315
  slackArrows,
@@ -244,20 +317,132 @@ export class MilestoneNode {
244
317
  style,
245
318
  cutTopY: ctx.chartTopY,
246
319
  cutBottomY: ctx.swimlaneBottomY,
247
- labelBox,
248
- labelSide,
249
320
  };
321
+ if (slipX !== undefined) placement.slipX = slipX;
322
+ // On a wave boundary the boundary line carries the vertical. A
323
+ // slipped milestone's cut line sits at `slipX`; an overrun one that
324
+ // did not slip keeps its red cut line, so it never counts as on a
325
+ // boundary.
326
+ const cutX = slipX ?? centerX;
327
+ if (ctx.waves && (slipX !== undefined || !isOverrun) && isOnWaveBoundary(ctx.waves, cutX)) {
328
+ placement.onWaveBoundary = true;
329
+ }
330
+ if (overrunByWave !== undefined && date) {
331
+ placement.overrunByWave = overrunByWave;
332
+ placement.overrunDate = formatIsoDate(date);
333
+ }
334
+ // A date the axis hides keeps its own day as a tooltip
335
+ // (specs/working-calendar.md §7.4). Under `show` the day is drawn,
336
+ // and the milestone sits on it.
337
+ if (date && !ctx.scale.showsNonWorking && !ctx.calendar.isWorkingDay(date)) {
338
+ placement.hiddenDate = formatIsoDate(date);
339
+ }
340
+ return placement;
250
341
  }
251
342
  }
252
343
 
253
344
  export function buildMilestones(
254
345
  milestones: Map<string, MilestoneDeclaration>,
255
346
  ctx: LayoutContext,
347
+ ): MilestonePlacement[] {
348
+ const out: MilestonePlacement[] = [];
349
+ for (const [id, m] of milestones) {
350
+ const placement = new MilestoneNode(id, m).place(ctx);
351
+ if (placement) out.push(placement);
352
+ }
353
+ return out;
354
+ }
355
+
356
+ /**
357
+ * Build the positioned milestones from their placements and the unified
358
+ * marker re-pack (`rows`, keyed by id), in placement order. The re-pack
359
+ * packs every placed milestone and anchor, so a missing row is an
360
+ * invariant violation and throws. An arrow that starts on a marker row
361
+ * takes that row's centre, already final; one from an item keeps its
362
+ * chart y. Optional keys stay omitted when the placement omits them.
363
+ */
364
+ export function positionMilestones(
365
+ placements: readonly MilestonePlacement[],
366
+ rows: ReadonlyMap<string, MarkerRowPlacement>,
256
367
  ): PositionedMilestone[] {
257
- const out: PositionedMilestone[] = [];
368
+ const rowOf = (id: string): MarkerRowPlacement => {
369
+ const row = rows.get(id);
370
+ if (!row) {
371
+ throw new Error(`layout: marker "${id}" has no marker-row placement`);
372
+ }
373
+ return row;
374
+ };
375
+ return placements.map((p) => {
376
+ const row = rowOf(p.id);
377
+ const positioned: PositionedMilestone = {
378
+ id: p.id,
379
+ title: p.title,
380
+ center: { x: p.x, y: row.centerY },
381
+ radius: p.radius,
382
+ fixed: p.fixed,
383
+ slackArrows: p.slackArrows?.map((a) =>
384
+ 'y' in a ? { x: a.x, y: a.y } : { x: a.x, y: rowOf(a.marker).centerY },
385
+ ),
386
+ isOverrun: p.isOverrun,
387
+ style: p.style,
388
+ cutTopY: p.cutTopY,
389
+ cutBottomY: p.cutBottomY,
390
+ labelBox: row.labelBox,
391
+ labelSide: row.labelSide,
392
+ };
393
+ if (p.slipX !== undefined) positioned.slip = { x: p.slipX };
394
+ if (p.onWaveBoundary) positioned.onWaveBoundary = true;
395
+ if (p.overrunByWave !== undefined) positioned.overrunByWave = p.overrunByWave;
396
+ if (p.overrunDate !== undefined) positioned.overrunDate = p.overrunDate;
397
+ if (p.hiddenDate !== undefined) positioned.hiddenDate = p.hiddenDate;
398
+ return positioned;
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Collect slack-arrow corridors from the resolved milestones using the
404
+ * current entity-edge maps. The result mirrors what
405
+ * `MilestoneNode.place` would emit on `slackArrows`, so the row-packer
406
+ * and the renderer agree on every arrow's geometry.
407
+ *
408
+ * Each non-binding item predecessor (`x < maxEnd`) of an after-only
409
+ * milestone becomes one corridor. A date-pinned milestone draws no arrow
410
+ * (an overrun slips it, in the marker band), so it owns none. An arrow
411
+ * from a marker runs in the marker band, above the lanes, so it owns no
412
+ * corridor; nor does a wave predecessor, which draws no slack arrow (a
413
+ * floating milestone bound by a wave still sets `maxEnd`).
414
+ */
415
+ export function collectSlackCorridors(
416
+ milestones: Map<string, import('@nowline/core').MilestoneDeclaration>,
417
+ ctx: LayoutContext,
418
+ ): SlackCorridor[] {
419
+ const out: SlackCorridor[] = [];
258
420
  for (const [id, m] of milestones) {
259
- const positioned = new MilestoneNode(id, m).place(ctx);
260
- if (positioned) out.push(positioned);
421
+ // A dated milestone draws no arrow, so it owns no corridor.
422
+ if (parseIsoDate(propValue(m.properties, 'date'))) continue;
423
+ // Reuse MilestoneNode's helpers so the corridor set agrees
424
+ // exactly with the rendered slack-arrow set: same source x
425
+ // (visual edge for items / cut-line for markers), same
426
+ // attach y, and the same flow-dedupe rule that collapses
427
+ // chained predecessors to one arrow per flow.
428
+ const preds = collectMilestonePredecessors(propValues(m.properties, 'after'), ctx);
429
+ const dedupedPreds = lastPredecessorPerFlow(preds);
430
+ if (dedupedPreds.length === 0) continue;
431
+ dedupedPreds.sort((a, b) => b.endX - a.endX);
432
+ const maxEnd = dedupedPreds[0].endX;
433
+ for (let i = 1; i < dedupedPreds.length; i++) {
434
+ const p = dedupedPreds[i];
435
+ const arrow = slackArrowStart(p, maxEnd);
436
+ if (arrow && 'y' in arrow) {
437
+ out.push({
438
+ xStart: p.endX,
439
+ xEnd: maxEnd,
440
+ y: arrow.y,
441
+ slackPredId: p.ref,
442
+ milestoneId: id,
443
+ });
444
+ }
445
+ }
261
446
  }
262
447
  return out;
263
448
  }
@@ -0,0 +1,87 @@
1
+ // The now-line (finalize step f12): the line at `today` on the final
2
+ // scale, with its pill in the reserved row above the tick panel.
3
+
4
+ import { localeStrings } from '../i18n.js';
5
+ import type { LayoutContext } from '../layout-context.js';
6
+ import { resolveStyle } from '../style-resolution.js';
7
+ import { estimateTextWidth } from '../text-measure.js';
8
+ import {
9
+ GUTTER_PX,
10
+ NOW_PILL_LABEL_FONT_SIZE_PX,
11
+ NOW_PILL_LABEL_INSET_X_PX,
12
+ NOW_PILL_WIDTH_PX,
13
+ } from '../themes/shared.js';
14
+ import type { PositionedNowline } from '../types.js';
15
+
16
+ /**
17
+ * The now-line on the final scale. `chartRightX` is the final canvas right
18
+ * edge; a pill that would cross it flips to flag mode.
19
+ */
20
+ export function buildNowline(
21
+ today: Date | undefined,
22
+ ctx: LayoutContext,
23
+ locale: string,
24
+ chartRightX: number,
25
+ ): PositionedNowline | null {
26
+ if (!today) return null;
27
+ const x = ctx.scale.forwardWithinDomain(today);
28
+ if (x === null) return null;
29
+ // Pill row is the band reserved at the very top of the timeline area.
30
+ // The line drops from the bottom of the pill (top of the date headers)
31
+ // through any marker row and into the chart, so the pill and line stay
32
+ // visually connected.
33
+ const pillTopY = ctx.timeline.box.y;
34
+ const lineTopY = ctx.timeline.tickPanelY;
35
+ // Pill width is locale-aware: en-US's `'now'` (3 chars) fits the
36
+ // 36 px default trivially; longer strings like fr's `'maint.'`
37
+ // (6 chars) need a wider pill to avoid clipping. Floor at the
38
+ // default so en-US output stays byte-stable; grow when the
39
+ // measured label needs it. The 2× inset matches the renderer's
40
+ // flag-mode label inset (`NOW_PILL_LABEL_INSET_X_PX`) and gives
41
+ // the same visual padding to centered-mode strings.
42
+ const label = localeStrings(locale).nowLabel;
43
+ const labelTextWidth = estimateTextWidth(label, NOW_PILL_LABEL_FONT_SIZE_PX);
44
+ const pillWidth = Math.max(
45
+ NOW_PILL_WIDTH_PX,
46
+ Math.ceil(labelTextWidth + 2 * NOW_PILL_LABEL_INSET_X_PX),
47
+ );
48
+ // The "chart's left edge" the pill must clear is `chartLeftX` (in
49
+ // beside-mode, the right edge of the header card; in above-mode,
50
+ // the canvas left edge at x=0). originX = chartLeftX + GUTTER_PX,
51
+ // so we recover chartLeftX as `originX - GUTTER_PX`.
52
+ const chartLeftX = ctx.timeline.originX - GUTTER_PX;
53
+ const halfPill = pillWidth / 2;
54
+ let pillMode: 'center' | 'flag-right' | 'flag-left';
55
+ if (x - halfPill < chartLeftX) {
56
+ // Centered pill would intrude into the header card / past the
57
+ // canvas left edge — anchor the pill's LEFT side to the line
58
+ // and let it extend right into the chart.
59
+ pillMode = 'flag-right';
60
+ } else if (x + halfPill > chartRightX) {
61
+ // Centered pill would clip past the canvas right edge — anchor
62
+ // the pill's RIGHT side to the line and let it extend left.
63
+ pillMode = 'flag-left';
64
+ } else {
65
+ pillMode = 'center';
66
+ }
67
+ // Bottom-most Y the now-line should reach. When a mirrored bottom
68
+ // tick panel exists, thread the line through it (so the line ties
69
+ // the two date strips together visually). Otherwise stop at the
70
+ // last swimlane — never extend into the footnote area below.
71
+ const bottomTickPanelY = ctx.timeline.bottomTickPanelY;
72
+ const bottomTickPanelHeight = ctx.timeline.bottomTickPanelHeight ?? 0;
73
+ const lineBottomY =
74
+ bottomTickPanelY !== undefined && bottomTickPanelHeight > 0
75
+ ? bottomTickPanelY + bottomTickPanelHeight
76
+ : ctx.swimlaneBottomY;
77
+ return {
78
+ x,
79
+ topY: lineTopY,
80
+ bottomY: lineBottomY,
81
+ pillTopY,
82
+ pillMode,
83
+ label,
84
+ pillWidth,
85
+ style: resolveStyle('item', [], ctx.styleCtx),
86
+ };
87
+ }