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