noreflow 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jbpete87
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,219 @@
1
+ # Noreflow
2
+
3
+ **Pure TypeScript layout engine. Flexbox + CSS Grid. Zero dependencies. No WASM. No DOM.**
4
+
5
+ Every time an AI chat streams a token, the browser recalculates the entire page layout. That's why ChatGPT stutters, Claude janks, and every streaming UI feels sluggish. The DOM reflow bottleneck is the performance wall every serious web app hits.
6
+
7
+ Noreflow computes layout as a pure function — feed it a tree of nodes with styles, get back exact pixel positions. Pair it with [Pretext](https://pretext.dev) for text measurement and you get **zero-reflow rendering** for streaming UIs, virtual scrolling, Canvas apps, and anywhere else DOM layout is too slow.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install noreflow
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { computeLayout } from 'noreflow';
19
+
20
+ const layout = computeLayout({
21
+ style: { width: 400, height: 200, gap: 16, padding: 20 },
22
+ children: [
23
+ { style: { flexGrow: 1, height: 60 } },
24
+ { style: { flexGrow: 2, height: 60 } },
25
+ { style: { width: 80, height: 60 } },
26
+ ],
27
+ });
28
+
29
+ // layout.children[0] → { x: 20, y: 20, width: 88, height: 60 }
30
+ // layout.children[1] → { x: 124, y: 20, width: 176, height: 60 }
31
+ // layout.children[2] → { x: 316, y: 20, width: 80, height: 60 }
32
+ ```
33
+
34
+ Pure data in, pure data out. No classes, no manual memory management, no `.free()`.
35
+
36
+ ## The Problem
37
+
38
+ The DOM reflow bottleneck is THE performance wall every serious web app hits. Every app that's gotten big enough has had to build ugly workarounds:
39
+
40
+ - **Slack** estimates message heights and gets them wrong (scroll jumping)
41
+ - **Google Docs** slows down on long documents because every keystroke triggers paragraph reflow
42
+ - **VS Code** built an entire custom editor (Monaco) because `contenteditable` + DOM layout couldn't keep up
43
+ - **Figma** renders entirely to Canvas because DOM layout can't handle a design tool
44
+ - **Every AI chat app** (ChatGPT, Claude) janks when streaming because tokens cause text reflow → height changes → scroll jumps
45
+
46
+ The root cause is the same: the browser's layout engine is a black box that forces synchronous reflow whenever content changes.
47
+
48
+ ## The Solution
49
+
50
+ Noreflow + [Pretext](https://pretext.dev) = zero-reflow rendering.
51
+
52
+ 1. **Pretext** measures text (line breaks, wrapping, height) as pure arithmetic — no DOM
53
+ 2. **Noreflow** computes layout (flexbox, grid, absolute positioning) as a pure function — no DOM
54
+ 3. Together they replace the browser's Style → Layout pipeline with deterministic, synchronous TypeScript
55
+
56
+ ```typescript
57
+ import { prepareWithSegments, layout } from '@chenglou/pretext';
58
+ import { computeLayout } from 'noreflow';
59
+
60
+ const prepared = prepareWithSegments(
61
+ 'Each new token can cause a line wrap, changing the height.',
62
+ '400 14px Inter',
63
+ );
64
+
65
+ const measure = (availableWidth: number) => {
66
+ const result = layout(prepared, availableWidth, 20);
67
+ return { width: availableWidth, height: result.height };
68
+ };
69
+
70
+ const chatMessage = computeLayout({
71
+ style: { width: 320, flexDirection: 'row', gap: 8, padding: 12 },
72
+ children: [
73
+ { style: { width: 32, height: 32, flexShrink: 0 } },
74
+ {
75
+ style: { flexGrow: 1, flexDirection: 'column', gap: 4 },
76
+ children: [
77
+ { style: { height: 16 } },
78
+ { measure },
79
+ ],
80
+ },
81
+ ],
82
+ });
83
+ ```
84
+
85
+ The message body height is computed from the actual text without touching the DOM. When a new token arrives, re-run the computation — it takes microseconds.
86
+
87
+ ## Use Cases
88
+
89
+ - **AI streaming UIs** — compute height before rendering, eliminate scroll jank
90
+ - **Virtual scrolling** — accurate variable-height rows without measuring DOM elements
91
+ - **Canvas / WebGPU apps** — full UI layout in a draw loop
92
+ - **Server-side rendering** — layout computation in Node.js, Workers, Deno, Bun
93
+ - **PDF / document generation** — without headless Chrome
94
+ - **Layout unit testing** — without a browser
95
+
96
+ ## API
97
+
98
+ ### `computeLayout(node, availableWidth?, availableHeight?)`
99
+
100
+ ```typescript
101
+ interface FlexNode {
102
+ style?: FlexStyle;
103
+ children?: FlexNode[];
104
+ measure?: (availableWidth: number, availableHeight: number) => { width: number; height: number };
105
+ }
106
+
107
+ interface LayoutResult {
108
+ x: number;
109
+ y: number;
110
+ width: number;
111
+ height: number;
112
+ children: LayoutResult[];
113
+ }
114
+ ```
115
+
116
+ ### Supported Style Properties
117
+
118
+ **Container:**
119
+ - `display`: `'flex'` | `'grid'` | `'none'`
120
+ - `flexDirection`: `'row'` | `'column'` | `'row-reverse'` | `'column-reverse'`
121
+ - `flexWrap`: `'nowrap'` | `'wrap'` | `'wrap-reverse'`
122
+ - `justifyContent`: `'flex-start'` | `'flex-end'` | `'center'` | `'space-between'` | `'space-around'` | `'space-evenly'`
123
+ - `alignItems`: `'flex-start'` | `'flex-end'` | `'center'` | `'stretch'`
124
+ - `alignContent`: `'flex-start'` | `'flex-end'` | `'center'` | `'stretch'` | `'space-between'` | `'space-around'`
125
+ - `gap`, `rowGap`, `columnGap`
126
+
127
+ **CSS Grid:**
128
+ - `gridTemplateColumns`, `gridTemplateRows` — explicit track sizes (number, `'Nfr'`, `'auto'`)
129
+ - `gridAutoRows`, `gridAutoColumns` — implicit track sizes
130
+ - `gridColumnStart`, `gridColumnEnd`, `gridRowStart`, `gridRowEnd` — item placement
131
+ - `gridAutoFlow`: `'row'` | `'column'`
132
+
133
+ **Item:**
134
+ - `flexGrow`, `flexShrink`, `flexBasis`
135
+ - `alignSelf`: `'auto'` | `'flex-start'` | `'flex-end'` | `'center'` | `'stretch'`
136
+
137
+ **Sizing:**
138
+ - `width`, `height`: number (px), `'${n}%'`, or `'auto'`
139
+ - `minWidth`, `minHeight`, `maxWidth`, `maxHeight`
140
+ - `padding`, `paddingTop/Right/Bottom/Left`
141
+ - `margin`, `marginTop/Right/Bottom/Left` (including `'auto'`)
142
+ - `border`, `borderTop/Right/Bottom/Left`
143
+ - `boxSizing`: `'content-box'` | `'border-box'`
144
+ - `aspectRatio`: number
145
+
146
+ **Positioning:**
147
+ - `position`: `'relative'` | `'absolute'` | `'fixed'`
148
+ - `top`, `right`, `bottom`, `left`
149
+
150
+ ### Measure Function
151
+
152
+ For leaf nodes with dynamic content (text, images), provide a `measure` callback:
153
+
154
+ ```typescript
155
+ const node = {
156
+ measure: (availableWidth, availableHeight) => ({
157
+ width: measureTextWidth(myText, availableWidth),
158
+ height: measureTextHeight(myText, availableWidth),
159
+ }),
160
+ };
161
+ ```
162
+
163
+ This is how you integrate with text measurement libraries like [Pretext](https://pretext.dev).
164
+
165
+ ## Performance
166
+
167
+ Benchmarked on Apple M-series, Node.js v24:
168
+
169
+ | Scenario | Items | Time | Ops/sec |
170
+ |----------|-------|------|---------|
171
+ | Flat row | 10 | 3.4 us | 292,000 |
172
+ | Flat row | 100 | 26 us | 38,000 |
173
+ | Flat row | 1,000 | 276 us | 3,600 |
174
+ | Flat row | 10,000 | 6.6 ms | 150 |
175
+ | Wrapped | 100 | 27 us | 36,500 |
176
+ | Wrapped | 1,000 | 280 us | 3,500 |
177
+ | Nested (84 nodes) | 84 | 204 us | 4,900 |
178
+ | Nested (780 nodes) | 780 | 5.2 ms | 192 |
179
+
180
+ ## Comparison
181
+
182
+ | | Noreflow | Yoga | Textura |
183
+ |---|---|---|---|
184
+ | Architecture | Purpose-built TS engine | C++ -> WASM | Wraps Yoga |
185
+ | Language | Pure TypeScript | WASM binary | WASM binary |
186
+ | API | Pure function, data in/out | Class-based, manual `.free()` | Imperative |
187
+ | Initialization | Synchronous | `await init()` | `await init()` |
188
+ | CSS Grid | Yes | No | No |
189
+ | Aspect Ratio | Yes | Yes | Yes |
190
+ | Absolute/Fixed | Yes | Yes | Yes |
191
+ | Bundle | ~10 KB gzip | ~45 KB (WASM) | ~45 KB+ (WASM) |
192
+ | Dependencies | Zero | None | yoga-layout + pretext |
193
+ | Debugging | JS debugger | WASM boundary | WASM boundary |
194
+ | Tree-shakeable | Yes | No | No |
195
+
196
+ ## Current Limitations
197
+
198
+ - Writing modes / RTL
199
+ - Baseline alignment
200
+ - Intrinsic sizing keywords (`min-content`, `max-content`, `fit-content`)
201
+ - `visibility: collapse`
202
+ - `position: sticky`
203
+
204
+ These are planned for future releases.
205
+
206
+ ## Development
207
+
208
+ ```bash
209
+ pnpm install
210
+ pnpm test # Run tests
211
+ pnpm test:watch # Watch mode
212
+ pnpm bench # Run benchmarks
213
+ pnpm build # Build ESM + CJS + types
214
+ pnpm typecheck # Type-check
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT