@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/LICENSE +674 -0
- package/README.md +170 -2
- package/hooks.js +397 -0
- package/index.d.ts +110 -0
- package/index.js +370 -0
- package/jsx-dev-runtime.d.ts +2 -0
- package/jsx-dev-runtime.js +9 -0
- package/jsx-runtime.d.ts +26 -0
- package/jsx-runtime.js +11 -0
- package/package.json +74 -6
package/index.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// @blender-ui/react: a React renderer for BUI (react-reconciler 0.34 / React 19.3).
|
|
2
|
+
//
|
|
3
|
+
// const { render } = require('@blender-ui/react')
|
|
4
|
+
// render(<div className="p-4 bg-[#4772b3]" onClick={...}>Hello</div>,
|
|
5
|
+
// { title: 'App', width: 400, height: 300 })
|
|
6
|
+
//
|
|
7
|
+
// Host components are HTML elements: React mutates @blender-ui/native's
|
|
8
|
+
// retained tree, which sends only the changes of each commit to the native
|
|
9
|
+
// UI thread. Events arrive as BUI events (onClick, onChange, onKeyDown ...).
|
|
10
|
+
//
|
|
11
|
+
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
12
|
+
'use strict'
|
|
13
|
+
|
|
14
|
+
const React = require('react')
|
|
15
|
+
const Reconciler = require('react-reconciler')
|
|
16
|
+
const { ConcurrentRoot, DefaultEventPriority, DiscreteEventPriority, NoEventPriority } =
|
|
17
|
+
require('react-reconciler/constants')
|
|
18
|
+
const native = require('@blender-ui/native')
|
|
19
|
+
const { App, TextNode, Signal, Memo, isSignal } = native
|
|
20
|
+
const hooks = require('./hooks')
|
|
21
|
+
|
|
22
|
+
// A signal rendered as a child (`<span>{count}</span>`) becomes this host
|
|
23
|
+
// type: a text node bound to the signal, updated without re-rendering.
|
|
24
|
+
const SIGNAL_TEXT = '#bui-signal-text'
|
|
25
|
+
for (const C of [Signal, Memo]) {
|
|
26
|
+
Object.defineProperties(C.prototype, {
|
|
27
|
+
$$typeof: { value: Symbol.for('react.transitional.element'), configurable: true },
|
|
28
|
+
type: { value: SIGNAL_TEXT, configurable: true },
|
|
29
|
+
key: { value: null, configurable: true },
|
|
30
|
+
ref: { value: null, configurable: true },
|
|
31
|
+
_owner: { value: null, configurable: true },
|
|
32
|
+
// React (dev) marks list children validated here: a fresh object each time
|
|
33
|
+
_store: { get () { return { validated: 1 } }, configurable: true },
|
|
34
|
+
props: { get () { return (this._reactProps ??= { signal: this }) }, configurable: true },
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
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
|
+
// Nodes a render created. React may throw a render away (an interrupted or
|
|
118
|
+
// suspended concurrent render); its nodes are then never attached, and are
|
|
119
|
+
// freed after the next commit.
|
|
120
|
+
function created (container, node) {
|
|
121
|
+
container.created.push(node)
|
|
122
|
+
return node
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sweep (container) {
|
|
126
|
+
const body = container.window.body
|
|
127
|
+
for (const node of container.created) {
|
|
128
|
+
let top = node
|
|
129
|
+
while (top.parentNode !== null) top = top.parentNode
|
|
130
|
+
if (top !== body && container.window._nodes.has(top.id)) top.destroy()
|
|
131
|
+
}
|
|
132
|
+
container.created.length = 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let updatePriority = NoEventPriority
|
|
136
|
+
const ROOT_CONTEXT = {}
|
|
137
|
+
|
|
138
|
+
const host = {
|
|
139
|
+
rendererPackageName: '@blender-ui/react',
|
|
140
|
+
rendererVersion: require('./package.json').version,
|
|
141
|
+
supportsMutation: true,
|
|
142
|
+
supportsPersistence: false,
|
|
143
|
+
supportsHydration: false,
|
|
144
|
+
supportsMicrotasks: true,
|
|
145
|
+
isPrimaryRenderer: true,
|
|
146
|
+
warnsIfNotActing: false,
|
|
147
|
+
noTimeout: -1,
|
|
148
|
+
scheduleTimeout: setTimeout,
|
|
149
|
+
cancelTimeout: clearTimeout,
|
|
150
|
+
scheduleMicrotask: queueMicrotask,
|
|
151
|
+
|
|
152
|
+
getRootHostContext: () => ROOT_CONTEXT, // must not be null (React checks)
|
|
153
|
+
getChildHostContext: (parent) => parent,
|
|
154
|
+
getPublicInstance: (instance) => instance,
|
|
155
|
+
prepareForCommit: () => null,
|
|
156
|
+
resetAfterCommit (container) {
|
|
157
|
+
if (container.created.length > 0) sweep(container)
|
|
158
|
+
container.window.flush() // one native call per commit
|
|
159
|
+
},
|
|
160
|
+
preparePortalMount () {},
|
|
161
|
+
|
|
162
|
+
createInstance (type, props, container) {
|
|
163
|
+
if (type === SIGNAL_TEXT) return created(container, container.window.createTextNode(props.signal))
|
|
164
|
+
const el = container.window.createElement(type)
|
|
165
|
+
el._react = {}
|
|
166
|
+
applyProps(el, NO_PROPS, props)
|
|
167
|
+
return created(container, el)
|
|
168
|
+
},
|
|
169
|
+
createTextInstance: (text, container) => created(container, container.window.createTextNode(text)),
|
|
170
|
+
appendInitialChild: (parent, child) => parent.appendChild(child),
|
|
171
|
+
finalizeInitialChildren: () => false,
|
|
172
|
+
shouldSetTextContent: (type, props) => isText(props.children),
|
|
173
|
+
|
|
174
|
+
appendChild: (parent, child) => parent.appendChild(child),
|
|
175
|
+
appendChildToContainer: (container, child) => container.window.body.appendChild(child),
|
|
176
|
+
insertBefore: (parent, child, before) => parent.insertBefore(child, before),
|
|
177
|
+
insertInContainerBefore: (container, child, before) => container.window.body.insertBefore(child, before),
|
|
178
|
+
removeChild (parent, child) {
|
|
179
|
+
parent.removeChild(child)
|
|
180
|
+
child.destroy()
|
|
181
|
+
},
|
|
182
|
+
removeChildFromContainer (container, child) {
|
|
183
|
+
container.window.body.removeChild(child)
|
|
184
|
+
child.destroy()
|
|
185
|
+
},
|
|
186
|
+
clearContainer (container) {
|
|
187
|
+
for (const c of [...container.window.body.childNodes]) container.window.body.removeChild(c)
|
|
188
|
+
},
|
|
189
|
+
// the subtree: text set through textContent has no fiber of its own (freeing twice is a no-op)
|
|
190
|
+
detachDeletedInstance: (node) => node.destroy(),
|
|
191
|
+
|
|
192
|
+
commitUpdate (el, type, prev, next) {
|
|
193
|
+
if (type === SIGNAL_TEXT) {
|
|
194
|
+
if (prev.signal !== next.signal) el.data = next.signal
|
|
195
|
+
} else {
|
|
196
|
+
applyProps(el, prev, next)
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
commitTextUpdate: (text, prev, next) => { text.data = next },
|
|
200
|
+
commitMount () {},
|
|
201
|
+
resetTextContent: (el) => { el.textContent = '' },
|
|
202
|
+
hideInstance (el) {
|
|
203
|
+
if (el instanceof TextNode) el.data = '' // a signal's text: unbound while hidden
|
|
204
|
+
else el.setAttribute('hidden', '')
|
|
205
|
+
},
|
|
206
|
+
unhideInstance (el, props) {
|
|
207
|
+
if (el instanceof TextNode) el.data = props.signal
|
|
208
|
+
else if (!props.hidden) el.removeAttribute('hidden')
|
|
209
|
+
},
|
|
210
|
+
hideTextInstance: (text) => { text._hidden = text.data; text.data = '' },
|
|
211
|
+
unhideTextInstance: (text, value) => { text.data = value },
|
|
212
|
+
|
|
213
|
+
setCurrentUpdatePriority: (p) => { updatePriority = p },
|
|
214
|
+
getCurrentUpdatePriority: () => updatePriority,
|
|
215
|
+
resolveUpdatePriority: () => updatePriority || DefaultEventPriority,
|
|
216
|
+
resolveEventType: () => null,
|
|
217
|
+
resolveEventTimeStamp: () => -1.1,
|
|
218
|
+
shouldAttemptEagerTransition: () => false,
|
|
219
|
+
trackSchedulerEvent () {},
|
|
220
|
+
requestPostPaintCallback () {},
|
|
221
|
+
maySuspendCommit: () => false,
|
|
222
|
+
preloadInstance: () => true,
|
|
223
|
+
startSuspendingCommit () {},
|
|
224
|
+
suspendInstance () {},
|
|
225
|
+
waitForCommitToBeReady: () => null,
|
|
226
|
+
NotPendingTransition: null,
|
|
227
|
+
HostTransitionContext: React.createContext(null),
|
|
228
|
+
resetFormInstance () {},
|
|
229
|
+
getInstanceFromNode: () => null,
|
|
230
|
+
getInstanceFromScope: () => null,
|
|
231
|
+
beforeActiveInstanceBlur () {},
|
|
232
|
+
afterActiveInstanceBlur () {},
|
|
233
|
+
prepareScopeUpdate () {},
|
|
234
|
+
bindToConsole: (method, args) => Function.prototype.bind.apply(console[method], [console, ...args]),
|
|
235
|
+
|
|
236
|
+
// Commits that wait on resources: none here.
|
|
237
|
+
maySuspendCommitOnUpdate: () => false,
|
|
238
|
+
maySuspendCommitInSyncRender: () => false,
|
|
239
|
+
getSuspendedCommitReason: () => null,
|
|
240
|
+
|
|
241
|
+
// View transitions (<ViewTransition>): not animated here. As React DOM does
|
|
242
|
+
// without the browser API: run the commit's phases now and report none.
|
|
243
|
+
startViewTransition (suspendedState, container, types, mutation, layout, afterMutation, spawnedWork) {
|
|
244
|
+
mutation()
|
|
245
|
+
layout()
|
|
246
|
+
spawnedWork()
|
|
247
|
+
return null
|
|
248
|
+
},
|
|
249
|
+
startGestureTransition: () => null,
|
|
250
|
+
stopViewTransition () {},
|
|
251
|
+
suspendOnActiveViewTransition () {},
|
|
252
|
+
addViewTransitionFinishedListener (transition, callback) { callback() },
|
|
253
|
+
createViewTransitionInstance: (name) => ({ name, group: null, imagePair: null, old: null, new: null }),
|
|
254
|
+
applyViewTransitionName () {},
|
|
255
|
+
restoreViewTransitionName () {},
|
|
256
|
+
cancelViewTransitionName () {},
|
|
257
|
+
cancelRootViewTransitionName () {},
|
|
258
|
+
restoreRootViewTransitionName () {},
|
|
259
|
+
cloneRootViewTransitionContainer: (container) => container,
|
|
260
|
+
removeRootViewTransitionClone () {},
|
|
261
|
+
measureInstance: () => null,
|
|
262
|
+
measureClonedInstance: () => null,
|
|
263
|
+
wasInstanceInViewport: () => true,
|
|
264
|
+
hasInstanceChanged: () => false,
|
|
265
|
+
hasInstanceAffectedParent: () => false,
|
|
266
|
+
getCurrentGestureOffset: () => 0,
|
|
267
|
+
|
|
268
|
+
// <Fragment ref>: the fragment's fiber, nothing more (no DOM-like API yet).
|
|
269
|
+
createFragmentInstance: (fiber) => ({ _fragmentFiber: fiber }),
|
|
270
|
+
updateFragmentInstanceFiber (fiber, instance) { instance._fragmentFiber = fiber },
|
|
271
|
+
commitNewChildToFragmentInstance () {},
|
|
272
|
+
deleteChildFromFragmentInstance () {},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const reconciler = Reconciler(host)
|
|
276
|
+
|
|
277
|
+
// Events: run handlers as discrete updates (React DOM does the same), so a
|
|
278
|
+
// click's state change commits right away.
|
|
279
|
+
const nativeDispatch = native.Element.prototype._dispatch
|
|
280
|
+
native.Element.prototype._dispatch = function (raw) {
|
|
281
|
+
const prev = updatePriority
|
|
282
|
+
updatePriority = DiscreteEventPriority
|
|
283
|
+
try {
|
|
284
|
+
nativeDispatch.call(this, raw)
|
|
285
|
+
} finally {
|
|
286
|
+
updatePriority = prev
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function report (kind) {
|
|
291
|
+
return (err) => console.error(`@blender-ui/react: ${kind}`, err)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// The root component gets render tracking even when created with React's
|
|
295
|
+
// own createElement (components below need this package's JSX runtime or
|
|
296
|
+
// createElement).
|
|
297
|
+
function trackRoot (element) {
|
|
298
|
+
if (!React.isValidElement(element)) return element
|
|
299
|
+
const type = hooks.track(element.type)
|
|
300
|
+
if (type === element.type) return element
|
|
301
|
+
return React.createElement(type, element.key != null ? { ...element.props, key: element.key } : element.props)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** A React root rendering into `window`'s body. */
|
|
305
|
+
function createRoot (window) {
|
|
306
|
+
const container = { window, created: [] }
|
|
307
|
+
const root = reconciler.createContainer(container, ConcurrentRoot, null, false, null, 'bui',
|
|
308
|
+
report('uncaught error'), report('caught error'), report('recoverable error'), null)
|
|
309
|
+
window.on('close', () => reconciler.updateContainer(null, root, null, null))
|
|
310
|
+
return {
|
|
311
|
+
render (element) { reconciler.updateContainer(trackRoot(element), root, null, null) },
|
|
312
|
+
unmount () { reconciler.updateContainer(null, root, null, null) },
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Open a window and render `element` into it.
|
|
318
|
+
* @param {React.ReactNode} element
|
|
319
|
+
* @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> }}
|
|
322
|
+
*/
|
|
323
|
+
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)
|
|
326
|
+
const root = createRoot(window)
|
|
327
|
+
root.render(element)
|
|
328
|
+
return { app, window, root }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Run `fn` and flush React's work synchronously (tests). */
|
|
332
|
+
function act (fn) {
|
|
333
|
+
const flushSync = reconciler.flushSyncFromReconciler ?? reconciler.flushSync
|
|
334
|
+
return typeof flushSync === 'function' ? flushSync(fn) : fn()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// React's hooks by React's names: those that hold or react to state are the
|
|
338
|
+
// signal versions (hooks.js), the others are React's own.
|
|
339
|
+
const reactHooks = {}
|
|
340
|
+
for (const k of Object.keys(React)) if (/^use([A-Z]|$)/.test(k)) reactHooks[k] = React[k]
|
|
341
|
+
|
|
342
|
+
// Blender's widgets as components: `<NumberSlider min={0} max={10} />`,
|
|
343
|
+
// `<Button onClick={..}>Apply</Button>`. Always Blender widgets; their
|
|
344
|
+
// 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
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
...blenderComponents,
|
|
354
|
+
...reactHooks,
|
|
355
|
+
...hooks,
|
|
356
|
+
render,
|
|
357
|
+
createRoot,
|
|
358
|
+
act,
|
|
359
|
+
reconciler,
|
|
360
|
+
signal: native.signal,
|
|
361
|
+
computed: native.memo, // `memo` stays React's (components)
|
|
362
|
+
effect: native.effect,
|
|
363
|
+
batch: native.batch,
|
|
364
|
+
untrack: native.untrack,
|
|
365
|
+
createSignal: native.createSignal,
|
|
366
|
+
ref: native.ref,
|
|
367
|
+
isSignal,
|
|
368
|
+
Signal,
|
|
369
|
+
Memo,
|
|
370
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
2
|
+
'use strict'
|
|
3
|
+
const runtime = require('react/jsx-dev-runtime')
|
|
4
|
+
const { track } = require('./hooks')
|
|
5
|
+
|
|
6
|
+
exports.Fragment = runtime.Fragment
|
|
7
|
+
// undefined in React's production build, as in React
|
|
8
|
+
exports.jsxDEV = runtime.jsxDEV && ((type, props, key, isStatic, source, self) =>
|
|
9
|
+
runtime.jsxDEV(track(type), props, key, isStatic, source, self))
|
package/jsx-runtime.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// With `"jsxImportSource": "@blender-ui/react"` in tsconfig, host elements'
|
|
2
|
+
// props accept signals: `<div style={styleMemo} className={cls}>`.
|
|
3
|
+
import type * as React from 'react'
|
|
4
|
+
import type { ReadSignal } from '@blender-ui/native'
|
|
5
|
+
export { Fragment, jsx, jsxs } from 'react/jsx-runtime'
|
|
6
|
+
|
|
7
|
+
type Fixed = 'children' | 'key' | 'ref' | 'dangerouslySetInnerHTML'
|
|
8
|
+
/** Each prop also takes a signal of its value (event handlers stay functions). */
|
|
9
|
+
export type Bindable<P> = {
|
|
10
|
+
[K in keyof P]: K extends Fixed | `on${string}` ? P[K]
|
|
11
|
+
: K extends 'style' ? P[K] | string | ReadSignal<Exclude<P[K], undefined> | string>
|
|
12
|
+
: P[K] | ReadSignal<Exclude<P[K], undefined>>
|
|
13
|
+
}
|
|
14
|
+
type Intrinsic = { [K in keyof React.JSX.IntrinsicElements]: Bindable<React.JSX.IntrinsicElements[K]> }
|
|
15
|
+
|
|
16
|
+
export namespace JSX {
|
|
17
|
+
type ElementType = React.JSX.ElementType
|
|
18
|
+
interface Element extends React.JSX.Element {}
|
|
19
|
+
interface ElementClass extends React.JSX.ElementClass {}
|
|
20
|
+
interface ElementAttributesProperty extends React.JSX.ElementAttributesProperty {}
|
|
21
|
+
interface ElementChildrenAttribute extends React.JSX.ElementChildrenAttribute {}
|
|
22
|
+
type LibraryManagedAttributes<C, P> = React.JSX.LibraryManagedAttributes<C, P>
|
|
23
|
+
interface IntrinsicAttributes extends React.JSX.IntrinsicAttributes {}
|
|
24
|
+
interface IntrinsicClassAttributes<T> extends React.JSX.IntrinsicClassAttributes<T> {}
|
|
25
|
+
interface IntrinsicElements extends Intrinsic {}
|
|
26
|
+
}
|
package/jsx-runtime.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `"jsxImportSource": "@blender-ui/react"`: React's JSX runtime. Components
|
|
2
|
+
// get render tracking (they re-render when a signal their render read
|
|
3
|
+
// changes); the types let every host-element prop take a signal.
|
|
4
|
+
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
5
|
+
'use strict'
|
|
6
|
+
const runtime = require('react/jsx-runtime')
|
|
7
|
+
const { track } = require('./hooks')
|
|
8
|
+
|
|
9
|
+
exports.Fragment = runtime.Fragment
|
|
10
|
+
exports.jsx = (type, props, key) => runtime.jsx(track(type), props, key)
|
|
11
|
+
exports.jsxs = (type, props, key) => runtime.jsxs(track(type), props, key)
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@blender-ui/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@blender-ui/react",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"default": "./index.js"
|
|
11
|
+
},
|
|
12
|
+
"./jsx-runtime": {
|
|
13
|
+
"types": "./jsx-runtime.d.ts",
|
|
14
|
+
"default": "./jsx-runtime.js"
|
|
15
|
+
},
|
|
16
|
+
"./jsx-dev-runtime": {
|
|
17
|
+
"types": "./jsx-dev-runtime.d.ts",
|
|
18
|
+
"default": "./jsx-dev-runtime.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"hooks.js",
|
|
25
|
+
"index.d.ts",
|
|
26
|
+
"jsx-runtime.js",
|
|
27
|
+
"jsx-runtime.d.ts",
|
|
28
|
+
"jsx-dev-runtime.js",
|
|
29
|
+
"jsx-dev-runtime.d.ts",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test",
|
|
35
|
+
"prepublishOnly": "node --test",
|
|
36
|
+
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE', 'LICENSE')\"",
|
|
37
|
+
"postpack": "node -e \"require('fs').rmSync('LICENSE', { force: true })\""
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@blender-ui/native": "^0.2.1",
|
|
44
|
+
"react-reconciler": "0.34.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": "^19.3.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"react": "19.3.1"
|
|
51
|
+
},
|
|
52
|
+
"license": "GPL-2.0-or-later",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/ppmpreetham/BUI.git",
|
|
56
|
+
"directory": "bindings/react"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://bui.preetham.top",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/ppmpreetham/BUI/issues"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"react",
|
|
64
|
+
"renderer",
|
|
65
|
+
"react-reconciler",
|
|
66
|
+
"signals",
|
|
67
|
+
"blender",
|
|
68
|
+
"ui",
|
|
69
|
+
"desktop",
|
|
70
|
+
"native",
|
|
71
|
+
"tailwind",
|
|
72
|
+
"bun"
|
|
73
|
+
]
|
|
74
|
+
}
|