@nowline/layout 0.2.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 (183) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +103 -0
  3. package/dist/band-scale.d.ts +56 -0
  4. package/dist/band-scale.d.ts.map +1 -0
  5. package/dist/band-scale.js +86 -0
  6. package/dist/band-scale.js.map +1 -0
  7. package/dist/calendar.d.ts +79 -0
  8. package/dist/calendar.d.ts.map +1 -0
  9. package/dist/calendar.js +210 -0
  10. package/dist/calendar.js.map +1 -0
  11. package/dist/capacity.d.ts +72 -0
  12. package/dist/capacity.d.ts.map +1 -0
  13. package/dist/capacity.js +163 -0
  14. package/dist/capacity.js.map +1 -0
  15. package/dist/dsl-utils.d.ts +5 -0
  16. package/dist/dsl-utils.d.ts.map +1 -0
  17. package/dist/dsl-utils.js +28 -0
  18. package/dist/dsl-utils.js.map +1 -0
  19. package/dist/edge-routing.d.ts +89 -0
  20. package/dist/edge-routing.d.ts.map +1 -0
  21. package/dist/edge-routing.js +435 -0
  22. package/dist/edge-routing.js.map +1 -0
  23. package/dist/frame-tab-geometry.d.ts +78 -0
  24. package/dist/frame-tab-geometry.d.ts.map +1 -0
  25. package/dist/frame-tab-geometry.js +115 -0
  26. package/dist/frame-tab-geometry.js.map +1 -0
  27. package/dist/header-card-geometry.d.ts +29 -0
  28. package/dist/header-card-geometry.d.ts.map +1 -0
  29. package/dist/header-card-geometry.js +41 -0
  30. package/dist/header-card-geometry.js.map +1 -0
  31. package/dist/i18n.d.ts +48 -0
  32. package/dist/i18n.d.ts.map +1 -0
  33. package/dist/i18n.js +114 -0
  34. package/dist/i18n.js.map +1 -0
  35. package/dist/include-chrome-geometry.d.ts +86 -0
  36. package/dist/include-chrome-geometry.d.ts.map +1 -0
  37. package/dist/include-chrome-geometry.js +104 -0
  38. package/dist/include-chrome-geometry.js.map +1 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +10 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/item-bar-geometry.d.ts +127 -0
  44. package/dist/item-bar-geometry.d.ts.map +1 -0
  45. package/dist/item-bar-geometry.js +173 -0
  46. package/dist/item-bar-geometry.js.map +1 -0
  47. package/dist/lane-utilization.d.ts +90 -0
  48. package/dist/lane-utilization.d.ts.map +1 -0
  49. package/dist/lane-utilization.js +206 -0
  50. package/dist/lane-utilization.js.map +1 -0
  51. package/dist/layout-context.d.ts +143 -0
  52. package/dist/layout-context.d.ts.map +1 -0
  53. package/dist/layout-context.js +8 -0
  54. package/dist/layout-context.js.map +1 -0
  55. package/dist/layout.d.ts +18 -0
  56. package/dist/layout.d.ts.map +1 -0
  57. package/dist/layout.js +1213 -0
  58. package/dist/layout.js.map +1 -0
  59. package/dist/nodes/anchor-node.d.ts +16 -0
  60. package/dist/nodes/anchor-node.d.ts.map +1 -0
  61. package/dist/nodes/anchor-node.js +68 -0
  62. package/dist/nodes/anchor-node.js.map +1 -0
  63. package/dist/nodes/footnote-node.d.ts +10 -0
  64. package/dist/nodes/footnote-node.d.ts.map +1 -0
  65. package/dist/nodes/footnote-node.js +41 -0
  66. package/dist/nodes/footnote-node.js.map +1 -0
  67. package/dist/nodes/group-node.d.ts +29 -0
  68. package/dist/nodes/group-node.d.ts.map +1 -0
  69. package/dist/nodes/group-node.js +187 -0
  70. package/dist/nodes/group-node.js.map +1 -0
  71. package/dist/nodes/include-node.d.ts +16 -0
  72. package/dist/nodes/include-node.d.ts.map +1 -0
  73. package/dist/nodes/include-node.js +117 -0
  74. package/dist/nodes/include-node.js.map +1 -0
  75. package/dist/nodes/item-node.d.ts +51 -0
  76. package/dist/nodes/item-node.d.ts.map +1 -0
  77. package/dist/nodes/item-node.js +108 -0
  78. package/dist/nodes/item-node.js.map +1 -0
  79. package/dist/nodes/marker-geometry.d.ts +22 -0
  80. package/dist/nodes/marker-geometry.d.ts.map +1 -0
  81. package/dist/nodes/marker-geometry.js +38 -0
  82. package/dist/nodes/marker-geometry.js.map +1 -0
  83. package/dist/nodes/milestone-node.d.ts +48 -0
  84. package/dist/nodes/milestone-node.d.ts.map +1 -0
  85. package/dist/nodes/milestone-node.js +210 -0
  86. package/dist/nodes/milestone-node.js.map +1 -0
  87. package/dist/nodes/parallel-node.d.ts +21 -0
  88. package/dist/nodes/parallel-node.d.ts.map +1 -0
  89. package/dist/nodes/parallel-node.js +80 -0
  90. package/dist/nodes/parallel-node.js.map +1 -0
  91. package/dist/nodes/roadmap-node.d.ts +76 -0
  92. package/dist/nodes/roadmap-node.d.ts.map +1 -0
  93. package/dist/nodes/roadmap-node.js +788 -0
  94. package/dist/nodes/roadmap-node.js.map +1 -0
  95. package/dist/nodes/swimlane-node.d.ts +38 -0
  96. package/dist/nodes/swimlane-node.d.ts.map +1 -0
  97. package/dist/nodes/swimlane-node.js +308 -0
  98. package/dist/nodes/swimlane-node.js.map +1 -0
  99. package/dist/renderable.d.ts +44 -0
  100. package/dist/renderable.d.ts.map +1 -0
  101. package/dist/renderable.js +21 -0
  102. package/dist/renderable.js.map +1 -0
  103. package/dist/row-packer.d.ts +125 -0
  104. package/dist/row-packer.d.ts.map +1 -0
  105. package/dist/row-packer.js +169 -0
  106. package/dist/row-packer.js.map +1 -0
  107. package/dist/style-resolution.d.ts +14 -0
  108. package/dist/style-resolution.d.ts.map +1 -0
  109. package/dist/style-resolution.js +191 -0
  110. package/dist/style-resolution.js.map +1 -0
  111. package/dist/themes/dark.d.ts +4 -0
  112. package/dist/themes/dark.d.ts.map +1 -0
  113. package/dist/themes/dark.js +241 -0
  114. package/dist/themes/dark.js.map +1 -0
  115. package/dist/themes/index.d.ts +15 -0
  116. package/dist/themes/index.d.ts.map +1 -0
  117. package/dist/themes/index.js +30 -0
  118. package/dist/themes/index.js.map +1 -0
  119. package/dist/themes/light.d.ts +4 -0
  120. package/dist/themes/light.d.ts.map +1 -0
  121. package/dist/themes/light.js +248 -0
  122. package/dist/themes/light.js.map +1 -0
  123. package/dist/themes/shape.d.ts +194 -0
  124. package/dist/themes/shape.d.ts.map +1 -0
  125. package/dist/themes/shape.js +6 -0
  126. package/dist/themes/shape.js.map +1 -0
  127. package/dist/themes/shared.d.ts +145 -0
  128. package/dist/themes/shared.d.ts.map +1 -0
  129. package/dist/themes/shared.js +310 -0
  130. package/dist/themes/shared.js.map +1 -0
  131. package/dist/time-scale.d.ts +39 -0
  132. package/dist/time-scale.d.ts.map +1 -0
  133. package/dist/time-scale.js +62 -0
  134. package/dist/time-scale.js.map +1 -0
  135. package/dist/types.d.ts +483 -0
  136. package/dist/types.d.ts.map +1 -0
  137. package/dist/types.js +6 -0
  138. package/dist/types.js.map +1 -0
  139. package/dist/view-preset.d.ts +23 -0
  140. package/dist/view-preset.d.ts.map +1 -0
  141. package/dist/view-preset.js +146 -0
  142. package/dist/view-preset.js.map +1 -0
  143. package/dist/working-calendar.d.ts +14 -0
  144. package/dist/working-calendar.d.ts.map +1 -0
  145. package/dist/working-calendar.js +74 -0
  146. package/dist/working-calendar.js.map +1 -0
  147. package/package.json +37 -0
  148. package/src/band-scale.ts +115 -0
  149. package/src/calendar.ts +244 -0
  150. package/src/capacity.ts +191 -0
  151. package/src/dsl-utils.ts +30 -0
  152. package/src/edge-routing.ts +550 -0
  153. package/src/frame-tab-geometry.ts +165 -0
  154. package/src/header-card-geometry.ts +48 -0
  155. package/src/i18n.ts +124 -0
  156. package/src/include-chrome-geometry.ts +156 -0
  157. package/src/index.ts +116 -0
  158. package/src/item-bar-geometry.ts +222 -0
  159. package/src/lane-utilization.ts +259 -0
  160. package/src/layout-context.ts +182 -0
  161. package/src/layout.ts +1446 -0
  162. package/src/nodes/anchor-node.ts +77 -0
  163. package/src/nodes/footnote-node.ts +60 -0
  164. package/src/nodes/group-node.ts +252 -0
  165. package/src/nodes/include-node.ts +168 -0
  166. package/src/nodes/item-node.ts +171 -0
  167. package/src/nodes/marker-geometry.ts +43 -0
  168. package/src/nodes/milestone-node.ts +263 -0
  169. package/src/nodes/parallel-node.ts +101 -0
  170. package/src/nodes/roadmap-node.ts +957 -0
  171. package/src/nodes/swimlane-node.ts +423 -0
  172. package/src/renderable.ts +68 -0
  173. package/src/row-packer.ts +271 -0
  174. package/src/style-resolution.ts +243 -0
  175. package/src/themes/dark.ts +244 -0
  176. package/src/themes/index.ts +36 -0
  177. package/src/themes/light.ts +251 -0
  178. package/src/themes/shape.ts +230 -0
  179. package/src/themes/shared.ts +369 -0
  180. package/src/time-scale.ts +78 -0
  181. package/src/types.ts +607 -0
  182. package/src/view-preset.ts +180 -0
  183. package/src/working-calendar.ts +91 -0
package/src/types.ts ADDED
@@ -0,0 +1,607 @@
1
+ // Positioned-model types. One type per entity in specs/rendering.md §
2
+ // The Positioned Model. Coordinates are in SVG user units with origin at
3
+ // top-left. All colors in `ResolvedStyle` are concrete hex strings baked
4
+ // in by style-resolution; the renderer is palette-dumb.
5
+
6
+ export type SizeBucket = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
7
+ export type ShadowKind = 'none' | 'subtle' | 'soft' | 'hard';
8
+ export type BorderKind = 'solid' | 'dashed' | 'dotted';
9
+ export type FontFamily = 'sans' | 'serif' | 'mono';
10
+ export type FontWeight = 'thin' | 'light' | 'normal' | 'bold';
11
+ export type BracketKind = 'none' | 'solid' | 'dashed';
12
+ export type HeaderPosition = 'beside' | 'above';
13
+ // Where the timeline date strip is rendered. Roadmap-only style property.
14
+ // - `top` (default) — single strip above the chart (legacy behavior)
15
+ // - `bottom` — single strip below the chart, no top strip
16
+ // - `both` — strips at both ends; tall canvases stay readable from
17
+ // either edge of the viewport
18
+ export type TimelinePosition = 'top' | 'bottom' | 'both';
19
+ export type StatusKind = 'planned' | 'in-progress' | 'done' | 'at-risk' | 'blocked' | 'neutral';
20
+
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`).
23
+ // Every one has a concrete value after resolution (theme + defaults fill gaps).
24
+ export interface ResolvedStyle {
25
+ bg: string; // hex or 'none'
26
+ fg: string; // hex
27
+ text: string; // hex
28
+ border: BorderKind;
29
+ icon: string; // identifier like 'linear' | 'github' | 'jira' | 'generic' | 'none'
30
+ shadow: ShadowKind;
31
+ font: FontFamily;
32
+ weight: FontWeight;
33
+ italic: boolean;
34
+ textSize: SizeBucket; // 'none'..'xl'
35
+ padding: SizeBucket; // 'none'..'xl'
36
+ spacing: SizeBucket; // 'none'..'xl'
37
+ headerHeight: SizeBucket; // 'none'..'xl'
38
+ cornerRadius: SizeBucket; // 'none'..'xl'|'full'
39
+ bracket: BracketKind;
40
+ headerPosition: HeaderPosition;
41
+ /**
42
+ * Glyph used as the suffix on capacity numbers (`5×`, `5 [person]`, etc.).
43
+ * Stores the raw value as the author wrote it: a built-in icon name
44
+ * (`'multiplier'`, `'person'`, ...), a custom symbol id declared via
45
+ * `symbol` in config, or an inline Unicode literal (`'💰'`). The renderer
46
+ * resolves built-in vs custom vs literal at paint time using
47
+ * `ResolvedConfig.symbols` and the `BUILTIN_CAPACITY_ICONS` set.
48
+ * Default `'multiplier'`.
49
+ */
50
+ capacityIcon: string;
51
+ timelinePosition: TimelinePosition;
52
+ minorGrid: boolean;
53
+ }
54
+
55
+ export interface BoundingBox {
56
+ x: number;
57
+ y: number;
58
+ width: number;
59
+ height: number;
60
+ }
61
+
62
+ /**
63
+ * Calendar-resolved view of a `size NAME ["TITLE"] effort:LITERAL`
64
+ * declaration. Built once per layout from the AST `SizeDeclaration`s
65
+ * collected by the include-resolver and the active calendar (which
66
+ * decides how many days a literal like `2w` maps to). Items reference
67
+ * these via `LayoutContext.sizes` rather than walking the AST +
68
+ * calendar each time they need an effort.
69
+ *
70
+ * `effortLiteral` keeps the raw literal so callers can show the size's
71
+ * effort string (`"2w"`) on tooltips without re-formatting from
72
+ * `effortDays`.
73
+ *
74
+ * `title` is the optional author-provided display label. The on-bar
75
+ * size chip prefers `title` when present, falling back to `name` (the
76
+ * id) verbatim — both rendered with the case the author typed. See
77
+ * specs/rendering.md § Item size chip.
78
+ */
79
+ export interface ResolvedSize {
80
+ name: string;
81
+ title?: string;
82
+ effortDays: number;
83
+ effortLiteral: string;
84
+ }
85
+
86
+ export interface Point {
87
+ x: number;
88
+ y: number;
89
+ }
90
+
91
+ // Header + attribution mark + optional logo.
92
+ export interface PositionedLogo {
93
+ box: BoundingBox;
94
+ assetRef?: string; // path as declared in DSL; renderer resolves via AssetResolver
95
+ }
96
+
97
+ export interface PositionedHeader {
98
+ box: BoundingBox;
99
+ position: HeaderPosition; // 'beside' | 'above'
100
+ title: string; // empty string if no title set
101
+ author?: string;
102
+ // Word-wrapped title and author lines, sized to the resolved card
103
+ // width (title text wraps when it exceeds HEADER_BESIDE_MAX_WIDTH_PX
104
+ // minus padding). The renderer stacks them inside `cardBox` and does
105
+ // not need to do any text measurement of its own.
106
+ titleLines: string[];
107
+ authorLines: string[];
108
+ // Bounding box of the visible white card inside `box`. The card hugs
109
+ // its content (width = max line width + padding, clamped to MIN..MAX;
110
+ // height grows for wrapped lines).
111
+ cardBox: BoundingBox;
112
+ logo?: PositionedLogo;
113
+ style: ResolvedStyle;
114
+ // Attribution mark (Nowline wordmark + link) lives in the top-right.
115
+ attributionBox: BoundingBox;
116
+ }
117
+
118
+ // A single tick on the timeline scale (e.g. "W1", "Q2", "Feb").
119
+ //
120
+ // `x` is the tick's BOUNDARY position (the start of the column the tick
121
+ // represents — also where the dotted grid line drops). `labelX` is where
122
+ // the label TEXT sits, centered horizontally within the column (i.e.
123
+ // halfway between this tick's x and the next tick's x). The final tick
124
+ // has no following column, so its `labelX` is undefined and the renderer
125
+ // skips drawing its label.
126
+ export interface PositionedTick {
127
+ x: number;
128
+ labelX?: number;
129
+ label?: string; // undefined for thinned ticks
130
+ major: boolean; // full-height major line vs short minor tick
131
+ }
132
+
133
+ export interface PositionedTimelineScale {
134
+ box: BoundingBox;
135
+ ticks: PositionedTick[];
136
+ // Pixel-per-day used for all entities in the chart.
137
+ pixelsPerDay: number;
138
+ // Day 0 (the roadmap start date) is at x = originX.
139
+ originX: number;
140
+ startDate: Date;
141
+ endDate: Date;
142
+ labelStyle: ResolvedStyle;
143
+ // Now-pill row sits at the very top of the timeline area (above the
144
+ // date labels). Height is 0 when there's no now-line to draw.
145
+ pillRowHeight: number;
146
+ // Tick-label panel (the date headers). Always rendered when
147
+ // `timelinePosition` is `top` or `both`; height is 0 when the
148
+ // roadmap requested `bottom`-only and no top strip is wanted.
149
+ tickPanelY: number;
150
+ tickPanelHeight: number;
151
+ // Marker row sits BELOW the top tick-label panel. Anchors + milestones
152
+ // live here. The collision band sits ABOVE the in-row baseline so an
153
+ // anchor colliding with a milestone can be bumped up. Height is 0 when
154
+ // there are no markers to render — the renderer then omits the panel
155
+ // entirely so we don't reserve dead space.
156
+ markerRow: {
157
+ y: number; // y of the in-row diamond center
158
+ height: number; // total height of the marker row band (in-row + collision)
159
+ collisionY: number; // y of the bumped-up diamond center
160
+ };
161
+ // Mirrored bottom tick-label panel. Populated when the roadmap's
162
+ // resolved `timelinePosition` is `bottom` or `both`. Width and
163
+ // x match the top panel (`box.x` / `box.width`); the renderer
164
+ // emits the same fill, border, label color, and tick labels at
165
+ // `bottomTickPanelY`. No now-pill, no marker row — the bottom strip
166
+ // is purely a date reference for tall canvases.
167
+ bottomTickPanelY?: number;
168
+ bottomTickPanelHeight?: number;
169
+ // When `true`, the renderer draws a faint dotted line at every tick
170
+ // boundary (not just major ticks) using `theme.timeline.minorGridLine`.
171
+ // Mirrors the roadmap's resolved `minor-grid` style property. Default
172
+ // `false` preserves byte-stable output for existing roadmaps.
173
+ minorGrid: boolean;
174
+ }
175
+
176
+ /**
177
+ * How the now-pill is positioned relative to the now-line.
178
+ *
179
+ * - `center` — pill centered on the line (default). Used when both
180
+ * edges have at least `NOW_PILL_WIDTH_PX/2` of clearance from the
181
+ * chart's left/right edges.
182
+ * - `flag-right` — line at the pill's left edge, pill extends to the
183
+ * right with the right side rounded and the label left-aligned.
184
+ * Used when the line lands close enough to `chartLeftX` that a
185
+ * centered pill would overlap the header card / canvas left edge.
186
+ * - `flag-left` — line at the pill's right edge, pill extends to the
187
+ * left with the left side rounded and the label right-aligned.
188
+ * Used when the line lands close enough to `chartRightX` that a
189
+ * centered pill would clip past the canvas right edge.
190
+ *
191
+ * In both flag modes the squared edge IS the now-line, so the pill
192
+ * visually anchors to the line without growing the canvas.
193
+ */
194
+ export type NowPillMode = 'center' | 'flag-right' | 'flag-left';
195
+
196
+ export interface PositionedNowline {
197
+ x: number;
198
+ // Top of the vertical red line. Sits at the BOTTOM of the now-pill —
199
+ // just above the tick-label panel — so the line drops through the
200
+ // date headers and any marker row, into the chart.
201
+ topY: number;
202
+ // Bottom of the vertical red line (chart bottom).
203
+ bottomY: number;
204
+ // Top edge of the pill rectangle. Pill height is fixed in the
205
+ // renderer; the layout reserves the space at the very top of the
206
+ // timeline area so the pill sits ABOVE the date headers.
207
+ pillTopY: number;
208
+ /** How the pill aligns to the line (see `NowPillMode`). */
209
+ pillMode: NowPillMode;
210
+ /**
211
+ * Locale-resolved string painted inside the pill (`'now'` for `en-US`,
212
+ * `'maint.'` for `fr`). Layout owns the locale lookup; the renderer
213
+ * just paints the string. See `packages/layout/src/i18n.ts`.
214
+ */
215
+ label: string;
216
+ /**
217
+ * Width (px) of the pill. Floored at `NOW_PILL_WIDTH_PX` so en-US
218
+ * (`'now'`) keeps its byte-stable 36 px footprint; longer locale
219
+ * strings (e.g. `'maint.'` for fr) grow the pill to fit instead of
220
+ * clipping. Computed in `buildNowline` from `estimateTextWidth(label)`.
221
+ */
222
+ pillWidth: number;
223
+ style: ResolvedStyle;
224
+ }
225
+
226
+ export interface PositionedItem {
227
+ kind: 'item';
228
+ id?: string;
229
+ title: string;
230
+ box: BoundingBox;
231
+ status: StatusKind;
232
+ progressFraction: number; // 0..1; 1 == fully filled
233
+ footnoteIndicators: number[]; // 1-based superscript numbers, empty when no footnotes
234
+ labelChips: PositionedLabelChip[];
235
+ /** True when the chip row's natural total width exceeded the bar's
236
+ * effective inner width and the whole row spilled past the bar's
237
+ * right edge. The chips' `box.x` already reflects the spilled
238
+ * position; this flag exists for the row-packer to reserve the
239
+ * spilled extent and for the renderer / debug overlays to know
240
+ * the row sits outside the bar's painted footprint. */
241
+ chipsOutside: boolean;
242
+ /** Logical right x reached by the chip row, INCLUDING the chips
243
+ * whether painted inside or outside the bar. Equals the start x
244
+ * when there are no chips. The row-packer's spill reservation
245
+ * uses this to grow the chart canvas / bump siblings. */
246
+ chipsRightX: number;
247
+ linkIcon?: LinkIconKind;
248
+ linkHref?: string;
249
+ hasOverflow: boolean; // true when before: forced the item past its natural end
250
+ overflowBox?: BoundingBox; // the offending tail, flagged red
251
+ // The id of the `before:` anchor/milestone the item overran. Used by the
252
+ // renderer to caption the overflow tail ("past <id>").
253
+ overflowAnchorId?: string;
254
+ owner?: string; // owner id (person/team) for annotation
255
+ description?: string;
256
+ // Pre-formatted secondary line shown under the title inside the item bar
257
+ // (e.g. "1w" or "2w — 50% remaining"). Layout assembles this so the
258
+ // renderer stays palette-and-string-dumb.
259
+ metaText?: string;
260
+ // True when the title OR the meta line is wider than the bar's inner
261
+ // padded width. We treat title + meta as an atomic block: if either
262
+ // one wouldn't fit inside the bar, BOTH get drawn beside the bar
263
+ // (stacked, just past its right edge) so they read as one caption
264
+ // rather than splitting across the bar boundary. The layout also
265
+ // bumps the next item to a fresh row so the spilled caption has
266
+ // empty space to occupy.
267
+ textSpills: boolean;
268
+ /** True when the bar is too narrow to host the status dot inside
269
+ * with its full inset (`MIN_BAR_WIDTH_FOR_DOT_PX`). The dot
270
+ * renders in the spill column to the right of the bar instead
271
+ * of overshooting the bar's left edge. */
272
+ dotSpills: boolean;
273
+ /** True when the bar is too narrow to host the link-icon tile
274
+ * inside without colliding with the status dot column
275
+ * (`MIN_BAR_WIDTH_FOR_LINK_AND_DOT_PX`). The icon spills out and
276
+ * renders ahead of the (also-spilled) title so the icon stays
277
+ * visually attached to the title text. Implies `textSpills`. */
278
+ iconSpills: boolean;
279
+ /** True when the bar is too narrow to host the footnote
280
+ * superscript at its inset-right position. The indicator(s)
281
+ * render in the spill column trailing the title text instead
282
+ * of at the bar's upper-right corner. */
283
+ footnoteSpills: boolean;
284
+ /** Pre-computed x positions for the spilled decorations. `null`
285
+ * when the matching `*Spills` flag is false (decoration stays
286
+ * inside the bar at its inset-anchored position). */
287
+ dotSpillCx: number | null;
288
+ iconSpillX: number | null;
289
+ /** First footnote indicator's left edge in the spill column.
290
+ * Subsequent indicators walk right by `ITEM_FOOTNOTE_INDICATOR_STEP_PX`. */
291
+ footnoteSpillStartX: number | null;
292
+ /** Right edge of the spilled-decoration cluster (inclusive of
293
+ * spilled title and footnote glyphs). Used by the row-packer
294
+ * to size the row's spill reservation so the next chained item
295
+ * doesn't land underneath. */
296
+ decorationsRightX: number;
297
+ /**
298
+ * Capacity suffix data when the item declares `capacity:N`. Null when
299
+ * the item has no capacity, the value is non-positive, or the resolved
300
+ * `capacity-icon` is `none` and no number should render either way.
301
+ *
302
+ * Renders alongside the item's `metaText` (or stand-alone when no meta
303
+ * is present) per specs/rendering.md § Item capacity suffix. The `text`
304
+ * is the formatted number (`'5'`, `'0.5'`, `'1.25'`); `icon` tells the
305
+ * renderer which glyph to draw and whether to use the SVG library, the
306
+ * `×` text node, or an inline literal.
307
+ */
308
+ capacity: PositionedCapacity | null;
309
+ /**
310
+ * Resolved size when the item declared `size:NAME`. Null when the item
311
+ * sized itself with a literal `duration:` or didn't declare a size at
312
+ * all. Used by the renderer for the size chip on the meta line (m6) and
313
+ * by the layout for capacity-aware duration derivation (m5).
314
+ */
315
+ size: ResolvedSize | null;
316
+ style: ResolvedStyle;
317
+ }
318
+
319
+ /**
320
+ * Positioned capacity suffix shared by `PositionedItem` and (in m7) the lane
321
+ * frame-tab badge. The shape stays small and serializable: a formatted number
322
+ * string plus a discriminated union for the glyph. The renderer paints both.
323
+ *
324
+ * `icon === null` means the resolved `capacity-icon` was `'none'` — render
325
+ * the bare number with no glyph or separator.
326
+ */
327
+ export interface PositionedCapacity {
328
+ /** Numeric capacity, post-percent-sugar conversion (e.g. `50%` → 0.5). */
329
+ value: number;
330
+ /** Display string per spec number-formatting rules (`'5'`, `'0.5'`). */
331
+ text: string;
332
+ /** Resolved glyph instruction, or `null` when the icon is `'none'`. */
333
+ icon: ResolvedCapacityIconRef | null;
334
+ }
335
+
336
+ /**
337
+ * Renderer-facing capacity-icon reference. Mirrors the layout-internal
338
+ * `ResolvedCapacityIcon` from `capacity.ts` but lives in the shared
339
+ * positioned-model types so the renderer can read it without importing
340
+ * layout internals.
341
+ */
342
+ export type ResolvedCapacityIconRef =
343
+ | { kind: 'builtin'; name: 'multiplier' | 'person' | 'people' | 'points' | 'time' }
344
+ | { kind: 'literal'; text: string };
345
+
346
+ export type LinkIconKind = 'linear' | 'github' | 'jira' | 'generic' | 'none';
347
+
348
+ export interface PositionedLabelChip {
349
+ text: string;
350
+ style: ResolvedStyle;
351
+ // Chip box is laid out inside the item bar; coordinates are absolute.
352
+ box: BoundingBox;
353
+ }
354
+
355
+ export interface PositionedGroup {
356
+ kind: 'group';
357
+ id?: string;
358
+ title?: string;
359
+ box: BoundingBox;
360
+ // Bracket is drawn on the left edge when style.bracket != 'none'.
361
+ children: PositionedTrackChild[];
362
+ style: ResolvedStyle;
363
+ }
364
+
365
+ export interface PositionedParallel {
366
+ kind: 'parallel';
367
+ id?: string;
368
+ title?: string;
369
+ box: BoundingBox;
370
+ children: PositionedTrackChild[]; // sub-tracks stacked vertically
371
+ style: ResolvedStyle;
372
+ }
373
+
374
+ export type PositionedTrackChild = PositionedItem | PositionedParallel | PositionedGroup;
375
+
376
+ export interface PositionedSwimlane {
377
+ id?: string;
378
+ title: string; // display name; falls back to id
379
+ box: BoundingBox;
380
+ bandIndex: number; // zero-based; even/odd drives tint
381
+ children: PositionedTrackChild[];
382
+ nested: PositionedSwimlane[]; // recursive sub-swimlanes
383
+ style: ResolvedStyle;
384
+ // Owner display string ("Platform Team", "Sam Chen") rendered inside
385
+ // the frame tab. Resolved from team/person id → title.
386
+ owner?: string;
387
+ // Footnote indicator numbers attached to this swimlane (via `on:` in the
388
+ // footnote declaration). Rendered in the upper-right of the frame tab.
389
+ footnoteIndicators: number[];
390
+ /**
391
+ * Lane-level capacity badge data when the swimlane declares
392
+ * `capacity:N`. Null when no capacity is set or the value parses to
393
+ * zero. The renderer paints the badge inside the frame tab after the
394
+ * owner badge (or after the lane title when no owner is present),
395
+ * per specs/rendering.md § Lane capacity badge. m8 (overload sweep)
396
+ * also reads `value` to compute load against item capacities.
397
+ *
398
+ * `capacity-icon:none` resolves to `icon: null` here (just the bare
399
+ * number renders, no glyph) but the badge still appears. Authors who
400
+ * want the badge fully hidden simply omit `capacity:`.
401
+ */
402
+ capacity: PositionedCapacity | null;
403
+ /**
404
+ * Tri-state utilization underline data when the lane declares
405
+ * `capacity:` AND has at least one item contributing load AND has not
406
+ * opted out of every color band via `utilization-*-at:none`. Null
407
+ * otherwise — the renderer paints no underline.
408
+ *
409
+ * Computed in m12 by `computeLaneUtilization` (see
410
+ * `lane-utilization.ts`) per specs/rendering.md § Lane utilization
411
+ * underline. The renderer paints one rectangle per coalesced segment
412
+ * along the bottom edge of the band, colored from the resolved theme's
413
+ * `swimlane.utilizationOk` / `…Warn` / `…Over` tokens.
414
+ */
415
+ utilization: PositionedLaneUtilization | null;
416
+ }
417
+
418
+ /**
419
+ * One classification for a lane utilization segment. `green` includes the
420
+ * zero-load case so the underline reads as a continuous health bar; the spec
421
+ * intentionally avoids a separate "idle" color.
422
+ */
423
+ export type UtilizationClassification = 'green' | 'yellow' | 'red';
424
+
425
+ /**
426
+ * One half-open `[startX, endX)` segment of the lane's utilization underline,
427
+ * pre-classified for the renderer. Adjacent same-classification segments are
428
+ * coalesced upstream so the renderer paints one rectangle per visible color
429
+ * band rather than per event boundary.
430
+ *
431
+ * `load` is the absolute concurrent capacity at this segment (sum of active
432
+ * items' `capacity:` values). Useful for tooltips and debug overlays; the
433
+ * paint color is determined by `classification` alone.
434
+ */
435
+ export interface PositionedUtilizationSegment {
436
+ startX: number;
437
+ endX: number;
438
+ load: number;
439
+ classification: UtilizationClassification;
440
+ }
441
+
442
+ /**
443
+ * Resolved utilization model for a swimlane. Carries the segment list plus
444
+ * the resolved thresholds and capacity so downstream consumers (renderer,
445
+ * tooltips, exporters) can describe the model without re-resolving config.
446
+ *
447
+ * `warnFraction` / `overFraction`: each is either a positive fraction of
448
+ * `capacityValue` (the lane paints that color band when load reaches the
449
+ * fraction) or `null` to mean "this color band is opted out via
450
+ * `utilization-*-at:none`".
451
+ */
452
+ export interface PositionedLaneUtilization {
453
+ segments: PositionedUtilizationSegment[];
454
+ capacityValue: number;
455
+ warnFraction: number | null;
456
+ overFraction: number | null;
457
+ }
458
+
459
+ export interface PositionedAnchor {
460
+ id?: string;
461
+ title: string;
462
+ center: Point; // diamond center (post-collision-resolution)
463
+ radius: number;
464
+ style: ResolvedStyle;
465
+ // Non-binding predecessor edges: small arrows from prior items, drawn by renderer.
466
+ predecessorPoints: Point[];
467
+ // Vertical span of the anchor's "cut line" through the swimlane area,
468
+ // drawn by the renderer after items so it overlays the lane fills.
469
+ cutTopY: number;
470
+ cutBottomY: number;
471
+ // True when this anchor was bumped above the in-row baseline because a
472
+ // milestone shares the same x-column.
473
+ bumpedUp: boolean;
474
+ // Resolved label placement. The marker-row packer decides whether the
475
+ // title sits to the right (default) or the left of the diamond, and
476
+ // whether the entity drops to a lower row to avoid colliding with
477
+ // earlier markers' label boxes. Renderer uses `labelBox.x/y` directly
478
+ // (start-anchored text) — no further geometry decisions.
479
+ labelBox: BoundingBox;
480
+ labelSide: 'left' | 'right';
481
+ }
482
+
483
+ export interface PositionedMilestone {
484
+ id?: string;
485
+ title: string;
486
+ center: Point;
487
+ radius: number;
488
+ fixed: boolean; // true for date: style, false for after: style
489
+ // One slack arrow per non-binding predecessor. Each entry's (x, y) is
490
+ // the predecessor's right-edge midpoint; the arrow runs horizontally
491
+ // from there to (center.x - 6) at y. Empty / undefined when the
492
+ // milestone has zero or one predecessor.
493
+ slackArrows?: Array<{ x: number; y: number }>;
494
+ isOverrun: boolean; // true when the aggregated predecessor end exceeds `date:`
495
+ style: ResolvedStyle;
496
+ // Vertical span of the milestone's cut line through the swimlane area.
497
+ cutTopY: number;
498
+ cutBottomY: number;
499
+ // See PositionedAnchor.labelBox — same packing logic applies.
500
+ labelBox: BoundingBox;
501
+ labelSide: 'left' | 'right';
502
+ }
503
+
504
+ /**
505
+ * Result of packing a marker-row entity (anchor or milestone) into the
506
+ * dynamic row stack. `rowIndex == 0` is the in-row baseline; positive
507
+ * indices push the diamond DOWN by `step` px each. The label box is
508
+ * absolute and already accounts for left/right side flipping when the
509
+ * preferred side would overflow the chart.
510
+ */
511
+ export interface MarkerRowPlacement {
512
+ rowIndex: number;
513
+ centerY: number;
514
+ labelBox: BoundingBox;
515
+ labelSide: 'left' | 'right';
516
+ }
517
+
518
+ /**
519
+ * Horizontal corridor occupied by a milestone's slack arrow. Sits at
520
+ * the slack predecessor's row Y, running from the predecessor's right
521
+ * edge to the milestone's column. Items whose natural placement would
522
+ * intersect this band must drop to a row whose Y does not match `y`,
523
+ * so the arrow has clear horizontal space to travel.
524
+ */
525
+ export interface SlackCorridor {
526
+ xStart: number; // slack pred's right edge (logical chart x)
527
+ xEnd: number; // binding pred's right edge / milestone center.x
528
+ y: number; // slack pred's row midpoint
529
+ slackPredId: string; // exempt from bumping (owns the arrow's origin)
530
+ milestoneId: string;
531
+ }
532
+
533
+ export interface PositionedDependencyEdge {
534
+ fromId: string;
535
+ toId: string;
536
+ waypoints: Point[]; // first = source port; last = target port
537
+ /**
538
+ * - `normal` — orthogonal arrow drawn AFTER swimlane / item /
539
+ * marker fills so it sits on top of lane bands.
540
+ * - `overflow` — currently unused at construction; reserved for the
541
+ * red `before:` overrun annotation arrow.
542
+ * - `underBar` — channel router could not find a clear vertical
543
+ * gutter between the source and target columns; the arrow's
544
+ * vertical leg crosses one or more item bars. The renderer paints
545
+ * these edges BEFORE bar fills with a thinner stroke so the bar
546
+ * stays the visual foreground.
547
+ */
548
+ kind: 'normal' | 'overflow' | 'underBar';
549
+ style: ResolvedStyle;
550
+ }
551
+
552
+ export interface PositionedFootnoteIndicator {
553
+ // Not a separate geometry in the chart — rendered as a superscript on the
554
+ // host item. Kept in the model for completeness, keyed to the host item id.
555
+ number: number;
556
+ hostItemId: string;
557
+ style: ResolvedStyle;
558
+ }
559
+
560
+ export interface PositionedFootnoteArea {
561
+ box: BoundingBox;
562
+ entries: PositionedFootnoteEntry[];
563
+ }
564
+
565
+ export interface PositionedFootnoteEntry {
566
+ number: number;
567
+ title: string;
568
+ description?: string;
569
+ style: ResolvedStyle;
570
+ }
571
+
572
+ export interface PositionedIncludeRegion {
573
+ sourcePath: string; // relative to the parent file
574
+ label: string; // e.g. the child roadmap's title or basename
575
+ box: BoundingBox;
576
+ // Nested swimlanes laid out inside the region. They share the parent's
577
+ // timeline (originX, pixelsPerDay) so cross-region dates align with the
578
+ // tick row above the region.
579
+ nestedSwimlanes: PositionedSwimlane[];
580
+ style: ResolvedStyle;
581
+ }
582
+
583
+ // Top-level result handed to the renderer.
584
+ export interface PositionedRoadmap {
585
+ width: number;
586
+ height: number;
587
+ theme: 'light' | 'dark';
588
+ /**
589
+ * Resolved palette — every color the renderer reads. m2.5d moved
590
+ * theme resolution into the layout side, so the renderer no longer
591
+ * branches on `theme === 'dark'`. The `theme` field above stays for
592
+ * `data-theme` SVG attribution; all color decisions read `palette`.
593
+ */
594
+ palette: import('./themes/index.js').Theme;
595
+ backgroundColor: string; // resolved from theme.surface.page
596
+ header: PositionedHeader;
597
+ timeline: PositionedTimelineScale;
598
+ nowline: PositionedNowline | null;
599
+ swimlanes: PositionedSwimlane[];
600
+ anchors: PositionedAnchor[];
601
+ milestones: PositionedMilestone[];
602
+ edges: PositionedDependencyEdge[];
603
+ footnotes: PositionedFootnoteArea;
604
+ includes: PositionedIncludeRegion[];
605
+ // Frame (chart area) in chart-space. Useful for renderer overlays.
606
+ chartBox: BoundingBox;
607
+ }