@nowline/layout 0.8.6 → 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 (338) 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/calendar-resolver.d.ts +27 -0
  6. package/dist/calendar-resolver.d.ts.map +1 -0
  7. package/dist/calendar-resolver.js +132 -0
  8. package/dist/calendar-resolver.js.map +1 -0
  9. package/dist/calendar.d.ts +0 -19
  10. package/dist/calendar.d.ts.map +1 -1
  11. package/dist/calendar.js +4 -44
  12. package/dist/calendar.js.map +1 -1
  13. package/dist/canvas-width.d.ts +23 -0
  14. package/dist/canvas-width.d.ts.map +1 -0
  15. package/dist/canvas-width.js +26 -0
  16. package/dist/canvas-width.js.map +1 -0
  17. package/dist/capacity.d.ts +28 -0
  18. package/dist/capacity.d.ts.map +1 -1
  19. package/dist/capacity.js +64 -29
  20. package/dist/capacity.js.map +1 -1
  21. package/dist/chart-translation.d.ts +22 -0
  22. package/dist/chart-translation.d.ts.map +1 -0
  23. package/dist/chart-translation.js +83 -0
  24. package/dist/chart-translation.js.map +1 -0
  25. package/dist/container-header-geometry.d.ts +64 -0
  26. package/dist/container-header-geometry.d.ts.map +1 -0
  27. package/dist/container-header-geometry.js +89 -0
  28. package/dist/container-header-geometry.js.map +1 -0
  29. package/dist/dsl-utils.d.ts +3 -4
  30. package/dist/dsl-utils.d.ts.map +1 -1
  31. package/dist/dsl-utils.js +8 -25
  32. package/dist/dsl-utils.js.map +1 -1
  33. package/dist/edge-routing.d.ts +69 -78
  34. package/dist/edge-routing.d.ts.map +1 -1
  35. package/dist/edge-routing.js +271 -401
  36. package/dist/edge-routing.js.map +1 -1
  37. package/dist/frame-tab-geometry.d.ts +24 -14
  38. package/dist/frame-tab-geometry.d.ts.map +1 -1
  39. package/dist/frame-tab-geometry.js +69 -23
  40. package/dist/frame-tab-geometry.js.map +1 -1
  41. package/dist/header-card-geometry.d.ts +12 -0
  42. package/dist/header-card-geometry.d.ts.map +1 -1
  43. package/dist/header-card-geometry.js +14 -2
  44. package/dist/header-card-geometry.js.map +1 -1
  45. package/dist/header-logo.d.ts +28 -0
  46. package/dist/header-logo.d.ts.map +1 -0
  47. package/dist/header-logo.js +51 -0
  48. package/dist/header-logo.js.map +1 -0
  49. package/dist/i18n.d.ts +11 -0
  50. package/dist/i18n.d.ts.map +1 -1
  51. package/dist/i18n.js +11 -1
  52. package/dist/i18n.js.map +1 -1
  53. package/dist/index.d.ts +20 -8
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +17 -7
  56. package/dist/index.js.map +1 -1
  57. package/dist/inline-date-pin-geometry.d.ts +19 -11
  58. package/dist/inline-date-pin-geometry.d.ts.map +1 -1
  59. package/dist/inline-date-pin-geometry.js +45 -46
  60. package/dist/inline-date-pin-geometry.js.map +1 -1
  61. package/dist/item-bar-geometry.d.ts +192 -4
  62. package/dist/item-bar-geometry.d.ts.map +1 -1
  63. package/dist/item-bar-geometry.js +258 -5
  64. package/dist/item-bar-geometry.js.map +1 -1
  65. package/dist/item-port-geometry.d.ts +13 -0
  66. package/dist/item-port-geometry.d.ts.map +1 -0
  67. package/dist/item-port-geometry.js +63 -0
  68. package/dist/item-port-geometry.js.map +1 -0
  69. package/dist/layout-context.d.ts +115 -51
  70. package/dist/layout-context.d.ts.map +1 -1
  71. package/dist/layout-context.js +96 -5
  72. package/dist/layout-context.js.map +1 -1
  73. package/dist/layout-insights.d.ts.map +1 -1
  74. package/dist/layout-insights.js +73 -1
  75. package/dist/layout-insights.js.map +1 -1
  76. package/dist/layout.d.ts +35 -0
  77. package/dist/layout.d.ts.map +1 -1
  78. package/dist/layout.js +18 -1315
  79. package/dist/layout.js.map +1 -1
  80. package/dist/nodes/anchor-node.d.ts.map +1 -1
  81. package/dist/nodes/anchor-node.js +11 -4
  82. package/dist/nodes/anchor-node.js.map +1 -1
  83. package/dist/nodes/content-extent.d.ts +24 -0
  84. package/dist/nodes/content-extent.d.ts.map +1 -0
  85. package/dist/nodes/content-extent.js +100 -0
  86. package/dist/nodes/content-extent.js.map +1 -0
  87. package/dist/nodes/dependency-edges.d.ts +6 -0
  88. package/dist/nodes/dependency-edges.d.ts.map +1 -0
  89. package/dist/nodes/dependency-edges.js +148 -0
  90. package/dist/nodes/dependency-edges.js.map +1 -0
  91. package/dist/nodes/footnote-node.d.ts +24 -3
  92. package/dist/nodes/footnote-node.d.ts.map +1 -1
  93. package/dist/nodes/footnote-node.js +129 -25
  94. package/dist/nodes/footnote-node.js.map +1 -1
  95. package/dist/nodes/group-node.d.ts +10 -21
  96. package/dist/nodes/group-node.d.ts.map +1 -1
  97. package/dist/nodes/group-node.js +79 -152
  98. package/dist/nodes/group-node.js.map +1 -1
  99. package/dist/nodes/header-node.d.ts +56 -0
  100. package/dist/nodes/header-node.d.ts.map +1 -0
  101. package/dist/nodes/header-node.js +163 -0
  102. package/dist/nodes/header-node.js.map +1 -0
  103. package/dist/nodes/include-node.d.ts +22 -12
  104. package/dist/nodes/include-node.d.ts.map +1 -1
  105. package/dist/nodes/include-node.js +95 -62
  106. package/dist/nodes/include-node.js.map +1 -1
  107. package/dist/nodes/item-measure.d.ts +135 -0
  108. package/dist/nodes/item-measure.d.ts.map +1 -0
  109. package/dist/nodes/item-measure.js +514 -0
  110. package/dist/nodes/item-measure.js.map +1 -0
  111. package/dist/nodes/item-node.d.ts +162 -9
  112. package/dist/nodes/item-node.d.ts.map +1 -1
  113. package/dist/nodes/item-node.js +256 -69
  114. package/dist/nodes/item-node.js.map +1 -1
  115. package/dist/nodes/marker-band.d.ts +118 -0
  116. package/dist/nodes/marker-band.d.ts.map +1 -0
  117. package/dist/nodes/marker-band.js +303 -0
  118. package/dist/nodes/marker-band.js.map +1 -0
  119. package/dist/nodes/marker-geometry.js +1 -1
  120. package/dist/nodes/marker-geometry.js.map +1 -1
  121. package/dist/nodes/milestone-node.d.ts +112 -15
  122. package/dist/nodes/milestone-node.d.ts.map +1 -1
  123. package/dist/nodes/milestone-node.js +246 -95
  124. package/dist/nodes/milestone-node.js.map +1 -1
  125. package/dist/nodes/nowline-node.d.ts +8 -0
  126. package/dist/nodes/nowline-node.d.ts.map +1 -0
  127. package/dist/nodes/nowline-node.js +74 -0
  128. package/dist/nodes/nowline-node.js.map +1 -0
  129. package/dist/nodes/parallel-node.d.ts +7 -12
  130. package/dist/nodes/parallel-node.d.ts.map +1 -1
  131. package/dist/nodes/parallel-node.js +66 -30
  132. package/dist/nodes/parallel-node.js.map +1 -1
  133. package/dist/nodes/region-markers.d.ts +41 -0
  134. package/dist/nodes/region-markers.d.ts.map +1 -0
  135. package/dist/nodes/region-markers.js +130 -0
  136. package/dist/nodes/region-markers.js.map +1 -0
  137. package/dist/nodes/roadmap-node.d.ts +9 -72
  138. package/dist/nodes/roadmap-node.d.ts.map +1 -1
  139. package/dist/nodes/roadmap-node.js +38 -788
  140. package/dist/nodes/roadmap-node.js.map +1 -1
  141. package/dist/nodes/row-track.d.ts +33 -0
  142. package/dist/nodes/row-track.d.ts.map +1 -0
  143. package/dist/nodes/row-track.js +170 -0
  144. package/dist/nodes/row-track.js.map +1 -0
  145. package/dist/nodes/sequence-item.d.ts +25 -0
  146. package/dist/nodes/sequence-item.d.ts.map +1 -0
  147. package/dist/nodes/sequence-item.js +396 -0
  148. package/dist/nodes/sequence-item.js.map +1 -0
  149. package/dist/nodes/sequence-one.d.ts +5 -0
  150. package/dist/nodes/sequence-one.d.ts.map +1 -0
  151. package/dist/nodes/sequence-one.js +26 -0
  152. package/dist/nodes/sequence-one.js.map +1 -0
  153. package/dist/nodes/swimlane-node.d.ts +5 -17
  154. package/dist/nodes/swimlane-node.d.ts.map +1 -1
  155. package/dist/nodes/swimlane-node.js +62 -145
  156. package/dist/nodes/swimlane-node.js.map +1 -1
  157. package/dist/nodes/wave-node.d.ts +52 -0
  158. package/dist/nodes/wave-node.d.ts.map +1 -0
  159. package/dist/nodes/wave-node.js +483 -0
  160. package/dist/nodes/wave-node.js.map +1 -0
  161. package/dist/non-working-display.d.ts +14 -0
  162. package/dist/non-working-display.d.ts.map +1 -0
  163. package/dist/non-working-display.js +28 -0
  164. package/dist/non-working-display.js.map +1 -0
  165. package/dist/phases/date-window.d.ts +48 -0
  166. package/dist/phases/date-window.d.ts.map +1 -0
  167. package/dist/phases/date-window.js +130 -0
  168. package/dist/phases/date-window.js.map +1 -0
  169. package/dist/phases/finalize.d.ts +198 -0
  170. package/dist/phases/finalize.d.ts.map +1 -0
  171. package/dist/phases/finalize.js +1058 -0
  172. package/dist/phases/finalize.js.map +1 -0
  173. package/dist/phases/measure.d.ts +77 -0
  174. package/dist/phases/measure.d.ts.map +1 -0
  175. package/dist/phases/measure.js +222 -0
  176. package/dist/phases/measure.js.map +1 -0
  177. package/dist/phases/place.d.ts +28 -0
  178. package/dist/phases/place.d.ts.map +1 -0
  179. package/dist/phases/place.js +225 -0
  180. package/dist/phases/place.js.map +1 -0
  181. package/dist/renderable.d.ts.map +1 -1
  182. package/dist/renderable.js +23 -3
  183. package/dist/renderable.js.map +1 -1
  184. package/dist/resolve-today.d.ts +11 -1
  185. package/dist/resolve-today.d.ts.map +1 -1
  186. package/dist/resolve-today.js +26 -12
  187. package/dist/resolve-today.js.map +1 -1
  188. package/dist/row-packer.d.ts +42 -16
  189. package/dist/row-packer.d.ts.map +1 -1
  190. package/dist/row-packer.js +51 -78
  191. package/dist/row-packer.js.map +1 -1
  192. package/dist/schedule.d.ts +58 -1
  193. package/dist/schedule.d.ts.map +1 -1
  194. package/dist/schedule.js +280 -106
  195. package/dist/schedule.js.map +1 -1
  196. package/dist/start-rules.d.ts +27 -0
  197. package/dist/start-rules.d.ts.map +1 -0
  198. package/dist/start-rules.js +91 -0
  199. package/dist/start-rules.js.map +1 -0
  200. package/dist/start-terms.d.ts +23 -0
  201. package/dist/start-terms.d.ts.map +1 -0
  202. package/dist/start-terms.js +27 -0
  203. package/dist/start-terms.js.map +1 -0
  204. package/dist/status-dot.d.ts +46 -0
  205. package/dist/status-dot.d.ts.map +1 -0
  206. package/dist/status-dot.js +78 -0
  207. package/dist/status-dot.js.map +1 -0
  208. package/dist/style-resolution.d.ts.map +1 -1
  209. package/dist/style-resolution.js +23 -43
  210. package/dist/style-resolution.js.map +1 -1
  211. package/dist/text-measure.d.ts +28 -0
  212. package/dist/text-measure.d.ts.map +1 -0
  213. package/dist/text-measure.js +70 -0
  214. package/dist/text-measure.js.map +1 -0
  215. package/dist/themes/contrast.d.ts +4 -0
  216. package/dist/themes/contrast.d.ts.map +1 -0
  217. package/dist/themes/contrast.js +32 -0
  218. package/dist/themes/contrast.js.map +1 -0
  219. package/dist/themes/dark.d.ts.map +1 -1
  220. package/dist/themes/dark.js +28 -1
  221. package/dist/themes/dark.js.map +1 -1
  222. package/dist/themes/grayscale.d.ts.map +1 -1
  223. package/dist/themes/grayscale.js +28 -1
  224. package/dist/themes/grayscale.js.map +1 -1
  225. package/dist/themes/index.d.ts +4 -11
  226. package/dist/themes/index.d.ts.map +1 -1
  227. package/dist/themes/index.js +6 -4
  228. package/dist/themes/index.js.map +1 -1
  229. package/dist/themes/light.d.ts.map +1 -1
  230. package/dist/themes/light.js +28 -1
  231. package/dist/themes/light.js.map +1 -1
  232. package/dist/themes/shape.d.ts +31 -0
  233. package/dist/themes/shape.d.ts.map +1 -1
  234. package/dist/themes/shared.d.ts +44 -50
  235. package/dist/themes/shared.d.ts.map +1 -1
  236. package/dist/themes/shared.js +79 -83
  237. package/dist/themes/shared.js.map +1 -1
  238. package/dist/time-scale.d.ts +42 -6
  239. package/dist/time-scale.d.ts.map +1 -1
  240. package/dist/time-scale.js +109 -17
  241. package/dist/time-scale.js.map +1 -1
  242. package/dist/types.d.ts +528 -11
  243. package/dist/types.d.ts.map +1 -1
  244. package/dist/view-preset.d.ts +61 -6
  245. package/dist/view-preset.d.ts.map +1 -1
  246. package/dist/view-preset.js +439 -24
  247. package/dist/view-preset.js.map +1 -1
  248. package/dist/wave-barrier.d.ts +107 -0
  249. package/dist/wave-barrier.d.ts.map +1 -0
  250. package/dist/wave-barrier.js +174 -0
  251. package/dist/wave-barrier.js.map +1 -0
  252. package/dist/wave-hatch.d.ts +4 -0
  253. package/dist/wave-hatch.d.ts.map +1 -0
  254. package/dist/wave-hatch.js +21 -0
  255. package/dist/wave-hatch.js.map +1 -0
  256. package/dist/wave-layout.d.ts +99 -0
  257. package/dist/wave-layout.d.ts.map +1 -0
  258. package/dist/wave-layout.js +130 -0
  259. package/dist/wave-layout.js.map +1 -0
  260. package/dist/working-calendar.d.ts +128 -4
  261. package/dist/working-calendar.d.ts.map +1 -1
  262. package/dist/working-calendar.js +725 -51
  263. package/dist/working-calendar.js.map +1 -1
  264. package/dist/working-span.d.ts +11 -0
  265. package/dist/working-span.d.ts.map +1 -0
  266. package/dist/working-span.js +18 -0
  267. package/dist/working-span.js.map +1 -0
  268. package/package.json +4 -4
  269. package/src/axis-columns.ts +140 -0
  270. package/src/calendar-resolver.ts +161 -0
  271. package/src/calendar.ts +4 -45
  272. package/src/canvas-width.ts +36 -0
  273. package/src/capacity.ts +71 -30
  274. package/src/chart-translation.ts +88 -0
  275. package/src/container-header-geometry.ts +116 -0
  276. package/src/dsl-utils.ts +8 -26
  277. package/src/edge-routing.ts +347 -491
  278. package/src/frame-tab-geometry.ts +78 -25
  279. package/src/header-card-geometry.ts +17 -2
  280. package/src/header-logo.ts +67 -0
  281. package/src/i18n.ts +22 -1
  282. package/src/index.ts +97 -12
  283. package/src/inline-date-pin-geometry.ts +55 -58
  284. package/src/item-bar-geometry.ts +341 -5
  285. package/src/item-port-geometry.ts +69 -0
  286. package/src/layout-context.ts +210 -72
  287. package/src/layout-insights.ts +109 -2
  288. package/src/layout.ts +57 -1530
  289. package/src/nodes/anchor-node.ts +11 -4
  290. package/src/nodes/content-extent.ts +107 -0
  291. package/src/nodes/dependency-edges.ts +171 -0
  292. package/src/nodes/footnote-node.ts +171 -31
  293. package/src/nodes/group-node.ts +92 -214
  294. package/src/nodes/header-node.ts +255 -0
  295. package/src/nodes/include-node.ts +124 -110
  296. package/src/nodes/item-measure.ts +723 -0
  297. package/src/nodes/item-node.ts +358 -80
  298. package/src/nodes/marker-band.ts +406 -0
  299. package/src/nodes/marker-geometry.ts +1 -1
  300. package/src/nodes/milestone-node.ts +321 -136
  301. package/src/nodes/nowline-node.ts +87 -0
  302. package/src/nodes/parallel-node.ts +69 -38
  303. package/src/nodes/region-markers.ts +194 -0
  304. package/src/nodes/roadmap-node.ts +46 -950
  305. package/src/nodes/row-track.ts +217 -0
  306. package/src/nodes/sequence-item.ts +433 -0
  307. package/src/nodes/sequence-one.ts +40 -0
  308. package/src/nodes/swimlane-node.ts +66 -205
  309. package/src/nodes/wave-node.ts +609 -0
  310. package/src/non-working-display.ts +32 -0
  311. package/src/phases/date-window.ts +191 -0
  312. package/src/phases/finalize.ts +1303 -0
  313. package/src/phases/measure.ts +329 -0
  314. package/src/phases/place.ts +318 -0
  315. package/src/renderable.ts +23 -3
  316. package/src/resolve-today.ts +28 -25
  317. package/src/row-packer.ts +85 -84
  318. package/src/schedule.ts +362 -114
  319. package/src/start-rules.ts +113 -0
  320. package/src/start-terms.ts +38 -0
  321. package/src/status-dot.ts +81 -0
  322. package/src/style-resolution.ts +27 -57
  323. package/src/text-measure.ts +71 -0
  324. package/src/themes/contrast.ts +34 -0
  325. package/src/themes/dark.ts +28 -1
  326. package/src/themes/grayscale.ts +28 -1
  327. package/src/themes/index.ts +10 -7
  328. package/src/themes/light.ts +28 -1
  329. package/src/themes/shape.ts +38 -0
  330. package/src/themes/shared.ts +85 -93
  331. package/src/time-scale.ts +117 -19
  332. package/src/types.ts +529 -20
  333. package/src/view-preset.ts +520 -27
  334. package/src/wave-barrier.ts +242 -0
  335. package/src/wave-hatch.ts +25 -0
  336. package/src/wave-layout.ts +203 -0
  337. package/src/working-calendar.ts +875 -57
  338. package/src/working-span.ts +19 -0
