@pilates/core 1.0.1 → 2.0.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 (77) hide show
  1. package/dist/algorithm/cache.d.ts +7 -19
  2. package/dist/algorithm/cache.d.ts.map +1 -1
  3. package/dist/algorithm/cache.js +31 -27
  4. package/dist/algorithm/cache.js.map +1 -1
  5. package/dist/algorithm/index.d.ts +31 -0
  6. package/dist/algorithm/index.d.ts.map +1 -1
  7. package/dist/algorithm/index.js +105 -13
  8. package/dist/algorithm/index.js.map +1 -1
  9. package/dist/algorithm/round.d.ts +13 -0
  10. package/dist/algorithm/round.d.ts.map +1 -1
  11. package/dist/algorithm/round.js +33 -16
  12. package/dist/algorithm/round.js.map +1 -1
  13. package/dist/algorithm/spineless/classify.d.ts +47 -0
  14. package/dist/algorithm/spineless/classify.d.ts.map +1 -0
  15. package/dist/algorithm/spineless/classify.js +109 -0
  16. package/dist/algorithm/spineless/classify.js.map +1 -0
  17. package/dist/algorithm/spineless/field-id-pool.d.ts +28 -0
  18. package/dist/algorithm/spineless/field-id-pool.d.ts.map +1 -0
  19. package/dist/algorithm/spineless/field-id-pool.js +35 -0
  20. package/dist/algorithm/spineless/field-id-pool.js.map +1 -0
  21. package/dist/algorithm/spineless/flex-grammar.d.ts +394 -0
  22. package/dist/algorithm/spineless/flex-grammar.d.ts.map +1 -0
  23. package/dist/algorithm/spineless/flex-grammar.js +2509 -0
  24. package/dist/algorithm/spineless/flex-grammar.js.map +1 -0
  25. package/dist/algorithm/spineless/grammar.d.ts +156 -0
  26. package/dist/algorithm/spineless/grammar.d.ts.map +1 -0
  27. package/dist/algorithm/spineless/grammar.js +145 -0
  28. package/dist/algorithm/spineless/grammar.js.map +1 -0
  29. package/dist/algorithm/spineless/layout.d.ts +167 -0
  30. package/dist/algorithm/spineless/layout.d.ts.map +1 -0
  31. package/dist/algorithm/spineless/layout.js +893 -0
  32. package/dist/algorithm/spineless/layout.js.map +1 -0
  33. package/dist/algorithm/spineless/order-maintenance.bench.d.ts +25 -0
  34. package/dist/algorithm/spineless/order-maintenance.bench.d.ts.map +1 -0
  35. package/dist/algorithm/spineless/order-maintenance.bench.js +78 -0
  36. package/dist/algorithm/spineless/order-maintenance.bench.js.map +1 -0
  37. package/dist/algorithm/spineless/order-maintenance.d.ts +201 -0
  38. package/dist/algorithm/spineless/order-maintenance.d.ts.map +1 -0
  39. package/dist/algorithm/spineless/order-maintenance.js +300 -0
  40. package/dist/algorithm/spineless/order-maintenance.js.map +1 -0
  41. package/dist/algorithm/spineless/priority-queue.bench.d.ts +17 -0
  42. package/dist/algorithm/spineless/priority-queue.bench.d.ts.map +1 -0
  43. package/dist/algorithm/spineless/priority-queue.bench.js +57 -0
  44. package/dist/algorithm/spineless/priority-queue.bench.js.map +1 -0
  45. package/dist/algorithm/spineless/priority-queue.d.ts +73 -0
  46. package/dist/algorithm/spineless/priority-queue.d.ts.map +1 -0
  47. package/dist/algorithm/spineless/priority-queue.js +149 -0
  48. package/dist/algorithm/spineless/priority-queue.js.map +1 -0
  49. package/dist/algorithm/spineless/runtime.d.ts +292 -0
  50. package/dist/algorithm/spineless/runtime.d.ts.map +1 -0
  51. package/dist/algorithm/spineless/runtime.js +609 -0
  52. package/dist/algorithm/spineless/runtime.js.map +1 -0
  53. package/dist/algorithm/spineless/style-dirty.d.ts +65 -0
  54. package/dist/algorithm/spineless/style-dirty.d.ts.map +1 -0
  55. package/dist/algorithm/spineless/style-dirty.js +75 -0
  56. package/dist/algorithm/spineless/style-dirty.js.map +1 -0
  57. package/dist/dirty-flags.d.ts +30 -0
  58. package/dist/dirty-flags.d.ts.map +1 -0
  59. package/dist/dirty-flags.js +35 -0
  60. package/dist/dirty-flags.js.map +1 -0
  61. package/dist/index.d.ts +4 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +9 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/inspect.d.ts +27 -0
  66. package/dist/inspect.d.ts.map +1 -0
  67. package/dist/inspect.js +61 -0
  68. package/dist/inspect.js.map +1 -0
  69. package/dist/layout-pool.d.ts +49 -0
  70. package/dist/layout-pool.d.ts.map +1 -0
  71. package/dist/layout-pool.js +75 -0
  72. package/dist/layout-pool.js.map +1 -0
  73. package/dist/node.d.ts +20 -3
  74. package/dist/node.d.ts.map +1 -1
  75. package/dist/node.js +63 -42
  76. package/dist/node.js.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,2509 @@
