@blender-ui/react 0.2.1 → 0.3.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 +2 -0
- package/index.js +6 -91
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ React 19 for the desktop, drawn by [BUI](https://github.com/ppmpreetham/BUI),
|
|
|
7
7
|
Blender's UI toolkit. You write JSX with HTML elements, CSS and Tailwind
|
|
8
8
|
classes, and BUI renders it in native windows on the CPU or the GPU. There's
|
|
9
9
|
no browser, no webview and no Electron.
|
|
10
|
+
Prefer Svelte? [`@blender-ui/svelte`](https://www.npmjs.com/package/@blender-ui/svelte)
|
|
11
|
+
is the same engine for Svelte 5.
|
|
10
12
|
|
|
11
13
|
- **React's hooks, by React's names, holding signals.** A change updates the
|
|
12
14
|
one node that shows it, without re-rendering the component.
|
package/index.js
CHANGED
|
@@ -16,7 +16,8 @@ const Reconciler = require('react-reconciler')
|
|
|
16
16
|
const { ConcurrentRoot, DefaultEventPriority, DiscreteEventPriority, NoEventPriority } =
|
|
17
17
|
require('react-reconciler/constants')
|
|
18
18
|
const native = require('@blender-ui/native')
|
|
19
|
-
const {
|
|
19
|
+
const { TextNode, Signal, Memo, isSignal } = native
|
|
20
|
+
const { open, blenderWidgets, applyProps, isText, NO_PROPS } = require('@blender-ui/native/host')
|
|
20
21
|
const hooks = require('./hooks')
|
|
21
22
|
|
|
22
23
|
// A signal rendered as a child (`<span>{count}</span>`) becomes this host
|
|
@@ -35,85 +36,6 @@ for (const C of [Signal, Memo]) {
|
|
|
35
36
|
})
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
// React prop -> BUI event; onChange is per keystroke on text fields, as in React DOM.
|
|
39
|
-
const EVENTS = {
|
|
40
|
-
onClick: 'click', onMouseDown: 'mousedown', onMouseUp: 'mouseup', onMouseMove: 'mousemove',
|
|
41
|
-
onMouseEnter: 'mouseenter', onMouseLeave: 'mouseleave', onWheel: 'wheel',
|
|
42
|
-
onKeyDown: 'keydown', onKeyUp: 'keyup', onInput: 'input', onChange: 'change',
|
|
43
|
-
}
|
|
44
|
-
const TEXT_INPUTS = new Set(['textarea'])
|
|
45
|
-
const ATTR_ALIASES = { className: 'class', htmlFor: 'for', tabIndex: 'tabindex' }
|
|
46
|
-
const SKIP = new Set(['children', 'key', 'ref', 'dangerouslySetInnerHTML', 'suppressHydrationWarning',
|
|
47
|
-
'suppressContentEditableWarning'])
|
|
48
|
-
const NO_PROPS = {}
|
|
49
|
-
|
|
50
|
-
function eventName (el, prop) {
|
|
51
|
-
if (prop === 'onChange') {
|
|
52
|
-
// text fields report every edit as `input`; checkboxes, selects: `change`
|
|
53
|
-
const type = el.getAttribute('type')
|
|
54
|
-
if (TEXT_INPUTS.has(el.localName) || (el.localName === 'input' &&
|
|
55
|
-
!['checkbox', 'radio', 'range', 'color', 'file'].includes(type))) return 'input'
|
|
56
|
-
}
|
|
57
|
-
return EVENTS[prop]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Listen with `fn` (or stop, when it isn't a function) for React prop `k`.
|
|
61
|
-
* The DOM event is remembered with it: onChange's depends on `type`. */
|
|
62
|
-
function listen (el, k, fn) {
|
|
63
|
-
const cur = el._react[k]
|
|
64
|
-
const type = typeof fn === 'function' ? eventName(el, k) : null
|
|
65
|
-
if (cur !== undefined && cur.fn === fn && cur.type === type) return
|
|
66
|
-
if (cur !== undefined) el.removeEventListener(cur.type, cur.fn)
|
|
67
|
-
if (type === null) delete el._react[k]
|
|
68
|
-
else {
|
|
69
|
-
el.addEventListener(type, fn)
|
|
70
|
-
el._react[k] = { type, fn }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function isText (children) {
|
|
75
|
-
return typeof children === 'string' || typeof children === 'number' || typeof children === 'bigint'
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Apply the difference between two prop objects to an element. Hot: React
|
|
79
|
-
* calls it for every element of a re-rendered list, mostly with equal props. */
|
|
80
|
-
function applyProps (el, prev, next) {
|
|
81
|
-
if (prev !== NO_PROPS) {
|
|
82
|
-
for (const k in prev) {
|
|
83
|
-
if (k in next || SKIP.has(k)) continue
|
|
84
|
-
if (k in EVENTS) {
|
|
85
|
-
listen(el, k, null)
|
|
86
|
-
} else {
|
|
87
|
-
el.removeAttribute(ATTR_ALIASES[k] ?? k)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
let events = false
|
|
92
|
-
for (const k in next) {
|
|
93
|
-
const v = next[k]
|
|
94
|
-
if (prev[k] === v || SKIP.has(k)) continue
|
|
95
|
-
if (k in EVENTS) {
|
|
96
|
-
events = true // after the attributes: onChange's event depends on `type`
|
|
97
|
-
} else if (k === 'style') {
|
|
98
|
-
if (v == null) el.removeAttribute('style')
|
|
99
|
-
else el.style = v // a string, an object or a signal of either
|
|
100
|
-
} else if (v == null || v === false) {
|
|
101
|
-
el.removeAttribute(ATTR_ALIASES[k] ?? k)
|
|
102
|
-
} else if (isSignal(v)) {
|
|
103
|
-
el.setAttribute(ATTR_ALIASES[k] ?? k, v) // bound: updates without a re-render
|
|
104
|
-
} else {
|
|
105
|
-
el.setAttribute(ATTR_ALIASES[k] ?? k, v === true ? '' : v)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (events || (next.onChange !== undefined && prev.type !== next.type)) {
|
|
109
|
-
for (const k in next) if (k in EVENTS) listen(el, k, next[k])
|
|
110
|
-
}
|
|
111
|
-
// text children; text -> elements is React's resetTextContent (before it
|
|
112
|
-
// inserts them), so nothing to clear here
|
|
113
|
-
const c = next.children
|
|
114
|
-
if (c !== prev.children && isText(c)) el.textContent = String(c)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
39
|
// Nodes a render created. React may throw a render away (an interrupted or
|
|
118
40
|
// suspended concurrent render); its nodes are then never attached, and are
|
|
119
41
|
// freed after the next commit.
|
|
@@ -162,7 +84,6 @@ const host = {
|
|
|
162
84
|
createInstance (type, props, container) {
|
|
163
85
|
if (type === SIGNAL_TEXT) return created(container, container.window.createTextNode(props.signal))
|
|
164
86
|
const el = container.window.createElement(type)
|
|
165
|
-
el._react = {}
|
|
166
87
|
applyProps(el, NO_PROPS, props)
|
|
167
88
|
return created(container, el)
|
|
168
89
|
},
|
|
@@ -317,12 +238,11 @@ function createRoot (window) {
|
|
|
317
238
|
* Open a window and render `element` into it.
|
|
318
239
|
* @param {React.ReactNode} element
|
|
319
240
|
* @param {{ title?: string, width?: number, height?: number, resizable?: boolean,
|
|
320
|
-
* gpu?: boolean, slug?: boolean, headless?: boolean, app?: App }} [options]
|
|
321
|
-
* @returns {{ app: App, window: import('@blender-ui/native').Window, root: ReturnType<typeof createRoot> }}
|
|
241
|
+
* gpu?: boolean, slug?: boolean, headless?: boolean, app?: import('@blender-ui/native').App }} [options]
|
|
242
|
+
* @returns {{ app: import('@blender-ui/native').App, window: import('@blender-ui/native').Window, root: ReturnType<typeof createRoot> }}
|
|
322
243
|
*/
|
|
323
244
|
function render (element, options = {}) {
|
|
324
|
-
const
|
|
325
|
-
const window = app.window(options)
|
|
245
|
+
const { app, window } = open(options)
|
|
326
246
|
const root = createRoot(window)
|
|
327
247
|
root.render(element)
|
|
328
248
|
return { app, window, root }
|
|
@@ -342,12 +262,7 @@ for (const k of Object.keys(React)) if (/^use([A-Z]|$)/.test(k)) reactHooks[k] =
|
|
|
342
262
|
// Blender's widgets as components: `<NumberSlider min={0} max={10} />`,
|
|
343
263
|
// `<Button onClick={..}>Apply</Button>`. Always Blender widgets; their
|
|
344
264
|
// lowercase HTML counterparts look like a browser's (Blender's inside `styled`).
|
|
345
|
-
const blenderComponents =
|
|
346
|
-
for (const name of Object.keys(native.BLENDER_COMPONENTS)) {
|
|
347
|
-
const C = (props) => React.createElement(name, props)
|
|
348
|
-
Object.defineProperty(C, 'name', { value: name })
|
|
349
|
-
blenderComponents[name] = C
|
|
350
|
-
}
|
|
265
|
+
const blenderComponents = blenderWidgets((name) => (props) => React.createElement(name, props))
|
|
351
266
|
|
|
352
267
|
module.exports = {
|
|
353
268
|
...blenderComponents,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blender-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "React renderer for BUI: Blender's UI in native windows from JSX, with React's hooks backed by signals. Node.js and Bun.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@blender-ui/native": "^0.
|
|
43
|
+
"@blender-ui/native": "^0.3.0",
|
|
44
44
|
"react-reconciler": "0.34.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|