onejs-react 0.1.0 → 0.1.1

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/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Components
2
2
  export {
3
3
  View,
4
+ Text,
4
5
  Label,
5
6
  Button,
6
7
  TextField,
@@ -12,7 +13,11 @@ export {
12
13
  } from './components';
13
14
 
14
15
  // Renderer
15
- export { render, unmount } from './renderer';
16
+ export { render, unmount, flushSync, batchedUpdates, getDebugInfo } from './renderer';
17
+
18
+ // Error Handling
19
+ export { ErrorBoundary, formatError } from './error-boundary';
20
+ export type { ErrorBoundaryProps } from './error-boundary';
16
21
 
17
22
  // Responsive Design
18
23
  export {
@@ -33,15 +38,32 @@ export type {
33
38
  // Types
34
39
  export type {
35
40
  ViewStyle,
41
+ // Event data types
36
42
  PointerEventData,
43
+ MouseEventData,
44
+ WheelEventData,
37
45
  KeyEventData,
38
46
  ChangeEventData,
47
+ FocusEventData,
48
+ DragEventData,
49
+ GeometryEventData,
50
+ NavigationEventData,
51
+ TransitionEventData,
52
+ // Event handler types
39
53
  PointerEventHandler,
54
+ MouseEventHandler,
55
+ WheelEventHandler,
40
56
  KeyEventHandler,
41
57
  ChangeEventHandler,
42
58
  FocusEventHandler,
59
+ DragEventHandler,
60
+ GeometryEventHandler,
61
+ NavigationEventHandler,
62
+ TransitionEventHandler,
63
+ // Component props
43
64
  BaseProps,
44
65
  ViewProps,
66
+ TextProps,
45
67
  LabelProps,
46
68
  ButtonProps,
47
69
  TextFieldProps,
@@ -49,6 +71,17 @@ export type {
49
71
  SliderProps,
50
72
  ScrollViewProps,
51
73
  ImageProps,
52
- VisualElement,
53
74
  ListViewProps,
75
+ // Container type for render()
76
+ RenderContainer,
77
+ // Element types for refs
78
+ VisualElement,
79
+ TextElement,
80
+ LabelElement,
81
+ ButtonElement,
82
+ TextFieldElement,
83
+ ToggleElement,
84
+ SliderElement,
85
+ ScrollViewElement,
86
+ ImageElement,
54
87
  } from './types';
package/src/renderer.ts CHANGED
@@ -1,30 +1,30 @@
1
1
  import Reconciler from 'react-reconciler';
2
2
  import type { ReactNode } from 'react';
3
3
  import { hostConfig, type Container } from './host-config';
4
+ import type { RenderContainer, VisualElement } from './types';
4
5
 
5
6
  declare const console: { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
6
7
 
7
8
  // Create the reconciler
8
9
  const reconciler = Reconciler(hostConfig);
9
10
 
10
- // Inject into dev tools (helps with proper initialization)
11
+ // Inject into dev tools with full configuration
12
+ // This enables React DevTools to inspect the component tree
11
13
  reconciler.injectIntoDevTools({
12
- bundleType: 1, // 0 for prod, 1 for dev
13
- version: '19.0.0',
14
- rendererPackageName: 'onejs-react',
14
+ bundleType: 1, // 0 for prod, 1 for dev
15
+ version: '19.0.0',
16
+ rendererPackageName: 'onejs-react',
15
17
  });
16
18
 
17
19
  // Track roots for hot reload / re-render
18
- const roots = new Map<Container, ReturnType<typeof reconciler.createContainer>>();
20
+ const roots = new Map<RenderContainer, ReturnType<typeof reconciler.createContainer>>();
19
21
 
20
- export function render(element: ReactNode, container: Container): void {
21
- console.log('[onejs-react] render() called');
22
+ export function render(element: ReactNode, container: RenderContainer): void {
22
23
  let root = roots.get(container);
23
24
 
24
25
  if (!root) {
25
- console.log('[onejs-react] creating new container');
26
26
  root = reconciler.createContainer(
27
- container,
27
+ container as Container,
28
28
  0, // LegacyRoot (0) vs ConcurrentRoot (1)
29
29
  null, // hydrationCallbacks
30
30
  false, // isStrictMode
@@ -36,30 +36,21 @@ export function render(element: ReactNode, container: Container): void {
36
36
  roots.set(container, root);
37
37
  }
38
38
 
39
- console.log('[onejs-react] calling updateContainer');
40
- reconciler.updateContainer(element, root, null, () => {
41
- console.log('[onejs-react] updateContainer callback fired');
42
- });
39
+ reconciler.updateContainer(element, root, null, () => {});
43
40
 
44
41
  // Try to flush synchronous work
45
- console.log('[onejs-react] attempting to flush sync work');
46
42
  try {
47
- // flushSync may be exported differently - try flushSyncWork first
48
43
  if (typeof (reconciler as any).flushSyncWork === 'function') {
49
44
  (reconciler as any).flushSyncWork();
50
- console.log('[onejs-react] flushSyncWork completed');
51
45
  } else if (typeof (reconciler as any).flushSync === 'function') {
52
46
  (reconciler as any).flushSync(() => {});
53
- console.log('[onejs-react] flushSync completed');
54
- } else {
55
- console.log('[onejs-react] no sync flush method available, relying on microtasks');
56
47
  }
57
48
  } catch (e) {
58
- console.log('[onejs-react] sync flush failed, relying on microtasks:', e);
49
+ // Sync flush failed, rely on microtasks
59
50
  }
60
51
  }
61
52
 
62
- export function unmount(container: Container): void {
53
+ export function unmount(container: RenderContainer): void {
63
54
  const root = roots.get(container);
64
55
  if (root) {
65
56
  reconciler.updateContainer(null, root, null, () => {});
@@ -67,7 +58,43 @@ export function unmount(container: Container): void {
67
58
  }
68
59
  }
69
60
 
61
+ /**
62
+ * Execute a callback synchronously, flushing all updates before returning.
63
+ * Useful for tests or when you need immediate UI updates.
64
+ */
65
+ export function flushSync<T>(callback: () => T): T {
66
+ if (typeof (reconciler as any).flushSync === 'function') {
67
+ return (reconciler as any).flushSync(callback);
68
+ }
69
+ // Fallback: just call the callback
70
+ return callback();
71
+ }
72
+
73
+ /**
74
+ * Batch multiple updates together for better performance.
75
+ * All updates inside the callback are batched into a single render.
76
+ */
77
+ export function batchedUpdates<T>(callback: () => T): T {
78
+ if (typeof (reconciler as any).batchedUpdates === 'function') {
79
+ return (reconciler as any).batchedUpdates(callback);
80
+ }
81
+ // Fallback: just call the callback
82
+ return callback();
83
+ }
84
+
70
85
  // Export for testing/debugging
71
- export function getRoot(container: Container) {
72
- return roots.get(container);
86
+ export function getRoot(container: RenderContainer) {
87
+ return roots.get(container);
88
+ }
89
+
90
+ /**
91
+ * Get debug info about all active render roots.
92
+ * Useful for debugging and DevTools integration.
93
+ */
94
+ export function getDebugInfo() {
95
+ return {
96
+ activeRoots: roots.size,
97
+ reconcilerVersion: '0.31.0',
98
+ reactVersion: '19.0.0',
99
+ };
73
100
  }
package/src/screen.tsx CHANGED
@@ -7,7 +7,7 @@
7
7
  * Mobile-first: At 1400px width, sm/md/lg/xl are all active (not just xl).
8
8
  */
9
9
 
10
- import { createContext, useContext, useState, useEffect, ReactNode } from "react"
10
+ import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
11
11
 
12
12
  // Globals from QuickJS environment
13
13
  declare const __root: {
@@ -130,13 +130,39 @@ export function parseLength(value: number | string): CSLength | number | null {
130
130
  }
131
131
 
132
132
  /**
133
- * Parse a hex color component (1 or 2 characters)
133
+ * Clamp a number to [0, 1] range
134
134
  */
135
- function parseHexComponent(hex: string): number {
136
- if (hex.length === 1) {
137
- hex = hex + hex // Expand shorthand: "f" -> "ff"
135
+ function clamp01(n: number): number {
136
+ return n < 0 ? 0 : n > 1 ? 1 : n
137
+ }
138
+
139
+ /**
140
+ * Create a Unity Color with clamped values
141
+ */
142
+ function createColor(r: number, g: number, b: number, a: number): CSColor {
143
+ return new CS.UnityEngine.Color(clamp01(r), clamp01(g), clamp01(b), clamp01(a))
144
+ }
145
+
146
+ /**
147
+ * Parse hex color string into RGBA components
148
+ * Supports: #rgb, #rgba, #rrggbb, #rrggbbaa
149
+ */
150
+ function parseHexColor(hex: string): CSColor | null {
151
+ const len = hex.length
152
+ const isShort = len === 3 || len === 4
153
+ const isLong = len === 6 || len === 8
154
+ if (!isShort && !isLong) return null
155
+
156
+ const step = isShort ? 1 : 2
157
+ const parse = (i: number): number => {
158
+ const slice = hex.slice(i * step, i * step + step)
159
+ const expanded = isShort ? slice + slice : slice
160
+ return parseInt(expanded, 16) / 255
138
161
  }
139
- return parseInt(hex, 16) / 255
162
+
163
+ const r = parse(0), g = parse(1), b = parse(2)
164
+ const a = (len === 4 || len === 8) ? parse(3) : 1
165
+ return createColor(r, g, b, a)
140
166
  }
141
167
 
142
168
  /**
@@ -155,75 +181,21 @@ export function parseColor(value: string): CSColor | null {
155
181
  return new CS.UnityEngine.Color(r, g, b, a)
156
182
  }
157
183
 
158
- // Hex colors: #rgb, #rgba, #rrggbb, #rrggbbaa
184
+ // Hex colors
159
185
  if (trimmed.startsWith("#")) {
160
- const hex = trimmed.slice(1)
161
-
162
- if (hex.length === 3) {
163
- // #rgb
164
- const r = parseHexComponent(hex[0])
165
- const g = parseHexComponent(hex[1])
166
- const b = parseHexComponent(hex[2])
167
- return new CS.UnityEngine.Color(r, g, b, 1)
168
- }
169
-
170
- if (hex.length === 4) {
171
- // #rgba
172
- const r = parseHexComponent(hex[0])
173
- const g = parseHexComponent(hex[1])
174
- const b = parseHexComponent(hex[2])
175
- const a = parseHexComponent(hex[3])
176
- return new CS.UnityEngine.Color(r, g, b, a)
177
- }
178
-
179
- if (hex.length === 6) {
180
- // #rrggbb
181
- const r = parseHexComponent(hex.slice(0, 2))
182
- const g = parseHexComponent(hex.slice(2, 4))
183
- const b = parseHexComponent(hex.slice(4, 6))
184
- return new CS.UnityEngine.Color(r, g, b, 1)
185
- }
186
-
187
- if (hex.length === 8) {
188
- // #rrggbbaa
189
- const r = parseHexComponent(hex.slice(0, 2))
190
- const g = parseHexComponent(hex.slice(2, 4))
191
- const b = parseHexComponent(hex.slice(4, 6))
192
- const a = parseHexComponent(hex.slice(6, 8))
193
- return new CS.UnityEngine.Color(r, g, b, a)
194
- }
195
-
196
- return null
186
+ return parseHexColor(trimmed.slice(1))
197
187
  }
198
188
 
199
- // rgb(r, g, b) or rgba(r, g, b, a)
200
- const rgbMatch = trimmed.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/)
189
+ // rgb(r, g, b) or rgba(r, g, b, a) - supports both integer and percentage values
190
+ const rgbMatch = trimmed.match(/^rgba?\s*\(\s*([\d.]+)(%?)\s*,\s*([\d.]+)(%?)\s*,\s*([\d.]+)(%?)\s*(?:,\s*([\d.]+))?\s*\)$/)
201
191
  if (rgbMatch) {
202
- const r = parseInt(rgbMatch[1]) / 255
203
- const g = parseInt(rgbMatch[2]) / 255
204
- const b = parseInt(rgbMatch[3]) / 255
205
- const a = rgbMatch[4] !== undefined ? parseFloat(rgbMatch[4]) : 1
206
- return new CS.UnityEngine.Color(
207
- Math.min(1, Math.max(0, r)),
208
- Math.min(1, Math.max(0, g)),
209
- Math.min(1, Math.max(0, b)),
210
- Math.min(1, Math.max(0, a))
211
- )
212
- }
213
-
214
- // rgb with percentages: rgb(100%, 0%, 0%)
215
- const rgbPercentMatch = trimmed.match(/^rgba?\s*\(\s*([\d.]+)%\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*([\d.]+))?\s*\)$/)
216
- if (rgbPercentMatch) {
217
- const r = parseFloat(rgbPercentMatch[1]) / 100
218
- const g = parseFloat(rgbPercentMatch[2]) / 100
219
- const b = parseFloat(rgbPercentMatch[3]) / 100
220
- const a = rgbPercentMatch[4] !== undefined ? parseFloat(rgbPercentMatch[4]) : 1
221
- return new CS.UnityEngine.Color(
222
- Math.min(1, Math.max(0, r)),
223
- Math.min(1, Math.max(0, g)),
224
- Math.min(1, Math.max(0, b)),
225
- Math.min(1, Math.max(0, a))
226
- )
192
+ const isPercent = rgbMatch[2] === "%"
193
+ const divisor = isPercent ? 100 : 255
194
+ const r = parseFloat(rgbMatch[1]) / divisor
195
+ const g = parseFloat(rgbMatch[3]) / divisor
196
+ const b = parseFloat(rgbMatch[5]) / divisor
197
+ const a = rgbMatch[7] !== undefined ? parseFloat(rgbMatch[7]) : 1
198
+ return createColor(r, g, b, a)
227
199
  }
228
200
 
229
201
  return null
package/src/types.ts CHANGED
@@ -81,6 +81,15 @@ export interface ViewStyle {
81
81
  // Background
82
82
  /** Background color. Examples: "#3498db", "rgba(0,0,0,0.5)", "red" */
83
83
  backgroundColor?: StyleColor;
84
+ /**
85
+ * Background image - accepts a Texture2D, RenderTexture, or RenderTexture object from GPU compute.
86
+ *
87
+ * For GPU compute RenderTextures, you can pass the RenderTexture object directly:
88
+ * @example
89
+ * const rt = compute.renderTexture({ width: 100, height: 100 })
90
+ * <View style={{ backgroundImage: rt }} />
91
+ */
92
+ backgroundImage?: object | null;
84
93
 
85
94
  // Border
86
95
  /** Border color for all sides. Examples: "#ccc", "rgba(0,0,0,0.1)" */
@@ -127,6 +136,23 @@ export interface PointerEventData {
127
136
  y: number;
128
137
  button: number;
129
138
  pointerId: number;
139
+ modifiers?: number;
140
+ }
141
+
142
+ export interface MouseEventData {
143
+ type: string;
144
+ x: number;
145
+ y: number;
146
+ button: number;
147
+ modifiers?: number;
148
+ }
149
+
150
+ export interface WheelEventData {
151
+ type: string;
152
+ x: number;
153
+ y: number;
154
+ delta: { x: number; y: number };
155
+ modifiers?: number;
130
156
  }
131
157
 
132
158
  export interface KeyEventData {
@@ -145,10 +171,47 @@ export interface ChangeEventData<T = unknown> {
145
171
  value: T;
146
172
  }
147
173
 
174
+ export interface FocusEventData {
175
+ type: string;
176
+ relatedTarget?: unknown;
177
+ }
178
+
179
+ export interface DragEventData {
180
+ type: string;
181
+ x: number;
182
+ y: number;
183
+ // Drag-specific properties
184
+ getData?: (type: string) => unknown;
185
+ }
186
+
187
+ export interface GeometryEventData {
188
+ type: string;
189
+ oldRect: { x: number; y: number; width: number; height: number };
190
+ newRect: { x: number; y: number; width: number; height: number };
191
+ }
192
+
193
+ export interface NavigationEventData {
194
+ type: string;
195
+ direction?: string;
196
+ modifiers?: number;
197
+ }
198
+
199
+ export interface TransitionEventData {
200
+ type: string;
201
+ styleProperty: string;
202
+ elapsedTime: number;
203
+ }
204
+
148
205
  export type PointerEventHandler = (event: PointerEventData) => void;
206
+ export type MouseEventHandler = (event: MouseEventData) => void;
207
+ export type WheelEventHandler = (event: WheelEventData) => void;
149
208
  export type KeyEventHandler = (event: KeyEventData) => void;
150
209
  export type ChangeEventHandler<T = unknown> = (event: ChangeEventData<T>) => void;
151
- export type FocusEventHandler = () => void;
210
+ export type FocusEventHandler = (event?: FocusEventData) => void;
211
+ export type DragEventHandler = (event: DragEventData) => void;
212
+ export type GeometryEventHandler = (event: GeometryEventData) => void;
213
+ export type NavigationEventHandler = (event: NavigationEventData) => void;
214
+ export type TransitionEventHandler = (event: TransitionEventData) => void;
152
215
 
153
216
  // Base props for all components
154
217
  export interface BaseProps {
@@ -157,26 +220,76 @@ export interface BaseProps {
157
220
  style?: ViewStyle;
158
221
  className?: string;
159
222
 
160
- // Pointer events
223
+ // Click
161
224
  onClick?: PointerEventHandler;
225
+
226
+ // Pointer events
162
227
  onPointerDown?: PointerEventHandler;
163
228
  onPointerUp?: PointerEventHandler;
164
229
  onPointerMove?: PointerEventHandler;
165
230
  onPointerEnter?: PointerEventHandler;
166
231
  onPointerLeave?: PointerEventHandler;
232
+ onPointerCancel?: PointerEventHandler;
233
+ onPointerCapture?: PointerEventHandler;
234
+ onPointerCaptureOut?: PointerEventHandler;
235
+ onPointerStationary?: PointerEventHandler;
236
+
237
+ // Mouse events
238
+ onMouseDown?: MouseEventHandler;
239
+ onMouseUp?: MouseEventHandler;
240
+ onMouseMove?: MouseEventHandler;
241
+ onMouseEnter?: MouseEventHandler;
242
+ onMouseLeave?: MouseEventHandler;
243
+ onMouseOver?: MouseEventHandler;
244
+ onMouseOut?: MouseEventHandler;
245
+ onWheel?: WheelEventHandler;
246
+ onContextClick?: MouseEventHandler;
167
247
 
168
248
  // Focus events
169
249
  onFocus?: FocusEventHandler;
170
250
  onBlur?: FocusEventHandler;
251
+ onFocusIn?: FocusEventHandler;
252
+ onFocusOut?: FocusEventHandler;
171
253
 
172
254
  // Keyboard events
173
255
  onKeyDown?: KeyEventHandler;
174
256
  onKeyUp?: KeyEventHandler;
257
+
258
+ // Input events
259
+ onInput?: ChangeEventHandler;
260
+
261
+ // Drag events
262
+ onDragEnter?: DragEventHandler;
263
+ onDragLeave?: DragEventHandler;
264
+ onDragUpdated?: DragEventHandler;
265
+ onDragPerform?: DragEventHandler;
266
+ onDragExited?: DragEventHandler;
267
+
268
+ // Geometry events
269
+ onGeometryChanged?: GeometryEventHandler;
270
+
271
+ // Navigation events
272
+ onNavigationMove?: NavigationEventHandler;
273
+ onNavigationSubmit?: NavigationEventHandler;
274
+ onNavigationCancel?: NavigationEventHandler;
275
+
276
+ // Tooltip
277
+ onTooltip?: () => void;
278
+
279
+ // Transition events
280
+ onTransitionRun?: TransitionEventHandler;
281
+ onTransitionStart?: TransitionEventHandler;
282
+ onTransitionEnd?: TransitionEventHandler;
283
+ onTransitionCancel?: TransitionEventHandler;
175
284
  }
176
285
 
177
286
  // Component-specific props
178
287
  export interface ViewProps extends BaseProps {}
179
288
 
289
+ export interface TextProps extends BaseProps {
290
+ text?: string;
291
+ }
292
+
180
293
  export interface LabelProps extends BaseProps {
181
294
  text?: string;
182
295
  }
@@ -235,18 +348,96 @@ export interface ImageProps extends BaseProps {
235
348
  scaleMode?: 'stretch-to-fill' | 'scale-and-crop' | 'scale-to-fit';
236
349
  }
237
350
 
238
- // VisualElement type for ListView callbacks
239
- // This is the C# VisualElement exposed to JS
240
- export interface VisualElement {
351
+ /**
352
+ * Minimal container type for render() function.
353
+ * Accepts any Unity VisualElement (CS.UnityEngine.UIElements.VisualElement)
354
+ * or the detailed VisualElement interface below.
355
+ */
356
+ export interface RenderContainer {
241
357
  __csHandle: number;
242
358
  __csType: string;
359
+ }
360
+
361
+ // VisualElement - base type for all UI Toolkit elements
362
+ // This is the C# VisualElement exposed to JS via refs
363
+ // Note: This interface represents the JS-side view of Unity's VisualElement
364
+ export interface VisualElement extends RenderContainer {
243
365
  style: Record<string, unknown>;
366
+ name: string;
367
+ visible: boolean;
368
+ enabledSelf: boolean;
369
+ enabledInHierarchy: boolean;
370
+
371
+ // Text content (for TextElement-derived types)
244
372
  text?: string;
373
+
374
+ // Label (for labeled controls like Toggle)
375
+ label?: string;
376
+
377
+ // Value (for input controls)
245
378
  value?: unknown;
379
+
380
+ // Hierarchy
246
381
  Add: (child: VisualElement) => void;
382
+ Insert: (index: number, child: VisualElement) => void;
247
383
  Remove: (child: VisualElement) => void;
384
+ RemoveAt: (index: number) => void;
385
+ Clear: () => void;
386
+ IndexOf: (child: VisualElement) => number;
387
+ childCount: number;
388
+ parent: VisualElement | null;
389
+
390
+ // Classes
248
391
  AddToClassList: (className: string) => void;
249
392
  RemoveFromClassList: (className: string) => void;
393
+ ClearClassList: () => void;
394
+ ClassListContains: (className: string) => boolean;
395
+
396
+ // Focus
397
+ Focus: () => void;
398
+ Blur: () => void;
399
+ focusable: boolean;
400
+
401
+ // Layout
402
+ MarkDirtyRepaint: () => void;
403
+ }
404
+
405
+ // Specific element types for better ref typing
406
+ export interface TextElement extends VisualElement {
407
+ text: string;
408
+ }
409
+
410
+ export interface LabelElement extends TextElement {}
411
+
412
+ export interface ButtonElement extends TextElement {}
413
+
414
+ export interface TextFieldElement extends VisualElement {
415
+ value: string;
416
+ text: string;
417
+ isReadOnly: boolean;
418
+ isPasswordField: boolean;
419
+ maxLength: number;
420
+ SelectAll: () => void;
421
+ }
422
+
423
+ export interface ToggleElement extends VisualElement {
424
+ value: boolean;
425
+ text: string;
426
+ }
427
+
428
+ export interface SliderElement extends VisualElement {
429
+ value: number;
430
+ lowValue: number;
431
+ highValue: number;
432
+ }
433
+
434
+ export interface ScrollViewElement extends VisualElement {
435
+ scrollOffset: { x: number; y: number };
436
+ ScrollTo: (child: VisualElement) => void;
437
+ }
438
+
439
+ export interface ImageElement extends VisualElement {
440
+ // Image-specific properties handled via style.backgroundImage
250
441
  }
251
442
 
252
443
  // ListView uses Unity's virtualization callbacks directly