1
+ /**
2
+ * Flexbox layout expressed as an attribute grammar.
3
+ *
4
+ * Current slice (v16b) covers:
5
+ *
6
+ * - flex-direction: `row`, `column`, `row-reverse`, `column-reverse`
7
+ * - flex-grow / flex-shrink / flex-basis (v3-v4)
8
+ * - padding / margin / gap (v5)
9
+ * - justify-content + align-items / align-self (v6)
10
+ * - flex-wrap (v7) — single-line wrap and multi-line packing,
11
+ * each line independently distributed / justified / aligned
12
+ * - positionType: `'absolute'` (v8) — out-of-flow children
13
+ * positioned against the parent's OUTER box via `style.position`
14
+ * and `style.margin`. Width / height resolve from explicit
15
+ * style, from opposing edges (`left`+`right` or `top`+`bottom`),
16
+ * or fall back to 0. Absolute children are filtered out of every
17
+ * in-flow computation (flex distribution, justify leftover,
18
+ * wrap line packing).
19
+ * - align-content (v9) — for multi-line wrap containers, the cross-
20
+ * axis leftover is distributed among / around the lines per
21
+ * `flex-start` / `flex-end` / `center` / `space-between` /
22
+ * `space-around` / `stretch`
23
+ * - flex-wrap: `wrap-reverse` (v10) — the line stack is mirrored
24
+ * on the cross axis
25
+ * - flex-direction: `row-reverse` / `column-reverse` (v11) — the
26
+ * main axis runs from the container's main END; each in-flow
27
+ * child's main position is reflected across the inner-main box,
28
+ * mirroring the imperative `flipMainAxis`
29
+ * - min / max size clamping (v12) — every node's main size, cross
30
+ * size, and an absolute child's width / height are clamped to
31
+ * the node's own `[minWidth/Height, maxWidth/Height]`. v12a
32
+ * covered the single-shot sites (non-distributed main size,
33
+ * cross size, absolute children); v12b folds min/max into the
34
+ * flex-distribution freeze loop and the wrap line packer — an
35
+ * item whose proportional grow / shrink target breaches a clamp
36
+ * is frozen at its bound and its share redistributed, iterating
37
+ * to a fixpoint, exactly as the imperative `distributeGrow` /
38
+ * `distributeShrink`.
39
+ * - `'auto'` main size (v13) — a non-measured `'auto'` main-axis
40
+ * size resolves to 0 (mirroring `resolveHypotheticalMainSize`);
41
+ * the root's `'auto'` axis resolves from the caller-supplied
42
+ * `available` size, modelled as a root input Field so a terminal
43
+ * resize is incremental.
44
+ * - `'auto'` cross size + `align-items: stretch` (v14) — an
45
+ * `'auto'` cross-axis size is 0 under a non-stretch align, but
46
+ * `stretch` (the default) resizes it to fill the line's inner
47
+ * cross, mirroring the imperative `crossAlignItemsInLine`
48
+ * stretch branch.
49
+ * - `aspectRatio` (v15) — an `'auto'` axis whose perpendicular
50
+ * axis is an explicit number derives `width = height × ratio` /
51
+ * `height = width ÷ ratio`, mirroring `effectivePreferredSize`.
52
+ * A derived axis is definite (not content-sized) — so it is not
53
+ * stretched.
54
+ * - measure-func leaves (v16) — a childless node with a measure
55
+ * function resolves its `'auto'` axes by calling the measurer.
56
+ * v16a covers the MAIN axis (main free, cross constrained,
57
+ * mirroring `resolveHypotheticalMainSize`); v16b the CROSS axis
58
+ * (cross constrained `AtMost` the parent inner cross, main free
59
+ * with a hint, mirroring `naturalCrossSize`). A measured cross
60
+ * feeds the wrap line cross-size aggregation and the non-stretch
61
+ * cross size; `align-items: stretch` still overrides it.
62
+ *
63
+ * With v16 the grammar covers the full imperative `'auto'` /
64
+ * measure / `aspectRatio` resolution: the Spineless engine is a
65
+ * drop-in for `calculateLayout` on real content-sized trees.
66
+ *
67
+ * Fields emitted per node:
68
+ *
69
+ * - `width` — main-axis size when parent is `row`, cross-axis size
70
+ * when parent is `column`. Cross axis reads
71
+ * `style.width` verbatim. Main axis is the result of
72
+ * flex distribution when the parent has any child with
73
+ * grow > 0, shrink > 0, or numeric `flexBasis`;
74
+ * otherwise it equals the basis.
75
+ * - `height` — symmetric to `width`.
76
+ * - `left` — position relative to parent. For row-parent children
77
+ * this is the main-axis cursor: `padLeft + myMarginLeft
78
+ * + sum_priors(marginLeft + width + marginRight) +
79
+ * i*gapColumn`. For column-parent children it's the
80
+ * cross-axis position: `padLeft + myMarginLeft`. Root
81
+ * is at 0.
82
+ * - `top` — symmetric to `left`.
83
+ *
84
+ * @internal
85
+ */
86
+ import { MeasureMode } from '../../measure-func.js';
87
+ import { isReverse, mainAxis } from '../axis.js';
88
+ import { field } from './grammar.js';
89
+ /**
90
+ * True iff `node` participates in its parent's flex flow — neither
91
+ * out-of-flow (`positionType: 'absolute'`) nor hidden
92
+ * (`display: 'none'`). A `display: 'none'` node is emitted no rules
93
+ * at all and skipped everywhere an in-flow sibling is consulted,
94
+ * mirroring the imperative algorithm — which `continue`s it in
95
+ * `layoutChildren` and never writes its `_layout`.
96
+ */
97
+ function isInFlow(node) {
98
+ return node.style.positionType !== 'absolute' && node.style.display !== 'none';
99
+ }
100
+ /**
101
+ * Build the topological per-node emitter — the `visit` recursion and
102
+ * the input-field helpers it closes over — bound to one
103
+ * `EmitContext`. `buildFlexGrammar` and the subtree fragment builders
104
+ * share this, so a fragment emits rules byte-identical to a full
105
+ * build.
106
+ *
107
+ * @internal
108
+ */
109
+ function makeEmitter(ctx) {
110
+ const { grammar, allFields, styleInputs, boundary, mainDistributionByParent } = ctx;
111
+ // Register (once) the input Field for the root's caller-supplied
112
+ // `available` size on one axis. Its `compute` reads `ctx.available`
113
+ // live, so a caller that mutates that object and `markDirty`s the
114
+ // field drives an incremental relayout after a terminal resize.
115
+ function availableInput(root, axis) {
116
+ const f = field(root, `available:${axis}`);
117
+ if (boundary?.has(f))
118
+ return f;
119
+ if (!grammar.has(f)) {
120
+ grammar.set(f, {
121
+ deps: [],
122
+ compute: () => ctx.available[axis] ?? 0,
123
+ });
124
+ ctx.availableInputs[axis] = f;
125
+ }
126
+ return f;
127
+ }
128
+ // Register (once) the `measure:main` input Field for a measure-leaf
129
+ // node's `'auto'` MAIN-axis size (v16a). Mirrors the imperative
130
+ // `resolveHypotheticalMainSize` measure branch: the measurer is
131
+ // called with the main axis FREE (`Undefined`) and the cross axis
132
+ // constrained `AtMost` the cross constraint — the cross style size
133
+ // when numeric, else the parent's inner cross. `mainProp` is the
134
+ // node's main-axis prop; `parent` supplies the inner-cross fields.
135
+ function measureMainInput(n, mainProp, parent) {
136
+ const f = field(n, 'measure:main');
137
+ if (boundary?.has(f))
138
+ return f;
139
+ if (!grammar.has(f)) {
140
+ const fn = n.getMeasureFunc();
141
+ const crossProp = mainProp === 'width' ? 'height' : 'width';
142
+ const deps = [];
143
+ let readCrossConstraint;
144
+ if (typeof n.style[crossProp] === 'number') {
145
+ const csInput = styleSizeInput(n, crossProp);
146
+ deps.push(csInput);
147
+ readCrossConstraint = (read) => read(csInput);
148
+ }
149
+ else {
150
+ // Cross is `'auto'` — constrain to the parent's inner cross.
151
+ const pdir = mainAxis(parent.style.flexDirection);
152
+ const parentCrossF = field(parent, crossProp);
153
+ const padStartF = paddingInput(parent, crossStartEdge(pdir));
154
+ const padEndF = paddingInput(parent, crossEndEdge(pdir));
155
+ deps.push(parentCrossF, padStartF, padEndF);
156
+ readCrossConstraint = (read) => Math.max(0, read(parentCrossF) - read(padStartF) - read(padEndF));
157
+ }
158
+ grammar.set(f, {
159
+ deps,
160
+ compute: (read) => {
161
+ const cc = readCrossConstraint(read);
162
+ // Main axis free; cross axis constrained AtMost.
163
+ const r = mainProp === 'width'
164
+ ? fn(0, MeasureMode.Undefined, cc, MeasureMode.AtMost)
165
+ : fn(cc, MeasureMode.AtMost, 0, MeasureMode.Undefined);
166
+ return mainProp === 'width' ? r.width : r.height;
167
+ },
168
+ });
169
+ }
170
+ return f;
171
+ }
172
+ // Register (once) the `measure:cross` input Field for a
173
+ // measure-leaf node's `'auto'` CROSS-axis size (v16b). Mirrors the
174
+ // imperative `naturalCrossSize`: the measurer is called with the
175
+ // cross axis constrained `AtMost` the parent's inner cross and the
176
+ // main axis FREE (`Undefined`) with a hint — the main style size
177
+ // when numeric, else the parent's inner cross. `crossProp` is the
178
+ // node's cross-axis prop; `parent` supplies the inner-cross fields.
179
+ function measureCrossInput(n, crossProp, parent) {
180
+ const f = field(n, 'measure:cross');
181
+ if (boundary?.has(f))
182
+ return f;
183
+ if (!grammar.has(f)) {
184
+ const fn = n.getMeasureFunc();
185
+ const mainProp = crossProp === 'width' ? 'height' : 'width';
186
+ const pdir = mainAxis(parent.style.flexDirection);
187
+ const parentCrossF = field(parent, crossProp);
188
+ const padStartF = paddingInput(parent, crossStartEdge(pdir));
189
+ const padEndF = paddingInput(parent, crossEndEdge(pdir));
190
+ const deps = [
191
+ parentCrossF,
192
+ padStartF,
193
+ padEndF,
194
+ ];
195
+ // The main-axis hint is the main STYLE size when numeric (a
196
+ // raw `preferredSize`, no aspectRatio), else the parent inner
197
+ // cross — matching `naturalCrossSize`'s `mainHint`.
198
+ let mainHintInput = null;
199
+ if (typeof n.style[mainProp] === 'number') {
200
+ mainHintInput = styleSizeInput(n, mainProp);
201
+ deps.push(mainHintInput);
202
+ }
203
+ grammar.set(f, {
204
+ deps,
205
+ compute: (read) => {
206
+ const innerCross = Math.max(0, read(parentCrossF) - read(padStartF) - read(padEndF));
207
+ const mainHint = mainHintInput !== null ? read(mainHintInput) : innerCross;
208
+ // Cross axis constrained AtMost the inner cross; main free.
209
+ const r = crossProp === 'width'
210
+ ? fn(innerCross, MeasureMode.AtMost, mainHint, MeasureMode.Undefined)
211
+ : fn(mainHint, MeasureMode.Undefined, innerCross, MeasureMode.AtMost);
212
+ return crossProp === 'width' ? r.width : r.height;
213
+ },
214
+ });
215
+ }
216
+ return f;
217
+ }
218
+ // Resolve the input Field a node's main / cross size rule reads for
219
+ // its preferred size on `axis`. Regimes (the auto/numeric split is
220
+ // structural — a mutation across it needs a fresh build):
221
+ // - numeric `style[axis]` → the live `styleSizeInput`;
222
+ // - `'auto'` + `aspectRatio` + the perpendicular axis numeric →
223
+ // an `aspect:*` Field deriving the size from the other axis
224
+ // (v15), mirroring `effectivePreferredSize`;
225
+ // - `'auto'` MAIN axis of a measure-leaf → a `measure:main` Field
226
+ // (v16a), mirroring `resolveHypotheticalMainSize`;
227
+ // - `'auto'` CROSS axis of a measure-leaf → a `measure:cross`
228
+ // Field (v16b), mirroring `naturalCrossSize`;
229
+ // - `'auto'` on the root → the `available:*` input;
230
+ // - `'auto'` elsewhere → a constant `0` (v13 — matches the
231
+ // imperative fallback for a non-measured `'auto'` node).
232
+ function preferredSizeInput(n, axis, role, parentOfN) {
233
+ if (typeof n.style[axis] === 'number')
234
+ return styleSizeInput(n, axis);
235
+ if (aspectDerivable(n, axis)) {
236
+ const other = axis === 'width' ? 'height' : 'width';
237
+ const ratio = n.style.aspectRatio;
238
+ const otherInput = styleSizeInput(n, other);
239
+ const f = field(n, `aspect:${axis}`);
240
+ if (boundary?.has(f))
241
+ return f;
242
+ if (!grammar.has(f)) {
243
+ grammar.set(f, {
244
+ deps: [otherInput],
245
+ // width = height × ratio; height = width ÷ ratio.
246
+ compute: (read) => axis === 'width' ? read(otherInput) * ratio : read(otherInput) / ratio,
247
+ });
248
+ }
249
+ return f;
250
+ }
251
+ if (parentOfN !== null && isMeasureLeaf(n)) {
252
+ return role === 'main'
253
+ ? measureMainInput(n, axis, parentOfN)
254
+ : measureCrossInput(n, axis, parentOfN);
255
+ }
256
+ if (parentOfN === null)
257
+ return availableInput(n, axis);
258
+ const f = field(n, `preferred:${axis}`);
259
+ if (boundary?.has(f))
260
+ return f;
261
+ if (!grammar.has(f)) {
262
+ grammar.set(f, {
263
+ deps: [],
264
+ compute: () => 0,
265
+ });
266
+ }
267
+ return f;
268
+ }
269
+ // The imperative `resolveRootAxisSize` clamps the root's size to
270
+ // [min, max] for an explicit / `aspectRatio` / `available`-derived
271
+ // axis — but its `'auto'` + no-`available` fallback returns a bare
272
+ // `0`, *unclamped*. This predicate flags exactly that case so the
273
+ // root size rule can skip the clamp and mirror the quirk (a root
274
+ // `minWidth` must not inflate an unavailable axis).
275
+ function rootAxisIsBareZero(n, axis) {
276
+ return (typeof n.style[axis] !== 'number' &&
277
+ !aspectDerivable(n, axis) &&
278
+ ctx.available[axis] === undefined);
279
+ }
280
+ function styleInputEntry(n) {
281
+ let entry = styleInputs.get(n);
282
+ if (entry === undefined) {
283
+ entry = {};
284
+ styleInputs.set(n, entry);
285
+ }
286
+ return entry;
287
+ }
288
+ // Register (once) the leaf input Field for a node's style SIZE
289
+ // prop and return it. The field has no deps; its `compute` reads
290
+ // `node.style` live. Layout fields that read a size declare the
291
+ // returned field as a dependency, so a `markDirty` on it
292
+ // propagates precisely through `recompute()`.
293
+ function styleSizeInput(n, prop) {
294
+ const f = field(n, `style:${prop}`);
295
+ if (boundary?.has(f))
296
+ return f;
297
+ if (!grammar.has(f)) {
298
+ grammar.set(f, {
299
+ deps: [],
300
+ compute: () => n.style[prop],
301
+ });
302
+ styleInputEntry(n)[prop] = f;
303
+ }
304
+ return f;
305
+ }
306
+ // Register (once) the leaf input Field for a node's min / max
307
+ // size clamp. `min*` reads `style.min{Width,Height}` (a number,
308
+ // default 0); `max*` reads `style.max{Width,Height}` and folds the
309
+ // `undefined` "no upper bound" sentinel to `Infinity`, so every
310
+ // consumer can clamp with one unconditional `clampMinMax`.
311
+ function minMaxInput(n, prop) {
312
+ const f = field(n, `style:${prop}`);
313
+ if (boundary?.has(f))
314
+ return f;
315
+ if (!grammar.has(f)) {
316
+ const isMax = prop === 'maxWidth' || prop === 'maxHeight';
317
+ grammar.set(f, {
318
+ deps: [],
319
+ compute: () => isMax ? (n.style[prop] ?? Number.POSITIVE_INFINITY) : n.style[prop],
320
+ });
321
+ styleInputEntry(n)[prop] = f;
322
+ }
323
+ return f;
324
+ }
325
+ // Register (once) the leaf input Field for a flex weight
326
+ // (`flexGrow` / `flexShrink`). Mutating a weight between two
327
+ // POSITIVE values (or two zeros) is an in-regime change driven via
328
+ // this field; crossing the zero boundary flips whether the parent
329
+ // flex-distributes and so needs a fresh `buildFlexGrammar()`.
330
+ function flexWeightInput(n, prop) {
331
+ const f = field(n, `style:${prop}`);
332
+ if (boundary?.has(f))
333
+ return f;
334
+ if (!grammar.has(f)) {
335
+ grammar.set(f, {
336
+ deps: [],
337
+ compute: () => n.style[prop],
338
+ });
339
+ styleInputEntry(n)[prop] = f;
340
+ }
341
+ return f;
342
+ }
343
+ // Register (once) the leaf input Field for a container's `gap`
344
+ // along one output axis (`gapRow` separates column-stacked items,
345
+ // `gapColumn` separates row-stacked items).
346
+ function gapInput(n, axis) {
347
+ const prop = axis === 'row' ? 'gapRow' : 'gapColumn';
348
+ const f = field(n, `style:${prop}`);
349
+ if (boundary?.has(f))
350
+ return f;
351
+ if (!grammar.has(f)) {
352
+ grammar.set(f, {
353
+ deps: [],
354
+ compute: () => n.style[prop],
355
+ });
356
+ styleInputEntry(n)[prop] = f;
357
+ }
358
+ return f;
359
+ }
360
+ // Register (once) the leaf input Field for one `padding` edge of a
361
+ // container (`edge` is a [top,right,bottom,left] index). Defaults
362
+ // to 0 when that edge is unset.
363
+ function paddingInput(n, edge) {
364
+ const f = field(n, `style:padding:${edge}`);
365
+ if (boundary?.has(f))
366
+ return f;
367
+ if (!grammar.has(f)) {
368
+ grammar.set(f, {
369
+ deps: [],
370
+ compute: () => n.style.padding[edge] ?? 0,
371
+ });
372
+ const entry = styleInputEntry(n);
373
+ if (entry.padding === undefined)
374
+ entry.padding = [];
375
+ entry.padding[edge] = f;
376
+ }
377
+ return f;
378
+ }
379
+ // Register (once) the leaf input Field for one `margin` edge of a
380
+ // node (`edge` is a [top,right,bottom,left] index). Defaults to 0
381
+ // when that edge is unset.
382
+ function marginInput(n, edge) {
383
+ const f = field(n, `style:margin:${edge}`);
384
+ if (boundary?.has(f))
385
+ return f;
386
+ if (!grammar.has(f)) {
387
+ grammar.set(f, {
388
+ deps: [],
389
+ compute: () => n.style.margin[edge] ?? 0,
390
+ });
391
+ const entry = styleInputEntry(n);
392
+ if (entry.margin === undefined)
393
+ entry.margin = [];
394
+ entry.margin[edge] = f;
395
+ }
396
+ return f;
397
+ }
398
+ /** Fold `minWidth`/`minHeight` (default 0) or `maxWidth`/`maxHeight`
399
+ * (default `undefined` → ∞). Returns a constant when at default;
400
+ * emits a leaf Field (via `minMaxInput`) otherwise. */
401
+ function foldMinMax(n, prop) {
402
+ const isMax = prop === 'maxWidth' || prop === 'maxHeight';
403
+ const raw = n.style[prop];
404
+ // Default sentinels MUST match nodeSig exactly (layout.ts):
405
+ // minWidth/minHeight default === 0
406
+ // maxWidth/maxHeight default === undefined
407
+ if (isMax ? raw === undefined : raw === 0) {
408
+ return { kind: 'const', value: isMax ? Number.POSITIVE_INFINITY : 0 };
409
+ }
410
+ return { kind: 'field', field: minMaxInput(n, prop) };
411
+ }
412
+ /** Fold one margin `edge` (default 0). Returns a constant when margin
413
+ * is 0; emits a leaf Field (via `marginInput`) otherwise. */
414
+ function foldMargin(n, edge) {
415
+ // Default sentinel MUST match nodeSig exactly (layout.ts):
416
+ // margin[edge] default === 0
417
+ if ((n.style.margin[edge] ?? 0) === 0)
418
+ return { kind: 'const', value: 0 };
419
+ return { kind: 'field', field: marginInput(n, edge) };
420
+ }
421
+ /** Read a FoldedInput inside a compute callback. */
422
+ function readFolded(fi, read) {
423
+ return fi.kind === 'field' ? read(fi.field) : fi.value;
424
+ }
425
+ /** Collect only the Field entries from a FoldedInput list. */
426
+ function foldedDeps(fis) {
427
+ const deps = [];
428
+ for (const fi of fis) {
429
+ if (fi.kind === 'field')
430
+ deps.push(fi.field);
431
+ }
432
+ return deps;
433
+ }
434
+ // ──────────────────────────────────────────────────────────────────────
435
+ function visit(node, parent, indexInParent, priorSiblings) {
436
+ const width = field(node, 'width');
437
+ const height = field(node, 'height');
438
+ const left = field(node, 'left');
439
+ const top = field(node, 'top');
440
+ // All four flex-direction values are supported (v11): the base
441
+ // axis (`row` / `column`) drives field assignment; reverse
442
+ // (`row-reverse` / `column-reverse`) flips child main positions.
443
+ // Absolute children short-circuit the in-flow flex pipeline:
444
+ // they're positioned independently against the parent's OUTER
445
+ // box (no padding subtraction) using their own `style.position`
446
+ // and `style.margin`. Their width / height can be `'auto'`, with
447
+ // size derived from opposing edges or falling back to 0 — so the
448
+ // in-flow "explicit numeric size" precondition is relaxed here.
449
+ if (parent !== null && node.style.positionType === 'absolute') {
450
+ emitAbsoluteRules(grammar, styleSizeInput, marginInput, minMaxInput, parent, node, width, height, left, top);
451
+ allFields.push({ node, width, height, left, top });
452
+ const childCount = node.getChildCount();
453
+ const childSiblings = [];
454
+ for (let i = 0; i < childCount; i++) {
455
+ const child = node.getChild(i);
456
+ // A `display: 'none'` child is laid out by nothing — emit it
457
+ // no rules and do not recurse, mirroring the imperative
458
+ // `layoutChildren` `continue`.
459
+ if (child.style.display === 'none')
460
+ continue;
461
+ if (child.style.positionType === 'absolute') {
462
+ visit(child, node, -1, []);
463
+ }
464
+ else {
465
+ visit(child, node, childSiblings.length, [...childSiblings]);
466
+ childSiblings.push(child);
467
+ }
468
+ }
469
+ return;
470
+ }
471
+ // `'auto'` width / height are supported (v13): a non-measured
472
+ // `'auto'` axis resolves to 0, the root's to its caller-supplied
473
+ // `available`. See `preferredSizeInput`.
474
+ // The parent's base axis decides which of {width, height} is the
475
+ // main-axis size for THIS child (and which of {left, top} is the
476
+ // main-axis position). Root is parent-less and treats both axes as
477
+ // cross — sizes from style, positions at 0. `parentDirection` is
478
+ // the base axis (`mainAxis` collapses `*-reverse` onto `row` /
479
+ // `column`); `parentReverse` records whether the parent runs its
480
+ // main axis backwards, applied as a post-hoc position flip.
481
+ const parentDirection = parent === null ? null : mainAxis(parent.style.flexDirection);
482
+ const parentReverse = parent !== null && isReverse(parent.style.flexDirection);
483
+ const mainSizeField = parentDirection === 'column' ? height : width;
484
+ const crossSizeField = parentDirection === 'column' ? width : height;
485
+ const mainPosField = parentDirection === 'column' ? top : left;
486
+ const crossPosField = parentDirection === 'column' ? left : top;
487
+ const mainSizeName = parentDirection === 'column' ? 'height' : 'width';
488
+ const mainPosName = parentDirection === 'column' ? 'top' : 'left';
489
+ // Spacing inputs for this child. Both the parent's padding and
490
+ // this child's own margin are modelled as leaf input Fields (see
491
+ // `paddingInput` / `marginInput`) — one per [top,right,bottom,
492
+ // left] edge — so each consumer declares the edge it reads as a
493
+ // dependency and a `setPadding` / `setMargin` propagates
494
+ // precisely through `recompute()`. The fields below are the
495
+ // edges THIS child's layout reads: parent padding on the main /
496
+ // cross axes, and this child's own main-start / cross-start /
497
+ // cross-end margins. They are `null` for the root (which has no
498
+ // parent and takes the constant path in every position rule).
499
+ // (Structural mutations — flex-direction, flex-wrap on/off,
500
+ // justify / align category — still need a fresh
501
+ // `buildFlexGrammar()`.)
502
+ const padMainStartF = parent === null ? null : paddingInput(parent, mainStartEdge(parentDirection));
503
+ const padMainEndF = parent === null ? null : paddingInput(parent, mainEndEdge(parentDirection));
504
+ const padCrossStartF = parent === null ? null : paddingInput(parent, crossStartEdge(parentDirection));
505
+ const padCrossEndF = parent === null ? null : paddingInput(parent, crossEndEdge(parentDirection));
506
+ // Phase 17: margin fields are resolved lazily at each rule site via
507
+ // foldMargin() (fold-eligible rules) or marginInput() (always-needed).
508
+ // Pre-compute edge indices once to avoid repeating edge-name calls.
509
+ const mainStartEdgeIdx = parent === null ? -1 : mainStartEdge(parentDirection);
510
+ const crossStartEdgeIdx = parent === null ? -1 : crossStartEdge(parentDirection);
511
+ const crossEndEdgeIdx = parent === null ? -1 : crossEndEdge(parentDirection);
512
+ // Rules that always require the full tracked Field (e.g. stretch
513
+ // crossSizeField, flex-end/center crossPos, reverse cumulative-sum)
514
+ // use these pre-resolved fields so the idempotent marginInput is called
515
+ // at most once per edge.
516
+ // NOTE: calling marginInput here forces Field creation for these edges
517
+ // regardless of whether they end up in a fold path. This is acceptable
518
+ // because the rules that read them (stretch/flex-end/center/reverse) are
519
+ // not fold-eligible — they always need tracked incremental propagation.
520
+ // For nodes on fully fold-eligible paths (flex-start, no-stretch, no-
521
+ // reverse), the Field is created if the code-path reaches it below;
522
+ // the fold-eligible rules that invoke foldMargin() bypass this and
523
+ // create no Field when margin is at default.
524
+ let myMarginMainStartF = null;
525
+ let myMarginCrossStartF = null;
526
+ let myMarginCrossEndF = null;
527
+ function getMarginMainStart() {
528
+ if (myMarginMainStartF === null)
529
+ myMarginMainStartF = marginInput(node, mainStartEdgeIdx);
530
+ return myMarginMainStartF;
531
+ }
532
+ function getMarginCrossStart() {
533
+ if (myMarginCrossStartF === null)
534
+ myMarginCrossStartF = marginInput(node, crossStartEdgeIdx);
535
+ return myMarginCrossStartF;
536
+ }
537
+ function getMarginCrossEnd() {
538
+ if (myMarginCrossEndF === null)
539
+ myMarginCrossEndF = marginInput(node, crossEndEdgeIdx);
540
+ return myMarginCrossEndF;
541
+ }
542
+ // Alignment for this child: justify-content lives on the parent,
543
+ // applies along the main axis once per line. align-items lives
544
+ // on the parent; align-self overrides per child (with 'auto'
545
+ // falling back to align-items).
546
+ const justify = parent === null ? 'flex-start' : parent.style.justifyContent;
547
+ const align = parent === null
548
+ ? 'auto'
549
+ : node.style.alignSelf === 'auto'
550
+ ? parent.style.alignItems
551
+ : node.style.alignSelf;
552
+ // Cross-axis size. For a numeric cross style, an `aspectRatio`-
553
+ // derived cross, the `'auto'` root (sized from `available`), or a
554
+ // content-`'auto'` cross under a non-stretch align, the size is
555
+ // the resolved input clamped to the node's own [min, max]
556
+ // (v12/v15). `align-items: stretch` (the default) instead
557
+ // resizes a CONTENT-`'auto'` cross size to fill the line's inner
558
+ // cross (v14) — mirroring the imperative `crossAlignItemsInLine`
559
+ // stretch branch. An aspectRatio-derived cross is definite, so it
560
+ // is not stretched. For a non-wrap parent the line cross IS the
561
+ // parent's inner cross; a wrapping parent overrides
562
+ // `crossSizeField` below with the per-line value.
563
+ const crossKey = parentDirection === 'column' ? 'width' : 'height';
564
+ const crossIsContentAuto = typeof node.style[crossKey] !== 'number' && !aspectDerivable(node, crossKey);
565
+ const crossSizeInput = preferredSizeInput(node, crossKey, 'cross', parent);
566
+ // Phase 17: fold min/max cross inputs when at default (0 / undefined→∞).
567
+ const fMinCross = foldMinMax(node, crossKey === 'width' ? 'minWidth' : 'minHeight');
568
+ const fMaxCross = foldMinMax(node, crossKey === 'width' ? 'maxWidth' : 'maxHeight');
569
+ if (crossIsContentAuto && parent !== null && align === 'stretch') {
570
+ // Stretch: resize to fill the parent's inner cross minus margins.
571
+ // The margins cannot be folded here (they appear in a subtraction
572
+ // expression), so use the always-needed field accessors.
573
+ const mcs = getMarginCrossStart();
574
+ const mce = getMarginCrossEnd();
575
+ const parentCrossF = field(parent, crossKey);
576
+ grammar.set(crossSizeField, {
577
+ deps: [
578
+ parentCrossF,
579
+ padCrossStartF,
580
+ padCrossEndF,
581
+ mcs,
582
+ mce,
583
+ ...foldedDeps([fMinCross, fMaxCross]),
584
+ ],
585
+ compute: (read) => {
586
+ const innerCross = Math.max(0, read(parentCrossF) - read(padCrossStartF) - read(padCrossEndF));
587
+ const lineInner = innerCross - read(mcs) - read(mce);
588
+ return clampMinMax(Math.max(0, lineInner), readFolded(fMinCross, read), readFolded(fMaxCross, read));
589
+ },
590
+ });
591
+ }
592
+ else if (parent === null && rootAxisIsBareZero(node, crossKey)) {
593
+ // `'auto'` root, no `available` → bare 0, unclamped (see
594
+ // `rootAxisIsBareZero`).
595
+ grammar.set(crossSizeField, {
596
+ deps: [crossSizeInput],
597
+ compute: (read) => read(crossSizeInput),
598
+ });
599
+ }
600
+ else {
601
+ // Phase 17: build deps only from non-default inputs.
602
+ grammar.set(crossSizeField, {
603
+ deps: [crossSizeInput, ...foldedDeps([fMinCross, fMaxCross])],
604
+ compute: (read) => clampMinMax(read(crossSizeInput), readFolded(fMinCross, read), readFolded(fMaxCross, read)),
605
+ });
606
+ }
607
+ // When the parent has flex-wrap='wrap', all three position fields
608
+ // (mainSize, mainPos, crossPos) flow through a single per-line
609
+ // helper that packs the line set on demand. The helper depends on
610
+ // the parent's main-axis size (line capacity), the parent's
611
+ // cross-axis size (for the single-line-wrap case), and only on
612
+ // the constant sibling style data — bases, margins, cross sizes
613
+ // are all captured inline. The dep graph stays compact (parent
614
+ // size fields only) at the cost of redoing the packing once per
615
+ // child read.
616
+ if (parent !== null && parent.style.flexWrap !== 'nowrap') {
617
+ // Capture the in-flow siblings and this child's index among
618
+ // them (both structural — a fresh build is needed if children
619
+ // are inserted / removed). Every per-sibling value the line
620
+ // packer reads — basis / main / cross size, grow / shrink
621
+ // weights, margins — is a declared input-field dep, so a
622
+ // size / flex / spacing mutation on any sibling propagates.
623
+ const crossKeyName = parentDirection === 'column' ? 'width' : 'height';
624
+ const mainStart = mainStartEdge(parentDirection);
625
+ const mainEnd = mainEndEdge(parentDirection);
626
+ const crossStart = crossStartEdge(parentDirection);
627
+ const crossEnd = crossEndEdge(parentDirection);
628
+ const wrapSibs = [];
629
+ let myIndex = -1;
630
+ for (let i = 0; i < parent.getChildCount(); i++) {
631
+ const sib = parent.getChild(i);
632
+ if (!isInFlow(sib))
633
+ continue;
634
+ if (sib === node)
635
+ myIndex = wrapSibs.length;
636
+ wrapSibs.push({
637
+ node: sib,
638
+ flexBasisInput: styleSizeInput(sib, 'flexBasis'),
639
+ mainInput: preferredSizeInput(sib, mainSizeName, 'main', parent),
640
+ crossInput: preferredSizeInput(sib, crossKeyName, 'cross', parent),
641
+ growInput: flexWeightInput(sib, 'flexGrow'),
642
+ shrinkInput: flexWeightInput(sib, 'flexShrink'),
643
+ marginMainStartInput: marginInput(sib, mainStart),
644
+ marginMainEndInput: marginInput(sib, mainEnd),
645
+ marginCrossStartInput: marginInput(sib, crossStart),
646
+ marginCrossEndInput: marginInput(sib, crossEnd),
647
+ minInput: minMaxInput(sib, mainSizeName === 'width' ? 'minWidth' : 'minHeight'),
648
+ maxInput: minMaxInput(sib, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight'),
649
+ minCrossInput: minMaxInput(sib, crossKeyName === 'width' ? 'minWidth' : 'minHeight'),
650
+ maxCrossInput: minMaxInput(sib, crossKeyName === 'width' ? 'maxWidth' : 'maxHeight'),
651
+ crossIsContentAuto: typeof sib.style[crossKeyName] !== 'number' && !aspectDerivable(sib, crossKeyName),
652
+ });
653
+ }
654
+ const parentMainField = field(parent, mainSizeName);
655
+ const parentCrossField = field(parent, crossKeyName);
656
+ // Main-axis gap separates items along the stacking axis; the
657
+ // cross-axis gap separates wrapped lines. Both are declared
658
+ // deps so a `setGap` propagates here.
659
+ const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
660
+ const crossGapInput = gapInput(parent, parentDirection === 'column' ? 'column' : 'row');
661
+ const wrapDeps = [
662
+ parentMainField,
663
+ parentCrossField,
664
+ mainGapInput,
665
+ crossGapInput,
666
+ padMainStartF,
667
+ padMainEndF,
668
+ padCrossStartF,
669
+ padCrossEndF,
670
+ ];
671
+ for (const s of wrapSibs) {
672
+ wrapDeps.push(s.flexBasisInput, s.mainInput, s.crossInput, s.growInput, s.shrinkInput, s.marginMainStartInput, s.marginMainEndInput, s.marginCrossStartInput, s.marginCrossEndInput, s.minInput, s.maxInput, s.minCrossInput, s.maxCrossInput);
673
+ }
674
+ const evalWrapped = (read) => {
675
+ const containerMain = read(parentMainField);
676
+ const containerCross = read(parentCrossField);
677
+ const padMainStart = read(padMainStartF);
678
+ const padCrossStart = read(padCrossStartF);
679
+ const innerMain = Math.max(0, containerMain - padMainStart - read(padMainEndF));
680
+ const innerCross = Math.max(0, containerCross - padCrossStart - read(padCrossEndF));
681
+ return evaluateWrappedChild(liveWrapSiblings(wrapSibs, parent, read), myIndex, innerMain, innerCross, read(mainGapInput), read(crossGapInput), justify, parent.style.alignContent, parent.style.flexWrap === 'wrap-reverse', padMainStart, padCrossStart);
682
+ };
683
+ grammar.set(mainSizeField, {
684
+ deps: wrapDeps,
685
+ compute: (read) => evalWrapped(read).mainSize,
686
+ });
687
+ grammar.set(mainPosField, {
688
+ deps: wrapDeps,
689
+ compute: (read) => evalWrapped(read).mainPos,
690
+ });
691
+ if (parentReverse) {
692
+ applyReverseMainPos(grammar, parent, mainPosField, mainSizeField, mainSizeName, padMainStartF, padMainEndF);
693
+ }
694
+ grammar.set(crossPosField, {
695
+ deps: wrapDeps,
696
+ compute: (read) => evalWrapped(read).crossPos,
697
+ });
698
+ // Override the shared cross-size rule: a wrapped child's cross
699
+ // size depends on its own LINE's cross size (v14 stretch
700
+ // resize), which only the line packer knows.
701
+ grammar.set(crossSizeField, {
702
+ deps: wrapDeps,
703
+ compute: (read) => evalWrapped(read).crossSize,
704
+ });
705
+ allFields.push({ node, width, height, left, top });
706
+ // Recurse into children. Absolute children are out-of-flow:
707
+ // they must NOT advance the in-flow index or the priorSiblings
708
+ // list (the same filtering the non-wrap path does below) —
709
+ // otherwise an absolute child's margin / size leaks into a
710
+ // later in-flow sibling's main position.
711
+ const childCount = node.getChildCount();
712
+ const inFlowSiblings = [];
713
+ for (let i = 0; i < childCount; i++) {
714
+ const child = node.getChild(i);
715
+ if (child.style.display === 'none')
716
+ continue;
717
+ if (child.style.positionType === 'absolute') {
718
+ visit(child, node, -1, []);
719
+ }
720
+ else {
721
+ visit(child, node, inFlowSiblings.length, [...inFlowSiblings]);
722
+ inFlowSiblings.push(child);
723
+ }
724
+ }
725
+ return;
726
+ }
727
+ // Main-axis size: depends on whether the parent flex-distributes
728
+ // its children. A parent flex-distributes when ANY of its children
729
+ // has grow > 0, shrink > 0, or a numeric flexBasis — i.e. anywhere
730
+ // a child's main size could legitimately differ from its raw
731
+ // style.{width|height}. Outside this case the main size is just
732
+ // the resolved basis.
733
+ if (parent === null || !parentNeedsFlexDistribution(parent)) {
734
+ // No flex distribution: this node's main size is its resolved
735
+ // basis, clamped to the node's own [min, max] (v12) — the
736
+ // imperative `buildItem` clamps the hypothetical main size even
737
+ // when no distribution follows. Declare deps on the node's own
738
+ // flexBasis + main-size + min/max inputs so a size or clamp
739
+ // mutation reaches this field precisely.
740
+ //
741
+ // The ROOT is special: `flexBasis` describes how a node behaves
742
+ // as a flex CHILD, and the root is not one. `resolveRootAxisSize`
743
+ // never consults it — so the root's main size is its preferred
744
+ // size directly, no `flexBasis` short-circuit.
745
+ const mainInput = preferredSizeInput(node, mainSizeName, 'main', parent);
746
+ // Phase 17: fold min/max main inputs when at default (0 / undefined→∞).
747
+ const fMinMain = foldMinMax(node, mainSizeName === 'width' ? 'minWidth' : 'minHeight');
748
+ const fMaxMain = foldMinMax(node, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight');
749
+ if (parent === null) {
750
+ if (rootAxisIsBareZero(node, mainSizeName)) {
751
+ // `'auto'` root, no `available` → bare 0, unclamped.
752
+ grammar.set(mainSizeField, {
753
+ deps: [mainInput],
754
+ compute: (read) => read(mainInput),
755
+ });
756
+ }
757
+ else {
758
+ // Phase 17: deps only include non-default min/max.
759
+ grammar.set(mainSizeField, {
760
+ deps: [mainInput, ...foldedDeps([fMinMain, fMaxMain])],
761
+ compute: (read) => clampMinMax(read(mainInput), readFolded(fMinMain, read), readFolded(fMaxMain, read)),
762
+ });
763
+ }
764
+ }
765
+ else {
766
+ // Phase 17: flexBasis 'auto' is folded — when flexBasis === 'auto',
767
+ // resolveBasisFromRead degenerates to read(mainInput). nodeSig already
768
+ // captures typeof s.flexBasis (the 'auto' vs numeric boundary), so a
769
+ // change from 'auto' to a numeric basis triggers a full rebuild.
770
+ const flexBasisIsAuto = node.style.flexBasis === 'auto';
771
+ if (flexBasisIsAuto) {
772
+ // Degenerate form: basis auto → just use mainInput (no flexBasisInput field).
773
+ grammar.set(mainSizeField, {
774
+ deps: [mainInput, ...foldedDeps([fMinMain, fMaxMain])],
775
+ compute: (read) => clampMinMax(read(mainInput), readFolded(fMinMain, read), readFolded(fMaxMain, read)),
776
+ });
777
+ }
778
+ else {
779
+ const flexBasisInput = styleSizeInput(node, 'flexBasis');
780
+ grammar.set(mainSizeField, {
781
+ deps: [
782
+ flexBasisInput,
783
+ mainInput,
784
+ ...foldedDeps([fMinMain, fMaxMain]),
785
+ ],
786
+ compute: (read) => clampMinMax(resolveBasisFromRead(read, flexBasisInput, mainInput), readFolded(fMinMain, read), readFolded(fMaxMain, read)),
787
+ });
788
+ }
789
+ }
790
+ }
791
+ else {
792
+ // Flex distribution. Capture the in-flow siblings + this
793
+ // child's index (structural); declare every per-sibling value
794
+ // the distribution reads — flexBasis / main size, grow /
795
+ // shrink weights, main-axis margins — as input-field deps, so
796
+ // a size / flex / spacing mutation on any sibling propagates
797
+ // here. The size is derived from the parent's main-axis size
798
+ // minus padding (the inner main).
799
+ const flexMainStart = mainStartEdge(parentDirection);
800
+ const flexMainEnd = mainEndEdge(parentDirection);
801
+ const flexSibs = [];
802
+ let myIndex = -1;
803
+ for (let i = 0; i < parent.getChildCount(); i++) {
804
+ const sib = parent.getChild(i);
805
+ if (!isInFlow(sib))
806
+ continue;
807
+ if (sib === node)
808
+ myIndex = flexSibs.length;
809
+ flexSibs.push({
810
+ node: sib,
811
+ flexBasisInput: styleSizeInput(sib, 'flexBasis'),
812
+ mainInput: preferredSizeInput(sib, mainSizeName, 'main', parent),
813
+ growInput: flexWeightInput(sib, 'flexGrow'),
814
+ shrinkInput: flexWeightInput(sib, 'flexShrink'),
815
+ marginMainStartInput: marginInput(sib, flexMainStart),
816
+ marginMainEndInput: marginInput(sib, flexMainEnd),
817
+ minInput: minMaxInput(sib, mainSizeName === 'width' ? 'minWidth' : 'minHeight'),
818
+ maxInput: minMaxInput(sib, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight'),
819
+ });
820
+ }
821
+ const parentMainField = field(parent, mainSizeName);
822
+ const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
823
+ // Phase 12 regime check: single-line + flex-distributing qualifies
824
+ // for the O(N) intermediate `mainDistribution` Field. This gates
825
+ // both the per-cell mainSize collapse (here) and the mainPos
826
+ // collapse (Task 3); mainPos additionally narrows on
827
+ // justify-content === 'flex-start'.
828
+ const isPhase12DistributionRegime = parent.style.flexWrap === undefined || parent.style.flexWrap === 'nowrap';
829
+ let parentMainDist;
830
+ if (isPhase12DistributionRegime) {
831
+ // Emit once per parent — memoize so subsequent children reuse
832
+ // the same Field rather than emitting duplicate rules.
833
+ parentMainDist = mainDistributionByParent.get(parent);
834
+ if (parentMainDist === undefined) {
835
+ parentMainDist = emitMainDistribution(grammar, parent, flexSibs, parentMainField, mainGapInput, padMainStartF, padMainEndF, marginInput, parentDirection);
836
+ mainDistributionByParent.set(parent, parentMainDist);
837
+ }
838
+ }
839
+ if (parentMainDist !== undefined) {
840
+ // Phase 12: cell mainSize is a trivial index read into the
841
+ // parent's pre-computed distribution array — O(1) dep edge
842
+ // instead of O(N siblings).
843
+ const myIndexCapture = myIndex;
844
+ grammar.set(mainSizeField, {
845
+ deps: [parentMainDist],
846
+ compute: (read) => read(parentMainDist).sizes[myIndexCapture],
847
+ });
848
+ }
849
+ else {
850
+ // Non-qualifying regime (wrap): keep today's per-cell rule with
851
+ // full sibling deps — distributeMainAxis called inline.
852
+ const deps = [
853
+ parentMainField,
854
+ mainGapInput,
855
+ padMainStartF,
856
+ padMainEndF,
857
+ ];
858
+ for (const s of flexSibs) {
859
+ deps.push(s.flexBasisInput, s.mainInput, s.growInput, s.shrinkInput, s.marginMainStartInput, s.marginMainEndInput, s.minInput, s.maxInput);
860
+ }
861
+ grammar.set(mainSizeField, {
862
+ deps,
863
+ compute: (read) => {
864
+ const innerMain = Math.max(0, read(parentMainField) - read(padMainStartF) - read(padMainEndF));
865
+ const siblings = liveFlexSiblings(flexSibs, read);
866
+ return distributeMainAxis(siblings, innerMain, read(mainGapInput))[myIndex];
867
+ },
868
+ });
869
+ }
870
+ }
871
+ // Main-axis position. Two regimes:
872
+ // - Default (justify === 'flex-start'): a child's main position
873
+ // depends only on its prior siblings' main sizes — a constant
874
+ // offset (padding + own margins + sum of prior margins + gaps)
875
+ // plus the read of prior sizes. This is the v1-v5 dep
876
+ // pattern; no value redistribution along the main axis.
877
+ // - Any other justify value: leftover space is computed from
878
+ // ALL siblings' final main sizes, then distributed as a
879
+ // leading offset and/or extra gap. mainPos now depends on
880
+ // every sibling's main size and on the parent's main size.
881
+ if (parent === null || indexInParent === 0) {
882
+ if (parent === null) {
883
+ // Root is parent-less — anchor at 0.
884
+ grammar.set(mainPosField, {
885
+ deps: [],
886
+ compute: () => 0,
887
+ });
888
+ }
889
+ else if (justify === 'flex-start') {
890
+ // Phase 12: read directly from the parent's mainDistribution if
891
+ // it was emitted (parent flex-distributes + single-line).
892
+ // Fallback to today's padding+margin rule when parent didn't qualify.
893
+ const parentMainDist = mainDistributionByParent.get(parent);
894
+ if (parentMainDist !== undefined) {
895
+ const myIndexCapture = priorSiblings.length; // in-flow index
896
+ grammar.set(mainPosField, {
897
+ deps: [parentMainDist],
898
+ compute: (read) => read(parentMainDist).positions[myIndexCapture],
899
+ });
900
+ }
901
+ else {
902
+ // Phase 17: fold myMarginMainStart when at default (0).
903
+ const fMyMarginMainStart = foldMargin(node, mainStartEdgeIdx);
904
+ grammar.set(mainPosField, {
905
+ deps: [padMainStartF, ...foldedDeps([fMyMarginMainStart])],
906
+ compute: (read) => read(padMainStartF) + readFolded(fMyMarginMainStart, read),
907
+ });
908
+ }
909
+ }
910
+ else {
911
+ // First child but parent uses non-default justify. Leading
912
+ // offset still depends on leftover, which depends on every
913
+ // sibling's main size.
914
+ emitJustifiedMainPos(grammar, parent, mainPosField, mainSizeName, justify, indexInParent, parentDirection, gapInput(parent, parentDirection === 'column' ? 'row' : 'column'), padMainStartF, padMainEndF, marginInput);
915
+ }
916
+ }
917
+ else if (justify === 'flex-start') {
918
+ // Phase 12: read directly from the parent's mainDistribution if it
919
+ // was emitted (parent flex-distributes + single-line). Fallback to
920
+ // today's prior-siblings rule when the parent didn't qualify.
921
+ const parentMainDist = mainDistributionByParent.get(parent);
922
+ if (parentMainDist !== undefined) {
923
+ const myIndexCapture = priorSiblings.length; // in-flow index
924
+ grammar.set(mainPosField, {
925
+ deps: [parentMainDist],
926
+ compute: (read) => read(parentMainDist).positions[myIndexCapture],
927
+ });
928
+ }
929
+ else if (!parentReverse) {
930
+ // Phase 16: linear recurrence. This child's main position is the
931
+ // immediate predecessor's position + its box + one gap. O(1) deps
932
+ // regardless of sibling count (was O(N) cumulative-sum). Unrolls
933
+ // to the same total — see the Phase 16 design doc.
934
+ // Note: only applies to forward directions; reverse directions use
935
+ // the cumulative-sum below because applyReverseMainPos overwrites
936
+ // each sibling's mainPosField with a reflected value, breaking the
937
+ // chain (the predecessor's field holds its reflected position, not
938
+ // the forward cursor this recurrence relies on).
939
+ const prevSibling = priorSiblings[priorSiblings.length - 1];
940
+ const prevMainPos = field(prevSibling, mainPosName);
941
+ const prevMainSize = field(prevSibling, mainSizeName);
942
+ const mainEndEdgeIdx = mainEndEdge(parentDirection);
943
+ // Phase 17: fold prevMarginEnd and myMarginMainStart when at default (0).
944
+ const fPrevMarginEnd = foldMargin(prevSibling, mainEndEdgeIdx);
945
+ const fMyMarginMainStart = foldMargin(node, mainStartEdgeIdx);
946
+ const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
947
+ grammar.set(mainPosField, {
948
+ deps: [
949
+ prevMainPos,
950
+ prevMainSize,
951
+ ...foldedDeps([fPrevMarginEnd, fMyMarginMainStart]),
952
+ mainGapInput,
953
+ ],
954
+ compute: (read) => read(prevMainPos) +
955
+ read(prevMainSize) +
956
+ readFolded(fPrevMarginEnd, read) +
957
+ readFolded(fMyMarginMainStart, read) +
958
+ read(mainGapInput),
959
+ });
960
+ }
961
+ else {
962
+ // Reverse direction: keep the cumulative-sum rule. The recurrence
963
+ // cannot chain through the predecessor's mainPosField here because
964
+ // applyReverseMainPos (applied per-sibling below) overwrites that
965
+ // field with a reflected value — the predecessor's field no longer
966
+ // carries the forward cursor the recurrence depends on.
967
+ const priorMainSizes = priorSiblings.map((s) => field(s, mainSizeName));
968
+ const priorMargins = priorSiblings.map((s) => ({
969
+ start: marginInput(s, mainStartEdge(parentDirection)),
970
+ end: marginInput(s, mainEndEdge(parentDirection)),
971
+ }));
972
+ const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
973
+ // Reverse cumulative-sum: cannot fold margins — the expression
974
+ // accumulates all sibling margins and this node's own marginMainStart.
975
+ const myMMS = getMarginMainStart();
976
+ grammar.set(mainPosField, {
977
+ deps: [
978
+ mainGapInput,
979
+ padMainStartF,
980
+ myMMS,
981
+ ...priorMainSizes,
982
+ ...priorMargins.flatMap((m) => [m.start, m.end]),
983
+ ],
984
+ compute: (read) => {
985
+ let sum = read(padMainStartF) + read(myMMS) + indexInParent * read(mainGapInput);
986
+ for (const m of priorMargins)
987
+ sum += read(m.start) + read(m.end);
988
+ for (const m of priorMainSizes)
989
+ sum += read(m);
990
+ return sum;
991
+ },
992
+ });
993
+ }
994
+ }
995
+ else {
996
+ emitJustifiedMainPos(grammar, parent, mainPosField, mainSizeName, justify, indexInParent, parentDirection, gapInput(parent, parentDirection === 'column' ? 'row' : 'column'), padMainStartF, padMainEndF, marginInput);
997
+ }
998
+ // Reverse flex-direction (`row-reverse` / `column-reverse`): the
999
+ // main axis runs from the container's main END. The position
1000
+ // rules above computed the forward-axis cursor; reflect it across
1001
+ // the inner-main box, exactly as the imperative `flipMainAxis`.
1002
+ if (parent !== null && parentReverse) {
1003
+ applyReverseMainPos(grammar, parent, mainPosField, mainSizeField, mainSizeName, padMainStartF, padMainEndF);
1004
+ }
1005
+ // Cross-axis position. Default (flex-start, stretch with explicit
1006
+ // cross size, and any other value the imperative doesn't special-
1007
+ // case) is a constant offset. flex-end and center derive an offset
1008
+ // from the parent's cross-axis size, gaining a dep edge on it.
1009
+ if (parent === null || align === 'flex-end') {
1010
+ if (parent !== null && align === 'flex-end') {
1011
+ const parentCrossField = field(parent, parentDirection === 'column' ? 'width' : 'height');
1012
+ const mce = getMarginCrossEnd();
1013
+ grammar.set(crossPosField, {
1014
+ deps: [
1015
+ parentCrossField,
1016
+ crossSizeField,
1017
+ padCrossStartF,
1018
+ padCrossEndF,
1019
+ mce,
1020
+ ],
1021
+ // Anchor against the line's inner cross, which the
1022
+ // imperative `crossAlignItemsInLine` clamps to >= 0 — a
1023
+ // container whose cross padding exceeds its cross size has
1024
+ // a zero-width line, not a negative one.
1025
+ compute: (read) => {
1026
+ const padStart = read(padCrossStartF);
1027
+ const innerCross = Math.max(0, read(parentCrossField) - padStart - read(padCrossEndF));
1028
+ return padStart + innerCross - read(crossSizeField) - read(mce);
1029
+ },
1030
+ });
1031
+ }
1032
+ else {
1033
+ // Root: no parent, no alignment to apply — anchor at 0.
1034
+ grammar.set(crossPosField, {
1035
+ deps: [],
1036
+ compute: () => 0,
1037
+ });
1038
+ }
1039
+ }
1040
+ else if (align === 'center') {
1041
+ const parentCrossField = field(parent, parentDirection === 'column' ? 'width' : 'height');
1042
+ const mcs2 = getMarginCrossStart();
1043
+ const mce2 = getMarginCrossEnd();
1044
+ grammar.set(crossPosField, {
1045
+ deps: [
1046
+ parentCrossField,
1047
+ crossSizeField,
1048
+ padCrossStartF,
1049
+ padCrossEndF,
1050
+ mcs2,
1051
+ mce2,
1052
+ ],
1053
+ compute: (read) => {
1054
+ const padStart = read(padCrossStartF);
1055
+ const innerCross = Math.max(0, read(parentCrossField) - padStart - read(padCrossEndF));
1056
+ const marginStart = read(mcs2);
1057
+ const innerLine = innerCross - marginStart - read(mce2);
1058
+ const myCross = read(crossSizeField);
1059
+ return padStart + marginStart + Math.max(0, (innerLine - myCross) / 2);
1060
+ },
1061
+ });
1062
+ }
1063
+ else {
1064
+ // flex-start, stretch (with explicit cross size — no resize),
1065
+ // and any other value (the imperative falls through to
1066
+ // flex-start) all share this offset: the parent's cross-start
1067
+ // padding plus this child's cross-start margin.
1068
+ // Phase 17: fold myMarginCrossStart when at default (0) — no Field
1069
+ // created, constant 0 inlined.
1070
+ const fMyMarginCrossStart = foldMargin(node, crossStartEdgeIdx);
1071
+ grammar.set(crossPosField, {
1072
+ deps: [padCrossStartF, ...foldedDeps([fMyMarginCrossStart])],
1073
+ compute: (read) => read(padCrossStartF) + readFolded(fMyMarginCrossStart, read),
1074
+ });
1075
+ }
1076
+ allFields.push({ node, width, height, left, top });
1077
+ // Recurse into children. Absolute children are out-of-flow: they
1078
+ // get visited (so their own subtree emits rules) but they don't
1079
+ // contribute to the in-flow sibling index or the priorSiblings
1080
+ // list that fuels positioning of subsequent in-flow siblings.
1081
+ const childCount = node.getChildCount();
1082
+ const inFlowSiblings = [];
1083
+ for (let i = 0; i < childCount; i++) {
1084
+ const child = node.getChild(i);
1085
+ if (child.style.display === 'none')
1086
+ continue;
1087
+ if (child.style.positionType === 'absolute') {
1088
+ visit(child, node, -1, []);
1089
+ }
1090
+ else {
1091
+ visit(child, node, inFlowSiblings.length, [...inFlowSiblings]);
1092
+ inFlowSiblings.push(child);
1093
+ }
1094
+ }
1095
+ }
1096
+ return visit;
1097
+ }
1098
+ /**
1099
+ * Walk the tree rooted at `root` and emit a `Grammar` that computes
1100
+ * each node's `{width, height, left, top}`. See the module header
1101
+ * for the field rules. A whole-tree build — `boundary` is `null`.
1102
+ *
1103
+ * `'auto'` width / height are supported (v13): a non-measured
1104
+ * `'auto'` axis resolves to 0; the root's `'auto'` axis resolves
1105
+ * from `available` (matching `calculateLayout`'s availability args).
1106
+ *
1107
+ * @internal
1108
+ */
1109
+ export function buildFlexGrammar(root, available = {}) {
1110
+ const grammar = new Map();
1111
+ const allFields = [];
1112
+ const styleInputs = new Map();
1113
+ const availableInputs = {};
1114
+ const mainDistributionByParent = new Map();
1115
+ makeEmitter({
1116
+ grammar,
1117
+ allFields,
1118
+ styleInputs,
1119
+ boundary: null,
1120
+ available,
1121
+ availableInputs,
1122
+ mainDistributionByParent,
1123
+ })(root, null, 0, []);
1124
+ return {
1125
+ grammar,
1126
+ rootFields: {
1127
+ width: field(root, 'width'),
1128
+ height: field(root, 'height'),
1129
+ left: field(root, 'left'),
1130
+ top: field(root, 'top'),
1131
+ },
1132
+ allFields,
1133
+ styleInputs,
1134
+ availableInputs,
1135
+ mainDistributionByParent,
1136
+ };
1137
+ }
1138
+ /** Merge two `StyleInputs` — `b`'s present fields win over `a`'s. */
1139
+ function mergeStyleInputs(a, b) {
1140
+ const m = { ...a };
1141
+ if (b.width !== undefined)
1142
+ m.width = b.width;
1143
+ if (b.height !== undefined)
1144
+ m.height = b.height;
1145
+ if (b.flexBasis !== undefined)
1146
+ m.flexBasis = b.flexBasis;
1147
+ if (b.gapRow !== undefined)
1148
+ m.gapRow = b.gapRow;
1149
+ if (b.gapColumn !== undefined)
1150
+ m.gapColumn = b.gapColumn;
1151
+ if (b.minWidth !== undefined)
1152
+ m.minWidth = b.minWidth;
1153
+ if (b.minHeight !== undefined)
1154
+ m.minHeight = b.minHeight;
1155
+ if (b.maxWidth !== undefined)
1156
+ m.maxWidth = b.maxWidth;
1157
+ if (b.maxHeight !== undefined)
1158
+ m.maxHeight = b.maxHeight;
1159
+ for (const k of ['padding', 'margin']) {
1160
+ const bArr = b[k];
1161
+ if (bArr === undefined)
1162
+ continue;
1163
+ const arr = m[k] !== undefined ? [...m[k]] : [];
1164
+ for (let i = 0; i < bArr.length; i++) {
1165
+ if (bArr[i] !== undefined)
1166
+ arr[i] = bArr[i];
1167
+ }
1168
+ m[k] = arr;
1169
+ }
1170
+ return m;
1171
+ }
1172
+ /**
1173
+ * Merge the per-node `styleInputs` of a fragment (`extra`) into a
1174
+ * copy of `base`. A node present in both — the previous last child,
1175
+ * which gains a main-end margin input when it acquires a follower —
1176
+ * has its `StyleInputs` deep-merged rather than overwritten.
1177
+ */
1178
+ function mergeStyleInputsMap(base, extra, mutateBase) {
1179
+ // When mutateBase is true the caller owns `base` exclusively (it is
1180
+ // the previous grammar output, swapped out by the caller right after
1181
+ // this returns) — mutate it directly and skip the ~1,100-entry clone.
1182
+ const merged = mutateBase ? base : new Map(base);
1183
+ for (const [node, entry] of extra) {
1184
+ const existing = merged.get(node);
1185
+ merged.set(node, existing === undefined ? entry : mergeStyleInputs(existing, entry));
1186
+ }
1187
+ return merged;
1188
+ }
1189
+ /**
1190
+ * Fast-path a child INSERT for the Spineless runtime. If adding
1191
+ * `child` under `parent` can be absorbed without a fresh runtime,
1192
+ * return the patch inputs; return `null` when a full rebuild is
1193
+ * required — `child` is not a child of `parent`, or `parent` uses a
1194
+ * reverse `flex-direction` (supported by the grammar since v11, but
1195
+ * not by this structural fast-path — reflecting every sibling's
1196
+ * position is a whole-subtree rewrite).
1197
+ *
1198
+ * The new subtree's fields are always topological-tail additions
1199
+ * handled by `graft` (`additions` / `newRoots`). When `parent` is in
1200
+ * the "simple" regime (no flex distribution, default `flex-start`
1201
+ * justify, no wrap) AND `child` is its LAST child — or `child` is
1202
+ * absolute — that is the whole patch and `rebinds` is empty, and the
1203
+ * fragment is built in **O(subtree)**: `makeEmitter` emits just the
1204
+ * appended subtree against the runtime's grammar as a boundary, no
1205
+ * whole-tree rebuild. Otherwise the insert also rewrites existing
1206
+ * in-flow siblings' rules — a flex-distributing / justified /
1207
+ * wrapping parent reads every sibling, and a MID-LIST insert (v32)
1208
+ * shifts every later in-flow sibling's main position — so the
1209
+ * grammar is rebuilt O(tree) and `rebinds` carries those siblings'
1210
+ * rewritten rules.
1211
+ *
1212
+ * `next.grammar` is always `prev.grammar` — the runtime's own Map,
1213
+ * which `graft` / `rebindRule` patch in place; `next.allFields` /
1214
+ * `next.styleInputs` are refreshed lookup tables the caller adopts.
1215
+ *
1216
+ * @internal
1217
+ */
1218
+ export function buildAppendFragment(prev, root, parent, child, available = {}) {
1219
+ // `child` must be a child of `parent`. Its index decides the path,
1220
+ // not whether the fragment is built: a last in-flow child can take
1221
+ // the O(subtree) graft; a mid-list in-flow insert shifts the later
1222
+ // siblings, so it takes the rebuild + rebind path below (v32).
1223
+ const count = parent.getChildCount();
1224
+ let childIndex = -1;
1225
+ for (let i = 0; i < count; i++) {
1226
+ if (parent.getChild(i) === child) {
1227
+ childIndex = i;
1228
+ break;
1229
+ }
1230
+ }
1231
+ if (childIndex === -1)
1232
+ return null;
1233
+ const isLast = childIndex === count - 1;
1234
+ // A `display: 'none'` appended child has no fields to graft — let
1235
+ // the rebuild path absorb it (a hidden node perturbs nothing, so
1236
+ // the rebuild is the simple correct route, not a fast-path miss).
1237
+ if (child.style.display === 'none')
1238
+ return null;
1239
+ // A reverse-direction parent is supported by the grammar (v11) but
1240
+ // not by this fast-path: the flip reflects every sibling, so fall
1241
+ // back to a full rebuild.
1242
+ const dir = parent.style.flexDirection;
1243
+ if (dir !== 'row' && dir !== 'column')
1244
+ return null;
1245
+ // An absolute child never perturbs in-flow siblings, so it is
1246
+ // always a pure tail graft. An in-flow child stays one only when it
1247
+ // is the LAST child of a simple-regime parent — a mid-list in-flow
1248
+ // insert shifts the later siblings and so takes the rebuild path.
1249
+ const simple = child.style.positionType === 'absolute' ||
1250
+ (isLast &&
1251
+ parent.style.flexWrap === 'nowrap' &&
1252
+ parent.style.justifyContent === 'flex-start' &&
1253
+ !parentNeedsFlexDistribution(parent));
1254
+ if (simple) {
1255
+ // O(subtree): emit just the appended subtree against the
1256
+ // runtime's grammar as a boundary, so the fragment's `grammar`
1257
+ // holds only genuinely-new fields. The expensive whole-tree walk
1258
+ // is skipped entirely.
1259
+ const ctx = {
1260
+ grammar: new Map(),
1261
+ allFields: [],
1262
+ styleInputs: new Map(),
1263
+ boundary: prev.grammar,
1264
+ available: {},
1265
+ availableInputs: {},
1266
+ mainDistributionByParent: new Map(),
1267
+ };
1268
+ const priors = [];
1269
+ for (let i = 0; i < childIndex; i++) {
1270
+ const sib = parent.getChild(i);
1271
+ if (isInFlow(sib))
1272
+ priors.push(sib);
1273
+ }
1274
+ makeEmitter(ctx)(child, parent, priors.length, priors);
1275
+ const newRoots = [];
1276
+ for (const e of ctx.allFields) {
1277
+ newRoots.push(e.width, e.height, e.left, e.top);
1278
+ }
1279
+ const next = {
1280
+ grammar: prev.grammar,
1281
+ rootFields: prev.rootFields,
1282
+ allFields: [...prev.allFields, ...ctx.allFields],
1283
+ styleInputs: mergeStyleInputsMap(prev.styleInputs, ctx.styleInputs, true),
1284
+ availableInputs: prev.availableInputs,
1285
+ mainDistributionByParent: prev.mainDistributionByParent,
1286
+ };
1287
+ return { additions: ctx.grammar, newRoots, rebinds: [], next };
1288
+ }
1289
+ // Non-simple: appending rewrites every surviving sibling's rules.
1290
+ // Rebuild the grammar O(tree) and diff it against `prev` for the
1291
+ // new fields; Field identity is stable across builds, so a key
1292
+ // absent from `prev.grammar` belongs to a newly-added node. The
1293
+ // rebuild needs the caller's `available` — the root's `'auto'`
1294
+ // size rule shape depends on it (`rootAxisIsBareZero`).
1295
+ const fresh = buildFlexGrammar(root, available);
1296
+ const additions = new Map();
1297
+ for (const [f, rule] of fresh.grammar) {
1298
+ if (!prev.grammar.has(f))
1299
+ additions.set(f, rule);
1300
+ }
1301
+ // `graft` integrates the DFS-closure of `newRoots`. A new node's
1302
+ // own layout fields don't reach every new field: a mid-list insert
1303
+ // leaves the inserted node's main-END margin read only by its
1304
+ // follower's (rebound, existing) rule — never by the node itself.
1305
+ // Starting the DFS from EVERY new field covers those orphans; the
1306
+ // DFS dedups, so the extra roots cost nothing.
1307
+ const newRoots = [...additions.keys()];
1308
+ const rebinds = [];
1309
+ for (let i = 0; i < parent.getChildCount(); i++) {
1310
+ const sib = parent.getChild(i);
1311
+ if (sib === child || !isInFlow(sib))
1312
+ continue;
1313
+ for (const name of ['width', 'height', 'left', 'top']) {
1314
+ const f = field(sib, name);
1315
+ const rule = fresh.grammar.get(f);
1316
+ if (rule !== undefined)
1317
+ rebinds.push([f, rule]);
1318
+ }
1319
+ }
1320
+ // Phase 12: if the parent's mainDistribution field already existed in
1321
+ // prev (the parent was already flex-distributing pre-append) its
1322
+ // closure now covers different siblings — rebind it so recompute sees
1323
+ // the updated sibling list.
1324
+ const parentMainDistF = fresh.mainDistributionByParent.get(parent);
1325
+ if (parentMainDistF !== undefined && prev.grammar.has(parentMainDistF)) {
1326
+ const freshRule = fresh.grammar.get(parentMainDistF);
1327
+ if (freshRule !== undefined)
1328
+ rebinds.unshift([parentMainDistF, freshRule]);
1329
+ }
1330
+ const next = {
1331
+ grammar: prev.grammar,
1332
+ rootFields: prev.rootFields,
1333
+ allFields: fresh.allFields,
1334
+ styleInputs: fresh.styleInputs,
1335
+ availableInputs: fresh.availableInputs,
1336
+ mainDistributionByParent: fresh.mainDistributionByParent,
1337
+ };
1338
+ return { additions, newRoots, rebinds, next };
1339
+ }
1340
+ /**
1341
+ * Candidate grammar fields belonging to `node` — its four layout
1342
+ * fields, its `measure:*` fields (a measure leaf's `'auto'`-axis
1343
+ * measure inputs), and its style input fields recorded in
1344
+ * `styleInputs`. The caller filters to those actually in the grammar.
1345
+ *
1346
+ * Every NON-leaf per-node field MUST be listed — the four layout
1347
+ * fields, `measure:main` / `measure:cross` (a measure leaf's
1348
+ * `'auto'`-axis inputs) and `aspect:width` / `aspect:height` (an
1349
+ * `aspectRatio` node's derived sizes). Unlike a leaf style input,
1350
+ * these carry dependencies, so `SpinelessRuntime.detach`'s orphan
1351
+ * cleanup (which only drops dependency-free leaves) cannot reclaim
1352
+ * them — they have to be in the removed set explicitly, or a removed
1353
+ * style input they read dangles. Leaf style inputs not listed here
1354
+ * are reclaimed by that orphan cleanup.
1355
+ */
1356
+ function nodeFields(node, styleInputs) {
1357
+ const out = [
1358
+ field(node, 'width'),
1359
+ field(node, 'height'),
1360
+ field(node, 'left'),
1361
+ field(node, 'top'),
1362
+ field(node, 'measure:main'),
1363
+ field(node, 'measure:cross'),
1364
+ field(node, 'aspect:width'),
1365
+ field(node, 'aspect:height'),
1366
+ // Phase 12: the per-parent mainDistribution intermediate field is a
1367
+ // non-leaf that has deps (siblings' size/flex inputs) and may be read
1368
+ // by each child's width/height rule. It MUST be in the removed set so
1369
+ // detach() does not see dangling reverse-dep edges from child layout
1370
+ // fields back to it.
1371
+ field(node, 'mainDistribution'),
1372
+ ];
1373
+ const si = styleInputs.get(node);
1374
+ if (si !== undefined) {
1375
+ for (const k of ['width', 'height', 'flexBasis', 'gapRow', 'gapColumn']) {
1376
+ const f = si[k];
1377
+ if (f !== undefined)
1378
+ out.push(f);
1379
+ }
1380
+ for (const arr of [si.padding, si.margin]) {
1381
+ if (arr === undefined)
1382
+ continue;
1383
+ for (const f of arr) {
1384
+ if (f !== undefined)
1385
+ out.push(f);
1386
+ }
1387
+ }
1388
+ }
1389
+ return out;
1390
+ }
1391
+ /**
1392
+ * Fast-path a child REMOVAL for the Spineless runtime — the mirror
1393
+ * of `buildAppendFragment`. Call this **before** detaching `child`
1394
+ * from `parent`: the simple-regime check needs `child` still in
1395
+ * place. Returns the patch inputs, or `null` when a full rebuild is
1396
+ * required (`child` is not `parent`'s child, or `parent` uses a
1397
+ * reverse `flex-direction` — supported by the grammar since v11 but
1398
+ * not by this fast-path).
1399
+ *
1400
+ * In the "simple" regime (no flex distribution, default `flex-start`
1401
+ * justify, no wrap — or `child` is absolute) the patch is built in
1402
+ * **O(subtree)**: `removed` is collected directly from `prev` by
1403
+ * walking the removed subtree, with no whole-tree rebuild, and
1404
+ * `rebinds` is empty. `SpinelessRuntime.detach` then drops those
1405
+ * fields and auto-cleans any input field they orphaned (e.g. the new
1406
+ * last child's now-unread main-end margin). Otherwise the removal
1407
+ * shrinks every surviving in-flow sibling's dependency set, so the
1408
+ * grammar is rebuilt O(tree) and `rebinds` carries those siblings'
1409
+ * rewritten rules. The caller applies `rebindRule` for each rebind
1410
+ * FIRST (so survivors stop depending on the removed fields), then
1411
+ * `detach`, then `recompute()`.
1412
+ *
1413
+ * Does not mutate the tree. `next.grammar` is `prev.grammar` — the
1414
+ * runtime's own Map, patched in place by `detach` / `rebindRule`.
1415
+ *
1416
+ * @internal
1417
+ */
1418
+ export function buildRemoveFragment(prev, root, parent, child, available = {}) {
1419
+ // `child` must be a child of `parent`.
1420
+ let index = -1;
1421
+ for (let i = 0; i < parent.getChildCount(); i++) {
1422
+ if (parent.getChild(i) === child) {
1423
+ index = i;
1424
+ break;
1425
+ }
1426
+ }
1427
+ if (index === -1)
1428
+ return null;
1429
+ // A `display: 'none'` child had no fields to begin with — there is
1430
+ // nothing to detach; let the rebuild path absorb the removal.
1431
+ if (child.style.display === 'none')
1432
+ return null;
1433
+ // A reverse-direction parent is supported by the grammar (v11) but
1434
+ // not by this fast-path: the flip reflects every sibling, so fall
1435
+ // back to a full rebuild.
1436
+ const dir = parent.style.flexDirection;
1437
+ if (dir !== 'row' && dir !== 'column')
1438
+ return null;
1439
+ // Removing a last child from a simple-regime parent perturbs no
1440
+ // surviving sibling. `child` is still attached, so
1441
+ // `parentNeedsFlexDistribution` sees it: a parent flex-distributing
1442
+ // *because of* `child` is correctly non-simple.
1443
+ const isLast = index === parent.getChildCount() - 1;
1444
+ const simple = child.style.positionType === 'absolute' ||
1445
+ (isLast &&
1446
+ parent.style.flexWrap === 'nowrap' &&
1447
+ parent.style.justifyContent === 'flex-start' &&
1448
+ !parentNeedsFlexDistribution(parent));
1449
+ // Collect the removed subtree's nodes (`child` + descendants).
1450
+ const removedNodes = new Set();
1451
+ const stack = [child];
1452
+ while (stack.length > 0) {
1453
+ const n = stack.pop();
1454
+ removedNodes.add(n);
1455
+ for (let i = 0; i < n.getChildCount(); i++)
1456
+ stack.push(n.getChild(i));
1457
+ }
1458
+ if (simple) {
1459
+ // O(subtree): the removed set is exactly the subtree's own
1460
+ // fields, gathered straight from `prev` — no grammar rebuild.
1461
+ // `detach` auto-cleans the input fields they orphan.
1462
+ const removed = [];
1463
+ for (const n of removedNodes) {
1464
+ for (const f of nodeFields(n, prev.styleInputs)) {
1465
+ if (prev.grammar.has(f))
1466
+ removed.push(f);
1467
+ }
1468
+ }
1469
+ // Mutate prev.styleInputs and prev.mainDistributionByParent in place:
1470
+ // `prev` is the old grammar output, single-use — the caller swaps
1471
+ // built.output to the new fragment immediately after this returns.
1472
+ for (const n of removedNodes) {
1473
+ prev.styleInputs.delete(n);
1474
+ prev.mainDistributionByParent.delete(n);
1475
+ }
1476
+ const next = {
1477
+ grammar: prev.grammar,
1478
+ rootFields: prev.rootFields,
1479
+ allFields: prev.allFields.filter((e) => !removedNodes.has(e.node)),
1480
+ styleInputs: prev.styleInputs,
1481
+ availableInputs: prev.availableInputs,
1482
+ mainDistributionByParent: prev.mainDistributionByParent,
1483
+ };
1484
+ return { removed, rebinds: [], next };
1485
+ }
1486
+ // Non-simple: the removal rewrites every surviving sibling's rules.
1487
+ // Rebuild the grammar O(tree) — detach `child` around the build,
1488
+ // then restore the tree — and diff `prev \ fresh` for `removed`.
1489
+ // The rebuild needs the caller's `available` (root size rule shape).
1490
+ parent.removeChild(child);
1491
+ const fresh = buildFlexGrammar(root, available);
1492
+ parent.insertChild(child, index);
1493
+ const removed = [];
1494
+ for (const f of prev.grammar.keys()) {
1495
+ if (!fresh.grammar.has(f))
1496
+ removed.push(f);
1497
+ }
1498
+ const rebinds = [];
1499
+ for (let i = 0; i < parent.getChildCount(); i++) {
1500
+ const sib = parent.getChild(i);
1501
+ if (sib === child || !isInFlow(sib))
1502
+ continue;
1503
+ for (const name of ['width', 'height', 'left', 'top']) {
1504
+ const f = field(sib, name);
1505
+ const rule = fresh.grammar.get(f);
1506
+ if (rule !== undefined)
1507
+ rebinds.push([f, rule]);
1508
+ }
1509
+ }
1510
+ // Phase 12: if the parent's mainDistribution field survived the
1511
+ // removal (it's still in fresh.grammar), its closure now covers
1512
+ // fewer siblings — rebind it so recompute sees the updated list.
1513
+ const parentMainDistFRem = fresh.mainDistributionByParent.get(parent);
1514
+ if (parentMainDistFRem !== undefined && prev.grammar.has(parentMainDistFRem)) {
1515
+ const freshRule = fresh.grammar.get(parentMainDistFRem);
1516
+ if (freshRule !== undefined)
1517
+ rebinds.unshift([parentMainDistFRem, freshRule]);
1518
+ }
1519
+ const next = {
1520
+ grammar: prev.grammar,
1521
+ rootFields: prev.rootFields,
1522
+ allFields: fresh.allFields,
1523
+ styleInputs: fresh.styleInputs,
1524
+ availableInputs: fresh.availableInputs,
1525
+ mainDistributionByParent: fresh.mainDistributionByParent,
1526
+ };
1527
+ return { removed, rebinds, next };
1528
+ }
1529
+ /**
1530
+ * Fast-path a child REORDER for the Spineless runtime — `parent`'s
1531
+ * children are a permutation of their former order (no node added or
1532
+ * removed). The grammar is rebuilt O(tree) and the patch applied to
1533
+ * the existing runtime without a fresh `init`.
1534
+ *
1535
+ * Reordering `parent`'s children can only change the rules of
1536
+ * `parent` itself (its `'auto'` / wrap content size now packs the
1537
+ * children in a new order) and of its in-flow children (their main
1538
+ * positions, and — under wrap — their line-dependent sizes), so the
1539
+ * rebind set is `parent` + its in-flow children's
1540
+ * `width / height / left / top`. Descendants and ancestors recompute
1541
+ * from the changed VALUES; their rules are untouched.
1542
+ *
1543
+ * A reorder also shifts the "has a follower" boundary, so a node's
1544
+ * main-end margin input can become newly read (`additions`) or
1545
+ * newly unread (`removed`) — the same diff `buildAppendFragment`'s
1546
+ * non-simple branch takes. The caller applies `graft` then
1547
+ * `rebindRule` then `detach`.
1548
+ *
1549
+ * `next.grammar` is `prev.grammar` — the runtime's own Map, patched
1550
+ * in place.
1551
+ *
1552
+ * @internal
1553
+ */
1554
+ export function buildReorderFragment(prev, root, parent, available = {}) {
1555
+ const fresh = buildFlexGrammar(root, available);
1556
+ const additions = new Map();
1557
+ for (const [f, rule] of fresh.grammar) {
1558
+ if (!prev.grammar.has(f))
1559
+ additions.set(f, rule);
1560
+ }
1561
+ const removed = [];
1562
+ for (const f of prev.grammar.keys()) {
1563
+ if (!fresh.grammar.has(f))
1564
+ removed.push(f);
1565
+ }
1566
+ const rebinds = [];
1567
+ const touched = [parent];
1568
+ for (let i = 0; i < parent.getChildCount(); i++) {
1569
+ const c = parent.getChild(i);
1570
+ if (isInFlow(c))
1571
+ touched.push(c);
1572
+ }
1573
+ for (const n of touched) {
1574
+ for (const name of ['width', 'height', 'left', 'top']) {
1575
+ const f = field(n, name);
1576
+ const rule = fresh.grammar.get(f);
1577
+ if (rule !== undefined)
1578
+ rebinds.push([f, rule]);
1579
+ }
1580
+ }
1581
+ // Phase 12: the parent's mainDistribution field captures flexSibs in
1582
+ // child-position order. A reorder changes that order, so rebind it
1583
+ // (prepend so it is rebound before any child width that reads it).
1584
+ const parentMainDistFReorder = fresh.mainDistributionByParent.get(parent);
1585
+ if (parentMainDistFReorder !== undefined &&
1586
+ prev.grammar.has(parentMainDistFReorder)) {
1587
+ const freshRule = fresh.grammar.get(parentMainDistFReorder);
1588
+ if (freshRule !== undefined)
1589
+ rebinds.unshift([parentMainDistFReorder, freshRule]);
1590
+ }
1591
+ const next = {
1592
+ grammar: prev.grammar,
1593
+ rootFields: prev.rootFields,
1594
+ allFields: fresh.allFields,
1595
+ styleInputs: fresh.styleInputs,
1596
+ availableInputs: fresh.availableInputs,
1597
+ mainDistributionByParent: fresh.mainDistributionByParent,
1598
+ };
1599
+ return { additions, newRoots: [...additions.keys()], removed, rebinds, next };
1600
+ }
1601
+ /**
1602
+ * Resolve a node's main-axis basis from its style input fields:
1603
+ * numeric `flexBasis` wins over the main-axis size. Mirrors the
1604
+ * imperative `resolveHypotheticalMainSize`. Both inputs are declared
1605
+ * deps of the calling rule, so this reads only cached values.
1606
+ *
1607
+ * @internal
1608
+ */
1609
+ function resolveBasisFromRead(read, flexBasisInput, mainInput) {
1610
+ const basis = read(flexBasisInput);
1611
+ return typeof basis === 'number' ? basis : read(mainInput);
1612
+ }
1613
+ /**
1614
+ * Clamp `value` to `[min, max]`. `max` carries `Infinity` for "no
1615
+ * upper bound" (see `minMaxInput`). Mirrors the imperative
1616
+ * `clampSize`: the floor is applied before the cap, so when
1617
+ * `min > max` the cap wins — `clampMinMax(5, 10, 3) === 3`.
1618
+ *
1619
+ * @internal
1620
+ */
1621
+ function clampMinMax(value, min, max) {
1622
+ return Math.min(Math.max(value, min), max);
1623
+ }
1624
+ /**
1625
+ * True iff `axis` resolves from `aspectRatio` (v15): the axis is
1626
+ * `'auto'`, an `aspectRatio` is set, and the perpendicular axis is
1627
+ * an explicit number — exactly the case where the imperative
1628
+ * `effectivePreferredSize` derives a concrete size for an otherwise
1629
+ * `'auto'` axis. When this holds the axis is NOT content-sized: it
1630
+ * has a definite preferred size, so (e.g.) `align-items: stretch`
1631
+ * does not resize it.
1632
+ *
1633
+ * @internal
1634
+ */
1635
+ function aspectDerivable(node, axis) {
1636
+ if (typeof node.style[axis] === 'number')
1637
+ return false;
1638
+ if (node.style.aspectRatio === undefined)
1639
+ return false;
1640
+ const other = axis === 'width' ? 'height' : 'width';
1641
+ return typeof node.style[other] === 'number';
1642
+ }
1643
+ /**
1644
+ * True iff `node` is a measure-function leaf: a childless node with
1645
+ * a measure function. The imperative algorithm consults the measurer
1646
+ * for such a node's `'auto'` axes (`resolveHypotheticalMainSize` /
1647
+ * `naturalCrossSize`); a non-leaf or measure-less node does not.
1648
+ *
1649
+ * @internal
1650
+ */
1651
+ function isMeasureLeaf(node) {
1652
+ return node.getChildCount() === 0 && node.getMeasureFunc() !== null;
1653
+ }
1654
+ /**
1655
+ * Build the flex-distribution inputs for a fixed in-flow sibling set.
1656
+ * Every value — basis, grow / shrink weights, main-axis margins — is
1657
+ * read from a declared input field via `read`, so the calling rule's
1658
+ * dep list fully covers them.
1659
+ *
1660
+ * @internal
1661
+ */
1662
+ function liveFlexSiblings(sibs, read) {
1663
+ return sibs.map((s) => ({
1664
+ basis: resolveBasisFromRead(read, s.flexBasisInput, s.mainInput),
1665
+ grow: read(s.growInput),
1666
+ shrink: read(s.shrinkInput),
1667
+ marginStart: read(s.marginMainStartInput),
1668
+ marginEnd: read(s.marginMainEndInput),
1669
+ min: read(s.minInput),
1670
+ max: read(s.maxInput),
1671
+ }));
1672
+ }
1673
+ /**
1674
+ * Build the `WrapSibling` set for a wrapping container's in-flow
1675
+ * children. Like `liveFlexSiblings` but also carries cross-axis size
1676
+ * (from the declared `crossInput` field) + margins and the resolved
1677
+ * align value, which the wrap line packer needs.
1678
+ *
1679
+ * @internal
1680
+ */
1681
+ function liveWrapSiblings(sibs, parent, read) {
1682
+ return sibs.map((s) => {
1683
+ const alignSelf = s.node.style.alignSelf;
1684
+ const crossNatural = read(s.crossInput);
1685
+ return {
1686
+ basis: resolveBasisFromRead(read, s.flexBasisInput, s.mainInput),
1687
+ grow: read(s.growInput),
1688
+ shrink: read(s.shrinkInput),
1689
+ min: read(s.minInput),
1690
+ max: read(s.maxInput),
1691
+ mainMarginStart: read(s.marginMainStartInput),
1692
+ mainMarginEnd: read(s.marginMainEndInput),
1693
+ // Two cross sizes (v12b): the imperative computes a line's
1694
+ // cross size from each item's UNCLAMPED natural cross
1695
+ // (`computeLineCrossSizes` → `naturalCross`), but positions an
1696
+ // item within its line using the CLAMPED cross
1697
+ // (`crossAlignItemsInLine` → `clampSize(naturalCross)`). A
1698
+ // min/max clamp can therefore make an item overflow its line.
1699
+ crossSizeNatural: crossNatural,
1700
+ crossSize: clampMinMax(crossNatural, read(s.minCrossInput), read(s.maxCrossInput)),
1701
+ crossMarginStart: read(s.marginCrossStartInput),
1702
+ crossMarginEnd: read(s.marginCrossEndInput),
1703
+ crossIsContentAuto: s.crossIsContentAuto,
1704
+ crossMin: read(s.minCrossInput),
1705
+ crossMax: read(s.maxCrossInput),
1706
+ align: alignSelf === 'auto' ? parent.style.alignItems : alignSelf,
1707
+ };
1708
+ });
1709
+ }
1710
+ /**
1711
+ * Emit the parent-level main-axis distribution Field for a
1712
+ * flex-distributing parent. Returns the Field so the per-child
1713
+ * mainSize / mainPos rules can declare it as their dependency.
1714
+ *
1715
+ * `deps` is the SAME deps list today's per-cell mainSizeField
1716
+ * rule declares (parent main, gap, padding, plus each in-flow
1717
+ * sibling's eight flex-related inputs).
1718
+ */
1719
+ function emitMainDistribution(grammar, parent, flexSibs, parentMainField, mainGapInput, padMainStartF, padMainEndF, marginInput, parentDirection) {
1720
+ const mainDistField = field(parent, 'mainDistribution');
1721
+ const deps = [
1722
+ parentMainField,
1723
+ mainGapInput,
1724
+ padMainStartF,
1725
+ padMainEndF,
1726
+ ];
1727
+ for (const s of flexSibs) {
1728
+ deps.push(s.flexBasisInput, s.mainInput, s.growInput, s.shrinkInput, s.marginMainStartInput, s.marginMainEndInput, s.minInput, s.maxInput);
1729
+ }
1730
+ const mainStartEdgeName = mainStartEdge(parentDirection);
1731
+ const mainEndEdgeName = mainEndEdge(parentDirection);
1732
+ grammar.set(mainDistField, {
1733
+ deps,
1734
+ compute: (read) => {
1735
+ const innerMain = Math.max(0, read(parentMainField) - read(padMainStartF) - read(padMainEndF));
1736
+ const siblings = liveFlexSiblings(flexSibs, read);
1737
+ const sizes = distributeMainAxis(siblings, innerMain, read(mainGapInput));
1738
+ // Fold sizes + margins + gaps into a prefix-sum positions array.
1739
+ // positions[i] is the i-th in-flow child's main offset from the
1740
+ // parent's border-box origin (so it INCLUDES the parent's main-
1741
+ // start padding), matching what the existing flex-start mainPos
1742
+ // rule returns. Task 2 assigns this directly to the child's
1743
+ // mainPos Field.
1744
+ const positions = new Array(sizes.length);
1745
+ const gap = read(mainGapInput);
1746
+ const startPad = read(padMainStartF);
1747
+ let cursor = startPad;
1748
+ for (let i = 0; i < sizes.length; i++) {
1749
+ const sib = flexSibs[i];
1750
+ const marginStart = read(marginInput(sib.node, mainStartEdgeName));
1751
+ const marginEnd = read(marginInput(sib.node, mainEndEdgeName));
1752
+ if (i > 0)
1753
+ cursor += gap;
1754
+ cursor += marginStart;
1755
+ positions[i] = cursor;
1756
+ cursor += sizes[i] + marginEnd;
1757
+ }
1758
+ return { sizes, positions };
1759
+ },
1760
+ });
1761
+ return mainDistField;
1762
+ }
1763
+ /**
1764
+ * Emit the main-position rule for a child when the parent's
1765
+ * `justify-content` is not the default `flex-start`. The leftover
1766
+ * along the main axis is `max(0, innerMain - usedMain)`, where
1767
+ * `usedMain` is the sum of post-distribution main sizes plus margins
1768
+ * plus inter-item gaps. The leftover is distributed as a leading
1769
+ * cursor offset and/or an extra gap between items (the CSS rule).
1770
+ *
1771
+ * Dep graph: every sibling's main size and the parent's main size.
1772
+ * This is broader than the default flex-start case (priors only) but
1773
+ * matches what CSS requires — change any sibling's size and every
1774
+ * item's position can move under space-* or center.
1775
+ *
1776
+ * Dep graph: every in-flow sibling's main size, main-axis margins,
1777
+ * the parent's main size, the main-axis `gap`, and the parent's
1778
+ * main-axis `padding` edges — all declared input / layout Fields, so
1779
+ * any of them changing re-runs this rule.
1780
+ *
1781
+ * @internal
1782
+ */
1783
+ function emitJustifiedMainPos(grammar, parent, mainPosField, mainSizeName, justify, indexInParent, direction, gapField, padStartField, padEndField, marginInput) {
1784
+ // In-flow siblings only — absolute and `display: 'none'` children
1785
+ // don't contribute to justify-content's leftover calculation.
1786
+ const inFlow = [];
1787
+ for (let i = 0; i < parent.getChildCount(); i++) {
1788
+ const sib = parent.getChild(i);
1789
+ if (!isInFlow(sib))
1790
+ continue;
1791
+ inFlow.push(sib);
1792
+ }
1793
+ const allSizes = inFlow.map((s) => field(s, mainSizeName));
1794
+ const startEdge = mainStartEdge(direction);
1795
+ const endEdge = mainEndEdge(direction);
1796
+ const marginStarts = inFlow.map((s) => marginInput(s, startEdge));
1797
+ const marginEnds = inFlow.map((s) => marginInput(s, endEdge));
1798
+ const n = allSizes.length;
1799
+ const parentMainField = field(parent, mainSizeName);
1800
+ grammar.set(mainPosField, {
1801
+ deps: [
1802
+ parentMainField,
1803
+ gapField,
1804
+ padStartField,
1805
+ padEndField,
1806
+ ...allSizes,
1807
+ ...marginStarts,
1808
+ ...marginEnds,
1809
+ ],
1810
+ compute: (read) => {
1811
+ const padStart = read(padStartField);
1812
+ const gap = read(gapField);
1813
+ const innerMain = Math.max(0, read(parentMainField) - padStart - read(padEndField));
1814
+ let usedMain = 0;
1815
+ for (let i = 0; i < n; i++) {
1816
+ usedMain += read(allSizes[i]) + read(marginStarts[i]) + read(marginEnds[i]);
1817
+ }
1818
+ if (n > 1)
1819
+ usedMain += (n - 1) * gap;
1820
+ const leftover = Math.max(0, innerMain - usedMain);
1821
+ let leadingOffset = 0;
1822
+ let extraGap = 0;
1823
+ switch (justify) {
1824
+ case 'flex-end':
1825
+ leadingOffset = leftover;
1826
+ break;
1827
+ case 'center':
1828
+ leadingOffset = leftover / 2;
1829
+ break;
1830
+ case 'space-between':
1831
+ if (n > 1)
1832
+ extraGap = leftover / (n - 1);
1833
+ break;
1834
+ case 'space-around': {
1835
+ const slot = leftover / n;
1836
+ leadingOffset = slot / 2;
1837
+ extraGap = slot;
1838
+ break;
1839
+ }
1840
+ case 'space-evenly': {
1841
+ const slot = leftover / (n + 1);
1842
+ leadingOffset = slot;
1843
+ extraGap = slot;
1844
+ break;
1845
+ }
1846
+ }
1847
+ // Cursor walks the line up to this child, mirroring the
1848
+ // imperative positionItemsInLine.
1849
+ let cursor = padStart + leadingOffset;
1850
+ for (let i = 0; i < indexInParent; i++) {
1851
+ cursor += read(marginStarts[i]) + read(allSizes[i]) + read(marginEnds[i]);
1852
+ cursor += gap + extraGap;
1853
+ }
1854
+ cursor += read(marginStarts[indexInParent]);
1855
+ return cursor;
1856
+ },
1857
+ });
1858
+ }
1859
+ /**
1860
+ * Re-wrap a child's already-emitted main-position rule so a
1861
+ * reverse-direction parent (`row-reverse` / `column-reverse`) lays
1862
+ * the child out from the main-axis END.
1863
+ *
1864
+ * Mirrors the imperative `flipMainAxis`: with `innerPos` the child's
1865
+ * forward offset inside the parent's inner-main box, the reflected
1866
+ * position is `padStart + innerMain - innerPos - childMain`. The
1867
+ * forward rule is preserved and invoked for `innerPos`; this wrapper
1868
+ * only reflects its result, so every regime (flex-start, justified,
1869
+ * wrap) reverses uniformly.
1870
+ *
1871
+ * The deps become the union of the forward rule's deps and the three
1872
+ * fields the reflection adds — the parent's main size, both main-axis
1873
+ * padding edges, and the child's own main size.
1874
+ *
1875
+ * @internal
1876
+ */
1877
+ function applyReverseMainPos(grammar, parent, mainPosField, mainSizeField, mainSizeName, padMainStartF, padMainEndF) {
1878
+ const forward = grammar.get(mainPosField);
1879
+ const parentMainField = field(parent, mainSizeName);
1880
+ const deps = [...forward.deps];
1881
+ for (const d of [
1882
+ parentMainField,
1883
+ padMainStartF,
1884
+ padMainEndF,
1885
+ mainSizeField,
1886
+ ]) {
1887
+ if (!deps.includes(d))
1888
+ deps.push(d);
1889
+ }
1890
+ grammar.set(mainPosField, {
1891
+ deps,
1892
+ compute: (read) => {
1893
+ const forwardPos = forward.compute(read);
1894
+ const padStart = read(padMainStartF);
1895
+ const innerMain = Math.max(0, read(parentMainField) - padStart - read(padMainEndF));
1896
+ const childMain = read(mainSizeField);
1897
+ const innerPos = forwardPos - padStart;
1898
+ return padStart + innerMain - innerPos - childMain;
1899
+ },
1900
+ });
1901
+ }
1902
+ /**
1903
+ * True iff a parent's children carry any flex property that lets a
1904
+ * child's main size differ from its raw `style.{width|height}`: a
1905
+ * positive grow weight, a positive shrink weight, or a numeric
1906
+ * `flexBasis`.
1907
+ */
1908
+ function parentNeedsFlexDistribution(parent) {
1909
+ const count = parent.getChildCount();
1910
+ for (let i = 0; i < count; i++) {
1911
+ const c = parent.getChild(i);
1912
+ if (!isInFlow(c))
1913
+ continue;
1914
+ const s = c.style;
1915
+ if (s.flexGrow > 0)
1916
+ return true;
1917
+ if (s.flexShrink > 0)
1918
+ return true;
1919
+ if (typeof s.flexBasis === 'number')
1920
+ return true;
1921
+ }
1922
+ return false;
1923
+ }
1924
+ /**
1925
+ * Distribute `budget` across siblings using CSS flex semantics with
1926
+ * min/max clamping (v12b). Returns each sibling's final main-axis
1927
+ * size in input order.
1928
+ *
1929
+ * `budget` is the parent's inner main size (containerMain minus
1930
+ * leading + trailing padding). Margins and gaps are accounted for via
1931
+ * the hypothetical sum: only the basis part of each sibling expands
1932
+ * (grow) or contracts (shrink); margins and gaps are fixed-width
1933
+ * spacers that consume budget but never resize.
1934
+ *
1935
+ * Each sibling's hypothetical size is its basis clamped to its own
1936
+ * `[min, max]` — the imperative `buildItem` clamps before packing /
1937
+ * distribution. The grow / shrink passes then run the CSS freeze
1938
+ * loop (`freezeLoopGrow` / `freezeLoopShrink`): an item whose
1939
+ * proportional target would breach a clamp is pinned ("frozen") at
1940
+ * its bound and its share is redistributed among the rest, iterating
1941
+ * to a fixpoint. Mirrors `distributeGrow` / `distributeShrink` in
1942
+ * `main-axis.ts`.
1943
+ *
1944
+ * @internal
1945
+ */
1946
+ function distributeMainAxis(siblings, budget, gap) {
1947
+ const n = siblings.length;
1948
+ // Hypothetical = basis clamped to the sibling's own [min, max].
1949
+ const hyp = siblings.map((s) => clampMinMax(s.basis, s.min, s.max));
1950
+ let hypotheticalMain = 0;
1951
+ for (let i = 0; i < n; i++) {
1952
+ hypotheticalMain += hyp[i] + siblings[i].marginStart + siblings[i].marginEnd;
1953
+ }
1954
+ if (n > 1)
1955
+ hypotheticalMain += (n - 1) * gap;
1956
+ const slack = budget - hypotheticalMain;
1957
+ const final = hyp.slice();
1958
+ if (slack > 0) {
1959
+ freezeLoopGrow(siblings, hyp, final, slack);
1960
+ }
1961
+ else if (slack < 0) {
1962
+ freezeLoopShrink(siblings, hyp, final, -slack);
1963
+ }
1964
+ return final;
1965
+ }
1966
+ /**
1967
+ * The flex-grow freeze loop. `hyp` holds each sibling's clamped
1968
+ * hypothetical; `final` is seeded with `hyp` and mutated in place to
1969
+ * the post-distribution sizes. Items with `grow <= 0` never grow;
1970
+ * an item whose proportional target breaches its `[min, max]` is
1971
+ * frozen at the clamped bound and drops out of subsequent rounds.
1972
+ * Mirrors the imperative `distributeGrow`.
1973
+ */
1974
+ function freezeLoopGrow(siblings, hyp, final, slack) {
1975
+ const n = siblings.length;
1976
+ const frozen = new Array(n).fill(false);
1977
+ for (let i = 0; i < n; i++) {
1978
+ if (siblings[i].grow <= 0)
1979
+ frozen[i] = true;
1980
+ }
1981
+ for (let iter = 0; iter < n + 1; iter++) {
1982
+ let totalGrow = 0;
1983
+ let frozenContribution = 0;
1984
+ for (let i = 0; i < n; i++) {
1985
+ if (frozen[i])
1986
+ frozenContribution += final[i] - hyp[i];
1987
+ else
1988
+ totalGrow += siblings[i].grow;
1989
+ }
1990
+ if (totalGrow <= 0)
1991
+ return;
1992
+ const remaining = slack - frozenContribution;
1993
+ if (remaining <= 0)
1994
+ return;
1995
+ let frozeAny = false;
1996
+ for (let i = 0; i < n; i++) {
1997
+ if (frozen[i])
1998
+ continue;
1999
+ const s = siblings[i];
2000
+ const target = hyp[i] + (remaining * s.grow) / totalGrow;
2001
+ const clamped = clampMinMax(target, s.min, s.max);
2002
+ final[i] = clamped;
2003
+ if (clamped !== target) {
2004
+ frozen[i] = true;
2005
+ frozeAny = true;
2006
+ }
2007
+ }
2008
+ if (!frozeAny)
2009
+ return;
2010
+ }
2011
+ }
2012
+ /**
2013
+ * The flex-shrink freeze loop — symmetric to `freezeLoopGrow`. The
2014
+ * shrink share is scaled by `shrink * hypothetical` (CSS weights
2015
+ * shrink by base size). Mirrors the imperative `distributeShrink`.
2016
+ */
2017
+ function freezeLoopShrink(siblings, hyp, final, overflow) {
2018
+ const n = siblings.length;
2019
+ const frozen = new Array(n).fill(false);
2020
+ for (let i = 0; i < n; i++) {
2021
+ if (siblings[i].shrink <= 0)
2022
+ frozen[i] = true;
2023
+ }
2024
+ for (let iter = 0; iter < n + 1; iter++) {
2025
+ let totalScaled = 0;
2026
+ let frozenContribution = 0;
2027
+ for (let i = 0; i < n; i++) {
2028
+ if (frozen[i])
2029
+ frozenContribution += hyp[i] - final[i];
2030
+ else
2031
+ totalScaled += siblings[i].shrink * hyp[i];
2032
+ }
2033
+ if (totalScaled <= 0)
2034
+ return;
2035
+ const remaining = overflow - frozenContribution;
2036
+ if (remaining <= 0)
2037
+ return;
2038
+ let frozeAny = false;
2039
+ for (let i = 0; i < n; i++) {
2040
+ if (frozen[i])
2041
+ continue;
2042
+ const s = siblings[i];
2043
+ const scaled = s.shrink * hyp[i];
2044
+ if (scaled <= 0) {
2045
+ frozen[i] = true;
2046
+ continue;
2047
+ }
2048
+ const reduction = (remaining * scaled) / totalScaled;
2049
+ const target = hyp[i] - reduction;
2050
+ const clamped = clampMinMax(target, s.min, s.max);
2051
+ final[i] = clamped;
2052
+ if (clamped !== target) {
2053
+ frozen[i] = true;
2054
+ frozeAny = true;
2055
+ }
2056
+ }
2057
+ if (!frozeAny)
2058
+ return;
2059
+ }
2060
+ }
2061
+ // ─── axis-aware spacing readers ─────────────────────────────────────────
2062
+ // Edge order in style boxes is [top, right, bottom, left]. Gap layout
2063
+ // is keyed on the OUTPUT axis (the one items stack along), not the
2064
+ // flex-direction name — `gapColumn` separates row-stacked items
2065
+ // (column between columns), `gapRow` separates column-stacked items.
2066
+ const TOP = 0;
2067
+ const RIGHT = 1;
2068
+ const BOTTOM = 2;
2069
+ const LEFT = 3;
2070
+ function mainStartEdge(direction) {
2071
+ return direction === 'column' ? TOP : LEFT;
2072
+ }
2073
+ function mainEndEdge(direction) {
2074
+ return direction === 'column' ? BOTTOM : RIGHT;
2075
+ }
2076
+ function crossStartEdge(direction) {
2077
+ return direction === 'column' ? LEFT : TOP;
2078
+ }
2079
+ function crossEndEdge(direction) {
2080
+ // Cross axis is perpendicular to the main axis; its end edge sits
2081
+ // opposite the cross start edge.
2082
+ return direction === 'column' ? RIGHT : BOTTOM;
2083
+ }
2084
+ // ─── absolute positioning ───────────────────────────────────────────────
2085
+ /**
2086
+ * Emit the four field rules for an out-of-flow (`positionType ===
2087
+ * 'absolute'`) child. Mirrors the imperative `layoutAbsoluteChild`
2088
+ * in `main-axis.ts`:
2089
+ *
2090
+ * - width: explicit `style.width` if numeric, else (if both LEFT
2091
+ * and RIGHT edges are set) derived from `parent.width - left -
2092
+ * right - margins`, else 0.
2093
+ * - height: symmetric, using TOP / BOTTOM edges.
2094
+ * - left: if LEFT edge set, `left + margin.left`; else if RIGHT
2095
+ * edge set, `parent.width - width - right - margin.right`; else
2096
+ * `margin.left`.
2097
+ * - top: symmetric, using TOP / BOTTOM and `parent.height`.
2098
+ *
2099
+ * The parent's OUTER size is used (no padding subtraction) —
2100
+ * matches Yoga / RN semantics that Pilates follows. Width and height
2101
+ * are clamped to the child's own [min, max] in every branch (v12),
2102
+ * exactly as the imperative `layoutAbsoluteChild` — including the
2103
+ * `0` fallback, so e.g. a `minWidth` with no explicit width still
2104
+ * binds.
2105
+ *
2106
+ * @internal
2107
+ */
2108
+ function emitAbsoluteRules(grammar, styleSizeInput, marginInput, minMaxInput, parent, child, width, height, left, top) {
2109
+ const pos = child.style.position;
2110
+ const posTop = pos[TOP];
2111
+ const posRight = pos[RIGHT];
2112
+ const posBottom = pos[BOTTOM];
2113
+ const posLeft = pos[LEFT];
2114
+ // Margins are declared input-field deps so a `setMargin` on the
2115
+ // absolute child propagates precisely. The `position` edges stay
2116
+ // captured: their presence selects the branch (structural).
2117
+ const mTop = marginInput(child, TOP);
2118
+ const mRight = marginInput(child, RIGHT);
2119
+ const mBottom = marginInput(child, BOTTOM);
2120
+ const mLeft = marginInput(child, LEFT);
2121
+ const styleW = child.style.width;
2122
+ const styleH = child.style.height;
2123
+ const parentWField = field(parent, 'width');
2124
+ const parentHField = field(parent, 'height');
2125
+ // Min / max clamp inputs — declared deps so a `setMinWidth` … on
2126
+ // the absolute child propagates precisely.
2127
+ const minW = minMaxInput(child, 'minWidth');
2128
+ const maxW = minMaxInput(child, 'maxWidth');
2129
+ const minH = minMaxInput(child, 'minHeight');
2130
+ const maxH = minMaxInput(child, 'maxHeight');
2131
+ // Width. The explicit-width branch reads the child's `style:width`
2132
+ // input field, so a `setWidth` on the absolute child propagates
2133
+ // precisely through markDirty + recompute. Mutating the child from
2134
+ // explicit to 'auto' (or vice-versa) requires a fresh grammar build
2135
+ // since that crosses branch boundaries — out of scope here. Every
2136
+ // branch clamps to [minW, maxW], matching `layoutAbsoluteChild`.
2137
+ if (typeof styleW === 'number') {
2138
+ const wInput = styleSizeInput(child, 'width');
2139
+ grammar.set(width, {
2140
+ deps: [wInput, minW, maxW],
2141
+ compute: (read) => clampMinMax(read(wInput), read(minW), read(maxW)),
2142
+ });
2143
+ }
2144
+ else if (posLeft !== undefined && posRight !== undefined) {
2145
+ grammar.set(width, {
2146
+ deps: [
2147
+ parentWField,
2148
+ mLeft,
2149
+ mRight,
2150
+ minW,
2151
+ maxW,
2152
+ ],
2153
+ compute: (read) => clampMinMax(Math.max(0, read(parentWField) - posLeft - posRight - read(mLeft) - read(mRight)), read(minW), read(maxW)),
2154
+ });
2155
+ }
2156
+ else if (isMeasureLeaf(child)) {
2157
+ // `'auto'` width, no opposing edges: the measurer sizes it.
2158
+ // Mirrors `layoutAbsoluteChild` — call the measurer with the
2159
+ // parent's outer box `AtMost` on both axes and take `.width`.
2160
+ const measure = child.getMeasureFunc();
2161
+ grammar.set(width, {
2162
+ deps: [
2163
+ parentWField,
2164
+ parentHField,
2165
+ minW,
2166
+ maxW,
2167
+ ],
2168
+ compute: (read) => clampMinMax(measure(read(parentWField), MeasureMode.AtMost, read(parentHField), MeasureMode.AtMost)
2169
+ .width, read(minW), read(maxW)),
2170
+ });
2171
+ }
2172
+ else {
2173
+ grammar.set(width, {
2174
+ deps: [minW, maxW],
2175
+ compute: (read) => clampMinMax(0, read(minW), read(maxW)),
2176
+ });
2177
+ }
2178
+ // Height — symmetric to width.
2179
+ if (typeof styleH === 'number') {
2180
+ const hInput = styleSizeInput(child, 'height');
2181
+ grammar.set(height, {
2182
+ deps: [hInput, minH, maxH],
2183
+ compute: (read) => clampMinMax(read(hInput), read(minH), read(maxH)),
2184
+ });
2185
+ }
2186
+ else if (posTop !== undefined && posBottom !== undefined) {
2187
+ grammar.set(height, {
2188
+ deps: [
2189
+ parentHField,
2190
+ mTop,
2191
+ mBottom,
2192
+ minH,
2193
+ maxH,
2194
+ ],
2195
+ compute: (read) => clampMinMax(Math.max(0, read(parentHField) - posTop - posBottom - read(mTop) - read(mBottom)), read(minH), read(maxH)),
2196
+ });
2197
+ }
2198
+ else if (isMeasureLeaf(child)) {
2199
+ // `'auto'` height, no opposing edges: measure with the resolved
2200
+ // width `Exactly` and the parent's outer height `AtMost` — the
2201
+ // `layoutAbsoluteChild` height branch. The dep on `width` orders
2202
+ // this rule after the width rule above.
2203
+ const measure = child.getMeasureFunc();
2204
+ grammar.set(height, {
2205
+ deps: [
2206
+ width,
2207
+ parentHField,
2208
+ minH,
2209
+ maxH,
2210
+ ],
2211
+ compute: (read) => clampMinMax(measure(read(width), MeasureMode.Exactly, read(parentHField), MeasureMode.AtMost).height, read(minH), read(maxH)),
2212
+ });
2213
+ }
2214
+ else {
2215
+ grammar.set(height, {
2216
+ deps: [minH, maxH],
2217
+ compute: (read) => clampMinMax(0, read(minH), read(maxH)),
2218
+ });
2219
+ }
2220
+ // Left
2221
+ if (posLeft !== undefined) {
2222
+ grammar.set(left, {
2223
+ deps: [mLeft],
2224
+ compute: (read) => posLeft + read(mLeft),
2225
+ });
2226
+ }
2227
+ else if (posRight !== undefined) {
2228
+ grammar.set(left, {
2229
+ deps: [parentWField, width, mRight],
2230
+ compute: (read) => read(parentWField) - read(width) - posRight - read(mRight),
2231
+ });
2232
+ }
2233
+ else {
2234
+ grammar.set(left, {
2235
+ deps: [mLeft],
2236
+ compute: (read) => read(mLeft),
2237
+ });
2238
+ }
2239
+ // Top
2240
+ if (posTop !== undefined) {
2241
+ grammar.set(top, {
2242
+ deps: [mTop],
2243
+ compute: (read) => posTop + read(mTop),
2244
+ });
2245
+ }
2246
+ else if (posBottom !== undefined) {
2247
+ grammar.set(top, {
2248
+ deps: [parentHField, height, mBottom],
2249
+ compute: (read) => read(parentHField) - read(height) - posBottom - read(mBottom),
2250
+ });
2251
+ }
2252
+ else {
2253
+ grammar.set(top, {
2254
+ deps: [mTop],
2255
+ compute: (read) => read(mTop),
2256
+ });
2257
+ }
2258
+ }
2259
+ /**
2260
+ * Pack `siblings` greedily into lines along the main axis, run flex
2261
+ * distribution per line, compute each line's cross size and start,
2262
+ * then resolve the indicated child's `{mainSize, mainPos, crossPos}`.
2263
+ *
2264
+ * Mirrors the imperative `packIntoLines` → `distributeFlexInLine` →
2265
+ * `computeLineCrossSizes` → `positionLinesOnCross` (the `alignContent`
2266
+ * line distribution) → `positionItemsInLine` → `crossAlignItemsInLine`
2267
+ * chain. The single-line case (one packed line) collapses crossSize
2268
+ * to `innerCross` and crossPos of the line to 0, matching the
2269
+ * imperative's `singleLineMode` branch.
2270
+ *
2271
+ * Called once per child per layout pass; the per-child callbacks pick
2272
+ * out their own value from the returned struct. Total work is O(N²)
2273
+ * for an N-child wrapped container — acceptable for v7; later
2274
+ * Spineless tiers can extract shared per-line fields.
2275
+ *
2276
+ * @internal
2277
+ */
2278
+ function evaluateWrappedChild(siblings, childIndex, innerMain, innerCross, mainGap, crossGap, justify, alignContent, reverse, padMainStart, padCrossStart) {
2279
+ const n = siblings.length;
2280
+ // Pack greedily, recording per-line start index and count.
2281
+ const lineFirst = [];
2282
+ const lineCount = [];
2283
+ {
2284
+ let start = 0;
2285
+ let acc = 0;
2286
+ for (let i = 0; i < n; i++) {
2287
+ const s = siblings[i];
2288
+ // Pack on the clamped hypothetical (v12b) — the imperative
2289
+ // `packIntoLines` keys on `item.hypothetical`, not raw basis.
2290
+ const itemMain = clampMinMax(s.basis, s.min, s.max) + s.mainMarginStart + s.mainMarginEnd;
2291
+ const inLine = i > start;
2292
+ const wouldUse = acc + (inLine ? mainGap : 0) + itemMain;
2293
+ if (inLine && wouldUse > innerMain) {
2294
+ lineFirst.push(start);
2295
+ lineCount.push(i - start);
2296
+ start = i;
2297
+ acc = itemMain;
2298
+ }
2299
+ else {
2300
+ if (inLine)
2301
+ acc += mainGap;
2302
+ acc += itemMain;
2303
+ }
2304
+ }
2305
+ if (start < n) {
2306
+ lineFirst.push(start);
2307
+ lineCount.push(n - start);
2308
+ }
2309
+ }
2310
+ const numLines = lineFirst.length;
2311
+ const isMultiline = numLines > 1;
2312
+ // Per-line distribution. WrapSibling renames the main-axis margins
2313
+ // to `mainMargin*` (they share fields with the cross-axis margins);
2314
+ // distributeMainAxis takes a smaller shape so we map at the boundary.
2315
+ const finalMainSizes = new Array(n);
2316
+ for (let li = 0; li < numLines; li++) {
2317
+ const first = lineFirst[li];
2318
+ const count = lineCount[li];
2319
+ const lineSiblings = siblings.slice(first, first + count).map((s) => ({
2320
+ basis: s.basis,
2321
+ grow: s.grow,
2322
+ shrink: s.shrink,
2323
+ marginStart: s.mainMarginStart,
2324
+ marginEnd: s.mainMarginEnd,
2325
+ min: s.min,
2326
+ max: s.max,
2327
+ }));
2328
+ const distributed = distributeMainAxis(lineSiblings, innerMain, mainGap);
2329
+ for (let k = 0; k < count; k++) {
2330
+ finalMainSizes[first + k] = distributed[k];
2331
+ }
2332
+ }
2333
+ // Per-line cross size: container's inner cross for single-line,
2334
+ // max of (item.crossSize + cross margins) otherwise.
2335
+ const lineCrossSizes = new Array(numLines);
2336
+ if (!isMultiline) {
2337
+ lineCrossSizes[0] = innerCross;
2338
+ }
2339
+ else {
2340
+ for (let li = 0; li < numLines; li++) {
2341
+ const first = lineFirst[li];
2342
+ const count = lineCount[li];
2343
+ let max = 0;
2344
+ for (let k = 0; k < count; k++) {
2345
+ const s = siblings[first + k];
2346
+ // Unclamped natural cross — a clamped item may overflow.
2347
+ const candidate = s.crossSizeNatural + s.crossMarginStart + s.crossMarginEnd;
2348
+ if (candidate > max)
2349
+ max = candidate;
2350
+ }
2351
+ lineCrossSizes[li] = max;
2352
+ }
2353
+ }
2354
+ // Per-line cross start (align-content). Single-line: the one line
2355
+ // sits at 0. Multi-line: distribute the cross-axis leftover among
2356
+ // or around the lines per `alignContent`, mirroring the imperative
2357
+ // `positionLinesOnCross`. `stretch` / `auto` grows each line's
2358
+ // cross size to absorb the leftover instead.
2359
+ const lineCrossStarts = new Array(numLines);
2360
+ if (!isMultiline) {
2361
+ lineCrossStarts[0] = 0;
2362
+ }
2363
+ else {
2364
+ let used = 0;
2365
+ for (let li = 0; li < numLines; li++)
2366
+ used += lineCrossSizes[li];
2367
+ used += (numLines - 1) * crossGap;
2368
+ const leftover = innerCross - used;
2369
+ let cursor = 0;
2370
+ let extraGap = 0;
2371
+ let lineSizeBoost = 0;
2372
+ switch (alignContent) {
2373
+ case 'flex-end':
2374
+ cursor = leftover;
2375
+ break;
2376
+ case 'center':
2377
+ cursor = leftover / 2;
2378
+ break;
2379
+ case 'space-between':
2380
+ if (numLines > 1 && leftover > 0)
2381
+ extraGap = leftover / (numLines - 1);
2382
+ break;
2383
+ case 'space-around':
2384
+ if (leftover > 0) {
2385
+ const slot = leftover / numLines;
2386
+ cursor = slot / 2;
2387
+ extraGap = slot;
2388
+ }
2389
+ break;
2390
+ case 'stretch':
2391
+ case 'auto':
2392
+ if (leftover > 0)
2393
+ lineSizeBoost = leftover / numLines;
2394
+ break;
2395
+ default:
2396
+ // flex-start: lines stacked from the cross start, no extra.
2397
+ break;
2398
+ }
2399
+ for (let li = 0; li < numLines; li++) {
2400
+ if (lineSizeBoost > 0)
2401
+ lineCrossSizes[li] = lineCrossSizes[li] + lineSizeBoost;
2402
+ lineCrossStarts[li] = cursor;
2403
+ cursor += lineCrossSizes[li] + crossGap + extraGap;
2404
+ }
2405
+ }
2406
+ // `flex-wrap: wrap-reverse` mirrors the line stack on the cross
2407
+ // axis — each line is measured from the cross END. Mirrors the
2408
+ // imperative `reverseLineStack`. (A no-op for a single line, whose
2409
+ // cross size already fills `innerCross`.)
2410
+ if (reverse) {
2411
+ for (let li = 0; li < numLines; li++) {
2412
+ lineCrossStarts[li] = innerCross - lineCrossStarts[li] - lineCrossSizes[li];
2413
+ }
2414
+ }
2415
+ // Locate the target child.
2416
+ let myLine = 0;
2417
+ let myPositionInLine = 0;
2418
+ for (let li = 0; li < numLines; li++) {
2419
+ const first = lineFirst[li];
2420
+ const count = lineCount[li];
2421
+ if (childIndex >= first && childIndex < first + count) {
2422
+ myLine = li;
2423
+ myPositionInLine = childIndex - first;
2424
+ break;
2425
+ }
2426
+ }
2427
+ const myLineFirst = lineFirst[myLine];
2428
+ const myLineCount = lineCount[myLine];
2429
+ const myLineCrossSize = lineCrossSizes[myLine];
2430
+ const myLineCrossStart = lineCrossStarts[myLine];
2431
+ const me = siblings[childIndex];
2432
+ // justify-content per line: compute leftover from this line's
2433
+ // used main, then leading offset / extra gap.
2434
+ let usedMain = 0;
2435
+ for (let k = 0; k < myLineCount; k++) {
2436
+ const idx = myLineFirst + k;
2437
+ const s = siblings[idx];
2438
+ usedMain += finalMainSizes[idx] + s.mainMarginStart + s.mainMarginEnd;
2439
+ }
2440
+ if (myLineCount > 1)
2441
+ usedMain += (myLineCount - 1) * mainGap;
2442
+ const leftover = Math.max(0, innerMain - usedMain);
2443
+ let leadingOffset = 0;
2444
+ let extraGap = 0;
2445
+ switch (justify) {
2446
+ case 'flex-end':
2447
+ leadingOffset = leftover;
2448
+ break;
2449
+ case 'center':
2450
+ leadingOffset = leftover / 2;
2451
+ break;
2452
+ case 'space-between':
2453
+ if (myLineCount > 1)
2454
+ extraGap = leftover / (myLineCount - 1);
2455
+ break;
2456
+ case 'space-around': {
2457
+ const slot = leftover / myLineCount;
2458
+ leadingOffset = slot / 2;
2459
+ extraGap = slot;
2460
+ break;
2461
+ }
2462
+ case 'space-evenly': {
2463
+ const slot = leftover / (myLineCount + 1);
2464
+ leadingOffset = slot;
2465
+ extraGap = slot;
2466
+ break;
2467
+ }
2468
+ default:
2469
+ // flex-start
2470
+ break;
2471
+ }
2472
+ // Main-axis cursor walks this line up to the target child.
2473
+ let cursor = padMainStart + leadingOffset;
2474
+ for (let k = 0; k < myPositionInLine; k++) {
2475
+ const idx = myLineFirst + k;
2476
+ const s = siblings[idx];
2477
+ cursor += s.mainMarginStart + finalMainSizes[idx] + s.mainMarginEnd;
2478
+ cursor += mainGap + extraGap;
2479
+ }
2480
+ cursor += me.mainMarginStart;
2481
+ const mainPos = cursor;
2482
+ // Cross-axis position via align-items / align-self within line.
2483
+ let withinLineCross = me.crossMarginStart;
2484
+ if (me.align === 'flex-end') {
2485
+ withinLineCross = myLineCrossSize - me.crossSize - me.crossMarginEnd;
2486
+ }
2487
+ else if (me.align === 'center') {
2488
+ const innerLine = myLineCrossSize - me.crossMarginStart - me.crossMarginEnd;
2489
+ withinLineCross = me.crossMarginStart + Math.max(0, (innerLine - me.crossSize) / 2);
2490
+ }
2491
+ const crossPos = padCrossStart + myLineCrossStart + withinLineCross;
2492
+ // Cross size: `align-items: stretch` (the default) resizes an
2493
+ // `'auto'` cross to fill the line's inner cross (v14); otherwise
2494
+ // the already-clamped `me.crossSize` stands (clamped explicit, or
2495
+ // 0 for a non-stretched `'auto'`). Mirrors the imperative
2496
+ // `crossAlignItemsInLine` stretch branch.
2497
+ let crossSize = me.crossSize;
2498
+ if (me.crossIsContentAuto && me.align === 'stretch') {
2499
+ const lineInner = myLineCrossSize - me.crossMarginStart - me.crossMarginEnd;
2500
+ crossSize = clampMinMax(Math.max(0, lineInner), me.crossMin, me.crossMax);
2501
+ }
2502
+ return {
2503
+ mainSize: finalMainSizes[childIndex],
2504
+ mainPos,
2505
+ crossPos,
2506
+ crossSize,
2507
+ };
2508
+ }
2509
+ //# sourceMappingURL=flex-grammar.js.map