package/src/types.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  // top-left. All colors in `ResolvedStyle` are concrete hex strings baked
4
4
  // in by style-resolution; the renderer is palette-dumb.
5
5
 
6
+ import type { FrameTabGeometry } from './frame-tab-geometry.js';
7
+ import type { NonWorkingDisplay } from './non-working-display.js';
8
+
6
9
  export type SizeBucket = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
7
10
  export type ShadowKind = 'none' | 'subtle' | 'soft' | 'hard';
8
11
  export type BorderKind = 'solid' | 'dashed' | 'dotted';
@@ -17,9 +20,16 @@ export type HeaderPosition = 'beside' | 'above';
17
20
  // either edge of the viewport
18
21
  export type TimelinePosition = 'top' | 'bottom' | 'both';
19
22
  export type StatusKind = 'planned' | 'in-progress' | 'done' | 'at-risk' | 'blocked' | 'neutral';
23
+ /**
24
+ * Which hatch stroke marks background work on a fill (specs/waves.md §9.3):
25
+ * `dark` (`wave.hatch`) on a light fill, `light` (`wave.hatchOnDark`) on a
26
+ * dark one.
27
+ */
28
+ export type WaveHatchKind = 'dark' | 'light';
20
29
 
21
- // The 17 style properties from specs/dsl.md § Style Properties plus header-position
22
- // and the two roadmap-only readability knobs (`timeline-position`, `minor-grid`).
30
+ // The style properties from specs/dsl.md § Style Properties, including the
31
+ // roadmap-only knobs (`header-height`, `header-position`, `timeline-position`,
32
+ // `minor-grid`, `non-working`).
23
33
  // Every one has a concrete value after resolution (theme + defaults fill gaps).
