@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/index.js +6 -91
  3. 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 { App, TextNode, Signal, Memo, isSignal } = native
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 app = options.app ?? new App({ gpu: options.gpu, slug: options.slug, headless: options.headless, theme: options.theme })
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.2.1",
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.2.1",
43
+ "@blender-ui/native": "^0.3.0",
44
44
  "react-reconciler": "0.34.0"
45
45
  },
46
46
  "peerDependencies": {