@glissade/scene 0.19.1 → 0.20.0-pre.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/layout.d.ts CHANGED
@@ -1,101 +1,9 @@
1
- import { r as DisplayListBuilder } from "./displayList.js";
2
- import { B as NodeProps, L as EvalContext, V as PropInit, W as TextMeasurer, i as Group, z as Node } from "./nodes.js";
3
1
  import { a as LayoutEngineMissingError, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
4
- import { BindableSignal } from "@glissade/core";
2
+ import { Column, Layout, LayoutProps, Row, RowProps, Stack, StackProps } from "./layoutCtors.js";
5
3
 
6
- //#region src/layout.d.ts
4
+ //#region src/layoutEngineYoga.d.ts
7
5
 
8
- interface LayoutProps extends NodeProps {
9
- children?: Node[];
10
- /** 'auto': the axis sizes itself from content (Yoga content sizing). */
11
- width?: PropInit<number> | 'auto';
12
- height?: PropInit<number> | 'auto';
13
- direction?: 'row' | 'column';
14
- gap?: PropInit<number>;
15
- padding?: PropInit<number>;
16
- justify?: LayoutContainerSpec['justify'];
17
- align?: LayoutContainerSpec['align'];
18
- }
19
- /**
20
- * Flexbox container (center-anchored like every node). Flowable children
21
- * (intrinsicSize ≠ null) are placed by the engine anchor-aware via
22
- * flowOffset(): shapes center into their box, Text aligns its baseline
23
- * origin to the box edge — labels in a column share a true left edge.
24
- * Non-flowable children (e.g. Groups) emit untouched at the layout origin,
25
- * BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
26
- * zIndex-sorted; flow order is array order.
27
- */
28
- declare class Layout extends Group {
29
- #private;
30
- /** CLI/host detection marker — avoids importing this entry just to instanceof. */
31
- static readonly isLayoutNode = true;
32
- readonly width: BindableSignal<number>;
33
- readonly height: BindableSignal<number>;
34
- readonly gap: BindableSignal<number>;
35
- readonly padding: BindableSignal<number>;
36
- readonly direction: 'row' | 'column';
37
- readonly justify: LayoutContainerSpec['justify'];
38
- readonly align: LayoutContainerSpec['align'];
39
- /** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
40
- readonly autoWidth: boolean;
41
- readonly autoHeight: boolean;
42
- constructor(props?: LayoutProps);
43
- intrinsicSize(measurer: TextMeasurer): {
44
- w: number;
45
- h: number;
46
- };
47
- /**
48
- * The resolved container size — content-driven on 'auto' axes. Pure pull:
49
- * reads the same signals the flow reads, so a sibling bound to it
50
- * (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
51
- * The measurer defaults to the scene-injected one (estimating pre-scene).
52
- */
53
- computedSize(measurer?: TextMeasurer): {
54
- w: number;
55
- h: number;
56
- };
57
- protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
58
- }
59
- /**
60
- * Stack-ergonomic props — a more DISCOVERABLE surface over {@link LayoutProps}.
61
- * Everything `Layout` accepts (id, position, opacity, width/height, gap,
62
- * padding, justify, children, …) passes through unchanged; the only difference
63
- * from constructing `Layout` directly is the defaulting `Stack()` applies.
64
- */
65
- interface StackProps extends LayoutProps {}
66
- /**
67
- * Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
68
- * class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
69
- * dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
70
- * hand-written `Layout({...})` produce identical child positions.
71
- *
72
- * Stack-ergonomic defaults (the ONLY divergence from `Layout`):
73
- * - `direction` defaults to `'column'` (the common vertical stack; Layout's own
74
- * default is `'row'`).
75
- * - `align` defaults to `'start'` — a true left edge for a label column (the
76
- * dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
77
- *
78
- * Every other prop passes straight through to `Layout`.
79
- */
80
- declare function Stack(props?: StackProps): Layout;
81
- /**
82
- * `Row`/`Column` props — `Stack`'s surface MINUS `direction` (the alias pins it).
83
- * Omitting `direction` from the type means an explicit one can't even be passed,
84
- * so `Row({...})` is unambiguously a row and `Column({...})` a column.
85
- */
86
- interface RowProps extends Omit<StackProps, 'direction'> {}
87
- /**
88
- * `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
89
- * Stack's `align:'start'` default and Layout's pure memoized resolve; the only
90
- * difference from `Stack` is the pinned `direction`.
91
- */
92
- declare function Row(props?: RowProps): Layout;
93
- /**
94
- * `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
95
- * direction `Stack` already defaults to, made explicit at the call site).
96
- */
97
- declare function Column(props?: RowProps): Layout;
98
6
  /** Load Yoga (wasm) and register it as the active LayoutEngine. Idempotent. */
99
7
  declare function loadYogaLayoutEngine(): Promise<LayoutEngine>;
100
8
  //#endregion
101
- export { Column, Layout, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, LayoutProps, type LayoutResult, Row, RowProps, Stack, StackProps, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
9
+ export { Column, Layout, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LayoutProps, type LayoutResult, Row, type RowProps, Stack, type StackProps, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
package/dist/layout.js CHANGED
@@ -1,201 +1,16 @@
1
- import { r as Group, y as fallbackMeasurer } from "./nodes.js";
2
- import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
3
- import { computed, signal } from "@glissade/core";
4
- //#region src/layout.ts
1
+ import { i as setLayoutEngine, n as getLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
2
+ import { Column, Layout, Row, Stack } from "./layoutCtors.js";
3
+ //#region src/layoutEngineYoga.ts
5
4
  /**
6
- * '@glissade/scene/layout' (DESIGN.md §3.2): the flexbox Layout node and the
7
- * Yoga-backed LayoutEngine. A SEPARATE entry point with its own budget the
8
- * base embed path never pays for the wasm. Determinism: the same Yoga build
9
- * computes layout in browser preview and headless export.
10
- *
11
- * Usage: `await loadYogaLayoutEngine()` once before mounting/rendering a
12
- * scene containing Layout nodes (the CLI does this automatically).
5
+ * The Yoga LayoutEngine LOADER (DESIGN.md §3.2). Split off the node ctors
6
+ * (`layoutCtors.ts`) so the ctors can ride the single-file IIFE without dragging
7
+ * Yoga's wasm: this module's `import('yoga-layout/load')` is what statically
8
+ * inlines the wasm-base64 binding under esbuild's IIFE format (no code-splitting
9
+ * keeps the dynamic import async). `@glissade/scene/layout` re-exports it, so
10
+ * existing importers are unaffected; the IIFE re-exports it too but externalizes
11
+ * `yoga-layout/load`, keeping the dynamic import a RUNTIME fetch (yoga stays out
12
+ * of the bundle).
13
13
  */
14
- /**
15
- * Flexbox container (center-anchored like every node). Flowable children
16
- * (intrinsicSize ≠ null) are placed by the engine anchor-aware via
17
- * flowOffset(): shapes center into their box, Text aligns its baseline
18
- * origin to the box edge — labels in a column share a true left edge.
19
- * Non-flowable children (e.g. Groups) emit untouched at the layout origin,
20
- * BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
21
- * zIndex-sorted; flow order is array order.
22
- */
23
- var Layout = class extends Group {
24
- /** CLI/host detection marker — avoids importing this entry just to instanceof. */
25
- static isLayoutNode = true;
26
- width;
27
- height;
28
- gap;
29
- padding;
30
- direction;
31
- justify;
32
- align;
33
- /** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
34
- autoWidth;
35
- autoHeight;
36
- /**
37
- * Sanctioned memoization (§2.1), core-`computed()`-backed: a pure function of
38
- * the PARTICIPATING signals. The compute reads exactly the container props
39
- * and child intrinsic-size signals it consumes, so the signal graph records
40
- * those as deps and re-invokes Yoga only when one of THEM changes — a sibling
41
- * mutating a non-participating signal does not invalidate the layout. Pulls
42
- * the scene-injected measurer (the same one draw() uses via ctx.measurer);
43
- * a caller-supplied non-default measurer bypasses this cache (see #compute).
44
- */
45
- #memo = computed(() => this.#computeUncached(this.measurerSource?.() ?? fallbackMeasurer()));
46
- constructor(props = {}) {
47
- super(props);
48
- this.autoWidth = props.width === "auto";
49
- this.autoHeight = props.height === "auto";
50
- this.width = initProp(signal(0), this.autoWidth ? void 0 : props.width);
51
- this.height = initProp(signal(0), this.autoHeight ? void 0 : props.height);
52
- this.gap = initProp(signal(0), props.gap);
53
- this.padding = initProp(signal(0), props.padding);
54
- this.direction = props.direction ?? "row";
55
- this.justify = props.justify ?? "start";
56
- this.align = props.align ?? "center";
57
- this.registerTarget("width", this.width, "number");
58
- this.registerTarget("height", this.height, "number");
59
- this.registerTarget("gap", this.gap, "number");
60
- this.registerTarget("padding", this.padding, "number");
61
- }
62
- intrinsicSize(measurer) {
63
- if (!this.autoWidth && !this.autoHeight) return {
64
- w: this.width(),
65
- h: this.height()
66
- };
67
- return this.#compute(measurer).size;
68
- }
69
- /**
70
- * The resolved container size — content-driven on 'auto' axes. Pure pull:
71
- * reads the same signals the flow reads, so a sibling bound to it
72
- * (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
73
- * The measurer defaults to the scene-injected one (estimating pre-scene).
74
- */
75
- computedSize(measurer) {
76
- const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
77
- return this.#compute(m).size;
78
- }
79
- /**
80
- * Route through the #memo (the dependency-tracked computed) when `measurer`
81
- * is the default the memo itself pulls; otherwise compute fresh & UNCACHED —
82
- * a caller-supplied non-default measurer must never read (or poison) a cache
83
- * keyed on the scene-singleton measurer (the `computedSize(customMeasurer)`
84
- * escape hatch).
85
- */
86
- #compute(measurer) {
87
- return measurer === (this.measurerSource?.() ?? fallbackMeasurer()) ? this.#memo() : this.#computeUncached(measurer);
88
- }
89
- #computeUncached(measurer) {
90
- const container = {
91
- width: this.autoWidth ? "auto" : this.width(),
92
- height: this.autoHeight ? "auto" : this.height(),
93
- direction: this.direction,
94
- gap: this.gap(),
95
- padding: this.padding(),
96
- justify: this.justify,
97
- align: this.align
98
- };
99
- this.trackStructure();
100
- const flowable = [];
101
- const absolute = [];
102
- this.children.forEach((child, index) => {
103
- const size = child.intrinsicSize(measurer);
104
- if (size) flowable.push({
105
- node: child,
106
- spec: {
107
- width: size.w,
108
- height: size.h
109
- },
110
- index
111
- });
112
- else absolute.push(child);
113
- });
114
- const result = requireLayoutEngine().compute(container, flowable.map((f) => f.spec));
115
- return {
116
- result,
117
- size: {
118
- w: this.autoWidth ? result.width : container.width,
119
- h: this.autoHeight ? result.height : container.height
120
- },
121
- flowable,
122
- absolute
123
- };
124
- }
125
- draw(out, ctx) {
126
- const { result, size, flowable, absolute } = this.#compute(ctx.measurer);
127
- const boxes = result.boxes;
128
- const order = [...flowable].sort((a, b) => a.node.zIndex() - b.node.zIndex() || a.index - b.index);
129
- const ox = -size.w / 2;
130
- const oy = -size.h / 2;
131
- for (const child of absolute) child.emit(out, ctx);
132
- for (const entry of order) {
133
- const box = boxes[flowable.indexOf(entry)];
134
- const off = entry.node.flowOffset(ctx.measurer);
135
- out.push({ op: "save" });
136
- out.push({
137
- op: "transform",
138
- m: [
139
- 1,
140
- 0,
141
- 0,
142
- 1,
143
- ox + box.x - off.x,
144
- oy + box.y - off.y
145
- ]
146
- });
147
- entry.node.emit(out, ctx);
148
- out.push({ op: "restore" });
149
- }
150
- }
151
- };
152
- /**
153
- * Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
154
- * class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
155
- * dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
156
- * hand-written `Layout({...})` produce identical child positions.
157
- *
158
- * Stack-ergonomic defaults (the ONLY divergence from `Layout`):
159
- * - `direction` defaults to `'column'` (the common vertical stack; Layout's own
160
- * default is `'row'`).
161
- * - `align` defaults to `'start'` — a true left edge for a label column (the
162
- * dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
163
- *
164
- * Every other prop passes straight through to `Layout`.
165
- */
166
- function Stack(props = {}) {
167
- return new Layout({
168
- ...props,
169
- direction: props.direction ?? "column",
170
- align: props.align ?? "start"
171
- });
172
- }
173
- /**
174
- * `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
175
- * Stack's `align:'start'` default and Layout's pure memoized resolve; the only
176
- * difference from `Stack` is the pinned `direction`.
177
- */
178
- function Row(props = {}) {
179
- return Stack({
180
- ...props,
181
- direction: "row"
182
- });
183
- }
184
- /**
185
- * `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
186
- * direction `Stack` already defaults to, made explicit at the call site).
187
- */
188
- function Column(props = {}) {
189
- return Stack({
190
- ...props,
191
- direction: "column"
192
- });
193
- }
194
- function initProp(sig, init) {
195
- if (typeof init === "function") sig.bindSource(init);
196
- else if (init !== void 0) sig.set(init);
197
- return sig;
198
- }
199
14
  /** Load Yoga (wasm) and register it as the active LayoutEngine. Idempotent. */
200
15
  async function loadYogaLayoutEngine() {
201
16
  const { loadYoga, FlexDirection, Justify, Align, Gutter, Edge, Direction } = await import("yoga-layout/load");
@@ -0,0 +1,99 @@
1
+ import { r as DisplayListBuilder } from "./displayList.js";
2
+ import { B as NodeProps, L as EvalContext, V as PropInit, W as TextMeasurer, i as Group, z as Node } from "./nodes.js";
3
+ import { r as LayoutContainerSpec } from "./layoutEngine.js";
4
+ import { BindableSignal } from "@glissade/core";
5
+
6
+ //#region src/layoutCtors.d.ts
7
+
8
+ interface LayoutProps extends NodeProps {
9
+ children?: Node[];
10
+ /** 'auto': the axis sizes itself from content (Yoga content sizing). */
11
+ width?: PropInit<number> | 'auto';
12
+ height?: PropInit<number> | 'auto';
13
+ direction?: 'row' | 'column';
14
+ gap?: PropInit<number>;
15
+ padding?: PropInit<number>;
16
+ justify?: LayoutContainerSpec['justify'];
17
+ align?: LayoutContainerSpec['align'];
18
+ }
19
+ /**
20
+ * Flexbox container (center-anchored like every node). Flowable children
21
+ * (intrinsicSize ≠ null) are placed by the engine anchor-aware via
22
+ * flowOffset(): shapes center into their box, Text aligns its baseline
23
+ * origin to the box edge — labels in a column share a true left edge.
24
+ * Non-flowable children (e.g. Groups) emit untouched at the layout origin,
25
+ * BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
26
+ * zIndex-sorted; flow order is array order.
27
+ */
28
+ declare class Layout extends Group {
29
+ #private;
30
+ /** CLI/host detection marker — avoids importing this entry just to instanceof. */
31
+ static readonly isLayoutNode = true;
32
+ readonly width: BindableSignal<number>;
33
+ readonly height: BindableSignal<number>;
34
+ readonly gap: BindableSignal<number>;
35
+ readonly padding: BindableSignal<number>;
36
+ readonly direction: 'row' | 'column';
37
+ readonly justify: LayoutContainerSpec['justify'];
38
+ readonly align: LayoutContainerSpec['align'];
39
+ /** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
40
+ readonly autoWidth: boolean;
41
+ readonly autoHeight: boolean;
42
+ constructor(props?: LayoutProps);
43
+ intrinsicSize(measurer: TextMeasurer): {
44
+ w: number;
45
+ h: number;
46
+ };
47
+ /**
48
+ * The resolved container size — content-driven on 'auto' axes. Pure pull:
49
+ * reads the same signals the flow reads, so a sibling bound to it
50
+ * (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
51
+ * The measurer defaults to the scene-injected one (estimating pre-scene).
52
+ */
53
+ computedSize(measurer?: TextMeasurer): {
54
+ w: number;
55
+ h: number;
56
+ };
57
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
58
+ }
59
+ /**
60
+ * Stack-ergonomic props — a more DISCOVERABLE surface over {@link LayoutProps}.
61
+ * Everything `Layout` accepts (id, position, opacity, width/height, gap,
62
+ * padding, justify, children, …) passes through unchanged; the only difference
63
+ * from constructing `Layout` directly is the defaulting `Stack()` applies.
64
+ */
65
+ interface StackProps extends LayoutProps {}
66
+ /**
67
+ * Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
68
+ * class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
69
+ * dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
70
+ * hand-written `Layout({...})` produce identical child positions.
71
+ *
72
+ * Stack-ergonomic defaults (the ONLY divergence from `Layout`):
73
+ * - `direction` defaults to `'column'` (the common vertical stack; Layout's own
74
+ * default is `'row'`).
75
+ * - `align` defaults to `'start'` — a true left edge for a label column (the
76
+ * dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
77
+ *
78
+ * Every other prop passes straight through to `Layout`.
79
+ */
80
+ declare function Stack(props?: StackProps): Layout;
81
+ /**
82
+ * `Row`/`Column` props — `Stack`'s surface MINUS `direction` (the alias pins it).
83
+ * Omitting `direction` from the type means an explicit one can't even be passed,
84
+ * so `Row({...})` is unambiguously a row and `Column({...})` a column.
85
+ */
86
+ interface RowProps extends Omit<StackProps, 'direction'> {}
87
+ /**
88
+ * `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
89
+ * Stack's `align:'start'` default and Layout's pure memoized resolve; the only
90
+ * difference from `Stack` is the pinned `direction`.
91
+ */
92
+ declare function Row(props?: RowProps): Layout;
93
+ /**
94
+ * `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
95
+ * direction `Stack` already defaults to, made explicit at the call site).
96
+ */
97
+ declare function Column(props?: RowProps): Layout;
98
+ //#endregion
99
+ export { Column, Layout, LayoutProps, Row, RowProps, Stack, StackProps };
@@ -0,0 +1,210 @@
1
+ import { r as Group, y as fallbackMeasurer } from "./nodes.js";
2
+ import { r as requireLayoutEngine } from "./layoutEngine.js";
3
+ import { computed, signal } from "@glissade/core";
4
+ //#region src/layoutCtors.ts
5
+ /**
6
+ * '@glissade/scene/layout-ctors' (DESIGN.md §3.2): the YOGA-FREE flexbox node
7
+ * constructors — `Layout`/`Stack`/`Row`/`Column`. These classes only touch the
8
+ * LayoutEngine SEAM (`requireLayoutEngine`/`setLayoutEngine`) at COMPUTE time;
9
+ * they never import Yoga at construction. Splitting them off the loader
10
+ * (`loadYogaLayoutEngine`, which statically reaches `yoga-layout/load`) lets the
11
+ * single-file `@glissade/browser` IIFE expose `Stack`/`Row`/`Column`/`Layout`
12
+ * WITHOUT inlining Yoga's wasm — esbuild's IIFE format can't keep the loader's
13
+ * `import('yoga-layout/load')` async, so it would balloon the bundle (~47→~99 kB
14
+ * gz). The ctors here carry no such import, so they ride the IIFE cleanly.
15
+ *
16
+ * CAVEAT (the no-build cost of the split): a ctor on the IIFE needs an engine
17
+ * registered before it computes. Call `await glissade.loadYogaLayoutEngine()`
18
+ * once before evaluating a scene with a Layout/Stack/Row/Column node, else the
19
+ * first compute throws a clear `LayoutEngineMissingError`.
20
+ *
21
+ * The `@glissade/scene/layout` entry re-exports everything here PLUS the loader,
22
+ * so existing `@glissade/scene/layout` importers are unaffected.
23
+ */
24
+ /**
25
+ * Flexbox container (center-anchored like every node). Flowable children
26
+ * (intrinsicSize ≠ null) are placed by the engine anchor-aware via
27
+ * flowOffset(): shapes center into their box, Text aligns its baseline
28
+ * origin to the box edge — labels in a column share a true left edge.
29
+ * Non-flowable children (e.g. Groups) emit untouched at the layout origin,
30
+ * BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
31
+ * zIndex-sorted; flow order is array order.
32
+ */
33
+ var Layout = class extends Group {
34
+ /** CLI/host detection marker — avoids importing this entry just to instanceof. */
35
+ static isLayoutNode = true;
36
+ width;
37
+ height;
38
+ gap;
39
+ padding;
40
+ direction;
41
+ justify;
42
+ align;
43
+ /** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
44
+ autoWidth;
45
+ autoHeight;
46
+ /**
47
+ * Sanctioned memoization (§2.1), core-`computed()`-backed: a pure function of
48
+ * the PARTICIPATING signals. The compute reads exactly the container props
49
+ * and child intrinsic-size signals it consumes, so the signal graph records
50
+ * those as deps and re-invokes Yoga only when one of THEM changes — a sibling
51
+ * mutating a non-participating signal does not invalidate the layout. Pulls
52
+ * the scene-injected measurer (the same one draw() uses via ctx.measurer);
53
+ * a caller-supplied non-default measurer bypasses this cache (see #compute).
54
+ */
55
+ #memo = computed(() => this.#computeUncached(this.measurerSource?.() ?? fallbackMeasurer()));
56
+ constructor(props = {}) {
57
+ super(props);
58
+ this.autoWidth = props.width === "auto";
59
+ this.autoHeight = props.height === "auto";
60
+ this.width = initProp(signal(0), this.autoWidth ? void 0 : props.width);
61
+ this.height = initProp(signal(0), this.autoHeight ? void 0 : props.height);
62
+ this.gap = initProp(signal(0), props.gap);
63
+ this.padding = initProp(signal(0), props.padding);
64
+ this.direction = props.direction ?? "row";
65
+ this.justify = props.justify ?? "start";
66
+ this.align = props.align ?? "center";
67
+ this.registerTarget("width", this.width, "number");
68
+ this.registerTarget("height", this.height, "number");
69
+ this.registerTarget("gap", this.gap, "number");
70
+ this.registerTarget("padding", this.padding, "number");
71
+ }
72
+ intrinsicSize(measurer) {
73
+ if (!this.autoWidth && !this.autoHeight) return {
74
+ w: this.width(),
75
+ h: this.height()
76
+ };
77
+ return this.#compute(measurer).size;
78
+ }
79
+ /**
80
+ * The resolved container size — content-driven on 'auto' axes. Pure pull:
81
+ * reads the same signals the flow reads, so a sibling bound to it
82
+ * (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
83
+ * The measurer defaults to the scene-injected one (estimating pre-scene).
84
+ */
85
+ computedSize(measurer) {
86
+ const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
87
+ return this.#compute(m).size;
88
+ }
89
+ /**
90
+ * Route through the #memo (the dependency-tracked computed) when `measurer`
91
+ * is the default the memo itself pulls; otherwise compute fresh & UNCACHED —
92
+ * a caller-supplied non-default measurer must never read (or poison) a cache
93
+ * keyed on the scene-singleton measurer (the `computedSize(customMeasurer)`
94
+ * escape hatch).
95
+ */
96
+ #compute(measurer) {
97
+ return measurer === (this.measurerSource?.() ?? fallbackMeasurer()) ? this.#memo() : this.#computeUncached(measurer);
98
+ }
99
+ #computeUncached(measurer) {
100
+ const container = {
101
+ width: this.autoWidth ? "auto" : this.width(),
102
+ height: this.autoHeight ? "auto" : this.height(),
103
+ direction: this.direction,
104
+ gap: this.gap(),
105
+ padding: this.padding(),
106
+ justify: this.justify,
107
+ align: this.align
108
+ };
109
+ this.trackStructure();
110
+ const flowable = [];
111
+ const absolute = [];
112
+ this.children.forEach((child, index) => {
113
+ const size = child.intrinsicSize(measurer);
114
+ if (size) flowable.push({
115
+ node: child,
116
+ spec: {
117
+ width: size.w,
118
+ height: size.h
119
+ },
120
+ index
121
+ });
122
+ else absolute.push(child);
123
+ });
124
+ const result = requireLayoutEngine().compute(container, flowable.map((f) => f.spec));
125
+ return {
126
+ result,
127
+ size: {
128
+ w: this.autoWidth ? result.width : container.width,
129
+ h: this.autoHeight ? result.height : container.height
130
+ },
131
+ flowable,
132
+ absolute
133
+ };
134
+ }
135
+ draw(out, ctx) {
136
+ const { result, size, flowable, absolute } = this.#compute(ctx.measurer);
137
+ const boxes = result.boxes;
138
+ const order = [...flowable].sort((a, b) => a.node.zIndex() - b.node.zIndex() || a.index - b.index);
139
+ const ox = -size.w / 2;
140
+ const oy = -size.h / 2;
141
+ for (const child of absolute) child.emit(out, ctx);
142
+ for (const entry of order) {
143
+ const box = boxes[flowable.indexOf(entry)];
144
+ const off = entry.node.flowOffset(ctx.measurer);
145
+ out.push({ op: "save" });
146
+ out.push({
147
+ op: "transform",
148
+ m: [
149
+ 1,
150
+ 0,
151
+ 0,
152
+ 1,
153
+ ox + box.x - off.x,
154
+ oy + box.y - off.y
155
+ ]
156
+ });
157
+ entry.node.emit(out, ctx);
158
+ out.push({ op: "restore" });
159
+ }
160
+ }
161
+ };
162
+ /**
163
+ * Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
164
+ * class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
165
+ * dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
166
+ * hand-written `Layout({...})` produce identical child positions.
167
+ *
168
+ * Stack-ergonomic defaults (the ONLY divergence from `Layout`):
169
+ * - `direction` defaults to `'column'` (the common vertical stack; Layout's own
170
+ * default is `'row'`).
171
+ * - `align` defaults to `'start'` — a true left edge for a label column (the
172
+ * dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
173
+ *
174
+ * Every other prop passes straight through to `Layout`.
175
+ */
176
+ function Stack(props = {}) {
177
+ return new Layout({
178
+ ...props,
179
+ direction: props.direction ?? "column",
180
+ align: props.align ?? "start"
181
+ });
182
+ }
183
+ /**
184
+ * `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
185
+ * Stack's `align:'start'` default and Layout's pure memoized resolve; the only
186
+ * difference from `Stack` is the pinned `direction`.
187
+ */
188
+ function Row(props = {}) {
189
+ return Stack({
190
+ ...props,
191
+ direction: "row"
192
+ });
193
+ }
194
+ /**
195
+ * `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
196
+ * direction `Stack` already defaults to, made explicit at the call site).
197
+ */
198
+ function Column(props = {}) {
199
+ return Stack({
200
+ ...props,
201
+ direction: "column"
202
+ });
203
+ }
204
+ function initProp(sig, init) {
205
+ if (typeof init === "function") sig.bindSource(init);
206
+ else if (init !== void 0) sig.set(init);
207
+ return sig;
208
+ }
209
+ //#endregion
210
+ export { Column, Layout, Row, Stack };