@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.
- package/LICENSE +190 -0
- package/README.md +103 -0
- package/dist/band-scale.d.ts +56 -0
- package/dist/band-scale.d.ts.map +1 -0
- package/dist/band-scale.js +86 -0
- package/dist/band-scale.js.map +1 -0
- package/dist/calendar.d.ts +79 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +210 -0
- package/dist/calendar.js.map +1 -0
- package/dist/capacity.d.ts +72 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +163 -0
- package/dist/capacity.js.map +1 -0
- package/dist/dsl-utils.d.ts +5 -0
- package/dist/dsl-utils.d.ts.map +1 -0
- package/dist/dsl-utils.js +28 -0
- package/dist/dsl-utils.js.map +1 -0
- package/dist/edge-routing.d.ts +89 -0
- package/dist/edge-routing.d.ts.map +1 -0
- package/dist/edge-routing.js +435 -0
- package/dist/edge-routing.js.map +1 -0
- package/dist/frame-tab-geometry.d.ts +78 -0
- package/dist/frame-tab-geometry.d.ts.map +1 -0
- package/dist/frame-tab-geometry.js +115 -0
- package/dist/frame-tab-geometry.js.map +1 -0
- package/dist/header-card-geometry.d.ts +29 -0
- package/dist/header-card-geometry.d.ts.map +1 -0
- package/dist/header-card-geometry.js +41 -0
- package/dist/header-card-geometry.js.map +1 -0
- package/dist/i18n.d.ts +48 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +114 -0
- package/dist/i18n.js.map +1 -0
- package/dist/include-chrome-geometry.d.ts +86 -0
- package/dist/include-chrome-geometry.d.ts.map +1 -0
- package/dist/include-chrome-geometry.js +104 -0
- package/dist/include-chrome-geometry.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/item-bar-geometry.d.ts +127 -0
- package/dist/item-bar-geometry.d.ts.map +1 -0
- package/dist/item-bar-geometry.js +173 -0
- package/dist/item-bar-geometry.js.map +1 -0
- package/dist/lane-utilization.d.ts +90 -0
- package/dist/lane-utilization.d.ts.map +1 -0
- package/dist/lane-utilization.js +206 -0
- package/dist/lane-utilization.js.map +1 -0
- package/dist/layout-context.d.ts +143 -0
- package/dist/layout-context.d.ts.map +1 -0
- package/dist/layout-context.js +8 -0
- package/dist/layout-context.js.map +1 -0
- package/dist/layout.d.ts +18 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +1213 -0
- package/dist/layout.js.map +1 -0
- package/dist/nodes/anchor-node.d.ts +16 -0
- package/dist/nodes/anchor-node.d.ts.map +1 -0
- package/dist/nodes/anchor-node.js +68 -0
- package/dist/nodes/anchor-node.js.map +1 -0
- package/dist/nodes/footnote-node.d.ts +10 -0
- package/dist/nodes/footnote-node.d.ts.map +1 -0
- package/dist/nodes/footnote-node.js +41 -0
- package/dist/nodes/footnote-node.js.map +1 -0
- package/dist/nodes/group-node.d.ts +29 -0
- package/dist/nodes/group-node.d.ts.map +1 -0
- package/dist/nodes/group-node.js +187 -0
- package/dist/nodes/group-node.js.map +1 -0
- package/dist/nodes/include-node.d.ts +16 -0
- package/dist/nodes/include-node.d.ts.map +1 -0
- package/dist/nodes/include-node.js +117 -0
- package/dist/nodes/include-node.js.map +1 -0
- package/dist/nodes/item-node.d.ts +51 -0
- package/dist/nodes/item-node.d.ts.map +1 -0
- package/dist/nodes/item-node.js +108 -0
- package/dist/nodes/item-node.js.map +1 -0
- package/dist/nodes/marker-geometry.d.ts +22 -0
- package/dist/nodes/marker-geometry.d.ts.map +1 -0
- package/dist/nodes/marker-geometry.js +38 -0
- package/dist/nodes/marker-geometry.js.map +1 -0
- package/dist/nodes/milestone-node.d.ts +48 -0
- package/dist/nodes/milestone-node.d.ts.map +1 -0
- package/dist/nodes/milestone-node.js +210 -0
- package/dist/nodes/milestone-node.js.map +1 -0
- package/dist/nodes/parallel-node.d.ts +21 -0
- package/dist/nodes/parallel-node.d.ts.map +1 -0
- package/dist/nodes/parallel-node.js +80 -0
- package/dist/nodes/parallel-node.js.map +1 -0
- package/dist/nodes/roadmap-node.d.ts +76 -0
- package/dist/nodes/roadmap-node.d.ts.map +1 -0
- package/dist/nodes/roadmap-node.js +788 -0
- package/dist/nodes/roadmap-node.js.map +1 -0
- package/dist/nodes/swimlane-node.d.ts +38 -0
- package/dist/nodes/swimlane-node.d.ts.map +1 -0
- package/dist/nodes/swimlane-node.js +308 -0
- package/dist/nodes/swimlane-node.js.map +1 -0
- package/dist/renderable.d.ts +44 -0
- package/dist/renderable.d.ts.map +1 -0
- package/dist/renderable.js +21 -0
- package/dist/renderable.js.map +1 -0
- package/dist/row-packer.d.ts +125 -0
- package/dist/row-packer.d.ts.map +1 -0
- package/dist/row-packer.js +169 -0
- package/dist/row-packer.js.map +1 -0
- package/dist/style-resolution.d.ts +14 -0
- package/dist/style-resolution.d.ts.map +1 -0
- package/dist/style-resolution.js +191 -0
- package/dist/style-resolution.js.map +1 -0
- package/dist/themes/dark.d.ts +4 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +241 -0
- package/dist/themes/dark.js.map +1 -0
- package/dist/themes/index.d.ts +15 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +30 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/light.d.ts +4 -0
- package/dist/themes/light.d.ts.map +1 -0
- package/dist/themes/light.js +248 -0
- package/dist/themes/light.js.map +1 -0
- package/dist/themes/shape.d.ts +194 -0
- package/dist/themes/shape.d.ts.map +1 -0
- package/dist/themes/shape.js +6 -0
- package/dist/themes/shape.js.map +1 -0
- package/dist/themes/shared.d.ts +145 -0
- package/dist/themes/shared.d.ts.map +1 -0
- package/dist/themes/shared.js +310 -0
- package/dist/themes/shared.js.map +1 -0
- package/dist/time-scale.d.ts +39 -0
- package/dist/time-scale.d.ts.map +1 -0
- package/dist/time-scale.js +62 -0
- package/dist/time-scale.js.map +1 -0
- package/dist/types.d.ts +483 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/view-preset.d.ts +23 -0
- package/dist/view-preset.d.ts.map +1 -0
- package/dist/view-preset.js +146 -0
- package/dist/view-preset.js.map +1 -0
- package/dist/working-calendar.d.ts +14 -0
- package/dist/working-calendar.d.ts.map +1 -0
- package/dist/working-calendar.js +74 -0
- package/dist/working-calendar.js.map +1 -0
- package/package.json +37 -0
- package/src/band-scale.ts +115 -0
- package/src/calendar.ts +244 -0
- package/src/capacity.ts +191 -0
- package/src/dsl-utils.ts +30 -0
- package/src/edge-routing.ts +550 -0
- package/src/frame-tab-geometry.ts +165 -0
- package/src/header-card-geometry.ts +48 -0
- package/src/i18n.ts +124 -0
- package/src/include-chrome-geometry.ts +156 -0
- package/src/index.ts +116 -0
- package/src/item-bar-geometry.ts +222 -0
- package/src/lane-utilization.ts +259 -0
- package/src/layout-context.ts +182 -0
- package/src/layout.ts +1446 -0
- package/src/nodes/anchor-node.ts +77 -0
- package/src/nodes/footnote-node.ts +60 -0
- package/src/nodes/group-node.ts +252 -0
- package/src/nodes/include-node.ts +168 -0
- package/src/nodes/item-node.ts +171 -0
- package/src/nodes/marker-geometry.ts +43 -0
- package/src/nodes/milestone-node.ts +263 -0
- package/src/nodes/parallel-node.ts +101 -0
- package/src/nodes/roadmap-node.ts +957 -0
- package/src/nodes/swimlane-node.ts +423 -0
- package/src/renderable.ts +68 -0
- package/src/row-packer.ts +271 -0
- package/src/style-resolution.ts +243 -0
- package/src/themes/dark.ts +244 -0
- package/src/themes/index.ts +36 -0
- package/src/themes/light.ts +251 -0
- package/src/themes/shape.ts +230 -0
- package/src/themes/shared.ts +369 -0
- package/src/time-scale.ts +78 -0
- package/src/types.ts +607 -0
- package/src/view-preset.ts +180 -0
- package/src/working-calendar.ts +91 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// RowPacker — the topmost-fit row-pack engine shared by `SwimlaneNode`
|
|
2
|
+
// and `GroupNode`. Tracks rows of variable height (each row's height is
|
|
3
|
+
// the max of its placed children's heights, with a per-packer minimum)
|
|
4
|
+
// and bumps a child to a new row when it would collide with a sibling's
|
|
5
|
+
// `rightEdge`, caption `spillX`, or a slack-arrow corridor.
|
|
6
|
+
//
|
|
7
|
+
// Two-phase API per child:
|
|
8
|
+
// 1) `placeItem(...)` / `placeBlock(...)` decides which row the child
|
|
9
|
+
// lands on. The caller then sequences the child at the returned
|
|
10
|
+
// `(rowIndex, y)`, producing a fully positioned subtree.
|
|
11
|
+
// 2) `commitItem(...)` / `commitBlock(...)` records the placed child
|
|
12
|
+
// back into the packer so the row's `rightEdge` / `spillX` advance
|
|
13
|
+
// and the row's height grows to accommodate the new child.
|
|
14
|
+
//
|
|
15
|
+
// When a row's height grows after later rows already exist, the packer
|
|
16
|
+
// shifts every subsequent row (and every positioned child placed in it)
|
|
17
|
+
// downward by the delta. Items keep their row-anchored top edge; the
|
|
18
|
+
// row simply gets taller. This keeps the contract "all children in a
|
|
19
|
+
// row share that row's top y" intact while letting individual children
|
|
20
|
+
// grow vertically (e.g. wrapped label-chiclet stacks in m3).
|
|
21
|
+
|
|
22
|
+
import type { PositionedItem, PositionedTrackChild, SlackCorridor } from './types.js';
|
|
23
|
+
|
|
24
|
+
export interface PackedRow {
|
|
25
|
+
/** Top-y of the row in canvas px. */
|
|
26
|
+
y: number;
|
|
27
|
+
/** Visible height of the row. Grows with the tallest committed child. */
|
|
28
|
+
height: number;
|
|
29
|
+
/** Logical x of the rightmost extent reached by a committed child. */
|
|
30
|
+
rightEdge: number;
|
|
31
|
+
/** Reserved x for caption spill. Items whose `desiredStart < spillX`
|
|
32
|
+
* bump to a new row even if `rightEdge` would let them in. */
|
|
33
|
+
spillX: number;
|
|
34
|
+
/** Children placed in this row. Used when the row's height grows
|
|
35
|
+
* after later rows exist — every child in every later row shifts
|
|
36
|
+
* down by the delta. */
|
|
37
|
+
placedChildren: PositionedTrackChild[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RowPackerOptions {
|
|
41
|
+
/** Logical left x of the track this packer owns (lane left edge or
|
|
42
|
+
* group's content left). New rows reset their `rightEdge` /
|
|
43
|
+
* `spillX` to this value. */
|
|
44
|
+
laneLeftX: number;
|
|
45
|
+
/** Top-y of the first row. */
|
|
46
|
+
originY: number;
|
|
47
|
+
/** Minimum height of any row. Typically `bandScale.step()`. */
|
|
48
|
+
minRowHeight: number;
|
|
49
|
+
/** Slack-arrow corridors to avoid. Items whose vertical band
|
|
50
|
+
* intersects a corridor at the candidate row bump to a new row. */
|
|
51
|
+
slackCorridors: SlackCorridor[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Item placement query — passed to `placeItem` BEFORE sequencing. */
|
|
55
|
+
export interface ItemPlacementInput {
|
|
56
|
+
/** Item id; corridor-bumping is skipped for the corridor's own
|
|
57
|
+
* predecessor (the slack arrow's source item is exempt). */
|
|
58
|
+
childId: string;
|
|
59
|
+
/** Logical left x where the item wants to start. */
|
|
60
|
+
desiredStart: number;
|
|
61
|
+
/** Logical right x of the item's natural extent. */
|
|
62
|
+
desiredEnd: number;
|
|
63
|
+
/** Predicted intrinsic height of the item. Used to set the row's
|
|
64
|
+
* initial height when this is the first item in a fresh row, or
|
|
65
|
+
* to grow the row when a taller item lands in an existing row. */
|
|
66
|
+
predictedHeight: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Result of a `placeItem` query. */
|
|
70
|
+
export interface ItemPlacement {
|
|
71
|
+
rowIndex: number;
|
|
72
|
+
y: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Item-commit payload — passed to `commitItem` AFTER sequencing. */
|
|
76
|
+
export interface ItemCommitInput {
|
|
77
|
+
rowIndex: number;
|
|
78
|
+
placed: PositionedItem;
|
|
79
|
+
/** Logical right edge of the placed item. Includes the trailing
|
|
80
|
+
* `ITEM_INSET_PX` so the next chained item butts edge-to-edge in
|
|
81
|
+
* logical space (with a 2 × ITEM_INSET_PX visible gutter between
|
|
82
|
+
* bars). */
|
|
83
|
+
logicalEnd: number;
|
|
84
|
+
/** Reserved spill x for captions that overflow the bar's right
|
|
85
|
+
* edge. `null` resets the row's spill reservation to `laneLeftX`. */
|
|
86
|
+
spillReservation: number | null;
|
|
87
|
+
/** Total vertical reservation the placed item needs (e.g. bar
|
|
88
|
+
* height plus a multi-row spilled chip column hanging below the
|
|
89
|
+
* bar). When omitted, the row only grows to `placed.box.height`,
|
|
90
|
+
* which is fine for items whose entire content fits inside the
|
|
91
|
+
* bar. Set this to `predictedHeight` from `placeItem(...)` when
|
|
92
|
+
* the item has a chip overhang so the row grows even if it
|
|
93
|
+
* landed in an EXISTING row (where the placement step ignores
|
|
94
|
+
* `predictedHeight`). */
|
|
95
|
+
rowHeight?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Result of a `placeBlock` query (parallel/group as a child). */
|
|
99
|
+
export interface BlockPlacement {
|
|
100
|
+
rowIndex: number;
|
|
101
|
+
y: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Block-commit payload — passed to `commitBlock` AFTER sequencing. */
|
|
105
|
+
export interface BlockCommitInput {
|
|
106
|
+
rowIndex: number;
|
|
107
|
+
placed: PositionedTrackChild;
|
|
108
|
+
/** Total vertical extent the block occupies (the block's reported
|
|
109
|
+
* `box.height`). The row this block landed on grows to at least
|
|
110
|
+
* this height. */
|
|
111
|
+
blockHeight: number;
|
|
112
|
+
/** Logical right edge of the block. */
|
|
113
|
+
blockEnd: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class RowPacker {
|
|
117
|
+
public readonly rows: PackedRow[];
|
|
118
|
+
private readonly opts: RowPackerOptions;
|
|
119
|
+
|
|
120
|
+
constructor(opts: RowPackerOptions) {
|
|
121
|
+
this.opts = opts;
|
|
122
|
+
this.rows = [
|
|
123
|
+
{
|
|
124
|
+
y: opts.originY,
|
|
125
|
+
height: opts.minRowHeight,
|
|
126
|
+
rightEdge: opts.laneLeftX,
|
|
127
|
+
spillX: opts.laneLeftX,
|
|
128
|
+
placedChildren: [],
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Find the topmost row where the item's natural extent fits without
|
|
135
|
+
* colliding with sibling content or a slack corridor; appending a
|
|
136
|
+
* fresh row at the bottom when none of the existing rows fit.
|
|
137
|
+
*/
|
|
138
|
+
placeItem(input: ItemPlacementInput): ItemPlacement {
|
|
139
|
+
for (let i = 0; i < this.rows.length; i += 1) {
|
|
140
|
+
const r = this.rows[i];
|
|
141
|
+
if (input.desiredStart < r.rightEdge) continue;
|
|
142
|
+
if (input.desiredStart < r.spillX) continue;
|
|
143
|
+
if (this.rowIntersectsCorridor(r, input)) continue;
|
|
144
|
+
return { rowIndex: i, y: r.y };
|
|
145
|
+
}
|
|
146
|
+
const fresh = this.appendRow(Math.max(this.opts.minRowHeight, input.predictedHeight));
|
|
147
|
+
return { rowIndex: this.rows.length - 1, y: fresh.y };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Blocks (parallel / group) always claim a fresh row at the bottom
|
|
152
|
+
* of the stack so their inner sub-tracks have contiguous rows to
|
|
153
|
+
* expand into. If the bottom row is empty, we reuse it; otherwise
|
|
154
|
+
* we append.
|
|
155
|
+
*/
|
|
156
|
+
placeBlock(): BlockPlacement {
|
|
157
|
+
const last = this.rows[this.rows.length - 1];
|
|
158
|
+
if (last.rightEdge > this.opts.laneLeftX) {
|
|
159
|
+
const fresh = this.appendRow(this.opts.minRowHeight);
|
|
160
|
+
return { rowIndex: this.rows.length - 1, y: fresh.y };
|
|
161
|
+
}
|
|
162
|
+
return { rowIndex: this.rows.length - 1, y: last.y };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Record a placed item back into its row. Grows the row's height to
|
|
167
|
+
* fit the item, advances `rightEdge`, sets the caption spill, and
|
|
168
|
+
* shifts later rows down if this row got taller.
|
|
169
|
+
*/
|
|
170
|
+
commitItem(input: ItemCommitInput): void {
|
|
171
|
+
const row = this.rows[input.rowIndex];
|
|
172
|
+
const target = Math.max(input.placed.box.height, input.rowHeight ?? 0);
|
|
173
|
+
this.growRowHeight(input.rowIndex, target);
|
|
174
|
+
row.rightEdge = input.logicalEnd;
|
|
175
|
+
row.spillX = input.spillReservation ?? this.opts.laneLeftX;
|
|
176
|
+
row.placedChildren.push(input.placed);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Record a placed block. Sets the row's height to the block's full
|
|
181
|
+
* height (blocks own their row, no other child shares it).
|
|
182
|
+
*/
|
|
183
|
+
commitBlock(input: BlockCommitInput): void {
|
|
184
|
+
const row = this.rows[input.rowIndex];
|
|
185
|
+
this.growRowHeight(input.rowIndex, input.blockHeight);
|
|
186
|
+
row.rightEdge = input.blockEnd;
|
|
187
|
+
row.spillX = this.opts.laneLeftX;
|
|
188
|
+
row.placedChildren.push(input.placed);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Total vertical extent reached (last row's bottom - originY). */
|
|
192
|
+
usedHeight(): number {
|
|
193
|
+
const last = this.rows[this.rows.length - 1];
|
|
194
|
+
return last.y + last.height - this.opts.originY;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Rightmost extent reached — bar logical end OR caption spill,
|
|
198
|
+
* whichever is wider, across every row. Empty rows contribute
|
|
199
|
+
* `laneLeftX`. */
|
|
200
|
+
usedRightX(): number {
|
|
201
|
+
let x = this.opts.laneLeftX;
|
|
202
|
+
for (const r of this.rows) {
|
|
203
|
+
if (r.rightEdge > x) x = r.rightEdge;
|
|
204
|
+
if (r.spillX > x) x = r.spillX;
|
|
205
|
+
}
|
|
206
|
+
return x;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private rowIntersectsCorridor(r: PackedRow, p: ItemPlacementInput): boolean {
|
|
210
|
+
return this.opts.slackCorridors.some(
|
|
211
|
+
(c) =>
|
|
212
|
+
c.y >= r.y &&
|
|
213
|
+
c.y < r.y + r.height &&
|
|
214
|
+
p.desiredStart < c.xEnd &&
|
|
215
|
+
p.desiredEnd > c.xStart &&
|
|
216
|
+
c.slackPredId !== p.childId,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private appendRow(height: number): PackedRow {
|
|
221
|
+
const last = this.rows[this.rows.length - 1];
|
|
222
|
+
const r: PackedRow = {
|
|
223
|
+
y: last.y + last.height,
|
|
224
|
+
height,
|
|
225
|
+
rightEdge: this.opts.laneLeftX,
|
|
226
|
+
spillX: this.opts.laneLeftX,
|
|
227
|
+
placedChildren: [],
|
|
228
|
+
};
|
|
229
|
+
this.rows.push(r);
|
|
230
|
+
return r;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private growRowHeight(rowIndex: number, newHeight: number): void {
|
|
234
|
+
const row = this.rows[rowIndex];
|
|
235
|
+
const delta = newHeight - row.height;
|
|
236
|
+
if (delta <= 0) return;
|
|
237
|
+
row.height = newHeight;
|
|
238
|
+
for (let i = rowIndex + 1; i < this.rows.length; i += 1) {
|
|
239
|
+
const r = this.rows[i];
|
|
240
|
+
r.y += delta;
|
|
241
|
+
for (const child of r.placedChildren) {
|
|
242
|
+
shiftPositionedY(child, delta);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Walk a positioned subtree and shift every absolute y by `dy`. Used
|
|
250
|
+
* when an earlier row in the packer grows, retroactively pushing every
|
|
251
|
+
* later row (and its placed subtrees) down to keep them clear.
|
|
252
|
+
*
|
|
253
|
+
* Items: shift the bar box plus label chips and any overflow box.
|
|
254
|
+
* Groups / parallels: shift the container box and recurse into
|
|
255
|
+
* children. The caller's `box.y` already moved with the parent.
|
|
256
|
+
*/
|
|
257
|
+
function shiftPositionedY(p: PositionedTrackChild, dy: number): void {
|
|
258
|
+
p.box.y += dy;
|
|
259
|
+
if (p.kind === 'item') {
|
|
260
|
+
for (const chip of p.labelChips) {
|
|
261
|
+
chip.box.y += dy;
|
|
262
|
+
}
|
|
263
|
+
if (p.overflowBox) {
|
|
264
|
+
p.overflowBox.y += dy;
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
for (const child of p.children) {
|
|
269
|
+
shiftPositionedY(child, dy);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DefaultDeclaration,
|
|
3
|
+
EntityProperty,
|
|
4
|
+
LabelDeclaration,
|
|
5
|
+
StyleDeclaration,
|
|
6
|
+
StyleProperty,
|
|
7
|
+
} from '@nowline/core';
|
|
8
|
+
import { resolveColor } from './themes/index.js';
|
|
9
|
+
import type { EntityStyle, Theme } from './themes/shape.js';
|
|
10
|
+
import type { ResolvedStyle, SizeBucket } from './types.js';
|
|
11
|
+
|
|
12
|
+
type EntityTypeKey =
|
|
13
|
+
| 'roadmap'
|
|
14
|
+
| 'swimlane'
|
|
15
|
+
| 'item'
|
|
16
|
+
| 'parallel'
|
|
17
|
+
| 'group'
|
|
18
|
+
| 'anchor'
|
|
19
|
+
| 'milestone'
|
|
20
|
+
| 'footnote'
|
|
21
|
+
| 'label';
|
|
22
|
+
|
|
23
|
+
export interface StyleContext {
|
|
24
|
+
theme: Theme;
|
|
25
|
+
// Map of style id → StyleDeclaration (from resolveIncludes().config.styles
|
|
26
|
+
// or a fallback scan of the parent file).
|
|
27
|
+
styles: Map<string, StyleDeclaration>;
|
|
28
|
+
// Map of `default <entity>` per entity type. May be missing entries.
|
|
29
|
+
defaults: Map<string, DefaultDeclaration>;
|
|
30
|
+
// Map of label id → LabelDeclaration for resolving label styles.
|
|
31
|
+
labels: Map<string, LabelDeclaration>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function propKey(prop: { key: string }): string {
|
|
35
|
+
return prop.key.endsWith(':') ? prop.key.slice(0, -1) : prop.key;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function entityStyleToResolved(e: EntityStyle, theme: Theme): ResolvedStyle {
|
|
39
|
+
return {
|
|
40
|
+
bg: resolveColor(e.bg, theme),
|
|
41
|
+
fg: resolveColor(e.fg, theme),
|
|
42
|
+
text: resolveColor(e.text, theme),
|
|
43
|
+
border: e.border,
|
|
44
|
+
icon: e.icon,
|
|
45
|
+
shadow: e.shadow,
|
|
46
|
+
font: e.font,
|
|
47
|
+
weight: e.weight,
|
|
48
|
+
italic: e.italic,
|
|
49
|
+
textSize: e.textSize,
|
|
50
|
+
padding: e.padding,
|
|
51
|
+
spacing: e.spacing,
|
|
52
|
+
headerHeight: e.headerHeight,
|
|
53
|
+
cornerRadius: e.cornerRadius,
|
|
54
|
+
bracket: e.bracket,
|
|
55
|
+
// DSL does not expose header-position on non-roadmap entities; system
|
|
56
|
+
// default `beside` applies. Roadmap entity's resolve step lifts this
|
|
57
|
+
// from the 5-level chain.
|
|
58
|
+
headerPosition: 'beside',
|
|
59
|
+
capacityIcon: e.capacityIcon,
|
|
60
|
+
// Roadmap-only readability knobs. Defaults preserve the existing
|
|
61
|
+
// single-top-strip layout and keep the major-ticks-only grid.
|
|
62
|
+
timelinePosition: 'top',
|
|
63
|
+
minorGrid: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply a single style property (from `style` blocks, `default` blocks, or
|
|
68
|
+
// label styles) onto an accumulating ResolvedStyle.
|
|
69
|
+
function applyProp(target: ResolvedStyle, key: string, value: string, theme: Theme): void {
|
|
70
|
+
switch (key) {
|
|
71
|
+
case 'bg':
|
|
72
|
+
target.bg = resolveColor(value, theme);
|
|
73
|
+
break;
|
|
74
|
+
case 'fg':
|
|
75
|
+
target.fg = resolveColor(value, theme);
|
|
76
|
+
break;
|
|
77
|
+
case 'text':
|
|
78
|
+
target.text = resolveColor(value, theme);
|
|
79
|
+
break;
|
|
80
|
+
case 'border':
|
|
81
|
+
if (value === 'solid' || value === 'dashed' || value === 'dotted')
|
|
82
|
+
target.border = value;
|
|
83
|
+
break;
|
|
84
|
+
case 'icon':
|
|
85
|
+
target.icon = value;
|
|
86
|
+
break;
|
|
87
|
+
case 'shadow':
|
|
88
|
+
if (value === 'none' || value === 'subtle' || value === 'soft' || value === 'hard') {
|
|
89
|
+
target.shadow = value;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 'font':
|
|
93
|
+
if (value === 'sans' || value === 'serif' || value === 'mono') target.font = value;
|
|
94
|
+
break;
|
|
95
|
+
case 'weight':
|
|
96
|
+
if (value === 'thin' || value === 'light' || value === 'normal' || value === 'bold') {
|
|
97
|
+
target.weight = value;
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 'italic':
|
|
101
|
+
target.italic = value === 'true';
|
|
102
|
+
break;
|
|
103
|
+
case 'text-size':
|
|
104
|
+
target.textSize = coerceSize(value, 'xl') as ResolvedStyle['textSize'];
|
|
105
|
+
break;
|
|
106
|
+
case 'padding':
|
|
107
|
+
target.padding = coerceSize(value, 'xl') as ResolvedStyle['padding'];
|
|
108
|
+
break;
|
|
109
|
+
case 'spacing':
|
|
110
|
+
target.spacing = coerceSize(value, 'xl') as ResolvedStyle['spacing'];
|
|
111
|
+
break;
|
|
112
|
+
case 'header-height':
|
|
113
|
+
target.headerHeight = coerceSize(value, 'xl') as ResolvedStyle['headerHeight'];
|
|
114
|
+
break;
|
|
115
|
+
case 'corner-radius':
|
|
116
|
+
target.cornerRadius = coerceSize(value, 'full') as ResolvedStyle['cornerRadius'];
|
|
117
|
+
break;
|
|
118
|
+
case 'bracket':
|
|
119
|
+
if (value === 'none' || value === 'solid' || value === 'dashed') target.bracket = value;
|
|
120
|
+
break;
|
|
121
|
+
case 'header-position':
|
|
122
|
+
if (value === 'beside' || value === 'above') target.headerPosition = value;
|
|
123
|
+
break;
|
|
124
|
+
case 'capacity-icon':
|
|
125
|
+
// Validator (rule 17e + checkSymbolReferences) has already verified
|
|
126
|
+
// the value is a built-in name, a declared symbol id, or an inline
|
|
127
|
+
// Unicode literal. Pass it through verbatim — interpretation
|
|
128
|
+
// happens in the renderer where we have access to ResolvedConfig.symbols.
|
|
129
|
+
target.capacityIcon = value;
|
|
130
|
+
break;
|
|
131
|
+
case 'timeline-position':
|
|
132
|
+
if (value === 'top' || value === 'bottom' || value === 'both') {
|
|
133
|
+
target.timelinePosition = value;
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case 'minor-grid':
|
|
137
|
+
target.minorGrid = value === 'true';
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function coerceSize(value: string, max: SizeBucket): SizeBucket {
|
|
145
|
+
const order: SizeBucket[] = ['none', 'xs', 'sm', 'md', 'lg', 'xl', 'full'];
|
|
146
|
+
const cap = order.indexOf(max);
|
|
147
|
+
const idx = order.indexOf(value as SizeBucket);
|
|
148
|
+
if (idx < 0) return 'md';
|
|
149
|
+
if (idx > cap) return order[cap];
|
|
150
|
+
return order[idx];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applyStyleDecl(
|
|
154
|
+
target: ResolvedStyle,
|
|
155
|
+
decl: StyleDeclaration | undefined,
|
|
156
|
+
theme: Theme,
|
|
157
|
+
): void {
|
|
158
|
+
if (!decl) return;
|
|
159
|
+
for (const p of decl.properties as StyleProperty[]) {
|
|
160
|
+
applyProp(target, propKey(p), p.value, theme);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyProperties(target: ResolvedStyle, props: EntityProperty[], theme: Theme): void {
|
|
165
|
+
for (const p of props) {
|
|
166
|
+
const key = propKey(p);
|
|
167
|
+
if (p.value !== undefined) {
|
|
168
|
+
applyProp(target, key, p.value, theme);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function applyLabelStyleRefs(
|
|
174
|
+
target: ResolvedStyle,
|
|
175
|
+
props: EntityProperty[],
|
|
176
|
+
ctx: StyleContext,
|
|
177
|
+
): void {
|
|
178
|
+
const labelsProp = props.find((p) => propKey(p) === 'labels');
|
|
179
|
+
if (!labelsProp) return;
|
|
180
|
+
const names: string[] = labelsProp.value ? [labelsProp.value] : labelsProp.values;
|
|
181
|
+
for (const name of names) {
|
|
182
|
+
const label = ctx.labels.get(name);
|
|
183
|
+
if (!label) continue;
|
|
184
|
+
// Label's `style:` ref gets applied.
|
|
185
|
+
const styleRef = label.properties.find((p) => propKey(p) === 'style');
|
|
186
|
+
if (styleRef?.value) {
|
|
187
|
+
applyStyleDecl(target, ctx.styles.get(styleRef.value), ctx.theme);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function applyEntityStyleRef(
|
|
193
|
+
target: ResolvedStyle,
|
|
194
|
+
props: EntityProperty[],
|
|
195
|
+
ctx: StyleContext,
|
|
196
|
+
): void {
|
|
197
|
+
const styleProp = props.find((p) => propKey(p) === 'style');
|
|
198
|
+
if (!styleProp?.value) return;
|
|
199
|
+
applyStyleDecl(target, ctx.styles.get(styleProp.value), ctx.theme);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Five-level precedence chain from specs/rendering.md § Style Precedence:
|
|
203
|
+
// 1. system default (theme's EntityStyle for this type)
|
|
204
|
+
// 2. config `default <entity>` properties
|
|
205
|
+
// 3. label `style:` refs (per applied label)
|
|
206
|
+
// 4. entity's own `style:` ref
|
|
207
|
+
// 5. inline style properties on the entity (banned by validator for roadmap
|
|
208
|
+
// entities; still supported for declared styles / defaults)
|
|
209
|
+
export function resolveStyle(
|
|
210
|
+
entityType: EntityTypeKey,
|
|
211
|
+
props: EntityProperty[],
|
|
212
|
+
ctx: StyleContext,
|
|
213
|
+
): ResolvedStyle {
|
|
214
|
+
const baseEntity = ctx.theme.entities[entityType];
|
|
215
|
+
const out = entityStyleToResolved(baseEntity, ctx.theme);
|
|
216
|
+
|
|
217
|
+
// Level 2: config defaults
|
|
218
|
+
const defaultDecl = ctx.defaults.get(entityType);
|
|
219
|
+
if (defaultDecl) {
|
|
220
|
+
for (const p of defaultDecl.properties) {
|
|
221
|
+
if (p.value !== undefined) {
|
|
222
|
+
applyProp(out, propKey(p), p.value, ctx.theme);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Level 3: label styles (labels on this entity, each label's `style:` ref)
|
|
228
|
+
applyLabelStyleRefs(out, props, ctx);
|
|
229
|
+
|
|
230
|
+
// Level 4: entity's own `style:` ref
|
|
231
|
+
applyEntityStyleRef(out, props, ctx);
|
|
232
|
+
|
|
233
|
+
// Level 5: inline props on the entity
|
|
234
|
+
applyProperties(out, props, ctx.theme);
|
|
235
|
+
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Resolve a label's *display* style (bg/fg/text + bracket etc.) as used on
|
|
240
|
+
// label chips rendered on items. The chip uses the label entity type.
|
|
241
|
+
export function resolveLabelChipStyle(label: LabelDeclaration, ctx: StyleContext): ResolvedStyle {
|
|
242
|
+
return resolveStyle('label', label.properties, ctx);
|
|
243
|
+
}
|