layout-sans 0.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.
- package/README.md +357 -0
- package/dist/absolute.d.ts +8 -0
- package/dist/engine.d.ts +29 -0
- package/dist/flex.d.ts +14 -0
- package/dist/grid.d.ts +8 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/magazine.d.ts +8 -0
- package/dist/measure.d.ts +20 -0
- package/dist/types.d.ts +138 -0
- package/dist/utils.d.ts +31 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# LayoutSans
|
|
2
|
+
|
|
3
|
+
**CSS Flex/Grid layout without the browser. No DOM. No WASM bloat.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/layout-sans)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://bundlephobia.com/package/layout-sans)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
A pure TypeScript 2D layout engine. Give it a tree of boxes with flex/grid rules; get back exact pixel positions for every box. Works in Node, Bun, Deno, Cloudflare Workers, browser — anything that runs JS.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
- **The browser is a constraint, not a requirement.** `getBoundingClientRect` forces synchronous reflows. For server-rendered layouts, virtual lists, canvas renderers, and PDF engines, the DOM is overhead you don't need.
|
|
20
|
+
- **Yoga is great, but it ships WASM.** That's 300+ kB before your first layout call, requires async initialization, and doesn't run everywhere.
|
|
21
|
+
- **LayoutSans is the missing layer after [Pretext](https://github.com/chenglou/pretext).** Pretext tells you *how big* text is. LayoutSans tells you *where everything goes*. Together they replace browser layout with pure math.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 5-line demo
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createLayout } from 'layout-sans'
|
|
29
|
+
|
|
30
|
+
const boxes = createLayout({
|
|
31
|
+
type: 'flex', direction: 'row', width: 800, height: 600, gap: 16,
|
|
32
|
+
children: [{ type: 'box', flex: 1 }, { type: 'box', width: 240 }],
|
|
33
|
+
}).compute()
|
|
34
|
+
|
|
35
|
+
// boxes →
|
|
36
|
+
// [
|
|
37
|
+
// { nodeId: '0', x: 0, y: 0, width: 800, height: 600 },
|
|
38
|
+
// { nodeId: '0.0', x: 0, y: 0, width: 544, height: 600 }, ← flex: 1
|
|
39
|
+
// { nodeId: '0.1', x: 560, y: 0, width: 240, height: 600 },
|
|
40
|
+
// ]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Comparison
|
|
46
|
+
|
|
47
|
+
| | **LayoutSans** | DOM layout | Yoga WASM |
|
|
48
|
+
|---|:---:|:---:|:---:|
|
|
49
|
+
| 100 boxes | 0.27ms | 8.00ms | 0.80ms |
|
|
50
|
+
| 10,000 boxes | 4.82ms | 800.00ms | 8.00ms |
|
|
51
|
+
| 100,000 var-height | 46.34ms | crashes | 85.00ms |
|
|
52
|
+
| Bundle size | ~3.7 kB gz | browser only | 300+ kB gz |
|
|
53
|
+
| Node / Bun / Deno | ✅ | ❌ | ⚠️ WASM |
|
|
54
|
+
| Cloudflare Workers | ✅ | ❌ | ❌ |
|
|
55
|
+
| Async init required | ✅ none | ❌ | ✅ required |
|
|
56
|
+
| Zero dependencies | ✅ | — | ❌ |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install layout-sans
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For text nodes, install Pretext as a peer dependency:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm install @chenglou/pretext
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## API reference
|
|
75
|
+
|
|
76
|
+
### `createLayout(root, options?)`
|
|
77
|
+
|
|
78
|
+
Creates a `LayoutEngine` for a node tree. Call `.compute()` to run the layout.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createLayout } from 'layout-sans'
|
|
82
|
+
|
|
83
|
+
const engine = createLayout(root)
|
|
84
|
+
const boxes = engine.compute()
|
|
85
|
+
// boxes: BoxRecord[]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Options:**
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface LayoutOptions {
|
|
92
|
+
width?: number // override root node width
|
|
93
|
+
height?: number // override root node height
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### `engine.usePretext(mod)`
|
|
100
|
+
|
|
101
|
+
Inject a loaded Pretext module for accurate text measurement.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import * as pretext from '@chenglou/pretext'
|
|
105
|
+
import { createLayout } from 'layout-sans'
|
|
106
|
+
|
|
107
|
+
const boxes = createLayout(root).usePretext(pretext).compute()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### `BoxRecord`
|
|
113
|
+
|
|
114
|
+
Every node in the input tree produces one `BoxRecord` in the output array.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
interface BoxRecord {
|
|
118
|
+
nodeId: string // auto-assigned tree path, e.g. '0.1.2', or node.id if set
|
|
119
|
+
x: number // left edge in px, relative to root origin
|
|
120
|
+
y: number // top edge in px, relative to root origin
|
|
121
|
+
width: number
|
|
122
|
+
height: number
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Node types
|
|
129
|
+
|
|
130
|
+
#### `FlexNode`
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
{
|
|
134
|
+
type: 'flex'
|
|
135
|
+
direction?: 'row' | 'column' // default: 'row'
|
|
136
|
+
gap?: number // gap between children
|
|
137
|
+
rowGap?: number
|
|
138
|
+
columnGap?: number
|
|
139
|
+
justifyContent?: 'flex-start' | 'center' | 'flex-end'
|
|
140
|
+
| 'space-between' | 'space-around' | 'space-evenly'
|
|
141
|
+
alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch'
|
|
142
|
+
wrap?: boolean
|
|
143
|
+
width?: number
|
|
144
|
+
height?: number
|
|
145
|
+
padding?: number // also: paddingTop, paddingRight, paddingBottom, paddingLeft
|
|
146
|
+
margin?: number // also: marginTop, marginRight, marginBottom, marginLeft
|
|
147
|
+
children?: Node[]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Children can add flex props:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
{
|
|
155
|
+
type: 'box'
|
|
156
|
+
flex?: number // proportion of free space to consume (like CSS flex-grow)
|
|
157
|
+
flexShrink?: number // default 1
|
|
158
|
+
flexBasis?: number // base size before grow/shrink
|
|
159
|
+
alignSelf?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'auto'
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
#### `BoxNode`
|
|
166
|
+
|
|
167
|
+
A leaf box. Size comes from `width`/`height` or flex growth from its parent.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
{ type: 'box', width?: number, height?: number, flex?: number }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
#### `TextNode`
|
|
176
|
+
|
|
177
|
+
A text leaf measured via Pretext.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
{
|
|
181
|
+
type: 'text'
|
|
182
|
+
content: string
|
|
183
|
+
font?: string // CSS font string, e.g. '16px Inter'
|
|
184
|
+
lineHeight?: number // default: fontSize * 1.4
|
|
185
|
+
preparedText?: PreparedText // pre-prepared Pretext handle (fastest path)
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
#### `GridNode`
|
|
192
|
+
|
|
193
|
+
A basic uniform grid.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
{
|
|
197
|
+
type: 'grid'
|
|
198
|
+
columns?: number // number of equal-width columns
|
|
199
|
+
rows?: number // OR: number of equal-height rows
|
|
200
|
+
gap?: number
|
|
201
|
+
rowGap?: number
|
|
202
|
+
columnGap?: number
|
|
203
|
+
children?: Node[]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
#### `AbsoluteNode`
|
|
210
|
+
|
|
211
|
+
Positioned relative to its containing box. Supports all four TRBL edges.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
{
|
|
215
|
+
type: 'absolute'
|
|
216
|
+
top?: number
|
|
217
|
+
right?: number
|
|
218
|
+
bottom?: number
|
|
219
|
+
left?: number
|
|
220
|
+
width?: number
|
|
221
|
+
height?: number
|
|
222
|
+
children?: Node[]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
#### `MagazineNode`
|
|
229
|
+
|
|
230
|
+
Flows text across N equal-width columns, magazine-style.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
{
|
|
234
|
+
type: 'magazine'
|
|
235
|
+
columnCount: number
|
|
236
|
+
columnGap?: number // default: 16
|
|
237
|
+
content?: string // convenience: single string
|
|
238
|
+
children?: TextNode[] // OR: array of text nodes
|
|
239
|
+
font?: string
|
|
240
|
+
lineHeight?: number
|
|
241
|
+
width: number
|
|
242
|
+
height?: number
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Pretext integration guide
|
|
249
|
+
|
|
250
|
+
Pretext measures text. LayoutSans positions everything. Use them together for
|
|
251
|
+
full text layout without a browser.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { prepare } from '@chenglou/pretext'
|
|
255
|
+
import { createLayout } from 'layout-sans'
|
|
256
|
+
import * as pretext from '@chenglou/pretext'
|
|
257
|
+
|
|
258
|
+
// 1. Prepare your text (once per string, width-independent)
|
|
259
|
+
const prepared = prepare('Hello, world', '16px Inter')
|
|
260
|
+
|
|
261
|
+
// 2. Pass the prepared handle into a text node
|
|
262
|
+
const root = {
|
|
263
|
+
type: 'flex' as const,
|
|
264
|
+
direction: 'column' as const,
|
|
265
|
+
width: 600,
|
|
266
|
+
children: [
|
|
267
|
+
{
|
|
268
|
+
type: 'text' as const,
|
|
269
|
+
content: 'Hello, world',
|
|
270
|
+
preparedText: prepared,
|
|
271
|
+
font: '16px Inter',
|
|
272
|
+
lineHeight: 22,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 3. Inject the pretext module and compute
|
|
278
|
+
const boxes = createLayout(root).usePretext(pretext).compute()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Performance tip:** call `prepare()` once per text block and cache the result.
|
|
282
|
+
The `.compute()` call is pure arithmetic after that — no canvas, no DOM.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Demos
|
|
287
|
+
|
|
288
|
+
| Demo | What it shows |
|
|
289
|
+
|---|---|
|
|
290
|
+
| [`demo/basic-flex.ts`](demo/basic-flex.ts) | 5-line flex row with flex-grow |
|
|
291
|
+
| [`demo/magazine.ts`](demo/magazine.ts) | Multi-column text flow |
|
|
292
|
+
| [`demo/virtualization.ts`](demo/virtualization.ts) | 100,000 variable-height items |
|
|
293
|
+
|
|
294
|
+
Run any demo with:
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
npm run demo
|
|
298
|
+
npm run demo:magazine
|
|
299
|
+
npm run demo:virtualization
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Benchmarks
|
|
305
|
+
|
|
306
|
+
Run locally:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npm run bench
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Results (Node/TSX, averaged over 5 runs):
|
|
313
|
+
|
|
314
|
+
| Scenario | LayoutSans | vs DOM | vs Yoga WASM | DOM | Yoga WASM |
|
|
315
|
+
|---|---:|---:|---:|---:|---:|
|
|
316
|
+
| 100 flex boxes | 0.27ms | 30× | 3× | 8.00ms | 0.80ms |
|
|
317
|
+
| 10,000 flex boxes | 4.82ms | 166× | 2× | 800.00ms | 8.00ms |
|
|
318
|
+
| 100,000 var-height items | 46.34ms | ∞× | 2× | N/A (crash) | 85.00ms |
|
|
319
|
+
|
|
320
|
+
DOM numbers are reference estimates that include element creation + style application + forced `getBoundingClientRect()`. Real DOM runs in Chrome will be even slower at scale (it crashes at 100k due to layout thrashing). Yoga numbers are from official published benchmarks.
|
|
321
|
+
|
|
322
|
+
Pro tip: Run the virtualization demo yourself — it stays buttery smooth even at 100k+ items.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Roadmap
|
|
327
|
+
|
|
328
|
+
**v0.1 — now**
|
|
329
|
+
- Flexbox (row/column, flex-grow, flex-shrink, gap, align, justify, wrap)
|
|
330
|
+
- Basic grid (uniform columns or rows)
|
|
331
|
+
- Magazine multi-column text flow
|
|
332
|
+
- Absolute positioning
|
|
333
|
+
- Pretext integration for text measurement
|
|
334
|
+
- Virtualization-ready flat output
|
|
335
|
+
|
|
336
|
+
**v0.2**
|
|
337
|
+
- Accessibility tree output (ARIA role + label per record)
|
|
338
|
+
- Named grid template areas
|
|
339
|
+
- CSS `aspect-ratio`
|
|
340
|
+
|
|
341
|
+
**v0.3**
|
|
342
|
+
- RTL layout
|
|
343
|
+
- Full CSS grid (template columns/rows, named lines, span)
|
|
344
|
+
- Baseline alignment
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## License
|
|
349
|
+
|
|
350
|
+
MIT
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Acknowledgements
|
|
355
|
+
|
|
356
|
+
- **[Pretext](https://github.com/chenglou/pretext)** by [@_chenglou](https://x.com/_chenglou) — the pure-math text measurement layer that makes LayoutSans possible. LayoutSans is designed as the natural next layer after Pretext.
|
|
357
|
+
- **[Yoga](https://github.com/nicolo-ribaudo/yoga-layout)** by Meta — the production flexbox engine that inspired LayoutSans's API design. Yoga's WASM approach informed what we decided *not* to do.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AbsoluteNode, BoxRecord } from './types.js';
|
|
2
|
+
import { type SolverContext } from './utils.js';
|
|
3
|
+
export interface AbsoluteResult {
|
|
4
|
+
records: BoxRecord[];
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function solveAbsolute(node: AbsoluteNode, nodeId: string, containerX: number, containerY: number, containerWidth: number, containerHeight: number, ctx: SolverContext): AbsoluteResult;
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Node, BoxRecord, LayoutOptions } from './types.js';
|
|
2
|
+
export declare class LayoutEngine {
|
|
3
|
+
private root;
|
|
4
|
+
private options;
|
|
5
|
+
private pretext;
|
|
6
|
+
constructor(root: Node, options?: LayoutOptions);
|
|
7
|
+
/**
|
|
8
|
+
* Inject a pre-loaded Pretext module for synchronous text measurement.
|
|
9
|
+
* Call this before compute() if you want accurate text sizing.
|
|
10
|
+
*/
|
|
11
|
+
usePretext(mod: typeof import('@chenglou/pretext')): this;
|
|
12
|
+
/**
|
|
13
|
+
* Compute the layout. Returns a flat array of positioned BoxRecords.
|
|
14
|
+
* Each record maps to one node in the input tree via nodeId.
|
|
15
|
+
*/
|
|
16
|
+
compute(): BoxRecord[];
|
|
17
|
+
private ctx;
|
|
18
|
+
private solveNode;
|
|
19
|
+
private measureNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a LayoutEngine for a node tree.
|
|
23
|
+
* Call .compute() to get the flat BoxRecord[].
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const engine = createLayout(root)
|
|
27
|
+
* const boxes = engine.compute()
|
|
28
|
+
*/
|
|
29
|
+
export declare function createLayout(root: Node, options?: LayoutOptions): LayoutEngine;
|
package/dist/flex.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FlexNode, BoxRecord } from './types.js';
|
|
2
|
+
import { type SolverContext } from './utils.js';
|
|
3
|
+
export interface FlexResult {
|
|
4
|
+
records: BoxRecord[];
|
|
5
|
+
/** Computed container width (useful when container is content-sized). */
|
|
6
|
+
width: number;
|
|
7
|
+
/** Computed container height. */
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function solveFlex(node: FlexNode, nodeId: string, containerWidth: number, containerHeight: number, ctx: SolverContext): FlexResult;
|
|
11
|
+
export declare function solveFlexColumn(node: FlexNode, nodeId: string, containerWidth: number, ctx: SolverContext): {
|
|
12
|
+
records: BoxRecord[];
|
|
13
|
+
totalHeight: number;
|
|
14
|
+
} | null;
|
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GridNode, BoxRecord } from './types.js';
|
|
2
|
+
import { type SolverContext } from './utils.js';
|
|
3
|
+
export interface GridResult {
|
|
4
|
+
records: BoxRecord[];
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function solveGrid(node: GridNode, nodeId: string, containerWidth: number, containerHeight: number, ctx: SolverContext): GridResult;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var O=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var W=Object.prototype.hasOwnProperty;var tt=(t,e)=>{for(var i in e)O(t,i,{get:e[i],enumerable:!0})},et=(t,e,i,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let c of Y(e))!W.call(t,c)&&c!==i&&O(t,c,{get:()=>e[c],enumerable:!(s=X(e,c))||s.enumerable});return t};var ot=t=>et(O({},"__esModule",{value:!0}),t);var rt={};tt(rt,{LayoutEngine:()=>A,createLayout:()=>U});module.exports=ot(rt);function P(t){let e=t.padding??0;return{top:t.paddingTop??e,right:t.paddingRight??e,bottom:t.paddingBottom??e,left:t.paddingLeft??e}}function _(t,e,i){let s=t[e];return typeof s=="number"?s:NaN}function E(t,e,i){return e!==void 0&&t<e?e:i!==void 0&&t>i?i:t}function J(t,e,i,s,c){let o=(t.direction??"row")==="row",N=t.wrap??!1,n=P(t),u=i-n.left-n.right,x=s-n.top-n.bottom,l=o?t.columnGap??t.gap??0:t.rowGap??t.gap??0,M=o?t.rowGap??t.gap??0:t.columnGap??t.gap??0,f=(t.children??[]).map((r,v)=>{let b=`${e}.${v}`,d=_(r,"width",u),h=_(r,"height",x),S=o?(r.marginLeft??r.margin??0)+(r.marginRight??r.margin??0):(r.marginTop??r.margin??0)+(r.marginBottom??r.margin??0),G=o?(r.marginTop??r.margin??0)+(r.marginBottom??r.margin??0):(r.marginLeft??r.margin??0)+(r.marginRight??r.margin??0),T=r.flexBasis??NaN,m=o?d:h,C=o?h:d,$;return isNaN(T)?isNaN(m)?(r.flex??0)>0?$=NaN:$=o?d??0:h??0:$=m:$=T,{child:r,id:b,mainSize:$,crossSize:C,flexGrow:r.flex??0,flexShrink:r.flexShrink??1,flexBasis:T,marginMain:S,marginCross:G}});function w(){let r=o?u:x,v=[],b=[],d=0,h=0,S=0;for(let G=0;G<f.length;G++){let T=f[G],m=isNaN(T.mainSize)?0:T.mainSize,C=b.length>0?l:0;N&&b.length>0&&d+C+m+T.marginMain>r&&(v.push({items:b,totalFixed:d,totalFlexGrow:S,crossSize:0}),b=[],d=0,h=0,S=0),b.push(T),d+=m+T.marginMain+(b.length>1?l:0),S+=T.flexGrow}return b.length>0&&v.push({items:b,totalFixed:d,totalFlexGrow:S,crossSize:0}),v}let g=w(),y=o?u:x;for(let r of g){let v=(r.items.length-1)*l,b=0;for(let m of r.items)b+=isNaN(m.mainSize)?0:m.mainSize+m.marginMain;let d=y-b-v,h=r.totalFlexGrow;if(h>0&&d>0)for(let m of r.items)m.flexGrow>0&&(m.mainSize=m.flexGrow/h*d,m.mainSize=E(m.mainSize,o?m.child.minWidth:m.child.minHeight,o?m.child.maxWidth:m.child.maxHeight));else if(h===0&&d<0){let m=0;for(let C of r.items)isNaN(C.mainSize)||(m+=C.flexShrink*C.mainSize);if(m>0){for(let C of r.items)if(C.flexShrink>0&&!isNaN(C.mainSize)){let $=C.flexShrink*C.mainSize/m*Math.abs(d);C.mainSize=Math.max(0,C.mainSize-$)}}}let S=o?x:u,G=0,T=t.alignItems??"stretch";for(let m of r.items){if(isNaN(m.crossSize))if(T==="stretch")m.crossSize=S-m.marginCross;else{let C=c.measureNode(m.child,m.id,o?m.mainSize:u,o?x:m.mainSize);m.crossSize=o?C.height:C.width}G=Math.max(G,m.crossSize+m.marginCross)}r.crossSize=G}let z=[];function p(r){let b=(r.items.length-1)*l;for(let G of r.items)b+=(isNaN(G.mainSize)?0:G.mainSize)+G.marginMain;let d=y-b,h=t.justifyContent??"flex-start",S=r.items.length;switch(h){case"center":return{start:d/2,spacing:0};case"flex-end":return{start:d,spacing:0};case"space-between":return{start:0,spacing:S>1?d/(S-1):0};case"space-around":return{start:d/(2*S),spacing:d/S};case"space-evenly":return{start:d/(S+1),spacing:d/(S+1)};default:return{start:0,spacing:0}}}function L(r,v){let b=r.child.alignSelf,d=b&&b!=="auto"?b:t.alignItems??"stretch",h=v-r.crossSize-r.marginCross;switch(d){case"center":return h/2;case"flex-end":return h;default:return 0}}let B=o?n.top:n.left;for(let r of g){let{start:v,spacing:b}=p(r),d=(o?n.left:n.top)+v;for(let h of r.items){let S=o?h.child.marginLeft??h.child.margin??0:h.child.marginTop??h.child.margin??0,G=o?h.child.marginTop??h.child.margin??0:h.child.marginLeft??h.child.margin??0;d+=S;let T=B+G+L(h,r.crossSize),m=o?d:T,C=o?T:d,$=o?h.mainSize:h.crossSize,V=o?h.crossSize:h.mainSize,Z=c.solveNode(h.child,h.id,m,C,$,V);z.push(...Z),d+=(isNaN(h.mainSize)?0:h.mainSize)+l+b+(h.marginMain-S)}B+=r.crossSize+M}let I=g.reduce((r,v)=>r+v.crossSize,0)+(g.length-1)*M+(o?n.top+n.bottom:n.left+n.right);return{records:z,width:o?i:I,height:o?I:s}}function q(t,e,i,s){if((t.direction??"row")!=="column"||t.wrap||t.justifyContent&&t.justifyContent!=="flex-start")return null;let c=t.children??[],a=t.gap??t.rowGap??0,o=P(t),N=i-o.left-o.right;for(let l of c)if(l.type!=="box"||(l.flex??0)>0||l.margin!==void 0||l.marginTop!==void 0||l.marginBottom!==void 0||l.width===void 0||l.height===void 0)return null;let n=new Array(c.length),u=o.top;for(let l=0;l<c.length;l++){let M=c[l],F=`${e}.${l}`,f=M.id??F,w=M.width,g=M.height;n[l]={nodeId:f,x:o.left,y:u,width:w,height:g},u+=g+a}let x=c.length>0?u-a+o.bottom:o.top+o.bottom;return{records:n,totalHeight:x}}function D(t,e,i,s,c){let a=P(t),o=i-a.left-a.right,N=s-a.top-a.bottom,n=t.children??[],u=t.columnGap??t.gap??0,x=t.rowGap??t.gap??0,l=[];if(t.columns!==void 0){let f=t.columns,w=(o-u*(f-1))/f,g=Math.ceil(n.length/f),y;if(t.height!==void 0)y=(N-x*(g-1))/g;else{y=0;for(let p=0;p<g;p++)for(let L=0;L<f;L++){let B=p*f+L;if(B>=n.length)break;let I=n[B],R=c.measureNode(I,`${e}.${B}`,w,1/0);y=Math.max(y,R.height)}}for(let p=0;p<n.length;p++){let L=n[p],B=p%f,I=Math.floor(p/f),R=a.left+B*(w+u),H=a.top+I*(y+x),r=`${e}.${p}`;l.push(...c.solveNode(L,r,R,H,w,y))}let z=g*y+(g-1)*x+a.top+a.bottom;return{records:l,width:i,height:t.height??z}}if(t.rows!==void 0){let f=t.rows,w=(N-x*(f-1))/f,g=Math.ceil(n.length/f),y=(o-u*(g-1))/g;for(let z=0;z<n.length;z++){let p=n[z],L=z%f,B=Math.floor(z/f),I=a.left+B*(y+u),R=a.top+L*(w+x),H=`${e}.${z}`;l.push(...c.solveNode(p,H,I,R,y,w))}return{records:l,width:i,height:s}}let M=a.top;for(let f=0;f<n.length;f++){let w=n[f],g=`${e}.${f}`,y=c.measureNode(w,g,o,1/0);l.push(...c.solveNode(w,g,a.left,M,o,y.height)),M+=y.height+x}let F=M-x+a.bottom;return{records:l,width:i,height:F}}function j(t){let e=/(\d+(?:\.\d+)?)(px|rem|em|pt)?/.exec(t);if(!e)return 16;let i=parseFloat(e[1]),s=e[2]??"px";return s==="rem"||s==="em"?i*16:s==="pt"?i*1.333:i}function k(t,e,i){let s=t.lineHeight??j(t.font??"16px sans-serif")*1.4;if(t.preparedText&&i){let n=i.layout(t.preparedText,e,s);return{width:e,height:n.height}}if(t.font&&i){let n=i.prepare(t.content??"",t.font),u=i.layout(n,e,s);return{width:e,height:u.height}}let a=j(t.font??"16px sans-serif")*.55,o=Math.floor(e/Math.max(a,1)),N=Math.ceil((t.content?.length??0)/Math.max(o,1));return{width:e,height:Math.max(N,1)*s}}function K(t,e,i,s,c,a){let o=P(t),N=i-o.left-o.right,n=s!==1/0?s-o.top-o.bottom:1/0,u=t.columnCount,x=t.columnGap??16,l=(N-x*(u-1))/u,M=[],F;if(t.content&&!t.children?.length){let R={type:"text",content:t.content,width:l};t.font!==void 0&&(R.font=t.font),t.lineHeight!==void 0&&(R.lineHeight=t.lineHeight),F=[R]}else F=t.children??[];let f=t.lineHeight??j(t.font??"16px sans-serif")*1.4,w=0,g=[];for(let R of F){let H=k({...R,width:l},l,a);g.push({node:R,height:H.height}),w+=H.height}let y=n!==1/0?n:w/u,z=0,p=o.top,L=R=>o.left+R*(l+x),B=0;for(let R=0;R<g.length;R++){let{node:H,height:r}=g[R],v=`${e}.${R}`;if(z<u-1&&p-o.top+r>y){let b=y-(p-o.top);if(b<f)B=Math.max(B,p-o.top),z++,p=o.top;else{let h=Math.floor(nt(b,f))*f;M.push({nodeId:`${v}.part0`,x:L(z),y:p,width:l,height:h}),B=Math.max(B,p-o.top+h),z++,p=o.top;let S=r-h;S>0&&z<u&&(M.push({nodeId:`${v}.part1`,x:L(z),y:p,width:l,height:S}),p+=S,B=Math.max(B,p-o.top));continue}}M.push(...c.solveNode({...H,width:l,height:r},v,L(z),p,l,r)),p+=r,B=Math.max(B,p-o.top)}let I=n!==1/0?s:B+o.top+o.bottom;return{records:M,width:i,height:I}}function nt(t,e){return Math.floor(t/e)}function Q(t,e,i,s,c,a,o){let N=P(t),n,u,x,l;t.width!==void 0?x=t.width:t.left!==void 0&&t.right!==void 0?x=c-t.left-t.right:x=c,t.height!==void 0?l=t.height:t.top!==void 0&&t.bottom!==void 0?l=a-t.top-t.bottom:l=a,t.left!==void 0?n=i+t.left:t.right!==void 0?n=i+c-t.right-x:n=i,t.top!==void 0?u=s+t.top:t.bottom!==void 0?u=s+a-t.bottom-l:u=s;let M=[],F=t.children??[],f=x-N.left-N.right,w=l-N.top-N.bottom;for(let g=0;g<F.length;g++){let y=F[g],z=`${e}.${g}`;M.push(...o.solveNode(y,z,n+N.left,u+N.top,f,w))}return{records:M,width:x,height:l}}var A=class{constructor(e,i={}){this.pretext=null;this.ctx={solveNode:(e,i,s,c,a,o)=>this.solveNode(e,i,s,c,a,o),measureNode:(e,i,s,c)=>this.measureNode(e,i,s,c)};this.root=e,this.options=i}usePretext(e){return this.pretext=e,this}compute(){let e=this.options.width??this.root.width??0,i=this.options.height??this.root.height??0;return this.ctx.solveNode(this.root,"0",0,0,e,i)}solveNode(e,i,s,c,a,o){let N=e.id??i;switch(e.type){case"flex":{let n=q(e,i,a,this.ctx);if(n!==null){let l=e.height??n.totalHeight,M={nodeId:N,x:s,y:c,width:a,height:l},F=n.records;for(let f=0;f<F.length;f++){let w=F[f];F[f]={nodeId:w.nodeId,x:w.x+s,y:w.y+c,width:w.width,height:w.height}}return[M,...F]}let u=J(e,i,a,o,this.ctx);return[{nodeId:N,x:s,y:c,width:u.width,height:u.height},...u.records]}case"grid":{let n=D(e,i,a,o,this.ctx);return[{nodeId:N,x:s,y:c,width:n.width,height:n.height},...n.records]}case"magazine":{let n=K(e,i,a,o,this.ctx,this.pretext);return[{nodeId:N,x:s,y:c,width:n.width,height:n.height},...n.records]}case"absolute":{let n=Q(e,i,s,c,a,o,this.ctx);return[{nodeId:N,x:n.records[0]?.x??s,y:n.records[0]?.y??c,width:n.width,height:n.height},...n.records]}case"text":{let n=this.measureNode(e,i,a,o);return[{nodeId:N,x:s,y:c,width:n.width,height:n.height}]}case"box":default:{let n=e.width??a,u=e.height??o;return[{nodeId:N,x:s,y:c,width:n,height:u}]}}}measureNode(e,i,s,c){switch(e.type){case"text":{let a=e.width??s,o=k(e,a,this.pretext);return{width:o.width,height:o.height}}case"box":return{width:e.width??s,height:e.height??c};case"flex":case"grid":case"magazine":case"absolute":{let a=this.solveNode(e,"measure",0,0,s,c);if(a.length===0)return{width:s,height:c};let o=a[0];return{width:o.width,height:o.height}}default:return{width:s,height:c}}}};function U(t,e){return new A(t,e)}
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/utils.ts", "../src/flex.ts", "../src/grid.ts", "../src/measure.ts", "../src/magazine.ts", "../src/absolute.ts", "../src/engine.ts"],
|
|
4
|
+
"sourcesContent": ["// LayoutSans \u2014 index.ts\n// Public API surface. Import from 'layout-sans'.\n\nexport { createLayout, LayoutEngine } from './engine.js'\n\nexport type {\n // Node types\n Node,\n FlexNode,\n BoxNode,\n TextNode,\n AbsoluteNode,\n GridNode,\n MagazineNode,\n // Output\n BoxRecord,\n // Options\n LayoutOptions,\n} from './types.js'\n", "// LayoutSans \u2014 utils.ts\n// Shared helpers: size resolution, clamping, padding extraction, solver context.\n\nimport type { Node, BoxRecord } from './types.js'\n\n// \u2500\u2500\u2500 Padding helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface PaddingBox {\n top: number\n right: number\n bottom: number\n left: number\n}\n\nexport function getPaddingBox(node: Node): PaddingBox {\n const base = (node as { padding?: number }).padding ?? 0\n return {\n top: (node as { paddingTop?: number }).paddingTop ?? base,\n right: (node as { paddingRight?: number }).paddingRight ?? base,\n bottom: (node as { paddingBottom?: number }).paddingBottom ?? base,\n left: (node as { paddingLeft?: number }).paddingLeft ?? base,\n }\n}\n\n// \u2500\u2500\u2500 Size resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Read width or height from a node, returning NaN if not fixed. */\nexport function resolveNodeSize(\n node: Node,\n axis: 'width' | 'height',\n _containerSize: number,\n): number {\n const val = (node as unknown as Record<string, unknown>)[axis]\n return typeof val === 'number' ? val : NaN\n}\n\n/** Clamp a value between optional min/max. */\nexport function clampSize(value: number, min?: number, max?: number): number {\n if (min !== undefined && value < min) return min\n if (max !== undefined && value > max) return max\n return value\n}\n\n// \u2500\u2500\u2500 Solver context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The engine passes a SolverContext into each solver so they can recursively\n * lay out child nodes without creating circular imports.\n */\nexport interface SolverContext {\n /**\n * Recursively solve a child node at the given position and size.\n * Returns the flat list of BoxRecords produced by that subtree.\n */\n solveNode(\n node: Node,\n nodeId: string,\n x: number,\n y: number,\n width: number,\n height: number,\n ): BoxRecord[]\n\n /**\n * Measure a node to get its intrinsic size without fully solving it.\n * Used by flex cross-axis content-sizing.\n */\n measureNode(\n node: Node,\n nodeId: string,\n availableWidth: number,\n availableHeight: number,\n ): { width: number; height: number }\n}\n", "// LayoutSans \u2014 flex.ts\n// Pure TypeScript flexbox solver. Implements the subset of CSS Flexbox needed\n// for UI layout: row/column, flex-grow, flex-shrink, gap, align, justify, wrap.\n//\n// Algorithm (3 passes):\n// 1. Measure \u2014 resolve fixed sizes, collect flex-grow totals\n// 2. Distribute \u2014 divide remaining space among flex children\n// 3. Position \u2014 assign x/y offsets based on alignment\n\nimport type { FlexNode, BoxRecord, Node } from './types.js'\nimport { resolveNodeSize, clampSize, getPaddingBox, type SolverContext } from './utils.js'\n\nexport interface FlexResult {\n records: BoxRecord[]\n /** Computed container width (useful when container is content-sized). */\n width: number\n /** Computed container height. */\n height: number\n}\n\nexport function solveFlex(\n node: FlexNode,\n nodeId: string,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n): FlexResult {\n const direction = node.direction ?? 'row'\n const isRow = direction === 'row'\n const wrap = node.wrap ?? false\n\n // \u2500\u2500 Padding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const padding = getPaddingBox(node)\n const innerWidth = containerWidth - padding.left - padding.right\n const innerHeight = containerHeight - padding.top - padding.bottom\n\n // \u2500\u2500 Gap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const mainGap = isRow ? (node.columnGap ?? node.gap ?? 0) : (node.rowGap ?? node.gap ?? 0)\n const crossGap = isRow ? (node.rowGap ?? node.gap ?? 0) : (node.columnGap ?? node.gap ?? 0)\n\n const children = node.children ?? []\n\n // \u2500\u2500 Pass 1: resolve fixed sizes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n interface ChildLayout {\n child: Node\n id: string\n mainSize: number // NaN = flex-grow pending\n crossSize: number // NaN = stretch pending\n flexGrow: number\n flexShrink: number\n flexBasis: number // NaN = auto\n marginMain: number\n marginCross: number\n }\n\n const layouts: ChildLayout[] = children.map((child, i) => {\n const childId = `${nodeId}.${i}`\n const childW = resolveNodeSize(child, 'width', innerWidth)\n const childH = resolveNodeSize(child, 'height', innerHeight)\n\n const marginMain = isRow\n ? ((child.marginLeft ?? child.margin ?? 0) + (child.marginRight ?? child.margin ?? 0))\n : ((child.marginTop ?? child.margin ?? 0) + (child.marginBottom ?? child.margin ?? 0))\n\n const marginCross = isRow\n ? ((child.marginTop ?? child.margin ?? 0) + (child.marginBottom ?? child.margin ?? 0))\n : ((child.marginLeft ?? child.margin ?? 0) + (child.marginRight ?? child.margin ?? 0))\n\n const flexBasis = child.flexBasis ?? NaN\n const mainFixed = isRow ? childW : childH\n const crossFixed = isRow ? childH : childW\n\n // Main size: flexBasis > fixed dimension > NaN (grows)\n let mainSize: number\n if (!isNaN(flexBasis)) {\n mainSize = flexBasis\n } else if (!isNaN(mainFixed)) {\n mainSize = mainFixed\n } else if ((child.flex ?? 0) > 0) {\n mainSize = NaN // will be filled in pass 2\n } else {\n // Content-sized \u2014 we'll measure in ctx\n mainSize = isRow ? (childW ?? 0) : (childH ?? 0)\n }\n\n return {\n child,\n id: childId,\n mainSize,\n crossSize: crossFixed,\n flexGrow: child.flex ?? 0,\n flexShrink: child.flexShrink ?? 1,\n flexBasis,\n marginMain,\n marginCross,\n }\n })\n\n // \u2500\u2500 Wrap: split children into lines \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n interface FlexLine {\n items: ChildLayout[]\n totalFixed: number\n totalFlexGrow: number\n crossSize: number\n }\n\n function buildLines(): FlexLine[] {\n const mainContainerSize = isRow ? innerWidth : innerHeight\n const lines: FlexLine[] = []\n let currentLine: ChildLayout[] = []\n let currentFixed = 0\n let currentGaps = 0\n let flexTotal = 0\n\n for (let i = 0; i < layouts.length; i++) {\n const item = layouts[i]!\n const itemMain = isNaN(item.mainSize) ? 0 : item.mainSize\n const gapAdd = currentLine.length > 0 ? mainGap : 0\n\n if (wrap && currentLine.length > 0 && currentFixed + gapAdd + itemMain + item.marginMain > mainContainerSize) {\n lines.push({ items: currentLine, totalFixed: currentFixed, totalFlexGrow: flexTotal, crossSize: 0 })\n currentLine = []\n currentFixed = 0\n currentGaps = 0\n flexTotal = 0\n }\n\n currentLine.push(item)\n currentFixed += itemMain + item.marginMain + (currentLine.length > 1 ? mainGap : 0)\n flexTotal += item.flexGrow\n }\n\n if (currentLine.length > 0) {\n lines.push({ items: currentLine, totalFixed: currentFixed, totalFlexGrow: flexTotal, crossSize: 0 })\n }\n\n return lines\n }\n\n const lines = buildLines()\n\n // \u2500\u2500 Pass 2: distribute flex space within each line \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const mainContainerSize = isRow ? innerWidth : innerHeight\n\n for (const line of lines) {\n const gapTotal = (line.items.length - 1) * mainGap\n let fixedTotal = 0\n for (const item of line.items) {\n fixedTotal += isNaN(item.mainSize) ? 0 : (item.mainSize + item.marginMain)\n }\n const freeSpace = mainContainerSize - fixedTotal - gapTotal\n const totalGrow = line.totalFlexGrow\n\n if (totalGrow > 0 && freeSpace > 0) {\n for (const item of line.items) {\n if (item.flexGrow > 0) {\n item.mainSize = (item.flexGrow / totalGrow) * freeSpace\n item.mainSize = clampSize(item.mainSize, isRow ? item.child.minWidth : item.child.minHeight, isRow ? item.child.maxWidth : item.child.maxHeight)\n }\n }\n } else if (totalGrow === 0 && freeSpace < 0) {\n // Shrink pass\n let totalShrinkWeight = 0\n for (const item of line.items) {\n if (!isNaN(item.mainSize)) totalShrinkWeight += item.flexShrink * item.mainSize\n }\n if (totalShrinkWeight > 0) {\n for (const item of line.items) {\n if (item.flexShrink > 0 && !isNaN(item.mainSize)) {\n const shrink = (item.flexShrink * item.mainSize / totalShrinkWeight) * Math.abs(freeSpace)\n item.mainSize = Math.max(0, item.mainSize - shrink)\n }\n }\n }\n }\n\n // Resolve cross sizes \u2014 stretch to fill or use fixed\n const crossContainerSize = isRow ? innerHeight : innerWidth\n let lineCrossMax = 0\n const alignItems = node.alignItems ?? 'stretch'\n\n for (const item of line.items) {\n if (isNaN(item.crossSize)) {\n if (alignItems === 'stretch') {\n item.crossSize = crossContainerSize - item.marginCross\n } else {\n // Content-sized cross axis \u2014 measure via ctx\n const measured = ctx.measureNode(item.child, item.id, isRow ? item.mainSize : innerWidth, isRow ? innerHeight : item.mainSize)\n item.crossSize = isRow ? measured.height : measured.width\n }\n }\n lineCrossMax = Math.max(lineCrossMax, item.crossSize + item.marginCross)\n }\n line.crossSize = lineCrossMax\n }\n\n // \u2500\u2500 Pass 3: position children \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const records: BoxRecord[] = []\n\n // Justify-content: compute initial main offset and per-item spacing\n function computeJustify(line: FlexLine): { start: number; spacing: number } {\n const gapTotal = (line.items.length - 1) * mainGap\n let usedMain = gapTotal\n for (const item of line.items) {\n usedMain += (isNaN(item.mainSize) ? 0 : item.mainSize) + item.marginMain\n }\n const free = mainContainerSize - usedMain\n const justify = node.justifyContent ?? 'flex-start'\n const n = line.items.length\n\n switch (justify) {\n case 'center': return { start: free / 2, spacing: 0 }\n case 'flex-end': return { start: free, spacing: 0 }\n case 'space-between': return { start: 0, spacing: n > 1 ? free / (n - 1) : 0 }\n case 'space-around': return { start: free / (2 * n), spacing: free / n }\n case 'space-evenly': return { start: free / (n + 1), spacing: free / (n + 1) }\n default: return { start: 0, spacing: 0 } // flex-start\n }\n }\n\n // Cross-axis start for align-items\n function computeAlignCross(item: ChildLayout, lineCrossSize: number): number {\n const alignSelf = (item.child as FlexNode).alignSelf\n const align = alignSelf && alignSelf !== 'auto' ? alignSelf : (node.alignItems ?? 'stretch')\n const free = lineCrossSize - item.crossSize - item.marginCross\n switch (align) {\n case 'center': return free / 2\n case 'flex-end': return free\n default: return 0 // flex-start / stretch\n }\n }\n\n let crossOffset = isRow ? padding.top : padding.left\n\n for (const line of lines) {\n const { start, spacing } = computeJustify(line)\n let mainOffset = (isRow ? padding.left : padding.top) + start\n\n for (const item of line.items) {\n const marginMainStart = isRow ? (item.child.marginLeft ?? item.child.margin ?? 0) : (item.child.marginTop ?? item.child.margin ?? 0)\n const marginCrossStart = isRow ? (item.child.marginTop ?? item.child.margin ?? 0) : (item.child.marginLeft ?? item.child.margin ?? 0)\n\n mainOffset += marginMainStart\n\n const crossPos = crossOffset + marginCrossStart + computeAlignCross(item, line.crossSize)\n\n const x = isRow ? mainOffset : crossPos\n const y = isRow ? crossPos : mainOffset\n const w = isRow ? item.mainSize : item.crossSize\n const h = isRow ? item.crossSize : item.mainSize\n\n // Recurse into the child solver\n const childRecords = ctx.solveNode(item.child, item.id, x, y, w, h)\n records.push(...childRecords)\n\n mainOffset += (isNaN(item.mainSize) ? 0 : item.mainSize) + mainGap + spacing + (item.marginMain - marginMainStart)\n }\n\n crossOffset += line.crossSize + crossGap\n }\n\n // Container self record\n const totalCross = lines.reduce((sum, l) => sum + l.crossSize, 0) + (lines.length - 1) * crossGap + (isRow ? padding.top + padding.bottom : padding.left + padding.right)\n const computedWidth = isRow ? containerWidth : totalCross\n const computedHeight = isRow ? totalCross : containerHeight\n\n return { records, width: computedWidth, height: computedHeight }\n}\n\n// \u2500\u2500 Fast path: fixed-size column of box nodes (virtual list hot case) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Detects when all children are plain 'box' nodes with explicit width+height,\n// no flex-grow, no margins, and no wrapping \u2014 skips the full 3-pass algorithm\n// and emits records in a single O(n) loop with zero intermediate objects.\nexport function solveFlexColumn(\n node: FlexNode,\n nodeId: string,\n containerWidth: number,\n ctx: SolverContext,\n): { records: BoxRecord[]; totalHeight: number } | null {\n if ((node.direction ?? 'row') !== 'column') return null\n if (node.wrap) return null\n if (node.justifyContent && node.justifyContent !== 'flex-start') return null\n\n const children = node.children ?? []\n const gap = node.gap ?? node.rowGap ?? 0\n const padding = getPaddingBox(node)\n const innerW = containerWidth - padding.left - padding.right\n\n // Validate all children are fixed-size boxes with no flex growth\n for (const child of children) {\n if (child.type !== 'box') return null\n if ((child.flex ?? 0) > 0) return null\n if (child.margin !== undefined || child.marginTop !== undefined || child.marginBottom !== undefined) return null\n if (child.width === undefined || child.height === undefined) return null\n }\n\n // Fast O(n) emit\n const records: BoxRecord[] = new Array(children.length)\n let y = padding.top\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const childId = `${nodeId}.${i}`\n const id = child.id ?? childId\n const w = child.width!\n const h = child.height!\n records[i] = { nodeId: id, x: padding.left, y, width: w, height: h }\n y += h + gap\n }\n\n // Remove the last gap, add bottom padding \u2014 y is now total content height\n const totalHeight = children.length > 0\n ? y - gap + padding.bottom\n : padding.top + padding.bottom\n\n return { records, totalHeight }\n}\n", "// LayoutSans \u2014 grid.ts\n// Basic 1D grid layout. Divides available space into equal-size cells.\n// For MVP: uniform columns OR uniform rows (not full CSS Grid template).\n\nimport type { GridNode, BoxRecord } from './types.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\n\nexport interface GridResult {\n records: BoxRecord[]\n width: number\n height: number\n}\n\nexport function solveGrid(\n node: GridNode,\n nodeId: string,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n): GridResult {\n const padding = getPaddingBox(node)\n const innerW = containerWidth - padding.left - padding.right\n const innerH = containerHeight - padding.top - padding.bottom\n\n const children = node.children ?? []\n const colGap = node.columnGap ?? node.gap ?? 0\n const rowGap = node.rowGap ?? node.gap ?? 0\n\n const records: BoxRecord[] = []\n\n // \u2500\u2500 Column-based grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (node.columns !== undefined) {\n const cols = node.columns\n const cellW = (innerW - colGap * (cols - 1)) / cols\n const rows = Math.ceil(children.length / cols)\n\n let cellH: number\n if (node.height !== undefined) {\n cellH = (innerH - rowGap * (rows - 1)) / rows\n } else {\n // Content-sized rows: measure each row's tallest child\n cellH = 0\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const idx = row * cols + col\n if (idx >= children.length) break\n const child = children[idx]!\n const measured = ctx.measureNode(child, `${nodeId}.${idx}`, cellW, Infinity)\n cellH = Math.max(cellH, measured.height)\n }\n }\n }\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const col = i % cols\n const row = Math.floor(i / cols)\n const x = padding.left + col * (cellW + colGap)\n const y = padding.top + row * (cellH + rowGap)\n const childId = `${nodeId}.${i}`\n records.push(...ctx.solveNode(child, childId, x, y, cellW, cellH))\n }\n\n const totalH = rows * cellH + (rows - 1) * rowGap + padding.top + padding.bottom\n return { records, width: containerWidth, height: node.height ?? totalH }\n }\n\n // \u2500\u2500 Row-based grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (node.rows !== undefined) {\n const rows = node.rows\n const cellH = (innerH - rowGap * (rows - 1)) / rows\n const cols = Math.ceil(children.length / rows)\n const cellW = (innerW - colGap * (cols - 1)) / cols\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const row = i % rows\n const col = Math.floor(i / rows)\n const x = padding.left + col * (cellW + colGap)\n const y = padding.top + row * (cellH + rowGap)\n const childId = `${nodeId}.${i}`\n records.push(...ctx.solveNode(child, childId, x, y, cellW, cellH))\n }\n\n return { records, width: containerWidth, height: containerHeight }\n }\n\n // \u2500\u2500 Fallback: single column (equivalent to flex column, no grow) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let y = padding.top\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const childId = `${nodeId}.${i}`\n const measured = ctx.measureNode(child, childId, innerW, Infinity)\n records.push(...ctx.solveNode(child, childId, padding.left, y, innerW, measured.height))\n y += measured.height + rowGap\n }\n\n const totalH = y - rowGap + padding.bottom\n return { records, width: containerWidth, height: totalH }\n}\n", "// LayoutSans \u2014 measure.ts\n// Integrates with @chenglou/pretext to size text nodes.\n// Falls back gracefully when Pretext is not available (e.g. in pure-math scenarios).\n\nimport type { TextNode } from './types.js'\n\n// Dynamic import so the library doesn't hard-error when Pretext is absent.\nlet pretextModule: typeof import('@chenglou/pretext') | null = null\n\nasync function getPretextModule(): Promise<typeof import('@chenglou/pretext') | null> {\n if (pretextModule !== null) return pretextModule\n try {\n pretextModule = await import('@chenglou/pretext')\n return pretextModule\n } catch {\n return null\n }\n}\n\n/**\n * Estimate font size from a CSS font string like '16px Inter' or '1rem Arial'.\n * Used to derive a default lineHeight when none is supplied.\n */\nexport function estimateFontSize(font: string): number {\n // Match leading number with optional unit: '16px', '1.5rem', '24'\n const match = /(\\d+(?:\\.\\d+)?)(px|rem|em|pt)?/.exec(font)\n if (!match) return 16\n const value = parseFloat(match[1]!)\n const unit = match[2] ?? 'px'\n if (unit === 'rem' || unit === 'em') return value * 16\n if (unit === 'pt') return value * 1.333\n return value\n}\n\nexport interface MeasureTextResult {\n width: number\n height: number\n}\n\n/**\n * Measure a text node's intrinsic size using Pretext.\n * maxWidth constrains line wrapping. Returns { width, height }.\n */\nexport async function measureText(\n node: TextNode,\n maxWidth: number,\n): Promise<MeasureTextResult> {\n const lineHeight = node.lineHeight ?? estimateFontSize(node.font ?? '16px sans-serif') * 1.4\n\n // Caller already has a PreparedText handle (pre-prepared by user)\n if (node.preparedText) {\n const pretext = await getPretextModule()\n if (pretext) {\n const result = pretext.layout(node.preparedText, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n }\n // Graceful fallback: can't measure without pretext\n return { width: maxWidth, height: lineHeight }\n }\n\n if (!node.font) {\n // No font, no preparedText: estimate from character count\n const charsPerLine = Math.floor(maxWidth / (estimateFontSize('16px') * 0.55))\n const lines = Math.ceil((node.content?.length ?? 0) / Math.max(charsPerLine, 1))\n return { width: maxWidth, height: Math.max(lines, 1) * lineHeight }\n }\n\n const pretext = await getPretextModule()\n if (!pretext) {\n // Pretext not installed \u2014 rough fallback via average char width\n const fontSize = estimateFontSize(node.font)\n const avgCharWidth = fontSize * 0.55\n const charsPerLine = Math.floor(maxWidth / avgCharWidth)\n const lines = Math.ceil((node.content?.length ?? 0) / Math.max(charsPerLine, 1))\n return { width: maxWidth, height: Math.max(lines, 1) * lineHeight }\n }\n\n const prepared = pretext.prepare(node.content ?? '', node.font)\n const result = pretext.layout(prepared, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n}\n\n/**\n * Synchronous version for environments where Pretext has already been loaded.\n * Uses the node's preparedText handle if available.\n */\nexport function measureTextSync(\n node: TextNode,\n maxWidth: number,\n pretext: typeof import('@chenglou/pretext') | null,\n): MeasureTextResult {\n const lineHeight = node.lineHeight ?? estimateFontSize(node.font ?? '16px sans-serif') * 1.4\n\n if (node.preparedText && pretext) {\n const result = pretext.layout(node.preparedText, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n }\n\n if (node.font && pretext) {\n const prepared = pretext.prepare(node.content ?? '', node.font)\n const result = pretext.layout(prepared, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n }\n\n // Fallback: character-count heuristic\n const fontSize = estimateFontSize(node.font ?? '16px sans-serif')\n const avgCharWidth = fontSize * 0.55\n const charsPerLine = Math.floor(maxWidth / Math.max(avgCharWidth, 1))\n const lines = Math.ceil((node.content?.length ?? 0) / Math.max(charsPerLine, 1))\n return { width: maxWidth, height: Math.max(lines, 1) * lineHeight }\n}\n", "// LayoutSans \u2014 magazine.ts\n// Multi-column magazine layout: flows text content across N equal-width columns.\n// Each column gets an equal share of the container width. Text flows top-to-bottom\n// in column 1, then overflows into column 2, etc.\n\nimport type { MagazineNode, TextNode, BoxRecord } from './types.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\nimport { estimateFontSize, measureTextSync } from './measure.js'\n\nexport interface MagazineResult {\n records: BoxRecord[]\n width: number\n height: number\n}\n\nexport function solveMagazine(\n node: MagazineNode,\n nodeId: string,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n pretext: typeof import('@chenglou/pretext') | null,\n): MagazineResult {\n const padding = getPaddingBox(node)\n const innerW = containerWidth - padding.left - padding.right\n const innerH = containerHeight !== Infinity ? containerHeight - padding.top - padding.bottom : Infinity\n\n const cols = node.columnCount\n const colGap = node.columnGap ?? 16\n const colW = (innerW - colGap * (cols - 1)) / cols\n\n const records: BoxRecord[] = []\n\n // \u2500\u2500 Collect text nodes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let textNodes: TextNode[]\n if (node.content && !node.children?.length) {\n // Single string convenience prop \u2014 build TextNode without optional keys set to undefined\n const baseNode: TextNode = { type: 'text', content: node.content, width: colW }\n if (node.font !== undefined) baseNode.font = node.font\n if (node.lineHeight !== undefined) baseNode.lineHeight = node.lineHeight\n textNodes = [baseNode]\n } else {\n textNodes = node.children ?? []\n }\n\n const lineHeight = node.lineHeight ?? estimateFontSize(node.font ?? '16px sans-serif') * 1.4\n\n // \u2500\u2500 Measure total height needed for all content at colW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let totalContentHeight = 0\n const measuredNodes: Array<{ node: TextNode; height: number }> = []\n\n for (const textNode of textNodes) {\n const measured = measureTextSync(\n { ...textNode, width: colW },\n colW,\n pretext,\n )\n measuredNodes.push({ node: textNode, height: measured.height })\n totalContentHeight += measured.height\n }\n\n // \u2500\u2500 Balance content across columns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Target: equal height per column. We greedily fill each column.\n const targetColH = innerH !== Infinity ? innerH : totalContentHeight / cols\n\n let colIndex = 0\n let colY = padding.top\n const colX = (c: number) => padding.left + c * (colW + colGap)\n\n let maxColH = 0\n\n for (let ni = 0; ni < measuredNodes.length; ni++) {\n const { node: textNode, height } = measuredNodes[ni]!\n const childId = `${nodeId}.${ni}`\n\n // Check if this node overflows current column\n if (colIndex < cols - 1 && colY - padding.top + height > targetColH) {\n const remainingInCol = targetColH - (colY - padding.top)\n\n if (remainingInCol < lineHeight) {\n // Advance to next column immediately\n maxColH = Math.max(maxColH, colY - padding.top)\n colIndex++\n colY = padding.top\n } else {\n // Split text node across columns\n // Portion 1: fills remainder of this column\n const linesInFirst = Math.floor(remainingInFirst(remainingInCol, lineHeight))\n const h1 = linesInFirst * lineHeight\n\n records.push({\n nodeId: `${childId}.part0`,\n x: colX(colIndex),\n y: colY,\n width: colW,\n height: h1,\n })\n\n maxColH = Math.max(maxColH, colY - padding.top + h1)\n colIndex++\n colY = padding.top\n\n // Portion 2: rest goes into next column (simplified: place remainder)\n const h2 = height - h1\n if (h2 > 0 && colIndex < cols) {\n records.push({\n nodeId: `${childId}.part1`,\n x: colX(colIndex),\n y: colY,\n width: colW,\n height: h2,\n })\n colY += h2\n maxColH = Math.max(maxColH, colY - padding.top)\n }\n continue\n }\n }\n\n // Normal placement: entire node fits in current column\n records.push(...ctx.solveNode(\n { ...textNode, width: colW, height: height },\n childId,\n colX(colIndex),\n colY,\n colW,\n height,\n ))\n colY += height\n maxColH = Math.max(maxColH, colY - padding.top)\n }\n\n const computedHeight = innerH !== Infinity ? containerHeight : maxColH + padding.top + padding.bottom\n\n return { records, width: containerWidth, height: computedHeight }\n}\n\nfunction remainingInFirst(available: number, lineHeight: number): number {\n return Math.floor(available / lineHeight)\n}\n", "// LayoutSans \u2014 absolute.ts\n// Absolute positioning solver. Places a node at exact coordinates relative to\n// its containing box. Supports top/right/bottom/left with width/height.\n\nimport type { AbsoluteNode, BoxRecord } from './types.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\n\nexport interface AbsoluteResult {\n records: BoxRecord[]\n width: number\n height: number\n}\n\nexport function solveAbsolute(\n node: AbsoluteNode,\n nodeId: string,\n containerX: number,\n containerY: number,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n): AbsoluteResult {\n const padding = getPaddingBox(node)\n\n // Resolve position from TRBL props\n let x: number\n let y: number\n let w: number\n let h: number\n\n // Width: explicit > (left + right stretch) > 0\n if (node.width !== undefined) {\n w = node.width\n } else if (node.left !== undefined && node.right !== undefined) {\n w = containerWidth - node.left - node.right\n } else {\n w = containerWidth // fallback: fill container\n }\n\n // Height: explicit > (top + bottom stretch) > 0\n if (node.height !== undefined) {\n h = node.height\n } else if (node.top !== undefined && node.bottom !== undefined) {\n h = containerHeight - node.top - node.bottom\n } else {\n h = containerHeight // fallback\n }\n\n // X position\n if (node.left !== undefined) {\n x = containerX + node.left\n } else if (node.right !== undefined) {\n x = containerX + containerWidth - node.right - w\n } else {\n x = containerX\n }\n\n // Y position\n if (node.top !== undefined) {\n y = containerY + node.top\n } else if (node.bottom !== undefined) {\n y = containerY + containerHeight - node.bottom - h\n } else {\n y = containerY\n }\n\n const records: BoxRecord[] = []\n\n // Recurse into children\n const children = node.children ?? []\n const innerW = w - padding.left - padding.right\n const innerH = h - padding.top - padding.bottom\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const childId = `${nodeId}.${i}`\n records.push(...ctx.solveNode(child, childId, x + padding.left, y + padding.top, innerW, innerH))\n }\n\n return { records, width: w, height: h }\n}\n", "// LayoutSans \u2014 engine.ts\n// The main layout orchestrator. Routes each node type to its solver, manages\n// the recursion, and flattens the output into a BoxRecord[].\n\nimport type { Node, BoxRecord, LayoutOptions, FlexNode, GridNode, MagazineNode, AbsoluteNode, TextNode, BoxNode } from './types.js'\nimport { solveFlex, solveFlexColumn } from './flex.js'\nimport { solveGrid } from './grid.js'\nimport { solveMagazine } from './magazine.js'\nimport { solveAbsolute } from './absolute.js'\nimport { measureTextSync } from './measure.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\n\nexport class LayoutEngine {\n private root: Node\n private options: LayoutOptions\n private pretext: typeof import('@chenglou/pretext') | null = null\n\n constructor(root: Node, options: LayoutOptions = {}) {\n this.root = root\n this.options = options\n }\n\n /**\n * Inject a pre-loaded Pretext module for synchronous text measurement.\n * Call this before compute() if you want accurate text sizing.\n */\n usePretext(mod: typeof import('@chenglou/pretext')): this {\n this.pretext = mod\n return this\n }\n\n /**\n * Compute the layout. Returns a flat array of positioned BoxRecords.\n * Each record maps to one node in the input tree via nodeId.\n */\n compute(): BoxRecord[] {\n const rootW = this.options.width ?? this.root.width ?? 0\n const rootH = this.options.height ?? this.root.height ?? 0\n return this.ctx.solveNode(this.root, '0', 0, 0, rootW, rootH)\n }\n\n // \u2500\u2500 Context (bound to this engine instance) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private ctx: SolverContext = {\n solveNode: (node: Node, nodeId: string, x: number, y: number, width: number, height: number): BoxRecord[] => {\n return this.solveNode(node, nodeId, x, y, width, height)\n },\n measureNode: (node: Node, nodeId: string, availableWidth: number, availableHeight: number): { width: number; height: number } => {\n return this.measureNode(node, nodeId, availableWidth, availableHeight)\n },\n }\n\n private solveNode(\n node: Node,\n nodeId: string,\n x: number,\n y: number,\n width: number,\n height: number,\n ): BoxRecord[] {\n const id = node.id ?? nodeId\n\n switch (node.type) {\n case 'flex': {\n // Try the O(n) fast path first (fixed-size column = virtual list case)\n const fast = solveFlexColumn(node as FlexNode, nodeId, width, this.ctx)\n if (fast !== null) {\n const contH = node.height ?? fast.totalHeight\n const container: BoxRecord = { nodeId: id, x, y, width, height: contH }\n const children = fast.records\n // Offset children to container's absolute position in one pass\n for (let i = 0; i < children.length; i++) {\n const r = children[i]!\n children[i] = { nodeId: r.nodeId, x: r.x + x, y: r.y + y, width: r.width, height: r.height }\n }\n return [container, ...children]\n }\n const result = solveFlex(node as FlexNode, nodeId, width, height, this.ctx)\n const self: BoxRecord = { nodeId: id, x, y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'grid': {\n const result = solveGrid(node as GridNode, nodeId, width, height, this.ctx)\n const self: BoxRecord = { nodeId: id, x, y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'magazine': {\n const result = solveMagazine(node as MagazineNode, nodeId, width, height, this.ctx, this.pretext)\n const self: BoxRecord = { nodeId: id, x, y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'absolute': {\n const result = solveAbsolute(node as AbsoluteNode, nodeId, x, y, width, height, this.ctx)\n const self: BoxRecord = { nodeId: id, x: result.records[0]?.x ?? x, y: result.records[0]?.y ?? y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'text': {\n const measured = this.measureNode(node, nodeId, width, height)\n return [{ nodeId: id, x, y, width: measured.width, height: measured.height }]\n }\n\n case 'box':\n default: {\n // A box with no children just occupies its given space\n const boxW = (node as BoxNode).width ?? width\n const boxH = (node as BoxNode).height ?? height\n return [{ nodeId: id, x, y, width: boxW, height: boxH }]\n }\n }\n }\n\n private measureNode(\n node: Node,\n _nodeId: string,\n availableWidth: number,\n availableHeight: number,\n ): { width: number; height: number } {\n switch (node.type) {\n case 'text': {\n const w = node.width ?? availableWidth\n const result = measureTextSync(node as TextNode, w, this.pretext)\n return { width: result.width, height: result.height }\n }\n\n case 'box': {\n return {\n width: node.width ?? availableWidth,\n height: node.height ?? availableHeight,\n }\n }\n\n case 'flex':\n case 'grid':\n case 'magazine':\n case 'absolute': {\n // For measurement purposes, run the full solver and report its size\n const records = this.solveNode(node, 'measure', 0, 0, availableWidth, availableHeight)\n if (records.length === 0) return { width: availableWidth, height: availableHeight }\n const root = records[0]!\n return { width: root.width, height: root.height }\n }\n\n default:\n return { width: availableWidth, height: availableHeight }\n }\n }\n}\n\n/**\n * Create a LayoutEngine for a node tree.\n * Call .compute() to get the flat BoxRecord[].\n *\n * @example\n * const engine = createLayout(root)\n * const boxes = engine.compute()\n */\nexport function createLayout(root: Node, options?: LayoutOptions): LayoutEngine {\n return new LayoutEngine(root, options)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["index_exports", "__export", "LayoutEngine", "createLayout", "__toCommonJS", "getPaddingBox", "node", "base", "resolveNodeSize", "axis", "_containerSize", "val", "clampSize", "value", "min", "max", "solveFlex", "node", "nodeId", "containerWidth", "containerHeight", "ctx", "isRow", "wrap", "padding", "getPaddingBox", "innerWidth", "innerHeight", "mainGap", "crossGap", "layouts", "child", "i", "childId", "childW", "resolveNodeSize", "childH", "marginMain", "marginCross", "flexBasis", "mainFixed", "crossFixed", "mainSize", "buildLines", "mainContainerSize", "lines", "currentLine", "currentFixed", "currentGaps", "flexTotal", "item", "itemMain", "gapAdd", "line", "gapTotal", "fixedTotal", "freeSpace", "totalGrow", "clampSize", "totalShrinkWeight", "shrink", "crossContainerSize", "lineCrossMax", "alignItems", "measured", "records", "computeJustify", "usedMain", "free", "justify", "n", "computeAlignCross", "lineCrossSize", "alignSelf", "align", "crossOffset", "start", "spacing", "mainOffset", "marginMainStart", "marginCrossStart", "crossPos", "x", "y", "w", "h", "childRecords", "totalCross", "sum", "l", "solveFlexColumn", "children", "gap", "innerW", "id", "totalHeight", "solveGrid", "node", "nodeId", "containerWidth", "containerHeight", "ctx", "padding", "getPaddingBox", "innerW", "innerH", "children", "colGap", "rowGap", "records", "cols", "cellW", "rows", "cellH", "row", "col", "idx", "child", "measured", "i", "x", "y", "childId", "totalH", "estimateFontSize", "font", "match", "value", "unit", "measureTextSync", "node", "maxWidth", "pretext", "lineHeight", "estimateFontSize", "result", "prepared", "avgCharWidth", "charsPerLine", "lines", "solveMagazine", "node", "nodeId", "containerWidth", "containerHeight", "ctx", "pretext", "padding", "getPaddingBox", "innerW", "innerH", "cols", "colGap", "colW", "records", "textNodes", "baseNode", "lineHeight", "estimateFontSize", "totalContentHeight", "measuredNodes", "textNode", "measured", "measureTextSync", "targetColH", "colIndex", "colY", "colX", "c", "maxColH", "ni", "height", "childId", "remainingInCol", "h1", "remainingInFirst", "h2", "computedHeight", "available", "solveAbsolute", "node", "nodeId", "containerX", "containerY", "containerWidth", "containerHeight", "ctx", "padding", "getPaddingBox", "x", "y", "w", "h", "records", "children", "innerW", "innerH", "i", "child", "childId", "LayoutEngine", "root", "options", "node", "nodeId", "x", "y", "width", "height", "availableWidth", "availableHeight", "mod", "rootW", "rootH", "id", "fast", "solveFlexColumn", "contH", "container", "children", "i", "r", "result", "solveFlex", "solveGrid", "solveMagazine", "solveAbsolute", "measured", "boxW", "boxH", "_nodeId", "w", "measureTextSync", "records", "createLayout"]
|
|
7
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function P(t){let o=t.padding??0;return{top:t.paddingTop??o,right:t.paddingRight??o,bottom:t.paddingBottom??o,left:t.paddingLeft??o}}function O(t,o,i){let s=t[o];return typeof s=="number"?s:NaN}function _(t,o,i){return o!==void 0&&t<o?o:i!==void 0&&t>i?i:t}function E(t,o,i,s,a){let e=(t.direction??"row")==="row",N=t.wrap??!1,n=P(t),u=i-n.left-n.right,x=s-n.top-n.bottom,c=e?t.columnGap??t.gap??0:t.rowGap??t.gap??0,M=e?t.rowGap??t.gap??0:t.columnGap??t.gap??0,f=(t.children??[]).map((r,v)=>{let b=`${o}.${v}`,d=O(r,"width",u),h=O(r,"height",x),S=e?(r.marginLeft??r.margin??0)+(r.marginRight??r.margin??0):(r.marginTop??r.margin??0)+(r.marginBottom??r.margin??0),G=e?(r.marginTop??r.margin??0)+(r.marginBottom??r.margin??0):(r.marginLeft??r.margin??0)+(r.marginRight??r.margin??0),T=r.flexBasis??NaN,m=e?d:h,C=e?h:d,$;return isNaN(T)?isNaN(m)?(r.flex??0)>0?$=NaN:$=e?d??0:h??0:$=m:$=T,{child:r,id:b,mainSize:$,crossSize:C,flexGrow:r.flex??0,flexShrink:r.flexShrink??1,flexBasis:T,marginMain:S,marginCross:G}});function w(){let r=e?u:x,v=[],b=[],d=0,h=0,S=0;for(let G=0;G<f.length;G++){let T=f[G],m=isNaN(T.mainSize)?0:T.mainSize,C=b.length>0?c:0;N&&b.length>0&&d+C+m+T.marginMain>r&&(v.push({items:b,totalFixed:d,totalFlexGrow:S,crossSize:0}),b=[],d=0,h=0,S=0),b.push(T),d+=m+T.marginMain+(b.length>1?c:0),S+=T.flexGrow}return b.length>0&&v.push({items:b,totalFixed:d,totalFlexGrow:S,crossSize:0}),v}let g=w(),y=e?u:x;for(let r of g){let v=(r.items.length-1)*c,b=0;for(let m of r.items)b+=isNaN(m.mainSize)?0:m.mainSize+m.marginMain;let d=y-b-v,h=r.totalFlexGrow;if(h>0&&d>0)for(let m of r.items)m.flexGrow>0&&(m.mainSize=m.flexGrow/h*d,m.mainSize=_(m.mainSize,e?m.child.minWidth:m.child.minHeight,e?m.child.maxWidth:m.child.maxHeight));else if(h===0&&d<0){let m=0;for(let C of r.items)isNaN(C.mainSize)||(m+=C.flexShrink*C.mainSize);if(m>0){for(let C of r.items)if(C.flexShrink>0&&!isNaN(C.mainSize)){let $=C.flexShrink*C.mainSize/m*Math.abs(d);C.mainSize=Math.max(0,C.mainSize-$)}}}let S=e?x:u,G=0,T=t.alignItems??"stretch";for(let m of r.items){if(isNaN(m.crossSize))if(T==="stretch")m.crossSize=S-m.marginCross;else{let C=a.measureNode(m.child,m.id,e?m.mainSize:u,e?x:m.mainSize);m.crossSize=e?C.height:C.width}G=Math.max(G,m.crossSize+m.marginCross)}r.crossSize=G}let z=[];function p(r){let b=(r.items.length-1)*c;for(let G of r.items)b+=(isNaN(G.mainSize)?0:G.mainSize)+G.marginMain;let d=y-b,h=t.justifyContent??"flex-start",S=r.items.length;switch(h){case"center":return{start:d/2,spacing:0};case"flex-end":return{start:d,spacing:0};case"space-between":return{start:0,spacing:S>1?d/(S-1):0};case"space-around":return{start:d/(2*S),spacing:d/S};case"space-evenly":return{start:d/(S+1),spacing:d/(S+1)};default:return{start:0,spacing:0}}}function L(r,v){let b=r.child.alignSelf,d=b&&b!=="auto"?b:t.alignItems??"stretch",h=v-r.crossSize-r.marginCross;switch(d){case"center":return h/2;case"flex-end":return h;default:return 0}}let B=e?n.top:n.left;for(let r of g){let{start:v,spacing:b}=p(r),d=(e?n.left:n.top)+v;for(let h of r.items){let S=e?h.child.marginLeft??h.child.margin??0:h.child.marginTop??h.child.margin??0,G=e?h.child.marginTop??h.child.margin??0:h.child.marginLeft??h.child.margin??0;d+=S;let T=B+G+L(h,r.crossSize),m=e?d:T,C=e?T:d,$=e?h.mainSize:h.crossSize,Q=e?h.crossSize:h.mainSize,U=a.solveNode(h.child,h.id,m,C,$,Q);z.push(...U),d+=(isNaN(h.mainSize)?0:h.mainSize)+c+b+(h.marginMain-S)}B+=r.crossSize+M}let I=g.reduce((r,v)=>r+v.crossSize,0)+(g.length-1)*M+(e?n.top+n.bottom:n.left+n.right);return{records:z,width:e?i:I,height:e?I:s}}function J(t,o,i,s){if((t.direction??"row")!=="column"||t.wrap||t.justifyContent&&t.justifyContent!=="flex-start")return null;let a=t.children??[],l=t.gap??t.rowGap??0,e=P(t),N=i-e.left-e.right;for(let c of a)if(c.type!=="box"||(c.flex??0)>0||c.margin!==void 0||c.marginTop!==void 0||c.marginBottom!==void 0||c.width===void 0||c.height===void 0)return null;let n=new Array(a.length),u=e.top;for(let c=0;c<a.length;c++){let M=a[c],F=`${o}.${c}`,f=M.id??F,w=M.width,g=M.height;n[c]={nodeId:f,x:e.left,y:u,width:w,height:g},u+=g+l}let x=a.length>0?u-l+e.bottom:e.top+e.bottom;return{records:n,totalHeight:x}}function q(t,o,i,s,a){let l=P(t),e=i-l.left-l.right,N=s-l.top-l.bottom,n=t.children??[],u=t.columnGap??t.gap??0,x=t.rowGap??t.gap??0,c=[];if(t.columns!==void 0){let f=t.columns,w=(e-u*(f-1))/f,g=Math.ceil(n.length/f),y;if(t.height!==void 0)y=(N-x*(g-1))/g;else{y=0;for(let p=0;p<g;p++)for(let L=0;L<f;L++){let B=p*f+L;if(B>=n.length)break;let I=n[B],R=a.measureNode(I,`${o}.${B}`,w,1/0);y=Math.max(y,R.height)}}for(let p=0;p<n.length;p++){let L=n[p],B=p%f,I=Math.floor(p/f),R=l.left+B*(w+u),H=l.top+I*(y+x),r=`${o}.${p}`;c.push(...a.solveNode(L,r,R,H,w,y))}let z=g*y+(g-1)*x+l.top+l.bottom;return{records:c,width:i,height:t.height??z}}if(t.rows!==void 0){let f=t.rows,w=(N-x*(f-1))/f,g=Math.ceil(n.length/f),y=(e-u*(g-1))/g;for(let z=0;z<n.length;z++){let p=n[z],L=z%f,B=Math.floor(z/f),I=l.left+B*(y+u),R=l.top+L*(w+x),H=`${o}.${z}`;c.push(...a.solveNode(p,H,I,R,y,w))}return{records:c,width:i,height:s}}let M=l.top;for(let f=0;f<n.length;f++){let w=n[f],g=`${o}.${f}`,y=a.measureNode(w,g,e,1/0);c.push(...a.solveNode(w,g,l.left,M,e,y.height)),M+=y.height+x}let F=M-x+l.bottom;return{records:c,width:i,height:F}}function A(t){let o=/(\d+(?:\.\d+)?)(px|rem|em|pt)?/.exec(t);if(!o)return 16;let i=parseFloat(o[1]),s=o[2]??"px";return s==="rem"||s==="em"?i*16:s==="pt"?i*1.333:i}function j(t,o,i){let s=t.lineHeight??A(t.font??"16px sans-serif")*1.4;if(t.preparedText&&i){let n=i.layout(t.preparedText,o,s);return{width:o,height:n.height}}if(t.font&&i){let n=i.prepare(t.content??"",t.font),u=i.layout(n,o,s);return{width:o,height:u.height}}let l=A(t.font??"16px sans-serif")*.55,e=Math.floor(o/Math.max(l,1)),N=Math.ceil((t.content?.length??0)/Math.max(e,1));return{width:o,height:Math.max(N,1)*s}}function D(t,o,i,s,a,l){let e=P(t),N=i-e.left-e.right,n=s!==1/0?s-e.top-e.bottom:1/0,u=t.columnCount,x=t.columnGap??16,c=(N-x*(u-1))/u,M=[],F;if(t.content&&!t.children?.length){let R={type:"text",content:t.content,width:c};t.font!==void 0&&(R.font=t.font),t.lineHeight!==void 0&&(R.lineHeight=t.lineHeight),F=[R]}else F=t.children??[];let f=t.lineHeight??A(t.font??"16px sans-serif")*1.4,w=0,g=[];for(let R of F){let H=j({...R,width:c},c,l);g.push({node:R,height:H.height}),w+=H.height}let y=n!==1/0?n:w/u,z=0,p=e.top,L=R=>e.left+R*(c+x),B=0;for(let R=0;R<g.length;R++){let{node:H,height:r}=g[R],v=`${o}.${R}`;if(z<u-1&&p-e.top+r>y){let b=y-(p-e.top);if(b<f)B=Math.max(B,p-e.top),z++,p=e.top;else{let h=Math.floor(V(b,f))*f;M.push({nodeId:`${v}.part0`,x:L(z),y:p,width:c,height:h}),B=Math.max(B,p-e.top+h),z++,p=e.top;let S=r-h;S>0&&z<u&&(M.push({nodeId:`${v}.part1`,x:L(z),y:p,width:c,height:S}),p+=S,B=Math.max(B,p-e.top));continue}}M.push(...a.solveNode({...H,width:c,height:r},v,L(z),p,c,r)),p+=r,B=Math.max(B,p-e.top)}let I=n!==1/0?s:B+e.top+e.bottom;return{records:M,width:i,height:I}}function V(t,o){return Math.floor(t/o)}function K(t,o,i,s,a,l,e){let N=P(t),n,u,x,c;t.width!==void 0?x=t.width:t.left!==void 0&&t.right!==void 0?x=a-t.left-t.right:x=a,t.height!==void 0?c=t.height:t.top!==void 0&&t.bottom!==void 0?c=l-t.top-t.bottom:c=l,t.left!==void 0?n=i+t.left:t.right!==void 0?n=i+a-t.right-x:n=i,t.top!==void 0?u=s+t.top:t.bottom!==void 0?u=s+l-t.bottom-c:u=s;let M=[],F=t.children??[],f=x-N.left-N.right,w=c-N.top-N.bottom;for(let g=0;g<F.length;g++){let y=F[g],z=`${o}.${g}`;M.push(...e.solveNode(y,z,n+N.left,u+N.top,f,w))}return{records:M,width:x,height:c}}var k=class{constructor(o,i={}){this.pretext=null;this.ctx={solveNode:(o,i,s,a,l,e)=>this.solveNode(o,i,s,a,l,e),measureNode:(o,i,s,a)=>this.measureNode(o,i,s,a)};this.root=o,this.options=i}usePretext(o){return this.pretext=o,this}compute(){let o=this.options.width??this.root.width??0,i=this.options.height??this.root.height??0;return this.ctx.solveNode(this.root,"0",0,0,o,i)}solveNode(o,i,s,a,l,e){let N=o.id??i;switch(o.type){case"flex":{let n=J(o,i,l,this.ctx);if(n!==null){let c=o.height??n.totalHeight,M={nodeId:N,x:s,y:a,width:l,height:c},F=n.records;for(let f=0;f<F.length;f++){let w=F[f];F[f]={nodeId:w.nodeId,x:w.x+s,y:w.y+a,width:w.width,height:w.height}}return[M,...F]}let u=E(o,i,l,e,this.ctx);return[{nodeId:N,x:s,y:a,width:u.width,height:u.height},...u.records]}case"grid":{let n=q(o,i,l,e,this.ctx);return[{nodeId:N,x:s,y:a,width:n.width,height:n.height},...n.records]}case"magazine":{let n=D(o,i,l,e,this.ctx,this.pretext);return[{nodeId:N,x:s,y:a,width:n.width,height:n.height},...n.records]}case"absolute":{let n=K(o,i,s,a,l,e,this.ctx);return[{nodeId:N,x:n.records[0]?.x??s,y:n.records[0]?.y??a,width:n.width,height:n.height},...n.records]}case"text":{let n=this.measureNode(o,i,l,e);return[{nodeId:N,x:s,y:a,width:n.width,height:n.height}]}case"box":default:{let n=o.width??l,u=o.height??e;return[{nodeId:N,x:s,y:a,width:n,height:u}]}}}measureNode(o,i,s,a){switch(o.type){case"text":{let l=o.width??s,e=j(o,l,this.pretext);return{width:e.width,height:e.height}}case"box":return{width:o.width??s,height:o.height??a};case"flex":case"grid":case"magazine":case"absolute":{let l=this.solveNode(o,"measure",0,0,s,a);if(l.length===0)return{width:s,height:a};let e=l[0];return{width:e.width,height:e.height}}default:return{width:s,height:a}}}};function Z(t,o){return new k(t,o)}export{k as LayoutEngine,Z as createLayout};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/utils.ts", "../src/flex.ts", "../src/grid.ts", "../src/measure.ts", "../src/magazine.ts", "../src/absolute.ts", "../src/engine.ts"],
|
|
4
|
+
"sourcesContent": ["// LayoutSans \u2014 utils.ts\n// Shared helpers: size resolution, clamping, padding extraction, solver context.\n\nimport type { Node, BoxRecord } from './types.js'\n\n// \u2500\u2500\u2500 Padding helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface PaddingBox {\n top: number\n right: number\n bottom: number\n left: number\n}\n\nexport function getPaddingBox(node: Node): PaddingBox {\n const base = (node as { padding?: number }).padding ?? 0\n return {\n top: (node as { paddingTop?: number }).paddingTop ?? base,\n right: (node as { paddingRight?: number }).paddingRight ?? base,\n bottom: (node as { paddingBottom?: number }).paddingBottom ?? base,\n left: (node as { paddingLeft?: number }).paddingLeft ?? base,\n }\n}\n\n// \u2500\u2500\u2500 Size resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Read width or height from a node, returning NaN if not fixed. */\nexport function resolveNodeSize(\n node: Node,\n axis: 'width' | 'height',\n _containerSize: number,\n): number {\n const val = (node as unknown as Record<string, unknown>)[axis]\n return typeof val === 'number' ? val : NaN\n}\n\n/** Clamp a value between optional min/max. */\nexport function clampSize(value: number, min?: number, max?: number): number {\n if (min !== undefined && value < min) return min\n if (max !== undefined && value > max) return max\n return value\n}\n\n// \u2500\u2500\u2500 Solver context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The engine passes a SolverContext into each solver so they can recursively\n * lay out child nodes without creating circular imports.\n */\nexport interface SolverContext {\n /**\n * Recursively solve a child node at the given position and size.\n * Returns the flat list of BoxRecords produced by that subtree.\n */\n solveNode(\n node: Node,\n nodeId: string,\n x: number,\n y: number,\n width: number,\n height: number,\n ): BoxRecord[]\n\n /**\n * Measure a node to get its intrinsic size without fully solving it.\n * Used by flex cross-axis content-sizing.\n */\n measureNode(\n node: Node,\n nodeId: string,\n availableWidth: number,\n availableHeight: number,\n ): { width: number; height: number }\n}\n", "// LayoutSans \u2014 flex.ts\n// Pure TypeScript flexbox solver. Implements the subset of CSS Flexbox needed\n// for UI layout: row/column, flex-grow, flex-shrink, gap, align, justify, wrap.\n//\n// Algorithm (3 passes):\n// 1. Measure \u2014 resolve fixed sizes, collect flex-grow totals\n// 2. Distribute \u2014 divide remaining space among flex children\n// 3. Position \u2014 assign x/y offsets based on alignment\n\nimport type { FlexNode, BoxRecord, Node } from './types.js'\nimport { resolveNodeSize, clampSize, getPaddingBox, type SolverContext } from './utils.js'\n\nexport interface FlexResult {\n records: BoxRecord[]\n /** Computed container width (useful when container is content-sized). */\n width: number\n /** Computed container height. */\n height: number\n}\n\nexport function solveFlex(\n node: FlexNode,\n nodeId: string,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n): FlexResult {\n const direction = node.direction ?? 'row'\n const isRow = direction === 'row'\n const wrap = node.wrap ?? false\n\n // \u2500\u2500 Padding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const padding = getPaddingBox(node)\n const innerWidth = containerWidth - padding.left - padding.right\n const innerHeight = containerHeight - padding.top - padding.bottom\n\n // \u2500\u2500 Gap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const mainGap = isRow ? (node.columnGap ?? node.gap ?? 0) : (node.rowGap ?? node.gap ?? 0)\n const crossGap = isRow ? (node.rowGap ?? node.gap ?? 0) : (node.columnGap ?? node.gap ?? 0)\n\n const children = node.children ?? []\n\n // \u2500\u2500 Pass 1: resolve fixed sizes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n interface ChildLayout {\n child: Node\n id: string\n mainSize: number // NaN = flex-grow pending\n crossSize: number // NaN = stretch pending\n flexGrow: number\n flexShrink: number\n flexBasis: number // NaN = auto\n marginMain: number\n marginCross: number\n }\n\n const layouts: ChildLayout[] = children.map((child, i) => {\n const childId = `${nodeId}.${i}`\n const childW = resolveNodeSize(child, 'width', innerWidth)\n const childH = resolveNodeSize(child, 'height', innerHeight)\n\n const marginMain = isRow\n ? ((child.marginLeft ?? child.margin ?? 0) + (child.marginRight ?? child.margin ?? 0))\n : ((child.marginTop ?? child.margin ?? 0) + (child.marginBottom ?? child.margin ?? 0))\n\n const marginCross = isRow\n ? ((child.marginTop ?? child.margin ?? 0) + (child.marginBottom ?? child.margin ?? 0))\n : ((child.marginLeft ?? child.margin ?? 0) + (child.marginRight ?? child.margin ?? 0))\n\n const flexBasis = child.flexBasis ?? NaN\n const mainFixed = isRow ? childW : childH\n const crossFixed = isRow ? childH : childW\n\n // Main size: flexBasis > fixed dimension > NaN (grows)\n let mainSize: number\n if (!isNaN(flexBasis)) {\n mainSize = flexBasis\n } else if (!isNaN(mainFixed)) {\n mainSize = mainFixed\n } else if ((child.flex ?? 0) > 0) {\n mainSize = NaN // will be filled in pass 2\n } else {\n // Content-sized \u2014 we'll measure in ctx\n mainSize = isRow ? (childW ?? 0) : (childH ?? 0)\n }\n\n return {\n child,\n id: childId,\n mainSize,\n crossSize: crossFixed,\n flexGrow: child.flex ?? 0,\n flexShrink: child.flexShrink ?? 1,\n flexBasis,\n marginMain,\n marginCross,\n }\n })\n\n // \u2500\u2500 Wrap: split children into lines \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n interface FlexLine {\n items: ChildLayout[]\n totalFixed: number\n totalFlexGrow: number\n crossSize: number\n }\n\n function buildLines(): FlexLine[] {\n const mainContainerSize = isRow ? innerWidth : innerHeight\n const lines: FlexLine[] = []\n let currentLine: ChildLayout[] = []\n let currentFixed = 0\n let currentGaps = 0\n let flexTotal = 0\n\n for (let i = 0; i < layouts.length; i++) {\n const item = layouts[i]!\n const itemMain = isNaN(item.mainSize) ? 0 : item.mainSize\n const gapAdd = currentLine.length > 0 ? mainGap : 0\n\n if (wrap && currentLine.length > 0 && currentFixed + gapAdd + itemMain + item.marginMain > mainContainerSize) {\n lines.push({ items: currentLine, totalFixed: currentFixed, totalFlexGrow: flexTotal, crossSize: 0 })\n currentLine = []\n currentFixed = 0\n currentGaps = 0\n flexTotal = 0\n }\n\n currentLine.push(item)\n currentFixed += itemMain + item.marginMain + (currentLine.length > 1 ? mainGap : 0)\n flexTotal += item.flexGrow\n }\n\n if (currentLine.length > 0) {\n lines.push({ items: currentLine, totalFixed: currentFixed, totalFlexGrow: flexTotal, crossSize: 0 })\n }\n\n return lines\n }\n\n const lines = buildLines()\n\n // \u2500\u2500 Pass 2: distribute flex space within each line \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const mainContainerSize = isRow ? innerWidth : innerHeight\n\n for (const line of lines) {\n const gapTotal = (line.items.length - 1) * mainGap\n let fixedTotal = 0\n for (const item of line.items) {\n fixedTotal += isNaN(item.mainSize) ? 0 : (item.mainSize + item.marginMain)\n }\n const freeSpace = mainContainerSize - fixedTotal - gapTotal\n const totalGrow = line.totalFlexGrow\n\n if (totalGrow > 0 && freeSpace > 0) {\n for (const item of line.items) {\n if (item.flexGrow > 0) {\n item.mainSize = (item.flexGrow / totalGrow) * freeSpace\n item.mainSize = clampSize(item.mainSize, isRow ? item.child.minWidth : item.child.minHeight, isRow ? item.child.maxWidth : item.child.maxHeight)\n }\n }\n } else if (totalGrow === 0 && freeSpace < 0) {\n // Shrink pass\n let totalShrinkWeight = 0\n for (const item of line.items) {\n if (!isNaN(item.mainSize)) totalShrinkWeight += item.flexShrink * item.mainSize\n }\n if (totalShrinkWeight > 0) {\n for (const item of line.items) {\n if (item.flexShrink > 0 && !isNaN(item.mainSize)) {\n const shrink = (item.flexShrink * item.mainSize / totalShrinkWeight) * Math.abs(freeSpace)\n item.mainSize = Math.max(0, item.mainSize - shrink)\n }\n }\n }\n }\n\n // Resolve cross sizes \u2014 stretch to fill or use fixed\n const crossContainerSize = isRow ? innerHeight : innerWidth\n let lineCrossMax = 0\n const alignItems = node.alignItems ?? 'stretch'\n\n for (const item of line.items) {\n if (isNaN(item.crossSize)) {\n if (alignItems === 'stretch') {\n item.crossSize = crossContainerSize - item.marginCross\n } else {\n // Content-sized cross axis \u2014 measure via ctx\n const measured = ctx.measureNode(item.child, item.id, isRow ? item.mainSize : innerWidth, isRow ? innerHeight : item.mainSize)\n item.crossSize = isRow ? measured.height : measured.width\n }\n }\n lineCrossMax = Math.max(lineCrossMax, item.crossSize + item.marginCross)\n }\n line.crossSize = lineCrossMax\n }\n\n // \u2500\u2500 Pass 3: position children \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const records: BoxRecord[] = []\n\n // Justify-content: compute initial main offset and per-item spacing\n function computeJustify(line: FlexLine): { start: number; spacing: number } {\n const gapTotal = (line.items.length - 1) * mainGap\n let usedMain = gapTotal\n for (const item of line.items) {\n usedMain += (isNaN(item.mainSize) ? 0 : item.mainSize) + item.marginMain\n }\n const free = mainContainerSize - usedMain\n const justify = node.justifyContent ?? 'flex-start'\n const n = line.items.length\n\n switch (justify) {\n case 'center': return { start: free / 2, spacing: 0 }\n case 'flex-end': return { start: free, spacing: 0 }\n case 'space-between': return { start: 0, spacing: n > 1 ? free / (n - 1) : 0 }\n case 'space-around': return { start: free / (2 * n), spacing: free / n }\n case 'space-evenly': return { start: free / (n + 1), spacing: free / (n + 1) }\n default: return { start: 0, spacing: 0 } // flex-start\n }\n }\n\n // Cross-axis start for align-items\n function computeAlignCross(item: ChildLayout, lineCrossSize: number): number {\n const alignSelf = (item.child as FlexNode).alignSelf\n const align = alignSelf && alignSelf !== 'auto' ? alignSelf : (node.alignItems ?? 'stretch')\n const free = lineCrossSize - item.crossSize - item.marginCross\n switch (align) {\n case 'center': return free / 2\n case 'flex-end': return free\n default: return 0 // flex-start / stretch\n }\n }\n\n let crossOffset = isRow ? padding.top : padding.left\n\n for (const line of lines) {\n const { start, spacing } = computeJustify(line)\n let mainOffset = (isRow ? padding.left : padding.top) + start\n\n for (const item of line.items) {\n const marginMainStart = isRow ? (item.child.marginLeft ?? item.child.margin ?? 0) : (item.child.marginTop ?? item.child.margin ?? 0)\n const marginCrossStart = isRow ? (item.child.marginTop ?? item.child.margin ?? 0) : (item.child.marginLeft ?? item.child.margin ?? 0)\n\n mainOffset += marginMainStart\n\n const crossPos = crossOffset + marginCrossStart + computeAlignCross(item, line.crossSize)\n\n const x = isRow ? mainOffset : crossPos\n const y = isRow ? crossPos : mainOffset\n const w = isRow ? item.mainSize : item.crossSize\n const h = isRow ? item.crossSize : item.mainSize\n\n // Recurse into the child solver\n const childRecords = ctx.solveNode(item.child, item.id, x, y, w, h)\n records.push(...childRecords)\n\n mainOffset += (isNaN(item.mainSize) ? 0 : item.mainSize) + mainGap + spacing + (item.marginMain - marginMainStart)\n }\n\n crossOffset += line.crossSize + crossGap\n }\n\n // Container self record\n const totalCross = lines.reduce((sum, l) => sum + l.crossSize, 0) + (lines.length - 1) * crossGap + (isRow ? padding.top + padding.bottom : padding.left + padding.right)\n const computedWidth = isRow ? containerWidth : totalCross\n const computedHeight = isRow ? totalCross : containerHeight\n\n return { records, width: computedWidth, height: computedHeight }\n}\n\n// \u2500\u2500 Fast path: fixed-size column of box nodes (virtual list hot case) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Detects when all children are plain 'box' nodes with explicit width+height,\n// no flex-grow, no margins, and no wrapping \u2014 skips the full 3-pass algorithm\n// and emits records in a single O(n) loop with zero intermediate objects.\nexport function solveFlexColumn(\n node: FlexNode,\n nodeId: string,\n containerWidth: number,\n ctx: SolverContext,\n): { records: BoxRecord[]; totalHeight: number } | null {\n if ((node.direction ?? 'row') !== 'column') return null\n if (node.wrap) return null\n if (node.justifyContent && node.justifyContent !== 'flex-start') return null\n\n const children = node.children ?? []\n const gap = node.gap ?? node.rowGap ?? 0\n const padding = getPaddingBox(node)\n const innerW = containerWidth - padding.left - padding.right\n\n // Validate all children are fixed-size boxes with no flex growth\n for (const child of children) {\n if (child.type !== 'box') return null\n if ((child.flex ?? 0) > 0) return null\n if (child.margin !== undefined || child.marginTop !== undefined || child.marginBottom !== undefined) return null\n if (child.width === undefined || child.height === undefined) return null\n }\n\n // Fast O(n) emit\n const records: BoxRecord[] = new Array(children.length)\n let y = padding.top\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const childId = `${nodeId}.${i}`\n const id = child.id ?? childId\n const w = child.width!\n const h = child.height!\n records[i] = { nodeId: id, x: padding.left, y, width: w, height: h }\n y += h + gap\n }\n\n // Remove the last gap, add bottom padding \u2014 y is now total content height\n const totalHeight = children.length > 0\n ? y - gap + padding.bottom\n : padding.top + padding.bottom\n\n return { records, totalHeight }\n}\n", "// LayoutSans \u2014 grid.ts\n// Basic 1D grid layout. Divides available space into equal-size cells.\n// For MVP: uniform columns OR uniform rows (not full CSS Grid template).\n\nimport type { GridNode, BoxRecord } from './types.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\n\nexport interface GridResult {\n records: BoxRecord[]\n width: number\n height: number\n}\n\nexport function solveGrid(\n node: GridNode,\n nodeId: string,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n): GridResult {\n const padding = getPaddingBox(node)\n const innerW = containerWidth - padding.left - padding.right\n const innerH = containerHeight - padding.top - padding.bottom\n\n const children = node.children ?? []\n const colGap = node.columnGap ?? node.gap ?? 0\n const rowGap = node.rowGap ?? node.gap ?? 0\n\n const records: BoxRecord[] = []\n\n // \u2500\u2500 Column-based grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (node.columns !== undefined) {\n const cols = node.columns\n const cellW = (innerW - colGap * (cols - 1)) / cols\n const rows = Math.ceil(children.length / cols)\n\n let cellH: number\n if (node.height !== undefined) {\n cellH = (innerH - rowGap * (rows - 1)) / rows\n } else {\n // Content-sized rows: measure each row's tallest child\n cellH = 0\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < cols; col++) {\n const idx = row * cols + col\n if (idx >= children.length) break\n const child = children[idx]!\n const measured = ctx.measureNode(child, `${nodeId}.${idx}`, cellW, Infinity)\n cellH = Math.max(cellH, measured.height)\n }\n }\n }\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const col = i % cols\n const row = Math.floor(i / cols)\n const x = padding.left + col * (cellW + colGap)\n const y = padding.top + row * (cellH + rowGap)\n const childId = `${nodeId}.${i}`\n records.push(...ctx.solveNode(child, childId, x, y, cellW, cellH))\n }\n\n const totalH = rows * cellH + (rows - 1) * rowGap + padding.top + padding.bottom\n return { records, width: containerWidth, height: node.height ?? totalH }\n }\n\n // \u2500\u2500 Row-based grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (node.rows !== undefined) {\n const rows = node.rows\n const cellH = (innerH - rowGap * (rows - 1)) / rows\n const cols = Math.ceil(children.length / rows)\n const cellW = (innerW - colGap * (cols - 1)) / cols\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const row = i % rows\n const col = Math.floor(i / rows)\n const x = padding.left + col * (cellW + colGap)\n const y = padding.top + row * (cellH + rowGap)\n const childId = `${nodeId}.${i}`\n records.push(...ctx.solveNode(child, childId, x, y, cellW, cellH))\n }\n\n return { records, width: containerWidth, height: containerHeight }\n }\n\n // \u2500\u2500 Fallback: single column (equivalent to flex column, no grow) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let y = padding.top\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const childId = `${nodeId}.${i}`\n const measured = ctx.measureNode(child, childId, innerW, Infinity)\n records.push(...ctx.solveNode(child, childId, padding.left, y, innerW, measured.height))\n y += measured.height + rowGap\n }\n\n const totalH = y - rowGap + padding.bottom\n return { records, width: containerWidth, height: totalH }\n}\n", "// LayoutSans \u2014 measure.ts\n// Integrates with @chenglou/pretext to size text nodes.\n// Falls back gracefully when Pretext is not available (e.g. in pure-math scenarios).\n\nimport type { TextNode } from './types.js'\n\n// Dynamic import so the library doesn't hard-error when Pretext is absent.\nlet pretextModule: typeof import('@chenglou/pretext') | null = null\n\nasync function getPretextModule(): Promise<typeof import('@chenglou/pretext') | null> {\n if (pretextModule !== null) return pretextModule\n try {\n pretextModule = await import('@chenglou/pretext')\n return pretextModule\n } catch {\n return null\n }\n}\n\n/**\n * Estimate font size from a CSS font string like '16px Inter' or '1rem Arial'.\n * Used to derive a default lineHeight when none is supplied.\n */\nexport function estimateFontSize(font: string): number {\n // Match leading number with optional unit: '16px', '1.5rem', '24'\n const match = /(\\d+(?:\\.\\d+)?)(px|rem|em|pt)?/.exec(font)\n if (!match) return 16\n const value = parseFloat(match[1]!)\n const unit = match[2] ?? 'px'\n if (unit === 'rem' || unit === 'em') return value * 16\n if (unit === 'pt') return value * 1.333\n return value\n}\n\nexport interface MeasureTextResult {\n width: number\n height: number\n}\n\n/**\n * Measure a text node's intrinsic size using Pretext.\n * maxWidth constrains line wrapping. Returns { width, height }.\n */\nexport async function measureText(\n node: TextNode,\n maxWidth: number,\n): Promise<MeasureTextResult> {\n const lineHeight = node.lineHeight ?? estimateFontSize(node.font ?? '16px sans-serif') * 1.4\n\n // Caller already has a PreparedText handle (pre-prepared by user)\n if (node.preparedText) {\n const pretext = await getPretextModule()\n if (pretext) {\n const result = pretext.layout(node.preparedText, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n }\n // Graceful fallback: can't measure without pretext\n return { width: maxWidth, height: lineHeight }\n }\n\n if (!node.font) {\n // No font, no preparedText: estimate from character count\n const charsPerLine = Math.floor(maxWidth / (estimateFontSize('16px') * 0.55))\n const lines = Math.ceil((node.content?.length ?? 0) / Math.max(charsPerLine, 1))\n return { width: maxWidth, height: Math.max(lines, 1) * lineHeight }\n }\n\n const pretext = await getPretextModule()\n if (!pretext) {\n // Pretext not installed \u2014 rough fallback via average char width\n const fontSize = estimateFontSize(node.font)\n const avgCharWidth = fontSize * 0.55\n const charsPerLine = Math.floor(maxWidth / avgCharWidth)\n const lines = Math.ceil((node.content?.length ?? 0) / Math.max(charsPerLine, 1))\n return { width: maxWidth, height: Math.max(lines, 1) * lineHeight }\n }\n\n const prepared = pretext.prepare(node.content ?? '', node.font)\n const result = pretext.layout(prepared, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n}\n\n/**\n * Synchronous version for environments where Pretext has already been loaded.\n * Uses the node's preparedText handle if available.\n */\nexport function measureTextSync(\n node: TextNode,\n maxWidth: number,\n pretext: typeof import('@chenglou/pretext') | null,\n): MeasureTextResult {\n const lineHeight = node.lineHeight ?? estimateFontSize(node.font ?? '16px sans-serif') * 1.4\n\n if (node.preparedText && pretext) {\n const result = pretext.layout(node.preparedText, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n }\n\n if (node.font && pretext) {\n const prepared = pretext.prepare(node.content ?? '', node.font)\n const result = pretext.layout(prepared, maxWidth, lineHeight)\n return { width: maxWidth, height: result.height }\n }\n\n // Fallback: character-count heuristic\n const fontSize = estimateFontSize(node.font ?? '16px sans-serif')\n const avgCharWidth = fontSize * 0.55\n const charsPerLine = Math.floor(maxWidth / Math.max(avgCharWidth, 1))\n const lines = Math.ceil((node.content?.length ?? 0) / Math.max(charsPerLine, 1))\n return { width: maxWidth, height: Math.max(lines, 1) * lineHeight }\n}\n", "// LayoutSans \u2014 magazine.ts\n// Multi-column magazine layout: flows text content across N equal-width columns.\n// Each column gets an equal share of the container width. Text flows top-to-bottom\n// in column 1, then overflows into column 2, etc.\n\nimport type { MagazineNode, TextNode, BoxRecord } from './types.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\nimport { estimateFontSize, measureTextSync } from './measure.js'\n\nexport interface MagazineResult {\n records: BoxRecord[]\n width: number\n height: number\n}\n\nexport function solveMagazine(\n node: MagazineNode,\n nodeId: string,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n pretext: typeof import('@chenglou/pretext') | null,\n): MagazineResult {\n const padding = getPaddingBox(node)\n const innerW = containerWidth - padding.left - padding.right\n const innerH = containerHeight !== Infinity ? containerHeight - padding.top - padding.bottom : Infinity\n\n const cols = node.columnCount\n const colGap = node.columnGap ?? 16\n const colW = (innerW - colGap * (cols - 1)) / cols\n\n const records: BoxRecord[] = []\n\n // \u2500\u2500 Collect text nodes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let textNodes: TextNode[]\n if (node.content && !node.children?.length) {\n // Single string convenience prop \u2014 build TextNode without optional keys set to undefined\n const baseNode: TextNode = { type: 'text', content: node.content, width: colW }\n if (node.font !== undefined) baseNode.font = node.font\n if (node.lineHeight !== undefined) baseNode.lineHeight = node.lineHeight\n textNodes = [baseNode]\n } else {\n textNodes = node.children ?? []\n }\n\n const lineHeight = node.lineHeight ?? estimateFontSize(node.font ?? '16px sans-serif') * 1.4\n\n // \u2500\u2500 Measure total height needed for all content at colW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let totalContentHeight = 0\n const measuredNodes: Array<{ node: TextNode; height: number }> = []\n\n for (const textNode of textNodes) {\n const measured = measureTextSync(\n { ...textNode, width: colW },\n colW,\n pretext,\n )\n measuredNodes.push({ node: textNode, height: measured.height })\n totalContentHeight += measured.height\n }\n\n // \u2500\u2500 Balance content across columns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Target: equal height per column. We greedily fill each column.\n const targetColH = innerH !== Infinity ? innerH : totalContentHeight / cols\n\n let colIndex = 0\n let colY = padding.top\n const colX = (c: number) => padding.left + c * (colW + colGap)\n\n let maxColH = 0\n\n for (let ni = 0; ni < measuredNodes.length; ni++) {\n const { node: textNode, height } = measuredNodes[ni]!\n const childId = `${nodeId}.${ni}`\n\n // Check if this node overflows current column\n if (colIndex < cols - 1 && colY - padding.top + height > targetColH) {\n const remainingInCol = targetColH - (colY - padding.top)\n\n if (remainingInCol < lineHeight) {\n // Advance to next column immediately\n maxColH = Math.max(maxColH, colY - padding.top)\n colIndex++\n colY = padding.top\n } else {\n // Split text node across columns\n // Portion 1: fills remainder of this column\n const linesInFirst = Math.floor(remainingInFirst(remainingInCol, lineHeight))\n const h1 = linesInFirst * lineHeight\n\n records.push({\n nodeId: `${childId}.part0`,\n x: colX(colIndex),\n y: colY,\n width: colW,\n height: h1,\n })\n\n maxColH = Math.max(maxColH, colY - padding.top + h1)\n colIndex++\n colY = padding.top\n\n // Portion 2: rest goes into next column (simplified: place remainder)\n const h2 = height - h1\n if (h2 > 0 && colIndex < cols) {\n records.push({\n nodeId: `${childId}.part1`,\n x: colX(colIndex),\n y: colY,\n width: colW,\n height: h2,\n })\n colY += h2\n maxColH = Math.max(maxColH, colY - padding.top)\n }\n continue\n }\n }\n\n // Normal placement: entire node fits in current column\n records.push(...ctx.solveNode(\n { ...textNode, width: colW, height: height },\n childId,\n colX(colIndex),\n colY,\n colW,\n height,\n ))\n colY += height\n maxColH = Math.max(maxColH, colY - padding.top)\n }\n\n const computedHeight = innerH !== Infinity ? containerHeight : maxColH + padding.top + padding.bottom\n\n return { records, width: containerWidth, height: computedHeight }\n}\n\nfunction remainingInFirst(available: number, lineHeight: number): number {\n return Math.floor(available / lineHeight)\n}\n", "// LayoutSans \u2014 absolute.ts\n// Absolute positioning solver. Places a node at exact coordinates relative to\n// its containing box. Supports top/right/bottom/left with width/height.\n\nimport type { AbsoluteNode, BoxRecord } from './types.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\n\nexport interface AbsoluteResult {\n records: BoxRecord[]\n width: number\n height: number\n}\n\nexport function solveAbsolute(\n node: AbsoluteNode,\n nodeId: string,\n containerX: number,\n containerY: number,\n containerWidth: number,\n containerHeight: number,\n ctx: SolverContext,\n): AbsoluteResult {\n const padding = getPaddingBox(node)\n\n // Resolve position from TRBL props\n let x: number\n let y: number\n let w: number\n let h: number\n\n // Width: explicit > (left + right stretch) > 0\n if (node.width !== undefined) {\n w = node.width\n } else if (node.left !== undefined && node.right !== undefined) {\n w = containerWidth - node.left - node.right\n } else {\n w = containerWidth // fallback: fill container\n }\n\n // Height: explicit > (top + bottom stretch) > 0\n if (node.height !== undefined) {\n h = node.height\n } else if (node.top !== undefined && node.bottom !== undefined) {\n h = containerHeight - node.top - node.bottom\n } else {\n h = containerHeight // fallback\n }\n\n // X position\n if (node.left !== undefined) {\n x = containerX + node.left\n } else if (node.right !== undefined) {\n x = containerX + containerWidth - node.right - w\n } else {\n x = containerX\n }\n\n // Y position\n if (node.top !== undefined) {\n y = containerY + node.top\n } else if (node.bottom !== undefined) {\n y = containerY + containerHeight - node.bottom - h\n } else {\n y = containerY\n }\n\n const records: BoxRecord[] = []\n\n // Recurse into children\n const children = node.children ?? []\n const innerW = w - padding.left - padding.right\n const innerH = h - padding.top - padding.bottom\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const childId = `${nodeId}.${i}`\n records.push(...ctx.solveNode(child, childId, x + padding.left, y + padding.top, innerW, innerH))\n }\n\n return { records, width: w, height: h }\n}\n", "// LayoutSans \u2014 engine.ts\n// The main layout orchestrator. Routes each node type to its solver, manages\n// the recursion, and flattens the output into a BoxRecord[].\n\nimport type { Node, BoxRecord, LayoutOptions, FlexNode, GridNode, MagazineNode, AbsoluteNode, TextNode, BoxNode } from './types.js'\nimport { solveFlex, solveFlexColumn } from './flex.js'\nimport { solveGrid } from './grid.js'\nimport { solveMagazine } from './magazine.js'\nimport { solveAbsolute } from './absolute.js'\nimport { measureTextSync } from './measure.js'\nimport { getPaddingBox, type SolverContext } from './utils.js'\n\nexport class LayoutEngine {\n private root: Node\n private options: LayoutOptions\n private pretext: typeof import('@chenglou/pretext') | null = null\n\n constructor(root: Node, options: LayoutOptions = {}) {\n this.root = root\n this.options = options\n }\n\n /**\n * Inject a pre-loaded Pretext module for synchronous text measurement.\n * Call this before compute() if you want accurate text sizing.\n */\n usePretext(mod: typeof import('@chenglou/pretext')): this {\n this.pretext = mod\n return this\n }\n\n /**\n * Compute the layout. Returns a flat array of positioned BoxRecords.\n * Each record maps to one node in the input tree via nodeId.\n */\n compute(): BoxRecord[] {\n const rootW = this.options.width ?? this.root.width ?? 0\n const rootH = this.options.height ?? this.root.height ?? 0\n return this.ctx.solveNode(this.root, '0', 0, 0, rootW, rootH)\n }\n\n // \u2500\u2500 Context (bound to this engine instance) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private ctx: SolverContext = {\n solveNode: (node: Node, nodeId: string, x: number, y: number, width: number, height: number): BoxRecord[] => {\n return this.solveNode(node, nodeId, x, y, width, height)\n },\n measureNode: (node: Node, nodeId: string, availableWidth: number, availableHeight: number): { width: number; height: number } => {\n return this.measureNode(node, nodeId, availableWidth, availableHeight)\n },\n }\n\n private solveNode(\n node: Node,\n nodeId: string,\n x: number,\n y: number,\n width: number,\n height: number,\n ): BoxRecord[] {\n const id = node.id ?? nodeId\n\n switch (node.type) {\n case 'flex': {\n // Try the O(n) fast path first (fixed-size column = virtual list case)\n const fast = solveFlexColumn(node as FlexNode, nodeId, width, this.ctx)\n if (fast !== null) {\n const contH = node.height ?? fast.totalHeight\n const container: BoxRecord = { nodeId: id, x, y, width, height: contH }\n const children = fast.records\n // Offset children to container's absolute position in one pass\n for (let i = 0; i < children.length; i++) {\n const r = children[i]!\n children[i] = { nodeId: r.nodeId, x: r.x + x, y: r.y + y, width: r.width, height: r.height }\n }\n return [container, ...children]\n }\n const result = solveFlex(node as FlexNode, nodeId, width, height, this.ctx)\n const self: BoxRecord = { nodeId: id, x, y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'grid': {\n const result = solveGrid(node as GridNode, nodeId, width, height, this.ctx)\n const self: BoxRecord = { nodeId: id, x, y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'magazine': {\n const result = solveMagazine(node as MagazineNode, nodeId, width, height, this.ctx, this.pretext)\n const self: BoxRecord = { nodeId: id, x, y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'absolute': {\n const result = solveAbsolute(node as AbsoluteNode, nodeId, x, y, width, height, this.ctx)\n const self: BoxRecord = { nodeId: id, x: result.records[0]?.x ?? x, y: result.records[0]?.y ?? y, width: result.width, height: result.height }\n return [self, ...result.records]\n }\n\n case 'text': {\n const measured = this.measureNode(node, nodeId, width, height)\n return [{ nodeId: id, x, y, width: measured.width, height: measured.height }]\n }\n\n case 'box':\n default: {\n // A box with no children just occupies its given space\n const boxW = (node as BoxNode).width ?? width\n const boxH = (node as BoxNode).height ?? height\n return [{ nodeId: id, x, y, width: boxW, height: boxH }]\n }\n }\n }\n\n private measureNode(\n node: Node,\n _nodeId: string,\n availableWidth: number,\n availableHeight: number,\n ): { width: number; height: number } {\n switch (node.type) {\n case 'text': {\n const w = node.width ?? availableWidth\n const result = measureTextSync(node as TextNode, w, this.pretext)\n return { width: result.width, height: result.height }\n }\n\n case 'box': {\n return {\n width: node.width ?? availableWidth,\n height: node.height ?? availableHeight,\n }\n }\n\n case 'flex':\n case 'grid':\n case 'magazine':\n case 'absolute': {\n // For measurement purposes, run the full solver and report its size\n const records = this.solveNode(node, 'measure', 0, 0, availableWidth, availableHeight)\n if (records.length === 0) return { width: availableWidth, height: availableHeight }\n const root = records[0]!\n return { width: root.width, height: root.height }\n }\n\n default:\n return { width: availableWidth, height: availableHeight }\n }\n }\n}\n\n/**\n * Create a LayoutEngine for a node tree.\n * Call .compute() to get the flat BoxRecord[].\n *\n * @example\n * const engine = createLayout(root)\n * const boxes = engine.compute()\n */\nexport function createLayout(root: Node, options?: LayoutOptions): LayoutEngine {\n return new LayoutEngine(root, options)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["getPaddingBox", "node", "base", "resolveNodeSize", "axis", "_containerSize", "val", "clampSize", "value", "min", "max", "solveFlex", "node", "nodeId", "containerWidth", "containerHeight", "ctx", "isRow", "wrap", "padding", "getPaddingBox", "innerWidth", "innerHeight", "mainGap", "crossGap", "layouts", "child", "i", "childId", "childW", "resolveNodeSize", "childH", "marginMain", "marginCross", "flexBasis", "mainFixed", "crossFixed", "mainSize", "buildLines", "mainContainerSize", "lines", "currentLine", "currentFixed", "currentGaps", "flexTotal", "item", "itemMain", "gapAdd", "line", "gapTotal", "fixedTotal", "freeSpace", "totalGrow", "clampSize", "totalShrinkWeight", "shrink", "crossContainerSize", "lineCrossMax", "alignItems", "measured", "records", "computeJustify", "usedMain", "free", "justify", "n", "computeAlignCross", "lineCrossSize", "alignSelf", "align", "crossOffset", "start", "spacing", "mainOffset", "marginMainStart", "marginCrossStart", "crossPos", "x", "y", "w", "h", "childRecords", "totalCross", "sum", "l", "solveFlexColumn", "children", "gap", "innerW", "id", "totalHeight", "solveGrid", "node", "nodeId", "containerWidth", "containerHeight", "ctx", "padding", "getPaddingBox", "innerW", "innerH", "children", "colGap", "rowGap", "records", "cols", "cellW", "rows", "cellH", "row", "col", "idx", "child", "measured", "i", "x", "y", "childId", "totalH", "estimateFontSize", "font", "match", "value", "unit", "measureTextSync", "node", "maxWidth", "pretext", "lineHeight", "estimateFontSize", "result", "prepared", "avgCharWidth", "charsPerLine", "lines", "solveMagazine", "node", "nodeId", "containerWidth", "containerHeight", "ctx", "pretext", "padding", "getPaddingBox", "innerW", "innerH", "cols", "colGap", "colW", "records", "textNodes", "baseNode", "lineHeight", "estimateFontSize", "totalContentHeight", "measuredNodes", "textNode", "measured", "measureTextSync", "targetColH", "colIndex", "colY", "colX", "c", "maxColH", "ni", "height", "childId", "remainingInCol", "h1", "remainingInFirst", "h2", "computedHeight", "available", "solveAbsolute", "node", "nodeId", "containerX", "containerY", "containerWidth", "containerHeight", "ctx", "padding", "getPaddingBox", "x", "y", "w", "h", "records", "children", "innerW", "innerH", "i", "child", "childId", "LayoutEngine", "root", "options", "node", "nodeId", "x", "y", "width", "height", "availableWidth", "availableHeight", "mod", "rootW", "rootH", "id", "fast", "solveFlexColumn", "contH", "container", "children", "i", "r", "result", "solveFlex", "solveGrid", "solveMagazine", "solveAbsolute", "measured", "boxW", "boxH", "_nodeId", "w", "measureTextSync", "records", "createLayout"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MagazineNode, BoxRecord } from './types.js';
|
|
2
|
+
import { type SolverContext } from './utils.js';
|
|
3
|
+
export interface MagazineResult {
|
|
4
|
+
records: BoxRecord[];
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function solveMagazine(node: MagazineNode, nodeId: string, containerWidth: number, containerHeight: number, ctx: SolverContext, pretext: typeof import('@chenglou/pretext') | null): MagazineResult;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TextNode } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Estimate font size from a CSS font string like '16px Inter' or '1rem Arial'.
|
|
4
|
+
* Used to derive a default lineHeight when none is supplied.
|
|
5
|
+
*/
|
|
6
|
+
export declare function estimateFontSize(font: string): number;
|
|
7
|
+
export interface MeasureTextResult {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Measure a text node's intrinsic size using Pretext.
|
|
13
|
+
* maxWidth constrains line wrapping. Returns { width, height }.
|
|
14
|
+
*/
|
|
15
|
+
export declare function measureText(node: TextNode, maxWidth: number): Promise<MeasureTextResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Synchronous version for environments where Pretext has already been loaded.
|
|
18
|
+
* Uses the node's preparedText handle if available.
|
|
19
|
+
*/
|
|
20
|
+
export declare function measureTextSync(node: TextNode, maxWidth: number, pretext: typeof import('@chenglou/pretext') | null): MeasureTextResult;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { PreparedText } from '@chenglou/pretext';
|
|
2
|
+
/** Final output record for a single node. Flat array of these from engine.compute(). */
|
|
3
|
+
export interface BoxRecord {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
interface BaseNode {
|
|
11
|
+
/** Optional stable id. Auto-assigned as tree path ('0', '0.1', '0.1.2') if omitted. */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** Fixed width in px. Required for root. Optional for children (can be flex-grown). */
|
|
14
|
+
width?: number;
|
|
15
|
+
/** Fixed height in px. Required for root unless content-sized. Optional for children. */
|
|
16
|
+
height?: number;
|
|
17
|
+
/** Minimum width constraint in px. */
|
|
18
|
+
minWidth?: number;
|
|
19
|
+
/** Maximum width constraint in px. */
|
|
20
|
+
maxWidth?: number;
|
|
21
|
+
/** Minimum height constraint in px. */
|
|
22
|
+
minHeight?: number;
|
|
23
|
+
/** Maximum height constraint in px. */
|
|
24
|
+
maxHeight?: number;
|
|
25
|
+
/** Uniform padding inside this box, in px. */
|
|
26
|
+
padding?: number;
|
|
27
|
+
paddingTop?: number;
|
|
28
|
+
paddingRight?: number;
|
|
29
|
+
paddingBottom?: number;
|
|
30
|
+
paddingLeft?: number;
|
|
31
|
+
/** Outer margin, in px. */
|
|
32
|
+
margin?: number;
|
|
33
|
+
marginTop?: number;
|
|
34
|
+
marginRight?: number;
|
|
35
|
+
marginBottom?: number;
|
|
36
|
+
marginLeft?: number;
|
|
37
|
+
/** Flex child: proportion of remaining space to grow into. */
|
|
38
|
+
flex?: number;
|
|
39
|
+
/** Flex child: weight of space to give up when container overflows. Default 1. */
|
|
40
|
+
flexShrink?: number;
|
|
41
|
+
/** Flex child: base size before grow/shrink is applied. */
|
|
42
|
+
flexBasis?: number;
|
|
43
|
+
}
|
|
44
|
+
/** A flex container. Children are laid out in a row or column. */
|
|
45
|
+
export interface FlexNode extends BaseNode {
|
|
46
|
+
type: 'flex';
|
|
47
|
+
/** Main axis direction. Default: 'row'. */
|
|
48
|
+
direction?: 'row' | 'column';
|
|
49
|
+
/** Gap between children in px. */
|
|
50
|
+
gap?: number;
|
|
51
|
+
/** Row gap (overrides gap for cross-axis direction). */
|
|
52
|
+
rowGap?: number;
|
|
53
|
+
/** Column gap (overrides gap for main-axis direction). */
|
|
54
|
+
columnGap?: number;
|
|
55
|
+
/** How children are distributed along the main axis. */
|
|
56
|
+
justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly';
|
|
57
|
+
/** How children are aligned along the cross axis. */
|
|
58
|
+
alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch';
|
|
59
|
+
/** Self-alignment override (applied on the child, not parent). */
|
|
60
|
+
alignSelf?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'auto';
|
|
61
|
+
/** Whether children wrap onto new lines. */
|
|
62
|
+
wrap?: boolean;
|
|
63
|
+
children?: Node[];
|
|
64
|
+
}
|
|
65
|
+
/** A plain box with no children. Size comes from width/height or flex growth. */
|
|
66
|
+
export interface BoxNode extends BaseNode {
|
|
67
|
+
type: 'box';
|
|
68
|
+
}
|
|
69
|
+
/** A text node measured via Pretext. */
|
|
70
|
+
export interface TextNode extends BaseNode {
|
|
71
|
+
type: 'text';
|
|
72
|
+
/** The string to measure and lay out. */
|
|
73
|
+
content: string;
|
|
74
|
+
/**
|
|
75
|
+
* A PreparedText handle from `@chenglou/pretext` (result of prepare()).
|
|
76
|
+
* Provide this OR content+font — if both are given, preparedText wins.
|
|
77
|
+
*/
|
|
78
|
+
preparedText?: PreparedText;
|
|
79
|
+
/** Font string as passed to Pretext's prepare(), e.g. '16px Inter'. */
|
|
80
|
+
font?: string;
|
|
81
|
+
/** Line height in px. Default: fontSize * 1.4 (estimated from font string). */
|
|
82
|
+
lineHeight?: number;
|
|
83
|
+
}
|
|
84
|
+
/** An absolutely positioned box. Coordinates relative to nearest non-static ancestor. */
|
|
85
|
+
export interface AbsoluteNode extends BaseNode {
|
|
86
|
+
type: 'absolute';
|
|
87
|
+
top?: number;
|
|
88
|
+
right?: number;
|
|
89
|
+
bottom?: number;
|
|
90
|
+
left?: number;
|
|
91
|
+
children?: Node[];
|
|
92
|
+
}
|
|
93
|
+
/** A basic grid container (1D: rows OR columns for MVP). */
|
|
94
|
+
export interface GridNode extends BaseNode {
|
|
95
|
+
type: 'grid';
|
|
96
|
+
/** Number of equal-width columns. */
|
|
97
|
+
columns?: number;
|
|
98
|
+
/** Number of equal-height rows. */
|
|
99
|
+
rows?: number;
|
|
100
|
+
/** Gap between cells. */
|
|
101
|
+
gap?: number;
|
|
102
|
+
rowGap?: number;
|
|
103
|
+
columnGap?: number;
|
|
104
|
+
children?: Node[];
|
|
105
|
+
}
|
|
106
|
+
/** A magazine-style multi-column text flow container. */
|
|
107
|
+
export interface MagazineNode extends BaseNode {
|
|
108
|
+
type: 'magazine';
|
|
109
|
+
/** Number of columns to flow text across. */
|
|
110
|
+
columnCount: number;
|
|
111
|
+
/** Gap between columns in px. Default 16. */
|
|
112
|
+
columnGap?: number;
|
|
113
|
+
/** Content to flow. Can be plain text or TextNodes. */
|
|
114
|
+
children?: TextNode[];
|
|
115
|
+
/** Convenience: single string content (creates one implicit TextNode). */
|
|
116
|
+
content?: string;
|
|
117
|
+
/** Font for text measurement. */
|
|
118
|
+
font?: string;
|
|
119
|
+
/** Line height in px. */
|
|
120
|
+
lineHeight?: number;
|
|
121
|
+
}
|
|
122
|
+
export type Node = FlexNode | BoxNode | TextNode | AbsoluteNode | GridNode | MagazineNode;
|
|
123
|
+
export interface ResolvedNode {
|
|
124
|
+
id: string;
|
|
125
|
+
node: Node;
|
|
126
|
+
resolvedWidth: number;
|
|
127
|
+
resolvedHeight: number;
|
|
128
|
+
children: ResolvedNode[];
|
|
129
|
+
x: number;
|
|
130
|
+
y: number;
|
|
131
|
+
}
|
|
132
|
+
export interface LayoutOptions {
|
|
133
|
+
/** Root width in px. Overrides node.width if provided. */
|
|
134
|
+
width?: number;
|
|
135
|
+
/** Root height in px. Overrides node.height if provided. */
|
|
136
|
+
height?: number;
|
|
137
|
+
}
|
|
138
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Node, BoxRecord } from './types.js';
|
|
2
|
+
export interface PaddingBox {
|
|
3
|
+
top: number;
|
|
4
|
+
right: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
left: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function getPaddingBox(node: Node): PaddingBox;
|
|
9
|
+
/** Read width or height from a node, returning NaN if not fixed. */
|
|
10
|
+
export declare function resolveNodeSize(node: Node, axis: 'width' | 'height', _containerSize: number): number;
|
|
11
|
+
/** Clamp a value between optional min/max. */
|
|
12
|
+
export declare function clampSize(value: number, min?: number, max?: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* The engine passes a SolverContext into each solver so they can recursively
|
|
15
|
+
* lay out child nodes without creating circular imports.
|
|
16
|
+
*/
|
|
17
|
+
export interface SolverContext {
|
|
18
|
+
/**
|
|
19
|
+
* Recursively solve a child node at the given position and size.
|
|
20
|
+
* Returns the flat list of BoxRecords produced by that subtree.
|
|
21
|
+
*/
|
|
22
|
+
solveNode(node: Node, nodeId: string, x: number, y: number, width: number, height: number): BoxRecord[];
|
|
23
|
+
/**
|
|
24
|
+
* Measure a node to get its intrinsic size without fully solving it.
|
|
25
|
+
* Used by flex cross-axis content-sizing.
|
|
26
|
+
*/
|
|
27
|
+
measureNode(node: Node, nodeId: string, availableWidth: number, availableHeight: number): {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "layout-sans",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CSS Flex/Grid layout without the browser. No DOM. No WASM bloat.",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"layout",
|
|
9
|
+
"flexbox",
|
|
10
|
+
"grid",
|
|
11
|
+
"css",
|
|
12
|
+
"no-dom",
|
|
13
|
+
"pretext",
|
|
14
|
+
"virtualization",
|
|
15
|
+
"server-side",
|
|
16
|
+
"edge",
|
|
17
|
+
"node"
|
|
18
|
+
],
|
|
19
|
+
"main": "dist/index.cjs",
|
|
20
|
+
"module": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node esbuild.config.js && tsc -p tsconfig.build.json",
|
|
36
|
+
"bench": "tsx bench/benchmark.ts",
|
|
37
|
+
"demo": "tsx demo/basic-flex.ts",
|
|
38
|
+
"demo:magazine": "tsx demo/magazine.ts",
|
|
39
|
+
"demo:virtualization": "tsx demo/virtualization.ts",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@chenglou/pretext": "*"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@chenglou/pretext": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@chenglou/pretext": "^0.0.3",
|
|
52
|
+
"@types/node": "^25.5.0",
|
|
53
|
+
"esbuild": "^0.24.0",
|
|
54
|
+
"ts-node": "^10.9.2",
|
|
55
|
+
"tsx": "^4.21.0",
|
|
56
|
+
"typescript": "^5.6.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
}
|
|
61
|
+
}
|