@flighthq/layout 0.3.0-next.1728.26853f6

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 (40) hide show
  1. package/dist/anchorLayout.d.ts +3 -0
  2. package/dist/anchorLayout.d.ts.map +1 -0
  3. package/dist/anchorLayout.js +98 -0
  4. package/dist/anchorLayout.js.map +1 -0
  5. package/dist/contract.d.ts +7 -0
  6. package/dist/contract.d.ts.map +1 -0
  7. package/dist/contract.js +7 -0
  8. package/dist/contract.js.map +1 -0
  9. package/dist/enableLayoutGuards.d.ts +3 -0
  10. package/dist/enableLayoutGuards.d.ts.map +1 -0
  11. package/dist/enableLayoutGuards.js +6 -0
  12. package/dist/enableLayoutGuards.js.map +1 -0
  13. package/dist/flexLayout.d.ts +3 -0
  14. package/dist/flexLayout.d.ts.map +1 -0
  15. package/dist/flexLayout.js +246 -0
  16. package/dist/flexLayout.js.map +1 -0
  17. package/dist/gridLayout.d.ts +3 -0
  18. package/dist/gridLayout.d.ts.map +1 -0
  19. package/dist/gridLayout.js +185 -0
  20. package/dist/gridLayout.js.map +1 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/layoutState.d.ts +4 -0
  26. package/dist/layoutState.d.ts.map +1 -0
  27. package/dist/layoutState.js +23 -0
  28. package/dist/layoutState.js.map +1 -0
  29. package/dist/resolveLayoutTree.d.ts +4 -0
  30. package/dist/resolveLayoutTree.d.ts.map +1 -0
  31. package/dist/resolveLayoutTree.js +98 -0
  32. package/dist/resolveLayoutTree.js.map +1 -0
  33. package/package.json +46 -0
  34. package/src/anchorLayout.test.ts +53 -0
  35. package/src/enableLayoutGuards.test.ts +16 -0
  36. package/src/flexLayout.test.ts +104 -0
  37. package/src/gridLayout.test.ts +123 -0
  38. package/src/layoutState.test.ts +26 -0
  39. package/src/layoutTreeShaking.test.ts +52 -0
  40. package/src/resolveLayoutTree.test.ts +84 -0
