@blender-ui/react 0.0.0-stage → 0.2.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 CHANGED
@@ -1,3 +1,171 @@
1
- # Temporary Holding Version
1
+ # @blender-ui/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@blender-ui/react)](https://www.npmjs.com/package/@blender-ui/react)
4
+ [![license](https://img.shields.io/npm/l/@blender-ui/react)](https://github.com/ppmpreetham/BUI/blob/main/LICENSE)
5
+
6
+ React 19 for the desktop, drawn by [BUI](https://github.com/ppmpreetham/BUI),
7
+ Blender's UI toolkit. You write JSX with HTML elements, CSS and Tailwind
8
+ classes, and BUI renders it in native windows on the CPU or the GPU. There's
9
+ no browser, no webview and no Electron.
10
+
11
+ - **React's hooks, by React's names, holding signals.** A change updates the
12
+ one node that shows it, without re-rendering the component.
13
+ - **Node.js and Bun.**
14
+ - **One small executable:** `bui build` makes an app of about 5 MB that
15
+ reaches its first frame in about 70 ms.
16
+
17
+ ```sh
18
+ npm install @blender-ui/react react # or: bun add @blender-ui/react react
19
+ ```
20
+
21
+ ```jsx
22
+ /** @jsxImportSource @blender-ui/react */
23
+ import { render, useState } from '@blender-ui/react'
24
+
25
+ function Counter () {
26
+ const [count, setCount] = useState(0)
27
+ return (
28
+ <main className="p-6 flex flex-col gap-3 bg-[#2b2b2b] text-[#e6e6e6]">
29
+ <span>Clicks: {count}</span>
30
+ <button onClick={() => setCount(c => c + 1)}>Add one</button>
31
+ </main>
32
+ )
33
+ }
34
+
35
+ render(<Counter />, { title: 'Counter', width: 360, height: 200, gpu: true })
36
+ ```
37
+
38
+ ```sh
39
+ bun app.jsx # run it (Bun reads JSX as is)
40
+ npx bui build app.jsx # ship it: app.exe, one ~5 MB file
41
+ ```
42
+
43
+ Put `"jsxImportSource": "@blender-ui/react"` in your `tsconfig.json`, or in
44
+ your bundler's JSX options; the `/** @jsxImportSource */` comment works per
45
+ file. Without JSX, use this package's `createElement`.
46
+
47
+ ## Hooks
48
+
49
+ The hooks have React's names, arguments and timing, but the state they hold
50
+ is a **signal**:
51
+
52
+ - **Passed into JSX** (`<span>{count}</span>`, `style={styleMemo}`,
53
+ `style={{ color: colorSignal }}`): a change updates that node. The
54
+ component doesn't re-render.
55
+ - **Read while rendering** (`count.value`; in plain JavaScript also
56
+ `count + 1` or `count > 5`): the component re-renders when it changes, as
57
+ with React.
58
+ - **Conditions and `===` need `.value`.** A signal is an object, so it's
59
+ always truthy: write `{isPending.value && <Spinner />}` and
60
+ `count.value === 0`. TypeScript also needs `.value` for arithmetic.
61
+
62
+ | Hook | Returns |
63
+ |---|---|
64
+ | `useState(init)` | `[signal, setState]` |
65
+ | `useReducer(reducer, arg, init?)` | `[signal, dispatch]` |
66
+ | `useMemo(fn, deps?)` | a memo, recomputed when the signals it reads change |
67
+ | `useRef(v)` | `{ current }`, observable by effects and memos |
68
+ | `useCallback(fn, deps?)` / `useEffectEvent(fn)` | a function whose calls are batched |
69
+ | `useEffect` / `useLayoutEffect` / `useInsertionEffect` | React's timing and deps, plus a re-run when the signals the effect read change |
70
+ | `useSyncExternalStore(subscribe, get)` | a signal following the store |
71
+ | `useTransition()` | `[isPending, startTransition]`; signal updates run after urgent ones |
72
+ | `useDeferredValue(value, initial?)` | a signal that follows `value` at low priority |
73
+ | `useActionState(action, initial)` | `[state, dispatch, isPending]` |
74
+ | `useOptimistic(state, reducer?)` | `[optimistic, addOptimistic]` |
75
+ | `use`, `useContext`, `useId`, `useDebugValue`, `useImperativeHandle` | React's own |
76
+
77
+ Also exported:
78
+
79
+ - **Rust-style names:** `useSignal(v)` and `useComputed(fn)`.
80
+ - **`useSignalValue(sig)`:** re-renders its component when `sig` changes.
81
+ - **Plain signals:** `signal`, `computed`, `effect`, `batch`, `untrack`,
82
+ `startTransition`.
83
+
84
+ React's plain-value hooks still work: import them from `react`.
85
+
86
+ Every hook is built from React's public hooks. A conformance suite runs each
87
+ one beside React's original, in development and production builds and under
88
+ StrictMode. It fails when a React release adds a hook this package doesn't
89
+ cover yet.
90
+
91
+ ## The renderer
92
+
93
+ - **Host components are HTML elements.** `className` takes Tailwind classes,
94
+ `style` takes strings, objects or signals, and the mouse, keyboard and form
95
+ events work (`onClick`, `onChange` ...). Events bubble, and
96
+ `stopPropagation` works.
97
+ - **One native batch per commit:** only what changed crosses to BUI's UI
98
+ thread.
99
+ - **React 19.3:** `<Suspense>`, `<Activity>`, transitions, actions and
100
+ `<ViewTransition>`, which commits without animating.
101
+ - **API:** `render(element, options)` opens a window. `createRoot(window)`
102
+ renders into one you opened with
103
+ [`@blender-ui/native`](https://www.npmjs.com/package/@blender-ui/native),
104
+ and `act(fn)` is for tests.
105
+ - Window options: `title`, `width`, `height`, `resizable`, `gpu`,
106
+ `slug`, `headless`, and `theme` (`'dark'`, the default, or `'light'`:
107
+ Blender's Light theme; `BUI_THEME` applies otherwise).
108
+
109
+ ## Standalone apps
110
+
111
+ `bui` comes with this package, through `@blender-ui/native`:
112
+
113
+ ```sh
114
+ npx bui build app.jsx # app.exe: BUI + embedded QuickJS, ~5 MB
115
+ npx bui build app.jsx --bun # app.exe: Bun, with Node's APIs and a JIT, ~115 MB
116
+ npx bui run app.jsx # run on the QuickJS runtime, no build
117
+ ```
118
+
119
+ Options:
120
+
121
+ - `-o <file>`: the output file.
122
+ - `--console`: keep a console window for logs.
123
+ - `--dev`: React's development build.
124
+
125
+ Bundling uses [Bun](https://bun.sh), so it must be installed.
126
+
127
+ The default build runs your app on QuickJS, a small JavaScript engine embedded
128
+ in BUI. It has no Node modules (`fs` and the like) and no JIT, so keep large
129
+ lists updating through signals, or use `--bun`.
130
+
131
+ ## Benchmarks
132
+
133
+ Ryzen 7 5800U laptop, Windows 11, GPU renderer, React's production build,
134
+ measured before the latest features landed.
135
+ The full method is in the [guide](https://github.com/ppmpreetham/BUI/blob/main/docs/javascript.md#performance).
136
+
137
+ **Startup:** a small React app that quits at its first frame; medians of 12
138
+ launches.
139
+
140
+ | | First frame | Launch to exit | RAM | Size |
141
+ |---|---:|---:|---:|---:|
142
+ | **`bui build`** (QuickJS) | **68 ms** | **104 ms** | **19 MB** | **4.9 MB** |
143
+ | `bui build --bun` | 79 ms | 147 ms | 35 MB | 114 MB |
144
+ | `bun app.js` | 80 ms | 152 ms | 35 MB | |
145
+ | `node app.js` | 108 ms | 187 ms | 45 MB | |
146
+
147
+ **Animation:** 3,000 cells, one changing per frame.
148
+
149
+ - **With signals:** the changing values are signals, so the grid renders once.
150
+ - **With state:** the grid re-renders every frame.
151
+
152
+ | 3,000 cells | FPS | CPU per frame | RAM |
153
+ |---|---:|---:|---:|
154
+ | Bun: with signals / with state | 58.4 / 55.8 | 13.3 / 20.8 ms | 85 / 118 MB |
155
+ | Node: with signals / with state | 56.7 / 54.8 | 14.0 / 18.9 ms | 93 / 121 MB |
156
+ | QuickJS: with signals / with state | 58.7 / 19.7 | 14.9 / 51.0 ms | 61 / 67 MB |
157
+ | *Rust, for reference* | 59.6 | 12.0 ms | 43 MB |
158
+
159
+ ## Requirements
160
+
161
+ - **Windows x64** prebuilt. On **Linux**, build the `@blender-ui/native`
162
+ addon from source (`npm run build` in `bindings/node`) and windows are
163
+ native X11 windows. On **macOS** it's headless for now. `headless: true`
164
+ renders off-screen anywhere.
165
+ - **Node.js 18** or newer, or Bun.
166
+ - **React 19.3.**
167
+
168
+ Guide: [docs/javascript.md](https://github.com/ppmpreetham/BUI/blob/main/docs/javascript.md) ·
169
+ Website: [bui.preetham.top](https://bui.preetham.top) ·
170
+ Source: [github.com/ppmpreetham/BUI](https://github.com/ppmpreetham/BUI) ·
171
+ License: GPL-2.0-or-later
package/hooks.js ADDED
@@ -0,0 +1,397 @@
1
+ // React's hooks, by React's names, with signals for state.
2
+ //
3
+ // `useState`, `useReducer`, `useMemo` ... take React's arguments, run at
4
+ // React's moments and return React's shapes, but the state they hold is a
5
+ // signal. Pass it as a prop or child (`<span>{count}</span>`) and a change
6
+ // updates that one node: the component doesn't re-render. Read it while
7
+ // rendering (`count.value`, `count + 1`, a condition) and the component
8
+ // re-renders when it changes, exactly as with React's own hooks.
9
+ //
10
+ // The second part needs to know which component is rendering: components
11
+ // created through this package's JSX runtime (`jsxImportSource:
12
+ // "@blender-ui/react"`) or `createElement` are wrapped with `track()`.
13
+ //
14
+ // Built only on React's public hooks (useState, useReducer, useRef, useMemo,
15
+ // the effect hooks, startTransition), so React's semantics (StrictMode,
16
+ // concurrent rendering) come from React itself. test/hooks.test.js runs each
17
+ // hook beside React's original and fails when React adds a hook that has
18
+ // neither a signal version here nor an entry in NO_TWIN.
19
+ //
20
+ // SPDX-License-Identifier: GPL-2.0-or-later
21
+ 'use strict'
22
+
23
+ const React = require('react')
24
+ const S = require('@blender-ui/native')
25
+ const { signal, memo, effect, batch, untrack, isSignal, Tracker, Ref } = S
26
+
27
+ /** React hooks re-exported unchanged, and why (the coverage test reads this). */
28
+ const NO_TWIN = {
29
+ use: 'suspends rendering: inherently a render-time read',
30
+ useContext: 'pass a signal as the context value: consumers bind to it without re-rendering',
31
+ useDebugValue: 'no state',
32
+ useId: 'no state',
33
+ useImperativeHandle: 'no state (works with useRef)',
34
+ }
35
+
36
+ // The latest value, for callbacks that outlive the render that made them.
37
+ function useLatest (value) {
38
+ const ref = React.useRef(value)
39
+ ref.current = value
40
+ return ref
41
+ }
42
+
43
+ /** A component-owned value, created once. */
44
+ function useOnce (create) {
45
+ return React.useState(create)[0]
46
+ }
47
+
48
+ // ---- render tracking -----------------------------------------------------------
49
+
50
+ const bump = (n) => n + 1
51
+
52
+ /** Re-render this component when a signal its render read changes. */
53
+ function useRenderTracker () {
54
+ const [, force] = React.useReducer(bump, 0)
55
+ const tracker = useOnce(() => new Tracker(() => force()))
56
+ const reads = React.useRef(null)
57
+ // after each commit: follow what that render read
58
+ React.useLayoutEffect(() => { tracker.commit(reads.current) })
59
+ // mount / unmount (StrictMode re-mounts: pause, then resume)
60
+ React.useLayoutEffect(() => {
61
+ tracker.resume()
62
+ return () => tracker.pause()
63
+ }, [tracker])
64
+ return { tracker, reads }
65
+ }
66
+
67
+ const tracked = new WeakMap()
68
+ const MEMO = Symbol.for('react.memo')
69
+ const FORWARD_REF = Symbol.for('react.forward_ref')
70
+
71
+ /** `type` with render tracking: function components, `memo` and `forwardRef`. */
72
+ function track (type) {
73
+ if (type == null) return type
74
+ if (typeof type === 'function') {
75
+ if (type.prototype?.isReactComponent) return type // class components: no hooks
76
+ } else if (typeof type !== 'object' || (type.$$typeof !== MEMO && type.$$typeof !== FORWARD_REF)) {
77
+ return type // host components, fragments, providers, lazy ...
78
+ }
79
+ let t = tracked.get(type)
80
+ if (t !== undefined) return t
81
+ if (typeof type === 'function') {
82
+ t = function (props, secondArg) {
83
+ const { tracker, reads } = useRenderTracker()
84
+ const [result, r] = tracker.run(() => type(props, secondArg))
85
+ reads.current = r
86
+ return result
87
+ }
88
+ Object.defineProperty(t, 'name', { value: type.name })
89
+ t.displayName = type.displayName || type.name
90
+ } else if (type.$$typeof === MEMO) {
91
+ t = React.memo(track(type.type), type.compare)
92
+ t.displayName = type.displayName
93
+ } else {
94
+ t = React.forwardRef(track(type.render))
95
+ t.displayName = type.displayName
96
+ }
97
+ tracked.set(type, t)
98
+ tracked.set(t, t)
99
+ return t
100
+ }
101
+
102
+ /** React's `createElement`, with render tracking for components. */
103
+ function createElement (type, props, ...children) {
104
+ return React.createElement(track(type), props, ...children)
105
+ }
106
+
107
+ // ---- state -----------------------------------------------------------------
108
+
109
+ /** Rust's `use_signal`: a signal owned by this component. */
110
+ function useSignal (initial) {
111
+ return useOnce(() => signal(typeof initial === 'function' ? initial() : initial))
112
+ }
113
+
114
+ /** `[signal, setState]`; `setState(v | prev => next)`; the same value: no update. */
115
+ function useState (initialState) {
116
+ const s = useSignal(initialState)
117
+ const set = React.useCallback((v) => s.set(typeof v === 'function' ? v(s.peek()) : v), [s])
118
+ return [s, set]
119
+ }
120
+
121
+ /** `[signal, dispatch]`, with the latest render's reducer (as React does). */
122
+ function useReducer (reducer, initialArg, init) {
123
+ const s = useOnce(() => signal(init !== undefined ? init(initialArg) : initialArg))
124
+ const latest = useLatest(reducer)
125
+ const dispatch = React.useCallback((action) => s.set(latest.current(s.peek(), action)), [s])
126
+ return [s, dispatch]
127
+ }
128
+
129
+ /** `{ current }`, observable by effects and memos (an element mounting re-runs
130
+ * them); reading it while rendering doesn't subscribe, as with React's refs. */
131
+ function useRef (initialValue) {
132
+ return useOnce(() => new Ref(initialValue))
133
+ }
134
+
135
+ // ---- derived values ----------------------------------------------------------
136
+
137
+ /** A memo: recomputed when the signals it reads change, and replaced (like
138
+ * useMemo's value) when `deps` change; without deps, every render. */
139
+ function useMemo (fn, deps) {
140
+ const latest = useLatest(fn)
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ return React.useMemo(() => memo(() => latest.current()), deps)
143
+ }
144
+
145
+ /** Rust's `Memo::new`: `useMemo` with `deps = []` by default. */
146
+ function useComputed (fn, deps = []) {
147
+ return useMemo(fn, deps)
148
+ }
149
+
150
+ /** React's `useCallback`; calls are batched (their writes make one update) and
151
+ * untracked (calling it in an effect doesn't subscribe to what it reads). */
152
+ function useCallback (fn, deps) {
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
154
+ return React.useCallback((...args) => batch(() => untrack(() => fn(...args))), deps)
155
+ }
156
+
157
+ /** React's `useEffectEvent`: a stable function that calls the latest render's
158
+ * `fn`; calls are batched and untracked (an effect calling it doesn't
159
+ * subscribe to the signals `fn` reads). */
160
+ function useEffectEvent (fn) {
161
+ const latest = useLatest(fn)
162
+ return React.useCallback((...args) => batch(() => untrack(() => latest.current(...args))), [latest])
163
+ }
164
+
165
+ // ---- effects -----------------------------------------------------------------
166
+
167
+ function effectHook (reactHook) {
168
+ return function (fn, deps) {
169
+ const latest = useLatest(fn)
170
+ // React's timing and deps; in between, re-runs when the signals it read change
171
+ reactHook(() => effect(() => latest.current()), deps)
172
+ }
173
+ }
174
+
175
+ /** React's `useEffect`, also re-run when the signals it read change. */
176
+ const useEffect = effectHook(React.useEffect)
177
+ /** React's `useLayoutEffect`, also re-run when the signals it read change. */
178
+ const useLayoutEffect = effectHook(React.useLayoutEffect)
179
+ /** React's `useInsertionEffect`, also re-run when the signals it read change. */
180
+ const useInsertionEffect = effectHook(React.useInsertionEffect)
181
+
182
+ // ---- external stores ---------------------------------------------------------
183
+
184
+ /** A signal following the store. */
185
+ function useSyncExternalStore (subscribe, getSnapshot) {
186
+ const s = useOnce(() => signal(getSnapshot()))
187
+ const latest = useLatest(getSnapshot)
188
+ React.useLayoutEffect(() => {
189
+ const check = () => s.set(latest.current())
190
+ check() // changed between render and subscribe
191
+ return subscribe(check)
192
+ }, [subscribe, s])
193
+ React.useLayoutEffect(() => { s.set(latest.current()) }) // a new getSnapshot
194
+ return s
195
+ }
196
+
197
+ /** Re-render this component when `source` changes; returns its value (for
198
+ * components rendered without this package's JSX runtime). */
199
+ function useSignalValue (source) {
200
+ const subscribe = React.useCallback((onChange) => {
201
+ let first = true
202
+ return effect(() => {
203
+ source.value // subscribe
204
+ if (first) first = false
205
+ else onChange()
206
+ })
207
+ }, [source])
208
+ return React.useSyncExternalStore(subscribe, () => source.peek())
209
+ }
210
+
211
+ // ---- transitions and actions -------------------------------------------------
212
+
213
+ // Run `action` as a BUI transition and a React transition at once: signal
214
+ // writes defer their effects, React state updates render at transition priority.
215
+ // React tracks the action's promise (its optimistic updates last until it
216
+ // settles) but never sees its rejection: each hook sends errors where React's does.
217
+ const quiet = () => {}
218
+ function transition (action) {
219
+ return S.startTransition(() => {
220
+ let result
221
+ let failure = null
222
+ React.startTransition(() => {
223
+ try {
224
+ result = action()
225
+ } catch (error) {
226
+ failure = { error } // React would report it globally and swallow it
227
+ return
228
+ }
229
+ return result && typeof result.then === 'function' ? result.then(quiet, quiet) : undefined
230
+ })
231
+ if (failure !== null) throw failure.error
232
+ return result
233
+ })
234
+ }
235
+
236
+ /** React's `startTransition`, for signal writes too. Returns a promise that
237
+ * settles when the action and the updates it caused are done. */
238
+ function startTransition (action) {
239
+ return transition(action)
240
+ }
241
+
242
+ /** A setter that rethrows `error` during render: React's way to reach the
243
+ * nearest error boundary from an async callback. */
244
+ function useThrowToBoundary () {
245
+ const [, set] = React.useState(null)
246
+ return React.useCallback((error) => set(() => { throw error }), [])
247
+ }
248
+
249
+ /** `[isPending signal, startTransition(action)]`. `action` may be async;
250
+ * isPending stays true until it settles and its deferred updates have run.
251
+ * An error goes to the nearest error boundary. */
252
+ function useTransition () {
253
+ const isPending = useOnce(() => signal(false))
254
+ const fail = useThrowToBoundary()
255
+ const start = React.useCallback((action) => {
256
+ isPending._count = (isPending._count ?? 0) + 1
257
+ S.urgent(() => isPending.set(true))
258
+ const settle = () => {
259
+ if (--isPending._count === 0) S.urgent(() => isPending.set(false))
260
+ }
261
+ let done
262
+ try {
263
+ done = transition(action)
264
+ } catch (error) {
265
+ settle()
266
+ return fail(error)
267
+ }
268
+ done.then(settle, (error) => { settle(); fail(error) })
269
+ }, [isPending, fail])
270
+ return [isPending, start]
271
+ }
272
+
273
+ /** A signal following `value` (a signal or a value) at transition priority,
274
+ * after urgent updates; `initialValue` first, if given. */
275
+ function useDeferredValue (value, initialValue) {
276
+ const read = () => (isSignal(value) ? value.peek() : value)
277
+ const out = useOnce(() => signal(initialValue !== undefined ? initialValue : read()))
278
+ const latest = useLatest(value)
279
+ React.useEffect(() => {
280
+ let scheduled = false
281
+ let live = true
282
+ const push = () => {
283
+ if (scheduled) return
284
+ scheduled = true
285
+ S.scheduleDeferred(() => {
286
+ scheduled = false
287
+ if (!live) return
288
+ const v = latest.current
289
+ out.set(isSignal(v) ? v.peek() : v)
290
+ })
291
+ }
292
+ if (isSignal(value)) {
293
+ const stop = effect(() => { value.value; push() })
294
+ return () => { live = false; stop() }
295
+ }
296
+ push()
297
+ return () => { live = false }
298
+ }, [value, out])
299
+ return out
300
+ }
301
+
302
+ /** `[state signal, dispatch, isPending signal]`. Dispatches queue and run in
303
+ * order, each with the previous state; an action may be async. A thrown
304
+ * error goes to the nearest error boundary and drops the queue, as in React. */
305
+ function useActionState (action, initialState) {
306
+ const state = useOnce(() => signal(initialState))
307
+ const isPending = useOnce(() => signal(false))
308
+ const latest = useLatest(action)
309
+ const queue = useOnce(() => ({ items: [], running: false }))
310
+ const fail = useThrowToBoundary()
311
+
312
+ const runNext = React.useCallback(() => {
313
+ const payload = queue.items.shift()
314
+ queue.running = true
315
+ let result
316
+ try {
317
+ transition(() => {
318
+ result = latest.current(state.peek(), payload)
319
+ return result
320
+ }).catch(quiet) // the error is handled below, from `result`
321
+ } catch (error) {
322
+ result = Promise.reject(error)
323
+ }
324
+ Promise.resolve(result).then((next) => {
325
+ const more = queue.items.length > 0
326
+ batch(() => {
327
+ state.set(next)
328
+ if (!more) isPending.set(false)
329
+ })
330
+ if (more) runNext()
331
+ else queue.running = false
332
+ }, (err) => {
333
+ queue.items.length = 0
334
+ queue.running = false
335
+ isPending.set(false)
336
+ fail(err)
337
+ })
338
+ }, [state, isPending, queue, fail])
339
+
340
+ const dispatch = React.useCallback((payload) => {
341
+ queue.items.push(payload)
342
+ if (!queue.running) {
343
+ S.urgent(() => isPending.set(true))
344
+ runNext()
345
+ }
346
+ }, [queue, isPending, runNext])
347
+
348
+ return [state, dispatch, isPending]
349
+ }
350
+
351
+ /** `[optimistic signal, addOptimistic]`. Updates added during a transition or
352
+ * action show at once and are dropped when it ends; `passthrough` (a signal
353
+ * or a value) shows through otherwise. */
354
+ function useOptimistic (passthrough, reducer) {
355
+ const updates = useOnce(() => signal([]))
356
+ const latestReducer = useLatest(reducer)
357
+ const out = React.useMemo(() => memo(() => {
358
+ let v = isSignal(passthrough) ? passthrough.value : passthrough
359
+ for (const u of updates.value) v = latestReducer.current ? latestReducer.current(v, u.action) : u.action
360
+ return v
361
+ }), [passthrough, updates])
362
+ const add = React.useCallback((action) => {
363
+ const entry = { action }
364
+ const remove = () => S.urgent(() => updates.set(updates.peek().filter((e) => e !== entry)))
365
+ S.urgent(() => updates.set([...updates.peek(), entry]))
366
+ if (!S.onTransitionDone(remove)) {
367
+ console.error('An optimistic state update occurred outside a transition or action. ' +
368
+ 'To fix, move the update to an action, or wrap it in startTransition.')
369
+ setTimeout(remove, 0)
370
+ }
371
+ }, [updates])
372
+ return [out, add]
373
+ }
374
+
375
+ module.exports = {
376
+ NO_TWIN,
377
+ track,
378
+ createElement,
379
+ startTransition,
380
+ useSignal,
381
+ useComputed,
382
+ useSignalValue,
383
+ useState,
384
+ useReducer,
385
+ useRef,
386
+ useMemo,
387
+ useCallback,
388
+ useEffectEvent,
389
+ useEffect,
390
+ useLayoutEffect,
391
+ useInsertionEffect,
392
+ useSyncExternalStore,
393
+ useTransition,
394
+ useDeferredValue,
395
+ useActionState,
396
+ useOptimistic,
397
+ }
package/index.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ // Types for @blender-ui/react.
2
+ import type { ReactNode, ReactElement, DependencyList, EffectCallback, Dispatch, SetStateAction, ElementType } from 'react'
3
+ import type { App, Window, AppOptions, WindowOptions, Signal, Memo, ReadSignal, Ref } from '@blender-ui/native'
4
+
5
+ export interface Root {
6
+ render (element: ReactNode): void
7
+ unmount (): void
8
+ }
9
+
10
+ /** A React root rendering into `window`'s body (unmounted when the window closes). */
11
+ export function createRoot (window: Window): Root
12
+
13
+ /** Open a window (and an App, unless `app` is given) and render `element` into it. */
14
+ export function render (element: ReactNode, options?: WindowOptions & AppOptions & { app?: App }):
15
+ { app: App, window: Window, root: Root }
16
+
17
+ /** Run `fn` and flush React's work synchronously (tests). */
18
+ export function act<R> (fn: () => R): R
19
+
20
+ /** React's `createElement`, with render tracking for components. */
21
+ export const createElement: typeof import('react').createElement
22
+ /** A component type with render tracking (re-renders when a signal its render read changes). */
23
+ export function track<T extends ElementType> (type: T): T
24
+
25
+ // React's hooks that hold no state, as React exports them.
26
+ export { use, useContext, useDebugValue, useId, useImperativeHandle } from 'react'
27
+
28
+ // Signals.
29
+ export { signal, effect, batch, untrack, createSignal, isSignal, Signal, Memo, ReadSignal, Ref } from '@blender-ui/native'
30
+ /** A derived, cached signal (`memo` in @blender-ui/native; here `memo` stays React's). */
31
+ export { memo as computed, ref } from '@blender-ui/native'
32
+
33
+ /** React hooks re-exported unchanged, and why. */
34
+ export const NO_TWIN: Record<string, string>
35
+
36
+ // React's hooks by React's names, with signals for state. Same arguments and
37
+ // timing as React's. A signal passed as a prop or child updates that node
38
+ // alone; read while rendering (`.value`, `count + 1`), it re-renders the
39
+ // component when it changes. Conditions and `===` need `.value`: a signal
40
+ // object is always truthy.
41
+
42
+ /** `[signal, setState]`: `setState(v)` or `setState(prev => next)`. */
43
+ export function useState<T> (initialState: T | (() => T)): [Signal<T>, Dispatch<SetStateAction<T>>]
44
+ export function useState<T = undefined> (): [Signal<T | undefined>, Dispatch<SetStateAction<T | undefined>>]
45
+ /** `[signal, dispatch]`, with the latest render's reducer. */
46
+ export function useReducer<S, A> (reducer: (state: S, action: A) => S, initialArg: S): [Signal<S>, Dispatch<A>]
47
+ export function useReducer<S, A, I> (reducer: (state: S, action: A) => S, initialArg: I, init: (arg: I) => S): [Signal<S>, Dispatch<A>]
48
+ /** `{ current }`, observable by effects and memos; reading it while rendering doesn't subscribe. */
49
+ export function useRef<T> (initialValue: T): Ref<T>
50
+ export function useRef<T = undefined> (): Ref<T | undefined>
51
+ /** A memo following its signals, replaced when `deps` change (without deps: every render). */
52
+ export function useMemo<T> (fn: () => T, deps?: DependencyList): Memo<T>
53
+ /** React's `useCallback`; calls are batched and untracked. */
54
+ export function useCallback<F extends (...args: any[]) => any> (fn: F, deps?: DependencyList): F
55
+ /** React's `useEffectEvent`; calls are batched and untracked. */
56
+ export function useEffectEvent<F extends (...args: any[]) => any> (fn: F): F
57
+ /** React's `useEffect`, also re-run when the signals it read change. */
58
+ export function useEffect (effect: EffectCallback, deps?: DependencyList): void
59
+ /** React's `useLayoutEffect`, also re-run when the signals it read change. */
60
+ export function useLayoutEffect (effect: EffectCallback, deps?: DependencyList): void
61
+ /** React's `useInsertionEffect`, also re-run when the signals it read change. */
62
+ export function useInsertionEffect (effect: EffectCallback, deps?: DependencyList): void
63
+ /** A signal following the store. */
64
+ export function useSyncExternalStore<T> (subscribe: (onChange: () => void) => () => void, getSnapshot: () => T,
65
+ getServerSnapshot?: () => T): Signal<T>
66
+ /** `[isPending, startTransition]`: what the action's writes trigger runs after urgent updates. */
67
+ export function useTransition (): [Signal<boolean>, (action: () => void | Promise<void>) => void]
68
+ /** React's `startTransition`, for signal writes too; settles when the action and its updates are done. */
69
+ export function startTransition (action: () => void | Promise<void>): Promise<void>
70
+ /** A signal following `value` (a signal or a value) at transition priority. */
71
+ export function useDeferredValue<T> (value: T | ReadSignal<T>, initialValue?: T): Signal<T>
72
+ /** `[state, dispatch, isPending]`: actions run in order, each with the previous state. */
73
+ export function useActionState<S, P = void> (action: (state: S, payload: P) => S | Promise<S>, initialState: S,
74
+ permalink?: string): [Signal<S>, (payload: P) => void, Signal<boolean>]
75
+ /** `[optimistic, addOptimistic]`: updates last until their transition or action ends. */
76
+ export function useOptimistic<S> (passthrough: S | ReadSignal<S>): [Memo<S>, (value: S) => void]
77
+ export function useOptimistic<S, A> (passthrough: S | ReadSignal<S>, reducer: (state: S, action: A) => S):
78
+ [Memo<S>, (action: A) => void]
79
+
80
+ /** Rust's `use_signal`: a signal owned by this component. */
81
+ export function useSignal<T> (initial: T | (() => T)): Signal<T>
82
+ /** Rust's `Memo::new`: `useMemo(fn, deps = [])`. */
83
+ export function useComputed<T> (fn: () => T, deps?: DependencyList): Memo<T>
84
+ /** Re-render this component when `source` changes; returns its value (components
85
+ * rendered without this package's JSX runtime or createElement). */
86
+ export function useSignalValue<T> (source: ReadSignal<T>): T
87
+
88
+ export type { ReactElement }
89
+
90
+ // A signal is a valid child: `<span>Clicks: {count}</span>`.
91
+ declare module 'react' {
92
+ interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES {
93
+ buiSignal: ReadSignal<unknown>
94
+ }
95
+ }
96
+
97
+ /** Blender's widgets as components: always drawn and handled like Blender.
98
+ * Their lowercase HTML counterparts look like a browser's form controls
99
+ * (and like Blender inside an element with the `styled` attribute). */
100
+ type BlenderProps = Record<string, unknown> & { children?: ReactNode }
101
+ export function Button (props: BlenderProps): ReactNode
102
+ export function Checkbox (props: BlenderProps): ReactNode
103
+ export function Radio (props: BlenderProps): ReactNode
104
+ export function NumberField (props: BlenderProps): ReactNode
105
+ export function NumberSlider (props: BlenderProps): ReactNode
106
+ export function TextField (props: BlenderProps): ReactNode
107
+ export function TextArea (props: BlenderProps): ReactNode
108
+ export function ColorSwatch (props: BlenderProps): ReactNode
109
+ export function Dropdown (props: BlenderProps): ReactNode
110
+ export function ProgressBar (props: BlenderProps): ReactNode