@nowline/layout 0.0.0-dev.20260601071750.g04bdff9

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 (193) 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/inline-date-pin-geometry.d.ts +46 -0
  44. package/dist/inline-date-pin-geometry.d.ts.map +1 -0
  45. package/dist/inline-date-pin-geometry.js +149 -0
  46. package/dist/inline-date-pin-geometry.js.map +1 -0
  47. package/dist/item-bar-geometry.d.ts +157 -0
  48. package/dist/item-bar-geometry.d.ts.map +1 -0
  49. package/dist/item-bar-geometry.js +214 -0
  50. package/dist/item-bar-geometry.js.map +1 -0
  51. package/dist/lane-utilization.d.ts +90 -0
  52. package/dist/lane-utilization.d.ts.map +1 -0
  53. package/dist/lane-utilization.js +206 -0
  54. package/dist/lane-utilization.js.map +1 -0
  55. package/dist/layout-context.d.ts +143 -0
  56. package/dist/layout-context.d.ts.map +1 -0
  57. package/dist/layout-context.js +8 -0
  58. package/dist/layout-context.js.map +1 -0
  59. package/dist/layout.d.ts +18 -0
  60. package/dist/layout.d.ts.map +1 -0
  61. package/dist/layout.js +1298 -0
  62. package/dist/layout.js.map +1 -0
  63. package/dist/nodes/anchor-node.d.ts +16 -0
  64. package/dist/nodes/anchor-node.d.ts.map +1 -0
  65. package/dist/nodes/anchor-node.js +68 -0
  66. package/dist/nodes/anchor-node.js.map +1 -0
  67. package/dist/nodes/footnote-node.d.ts +10 -0
  68. package/dist/nodes/footnote-node.d.ts.map +1 -0
  69. package/dist/nodes/footnote-node.js +41 -0
  70. package/dist/nodes/footnote-node.js.map +1 -0
  71. package/dist/nodes/group-node.d.ts +29 -0
  72. package/dist/nodes/group-node.d.ts.map +1 -0
  73. package/dist/nodes/group-node.js +195 -0
  74. package/dist/nodes/group-node.js.map +1 -0
  75. package/dist/nodes/include-node.d.ts +16 -0
  76. package/dist/nodes/include-node.d.ts.map +1 -0
  77. package/dist/nodes/include-node.js +117 -0
  78. package/dist/nodes/include-node.js.map +1 -0
  79. package/dist/nodes/item-node.d.ts +51 -0
  80. package/dist/nodes/item-node.d.ts.map +1 -0
  81. package/dist/nodes/item-node.js +108 -0
  82. package/dist/nodes/item-node.js.map +1 -0
  83. package/dist/nodes/marker-geometry.d.ts +22 -0
  84. package/dist/nodes/marker-geometry.d.ts.map +1 -0
  85. package/dist/nodes/marker-geometry.js +38 -0
  86. package/dist/nodes/marker-geometry.js.map +1 -0
  87. package/dist/nodes/milestone-node.d.ts +48 -0
  88. package/dist/nodes/milestone-node.d.ts.map +1 -0
  89. package/dist/nodes/milestone-node.js +210 -0
  90. package/dist/nodes/milestone-node.js.map +1 -0
  91. package/dist/nodes/parallel-node.d.ts +21 -0
  92. package/dist/nodes/parallel-node.d.ts.map +1 -0
  93. package/dist/nodes/parallel-node.js +88 -0
  94. package/dist/nodes/parallel-node.js.map +1 -0
  95. package/dist/nodes/roadmap-node.d.ts +76 -0
  96. package/dist/nodes/roadmap-node.d.ts.map +1 -0
  97. package/dist/nodes/roadmap-node.js +788 -0
  98. package/dist/nodes/roadmap-node.js.map +1 -0
  99. package/dist/nodes/swimlane-node.d.ts +38 -0
  100. package/dist/nodes/swimlane-node.d.ts.map +1 -0
  101. package/dist/nodes/swimlane-node.js +308 -0
  102. package/dist/nodes/swimlane-node.js.map +1 -0
  103. package/dist/renderable.d.ts +44 -0
  104. package/dist/renderable.d.ts.map +1 -0
  105. package/dist/renderable.js +21 -0
  106. package/dist/renderable.js.map +1 -0
  107. package/dist/row-packer.d.ts +125 -0
  108. package/dist/row-packer.d.ts.map +1 -0
  109. package/dist/row-packer.js +169 -0
  110. package/dist/row-packer.js.map +1 -0
  111. package/dist/style-resolution.d.ts +14 -0
  112. package/dist/style-resolution.d.ts.map +1 -0
  113. package/dist/style-resolution.js +191 -0
  114. package/dist/style-resolution.js.map +1 -0
  115. package/dist/themes/dark.d.ts +4 -0
  116. package/dist/themes/dark.d.ts.map +1 -0
  117. package/dist/themes/dark.js +241 -0
  118. package/dist/themes/dark.js.map +1 -0
  119. package/dist/themes/grayscale.d.ts +4 -0
  120. package/dist/themes/grayscale.d.ts.map +1 -0
  121. package/dist/themes/grayscale.js +237 -0
  122. package/dist/themes/grayscale.js.map +1 -0
  123. package/dist/themes/index.d.ts +19 -0
  124. package/dist/themes/index.d.ts.map +1 -0
  125. package/dist/themes/index.js +57 -0
  126. package/dist/themes/index.js.map +1 -0
  127. package/dist/themes/light.d.ts +4 -0
  128. package/dist/themes/light.d.ts.map +1 -0
  129. package/dist/themes/light.js +248 -0
  130. package/dist/themes/light.js.map +1 -0
  131. package/dist/themes/shape.d.ts +194 -0
  132. package/dist/themes/shape.d.ts.map +1 -0
  133. package/dist/themes/shape.js +6 -0
  134. package/dist/themes/shape.js.map +1 -0
  135. package/dist/themes/shared.d.ts +145 -0
  136. package/dist/themes/shared.d.ts.map +1 -0
  137. package/dist/themes/shared.js +310 -0
  138. package/dist/themes/shared.js.map +1 -0
  139. package/dist/time-scale.d.ts +39 -0
  140. package/dist/time-scale.d.ts.map +1 -0
  141. package/dist/time-scale.js +62 -0
  142. package/dist/time-scale.js.map +1 -0
  143. package/dist/types.d.ts +516 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +6 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/view-preset.d.ts +23 -0
  148. package/dist/view-preset.d.ts.map +1 -0
  149. package/dist/view-preset.js +146 -0
  150. package/dist/view-preset.js.map +1 -0
  151. package/dist/working-calendar.d.ts +14 -0
  152. package/dist/working-calendar.d.ts.map +1 -0
  153. package/dist/working-calendar.js +74 -0
  154. package/dist/working-calendar.js.map +1 -0
  155. package/package.json +43 -0
  156. package/src/band-scale.ts +115 -0
  157. package/src/calendar.ts +244 -0
  158. package/src/capacity.ts +191 -0
  159. package/src/dsl-utils.ts +30 -0
  160. package/src/edge-routing.ts +550 -0
  161. package/src/frame-tab-geometry.ts +165 -0
  162. package/src/header-card-geometry.ts +48 -0
  163. package/src/i18n.ts +124 -0
  164. package/src/include-chrome-geometry.ts +156 -0
  165. package/src/index.ts +118 -0
  166. package/src/inline-date-pin-geometry.ts +196 -0
  167. package/src/item-bar-geometry.ts +271 -0
  168. package/src/lane-utilization.ts +259 -0
  169. package/src/layout-context.ts +182 -0
  170. package/src/layout.ts +1530 -0
  171. package/src/nodes/anchor-node.ts +77 -0
  172. package/src/nodes/footnote-node.ts +60 -0
  173. package/src/nodes/group-node.ts +260 -0
  174. package/src/nodes/include-node.ts +168 -0
  175. package/src/nodes/item-node.ts +171 -0
  176. package/src/nodes/marker-geometry.ts +43 -0
  177. package/src/nodes/milestone-node.ts +263 -0
  178. package/src/nodes/parallel-node.ts +110 -0
  179. package/src/nodes/roadmap-node.ts +957 -0
  180. package/src/nodes/swimlane-node.ts +423 -0
  181. package/src/renderable.ts +68 -0
  182. package/src/row-packer.ts +271 -0
  183. package/src/style-resolution.ts +243 -0
  184. package/src/themes/dark.ts +244 -0
  185. package/src/themes/grayscale.ts +240 -0
  186. package/src/themes/index.ts +66 -0
  187. package/src/themes/light.ts +251 -0
  188. package/src/themes/shape.ts +230 -0
  189. package/src/themes/shared.ts +369 -0
  190. package/src/time-scale.ts +78 -0
  191. package/src/types.ts +641 -0
  192. package/src/view-preset.ts +180 -0
  193. package/src/working-calendar.ts +91 -0
