onejs-react 0.1.0 → 0.1.2

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 {
@@ -30,18 +35,38 @@ export type {
30
35
  BreakpointName,
31
36
  } from './screen';
32
37
 
38
+ // Vector Drawing
39
+ export { Transform2D, useVectorContent } from './vector';
40
+
33
41
  // Types
34
42
  export type {
35
43
  ViewStyle,
44
+ // Event data types
36
45
  PointerEventData,
46
+ MouseEventData,
47
+ WheelEventData,
37
48
  KeyEventData,
38
49
  ChangeEventData,
50
+ FocusEventData,
51
+ DragEventData,
52
+ GeometryEventData,
53
+ NavigationEventData,
54
+ TransitionEventData,
55
+ // Event handler types
39
56
  PointerEventHandler,
57
+ MouseEventHandler,
58
+ WheelEventHandler,
40
59
  KeyEventHandler,
41
60
  ChangeEventHandler,
42
61
  FocusEventHandler,
62
+ DragEventHandler,
63
+ GeometryEventHandler,
64
+ NavigationEventHandler,
65
+ TransitionEventHandler,
66
+ // Component props
43
67
  BaseProps,
44
68
  ViewProps,
69
+ TextProps,
45
70
  LabelProps,
46
71
  ButtonProps,
47
72
  TextFieldProps,
@@ -49,6 +74,25 @@ export type {
49
74
  SliderProps,
50
75
  ScrollViewProps,
51
76
  ImageProps,
52
- VisualElement,
53
77
  ListViewProps,
78
+ // Container type for render()
79
+ RenderContainer,
80
+ // Element types for refs
81
+ VisualElement,
82
+ TextElement,
83
+ LabelElement,
84
+ ButtonElement,
85
+ TextFieldElement,
86
+ ToggleElement,
87
+ SliderElement,
88
+ ScrollViewElement,
89
+ ImageElement,
90
+ // Vector drawing types
91
+ Vector2,
92
+ Color,
93
+ Angle,
94
+ ArcDirection,
95
+ Painter2D,
96
+ MeshGenerationContext,
97
+ GenerateVisualContentCallback,
54
98
  } 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: {
@@ -13,6 +13,17 @@ declare const CS: {
13
13
  Length: new (value: number, unit?: number) => CSLength;
14
14
  LengthUnit: { Pixel: number; Percent: number };
15
15
  StyleKeyword: { Auto: number; None: number; Initial: number };
16
+ // Enums for style properties
17
+ FlexDirection: Record<string, number>;
18
+ Wrap: Record<string, number>;
19
+ Align: Record<string, number>;
20
+ Justify: Record<string, number>;
21
+ Position: Record<string, number>;
22
+ Overflow: Record<string, number>;
23
+ DisplayStyle: Record<string, number>;
24
+ Visibility: Record<string, number>;
25
+ WhiteSpace: Record<string, number>;
26
+ TextAnchor: Record<string, number>;
16
27
  };
17
28
  };
18
29
  };
@@ -80,12 +91,119 @@ const NUMBER_PROPERTIES = new Set([
80
91
  "flexGrow", "flexShrink", "opacity",
81
92
  ])
82
93
 
83
- // Enum properties - these get passed through as-is (C# handles conversion)
84
- const ENUM_PROPERTIES = new Set([
85
- "flexDirection", "flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
86
- "position", "overflow", "display", "visibility", "whiteSpace",
87
- "unityTextAlign", "fontStyle",
88
- ])
94
+ // Enum property mappings: React style value -> Unity enum value
95
+ // Keys are camelCase (React/CSS style), values map to Unity enum member names
96
+ const ENUM_MAPPINGS: Record<string, { enum: () => Record<string, number>, values: Record<string, string> }> = {
97
+ flexDirection: {
98
+ enum: () => CS.UnityEngine.UIElements.FlexDirection,
99
+ values: {
100
+ "row": "Row",
101
+ "row-reverse": "RowReverse",
102
+ "column": "Column",
103
+ "column-reverse": "ColumnReverse",
104
+ }
105
+ },
106
+ flexWrap: {
107
+ enum: () => CS.UnityEngine.UIElements.Wrap,
108
+ values: {
109
+ "nowrap": "NoWrap",
110
+ "wrap": "Wrap",
111
+ "wrap-reverse": "WrapReverse",
112
+ }
113
+ },
114
+ alignItems: {
115
+ enum: () => CS.UnityEngine.UIElements.Align,
116
+ values: {
117
+ "auto": "Auto",
118
+ "flex-start": "FlexStart",
119
+ "flex-end": "FlexEnd",
120
+ "center": "Center",
121
+ "stretch": "Stretch",
122
+ }
123
+ },
124
+ alignSelf: {
125
+ enum: () => CS.UnityEngine.UIElements.Align,
126
+ values: {
127
+ "auto": "Auto",
128
+ "flex-start": "FlexStart",
129
+ "flex-end": "FlexEnd",
130
+ "center": "Center",
131
+ "stretch": "Stretch",
132
+ }
133
+ },
134
+ alignContent: {
135
+ enum: () => CS.UnityEngine.UIElements.Align,
136
+ values: {
137
+ "auto": "Auto",
138
+ "flex-start": "FlexStart",
139
+ "flex-end": "FlexEnd",
140
+ "center": "Center",
141
+ "stretch": "Stretch",
142
+ }
143
+ },
144
+ justifyContent: {
145
+ enum: () => CS.UnityEngine.UIElements.Justify,
146
+ values: {
147
+ "flex-start": "FlexStart",
148
+ "flex-end": "FlexEnd",
149
+ "center": "Center",
150
+ "space-between": "SpaceBetween",
151
+ "space-around": "SpaceAround",
152
+ }
153
+ },
154
+ position: {
155
+ enum: () => CS.UnityEngine.UIElements.Position,
156
+ values: {
157
+ "relative": "Relative",
158
+ "absolute": "Absolute",
159
+ }
160
+ },
161
+ overflow: {
162
+ enum: () => CS.UnityEngine.UIElements.Overflow,
163
+ values: {
164
+ "visible": "Visible",
165
+ "hidden": "Hidden",
166
+ }
167
+ },
168
+ display: {
169
+ enum: () => CS.UnityEngine.UIElements.DisplayStyle,
170
+ values: {
171
+ "flex": "Flex",
172
+ "none": "None",
173
+ }
174
+ },
175
+ visibility: {
176
+ enum: () => CS.UnityEngine.UIElements.Visibility,
177
+ values: {
178
+ "visible": "Visible",
179
+ "hidden": "Hidden",
180
+ }
181
+ },
182
+ whiteSpace: {
183
+ enum: () => CS.UnityEngine.UIElements.WhiteSpace,
184
+ values: {
185
+ "normal": "Normal",
186
+ "nowrap": "NoWrap",
187
+ }
188
+ },
189
+ }
190
+
191
+ /**
192
+ * Parse an enum style value
193
+ * @param key - Style property name
194
+ * @param value - String value from React style (e.g., "row", "flex-start")
195
+ * @returns Unity enum value or null if not found
196
+ */
197
+ function parseEnumValue(key: string, value: string): number | null {
198
+ const mapping = ENUM_MAPPINGS[key]
199
+ if (!mapping) return null
200
+
201
+ const unityEnumName = mapping.values[value]
202
+ if (!unityEnumName) return null
203
+
204
+ const enumType = mapping.enum()
205
+ return enumType[unityEnumName] ?? null
206
+ }
89
207
 
90
208
  /**
91
209
  * Parse a length value from various formats
@@ -130,13 +248,39 @@ export function parseLength(value: number | string): CSLength | number | null {
130
248
  }
131
249
 
132
250
  /**
133
- * Parse a hex color component (1 or 2 characters)
251
+ * Clamp a number to [0, 1] range
134
252
  */
135
- function parseHexComponent(hex: string): number {
136
- if (hex.length === 1) {
137
- hex = hex + hex // Expand shorthand: "f" -> "ff"
253
+ function clamp01(n: number): number {
254
+ return n < 0 ? 0 : n > 1 ? 1 : n
255
+ }
256
+
257
+ /**
258
+ * Create a Unity Color with clamped values
259
+ */
260
+ function createColor(r: number, g: number, b: number, a: number): CSColor {
261
+ return new CS.UnityEngine.Color(clamp01(r), clamp01(g), clamp01(b), clamp01(a))
262
+ }
263
+
264
+ /**
265
+ * Parse hex color string into RGBA components
266
+ * Supports: #rgb, #rgba, #rrggbb, #rrggbbaa
267
+ */
268
+ function parseHexColor(hex: string): CSColor | null {
269
+ const len = hex.length
270
+ const isShort = len === 3 || len === 4
271
+ const isLong = len === 6 || len === 8
272
+ if (!isShort && !isLong) return null
273
+
274
+ const step = isShort ? 1 : 2
275
+ const parse = (i: number): number => {
276
+ const slice = hex.slice(i * step, i * step + step)
277
+ const expanded = isShort ? slice + slice : slice
278
+ return parseInt(expanded, 16) / 255
138
279
  }
139
- return parseInt(hex, 16) / 255
280
+
281
+ const r = parse(0), g = parse(1), b = parse(2)
282
+ const a = (len === 4 || len === 8) ? parse(3) : 1
283
+ return createColor(r, g, b, a)
140
284
  }
141
285
 
142
286
  /**
@@ -155,75 +299,21 @@ export function parseColor(value: string): CSColor | null {
155
299
  return new CS.UnityEngine.Color(r, g, b, a)
156
300
  }
157
301
 
158
- // Hex colors: #rgb, #rgba, #rrggbb, #rrggbbaa
302
+ // Hex colors
159
303
  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
304
+ return parseHexColor(trimmed.slice(1))
197
305
  }
198
306
 
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*\)$/)
307
+ // rgb(r, g, b) or rgba(r, g, b, a) - supports both integer and percentage values
308
+ const rgbMatch = trimmed.match(/^rgba?\s*\(\s*([\d.]+)(%?)\s*,\s*([\d.]+)(%?)\s*,\s*([\d.]+)(%?)\s*(?:,\s*([\d.]+))?\s*\)$/)
201
309
  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
- )
310
+ const isPercent = rgbMatch[2] === "%"
311
+ const divisor = isPercent ? 100 : 255
312
+ const r = parseFloat(rgbMatch[1]) / divisor
313
+ const g = parseFloat(rgbMatch[3]) / divisor
314
+ const b = parseFloat(rgbMatch[5]) / divisor
315
+ const a = rgbMatch[7] !== undefined ? parseFloat(rgbMatch[7]) : 1
316
+ return createColor(r, g, b, a)
227
317
  }
228
318
 
229
319
  return null
@@ -264,9 +354,11 @@ export function parseStyleValue(key: string, value: unknown): unknown {
264
354
  return value
265
355
  }
266
356
 
267
- // Enum properties - pass through as-is (C# handles string -> enum)
268
- if (ENUM_PROPERTIES.has(key)) {
269
- return value
357
+ // Enum properties - convert string to Unity enum value
358
+ if (key in ENUM_MAPPINGS && typeof value === "string") {
359
+ const parsed = parseEnumValue(key, value)
360
+ if (parsed !== null) return parsed
361
+ // Fall through if parsing failed
270
362
  }
271
363
 
272
364
  // Unknown property - pass through unchanged