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/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # onejs-react
2
+
3
+ React 19 reconciler for Unity's UI Toolkit.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | `src/host-config.ts` | React reconciler implementation (createInstance, commitUpdate, etc.) |
10
+ | `src/renderer.ts` | Entry point: `render(element, container)` |
11
+ | `src/components.tsx` | Component wrappers: View, Text, Label, Button, TextField, etc. |
12
+ | `src/screen.tsx` | Responsive design: ScreenProvider, useBreakpoint, useScreenSize, useResponsive |
13
+ | `src/types.ts` | TypeScript type definitions (includes Vector Drawing types) |
14
+ | `src/index.ts` | Package exports |
15
+
16
+ ## Components
17
+
18
+ | Component | UI Toolkit Element | Description |
19
+ |-----------|-------------------|-------------|
20
+ | `View` | VisualElement | Container element |
21
+ | `Text` | TextElement | Primary text display |
22
+ | `Label` | Label | Form labels, semantic labeling |
23
+ | `Button` | Button | Interactive button |
24
+ | `TextField` | TextField | Text input |
25
+ | `Toggle` | Toggle | Checkbox/toggle |
26
+ | `Slider` | Slider | Numeric slider |
27
+ | `ScrollView` | ScrollView | Scrollable container |
28
+ | `Image` | Image | Image display |
29
+ | `ListView` | ListView | Virtualized list |
30
+
31
+ **Raw text in JSX** (e.g., `<View>Hello</View>`) creates a `TextElement`, providing semantic distinction from explicit `<Label>` components.
32
+
33
+ ## Usage
34
+
35
+ ```tsx
36
+ import { render, View, Text, Label, Button } from 'onejs-react';
37
+
38
+ function App() {
39
+ return (
40
+ <View style={{ padding: 20 }}>
41
+ <Text text="Welcome!" style={{ fontSize: 24 }} />
42
+ <Button text="Click me" onClick={() => console.log('clicked')} />
43
+ <View>Raw text also works</View>
44
+ </View>
45
+ );
46
+ }
47
+
48
+ render(<App />, __root);
49
+ ```
50
+
51
+ ## Type Usage Guide
52
+
53
+ OneJS has multiple type sources. Here's when to use each:
54
+
55
+ ### React Components (Most Common)
56
+
57
+ Import types from `onejs-react` for refs and component props:
58
+
59
+ ```tsx
60
+ import { View, Button, VisualElement, ButtonElement } from "onejs-react"
61
+
62
+ function MyComponent() {
63
+ const viewRef = useRef<VisualElement>(null)
64
+ const buttonRef = useRef<ButtonElement>(null)
65
+
66
+ useEffect(() => {
67
+ buttonRef.current?.Focus()
68
+ }, [])
69
+
70
+ return (
71
+ <View ref={viewRef}>
72
+ <Button ref={buttonRef} text="Click me" />
73
+ </View>
74
+ )
75
+ }
76
+ ```
77
+
78
+ ### Imperative Element Creation
79
+
80
+ For creating elements outside React, use `unity-types`:
81
+
82
+ ```tsx
83
+ import { Button } from "UnityEngine.UIElements"
84
+
85
+ const btn = new Button()
86
+ btn.text = "Dynamic Button"
87
+ __root.Add(btn)
88
+ ```
89
+
90
+ ### render() Container
91
+
92
+ The `render()` function accepts any `RenderContainer`:
93
+
94
+ ```tsx
95
+ import { render, RenderContainer } from "onejs-react"
96
+
97
+ // __root is provided by the runtime
98
+ render(<App />, __root)
99
+ ```
100
+
101
+ ### Type Hierarchy
102
+
103
+ ```
104
+ RenderContainer (minimal: __csHandle, __csType)
105
+ └── VisualElement (full API: style, hierarchy, events)
106
+ ├── TextElement (+ text property)
107
+ │ ├── LabelElement
108
+ │ └── ButtonElement
109
+ ├── TextFieldElement (+ value, isPasswordField, etc.)
110
+ ├── ToggleElement (+ value: boolean)
111
+ ├── SliderElement (+ value, lowValue, highValue)
112
+ └── ScrollViewElement (+ scrollOffset, ScrollTo)
113
+ ```
114
+
115
+ ## Key Concepts
116
+
117
+ - **Element types**: Use `ojs-` prefix internally (e.g., `ojs-view`, `ojs-button`) to avoid conflicts with HTML types
118
+ - **Style shorthands**: `padding`/`margin` are expanded to individual properties (UI Toolkit requirement)
119
+ - **Style cleanup**: When props change, removed style properties are cleared (not just new ones applied)
120
+ - **className updates**: Selective add/remove of classes (not full clear + reapply)
121
+ - **Event handlers**: Registered via `__eventAPI` from QuickJSBootstrap.js
122
+ - **Instance structure**: `{ element, type, props, eventHandlers: Map, appliedStyleKeys: Set }`
123
+
124
+ ## Build & Test
125
+
126
+ ```bash
127
+ npm run typecheck # TypeScript check (no build output - consumed directly by App)
128
+ npm test # Run test suite
129
+ npm run test:watch # Run tests in watch mode
130
+ ```
131
+
132
+ ## Testing
133
+
134
+ Test suite uses Vitest with mocked Unity CS globals. Tests are in `src/__tests__/`:
135
+
136
+ | File | Coverage |
137
+ |------|----------|
138
+ | `host-config.test.ts` | Instance creation, style/className management, events, children |
139
+ | `renderer.test.tsx` | Integration tests: render(), unmount(), React state, effects |
140
+ | `components.test.tsx` | Component wrappers, prop passing, event mapping |
141
+ | `mocks.ts` | Mock implementations of Unity UI Toolkit classes |
142
+ | `setup.ts` | Global test setup for CS, __eventAPI |
143
+
144
+ ## Vector Drawing
145
+
146
+ OneJS exposes Unity's `Painter2D` API for GPU-accelerated vector graphics. Any element can render custom vector content via `onGenerateVisualContent`.
147
+
148
+ ### Basic Usage
149
+
150
+ ```tsx
151
+ import { View, render } from "onejs-react"
152
+
153
+ function Circle() {
154
+ return (
155
+ <View
156
+ style={{ width: 200, height: 200, backgroundColor: "#333" }}
157
+ onGenerateVisualContent={(mgc) => {
158
+ const p = mgc.painter2D
159
+
160
+ // Draw a filled circle
161
+ p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1) // Red
162
+ p.BeginPath()
163
+ p.Arc(
164
+ new CS.UnityEngine.Vector2(100, 100), // center
165
+ 80, // radius
166
+ CS.UnityEngine.UIElements.Angle.Degrees(0),
167
+ CS.UnityEngine.UIElements.Angle.Degrees(360),
168
+ CS.UnityEngine.UIElements.ArcDirection.Clockwise
169
+ )
170
+ p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
171
+ }}
172
+ />
173
+ )
174
+ }
175
+ ```
176
+
177
+ ### Painter2D Methods
178
+
179
+ Path operations:
180
+ - `BeginPath()` - Start a new path
181
+ - `ClosePath()` - Close the current subpath
182
+ - `MoveTo(point)` - Move to point without drawing
183
+ - `LineTo(point)` - Draw line to point
184
+ - `Arc(center, radius, startAngle, endAngle, direction)` - Draw arc
185
+ - `ArcTo(p1, p2, radius)` - Draw arc tangent to two lines
186
+ - `BezierCurveTo(cp1, cp2, end)` - Cubic bezier curve
187
+ - `QuadraticCurveTo(cp, end)` - Quadratic bezier curve
188
+
189
+ Rendering:
190
+ - `Fill(fillRule)` - Fill the current path
191
+ - `Stroke()` - Stroke the current path
192
+
193
+ Properties:
194
+ - `fillColor` - Fill color (Unity Color)
195
+ - `strokeColor` - Stroke color (Unity Color)
196
+ - `lineWidth` - Stroke width in pixels
197
+ - `lineCap` - Line cap style (Butt, Round, Square)
198
+ - `lineJoin` - Line join style (Miter, Round, Bevel)
199
+
200
+ ### Triggering Repaints
201
+
202
+ Use `MarkDirtyRepaint()` to trigger a repaint when drawing state changes:
203
+
204
+ ```tsx
205
+ function AnimatedCircle() {
206
+ const ref = useRef<VisualElement>(null)
207
+ const [radius, setRadius] = useState(50)
208
+
209
+ useEffect(() => {
210
+ // Trigger repaint when radius changes
211
+ ref.current?.MarkDirtyRepaint()
212
+ }, [radius])
213
+
214
+ return (
215
+ <View
216
+ ref={ref}
217
+ style={{ width: 200, height: 200 }}
218
+ onGenerateVisualContent={(mgc) => {
219
+ const p = mgc.painter2D
220
+ p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
221
+ p.BeginPath()
222
+ p.Arc(
223
+ new CS.UnityEngine.Vector2(100, 100),
224
+ radius,
225
+ CS.UnityEngine.UIElements.Angle.Degrees(0),
226
+ CS.UnityEngine.UIElements.Angle.Degrees(360),
227
+ CS.UnityEngine.UIElements.ArcDirection.Clockwise
228
+ )
229
+ p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
230
+ }}
231
+ />
232
+ )
233
+ }
234
+ ```
235
+
236
+ ### Differences from HTML5 Canvas
237
+
238
+ | Feature | Unity Painter2D | HTML5 Canvas |
239
+ |---------|-----------------|--------------|
240
+ | Transforms | Manual point calculation | Built-in translate/rotate/scale |
241
+ | Gradients | Limited (strokeGradient) | Full linear/radial/conic |
242
+ | State Stack | Not built-in | save()/restore() |
243
+ | Text | Via MeshGenerationContext.DrawText() | fillText/strokeText |
244
+ | Shadows | Not available | shadowBlur, shadowColor |
245
+ | Clipping | Via nested VisualElements | clip() path-based |
246
+
247
+ ### Types
248
+
249
+ The following types are re-exported from `unity-types`:
250
+
251
+ ```typescript
252
+ type Vector2 = CS.UnityEngine.Vector2
253
+ type Color = CS.UnityEngine.Color
254
+ type Angle = CS.UnityEngine.UIElements.Angle
255
+ type ArcDirection = CS.UnityEngine.UIElements.ArcDirection
256
+ type Painter2D = CS.UnityEngine.UIElements.Painter2D
257
+ type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContext
258
+ type GenerateVisualContentCallback = (context: MeshGenerationContext) => void
259
+ ```
260
+
261
+ ## Dependencies
262
+
263
+ - `react-reconciler@0.31.x` (React 19 compatible)
264
+ - `vitest` (dev) - Test runner
265
+ - Peer: `react@18.x || 19.x`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-react",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "React 19 renderer for OneJS (Unity UI Toolkit)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -33,10 +33,12 @@
33
33
  "react-reconciler": "^0.31.0"
