@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
@@ -64,13 +64,19 @@ export const lightTheme: Theme = {
64
64
  shadow: 'subtle',
65
65
  cornerRadius: 'sm',
66
66
  },
67
+ // Containers shade their body with `bg` as a light tint
68
+ // (CONTAINER_BODY_FILL_OPACITY); groups and parallels get distinct
69
+ // ones, and a parallel's header bracket is dashed, a group's solid
70
+ // (specs/true-width-layout.md §3.3).
67
71
  parallel: {
68
72
  ...baseEntity,
69
- bracket: 'none',
73
+ bg: '#635cf1',
74
+ bracket: 'dashed',
70
75
  padding: 'xs',
71
76
  },
72
77
  group: {
73
78
  ...baseEntity,
79
+ bg: '#ea9818',
74
80
  bracket: 'solid',
75
81
  padding: 'xs',
76
82
  fg: '#475569',
@@ -127,6 +133,8 @@ export const lightTheme: Theme = {
127
133
  timeline: {
128
134
  gridLine: '#cbd5e1',
129
135
  minorGridLine: '#e2e8f0',
136
+ nonWorkingSeam: '#a0aec0',
137
+ nonWorkingFill: '#64748b',
130
138
  tickMark: '#cbd5e1',
131
139
  labelText: '#64748b',
132
140
  panelFill: '#ffffff',
@@ -199,6 +207,15 @@ export const lightTheme: Theme = {
199
207
  badgeStroke: '#cbd5e1',
200
208
  badgeText: '#475569',
201
209
  },
210
+ wave: {
211
+ stripFill: '#f0fdfa',
212
+ stripFillAlt: '#ccfbf1',
213
+ labelText: '#134e4a',
214
+ labelMuted: '#0f766e',
215
+ boundary: '#0d9488',
216
+ hatch: '#0f172a',
217
+ hatchOnDark: '#ffffff',
218
+ },
202
219
  arrowhead: {
203
220
  neutral: '#475569',
204
221
  light: '#94a3b8',
@@ -212,6 +229,16 @@ export const lightTheme: Theme = {
212
229
  planned: '#94a3b8',
213
230
  neutral: '#94a3b8',
214
231
  },
232
+ // Item bar fill per status, applied by layout when the item's resolved
233
+ // `bg` is still `entities.item.bg` (m2d Resolution 3).
234
+ statusTint: {
235
+ done: '#ecfdf5',
236
+ inProgress: '#eff6ff',
237
+ atRisk: '#fffbeb',
238
+ blocked: '#fee2e2',
239
+ planned: '#f8fafc',
240
+ neutral: '#f8fafc',
241
+ },
215
242
  // Status-dot palettes — the renderer picks one based on the
216
243
  // bar's effective bg luminance.
217
244
  //
@@ -82,6 +82,15 @@ export interface Theme {
82
82
  // when the roadmap's resolved `minor-grid` style is `true`. A step
83
83
  // lighter than `gridLine` so the major lines still dominate.
84
84
  minorGridLine: string;
85
+ // Faint dotted seam where hidden non-working days collapse between
86
+ // two working-day columns and no grid line already marks the spot
87
+ // (specs/working-calendar.md §7.4). Darker than `gridLine`, so it
88
+ // reads as a join rather than a column boundary.
89
+ nonWorkingSeam: string;
90
+ // Fill of a non-working band under the `show` display, painted at
91
+ // `NON_WORKING_FILL_OPACITY` over the row tints (specs/working-
92
+ // calendar.md §7.3): a faint shade a grid line still reads over.
93
+ nonWorkingFill: string;
85
94
  tickMark: string;
86
95
  labelText: string;
87
96
  // m2.5d: lifted from renderTimeline.
@@ -160,6 +169,22 @@ export interface Theme {
160
169
  badgeStroke: string;
161
170
  badgeText: string;
162
171
  };
172
+ /**
173
+ * Wave tokens (specs/waves.md section 9.10). `stripFill` and
174
+ * `stripFillAlt` alternate across the wave strip cells; `labelText`
175
+ * and `labelMuted` paint strip labels and secondary text; `boundary`
176
+ * is the vertical wave boundary line; `hatch` and `hatchOnDark` are
177
+ * the background-work hatch stroke on light and dark bar fills.
178
+ */
179
+ wave: {
180
+ stripFill: string;
181
+ stripFillAlt: string;
182
+ labelText: string;
183
+ labelMuted: string;
184
+ boundary: string;
185
+ hatch: string;
186
+ hatchOnDark: string;
187
+ };
163
188
  // m2.5d: lifted from renderEdge marker defs.
164
189
  arrowhead: {
165
190
  neutral: string;
@@ -175,6 +200,19 @@ export interface Theme {
175
200
  planned: string;
176
201
  neutral: string;
177
202
  };
203
+ /**
204
+ * Item bar fill per status when the item's resolved `bg` is the theme's
205
+ * item default (`entities.item.bg`); an explicit `bg:` wins. Same keys as
206
+ * `status`, which colors the border the same way for `entities.item.fg`.
207
+ */
208
+ statusTint: {
209
+ done: string;
210
+ inProgress: string;
211
+ atRisk: string;
212
+ blocked: string;
213
+ planned: string;
214
+ neutral: string;
215
+ };
178
216
  /**
179
217
  * Upper-right status-dot colors. Two palettes — the renderer
180
218
  * picks `onLight` for bars whose effective bg is light/pale and
@@ -21,15 +21,6 @@ export const PADDING_PX: Record<'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', numbe
21
21
  xl: 32,
22
22
  };
23
23
 
24
- export const HEADER_HEIGHT_PX: Record<'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {
25
- none: 0,
26
- xs: 24,
27
- sm: 36,
28
- md: 56,
29
- lg: 80,
30
- xl: 112,
31
- };
32
-
33
24
  export const TEXT_SIZE_PX: Record<'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {
34
25
  none: 0,
35
26
  xs: 10,
@@ -63,8 +54,10 @@ export const LOGO_SIZE_PX: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {
63
54
  // `defaultRowBand()` keeps the legacy 64/56 split byte-stable; new
64
55
  // callers should consume `bandScale.step()` and `bandScale.bandwidth()`.
65
56
 
66
- // Minimum item bar width so zero-duration items remain visible.
67
- export const MIN_ITEM_WIDTH = 8;
57
+ // Minimum painted bar width: a bar spans exactly its dates, so this floor
58
+ // only keeps a sub-pixel or zero-duration item visible
59
+ // (specs/true-width-layout.md §3.1).
60
+ export const MIN_ITEM_WIDTH = 1;
68
61
 
69
62
  // Height (px) of the bottom progress strip drawn along the item bar's
70
63
  // bottom edge. Single source of truth used by:
@@ -80,28 +73,43 @@ export const MIN_ITEM_WIDTH = 8;
80
73
  export const PROGRESS_STRIP_HEIGHT_PX = 4;
81
74
 
82
75
  // Timeline tick header band — a single row that holds the date labels
83
- // (e.g. "Jan 05", "Feb 14"). Layout sizes the panel; the renderer
84
- // paints the labels at a fixed baseline offset from the panel's top.
85
- //
86
- // Bumping the panel height without re-centering the baseline mis-centers
87
- // the dates — keep both knobs together so a change to either one is
88
- // visible at a glance.
89
- export const TIMELINE_TICK_PANEL_HEIGHT_PX = 24;
76
+ // (e.g. "Jan 05", "Feb 14"). The roadmap's `header-height` style key
77
+ // picks the panel height from this table; `none` drops the panel (top
78
+ // and mirrored bottom) entirely. Layout sizes the panel; the renderer
79
+ // paints the labels at `timelineTickLabelBaselineOffsetPx(height)` from
80
+ // the panel's top, so the dates stay centered at every bucket.
81
+ export const HEADER_HEIGHT_PX: Record<'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {
82
+ none: 0,
83
+ xs: 16,
84
+ sm: 20,
85
+ md: 24,
86
+ lg: 32,
87
+ xl: 40,
88
+ };
89
+
90
+ /** Tick panel height at the default `header-height:md`. */
91
+ export const TIMELINE_TICK_PANEL_HEIGHT_PX = HEADER_HEIGHT_PX.md;
90
92
 
91
93
  /**
92
- * Baseline Y of the tick-label text relative to the tick panel's top.
93
- * Approximately vertically centers a 10 pt label in the panel.
94
+ * Baseline Y of the tick-label text relative to the tick panel's top,
95
+ * for a panel `panelHeightPx` tall. Approximately vertically centers a
96
+ * 10 pt label: half the panel plus ~3 px of descender allowance.
94
97
  */
95
- export const TIMELINE_TICK_LABEL_BASELINE_OFFSET_PX = 15;
98
+ export function timelineTickLabelBaselineOffsetPx(panelHeightPx: number): number {
99
+ return Math.round(panelHeightPx / 2 + 3);
100
+ }
101
+
102
+ /** Tick-label baseline offset at the default panel height. */
103
+ export const TIMELINE_TICK_LABEL_BASELINE_OFFSET_PX = timelineTickLabelBaselineOffsetPx(
104
+ TIMELINE_TICK_PANEL_HEIGHT_PX,
105
+ );
96
106
 
97
107
  /**
98
- * Horizontal breathing room (px) added to a track-block's right edge
99
- * after sequencing. Applied to `group { ... }` and `parallel { ... }`
100
- * blocks so successive blocks don't crowd each other. Items butt up
101
- * against their neighbors (no gutter) — only block boundaries get this
102
- * gutter.
108
+ * Font size of the tick labels. Layout reads it to decide whether a
109
+ * partial edge column is wide enough to hold its label; the renderer
110
+ * paints at the same size.
103
111
  */
104
- export const TRACK_BLOCK_TAIL_GUTTER_PX = 8;
112
+ export const TIMELINE_TICK_LABEL_FONT_SIZE_PX = 10;
105
113
 
106
114
  // Frame-tab chiclet — the rounded label tab that overhangs a swimlane
107
115
  // frame and the dashed include-region border. Both surfaces share the
@@ -117,60 +125,6 @@ export const FRAME_TAB_HEIGHT_PX = 22;
117
125
  /** Baseline Y of the chiclet label relative to the tab's top edge. */
118
126
  export const FRAME_TAB_LABEL_BASELINE_OFFSET_PX = 15;
119
127
 
120
- // Group title chiclet — the small label tab anchored flush in the upper-left
121
- // corner of a styled group's bounding box. Smaller than the frame-tab so it
122
- // reads as a "container label" rather than a sibling of the swimlane tab.
123
- //
124
- // The chiclet's top edge aligns with the group box's top edge and its left
125
- // edge aligns with the box's left edge (no overhang). The group reserves
126
- // `GROUP_TITLE_TAB_HEIGHT_PX + GROUP_TITLE_TAB_GUTTER_PX` of vertical top
127
- // padding inside the box before the first child row begins, so the chiclet
128
- // never overlaps content. Bumping the height without re-tuning
129
- // `GROUP_TITLE_TAB_LABEL_BASELINE_OFFSET_PX` mis-centers the label.
130
-
131
- /** Height (px) of the group-title chiclet anchored at the box's upper-left. */
132
- export const GROUP_TITLE_TAB_HEIGHT_PX = 16;
133
-
134
- /** Horizontal padding (px) on each side of the title text inside the chiclet. */
135
- export const GROUP_TITLE_TAB_PAD_X_PX = 6;
136
-
137
- /** Vertical gap (px) between the chiclet's bottom edge and the first inner row. */
138
- export const GROUP_TITLE_TAB_GUTTER_PX = 4;
139
-
140
- /** Baseline Y of the chiclet label relative to the chiclet's top edge.
141
- * Tuned for a 9 pt bold glyph roughly vertically centered in a 16 px tab. */
142
- export const GROUP_TITLE_TAB_LABEL_BASELINE_OFFSET_PX = 11;
143
-
144
- /** Font size (px) of the group-title chiclet label. */
145
- export const GROUP_TITLE_TAB_LABEL_FONT_SIZE_PX = 9;
146
-
147
- /** Approximate per-character width factor used to size the chiclet to its
148
- * title text. Mirrors the constant used in the renderer's pre-port code
149
- * so the chiclet hugs short titles without clipping long ones. */
150
- export const GROUP_TITLE_TAB_CHAR_WIDTH_PX = 5.5;
151
-
152
- /** Padding (px) reserved on the inside-bottom of a styled group box before
153
- * its lower stroke. Symmetric with the chiclet gutter so children breathe
154
- * on both ends. */
155
- export const GROUP_BOTTOM_PAD_PX = 4;
156
-
157
- /**
158
- * Vertical space (px) reserved ABOVE a bracket-style (no-fill) group's
159
- * box to host its overhanging title label. Bracket groups paint their
160
- * label at `box.y - 2` (baseline) in the renderer — the glyph extent
161
- * lives entirely ABOVE box.y. Without an explicit reservation, that
162
- * overhang collides with the previous sibling's bracket-foot when two
163
- * bracket-titled groups stack in a parallel (the foot ends right where
164
- * the next label's top would render). The group shifts its own box.y
165
- * down by this amount so the label has clear space above it; the
166
- * caller advances by `bracketLabelOverhang + box.height + interRowGap`.
167
- *
168
- * Filled-style ("chiclet") groups use `GROUP_TITLE_TAB_HEIGHT_PX +
169
- * GROUP_TITLE_TAB_GUTTER_PX` instead — the chiclet sits INSIDE the box,
170
- * so they need no above-box reservation.
171
- */
172
- export const GROUP_BRACKET_LABEL_OVERHANG_PX = 12;
173
-
174
128
  /**
175
129
  * Dashed-accent pattern (px on / px off) used for two nowline accents
176
130
  * that read as "structural emphasis": the milestone vertical cut line
@@ -226,15 +180,15 @@ export const NOW_PILL_LABEL_INSET_X_PX = 6;
226
180
  */
227
181
  export const NOWLINE_STROKE_WIDTH_PX = 2.25;
228
182
 
229
- // Visual inset applied on each side of an item bar. Two adjacent (logically
230
- // chained) items therefore have a 2× ITEM_INSET_PX visible gutter between
231
- // them, leaving room for vertical drop-lines (dependency arrows, anchor /
232
- // milestone cuts, the now-line) to pass between bars without crossing them.
233
- export const ITEM_INSET_PX = 6;
183
+ // The seam a bar gives up on each side, from inside its dates, so two bars
184
+ // that abut in time show a 2 px gap between their strokes
185
+ // (specs/true-width-layout.md §3.1). A bar too short for both seams and the
186
+ // `MIN_ITEM_WIDTH` floor shrinks its seams instead, staying centred in its
187
+ // dates.
188
+ export const ITEM_SEAM_PX = 1;
234
189
 
235
190
  // Canonical content gutter — the rest-state spacing between adjacent
236
191
  // pieces of chart content. Used for:
237
- // - the gap between two adjacent items in a track (= 2 × ITEM_INSET_PX),
238
192
  // - the gap between the header card and the chart's left edge (originX
239
193
  // offset from chartLeftX),
240
194
  // - the bottom margin around the attribution wordmark.
@@ -244,11 +198,14 @@ export const ITEM_INSET_PX = 6;
244
198
  // That's a runtime concern — the layout engine emits a static positioned
245
199
  // model and the interactive shell animates spacing on top. Keep this
246
200
  // constant as the rest-state baseline.
247
- export const GUTTER_PX = 2 * ITEM_INSET_PX;
201
+ export const GUTTER_PX = 12;
248
202
 
249
- // Default pixel-per-day when no explicit scale is set. Calibrated so a
250
- // 26-week (180 day) roadmap fits a 1200 px content area.
251
- export const DEFAULT_PIXELS_PER_DAY = 5;
203
+ // Pixels per day at the days scale (`scale:1d`). A day literal labels
204
+ // every column, so a day must hold its widest `M/D` label (`12/31`, about
205
+ // 29 px at the 10 px tick font), as a 40 px week holds `Sep 28`. That
206
+ // also keeps a day here wider than a day inside a default week column
207
+ // (8 px under business, about 5.7 px under full).
208
+ export const DEFAULT_PIXELS_PER_DAY = 32;
252
209
 
253
210
  // Header box defaults per position. The beside-mode card width is dynamic
254
211
  // (sized to the title + author text, clamped to MIN..MAX with text wrap),
@@ -266,13 +223,15 @@ export const HEADER_ABOVE_HEIGHT_PX = 72;
266
223
  // │ Footnotes ↑ │ header band (HEADER_HEIGHT)
267
224
  // │ │ │
268
225
  // ├─────────────────────────┘ │ gap = PANEL_PADDING
269
- // │ N Title — description │ row N (ROW_HEIGHT each)
226
+ // │ N Title — description │ row (ROW_HEIGHT each)
227
+ // │ that wraps at the title │ wrapped row
270
228
  // │ N … │
271
229
  // │ │
272
230
  // └───────────────────────────────┘ bottom pad = PANEL_PADDING
273
231
  //
274
232
  // Total panel height (when entries.length > 0):
275
- // HEADER_HEIGHT + entries × ROW_HEIGHT + PANEL_PADDING
233
+ // HEADER_HEIGHT + rows × ROW_HEIGHT + PANEL_PADDING, where rows counts
234
+ // every entry's rows (wrapped ones included) and each region heading.
276
235
 
277
236
  /** Height (px) of the header band above the entry rows. */
278
237
  export const FOOTNOTE_HEADER_HEIGHT_PX = 28;
@@ -367,3 +326,36 @@ export const LABEL_THINNING: Record<'days' | 'weeks' | 'months' | 'quarters' | '
367
326
  quarters: 4,
368
327
  years: 5,
369
328
  };
329
+
330
+ // Opacity of a non-working band (`timeline.nonWorkingFill`) under the
331
+ // `show` display (specs/working-calendar.md §7.3).
332
+ export const NON_WORKING_FILL_OPACITY = 0.1;
333
+
334
+ // Waves (specs/waves.md section 9.10; hatch geometry section 9.3).
335
+ export const WAVE_STRIP_HEIGHT_PX = 20;
336
+ export const WAVE_BOUNDARY_WIDTH_PX = 2;
337
+ export const WAVE_CROSS_DASH = '2 2';
338
+ export const WAVE_HATCH_OPACITY = 0.13;
339
+ export const WAVE_HATCH_TILE_PX = 6;
340
+ export const WAVE_HATCH_STROKE_PX = 2;
341
+ export const WAVE_STYLED_TINT_OPACITY = 0.12;
342
+ export const WAVE_STYLED_STRIP_MIX_OPACITY = 0.25;
343
+ export const WAVE_STRIP_LABEL_FONT_SIZE_PX = 10;
344
+ export const WAVE_LEGEND_LINE_PX = 14;
345
+ export const WAVE_LEGEND_GAP_PX = 8;
346
+ // Wave strip and legend geometry (specs/waves.md §9.1, §9.5).
347
+ /** Horizontal padding on each side of a strip label in the fit test. */
348
+ export const WAVE_STRIP_LABEL_PAD_PX = 6;
349
+ /** Horizontal padding of a strip label's halo on each side. */
350
+ export const WAVE_LABEL_HALO_PAD_PX = 2;
351
+ /** Vertical inset of a strip label's halo from the strip's top and bottom. */
352
+ export const WAVE_LABEL_HALO_INSET_Y_PX = 3;
353
+ /** How far a strip cell sits inside the backing panel: half its 1 px stroke. */
354
+ export const WAVE_CELL_INSET_PX = 0.5;
355
+ /** Legend swatch (a hatched bar) size. */
356
+ export const WAVE_LEGEND_SWATCH_WIDTH_PX = 18;
357
+ export const WAVE_LEGEND_SWATCH_HEIGHT_PX = 10;
358
+ /** Gap between a legend swatch and its text. */
359
+ export const WAVE_LEGEND_SWATCH_GAP_PX = 6;
360
+ /** Legend text baseline below its line's top (10 px text in a 14 px line). */
361
+ export const WAVE_LEGEND_BASELINE_OFFSET_PX = 11;
package/src/time-scale.ts CHANGED
@@ -3,23 +3,39 @@
3
3
  // also exposes `invert(x) -> Date` for the m4 editor work and that
4
4
  // composes with a `WorkingCalendar` for non-continuous time models.
5
5
  //
6
- // Forward direction matches the legacy arithmetic byte-for-byte
7
- // (`originX + daysBetween(domain[0], date) * pixelsPerDay`) so the
8
- // m2.5a refactor leaves the rendered output unchanged. Invert uses
9
- // `d3-scale.scaleTime` so callers get a precise Date back from a
10
- // pixel coordinate.
11
-
12
- import { type ScaleTime, scaleTime } from 'd3-scale';
13
- import { daysBetween } from './calendar.js';
6
+ // x is linear in the working-day index from the domain start
7
+ // (specs/working-calendar.md §7.1): `originX + index * pixelsPerDay`.
8
+ // With no calendar, or one with no non-working day, the index is
9
+ // `daysBetween`, so the forward direction matches the legacy
10
+ // arithmetic byte-for-byte. Under a calendar with non-working days a
11
+ // hidden date maps to the start of the next working day, and `invert`
12
+ // returns the working day whose start is nearest x.
13
+ //
14
+ // Under the `show` display (§7.3) a calendar's non-working days keep their
15
+ // full width instead: x is linear in calendar days, and `startX` /
16
+ // `advanceX` step an item over the non-working days it crosses. Every
17
+ // branch for it is gated on `showsNonWorking`, so `hide`, a calendar with no
18
+ // non-working day and a scale with no calendar keep the code above verbatim.
19
+
20
+ import { addDays, daysBetween } from './calendar.js';
21
+ import type { NonWorkingDisplay } from './non-working-display.js';
14
22
  import type { WorkingCalendar } from './working-calendar.js';
15
23
 
24
+ /**
25
+ * Tolerance, in days, for reading a day off an x. Pixel sums carry float
26
+ * noise, so an x a hair before a day's start counts as that day.
27
+ */
28
+ const DAY_EPSILON = 1e-6;
29
+
16
30
  export interface TimeScaleOptions {
17
31
  /** [start, end] in calendar dates (UTC midnight assumed). */
18
32
  domain: [Date, Date];
19
33
  /** [originX, originX + chartWidth]. */
20
34
  range: [number, number];
21
- /** Optional non-continuous calendar for future weekend-skip support. */
35
+ /** The working calendar; without one every day is a working day. */
22
36
  calendar?: WorkingCalendar;
37
+ /** How non-working days are drawn; `hide` when omitted. */
38
+ nonWorking?: NonWorkingDisplay;
23
39
  }
24
40
 
25
41
  export class TimeScale {
@@ -27,15 +43,43 @@ export class TimeScale {
27
43
  readonly range: [number, number];
28
44
  readonly pixelsPerDay: number;
29
45
  readonly calendar?: WorkingCalendar;
30
- private readonly d3: ScaleTime<number, number>;
46
+ /** The requested display; `showsNonWorking` says whether it takes effect. */
47
+ readonly nonWorking: NonWorkingDisplay;
31
48
 
32
49
  constructor(opts: TimeScaleOptions) {
33
50
  this.domain = opts.domain;
34
51
  this.range = opts.range;
35
52
  this.calendar = opts.calendar;
36
- const spanDays = Math.max(1, daysBetween(opts.domain[0], opts.domain[1]));
53
+ this.nonWorking = opts.nonWorking ?? 'hide';
54
+ const spanDays = Math.max(
55
+ 1,
56
+ this.showsNonWorking
57
+ ? daysBetween(opts.domain[0], opts.domain[1])
58
+ : this.indexOf(opts.domain[1]),
59
+ );
37
60
  this.pixelsPerDay = (opts.range[1] - opts.range[0]) / spanDays;
38
- this.d3 = scaleTime().domain(opts.domain).range(opts.range);
61
+ }
62
+
63
+ /**
64
+ * True when non-working days are drawn at full width: the `show`
65
+ * display over a calendar that has non-working days. The one predicate
66
+ * every show-only branch in the layout is gated on.
67
+ */
68
+ get showsNonWorking(): boolean {
69
+ return this.nonWorking === 'show' && this.calendar?.hasNonWorkingDays === true;
70
+ }
71
+
72
+ /** Working days from the domain start to `date` (calendar days without a calendar). */
73
+ private indexOf(date: Date): number {
74
+ return this.calendar
75
+ ? this.calendar.workingIndexOf(this.domain[0], date)
76
+ : daysBetween(this.domain[0], date);
77
+ }
78
+
79
+ /** Calendar days from the domain start to `x`, and the whole day `x` lies in. */
80
+ private dayAt(x: number): { days: number; day: number } {
81
+ const days = (x - this.range[0]) / this.pixelsPerDay;
82
+ return { days, day: Math.floor(days + DAY_EPSILON) };
39
83
  }
40
84
 
41
85
  /**
@@ -44,14 +88,17 @@ export class TimeScale {
44
88
  * `forwardWithinDomain`).
45
89
  */
46
90
  forward(date: Date): number {
47
- const days = daysBetween(this.domain[0], date);
48
- return this.range[0] + days * this.pixelsPerDay;
91
+ if (this.showsNonWorking) {
92
+ return this.range[0] + daysBetween(this.domain[0], date) * this.pixelsPerDay;
93
+ }
94
+ return this.range[0] + this.indexOf(date) * this.pixelsPerDay;
49
95
  }
50
96
 
51
97
  /**
52
98
  * Project a date onto the x-axis, returning `null` when the date
53
- * is outside [domain[0], domain[1]]. Replaces the legacy
54
- * `xForDate(date, timeline)` helper.
99
+ * is outside [domain[0], domain[1]]. The guard compares raw dates,
100
+ * so a hidden day inside the window still projects. Replaces the
101
+ * legacy `xForDate(date, timeline)` helper.
55
102
  */
56
103
  forwardWithinDomain(date: Date): number | null {
57
104
  if (date < this.domain[0] || date > this.domain[1]) return null;
@@ -59,11 +106,62 @@ export class TimeScale {
59
106
  }
60
107
 
61
108
  /**
62
- * Inverse projection. Returns a Date for any x in the chart;
63
- * callers that want day-resolution can floor the result.
109
+ * Inverse projection: the working day whose start is nearest x
110
+ * (UTC midnight). Never a non-working day, except under `show`, where
111
+ * every day has width and this is the calendar day nearest x.
64
112
  */
65
113
  invert(x: number): Date {
66
- return this.d3.invert(x);
114
+ const index = Math.round((x - this.range[0]) / this.pixelsPerDay);
115
+ if (this.showsNonWorking) return addDays(this.domain[0], index);
116
+ return this.calendar
117
+ ? this.calendar.dateAtWorkingIndex(this.domain[0], index)
118
+ : addDays(this.domain[0], index);
119
+ }
120
+
121
+ /**
122
+ * The working-day index at `x`, with the fraction of a working day it
123
+ * is into. Under `show` an x inside a non-working day reads as the start
124
+ * of the next working day; otherwise x is linear in the index.
125
+ */
126
+ indexAtX(x: number): number {
127
+ const { days, day } = this.dayAt(x);
128
+ if (!this.showsNonWorking || !this.calendar) return days;
129
+ const date = addDays(this.domain[0], day);
130
+ const index = this.calendar.workingIndexOf(this.domain[0], date);
131
+ if (!this.calendar.isWorkingDay(date)) return index;
132
+ return index + Math.max(0, days - day);
133
+ }
134
+
135
+ /**
136
+ * Where an item placed at `x` starts. Under `show` an x inside a
137
+ * non-working day moves forward to the start of the next working day;
138
+ * an x inside a working day keeps its fraction, so a start mid-Friday
139
+ * stays mid-Friday. The identity otherwise.
140
+ */
141
+ startX(x: number): number {
142
+ if (!this.showsNonWorking || !this.calendar) return x;
143
+ const date = addDays(this.domain[0], this.dayAt(x).day);
144
+ if (this.calendar.isWorkingDay(date)) return x;
145
+ const index = this.calendar.workingIndexOf(this.domain[0], date);
146
+ return this.forward(this.calendar.dateAtWorkingIndex(this.domain[0], index));
147
+ }
148
+
149
+ /**
150
+ * The x where `n` working days from `startX(x)` end. Under `show` that is
151
+ * the end of the last working day the span reaches, with a partial day
152
+ * interpolated, so a Mon-Fri item ends on Friday evening and the weekend
153
+ * after it stays open. `x + n * pixelsPerDay` otherwise.
154
+ */
155
+ advanceX(x: number, n: number): number {
156
+ if (!this.showsNonWorking || !this.calendar) return x + n * this.pixelsPerDay;
157
+ const start = this.startX(x);
158
+ if (n <= 0) return start;
159
+ const target = this.indexAtX(start) + n;
160
+ // The working day the span ends in, and how much of it it covers (0, 1].
161
+ const lastDay = Math.ceil(target - DAY_EPSILON) - 1;
162
+ const covered = target - lastDay;
163
+ const lastDate = this.calendar.dateAtWorkingIndex(this.domain[0], lastDay);
164
+ return this.forward(lastDate) + covered * this.pixelsPerDay;
67
165
  }
68
166
 
69
167
  /** First pixel of the chart (start of range). */