24
34
  export interface ResolvedStyle {
25
35
  bg: string; // hex or 'none'
@@ -50,6 +60,12 @@ export interface ResolvedStyle {
50
60
  capacityIcon: string;
51
61
  timelinePosition: TimelinePosition;
52
62
  minorGrid: boolean;
63
+ /**
64
+ * How a calendar's non-working days are drawn (roadmap-only, from
65
+ * `default roadmap non-working:`). Default `'hide'`; a surface option
66
+ * overrides it (`LayoutOptions.nonWorking`).
67
+ */
68
+ nonWorking: NonWorkingDisplay;
53
69
  }
54
70
 
55
71
  export interface BoundingBox {
@@ -90,8 +106,13 @@ export interface Point {
90
106
 
91
107
  // Header + attribution mark + optional logo.
92
108
  export interface PositionedLogo {
109
+ // Drawn box in canvas coordinates. Its aspect ratio matches the asset's,
110
+ // so the logo fills it exactly.
93
111
  box: BoundingBox;
94
112
  assetRef?: string; // path as declared in DSL; renderer resolves via AssetResolver
113
+ // The asset's own size (SVG width/height or viewBox, raster pixels),
114
+ // which the renderer scales from.
115
+ intrinsic?: { width: number; height: number };
95
116
  }
96
117
 
97
118
  export interface PositionedHeader {
@@ -110,6 +131,10 @@ export interface PositionedHeader {
110
131
  // height grows for wrapped lines).
111
132
  cardBox: BoundingBox;
112
133
  logo?: PositionedLogo;
134
+ // Offset applied to every title and author line, set only when a logo
135
+ // shares the card: `x` clears the logo, `y` centers the text block
136
+ // when the logo made the card taller than the text needs.
137
+ textInset?: Point;
113
138
  style: ResolvedStyle;
114
139
  // Attribution mark (Nowline wordmark + link) lives in the top-right.
115
140
  attributionBox: BoundingBox;
@@ -166,11 +191,107 @@ export interface PositionedTimelineScale {
166
191
  // is purely a date reference for tall canvases.
167
192
  bottomTickPanelY?: number;
168
193
  bottomTickPanelHeight?: number;
194
+ /**
195
+ * The wave strip row (specs/waves.md §8.8): `WAVE_STRIP_HEIGHT_PX`
196
+ * high, directly under the top tick panel (under the now-pill row
197
+ * with `timeline-position:bottom`) and above the marker rows, which
198
+ * move down by its height. Present only when some declared wave has
199
+ * items (specs/waves.md §9.1); the mirrored bottom panel never has one.
200
+ */
201
+ waveStrip?: {
202
+ y: number;
203
+ height: number;
204
+ };
169
205
  // When `true`, the renderer draws a faint dotted line at every tick
170
206
  // boundary (not just major ticks) using `theme.timeline.minorGridLine`.
171
207
  // Mirrors the roadmap's resolved `minor-grid` style property. Default
172
208
  // `false` preserves byte-stable output for existing roadmaps.
173
209
  minorGrid: boolean;
210
+ /**
211
+ * How the axis shows non-working days (specs/working-calendar.md §7):
212
+ * `hide` collapses them to zero width, `show` draws them at full width.
213
+ * Present, with `nonWorking`, only when the window holds a non-working
214
+ * day.
215
+ */
216
+ nonWorkingDisplay?: NonWorkingDisplay;
217
+ /** The window's non-working runs, left to right; see `PositionedNonWorkingRun`. */
218
+ nonWorking?: PositionedNonWorkingRun[];
219
+ /**
220
+ * The window's labelled working exceptions, left to right; see
221
+ * `PositionedWorkingException`. Omitted when there are none.
222
+ */
223
+ workingExceptions?: PositionedWorkingException[];
224
+ }
225
+
226
+ /**
227
+ * One run of days that titled dated `working` rules turn from non-working
228
+ * to working (specs/working-calendar.md §7.4, §7.5). It is labelled in the
229
+ * marker row like a titled non-working run, but never drawn as a cut line
230
+ * or a band: a working day removes no time.
231
+ */
232
+ export interface PositionedWorkingException {
233
+ /** The run's first day: its column start under `hide`, its day start under `show` (clamped to the window). */
234
+ x: number;
235
+ /** First day the rules turn (UTC midnight). */
236
+ from: Date;
237
+ /** Last day the rules turn, inclusive (UTC midnight). */
238
+ through: Date;
239
+ /** Titles of the titled dated working rules that turn a day of the run. */
240
+ titles: string[];
241
+ /** The titles joined with `, `, then ` · +Nd` for the N days turned. */
242
+ label: string;
243
+ /** Resolved placement of `label` in the marker row, like `PositionedNonWorkingRun.labelBox`. */
244
+ labelBox?: BoundingBox;
245
+ labelSide?: 'left' | 'right';
246
+ }
247
+
248
+ /**
249
+ * One maximal run of non-working days in (or straddling) the window
250
+ * (specs/working-calendar.md §7.5). Under `hide` its `width` is 0 and `x`
251
+ * is the start of the next working day. Under `show` it spans its days,
252
+ * with `x` and the right edge clamped to the window; `from` / `through`
253
+ * keep the run's real dates.
254
+ */
255
+ export interface PositionedNonWorkingRun {
256
+ x: number;
257
+ width: number;
258
+ /** First non-working date of the run (UTC midnight). */
259
+ from: Date;
260
+ /** Last non-working date of the run, inclusive. */
261
+ through: Date;
262
+ /** Titles of the dated rules inside it; omitted for a plain weekend. */
263
+ titles?: string[];
264
+ /**
265
+ * The marker-row label of a titled run: its titles joined with `, `,
266
+ * then ` · Nd` for the N working days of the open-ended week the run
267
+ * closes (dropped when N is 0). Omitted on a run without titles.
268
+ */
269
+ label?: string;
270
+ /**
271
+ * Resolved placement of `label` in the marker row, exactly like
272
+ * `PositionedMilestone.labelBox` / `labelSide`. Omitted on a run
273
+ * without titles.
274
+ */
275
+ labelBox?: BoundingBox;
276
+ labelSide?: 'left' | 'right';
277
+ /**
278
+ * Set under `hide` on a titled run, at every scale: the renderer draws
279
+ * the named cut line at `x` from the label row down through the chart
280
+ * (spec §7.2, "a named seam always draws"). Independent of `seam`.
281
+ */
282
+ cut?: true;
283
+ /**
284
+ * Set at the days scale when the run lies strictly inside the chart and
285
+ * no grid line is drawn at its x; the renderer draws a seam there.
286
+ * Never set under `show`.
287
+ */
288
+ seam?: true;
289
+ /**
290
+ * Set under `show` at the days and weeks scales, or when the run has
291
+ * titles: the renderer paints the run as a shaded band. Coarser scales
292
+ * leave a plain weekend unpainted.
293
+ */
294
+ band?: true;
174
295
  }
175
296
 
176
297
  /**
@@ -250,9 +371,56 @@ export interface InlineDatePin {
250
371
  export interface PositionedItem {
251
372
  kind: 'item';
252
373
  id?: string;
374
+ /**
375
+ * The full, unwrapped title exactly as authored, explicit line breaks
376
+ * included (`"Technology\nSelection"` keeps its newline here). Always
377
+ * present, wrapped or not.
378
+ */
253
379
  title: string;
380
+ /**
381
+ * The title split into the lines the renderer paints, one `<text>` per
382
+ * line. Present when the painted lines differ from `[title]`:
383
+ *
384
+ * - the title word-wrapped INSIDE the bar (`length === 2`, at most
385
+ * `ITEM_CAPTION_TITLE_MAX_LINES`), or
386
+ * - the title carries explicit `\n` breaks (any `length >= 2`, in-bar
387
+ * or spilled to the right of the bar). Lines are trimmed, leading
388
+ * and trailing empty lines are dropped, interior empty lines stay as
389
+ * blank entries. An explicit title never auto-wraps.
390
+ *
391
+ * Omitted for a one-line title, and for a break-free title that spilled
392
+ * to the right (always one line), unless the caption cap at the chart's
393
+ * end truncated it (`titleTruncated`). Line `n` (0-based) sits at baseline
394
+ * `ITEM_CAPTION_TITLE_BASELINE_OFFSET_PX + n * ITEM_CAPTION_TITLE_LINE_HEIGHT_PX`
395
+ * from the bar's top, and the meta line follows at
396
+ * `itemCaptionMetaBaselineOffset(titleLines.length)`. A multi-line
397
+ * caption with a meta line grows the bar (`box.height`) by 16px per
398
+ * extra line.
399
+ */
400
+ titleLines?: string[];
401
+ /**
402
+ * True when a spilled caption that would run past the axis end was
403
+ * capped at the overhang budget past it: `titleLines` carries the lines truncated
404
+ * with `…`, and the renderer gives the bar the full `title` as its
405
+ * `<title>` tooltip (specs/rendering.md § "Caption cap at the chart's
406
+ * end"). Omitted otherwise.
407
+ */
408
+ titleTruncated?: true;
254
409
  box: BoundingBox;
410
+ /**
411
+ * The item's dates in x: its scheduled start and end through the time
412
+ * scale. The bar (`box`) is drawn inside them, a seam in from each
413
+ * (specs/true-width-layout.md §3.1); dependency arrows and milestone
414
+ * slack lines read them (§3.4, §3.5).
415
+ */
416
+ dateSpan: { startX: number; endX: number };
255
417
  status: StatusKind;
418
+ /**
419
+ * The status dot's resolved colour: the theme's `statusDot.onLight` or
420
+ * `onDark` entry for `status`, picked by the luminance of `style.bg`
421
+ * (`statusDotColor` in `status-dot.ts`).
422
+ */
423
+ statusDotColor: string;
256
424
  progressFraction: number; // 0..1; 1 == fully filled
257
425
  footnoteIndicators: number[]; // 1-based superscript numbers, empty when no footnotes
258
426
  labelChips: PositionedLabelChip[];
@@ -286,12 +454,14 @@ export interface PositionedItem {
286
454
  // renderer stays palette-and-string-dumb.
287
455
  metaText?: string;
288
456
  // True when the title OR the meta line is wider than the bar's inner
289
- // padded width. We treat title + meta as an atomic block: if either
457
+ // padded width AND a word-wrap of the title can't make it fit (see
458
+ // `titleLines`). We treat title + meta as an atomic block: if either
290
459
  // one wouldn't fit inside the bar, BOTH get drawn beside the bar
291
460
  // (stacked, just past its right edge) so they read as one caption
292
461
  // rather than splitting across the bar boundary. The layout also
293
462
  // bumps the next item to a fresh row so the spilled caption has
294
- // empty space to occupy.
463
+ // empty space to occupy. A title that wrapped inside the bar has
464
+ // `textSpills === false` and reserves no spill extent.
295
465
  textSpills: boolean;
296
466
  /** True when the bar is too narrow to host the status dot inside
297
467
  * with its full inset (`MIN_BAR_WIDTH_FOR_DOT_PX`). The dot
@@ -342,6 +512,32 @@ export interface PositionedItem {
342
512
  */
343
513
  size: ResolvedSize | null;
344
514
  style: ResolvedStyle;
515
+ /**
516
+ * Wave membership (specs/waves.md §8.7): `member` for an item with an
517
+ * effective wave, `background` for one without. Set on every item, in
518
+ * the main lanes and in isolated regions, when the roadmap declares
519
+ * waves; omitted otherwise.
520
+ */
521
+ waveRole?: 'member' | 'background';
522
+ /**
523
+ * The hatch kind for the bar's fill (`waveHatchKind` in
524
+ * `wave-hatch.ts`). Set exactly on background items; omitted otherwise.
525
+ */
526
+ waveHatch?: WaveHatchKind;
527
+ /**
528
+ * The hover tooltip line wave membership adds to the bar, localized
529
+ * (`Background (no wave)`; specs/waves.md §9.3). Set only on
530
+ * background items; omitted otherwise.
531
+ */
532
+ waveTooltip?: string;
533
+ /**
534
+ * Present when the item's `after:DATE` pin falls on a non-working day
535
+ * and that pin set the start (NL.I1008, specs/working-calendar.md §6):
536
+ * `key` / `pin` are the pin as written, `start` the working day the
537
+ * item starts on (YYYY-MM-DD). Omitted when a later `after:` ref, the
538
+ * lane cursor or a wave floor set the start.
539
+ */
540
+ nonWorkingPin?: { key: 'after'; pin: string; start: string };
345
541
  }
346
542
 
347
543
  /**
@@ -359,6 +555,15 @@ export interface PositionedCapacity {
359
555
  text: string;
360
556
  /** Resolved glyph instruction, or `null` when the icon is `'none'`. */
361
557
  icon: ResolvedCapacityIconRef | null;
558
+ /**
559
+ * For a built-in SVG icon (person, people, points, time): the px terms
560
+ * that, added in order to the start x of the text run the suffix is
561
+ * painted in (the item caption x, or the lane badge x), give the icon's
562
+ * x, from `capacityIconDxTerms`. Kept apart so the sum rounds the same
563
+ * as adding each width to x in turn. Omitted for every other glyph,
564
+ * which paints inside the `<text>`.
565
+ */
566
+ iconDxTerms?: readonly number[];
362
567
  }
363
568
 
364
569
  /**
@@ -384,20 +589,36 @@ export interface PositionedGroup {
384
589
  kind: 'group';
385
590
  id?: string;
386
591
  title?: string;
592
+ /** The whole container: `box.y` is the header's top, `box.x` and
593
+ * `box.width` span exactly its children's dates
594
+ * (specs/true-width-layout.md §3.3). */
387
595
  box: BoundingBox;
388
- // Bracket is drawn on the left edge when style.bracket != 'none'.
596
+ /** Header band height above the body (title, glyphs, bracket); 0 when
597
+ * there is no header. The body runs from `box.y + headerHeight` to the
598
+ * box's bottom. */
599
+ headerHeight: number;
389
600
  children: PositionedTrackChild[];
390
601
  /** Inline-date pins attached to this group (`after:DATE` / `before:DATE`).
391
602
  * See `InlineDatePin` and specs/rendering.md "Inline-date glyph". */
392
603
  inlineDatePins?: InlineDatePin[];
393
604
  style: ResolvedStyle;
605
+ /**
606
+ * True for an untitled group with no `style:` and no `labels:` that
607
+ * carries `wave:`, in a roadmap with waves: it only assigns wave
608
+ * membership, so the renderer draws nothing for it (specs/waves.md §9.7).
609
+ * Omitted otherwise.
610
+ */
611
+ waveOnly?: true;
394
612
  }
395
613
 
396
614
  export interface PositionedParallel {
397
615
  kind: 'parallel';
398
616
  id?: string;
399
617
  title?: string;
618
+ /** The whole container, as `PositionedGroup.box`. */
400
619
  box: BoundingBox;
620
+ /** Header band height above the body, as `PositionedGroup.headerHeight`. */
621
+ headerHeight: number;
401
622
  children: PositionedTrackChild[]; // sub-tracks stacked vertically
402
623
  /** Inline-date pins attached to this parallel (`after:DATE` / `before:DATE`).
403
624
  * See `InlineDatePin` and specs/rendering.md "Inline-date glyph". */
@@ -416,8 +637,15 @@ export interface PositionedSwimlane {
416
637
  nested: PositionedSwimlane[]; // recursive sub-swimlanes
417
638
  style: ResolvedStyle;
418
639
  // Owner display string ("Platform Team", "Sam Chen") rendered inside
419
- // the frame tab. Resolved from team/person id → title.
640
+ // the frame tab. Resolved from team/person id → title (nested teams
641
+ // included); the bare id when it names neither.
420
642
  owner?: string;
643
+ /**
644
+ * What `owner` names, for the glyph that leads it in the frame tab:
645
+ * `person` for a declared person, `team` otherwise (a team, or an id
646
+ * that names neither). Set exactly when `owner` is.
647
+ */
648
+ ownerKind?: 'person' | 'team';
421
649
  // Footnote indicator numbers attached to this swimlane (via `on:` in the
422
650
  // footnote declaration). Rendered in the upper-right of the frame tab.
423
651
  footnoteIndicators: number[];
@@ -434,6 +662,13 @@ export interface PositionedSwimlane {
434
662
  * want the badge fully hidden simply omit `capacity:`.
435
663
  */
436
664
  capacity: PositionedCapacity | null;
665
+ /**
666
+ * The frame tab's geometry, from `frameTabGeometry` with the lane's
667
+ * box x, its title, the owner display title and the capacity badge and
668
+ * footnote-indicator widths. Layout sizes the tab's collision box from
669
+ * it and the renderer draws it as is. Null when the lane has no title.
670
+ */
671
+ tab: FrameTabGeometry | null;
437
672
  /**
438
673
  * Tri-state utilization underline data when the lane declares
439
674
  * `capacity:` AND has at least one item contributing load AND has not
@@ -512,6 +747,12 @@ export interface PositionedAnchor {
512
747
  // (start-anchored text) — no further geometry decisions.
513
748
  labelBox: BoundingBox;
514
749
  labelSide: 'left' | 'right';
750
+ /**
751
+ * The anchor's `date:` (YYYY-MM-DD) when it falls on a non-working day
752
+ * the axis hides; the renderer surfaces it as a `<title>`. Omitted on a
753
+ * working day and under a calendar with no non-working days.
754
+ */
755
+ hiddenDate?: string;
515
756
  }
516
757
 
517
758
  export interface PositionedMilestone {
@@ -526,6 +767,16 @@ export interface PositionedMilestone {
526
767
  // milestone has zero or one predecessor.
527
768
  slackArrows?: Array<{ x: number; y: number }>;
528
769
  isOverrun: boolean; // true when the aggregated predecessor end exceeds `date:`
770
+ /**
771
+ * Where an overrun dated milestone actually lands: the end of its
772
+ * latest predecessor or overrunning wave. `center` stays on the
773
+ * planned date; the renderer draws that diamond in the overrun colour,
774
+ * a dotted span on the same row to a second diamond at `slip.x`, and
775
+ * puts the label and the cut line there (specs/rendering.md,
776
+ * Milestones). Omitted when the milestone does not overrun, or when
777
+ * the two diamonds would meet.
778
+ */
779
+ slip?: { x: number };
529
780
  style: ResolvedStyle;
530
781
  // Vertical span of the milestone's cut line through the swimlane area.
531
782
  cutTopY: number;
@@ -533,6 +784,31 @@ export interface PositionedMilestone {
533
784
  // See PositionedAnchor.labelBox — same packing logic applies.
534
785
  labelBox: BoundingBox;
535
786
  labelSide: 'left' | 'right';
787
+ /**
788
+ * True when the diamond sits on a wave boundary (within 0.5 px of any
789
+ * `S_k` or `E_k` other than the origin; specs/waves.md §9.2). The
790
+ * boundary line carries the vertical, so the cut line is not drawn.
791
+ * Omitted when false and when the roadmap has no waves.
792
+ */
793
+ onWaveBoundary?: boolean;
794
+ /**
795
+ * The wave that runs past this dated milestone: its `after:` lists the
796
+ * wave and the wave's end `E_k` lies past `date:` (NL.I1007). The
797
+ * latest-ending such wave; omitted when none.
798
+ */
799
+ overrunByWave?: string;
800
+ /**
801
+ * The milestone's own `date:` (YYYY-MM-DD), set together with
802
+ * `overrunByWave`, so NL.I1007 never reads the date back off x.
803
+ */
804
+ overrunDate?: string;
805
+ /**
806
+ * The milestone's `date:` (YYYY-MM-DD) when it falls on a non-working
807
+ * day the axis hides; the renderer surfaces it as a `<title>`. Omitted
808
+ * on a working day, for after-only milestones, and under a calendar
809
+ * with no non-working days.
810
+ */
811
+ hiddenDate?: string;
536
812
  }
537
813
 
538
814
  /**
@@ -559,7 +835,7 @@ export interface MarkerRowPlacement {
559
835
  export interface SlackCorridor {
560
836
  xStart: number; // slack pred's right edge (logical chart x)
561
837
  xEnd: number; // binding pred's right edge / milestone center.x
562
- y: number; // slack pred's row midpoint
838
+ y: number; // slack pred's slack-arrow attach y, in the chart's local frame
563
839
  slackPredId: string; // exempt from bumping (owns the arrow's origin)
564
840
  milestoneId: string;
565
841
  }
@@ -567,19 +843,23 @@ export interface SlackCorridor {
567
843
  export interface PositionedDependencyEdge {
568
844
  fromId: string;
569
845
  toId: string;
570
- waypoints: Point[]; // first = source port; last = target port
571
- /**
572
- * - `normal` — orthogonal arrow drawn AFTER swimlane / item /
573
- * marker fills so it sits on top of lane bands.
574
- * - `overflow` — currently unused at construction; reserved for the
575
- * red `before:` overrun annotation arrow.
576
- * - `underBar` — channel router could not find a clear vertical
577
- * gutter between the source and target columns; the arrow's
578
- * vertical leg crosses one or more item bars. The renderer paints
579
- * these edges BEFORE bar fills with a thinner stroke so the bar
580
- * stays the visual foreground.
581
- */
582
- kind: 'normal' | 'overflow' | 'underBar';
846
+ /**
847
+ * Orthogonal path on the dates (specs/true-width-layout.md §3.4): first
848
+ * = source port, last = target port, where the arrowhead's tip lands.
849
+ * Item and container sources route vertical, horizontal (in a gap
850
+ * between rows), vertical; a target on its source's row and a marker
851
+ * stub are one horizontal leg.
852
+ */
853
+ waypoints: Point[];
854
+ /** The target waits on its source: the horizontal legs are dotted. */
855
+ slack: boolean;
856
+ /**
857
+ * Arrowhead width across its leg, scaled down to the target's width;
858
+ * a target narrower than `ARROW_HEAD_MIN_PX` gets a dot that wide
859
+ * (`head: 'dot'`) instead (§8 O6).
860
+ */
861
+ headWidth: number;
862
+ head: 'arrow' | 'dot';
583
863
  style: ResolvedStyle;
584
864
  }
585
865
 
@@ -597,12 +877,40 @@ export interface PositionedFootnoteArea {
597
877
  }
598
878
 
599
879
  export interface PositionedFootnoteEntry {
880
+ /**
881
+ * The title of the isolated region the footnote comes from; the
882
+ * renderer heads each region's run of entries with it. Omitted on the
883
+ * host's own footnotes.
884
+ */
885
+ region?: string;
886
+ /**
887
+ * Baseline of the region's heading row, on the first entry of each
888
+ * region's run only.
889
+ */
890
+ regionHeadingY?: number;
600
891
  number: number;
892
+ /** Baseline of the entry's first row, where its number sits. */
893
+ y: number;
601
894
  title: string;
895
+ /** The title's rows: one unless it is wider than the panel. */
896
+ titleLines: PositionedFootnoteLine[];
602
897
  description?: string;
898
+ /**
899
+ * The description's rows, the first led by `— `: beside the title when
900
+ * its first word fits there, the rest wrapped at the title's x. Omitted
901
+ * when the footnote has no description.
902
+ */
903
+ descriptionLines?: PositionedFootnoteLine[];
603
904
  style: ResolvedStyle;
604
905
  }
605
906
 
907
+ /** One row of footnote text, at its baseline. */
908
+ export interface PositionedFootnoteLine {
909
+ x: number;
910
+ y: number;
911
+ text: string;
912
+ }
913
+
606
914
  export interface PositionedIncludeRegion {
607
915
  sourcePath: string; // relative to the parent file
608
916
  label: string; // e.g. the child roadmap's title or basename
@@ -612,6 +920,193 @@ export interface PositionedIncludeRegion {
612
920
  // tick row above the region.
613
921
  nestedSwimlanes: PositionedSwimlane[];
614
922
  style: ResolvedStyle;
923
+ /**
924
+ * Wave crossings over this region's background bars (specs/waves.md
925
+ * §9.3, §9.8), re-emitted inside the region because its opaque fill
926
+ * hides the global layers. Omitted when there are none.
927
+ */
928
+ waveCrossings?: PositionedWaveCrossing[];
929
+ /**
930
+ * The region's own anchors and milestones, in its own marker row
931
+ * under the label tab; their cut lines stop at its last lane. Omitted
932
+ * when it has none to draw.
933
+ */
934
+ anchors?: PositionedAnchor[];
935
+ milestones?: PositionedMilestone[];
936
+ /**
937
+ * The region's own dependency edges, routed among its lanes. Omitted
938
+ * when it has none.
939
+ */
940
+ edges?: PositionedDependencyEdge[];
941
+ }
942
+
943
+ /**
944
+ * One wave's solved span (specs/waves.md §5.1, §8.7), in declaration order.
945
+ * Only present in a roadmap that declares waves.
946
+ */
947
+ export interface PositionedWave {
948
+ id: string;
949
+ /** The wave's title, else its id. */
950
+ title: string;
951
+ /** 0-based declaration order. */
952
+ index: number;
953
+ /** `S_k` in logical px. */
954
+ startX: number;
955
+ /** `E_k` in logical px; equals `startX` for an empty wave. */
956
+ endX: number;
957
+ /** `S_k` as a whole UTC day. */
958
+ startDate: Date;
959
+ /** `E_k` as a whole UTC day, exclusive. */
960
+ endDate: Date;
961
+ /** Members placed in the main lanes and first-level isolated regions. */
962
+ memberCount: number;
963
+ /** True when the wave has no members (it spans no time). */
964
+ empty: boolean;
965
+ /**
966
+ * The member that sets `E_k` (its id, else its title). Omitted when no
967
+ * member's end equals `E_k` with `E_k > S_k` (§5.1 "Held by").
968
+ */
969
+ heldBy?: string;
970
+ /** The wave's `after:` value that set `S_k`, when it beat `E_{k-1}`. */
971
+ floorRef?: string;
972
+ /**
973
+ * 0-based position among the non-empty waves; drives the strip-cell
974
+ * alternation (even: `wave.stripFill`, odd: `wave.stripFillAlt`), so
975
+ * empty waves and gaps never break it. Omitted for an empty wave.
976
+ */
977
+ visibleOrdinal?: number;
978
+ /** The wave's column: `startX..endX` × `chartTopY..swimlaneBottomY`. */
979
+ columnBox: BoundingBox;
980
+ /** The wave's strip cell (specs/waves.md §9.1). */
981
+ strip: PositionedWaveStrip;
982
+ /** Resolved colours (specs/waves.md §9.4). */
983
+ style: PositionedWaveStyle;
984
+ /** 1-based numbers of the footnotes whose `on:` names this wave, ascending. */
985
+ footnoteIndicators: number[];
986
+ }
987
+
988
+ /** One wave's strip cell (specs/waves.md §9.1). */
989
+ export interface PositionedWaveStrip {
990
+ /** `startX..endX` at `waveStrip.y`; zero width for an empty wave. */
991
+ box: BoundingBox;
992
+ /** The label text; omitted when `labelKind` is `none`. */
993
+ label?: string;
994
+ /**
995
+ * The painted cell: `box` clipped to the timeline's backing panel and
996
+ * inset by `WAVE_CELL_INSET_PX` (half the panel's stroke) so it never
997
+ * covers the panel border. Omitted for an empty wave and for one that
998
+ * lies outside the timeline.
999
+ */
1000
+ cell?: BoundingBox;
1001
+ /**
1002
+ * Where the label is drawn when the wave has a `cell`: `x` is the text
1003
+ * centre (`labelX`, moved left when the superscripts would cover it)
1004
+ * and `halo` the backing box in the cell's fill. Omitted when there is
1005
+ * no label or no cell.
1006
+ */
1007
+ labelDraw?: { x: number; halo: BoundingBox };
1008
+ /**
1009
+ * The calendar glyph for a declared date floor (`wave launch after:2026-11-02`),
1010
+ * at the cell's left, vertically centred in the strip, whether or not the
1011
+ * floor opened a gap. The label clears it. Omitted when the wave has no
1012
+ * date floor, no `cell`, or the cell is too narrow to host it.
1013
+ */
1014
+ inlineDatePin?: InlineDatePin;
1015
+ /**
1016
+ * The footnote superscripts when `footnotesShown` and the wave has a
1017
+ * `cell`: `rightX` is the last superscript's right edge (they walk left
1018
+ * by `ITEM_FOOTNOTE_INDICATOR_STEP_PX`, end-anchored) and `halo` their
1019
+ * backing box. Omitted otherwise.
1020
+ */
1021
+ superscripts?: { rightX: number; halo: BoundingBox };
1022
+ /**
1023
+ * Which step of the fit chain won: the title, the id, the title
1024
+ * ellipsized to 3 or more characters plus "…", the 1-based `#k`, or
1025
+ * no label. Always `none` for an empty wave (it has no cell).
1026
+ */
1027
+ labelKind: 'title' | 'id' | 'ellipsis' | 'ordinal' | 'none';
1028
+ /**
1029
+ * Centre of the visible part of the span (clipped to the timeline);
1030
+ * `startX` for an empty wave, which is not drawn.
1031
+ */
1032
+ labelX: number;
1033
+ /**
1034
+ * The cell's hover text, e.g. `Launch · 2026-02-02 – 2026-02-23 · held
1035
+ * by i2` (end exclusive). An empty wave reads `title · date · no items`.
1036
+ */
1037
+ tooltip: string;
1038
+ /**
1039
+ * True when `footnoteIndicators` are drawn right-aligned in the cell;
1040
+ * false when there are none or they did not fit (they are then not
1041
+ * drawn; the footnote panel still lists the footnote).
1042
+ */
1043
+ footnotesShown: boolean;
1044
+ /**
1045
+ * The floor reference (`fy-budget`, `2026-02-02`) centred in the gap
1046
+ * this wave's start floor opened before it (`S_k > E_{k-1}`, k >= 2),
1047
+ * when it fits; drawn in muted italics. `halo` is its backing box in the
1048
+ * panel's fill, sized from the text's estimated width at 10 px (the one
1049
+ * its fit was tested at). Omitted otherwise.
1050
+ */
1051
+ gapLabel?: { text: string; x: number; halo: BoundingBox };
1052
+ }
1053
+
1054
+ /** A wave's resolved colours (specs/waves.md §9.4). */
1055
+ export interface PositionedWaveStyle {
1056
+ /** The style's `bg`: the column tint and the strip-cell overlay. Omitted when unset. */
1057
+ tint?: string;
1058
+ /** The cell's base fill by `visibleOrdinal` parity. */
1059
+ stripFill: string;
1060
+ /** Label colour: the style's `text`, else `wave.labelText`, else a dark or light pick at 4.5:1. */
1061
+ text: string;
1062
+ /** Boundary colour: the style's `fg`, else `wave.boundary`. */
1063
+ boundary: string;
1064
+ /** `4 2` for `border:dashed`, `1 2` for `dotted`, null for solid. */
1065
+ boundaryDash: string | null;
1066
+ }
1067
+
1068
+ /**
1069
+ * One wave boundary line (specs/waves.md §9.2), at a distinct x in
1070
+ * `{S_k} ∪ {E_k}` of the non-empty waves other than the origin. It runs
1071
+ * from the top of the strip down to `swimlaneBottomY`.
1072
+ */
1073
+ export interface PositionedWaveBoundary {
1074
+ x: number;
1075
+ topY: number;
1076
+ bottomY: number;
1077
+ stroke: string;
1078
+ dash: string | null;
1079
+ }
1080
+
1081
+ /**
1082
+ * A boundary drawn over a background bar whose visual extent strictly
1083
+ * contains it (specs/waves.md §9.3), from the bar's top to its bottom.
1084
+ */
1085
+ export interface PositionedWaveCrossing {
1086
+ x: number;
1087
+ topY: number;
1088
+ bottomY: number;
1089
+ stroke: string;
1090
+ }
1091
+
1092
+ /** One legend entry (specs/waves.md §9.5). */
1093
+ export interface WaveLegendEntry {
1094
+ /** The hatch swatch for background work, the legend's only entry. */
1095
+ kind: 'background';
1096
+ /** The entry's full text. */
1097
+ text: string;
1098
+ /** The swatch box: a hatched bar. */
1099
+ swatch: BoundingBox;
1100
+ /** The swatch's hatch kind, a default item bar's fill. */
1101
+ hatch: WaveHatchKind;
1102
+ /** The text, one run on the swatch's line. `y` is the text baseline. */
1103
+ runs: Array<{ text: string; x: number; y: number }>;
1104
+ }
1105
+
1106
+ /** The wave legend below the chart (specs/waves.md §9.5). */
1107
+ export interface PositionedWaveLegend {
1108
+ box: BoundingBox;
1109
+ entries: WaveLegendEntry[];
615
1110
  }
616
1111
 
617
1112
  // Top-level result handed to the renderer.
@@ -638,4 +1133,18 @@ export interface PositionedRoadmap {
638
1133
  includes: PositionedIncludeRegion[];
639
1134
  // Frame (chart area) in chart-space. Useful for renderer overlays.
640
1135
  chartBox: BoundingBox;
1136
+ /**
1137
+ * The wave barrier driver's result (specs/waves.md §8.7): the passes it
1138
+ * ran and whether it hit the `n + 1` cap (NL.W1002, invalid input).
1139
+ * Present only when the roadmap declares waves.
1140
+ */
1141
+ waveSolve?: { passes: number; capped: boolean };
1142
+ /** The waves in declaration order; present only when the roadmap declares waves. */
1143
+ waves?: PositionedWave[];
1144
+ /** Wave boundary lines, left to right; omitted when there are none. */
1145
+ waveBoundaries?: PositionedWaveBoundary[];
1146
+ /** Crossings over the main lanes' background bars; omitted when there are none. */
1147
+ waveCrossings?: PositionedWaveCrossing[];
1148
+ /** The wave legend; present only when one of its triggers holds (§9.5). */
1149
+ waveLegend?: PositionedWaveLegend;
641
1150
  }