package/src/types.ts ADDED
@@ -0,0 +1,641 @@
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
+ /**
227
+ * One inline-date pin (`after:DATE` / `before:DATE`) attached to an entity.
228
+ * The renderer paints a small calendar glyph at `glyphTopLeft` with side
229
+ * length `glyphSize`, fills with the entity's resolved meta color, and emits
230
+ * a `<title>` child carrying `isoDate` for native browser tooltips. There is
231
+ * no inline date caption — see specs/rendering.md "Inline-date glyph".
232
+ *
233
+ * `side` decides which corner the glyph belongs in (top-LEFT for `after`,
234
+ * top-RIGHT for `before`). The validator enforces "at most one inline date
235
+ * per direction" so `inlineDatePins` carries 0..2 entries.
236
+ *
237
+ * `spilled === true` means the entity (item bar) was too narrow to host the
238
+ * glyph inside its decoration row, so `glyphTopLeft` points at the spill
239
+ * column outside the bar instead. Group and parallel containers never spill;
240
+ * they always have room for the glyph in their bounding box.
241
+ */
242
+ export interface InlineDatePin {
243
+ side: 'after' | 'before';
244
+ isoDate: string;
245
+ glyphTopLeft: Point;
246
+ glyphSize: number;
247
+ spilled: boolean;
248
+ }
249
+
250
+ export interface PositionedItem {
251
+ kind: 'item';
252
+ id?: string;
253
+ title: string;
254
+ box: BoundingBox;
255
+ status: StatusKind;
256
+ progressFraction: number; // 0..1; 1 == fully filled
257
+ footnoteIndicators: number[]; // 1-based superscript numbers, empty when no footnotes
258
+ labelChips: PositionedLabelChip[];
259
+ /** Inline-date pins attached to this item (`after:DATE` / `before:DATE`).
260
+ * Empty (or undefined) when the item has no inline-date pins. See
261
+ * `InlineDatePin` and specs/rendering.md "Inline-date glyph". */
262
+ inlineDatePins?: InlineDatePin[];
263
+ /** True when the chip row's natural total width exceeded the bar's
264
+ * effective inner width and the whole row spilled past the bar's
265
+ * right edge. The chips' `box.x` already reflects the spilled
266
+ * position; this flag exists for the row-packer to reserve the
267
+ * spilled extent and for the renderer / debug overlays to know
268
+ * the row sits outside the bar's painted footprint. */
269
+ chipsOutside: boolean;
270
+ /** Logical right x reached by the chip row, INCLUDING the chips
271
+ * whether painted inside or outside the bar. Equals the start x
272
+ * when there are no chips. The row-packer's spill reservation
273
+ * uses this to grow the chart canvas / bump siblings. */
274
+ chipsRightX: number;
275
+ linkIcon?: LinkIconKind;
276
+ linkHref?: string;
277
+ hasOverflow: boolean; // true when before: forced the item past its natural end
278
+ overflowBox?: BoundingBox; // the offending tail, flagged red
279
+ // The id of the `before:` anchor/milestone the item overran. Used by the
280
+ // renderer to caption the overflow tail ("past <id>").
281
+ overflowAnchorId?: string;
282
+ owner?: string; // owner id (person/team) for annotation
283
+ description?: string;
284
+ // Pre-formatted secondary line shown under the title inside the item bar
285
+ // (e.g. "1w" or "2w — 50% remaining"). Layout assembles this so the
286
+ // renderer stays palette-and-string-dumb.
287
+ metaText?: string;
288
+ // 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
290
+ // one wouldn't fit inside the bar, BOTH get drawn beside the bar
291
+ // (stacked, just past its right edge) so they read as one caption
292
+ // rather than splitting across the bar boundary. The layout also
293
+ // bumps the next item to a fresh row so the spilled caption has
294
+ // empty space to occupy.
295
+ textSpills: boolean;
296
+ /** True when the bar is too narrow to host the status dot inside
297
+ * with its full inset (`MIN_BAR_WIDTH_FOR_DOT_PX`). The dot
298
+ * renders in the spill column to the right of the bar instead
299
+ * of overshooting the bar's left edge. */
300
+ dotSpills: boolean;
301
+ /** True when the bar is too narrow to host the link-icon tile
302
+ * inside without colliding with the status dot column
303
+ * (`MIN_BAR_WIDTH_FOR_LINK_AND_DOT_PX`). The icon spills out and
304
+ * renders ahead of the (also-spilled) title so the icon stays
305
+ * visually attached to the title text. Implies `textSpills`. */
306
+ iconSpills: boolean;
307
+ /** True when the bar is too narrow to host the footnote
308
+ * superscript at its inset-right position. The indicator(s)
309
+ * render in the spill column trailing the title text instead
310
+ * of at the bar's upper-right corner. */
311
+ footnoteSpills: boolean;
312
+ /** Pre-computed x positions for the spilled decorations. `null`
313
+ * when the matching `*Spills` flag is false (decoration stays
314
+ * inside the bar at its inset-anchored position). */
315
+ dotSpillCx: number | null;
316
+ iconSpillX: number | null;
317
+ /** First footnote indicator's left edge in the spill column.
318
+ * Subsequent indicators walk right by `ITEM_FOOTNOTE_INDICATOR_STEP_PX`. */
319
+ footnoteSpillStartX: number | null;
320
+ /** Right edge of the spilled-decoration cluster (inclusive of
321
+ * spilled title and footnote glyphs). Used by the row-packer
322
+ * to size the row's spill reservation so the next chained item
323
+ * doesn't land underneath. */
324
+ decorationsRightX: number;
325
+ /**
326
+ * Capacity suffix data when the item declares `capacity:N`. Null when
327
+ * the item has no capacity, the value is non-positive, or the resolved
328
+ * `capacity-icon` is `none` and no number should render either way.
329
+ *
330
+ * Renders alongside the item's `metaText` (or stand-alone when no meta
331
+ * is present) per specs/rendering.md § Item capacity suffix. The `text`
332
+ * is the formatted number (`'5'`, `'0.5'`, `'1.25'`); `icon` tells the
333
+ * renderer which glyph to draw and whether to use the SVG library, the
334
+ * `×` text node, or an inline literal.
335
+ */
336
+ capacity: PositionedCapacity | null;
337
+ /**
338
+ * Resolved size when the item declared `size:NAME`. Null when the item
339
+ * sized itself with a literal `duration:` or didn't declare a size at
340
+ * all. Used by the renderer for the size chip on the meta line (m6) and
341
+ * by the layout for capacity-aware duration derivation (m5).
342
+ */
343
+ size: ResolvedSize | null;
344
+ style: ResolvedStyle;
345
+ }
346
+
347
+ /**
348
+ * Positioned capacity suffix shared by `PositionedItem` and (in m7) the lane
349
+ * frame-tab badge. The shape stays small and serializable: a formatted number
350
+ * string plus a discriminated union for the glyph. The renderer paints both.
351
+ *
352
+ * `icon === null` means the resolved `capacity-icon` was `'none'` — render
353
+ * the bare number with no glyph or separator.
354
+ */
355
+ export interface PositionedCapacity {
356
+ /** Numeric capacity, post-percent-sugar conversion (e.g. `50%` → 0.5). */
357
+ value: number;
358
+ /** Display string per spec number-formatting rules (`'5'`, `'0.5'`). */
359
+ text: string;
360
+ /** Resolved glyph instruction, or `null` when the icon is `'none'`. */
361
+ icon: ResolvedCapacityIconRef | null;
362
+ }
363
+
364
+ /**
365
+ * Renderer-facing capacity-icon reference. Mirrors the layout-internal
366
+ * `ResolvedCapacityIcon` from `capacity.ts` but lives in the shared
367
+ * positioned-model types so the renderer can read it without importing
368
+ * layout internals.
369
+ */
370
+ export type ResolvedCapacityIconRef =
371
+ | { kind: 'builtin'; name: 'multiplier' | 'person' | 'people' | 'points' | 'time' }
372
+ | { kind: 'literal'; text: string };
373
+
374
+ export type LinkIconKind = 'linear' | 'github' | 'jira' | 'generic' | 'none';
375
+
376
+ export interface PositionedLabelChip {
377
+ text: string;
378
+ style: ResolvedStyle;
379
+ // Chip box is laid out inside the item bar; coordinates are absolute.
380
+ box: BoundingBox;
381
+ }
382
+
383
+ export interface PositionedGroup {
384
+ kind: 'group';
385
+ id?: string;
386
+ title?: string;
387
+ box: BoundingBox;
388
+ // Bracket is drawn on the left edge when style.bracket != 'none'.
389
+ children: PositionedTrackChild[];
390
+ /** Inline-date pins attached to this group (`after:DATE` / `before:DATE`).
391
+ * See `InlineDatePin` and specs/rendering.md "Inline-date glyph". */
392
+ inlineDatePins?: InlineDatePin[];
393
+ style: ResolvedStyle;
394
+ }
395
+
396
+ export interface PositionedParallel {
397
+ kind: 'parallel';
398
+ id?: string;
399
+ title?: string;
400
+ box: BoundingBox;
401
+ children: PositionedTrackChild[]; // sub-tracks stacked vertically
402
+ /** Inline-date pins attached to this parallel (`after:DATE` / `before:DATE`).
403
+ * See `InlineDatePin` and specs/rendering.md "Inline-date glyph". */
404
+ inlineDatePins?: InlineDatePin[];
405
+ style: ResolvedStyle;
406
+ }
407
+
408
+ export type PositionedTrackChild = PositionedItem | PositionedParallel | PositionedGroup;
409
+
410
+ export interface PositionedSwimlane {
411
+ id?: string;
412
+ title: string; // display name; falls back to id
413
+ box: BoundingBox;
414
+ bandIndex: number; // zero-based; even/odd drives tint
415
+ children: PositionedTrackChild[];
416
+ nested: PositionedSwimlane[]; // recursive sub-swimlanes
417
+ style: ResolvedStyle;
418
+ // Owner display string ("Platform Team", "Sam Chen") rendered inside
419
+ // the frame tab. Resolved from team/person id → title.
420
+ owner?: string;
421
+ // Footnote indicator numbers attached to this swimlane (via `on:` in the
422
+ // footnote declaration). Rendered in the upper-right of the frame tab.
423
+ footnoteIndicators: number[];
424
+ /**
425
+ * Lane-level capacity badge data when the swimlane declares
426
+ * `capacity:N`. Null when no capacity is set or the value parses to
427
+ * zero. The renderer paints the badge inside the frame tab after the
428
+ * owner badge (or after the lane title when no owner is present),
429
+ * per specs/rendering.md § Lane capacity badge. m8 (overload sweep)
430
+ * also reads `value` to compute load against item capacities.
431
+ *
432
+ * `capacity-icon:none` resolves to `icon: null` here (just the bare
433
+ * number renders, no glyph) but the badge still appears. Authors who
434
+ * want the badge fully hidden simply omit `capacity:`.
435
+ */
436
+ capacity: PositionedCapacity | null;
437
+ /**
438
+ * Tri-state utilization underline data when the lane declares
439
+ * `capacity:` AND has at least one item contributing load AND has not
440
+ * opted out of every color band via `utilization-*-at:none`. Null
441
+ * otherwise — the renderer paints no underline.
442
+ *
443
+ * Computed in m12 by `computeLaneUtilization` (see
444
+ * `lane-utilization.ts`) per specs/rendering.md § Lane utilization
445
+ * underline. The renderer paints one rectangle per coalesced segment
446
+ * along the bottom edge of the band, colored from the resolved theme's
447
+ * `swimlane.utilizationOk` / `…Warn` / `…Over` tokens.
448
+ */
449
+ utilization: PositionedLaneUtilization | null;
450
+ }
451
+
452
+ /**
453
+ * One classification for a lane utilization segment. `green` includes the
454
+ * zero-load case so the underline reads as a continuous health bar; the spec
455
+ * intentionally avoids a separate "idle" color.
456
+ */
457
+ export type UtilizationClassification = 'green' | 'yellow' | 'red';
458
+
459
+ /**
460
+ * One half-open `[startX, endX)` segment of the lane's utilization underline,
461
+ * pre-classified for the renderer. Adjacent same-classification segments are
462
+ * coalesced upstream so the renderer paints one rectangle per visible color
463
+ * band rather than per event boundary.
464
+ *
465
+ * `load` is the absolute concurrent capacity at this segment (sum of active
466
+ * items' `capacity:` values). Useful for tooltips and debug overlays; the
467
+ * paint color is determined by `classification` alone.
468
+ */
469
+ export interface PositionedUtilizationSegment {
470
+ startX: number;
471
+ endX: number;
472
+ load: number;
473
+ classification: UtilizationClassification;
474
+ }
475
+
476
+ /**
477
+ * Resolved utilization model for a swimlane. Carries the segment list plus
478
+ * the resolved thresholds and capacity so downstream consumers (renderer,
479
+ * tooltips, exporters) can describe the model without re-resolving config.
480
+ *
481
+ * `warnFraction` / `overFraction`: each is either a positive fraction of
482
+ * `capacityValue` (the lane paints that color band when load reaches the
483
+ * fraction) or `null` to mean "this color band is opted out via
484
+ * `utilization-*-at:none`".
485
+ */
486
+ export interface PositionedLaneUtilization {
487
+ segments: PositionedUtilizationSegment[];
488
+ capacityValue: number;
489
+ warnFraction: number | null;
490
+ overFraction: number | null;
491
+ }
492
+
493
+ export interface PositionedAnchor {
494
+ id?: string;
495
+ title: string;
496
+ center: Point; // diamond center (post-collision-resolution)
497
+ radius: number;
498
+ style: ResolvedStyle;
499
+ // Non-binding predecessor edges: small arrows from prior items, drawn by renderer.
500
+ predecessorPoints: Point[];
501
+ // Vertical span of the anchor's "cut line" through the swimlane area,
502
+ // drawn by the renderer after items so it overlays the lane fills.
503
+ cutTopY: number;
504
+ cutBottomY: number;
505
+ // True when this anchor was bumped above the in-row baseline because a
506
+ // milestone shares the same x-column.
507
+ bumpedUp: boolean;
508
+ // Resolved label placement. The marker-row packer decides whether the
509
+ // title sits to the right (default) or the left of the diamond, and
510
+ // whether the entity drops to a lower row to avoid colliding with
511
+ // earlier markers' label boxes. Renderer uses `labelBox.x/y` directly
512
+ // (start-anchored text) — no further geometry decisions.
513
+ labelBox: BoundingBox;
514
+ labelSide: 'left' | 'right';
515
+ }
516
+
517
+ export interface PositionedMilestone {
518
+ id?: string;
519
+ title: string;
520
+ center: Point;
521
+ radius: number;
522
+ fixed: boolean; // true for date: style, false for after: style
523
+ // One slack arrow per non-binding predecessor. Each entry's (x, y) is
524
+ // the predecessor's right-edge midpoint; the arrow runs horizontally
525
+ // from there to (center.x - 6) at y. Empty / undefined when the
526
+ // milestone has zero or one predecessor.
527
+ slackArrows?: Array<{ x: number; y: number }>;
528
+ isOverrun: boolean; // true when the aggregated predecessor end exceeds `date:`
529
+ style: ResolvedStyle;
530
+ // Vertical span of the milestone's cut line through the swimlane area.
531
+ cutTopY: number;
532
+ cutBottomY: number;
533
+ // See PositionedAnchor.labelBox — same packing logic applies.
534
+ labelBox: BoundingBox;
535
+ labelSide: 'left' | 'right';
536
+ }
537
+
538
+ /**
539
+ * Result of packing a marker-row entity (anchor or milestone) into the
540
+ * dynamic row stack. `rowIndex == 0` is the in-row baseline; positive
541
+ * indices push the diamond DOWN by `step` px each. The label box is
542
+ * absolute and already accounts for left/right side flipping when the
543
+ * preferred side would overflow the chart.
544
+ */
545
+ export interface MarkerRowPlacement {
546
+ rowIndex: number;
547
+ centerY: number;
548
+ labelBox: BoundingBox;
549
+ labelSide: 'left' | 'right';
550
+ }
551
+
552
+ /**
553
+ * Horizontal corridor occupied by a milestone's slack arrow. Sits at
554
+ * the slack predecessor's row Y, running from the predecessor's right
555
+ * edge to the milestone's column. Items whose natural placement would
556
+ * intersect this band must drop to a row whose Y does not match `y`,
557
+ * so the arrow has clear horizontal space to travel.
558
+ */
559
+ export interface SlackCorridor {
560
+ xStart: number; // slack pred's right edge (logical chart x)
561
+ xEnd: number; // binding pred's right edge / milestone center.x
562
+ y: number; // slack pred's row midpoint
563
+ slackPredId: string; // exempt from bumping (owns the arrow's origin)
564
+ milestoneId: string;
565
+ }
566
+
567
+ export interface PositionedDependencyEdge {
568
+ fromId: string;
569
+ 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';
583
+ style: ResolvedStyle;
584
+ }
585
+
586
+ export interface PositionedFootnoteIndicator {
587
+ // Not a separate geometry in the chart — rendered as a superscript on the
588
+ // host item. Kept in the model for completeness, keyed to the host item id.
589
+ number: number;
590
+ hostItemId: string;
591
+ style: ResolvedStyle;
592
+ }
593
+
594
+ export interface PositionedFootnoteArea {
595
+ box: BoundingBox;
596
+ entries: PositionedFootnoteEntry[];
597
+ }
598
+
599
+ export interface PositionedFootnoteEntry {
600
+ number: number;
601
+ title: string;
602
+ description?: string;
603
+ style: ResolvedStyle;
604
+ }
605
+
606
+ export interface PositionedIncludeRegion {
607
+ sourcePath: string; // relative to the parent file
608
+ label: string; // e.g. the child roadmap's title or basename
609
+ box: BoundingBox;
610
+ // Nested swimlanes laid out inside the region. They share the parent's
611
+ // timeline (originX, pixelsPerDay) so cross-region dates align with the
612
+ // tick row above the region.
613
+ nestedSwimlanes: PositionedSwimlane[];
614
+ style: ResolvedStyle;
615
+ }
616
+
617
+ // Top-level result handed to the renderer.
618
+ export interface PositionedRoadmap {
619
+ width: number;
620
+ height: number;
621
+ theme: import('./themes/index.js').ThemeName;
622
+ /**
623
+ * Resolved palette — every color the renderer reads. m2.5d moved
624
+ * theme resolution into the layout side, so the renderer no longer
625
+ * branches on `theme === 'dark'`. The `theme` field above stays for
626
+ * `data-theme` SVG attribution; all color decisions read `palette`.
627
+ */
628
+ palette: import('./themes/index.js').Theme;
629
+ backgroundColor: string; // resolved from theme.surface.page
630
+ header: PositionedHeader;
631
+ timeline: PositionedTimelineScale;
632
+ nowline: PositionedNowline | null;
633
+ swimlanes: PositionedSwimlane[];
634
+ anchors: PositionedAnchor[];
635
+ milestones: PositionedMilestone[];
636
+ edges: PositionedDependencyEdge[];
637
+ footnotes: PositionedFootnoteArea;
638
+ includes: PositionedIncludeRegion[];
639
+ // Frame (chart area) in chart-space. Useful for renderer overlays.
640
+ chartBox: BoundingBox;
641
+ }