textura 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pretext contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # Textura
2
+
3
+ DOM-free layout engine for the web. Combines [Yoga](https://github.com/facebook/yoga) (flexbox) with [Pretext](https://github.com/chenglou/pretext) (text measurement) to compute complete UI geometry — positions, sizes, and text line breaks — without ever touching the DOM.
4
+
5
+ ## Why
6
+
7
+ The browser's layout engine is a black box that blocks the main thread. Every `getBoundingClientRect()` or `offsetHeight` call triggers synchronous layout reflow. When components independently measure text, each measurement triggers a reflow of the entire document.
8
+
9
+ Yoga solves box layout (flexbox) in pure JS/WASM, but punts on text — it requires you to supply a `MeasureFunction` callback externally. Pretext solves text measurement with canvas `measureText`, but doesn't do box layout. **Textura joins them**: declare a tree of flex containers and text nodes, get back exact pixel geometry for everything.
10
+
11
+ This unlocks:
12
+ - **Worker-thread UI layout** — compute layout off the main thread, send only coordinates for painting
13
+ - **Zero-estimation virtualization** — know exact heights for 100K items before mounting a single DOM node
14
+ - **Canvas/WebGL/SVG rendering** — full layout engine for non-DOM renderers
15
+ - **Server-side layout** — generate pixel positions server-side (once Pretext gets server canvas)
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install textura
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```ts
26
+ import { init, computeLayout } from 'textura'
27
+
28
+ // Initialize Yoga WASM (call once)
29
+ await init()
30
+
31
+ // Describe your UI as a tree
32
+ const result = computeLayout({
33
+ width: 400,
34
+ padding: 16,
35
+ flexDirection: 'column',
36
+ gap: 12,
37
+ children: [
38
+ {
39
+ text: 'Hello World',
40
+ font: '24px Inter',
41
+ lineHeight: 32,
42
+ },
43
+ {
44
+ flexDirection: 'row',
45
+ gap: 8,
46
+ children: [
47
+ { width: 80, height: 80 }, // avatar
48
+ {
49
+ text: 'This is a message that will wrap to multiple lines based on available width.',
50
+ font: '16px Inter',
51
+ lineHeight: 22,
52
+ flexGrow: 1,
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ })
58
+
59
+ // result is a tree of computed layouts:
60
+ // {
61
+ // x: 0, y: 0, width: 400, height: ...,
62
+ // children: [
63
+ // { x: 16, y: 16, width: 368, height: 32, text: 'Hello World', lineCount: 1 },
64
+ // { x: 16, y: 60, width: 368, height: ...,
65
+ // children: [
66
+ // { x: 0, y: 0, width: 80, height: 80 },
67
+ // { x: 88, y: 0, width: 280, height: ..., text: '...', lineCount: ... },
68
+ // ]
69
+ // },
70
+ // ]
71
+ // }
72
+ ```
73
+
74
+ ## API
75
+
76
+ ### `init(): Promise<void>`
77
+
78
+ Initialize the Yoga WASM runtime. Must be called once before `computeLayout`.
79
+
80
+ ### `computeLayout(tree, options?): ComputedLayout`
81
+
82
+ Compute layout for a declarative UI tree. Returns positions, sizes, and text metadata for every node.
83
+
84
+ **Options:**
85
+ - `width?: number` — available width for the root
86
+ - `height?: number` — available height for the root
87
+ - `direction?: 'ltr' | 'rtl'` — text direction (default: `'ltr'`)
88
+
89
+ ### Node types
90
+
91
+ **Box nodes** — flex containers with children:
92
+ ```ts
93
+ {
94
+ flexDirection: 'row',
95
+ gap: 8,
96
+ padding: 16,
97
+ children: [...]
98
+ }
99
+ ```
100
+
101
+ **Text nodes** — leaf nodes with measured text content:
102
+ ```ts
103
+ {
104
+ text: 'Hello world',
105
+ font: '16px Inter', // canvas font shorthand
106
+ lineHeight: 22, // line height in px
107
+ whiteSpace: 'pre-wrap', // optional: preserve spaces/tabs/newlines
108
+ }
109
+ ```
110
+
111
+ Both node types accept all flexbox properties: `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `alignSelf`, `alignContent`, `flexGrow`, `flexShrink`, `flexBasis`, `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `padding*`, `margin*`, `border*`, `gap`, `position`, `top/right/bottom/left`, `aspectRatio`, `overflow`, `display`.
112
+
113
+ ### `ComputedLayout`
114
+
115
+ ```ts
116
+ interface ComputedLayout {
117
+ x: number
118
+ y: number
119
+ width: number
120
+ height: number
121
+ children: ComputedLayout[]
122
+ text?: string // present on text nodes
123
+ lineCount?: number // present on text nodes
124
+ }
125
+ ```
126
+
127
+ ### `clearCache(): void`
128
+
129
+ Clear Pretext's internal measurement caches.
130
+
131
+ ### `destroy(): void`
132
+
133
+ Release Yoga resources. Mostly useful for tests.
134
+
135
+ ## How it works
136
+
137
+ 1. You describe a UI tree using plain objects with CSS-like flex properties
138
+ 2. `computeLayout` builds a Yoga node tree from your description
139
+ 3. For text nodes, it wires Pretext's `prepare()` + `layout()` into Yoga's `MeasureFunction` — when Yoga asks "how tall is this text at width X?", Pretext answers using canvas-based measurement with cached segment widths
140
+ 4. Yoga runs its flexbox algorithm over the tree
141
+ 5. The computed positions and sizes are read back into a plain object tree
142
+
143
+ The text measurement is the key piece: Pretext handles Unicode segmentation, CJK character breaking, Arabic/bidi text, emoji, soft hyphens, and browser-specific quirks — all via `Intl.Segmenter` and canvas `measureText`, with 7680/7680 accuracy across Chrome/Safari/Firefox.
144
+
145
+ ## Limitations
146
+
147
+ - Requires a browser environment (or `OffscreenCanvas` in a worker) for text measurement
148
+ - Text nodes use the same CSS target as Pretext: `white-space: normal`, `word-break: normal`, `overflow-wrap: break-word`, `line-break: auto`
149
+ - Use named fonts (`Inter`, `Helvetica`) — `system-ui` can produce inaccurate measurements on macOS
150
+
151
+ ## Credits
152
+
153
+ Built on [Yoga](https://github.com/facebook/yoga) by Meta and [Pretext](https://github.com/chenglou/pretext) by Cheng Lou.
@@ -0,0 +1,23 @@
1
+ import { clearCache } from '@chenglou/pretext';
2
+ import { type LayoutNode, type ComputedLayout } from './types.js';
3
+ /** Initialize the Yoga WASM runtime. Must be called once before computeLayout. */
4
+ export declare function init(): Promise<void>;
5
+ /** Release Yoga config. Mostly useful for tests. */
6
+ export declare function destroy(): void;
7
+ /** Clear Pretext's internal measurement caches. */
8
+ export { clearCache };
9
+ export interface ComputeOptions {
10
+ /** Available width for the root container. Default: unconstrained. */
11
+ width?: number;
12
+ /** Available height for the root container. Default: unconstrained. */
13
+ height?: number;
14
+ /** Text direction. Default: 'ltr'. */
15
+ direction?: 'ltr' | 'rtl';
16
+ }
17
+ /**
18
+ * Compute the full layout geometry for a declarative UI tree.
19
+ *
20
+ * Builds a Yoga node tree, wires Pretext text measurement into leaf nodes,
21
+ * runs Yoga's flexbox algorithm, and returns the computed positions and sizes.
22
+ */
23
+ export declare function computeLayout(tree: LayoutNode, options?: ComputeOptions): ComputedLayout;
package/dist/engine.js ADDED
@@ -0,0 +1,245 @@
1
+ import { loadYoga, FlexDirection, Align, Justify, Wrap, Edge, Gutter, MeasureMode, PositionType, Overflow, Display, Direction, } from 'yoga-layout/load';
2
+ import { prepare, layout, clearCache } from '@chenglou/pretext';
3
+ import { isTextNode, } from './types.js';
4
+ let yoga = null;
5
+ let config = null;
6
+ function getConfig() {
7
+ if (config === null)
8
+ throw new Error('textura: call init() first');
9
+ return config;
10
+ }
11
+ /** Initialize the Yoga WASM runtime. Must be called once before computeLayout. */
12
+ export async function init() {
13
+ if (yoga !== null)
14
+ return;
15
+ yoga = await loadYoga();
16
+ config = yoga.Config.create();
17
+ config.setUseWebDefaults(true);
18
+ }
19
+ /** Release Yoga config. Mostly useful for tests. */
20
+ export function destroy() {
21
+ if (config !== null) {
22
+ config.free();
23
+ config = null;
24
+ }
25
+ yoga = null;
26
+ }
27
+ /** Clear Pretext's internal measurement caches. */
28
+ export { clearCache };
29
+ // --- Flex property mapping ---
30
+ const FLEX_DIRECTION_MAP = {
31
+ row: FlexDirection.Row,
32
+ column: FlexDirection.Column,
33
+ 'row-reverse': FlexDirection.RowReverse,
34
+ 'column-reverse': FlexDirection.ColumnReverse,
35
+ };
36
+ const JUSTIFY_MAP = {
37
+ 'flex-start': Justify.FlexStart,
38
+ center: Justify.Center,
39
+ 'flex-end': Justify.FlexEnd,
40
+ 'space-between': Justify.SpaceBetween,
41
+ 'space-around': Justify.SpaceAround,
42
+ 'space-evenly': Justify.SpaceEvenly,
43
+ };
44
+ const ALIGN_MAP = {
45
+ auto: Align.Auto,
46
+ 'flex-start': Align.FlexStart,
47
+ center: Align.Center,
48
+ 'flex-end': Align.FlexEnd,
49
+ stretch: Align.Stretch,
50
+ baseline: Align.Baseline,
51
+ 'space-between': Align.SpaceBetween,
52
+ 'space-around': Align.SpaceAround,
53
+ 'space-evenly': Align.SpaceEvenly,
54
+ };
55
+ const WRAP_MAP = {
56
+ nowrap: Wrap.NoWrap,
57
+ wrap: Wrap.Wrap,
58
+ 'wrap-reverse': Wrap.WrapReverse,
59
+ };
60
+ function applyFlexProps(node, props) {
61
+ if (props.flexDirection !== undefined)
62
+ node.setFlexDirection(FLEX_DIRECTION_MAP[props.flexDirection]);
63
+ if (props.flexWrap !== undefined)
64
+ node.setFlexWrap(WRAP_MAP[props.flexWrap]);
65
+ if (props.justifyContent !== undefined)
66
+ node.setJustifyContent(JUSTIFY_MAP[props.justifyContent]);
67
+ if (props.alignItems !== undefined)
68
+ node.setAlignItems(ALIGN_MAP[props.alignItems]);
69
+ if (props.alignSelf !== undefined)
70
+ node.setAlignSelf(ALIGN_MAP[props.alignSelf]);
71
+ if (props.alignContent !== undefined)
72
+ node.setAlignContent(ALIGN_MAP[props.alignContent]);
73
+ if (props.flexGrow !== undefined)
74
+ node.setFlexGrow(props.flexGrow);
75
+ if (props.flexShrink !== undefined)
76
+ node.setFlexShrink(props.flexShrink);
77
+ if (props.flexBasis !== undefined)
78
+ node.setFlexBasis(props.flexBasis);
79
+ // Dimensions
80
+ if (props.width !== undefined)
81
+ node.setWidth(props.width);
82
+ if (props.height !== undefined)
83
+ node.setHeight(props.height);
84
+ if (props.minWidth !== undefined)
85
+ node.setMinWidth(props.minWidth);
86
+ if (props.maxWidth !== undefined)
87
+ node.setMaxWidth(props.maxWidth);
88
+ if (props.minHeight !== undefined)
89
+ node.setMinHeight(props.minHeight);
90
+ if (props.maxHeight !== undefined)
91
+ node.setMaxHeight(props.maxHeight);
92
+ // Padding
93
+ if (props.padding !== undefined)
94
+ node.setPadding(Edge.All, props.padding);
95
+ if (props.paddingTop !== undefined)
96
+ node.setPadding(Edge.Top, props.paddingTop);
97
+ if (props.paddingRight !== undefined)
98
+ node.setPadding(Edge.Right, props.paddingRight);
99
+ if (props.paddingBottom !== undefined)
100
+ node.setPadding(Edge.Bottom, props.paddingBottom);
101
+ if (props.paddingLeft !== undefined)
102
+ node.setPadding(Edge.Left, props.paddingLeft);
103
+ if (props.paddingHorizontal !== undefined)
104
+ node.setPadding(Edge.Horizontal, props.paddingHorizontal);
105
+ if (props.paddingVertical !== undefined)
106
+ node.setPadding(Edge.Vertical, props.paddingVertical);
107
+ // Margin
108
+ if (props.margin !== undefined)
109
+ node.setMargin(Edge.All, props.margin);
110
+ if (props.marginTop !== undefined)
111
+ node.setMargin(Edge.Top, props.marginTop);
112
+ if (props.marginRight !== undefined)
113
+ node.setMargin(Edge.Right, props.marginRight);
114
+ if (props.marginBottom !== undefined)
115
+ node.setMargin(Edge.Bottom, props.marginBottom);
116
+ if (props.marginLeft !== undefined)
117
+ node.setMargin(Edge.Left, props.marginLeft);
118
+ if (props.marginHorizontal !== undefined)
119
+ node.setMargin(Edge.Horizontal, props.marginHorizontal);
120
+ if (props.marginVertical !== undefined)
121
+ node.setMargin(Edge.Vertical, props.marginVertical);
122
+ // Border
123
+ if (props.border !== undefined)
124
+ node.setBorder(Edge.All, props.border);
125
+ if (props.borderTop !== undefined)
126
+ node.setBorder(Edge.Top, props.borderTop);
127
+ if (props.borderRight !== undefined)
128
+ node.setBorder(Edge.Right, props.borderRight);
129
+ if (props.borderBottom !== undefined)
130
+ node.setBorder(Edge.Bottom, props.borderBottom);
131
+ if (props.borderLeft !== undefined)
132
+ node.setBorder(Edge.Left, props.borderLeft);
133
+ // Gap
134
+ if (props.gap !== undefined)
135
+ node.setGap(Gutter.All, props.gap);
136
+ if (props.rowGap !== undefined)
137
+ node.setGap(Gutter.Row, props.rowGap);
138
+ if (props.columnGap !== undefined)
139
+ node.setGap(Gutter.Column, props.columnGap);
140
+ // Position
141
+ if (props.position !== undefined)
142
+ node.setPositionType(props.position === 'absolute' ? PositionType.Absolute : PositionType.Relative);
143
+ if (props.top !== undefined)
144
+ node.setPosition(Edge.Top, props.top);
145
+ if (props.right !== undefined)
146
+ node.setPosition(Edge.Right, props.right);
147
+ if (props.bottom !== undefined)
148
+ node.setPosition(Edge.Bottom, props.bottom);
149
+ if (props.left !== undefined)
150
+ node.setPosition(Edge.Left, props.left);
151
+ // Other
152
+ if (props.aspectRatio !== undefined)
153
+ node.setAspectRatio(props.aspectRatio);
154
+ if (props.overflow !== undefined) {
155
+ const map = { visible: Overflow.Visible, hidden: Overflow.Hidden, scroll: Overflow.Scroll };
156
+ node.setOverflow(map[props.overflow]);
157
+ }
158
+ if (props.display !== undefined)
159
+ node.setDisplay(props.display === 'none' ? Display.None : Display.Flex);
160
+ }
161
+ /** Metadata attached to Yoga nodes so we can extract text info during readback. */
162
+ const nodeMeta = new WeakMap();
163
+ function buildNode(desc) {
164
+ if (yoga === null)
165
+ throw new Error('textura: call init() first');
166
+ const node = yoga.Node.create(getConfig());
167
+ applyFlexProps(node, desc);
168
+ if (isTextNode(desc)) {
169
+ const whiteSpace = desc.whiteSpace;
170
+ const font = desc.font;
171
+ const text = desc.text;
172
+ const lineHeight = desc.lineHeight;
173
+ const meta = { text, font, lineHeight, lastLineCount: 0 };
174
+ nodeMeta.set(node, meta);
175
+ node.setMeasureFunc((width, widthMode, _height, _heightMode) => {
176
+ const prepared = prepare(text, font, whiteSpace ? { whiteSpace } : undefined);
177
+ // Determine max width for line breaking
178
+ let maxWidth;
179
+ if (widthMode === MeasureMode.Exactly) {
180
+ maxWidth = width;
181
+ }
182
+ else if (widthMode === MeasureMode.AtMost) {
183
+ maxWidth = width;
184
+ }
185
+ else {
186
+ // Undefined — no constraint. Use a very large width (single line).
187
+ maxWidth = 1e7;
188
+ }
189
+ const result = layout(prepared, maxWidth, lineHeight);
190
+ meta.lastLineCount = result.lineCount;
191
+ // For AtMost/Undefined, the intrinsic width could be narrower.
192
+ // We report the constrained width for Exactly/AtMost, and the
193
+ // full container for Undefined (Yoga handles shrinking).
194
+ const reportedWidth = widthMode === MeasureMode.Undefined ? maxWidth : width;
195
+ return { width: reportedWidth, height: result.height };
196
+ });
197
+ }
198
+ else {
199
+ const children = desc.children;
200
+ if (children) {
201
+ for (let i = 0; i < children.length; i++) {
202
+ node.insertChild(buildNode(children[i]), i);
203
+ }
204
+ }
205
+ }
206
+ return node;
207
+ }
208
+ // --- Layout readback ---
209
+ function readLayout(node) {
210
+ const computed = {
211
+ x: node.getComputedLeft(),
212
+ y: node.getComputedTop(),
213
+ width: node.getComputedWidth(),
214
+ height: node.getComputedHeight(),
215
+ children: [],
216
+ };
217
+ const meta = nodeMeta.get(node);
218
+ if (meta) {
219
+ computed.text = meta.text;
220
+ computed.lineCount = meta.lastLineCount;
221
+ }
222
+ const childCount = node.getChildCount();
223
+ for (let i = 0; i < childCount; i++) {
224
+ computed.children.push(readLayout(node.getChild(i)));
225
+ }
226
+ return computed;
227
+ }
228
+ /**
229
+ * Compute the full layout geometry for a declarative UI tree.
230
+ *
231
+ * Builds a Yoga node tree, wires Pretext text measurement into leaf nodes,
232
+ * runs Yoga's flexbox algorithm, and returns the computed positions and sizes.
233
+ */
234
+ export function computeLayout(tree, options) {
235
+ if (yoga === null)
236
+ throw new Error('textura: call init() first');
237
+ const root = buildNode(tree);
238
+ const w = options?.width;
239
+ const h = options?.height;
240
+ const dir = options?.direction === 'rtl' ? Direction.RTL : Direction.LTR;
241
+ root.calculateLayout(w, h, dir);
242
+ const result = readLayout(root);
243
+ root.freeRecursive();
244
+ return result;
245
+ }
@@ -0,0 +1,4 @@
1
+ export { init, destroy, clearCache, computeLayout } from './engine.js';
2
+ export type { ComputeOptions } from './engine.js';
3
+ export type { LayoutNode, TextNode, BoxNode, FlexProps, ComputedLayout, } from './types.js';
4
+ export { isTextNode } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { init, destroy, clearCache, computeLayout } from './engine.js';
2
+ export { isTextNode } from './types.js';
@@ -0,0 +1,79 @@
1
+ /** CSS-like flexbox properties shared by all container nodes. */
2
+ export interface FlexProps {
3
+ /** Default: 'column' */
4
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
5
+ flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
6
+ justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly';
7
+ alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
8
+ alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
9
+ alignContent?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
10
+ flexGrow?: number;
11
+ flexShrink?: number;
12
+ flexBasis?: number | 'auto';
13
+ width?: number | 'auto';
14
+ height?: number | 'auto';
15
+ minWidth?: number;
16
+ maxWidth?: number;
17
+ minHeight?: number;
18
+ maxHeight?: number;
19
+ padding?: number;
20
+ paddingTop?: number;
21
+ paddingRight?: number;
22
+ paddingBottom?: number;
23
+ paddingLeft?: number;
24
+ paddingHorizontal?: number;
25
+ paddingVertical?: number;
26
+ margin?: number | 'auto';
27
+ marginTop?: number | 'auto';
28
+ marginRight?: number | 'auto';
29
+ marginBottom?: number | 'auto';
30
+ marginLeft?: number | 'auto';
31
+ marginHorizontal?: number | 'auto';
32
+ marginVertical?: number | 'auto';
33
+ border?: number;
34
+ borderTop?: number;
35
+ borderRight?: number;
36
+ borderBottom?: number;
37
+ borderLeft?: number;
38
+ gap?: number;
39
+ rowGap?: number;
40
+ columnGap?: number;
41
+ position?: 'relative' | 'absolute';
42
+ top?: number;
43
+ right?: number;
44
+ bottom?: number;
45
+ left?: number;
46
+ aspectRatio?: number;
47
+ overflow?: 'visible' | 'hidden' | 'scroll';
48
+ display?: 'flex' | 'none';
49
+ }
50
+ /** A text leaf node. Has text content, font, and lineHeight for measurement. */
51
+ export interface TextNode extends FlexProps {
52
+ text: string;
53
+ /** Canvas font shorthand, e.g. '16px Inter' */
54
+ font: string;
55
+ /** Line height in pixels */
56
+ lineHeight: number;
57
+ /** Pretext whiteSpace mode */
58
+ whiteSpace?: 'normal' | 'pre-wrap';
59
+ }
60
+ /** A container (box) node that can have children. */
61
+ export interface BoxNode extends FlexProps {
62
+ children?: LayoutNode[];
63
+ }
64
+ /** A node in the declarative layout tree. */
65
+ export type LayoutNode = TextNode | BoxNode;
66
+ /** Type guard: is this node a text leaf? */
67
+ export declare function isTextNode(node: LayoutNode): node is TextNode;
68
+ /** Computed layout for a single node in the tree. */
69
+ export interface ComputedLayout {
70
+ x: number;
71
+ y: number;
72
+ width: number;
73
+ height: number;
74
+ children: ComputedLayout[];
75
+ /** Present only on text nodes: the measured line count. */
76
+ lineCount?: number;
77
+ /** Present only on text nodes: the original text content. */
78
+ text?: string;
79
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ // --- Input types: declarative UI tree description ---
2
+ /** Type guard: is this node a text leaf? */
3
+ export function isTextNode(node) {
4
+ return 'text' in node && typeof node.text === 'string';
5
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "textura",
3
+ "version": "1.0.0",
4
+ "description": "DOM-free layout engine combining Yoga flexbox with Pretext text measurement",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/razroo/textura"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
26
+ "check": "tsc",
27
+ "test": "bun test"
28
+ },
29
+ "dependencies": {
30
+ "@chenglou/pretext": "^0.0.3",
31
+ "yoga-layout": "^3.2.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "latest",
35
+ "typescript": "6.0.2"
36
+ }
37
+ }