34
34
  },
35
35
  "devDependencies": {
36
+ "@types/node": "^25.0.3",
36
37
  "@types/react": "^19.0.0",
37
38
  "@types/react-reconciler": "^0.28.9",
38
39
  "react": "^19.0.0",
39
40
  "typescript": "^5.7.0",
41
+ "unity-types": "file:../unity-types",
40
42
  "vitest": "^2.1.0"
41
43
  },
42
44
  "license": "MIT"
@@ -12,6 +12,7 @@ import React from 'react';
12
12
  import { render } from '../renderer';
13
13
  import {
14
14
  View,
15
+ Text,
15
16
  Label,
16
17
  Button,
17
18
  TextField,
@@ -108,6 +109,41 @@ describe('components', () => {
108
109
  });
109
110
  });
110
111
 
112
+ describe('Text', () => {
113
+ it('renders as TextElement', async () => {
114
+ const container = createMockContainer();
115
+ render(<Text text="Hello" />, container as any);
116
+ await flushMicrotasks();
117
+
118
+ expect(container.children[0].__csType).toBe('UnityEngine.UIElements.TextElement');
119
+ });
120
+
121
+ it('sets text property', async () => {
122
+ const container = createMockContainer();
123
+ render(<Text text="Hello World" />, container as any);
124
+ await flushMicrotasks();
125
+
126
+ const el = container.children[0] as MockVisualElement;
127
+ expect(el.text).toBe('Hello World');
128
+ });
129
+
130
+ it('applies styles', async () => {
131
+ const container = createMockContainer();
132
+ render(
133
+ <Text
134
+ text="Styled"
135
+ style={{ fontSize: 24, color: 'white' }}
136
+ />,
137
+ container as any
138
+ );
139
+ await flushMicrotasks();
140
+
141
+ const el = container.children[0] as MockVisualElement;
142
+ expect(getStyleValue(el.style.fontSize)).toBe(24);
143
+ expect(el.style.color).toBeInstanceOf(MockColor);
144
+ });
145
+ });
146
+
111
147
  describe('Label', () => {
112
148
  it('renders as Label element', async () => {
113
149
  const container = createMockContainer();