@@ -0,0 +1,2 @@
1
+ export { createLayoutState, enableLayoutGuards, explainLayoutResolution, registerAnchorLayoutResolver, registerFlexLayoutResolver, registerGridLayoutResolver, registerLayoutResolver, resolveLayoutTree, } from './contract';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,4BAA4B,EAC5B,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createLayoutState, enableLayoutGuards, explainLayoutResolution, registerAnchorLayoutResolver, registerFlexLayoutResolver, registerGridLayoutResolver, registerLayoutResolver, resolveLayoutTree, } from './contract';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,4BAA4B,EAC5B,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { LayoutResolver, LayoutState } from '@flighthq/types/contract';
2
+ export declare function createLayoutState(): LayoutState;
3
+ export declare function registerLayoutResolver(state: Readonly<LayoutState>, kind: string, resolver: LayoutResolver | null): void;
4
+ //# sourceMappingURL=layoutState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layoutState.d.ts","sourceRoot":"","sources":["../src/layoutState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAI5E,wBAAgB,iBAAiB,IAAI,WAAW,CAW/C;AAID,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC5B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,cAAc,GAAG,IAAI,GAC9B,IAAI,CAGN"}
@@ -0,0 +1,23 @@
1
+ // Allocates the operation state and its open, last-write-wins resolver registry. Built-in resolvers are
2
+ // opt-in registrations, so creating the core state never links anchor, flex, or grid code.
3
+ export function createLayoutState() {
4
+ return {
5
+ guard: null,
6
+ lastFailureActualLength: 0,
7
+ lastFailureKind: null,
8
+ lastFailureNodeIndex: -1,
9
+ lastFailureParentIndex: -1,
10
+ lastFailureRequiredLength: 0,
11
+ lastFailureResolverKind: null,
12
+ resolvers: new Map(),
13
+ };
14
+ }
15
+ // Registers or replaces one container-kind resolver. Passing null removes it. Bare built-in names are
16
+ // reserved by the SDK; custom kinds use a vendor prefix such as `acme.Flow`.
17
+ export function registerLayoutResolver(state, kind, resolver) {
18
+ if (resolver === null)
19
+ state.resolvers.delete(kind);
20
+ else
21
+ state.resolvers.set(kind, resolver);
22
+ }
23
+ //# sourceMappingURL=layoutState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layoutState.js","sourceRoot":"","sources":["../src/layoutState.ts"],"names":[],"mappings":"AAEA,wGAAwG;AACxG,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,KAAK,EAAE,IAAI;QACX,uBAAuB,EAAE,CAAC;QAC1B,eAAe,EAAE,IAAI;QACrB,oBAAoB,EAAE,CAAC,CAAC;QACxB,sBAAsB,EAAE,CAAC,CAAC;QAC1B,yBAAyB,EAAE,CAAC;QAC5B,uBAAuB,EAAE,IAAI;QAC7B,SAAS,EAAE,IAAI,GAAG,EAAE;KACrB,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,6EAA6E;AAC7E,MAAM,UAAU,sBAAsB,CACpC,KAA4B,EAC5B,IAAY,EACZ,QAA+B;IAE/B,IAAI,QAAQ,KAAK,IAAI;QAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;;QAC/C,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { LayoutResolutionExplanation, LayoutState, LayoutTree } from '@flighthq/types/contract';
2
+ export declare function explainLayoutResolution(state: Readonly<LayoutState>, tree: Readonly<LayoutTree>, nodeIndex: number): LayoutResolutionExplanation | null;
3
+ export declare function resolveLayoutTree(out: Float32Array, state: LayoutState, tree: Readonly<LayoutTree>, intrinsicSizes: ArrayLike<number>, availableWidth: number, availableHeight: number): boolean;
4
+ //# sourceMappingURL=resolveLayoutTree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveLayoutTree.d.ts","sourceRoot":"","sources":["../src/resolveLayoutTree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAE3B,WAAW,EACX,UAAU,EACX,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC5B,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC1B,SAAS,EAAE,MAAM,GAChB,2BAA2B,GAAG,IAAI,CA2BpC;AAKD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,YAAY,EACjB,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC1B,cAAc,EAAE,SAAS,CAAC,MAAM,CAAC,EACjC,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAkDT"}
@@ -0,0 +1,98 @@
1
+ import { LayoutResolutionFailureKind as Failure } from '@flighthq/types/contract';
2
+ // Returns a detached explanation for the latest matching sentinel, or diagnoses hierarchy/registration at
3
+ // `nodeIndex` without mutating state. Expectedly valid nodes return null.
4
+ export function explainLayoutResolution(state, tree, nodeIndex) {
5
+ if (state.lastFailureKind !== null &&
6
+ (nodeIndex < 0 || state.lastFailureNodeIndex < 0 || nodeIndex === state.lastFailureNodeIndex)) {
7
+ return {
8
+ actualLength: state.lastFailureActualLength,
9
+ kind: state.lastFailureKind,
10
+ nodeIndex: state.lastFailureNodeIndex,
11
+ parentIndex: state.lastFailureParentIndex,
12
+ requiredLength: state.lastFailureRequiredLength,
13
+ resolverKind: state.lastFailureResolverKind,
14
+ };
15
+ }
16
+ const nodes = tree.nodes;
17
+ if (nodeIndex < 0 || nodeIndex >= nodes.length)
18
+ return null;
19
+ const node = nodes[nodeIndex];
20
+ if (node.parentIndex < -1 || node.parentIndex >= nodeIndex) {
21
+ return explanation(Failure.InvalidHierarchy, nodeIndex, node.parentIndex, node.kind);
22
+ }
23
+ for (let i = nodeIndex + 1; i < nodes.length; i++) {
24
+ if (nodes[i].parentIndex === nodeIndex && !state.resolvers.has(node.kind)) {
25
+ return explanation(Failure.UnregisteredKind, nodeIndex, -1, node.kind);
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ // Resolves a flat, parent-before-child tree into absolute rectangles. `intrinsicSizes` is two floats per
31
+ // node (natural width, height); `out` is four floats per node (x, y, width, height). Both buffers belong to
32
+ // the caller. The successful path performs no allocation, and an anchor-only tree is one forward pass.
33
+ export function resolveLayoutTree(out, state, tree, intrinsicSizes, availableWidth, availableHeight) {
34
+ clearLayoutFailure(state);
35
+ const nodes = tree.nodes;
36
+ const requiredOutLength = nodes.length * 4;
37
+ if (out.length < requiredOutLength) {
38
+ return failLayoutResolution(state, tree, Failure.OutputTooSmall, -1, -1, requiredOutLength, out.length, null);
39
+ }
40
+ const requiredIntrinsicLength = nodes.length * 2;
41
+ if (intrinsicSizes.length < requiredIntrinsicLength) {
42
+ return failLayoutResolution(state, tree, Failure.IntrinsicSizesTooSmall, -1, -1, requiredIntrinsicLength, intrinsicSizes.length, null);
43
+ }
44
+ const rootWidth = finiteSize(availableWidth);
45
+ const rootHeight = finiteSize(availableHeight);
46
+ for (let i = 0; i < nodes.length; i++) {
47
+ const node = nodes[i];
48
+ const parentIndex = node.parentIndex;
49
+ if (parentIndex < -1 || parentIndex >= i) {
50
+ return failLayoutResolution(state, tree, Failure.InvalidHierarchy, i, parentIndex, 0, 0, node.kind);
51
+ }
52
+ const offset = i * 4;
53
+ if (parentIndex === -1) {
54
+ out[offset] = 0;
55
+ out[offset + 1] = 0;
56
+ out[offset + 2] = rootWidth;
57
+ out[offset + 3] = rootHeight;
58
+ continue;
59
+ }
60
+ const parentKind = nodes[parentIndex].kind;
61
+ const resolver = state.resolvers.get(parentKind);
62
+ if (resolver === undefined) {
63
+ return failLayoutResolution(state, tree, Failure.UnregisteredKind, parentIndex, -1, 0, 0, parentKind);
64
+ }
65
+ const failure = resolver(out, tree, intrinsicSizes, parentIndex, i);
66
+ if (failure !== null) {
67
+ const failureNodeIndex = failure === Failure.InvalidContainerStyle ? parentIndex : i;
68
+ return failLayoutResolution(state, tree, failure, failureNodeIndex, parentIndex, 0, 0, parentKind);
69
+ }
70
+ }
71
+ return true;
72
+ }
73
+ function explanation(kind, nodeIndex, parentIndex, resolverKind) {
74
+ return { actualLength: 0, kind, nodeIndex, parentIndex, requiredLength: 0, resolverKind };
75
+ }
76
+ function clearLayoutFailure(state) {
77
+ state.lastFailureActualLength = 0;
78
+ state.lastFailureKind = null;
79
+ state.lastFailureNodeIndex = -1;
80
+ state.lastFailureParentIndex = -1;
81
+ state.lastFailureRequiredLength = 0;
82
+ state.lastFailureResolverKind = null;
83
+ }
84
+ function failLayoutResolution(state, tree, kind, nodeIndex, parentIndex, requiredLength, actualLength, resolverKind) {
85
+ state.lastFailureActualLength = actualLength;
86
+ state.lastFailureKind = kind;
87
+ state.lastFailureNodeIndex = nodeIndex;
88
+ state.lastFailureParentIndex = parentIndex;
89
+ state.lastFailureRequiredLength = requiredLength;
90
+ state.lastFailureResolverKind = resolverKind;
91
+ if (state.guard !== null)
92
+ state.guard(explainLayoutResolution(state, tree, nodeIndex));
93
+ return false;
94
+ }
95
+ function finiteSize(value) {
96
+ return Number.isFinite(value) && value > 0 ? value : 0;
97
+ }
98
+ //# sourceMappingURL=resolveLayoutTree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveLayoutTree.js","sourceRoot":"","sources":["../src/resolveLayoutTree.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@flighthq/layout",
3
+ "version": "0.3.0-next.1728.26853f6",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/layout"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./contract": {
18
+ "types": "./dist/contract.d.ts",
19
+ "default": "./dist/contract.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src/**/*.test.ts",
25
+ "!dist/**/*.test.js",
26
+ "!dist/**/*.test.d.ts",
27
+ "!dist/**/*.test.js.map",
28
+ "!dist/**/*.test.d.ts.map"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc -b",
32
+ "clean": "tsc -b --clean",
33
+ "test": "vitest run --config vitest.config.ts",
34
+ "test:watch": "vitest --watch --config vitest.config.ts",
35
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
36
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
+ },
38
+ "dependencies": {
39
+ "@flighthq/types": "0.3.0-next.1728.26853f6"
40
+ },
41
+ "devDependencies": {
42
+ "typescript": "^5.3.0"
43
+ },
44
+ "description": "Renderer-neutral anchor, flex, and grid rectangle layout over flat trees",
45
+ "sideEffects": false
46
+ }
@@ -0,0 +1,53 @@
1
+ import type { AnchorLayoutItemStyle, LayoutNode, LayoutTree } from '@flighthq/types/contract';
2
+ import { AnchorLayoutKind } from '@flighthq/types/contract';
3
+
4
+ import { registerAnchorLayoutResolver } from './anchorLayout';
5
+ import { createLayoutState } from './layoutState';
6
+ import { explainLayoutResolution, resolveLayoutTree } from './resolveLayoutTree';
7
+
8
+ function node(parentIndex: number, itemStyle: AnchorLayoutItemStyle | null = null): LayoutNode {
9
+ return { containerStyle: null, itemStyle, kind: AnchorLayoutKind, parentIndex };
10
+ }
11
+
12
+ function resolve(nodes: LayoutNode[], intrinsicSizes: number[], width = 200, height = 100): Float32Array {
13
+ const out = new Float32Array(nodes.length * 4);
14
+ const state = createLayoutState();
15
+ registerAnchorLayoutResolver(state);
16
+ expect(resolveLayoutTree(out, state, { nodes }, intrinsicSizes, width, height)).toBe(true);
17
+ return out;
18
+ }
19
+
20
+ describe('registerAnchorLayoutResolver', () => {
21
+ it('aligns a natural-size child with the existing viewport vocabulary', () => {
22
+ const out = resolve([node(-1), node(0, { align: 'bottomright' })], [0, 0, 40, 20]);
23
+ expect([...out.slice(4)]).toEqual([160, 80, 40, 20]);
24
+ });
25
+
26
+ it('stretches between opposing pins as the available rect changes', () => {
27
+ const nodes = [node(-1), node(0, { bottom: 10, left: 20, right: 30, top: 5 })];
28
+ expect([...resolve(nodes, [0, 0, 1, 1], 200, 100).slice(4)]).toEqual([20, 5, 150, 85]);
29
+ expect([...resolve(nodes, [0, 0, 1, 1], 300, 160).slice(4)]).toEqual([20, 5, 250, 145]);
30
+ });
31
+
32
+ it('lets an edge pin override alignment on only that axis', () => {
33
+ const out = resolve([node(-1), node(0, { align: 'bottomright', left: 12 })], [0, 0, 30, 20]);
34
+ expect([...out.slice(4)]).toEqual([12, 80, 30, 20]);
35
+ });
36
+
37
+ it('propagates nested rectangles in one parent-before-child pass', () => {
38
+ const out = resolve(
39
+ [node(-1), node(0, { bottom: 10, left: 10, right: 10, top: 10 }), node(1, { align: 'bottomright' })],
40
+ [0, 0, 0, 0, 20, 15],
41
+ );
42
+ expect([...out.slice(4, 8)]).toEqual([10, 10, 180, 80]);
43
+ expect([...out.slice(8)]).toEqual([170, 75, 20, 15]);
44
+ });
45
+
46
+ it('rejects a style shape its parent resolver cannot interpret', () => {
47
+ const input: LayoutTree = { nodes: [node(-1), { ...node(0), itemStyle: { align: 'middle' } }] };
48
+ const state = createLayoutState();
49
+ registerAnchorLayoutResolver(state);
50
+ expect(resolveLayoutTree(new Float32Array(8), state, input, new Float32Array(4), 100, 100)).toBe(false);
51
+ expect(explainLayoutResolution(state, input, 1)?.kind).toBe('InvalidItemStyle');
52
+ });
53
+ });
@@ -0,0 +1,16 @@
1
+ import type { LayoutNode } from '@flighthq/types/contract';
2
+
3
+ import { enableLayoutGuards } from './enableLayoutGuards';
4
+ import { createLayoutState } from './layoutState';
5
+ import { resolveLayoutTree } from './resolveLayoutTree';
6
+
7
+ describe('enableLayoutGuards', () => {
8
+ it('warns with the structured explanation for a silent sentinel', () => {
9
+ const warn = vi.fn();
10
+ const state = createLayoutState();
11
+ enableLayoutGuards(state, warn);
12
+ const root: LayoutNode = { containerStyle: null, itemStyle: null, kind: 'acme.Root', parentIndex: -1 };
13
+ resolveLayoutTree(new Float32Array(3), state, { nodes: [root] }, new Float32Array(2), 100, 100);
14
+ expect(warn).toHaveBeenCalledWith(expect.objectContaining({ kind: 'OutputTooSmall' }));
15
+ });
16
+ });
@@ -0,0 +1,104 @@
1
+ import type { FlexLayoutContainerStyle, FlexLayoutItemStyle, LayoutNode } from '@flighthq/types/contract';
2
+ import { FlexLayoutKind } from '@flighthq/types/contract';
3
+
4
+ import { registerFlexLayoutResolver } from './flexLayout';
5
+ import { createLayoutState } from './layoutState';
6
+ import { explainLayoutResolution, resolveLayoutTree } from './resolveLayoutTree';
7
+
8
+ function root(containerStyle: FlexLayoutContainerStyle | null = null): LayoutNode {
9
+ return { containerStyle, itemStyle: null, kind: FlexLayoutKind, parentIndex: -1 };
10
+ }
11
+
12
+ function child(itemStyle: FlexLayoutItemStyle | null = null): LayoutNode {
13
+ return { containerStyle: null, itemStyle, kind: FlexLayoutKind, parentIndex: 0 };
14
+ }
15
+
16
+ function resolve(nodes: LayoutNode[], intrinsicSizes: number[], width = 200, height = 100): Float32Array {
17
+ const out = new Float32Array(nodes.length * 4);
18
+ const state = createLayoutState();
19
+ registerFlexLayoutResolver(state);
20
+ expect(resolveLayoutTree(out, state, { nodes }, intrinsicSizes, width, height)).toBe(true);
21
+ return out;
22
+ }
23
+
24
+ describe('registerFlexLayoutResolver', () => {
25
+ it('uses intrinsic sizes for basis-auto and distributes growth', () => {
26
+ const out = resolve([root({ align: 'start' }), child({ grow: 1 }), child({ grow: 2 })], [0, 0, 20, 10, 20, 15]);
27
+ expect([...out.slice(4, 8)]).toEqual([0, 0, 73.33333587646484, 10]);
28
+ expect(out[8]).toBeCloseTo(73.3333);
29
+ expect(out[10]).toBeCloseTo(126.6667);
30
+ });
31
+
32
+ it('shrinks overflowing items in proportion to basis and shrink', () => {
33
+ const out = resolve(
34
+ [root({ align: 'start' }), child({ basis: 80, shrink: 1 }), child({ basis: 80, shrink: 1 })],
35
+ [0, 0, 0, 10, 0, 10],
36
+ 100,
37
+ );
38
+ expect(out[6]).toBe(50);
39
+ expect(out[8]).toBe(50);
40
+ expect(out[10]).toBe(50);
41
+ });
42
+
43
+ it('applies padding, gap, justify, and cross-axis alignment', () => {
44
+ const out = resolve(
45
+ [
46
+ root({
47
+ align: 'center',
48
+ gap: 10,
49
+ justify: 'space-between',
50
+ paddingBottom: 10,
51
+ paddingLeft: 10,
52
+ paddingRight: 10,
53
+ paddingTop: 10,
54
+ }),
55
+ child(),
56
+ child({ alignSelf: 'end' }),
57
+ ],
58
+ [0, 0, 20, 10, 30, 20],
59
+ 120,
60
+ 80,
61
+ );
62
+ expect([...out.slice(4, 8)]).toEqual([10, 35, 20, 10]);
63
+ expect([...out.slice(8)]).toEqual([80, 50, 30, 20]);
64
+ });
65
+
66
+ it('supports column-reverse without changing item role semantics', () => {
67
+ const out = resolve(
68
+ [root({ align: 'start', direction: 'column-reverse', gap: 5 }), child(), child()],
69
+ [0, 0, 20, 10, 30, 15],
70
+ 100,
71
+ 60,
72
+ );
73
+ expect([...out.slice(4, 8)]).toEqual([0, 50, 20, 10]);
74
+ expect([...out.slice(8)]).toEqual([0, 30, 30, 15]);
75
+ });
76
+
77
+ it('wraps lines and supports wrap-reverse', () => {
78
+ const normal = resolve(
79
+ [root({ align: 'start', gap: 5, wrap: 'wrap' }), child({ basis: 60 }), child({ basis: 60 })],
80
+ [0, 0, 0, 10, 0, 20],
81
+ 100,
82
+ 80,
83
+ );
84
+ expect([...normal.slice(4, 8)]).toEqual([0, 0, 60, 10]);
85
+ expect([...normal.slice(8)]).toEqual([0, 15, 60, 20]);
86
+
87
+ const reversed = resolve(
88
+ [root({ align: 'start', gap: 5, wrap: 'wrap-reverse' }), child({ basis: 60 }), child({ basis: 60 })],
89
+ [0, 0, 0, 10, 0, 20],
90
+ 100,
91
+ 80,
92
+ );
93
+ expect(reversed[5]).toBe(45);
94
+ expect(reversed[9]).toBe(60);
95
+ });
96
+
97
+ it('returns a diagnosable sentinel for mismatched styles', () => {
98
+ const nodes = [root(), { ...child(), itemStyle: { grow: 'yes' } }];
99
+ const state = createLayoutState();
100
+ registerFlexLayoutResolver(state);
101
+ expect(resolveLayoutTree(new Float32Array(8), state, { nodes }, new Float32Array(4), 100, 100)).toBe(false);
102
+ expect(explainLayoutResolution(state, { nodes }, 1)?.kind).toBe('InvalidItemStyle');
103
+ });
104
+ });
@@ -0,0 +1,123 @@
1
+ import type { GridLayoutContainerStyle, GridLayoutItemStyle, LayoutNode } from '@flighthq/types/contract';
2
+ import { GridLayoutKind } from '@flighthq/types/contract';
3
+
4
+ import { registerGridLayoutResolver } from './gridLayout';
5
+ import { createLayoutState } from './layoutState';
6
+ import { explainLayoutResolution, resolveLayoutTree } from './resolveLayoutTree';
7
+
8
+ function root(containerStyle: GridLayoutContainerStyle): LayoutNode {
9
+ return { containerStyle, itemStyle: null, kind: GridLayoutKind, parentIndex: -1 };
10
+ }
11
+
12
+ function child(itemStyle: GridLayoutItemStyle | null = null): LayoutNode {
13
+ return { containerStyle: null, itemStyle, kind: GridLayoutKind, parentIndex: 0 };
14
+ }
15
+
16
+ function resolve(nodes: LayoutNode[], intrinsicSizes: number[], width = 200, height = 100): Float32Array {
17
+ const out = new Float32Array(nodes.length * 4);
18
+ const state = createLayoutState();
19
+ registerGridLayoutResolver(state);
20
+ expect(resolveLayoutTree(out, state, { nodes }, intrinsicSizes, width, height)).toBe(true);
21
+ return out;
22
+ }
23
+
24
+ describe('registerGridLayoutResolver', () => {
25
+ it('sizes fixed, fractional, and intrinsic tracks', () => {
26
+ const out = resolve(
27
+ [
28
+ root({
29
+ columnGap: 10,
30
+ columns: [{ kind: 'fixed', size: 40 }, { kind: 'auto' }, { fraction: 1, kind: 'fraction' }],
31
+ rows: [{ kind: 'auto' }],
32
+ }),
33
+ child({ column: 0, row: 0 }),
34
+ child({ column: 1, row: 0 }),
35
+ child({ column: 2, row: 0 }),
36
+ ],
37
+ [0, 0, 20, 10, 30, 25, 15, 20],
38
+ );
39
+ expect([...out.slice(4, 8)]).toEqual([0, 0, 40, 25]);
40
+ expect([...out.slice(8, 12)]).toEqual([50, 0, 30, 25]);
41
+ expect([...out.slice(12)]).toEqual([90, 0, 110, 25]);
42
+ });
43
+
44
+ it('assigns omitted placement row-major', () => {
45
+ const out = resolve(
46
+ [
47
+ root({
48
+ columnGap: 5,
49
+ columns: [
50
+ { kind: 'fixed', size: 20 },
51
+ { kind: 'fixed', size: 30 },
52
+ ],
53
+ rowGap: 4,
54
+ rows: [
55
+ { kind: 'fixed', size: 10 },
56
+ { kind: 'fixed', size: 15 },
57
+ ],
58
+ }),
59
+ child(),
60
+ child(),
61
+ child(),
62
+ ],
63
+ new Array(8).fill(0),
64
+ );
65
+ expect([...out.slice(4, 8)]).toEqual([0, 0, 20, 10]);
66
+ expect([...out.slice(8, 12)]).toEqual([25, 0, 30, 10]);
67
+ expect([...out.slice(12)]).toEqual([0, 14, 20, 15]);
68
+ });
69
+
70
+ it('supports spans, padding, and independent gaps', () => {
71
+ const out = resolve(
72
+ [
73
+ root({
74
+ columnGap: 5,
75
+ columns: [
76
+ { kind: 'fixed', size: 20 },
77
+ { kind: 'fixed', size: 30 },
78
+ ],
79
+ paddingLeft: 7,
80
+ paddingTop: 11,
81
+ rowGap: 3,
82
+ rows: [
83
+ { kind: 'fixed', size: 10 },
84
+ { kind: 'fixed', size: 15 },
85
+ ],
86
+ }),
87
+ child({ column: 0, columnSpan: 2, row: 0, rowSpan: 2 }),
88
+ ],
89
+ [0, 0, 0, 0],
90
+ );
91
+ expect([...out.slice(4)]).toEqual([7, 11, 55, 28]);
92
+ });
93
+
94
+ it('shares intrinsic size across auto tracks in a span', () => {
95
+ const out = resolve(
96
+ [
97
+ root({ columns: [{ kind: 'auto' }, { kind: 'auto' }], rows: [{ kind: 'auto' }] }),
98
+ child({ column: 0, columnSpan: 2, row: 0 }),
99
+ ],
100
+ [0, 0, 80, 20],
101
+ );
102
+ expect([...out.slice(4)]).toEqual([0, 0, 80, 20]);
103
+ });
104
+
105
+ it('returns diagnosable sentinels for mismatched container and item styles', () => {
106
+ const invalidContainerNodes = [
107
+ { ...root({ columns: [{ kind: 'auto' }], rows: [{ kind: 'auto' }] }), containerStyle: { columns: [] } },
108
+ child(),
109
+ ];
110
+ const state = createLayoutState();
111
+ registerGridLayoutResolver(state);
112
+ expect(
113
+ resolveLayoutTree(new Float32Array(8), state, { nodes: invalidContainerNodes }, new Float32Array(4), 10, 10),
114
+ ).toBe(false);
115
+ expect(explainLayoutResolution(state, { nodes: invalidContainerNodes }, 0)?.kind).toBe('InvalidContainerStyle');
116
+
117
+ const invalidItemNodes = [root({ columns: [{ kind: 'auto' }], rows: [{ kind: 'auto' }] }), child({ column: 0 })];
118
+ expect(
119
+ resolveLayoutTree(new Float32Array(8), state, { nodes: invalidItemNodes }, new Float32Array(4), 10, 10),
120
+ ).toBe(false);
121
+ expect(explainLayoutResolution(state, { nodes: invalidItemNodes }, 1)?.kind).toBe('InvalidItemStyle');
122
+ });
123
+ });
@@ -0,0 +1,26 @@
1
+ import type { LayoutResolver } from '@flighthq/types/contract';
2
+
3
+ import { createLayoutState, registerLayoutResolver } from './layoutState';
4
+
5
+ const resolver: LayoutResolver = () => null;
6
+
7
+ describe('createLayoutState', () => {
8
+ it('starts with an empty resolver registry and no retained failure', () => {
9
+ const state = createLayoutState();
10
+ expect(state.resolvers.size).toBe(0);
11
+ expect(state.lastFailureKind).toBeNull();
12
+ expect(state.guard).toBeNull();
13
+ });
14
+ });
15
+
16
+ describe('registerLayoutResolver', () => {
17
+ it('is open, last-write-wins, and accepts null to unregister', () => {
18
+ const state = createLayoutState();
19
+ const replacement: LayoutResolver = () => 'InvalidItemStyle';
20
+ registerLayoutResolver(state, 'acme.Flow', resolver);
21
+ registerLayoutResolver(state, 'acme.Flow', replacement);
22
+ expect(state.resolvers.get('acme.Flow')).toBe(replacement);
23
+ registerLayoutResolver(state, 'acme.Flow', null);
24
+ expect(state.resolvers.has('acme.Flow')).toBe(false);
25
+ });
26
+ });
@@ -0,0 +1,52 @@
1
+ // @vitest-environment node
2
+
3
+ import { build } from 'esbuild';
4
+
5
+ const resolveDir = getFileUrlDirectory(import.meta.url);
6
+
7
+ async function bundleLayoutExports(names: readonly string[]): Promise<string> {
8
+ const result = await build({
9
+ bundle: true,
10
+ format: 'esm',
11
+ logLevel: 'silent',
12
+ minify: true,
13
+ packages: 'external',
14
+ stdin: {
15
+ contents: `export { ${names.join(', ')} } from './contract.ts';`,
16
+ resolveDir,
17
+ sourcefile: `tree-shake-${names.join('-')}.ts`,
18
+ },
19
+ treeShaking: true,
20
+ write: false,
21
+ });
22
+ return result.outputFiles[0].text;
23
+ }
24
+
25
+ describe('layout resolver tree shaking', () => {
26
+ it('keeps anchor-only assembly independent of flex and grid', async () => {
27
+ const output = await bundleLayoutExports([
28
+ 'createLayoutState',
29
+ 'registerAnchorLayoutResolver',
30
+ 'resolveLayoutTree',
31
+ ]);
32
+ expect(output).toContain('AnchorLayout');
33
+ expect(output).not.toContain('space-evenly');
34
+ expect(output).not.toContain('columnGap');
35
+ });
36
+
37
+ it('includes each optional resolver only through its named registrar', async () => {
38
+ const flex = await bundleLayoutExports(['registerFlexLayoutResolver']);
39
+ const grid = await bundleLayoutExports(['registerGridLayoutResolver']);
40
+ expect(flex).toContain('space-evenly');
41
+ expect(flex).not.toContain('columnGap');
42
+ expect(grid).toContain('columnGap');
43
+ expect(grid).not.toContain('space-evenly');
44
+ });
45
+ });
46
+
47
+ function getFileUrlDirectory(url: string): string {
48
+ const directory = new URL('.', url);
49
+ const pathname = decodeURIComponent(directory.pathname);
50
+ if (directory.hostname !== '') return `//${directory.hostname}${pathname}`;
51
+ return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
52
+ }