react-x11 2.51.0 → 2.52.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/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Forms and validation
|
|
2
|
+
|
|
3
|
+
There is no `<form>` element here, so nothing submits itself — wire
|
|
4
|
+
`handleSubmit` to a `Button`'s `onPress` or to `<textinput onSubmit>`.
|
|
5
|
+
|
|
6
|
+
Everything else follows from **one signature**. Every value control in the
|
|
7
|
+
library — the host elements `<textinput>` and `<textarea>`, and the widgets
|
|
8
|
+
`Checkbox`, `Switch`, `RadioGroup`, `Select` and `Slider` — calls
|
|
9
|
+
`onChange(ev)` with a change event carrying `ev.value`, `ev.target.value`,
|
|
10
|
+
`ev.target.name` and, for a checkbox, `ev.target.checked`. That is the shape
|
|
11
|
+
every DOM form library reads, so a library's handler goes straight in:
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<textinput name="host" value={f.values.host} onChange={f.handleChange} />
|
|
15
|
+
<Checkbox name="agree" checked={f.values.agree} onChange={f.handleChange} />
|
|
16
|
+
<Select name="size" value={f.values.size} options={sizes} onChange={f.handleChange} />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
No adapter, no per-widget bridge, nothing to remember about which control
|
|
20
|
+
you are wiring.
|
|
21
|
+
|
|
22
|
+
| library | how it goes in |
|
|
23
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
24
|
+
| `react-hook-form` | `{...register('host')}`, or `<Controller>` for a widget |
|
|
25
|
+
| `formik` | `onChange={handleChange}`, `getFieldProps`, or `<Field>` |
|
|
26
|
+
| `downshift` | `getInputProps()` spread onto a `<textinput>` |
|
|
27
|
+
| `@tanstack/react-form` | `onChange={(ev) => field.handleChange(ev.value)}` — below |
|
|
28
|
+
|
|
29
|
+
Every row was established by running the library headless against this
|
|
30
|
+
renderer rather than by reading its documentation: react-hook-form@7.84.0,
|
|
31
|
+
formik@2.4.9, downshift@9.4.0, @tanstack/react-form@1.33.2.
|
|
32
|
+
|
|
33
|
+
**The one thing to know**: a library whose field contract is
|
|
34
|
+
value-in/value-out — TanStack Form is the one here — needs the value taken
|
|
35
|
+
off the event, and a bare `onChange={field.handleChange}` stores the _event
|
|
36
|
+
object_ as the field value with no error. See below.
|
|
37
|
+
|
|
38
|
+
**Where the line falls.** A control that takes a `name` is a form field and
|
|
39
|
+
reports an event. `Tabs`, `Table` and the menus are not form fields
|
|
40
|
+
and keep their plain callbacks
|
|
41
|
+
([components](../components.md#the-change-event-and-name)).
|
|
42
|
+
|
|
43
|
+
## TanStack Form {#tanstack-form}
|
|
44
|
+
|
|
45
|
+
**Out of the box.** @tanstack/react-form@1.33.2 (form-core 1.33.2).
|
|
46
|
+
|
|
47
|
+
A headless, framework-agnostic form state manager: field values, dirty and
|
|
48
|
+
touched tracking, sync and async validation (including Standard Schema, so
|
|
49
|
+
zod/valibot/arktype plug in directly), and submission handling. Fields are
|
|
50
|
+
value-in/value-out — no DOM events, no refs.
|
|
51
|
+
|
|
52
|
+
The field contract is `field.state.value` + `field.handleChange(value)` —
|
|
53
|
+
a **value, not an event**. react-x11's controls fire an event, so each field
|
|
54
|
+
takes the value off it:
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
onChange={(ev) => field.handleChange(ev.value)}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
One expression, the same one a DOM app writes, and it is identical for a
|
|
61
|
+
`<textinput>` and for a `Checkbox`.
|
|
62
|
+
|
|
63
|
+
Watch for the bare form: `onChange={field.handleChange}` stores the event
|
|
64
|
+
object as the field's value — no error, no warning, and validation sees an
|
|
65
|
+
object where it expected a string.
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
import React from 'react';
|
|
69
|
+
import { useForm } from '@tanstack/react-form';
|
|
70
|
+
import { Button } from 'react-x11';
|
|
71
|
+
import { z } from 'zod';
|
|
72
|
+
|
|
73
|
+
function ConnectForm({ connect }) {
|
|
74
|
+
const form = useForm({
|
|
75
|
+
defaultValues: { host: '', display: ':0' },
|
|
76
|
+
onSubmit: async ({ value }) => connect(value),
|
|
77
|
+
});
|
|
78
|
+
return (
|
|
79
|
+
<box style={{ flexDirection: 'column', gap: 8 }}>
|
|
80
|
+
<form.Field
|
|
81
|
+
name="host"
|
|
82
|
+
validators={{ onChange: z.string().min(3, 'too short') }}
|
|
83
|
+
>
|
|
84
|
+
{(field) => (
|
|
85
|
+
<box style={{ flexDirection: 'column' }}>
|
|
86
|
+
<textinput
|
|
87
|
+
value={field.state.value}
|
|
88
|
+
onChange={(ev) => field.handleChange(ev.value)}
|
|
89
|
+
/>
|
|
90
|
+
{field.state.meta.errors.length > 0 && (
|
|
91
|
+
<text style={{ color: '#cc4444' }}>
|
|
92
|
+
{field.state.meta.errors[0].message}
|
|
93
|
+
</text>
|
|
94
|
+
)}
|
|
95
|
+
</box>
|
|
96
|
+
)}
|
|
97
|
+
</form.Field>
|
|
98
|
+
<Button label="Connect" onPress={() => form.handleSubmit()} />
|
|
99
|
+
</box>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- No `<form>` element exists, so there is no submit-on-Enter for free. Wire
|
|
105
|
+
`form.handleSubmit()` to a button's `onPress` and/or a window-level Enter
|
|
106
|
+
key handler.
|
|
107
|
+
- The **widgets** are the exception: `Checkbox`, `Switch`, `RadioGroup`,
|
|
108
|
+
`Select` and `Slider` still pass the next value first, so
|
|
109
|
+
`onChange={field.handleChange}` is right there and needs no unwrap. Only
|
|
110
|
+
the host elements `<textinput>`/`<textarea>` changed.
|
|
111
|
+
- Validation errors from a zod schema are error _objects_
|
|
112
|
+
(`errors[0].message`), not strings; a plain string validator returns
|
|
113
|
+
strings. A `<text>` child must be a string, so render `.message`.
|
|
114
|
+
- The package also ships SSR and Next helpers under subpaths; the root
|
|
115
|
+
import used here pulls in no react-dom.
|
|
116
|
+
|
|
117
|
+
## zod {#zod}
|
|
118
|
+
|
|
119
|
+
**Out of the box.** zod@4.4.3.
|
|
120
|
+
|
|
121
|
+
TypeScript-first schema validation. Pure computation, no DOM, nothing
|
|
122
|
+
renderer-specific — the interesting part is only where it plugs in:
|
|
123
|
+
|
|
124
|
+
- **TanStack Form** — pass a schema straight into a field's
|
|
125
|
+
`validators: { onChange: z.string().min(3, 'too short') }`. Zod 4
|
|
126
|
+
implements Standard Schema, so no resolver package is needed.
|
|
127
|
+
- **React Hook Form** — via `@hookform/resolvers/zod`, with either
|
|
128
|
+
`register()` or `Controller`; see below.
|
|
129
|
+
- **Standalone** — validating a settings file or a config blob before
|
|
130
|
+
rendering is just `schema.safeParse(data)`.
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
import { z } from 'zod';
|
|
134
|
+
|
|
135
|
+
const settings = z.object({
|
|
136
|
+
display: z.string().regex(/^:\d+$/, 'DISPLAY looks like :0'),
|
|
137
|
+
scale: z.coerce.number().min(0.5).max(3).default(1),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function Settings({ raw }) {
|
|
141
|
+
const parsed = settings.safeParse(raw);
|
|
142
|
+
if (!parsed.success) {
|
|
143
|
+
// render the issues instead of crashing the app
|
|
144
|
+
return (
|
|
145
|
+
<box style={{ flexDirection: 'column' }}>
|
|
146
|
+
{parsed.error.issues.map((i) => (
|
|
147
|
+
<text
|
|
148
|
+
key={i.path.join('.')}
|
|
149
|
+
>{`${i.path.join('.')}: ${i.message}`}</text>
|
|
150
|
+
))}
|
|
151
|
+
</box>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return <text>display {parsed.data.display}</text>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- Zod 4 field errors surface as issue objects; render `issue.message`, not
|
|
159
|
+
the issue itself.
|
|
160
|
+
- `valibot` and `arktype` are equally viable (also Standard Schema). zod is
|
|
161
|
+
documented here because both verified form integrations used it.
|
|
162
|
+
- Bundle-size arguments from the web world do not apply — an X11 app ships
|
|
163
|
+
no bundle.
|
|
164
|
+
|
|
165
|
+
## React Hook Form {#react-hook-form}
|
|
166
|
+
|
|
167
|
+
**Out of the box, both APIs.** react-hook-form@7.84.0 with
|
|
168
|
+
@hookform/resolvers@5.5.7.
|
|
169
|
+
|
|
170
|
+
RHF's headline API is uncontrolled-first: `register()` returns DOM-shaped
|
|
171
|
+
props (`{name, ref, onChange(e), onBlur(e)}`) and reads `e.target.value` off
|
|
172
|
+
the event. That is exactly what `<textinput>` provides, so the terse spread
|
|
173
|
+
works:
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
const { register, handleSubmit } = useForm({ defaultValues: { host: '' } });
|
|
177
|
+
<textinput {...register('host')} />;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Three things had to line up, and all three now do: `name` exists as a prop,
|
|
181
|
+
`onChange` receives an event whose `target.value` is the new text, and the
|
|
182
|
+
`ref` RHF attaches lands on a node whose `value` is **writable** — RHF
|
|
183
|
+
assigns `ref.value = ''` while registering the field, which used to throw
|
|
184
|
+
`TypeError: Cannot set property value of #<TextInputNode> which has only a
|
|
185
|
+
getter` from inside the commit phase and take the render down with it.
|
|
186
|
+
|
|
187
|
+
Verified working through `register()`: typing updates `getValues()`,
|
|
188
|
+
`handleSubmit` receives them, `setValue(name, v)` writes through the ref so
|
|
189
|
+
the field redraws, and `setFocus(name)` focuses the node.
|
|
190
|
+
|
|
191
|
+
One asymmetry to know about: **`reset()` updates form state but does not
|
|
192
|
+
clear an uncontrolled field's text.** RHF only writes values back through
|
|
193
|
+
refs on the reset path when its `isWeb` probe
|
|
194
|
+
(`typeof window !== 'undefined' && window.HTMLElement && document`) passes,
|
|
195
|
+
which it never does here. `setValue()` is not gated that way and does write
|
|
196
|
+
through. If you need `reset()` to clear the display, make the field
|
|
197
|
+
controlled — `<Controller>`, below — or assign `ref.current.value = ''`
|
|
198
|
+
yourself.
|
|
199
|
+
|
|
200
|
+
`<Controller>` / `useController` still works, unchanged, and is the better
|
|
201
|
+
choice when you want the field controlled. `field.onChange` accepts either a
|
|
202
|
+
plain value or an event, so both of these are fine:
|
|
203
|
+
|
|
204
|
+
```jsx
|
|
205
|
+
onChange={(ev) => field.onChange(ev.target.value)} // explicit
|
|
206
|
+
onChange={field.onChange} // RHF unwraps .target
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
import React from 'react';
|
|
211
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
212
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
213
|
+
import { Button } from 'react-x11';
|
|
214
|
+
import { z } from 'zod';
|
|
215
|
+
|
|
216
|
+
const schema = z.object({
|
|
217
|
+
host: z.string().min(3, 'too short'),
|
|
218
|
+
display: z.string(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
function ConnectForm({ connect }) {
|
|
222
|
+
const { control, handleSubmit } = useForm({
|
|
223
|
+
defaultValues: { host: '', display: ':0' },
|
|
224
|
+
resolver: zodResolver(schema),
|
|
225
|
+
mode: 'onChange',
|
|
226
|
+
});
|
|
227
|
+
return (
|
|
228
|
+
<box style={{ flexDirection: 'column', gap: 8 }}>
|
|
229
|
+
<Controller
|
|
230
|
+
name="host"
|
|
231
|
+
control={control}
|
|
232
|
+
render={({ field, fieldState }) => (
|
|
233
|
+
<box style={{ flexDirection: 'column' }}>
|
|
234
|
+
<textinput
|
|
235
|
+
value={field.value}
|
|
236
|
+
onChange={field.onChange}
|
|
237
|
+
onBlur={field.onBlur}
|
|
238
|
+
/>
|
|
239
|
+
{fieldState.error && <text>{fieldState.error.message}</text>}
|
|
240
|
+
</box>
|
|
241
|
+
)}
|
|
242
|
+
/>
|
|
243
|
+
<Button label="Connect" onPress={handleSubmit(connect)} />
|
|
244
|
+
</box>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- A `Controller` field is controlled, so its subtree re-renders on every
|
|
250
|
+
keystroke. `register()` does not, which is the same performance argument
|
|
251
|
+
RHF makes on the web — and it now applies here too.
|
|
252
|
+
- `setFocus(name)` works: it calls `.focus()` on whatever the ref holds, and
|
|
253
|
+
a `<textinput>` node has one.
|
|
254
|
+
- `handleSubmit` returns a function. Call it: `handleSubmit(fn)` in an
|
|
255
|
+
`onPress` is the curried form, and there is no submit event to pass.
|
|
256
|
+
|
|
257
|
+
## Formik {#formik}
|
|
258
|
+
|
|
259
|
+
**Out of the box.** formik@2.4.9.
|
|
260
|
+
|
|
261
|
+
Formik was the clearest casualty of the old `onChange(string)` contract, and
|
|
262
|
+
it failed _silently_: `handleChange` treats a string argument as its curried
|
|
263
|
+
field-name form, returns a handler, and never touches state — so spreading
|
|
264
|
+
`getFieldProps` onto a `<textinput>` left `values` at its initial object and
|
|
265
|
+
`touched` empty, with nothing logged. That is fixed by the event, not by an
|
|
266
|
+
adapter. All three of Formik's field styles now work:
|
|
267
|
+
|
|
268
|
+
```jsx
|
|
269
|
+
import { Formik, Field, useFormik } from 'formik';
|
|
270
|
+
|
|
271
|
+
// 1. the spread
|
|
272
|
+
<textinput {...formik.getFieldProps('host')} />
|
|
273
|
+
|
|
274
|
+
// 2. by hand
|
|
275
|
+
<textinput
|
|
276
|
+
name="host"
|
|
277
|
+
value={formik.values.host}
|
|
278
|
+
onChange={formik.handleChange}
|
|
279
|
+
onBlur={formik.handleBlur}
|
|
280
|
+
/>
|
|
281
|
+
|
|
282
|
+
// 3. <Field> with a render prop
|
|
283
|
+
<Field name="host">
|
|
284
|
+
{({ field }) => <textinput {...field} />}
|
|
285
|
+
</Field>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`handleChange` reads `target.name` to decide which field to write, so the
|
|
289
|
+
`name` prop is not optional in any of these — that is what the synthetic
|
|
290
|
+
event carries it for.
|
|
291
|
+
|
|
292
|
+
The widgets need no bridge — same handler, same signature:
|
|
293
|
+
|
|
294
|
+
```jsx
|
|
295
|
+
<Checkbox
|
|
296
|
+
name="agree"
|
|
297
|
+
checked={formik.values.agree}
|
|
298
|
+
onChange={formik.handleChange}
|
|
299
|
+
>
|
|
300
|
+
I agree
|
|
301
|
+
</Checkbox>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`ev.target` is `{ type: 'checkbox', name, value, checked }`, which is exactly
|
|
305
|
+
what `handleChange` destructures — including `type`, which is how it knows to
|
|
306
|
+
take `checked` rather than `value`.
|
|
307
|
+
|
|
308
|
+
- `<Formik>`'s render-prop form and `useFormik` both work; there is no
|
|
309
|
+
`<form>` element, so call `submitForm()` (or `handleSubmit()`) yourself
|
|
310
|
+
from a `Button` or from `<textinput onSubmit>`.
|
|
311
|
+
- Passing a non-event object to `handleChange` — a hand-rolled fake event
|
|
312
|
+
with no `target` — still throws `TypeError: Cannot read properties of
|
|
313
|
+
undefined (reading 'type')` rather than no-opping. Pass the real event.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Headless components
|
|
2
|
+
|
|
3
|
+
"Headless" is a promise that gets broken a lot. A library that returns DOM
|
|
4
|
+
prop bags — `getInputProps()`, `useButton()` — is not headless in any sense
|
|
5
|
+
that helps here; it is DOM-coupled with the rendering deferred. The two
|
|
6
|
+
packages below are the real thing: they own state and collections, return
|
|
7
|
+
plain values, and never touch a ref.
|
|
8
|
+
|
|
9
|
+
Everything else that markets itself as headless is in the
|
|
10
|
+
[negative-results register](../ecosystem.md#what-does-not-work) — Headless
|
|
11
|
+
UI, the Radix primitives and downshift all fail, the last one silently until
|
|
12
|
+
the first keystroke.
|
|
13
|
+
|
|
14
|
+
For finished widgets, react-x11 ships its own: `Button`, `Checkbox`,
|
|
15
|
+
`Radio`/`RadioGroup`, `Switch`, `ProgressBar`, `Select`, `Slider`, `Tooltip`,
|
|
16
|
+
`Dialog`, `MenuBar`/`ContextMenu`, `Tabs`, `Table`, `SplitPane`. See
|
|
17
|
+
[components](../components.md).
|
|
18
|
+
|
|
19
|
+
## react-stately {#react-stately}
|
|
20
|
+
|
|
21
|
+
**Out of the box.** react-stately@3.48.0.
|
|
22
|
+
|
|
23
|
+
The state-machine half of React Aria: `useListState`, `useTreeState`,
|
|
24
|
+
`useTableState`, `useSelectState`, `useToggleState`, `useSliderState`,
|
|
25
|
+
`useComboBoxState`, `useMenuTriggerState`, `useNumberFieldState` and about
|
|
26
|
+
thirty-five more, plus the `Item`/`Section` collection API. Each hook owns
|
|
27
|
+
selection, disabled-key traversal, expansion and typeahead-ready collections
|
|
28
|
+
— everything a widget needs _except_ rendering and event binding, which is
|
|
29
|
+
exactly the part react-x11 supplies.
|
|
30
|
+
|
|
31
|
+
There is no seam to bridge: the hooks take props, return state, and never
|
|
32
|
+
touch a ref. A scan of the shipped `dist/` finds no runtime reference to
|
|
33
|
+
`document`, `window` or `getBoundingClientRect` in any hook, and its
|
|
34
|
+
dependencies (`@internationalized/*`, `@react-types/shared`,
|
|
35
|
+
`use-sync-external-store`) are DOM-free.
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
import React from 'react';
|
|
39
|
+
import { useListState, Item } from 'react-stately';
|
|
40
|
+
|
|
41
|
+
function Picker() {
|
|
42
|
+
const state = useListState({
|
|
43
|
+
children: [
|
|
44
|
+
<Item key="alpha">Alpha</Item>,
|
|
45
|
+
<Item key="beta">Beta</Item>,
|
|
46
|
+
<Item key="gamma">Gamma</Item>,
|
|
47
|
+
],
|
|
48
|
+
selectionMode: 'multiple',
|
|
49
|
+
});
|
|
50
|
+
const mgr = state.selectionManager;
|
|
51
|
+
return (
|
|
52
|
+
<box
|
|
53
|
+
style={{ flexDirection: 'column' }}
|
|
54
|
+
focusable
|
|
55
|
+
onKeyDown={(ev) => {
|
|
56
|
+
if (ev.key === 'ArrowDown')
|
|
57
|
+
mgr.setFocusedKey(
|
|
58
|
+
state.collection.getKeyAfter(mgr.focusedKey) ?? mgr.focusedKey,
|
|
59
|
+
);
|
|
60
|
+
if (ev.codepoint === 32) mgr.toggleSelection(mgr.focusedKey);
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{[...state.collection].map((item) => (
|
|
64
|
+
<box
|
|
65
|
+
key={item.key}
|
|
66
|
+
style={{ height: 20, justifyContent: 'center' }}
|
|
67
|
+
onClick={() => mgr.toggleSelection(item.key)}
|
|
68
|
+
>
|
|
69
|
+
<text>
|
|
70
|
+
{(mgr.isSelected(item.key) ? '* ' : ' ') + item.rendered}
|
|
71
|
+
</text>
|
|
72
|
+
</box>
|
|
73
|
+
))}
|
|
74
|
+
</box>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Only `react-stately` is portable.** `react-aria` (the hooks that produce
|
|
80
|
+
DOM props and bind DOM events) and `react-aria-components` are not — do not
|
|
81
|
+
follow Adobe's docs into `useListBox` or `useButton`, which return DOM prop
|
|
82
|
+
bags.
|
|
83
|
+
- As of 3.48 the package is consolidated: the old `@react-stately/*` packages
|
|
84
|
+
are bundled in, with per-hook subpath exports
|
|
85
|
+
(`react-stately/useListState`) for lean imports.
|
|
86
|
+
- The collection API wants `<Item>`/`<Section>` elements, or an `items` plus
|
|
87
|
+
render-function pair. It is a compile target of its own, slightly
|
|
88
|
+
ceremonious for a three-entry menu.
|
|
89
|
+
- `useAsyncList` pulls in fetch-based loading state — fine under Node, but it
|
|
90
|
+
is the one hook with I/O opinions.
|
|
91
|
+
- Drag-and-drop hooks (`useDraggableCollectionState`) hold state fine, but
|
|
92
|
+
everything that would _feed_ them events is DOM-side. Expect to write the
|
|
93
|
+
event plumbing.
|
|
94
|
+
|
|
95
|
+
## `@tanstack/react-table` {#tanstack-table}
|
|
96
|
+
|
|
97
|
+
**Out of the box.** @tanstack/react-table@9.1.2 (table-core 9.1.2,
|
|
98
|
+
react-store 0.11.1).
|
|
99
|
+
|
|
100
|
+
A headless table engine: column defs, row models, sorting, filtering,
|
|
101
|
+
grouping, pagination, column sizing, visibility and pinning — as pure state
|
|
102
|
+
plus derived row models. It renders nothing; you map `getHeaderGroups()` and
|
|
103
|
+
`getRowModel().rows` to whatever the host renderer draws.
|
|
104
|
+
|
|
105
|
+
v9 talks about "adapters", and the word means the framework, not the
|
|
106
|
+
renderer: `@tanstack/react-table` _is_ the React adapter, wiring the
|
|
107
|
+
framework-agnostic core's store to React's subscription model. react-x11 is
|
|
108
|
+
React, so that adapter is already ours and there is nothing renderer-shaped
|
|
109
|
+
to write. It still never touches `react-dom` or `document` at runtime —
|
|
110
|
+
though v9 moves two of this page's checks, see the react-dom and `flexRender`
|
|
111
|
+
bullets below. `column.getToggleSortingHandler()` still goes straight onto a
|
|
112
|
+
header `onClick`.
|
|
113
|
+
|
|
114
|
+
v9's API is modular where v8's was monolithic: features and row models are
|
|
115
|
+
registered up front through `tableFeatures`, and the hook is `useTable`.
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
import React from 'react';
|
|
119
|
+
import {
|
|
120
|
+
useTable,
|
|
121
|
+
tableFeatures,
|
|
122
|
+
rowSortingFeature,
|
|
123
|
+
createCoreRowModel,
|
|
124
|
+
createSortedRowModel,
|
|
125
|
+
createColumnHelper,
|
|
126
|
+
flexRender,
|
|
127
|
+
} from '@tanstack/react-table';
|
|
128
|
+
|
|
129
|
+
// Registered once, outside the component. A feature that is not listed here
|
|
130
|
+
// does not exist: its state slice, its column methods, its row model.
|
|
131
|
+
const features = tableFeatures({
|
|
132
|
+
rowSortingFeature,
|
|
133
|
+
coreRowModel: createCoreRowModel(),
|
|
134
|
+
sortedRowModel: createSortedRowModel(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const col = createColumnHelper();
|
|
138
|
+
const columns = [
|
|
139
|
+
col.accessor('name', { header: 'Name' }),
|
|
140
|
+
col.accessor('port', { header: 'Port', cell: (i) => `:${i.getValue()}` }),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
function Servers({ data }) {
|
|
144
|
+
const table = useTable({ features, columns, data });
|
|
145
|
+
return (
|
|
146
|
+
<box style={{ flexDirection: 'column' }}>
|
|
147
|
+
{table.getHeaderGroups().map((hg) => (
|
|
148
|
+
<box key={hg.id} style={{ flexDirection: 'row', height: 20 }}>
|
|
149
|
+
{hg.headers.map((h) => (
|
|
150
|
+
<box
|
|
151
|
+
key={h.id}
|
|
152
|
+
style={{ width: 120 }}
|
|
153
|
+
onClick={h.column.getToggleSortingHandler()}
|
|
154
|
+
>
|
|
155
|
+
<text>
|
|
156
|
+
{flexRender(h.column.columnDef.header, h.getContext())}
|
|
157
|
+
</text>
|
|
158
|
+
</box>
|
|
159
|
+
))}
|
|
160
|
+
</box>
|
|
161
|
+
))}
|
|
162
|
+
{table.getRowModel().rows.map((row) => (
|
|
163
|
+
<box key={row.id} style={{ flexDirection: 'row', height: 20 }}>
|
|
164
|
+
{row.getAllCells().map((cell) => (
|
|
165
|
+
<text key={cell.id} style={{ width: 120 }}>
|
|
166
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
167
|
+
</text>
|
|
168
|
+
))}
|
|
169
|
+
</box>
|
|
170
|
+
))}
|
|
171
|
+
</box>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Sorting is uncontrolled here — the table's own store holds it, and the
|
|
177
|
+
header click re-renders through the adapter's subscription. Pass
|
|
178
|
+
`state`/`onSortingChange` as before to control it, or a selector as
|
|
179
|
+
`useTable`'s second argument to subscribe a component to only some slices.
|
|
180
|
+
|
|
181
|
+
The natural pairing is inside the existing `Table` component, or raw
|
|
182
|
+
`<box style={{ flexDirection: 'row' }}>` rows: TanStack owns the data logic,
|
|
183
|
+
react-x11 owns painting.
|
|
184
|
+
|
|
185
|
+
- **`coreRowModel` is opt-in, and forgetting it fails silently.** Without
|
|
186
|
+
`coreRowModel: createCoreRowModel()` in `tableFeatures`, the table
|
|
187
|
+
constructs, headers render, and `getRowModel().rows` is `[]` — no error.
|
|
188
|
+
The official examples lean on framework presets that include it; register
|
|
189
|
+
it yourself.
|
|
190
|
+
- **npm installs a `react-dom` you must not keep.** `@tanstack/react-store`
|
|
191
|
+
declares a `react-dom` _peer_ dependency, which npm 7+ auto-installs. The
|
|
192
|
+
shipped code never imports it — grep test 2 on all four `@tanstack`
|
|
193
|
+
packages finds one `"button"` in a doc comment, and this section was
|
|
194
|
+
verified with `node_modules/react-dom` deleted — but a resident react-dom
|
|
195
|
+
is exactly the [silent-failures](../ecosystem.md#silent-failures) hazard.
|
|
196
|
+
Install with `--legacy-peer-deps`, or delete it and let `npm ls react-dom`
|
|
197
|
+
stay empty.
|
|
198
|
+
- **`flexRender` now returns a React element even for a plain string cell**
|
|
199
|
+
(v8 returned the string itself). Inside `<text>` that is fine — the string
|
|
200
|
+
arrives as a text chunk and paints identically — but a test or a11y walk
|
|
201
|
+
that reads a `<text>` node's `children` prop now sees an element object.
|
|
202
|
+
Read the rendered chunks, not the prop. A column def whose `cell` returns
|
|
203
|
+
DOM elements still throws the renderer's unknown-element error; return
|
|
204
|
+
strings or react-x11 elements.
|
|
205
|
+
- Column _resizing_ helpers (`header.getResizeHandler`) still expect DOM
|
|
206
|
+
mouse or touch events with `clientX`, and now fall back to a
|
|
207
|
+
`document`-listener drag loop (injectable as `_contextDocument`). Simpler
|
|
208
|
+
to ignore the helper: write your own drag handling off
|
|
209
|
+
`onMouseDown`/`onMouseMove` — react-x11 events carry `x`/`y` — and call
|
|
210
|
+
`table.setColumnSizing` (needs `columnSizingFeature` registered).
|
|
211
|
+
- `useTable` probes `typeof window` to pick `useLayoutEffect`; under Node it
|
|
212
|
+
gets the `useEffect` branch. That is a grep-test-3 hit of the harmless
|
|
213
|
+
kind — timing, not a feature gate — and the sorted re-render above runs
|
|
214
|
+
through it.
|
|
215
|
+
- For thousands of rows, pair it with a virtualizer — see
|
|
216
|
+
[layout](layout.md#tanstack-virtual). TanStack Table will happily hand you
|
|
217
|
+
10,000 row objects and let the renderer drown.
|
|
218
|
+
- State updates driven from outside a React event land one frame later than
|
|
219
|
+
the call, the same as any react-x11 update.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Internationalization
|
|
2
|
+
|
|
3
|
+
## i18next + react-i18next
|
|
4
|
+
|
|
5
|
+
**Out of the box.** i18next@26.3.6, react-i18next@17.0.11.
|
|
6
|
+
|
|
7
|
+
i18next is the de-facto JS i18n core — resources, interpolation, plurals,
|
|
8
|
+
namespaces, backends — and react-i18next binds it to React with
|
|
9
|
+
`useTranslation`, `<Trans>` for translations containing markup, and
|
|
10
|
+
provider-driven re-render on language change.
|
|
11
|
+
|
|
12
|
+
The core has no DOM in it and react-i18next renders whatever elements you
|
|
13
|
+
hand it, so the whole stack runs unmodified: interpolation, count-based
|
|
14
|
+
plurals, `<Trans>`, and `i18n.changeLanguage('de')` re-rendering the mounted
|
|
15
|
+
tree in place. Node ships full ICU, so `Intl.NumberFormat` and
|
|
16
|
+
`Intl.DateTimeFormat` formatting work too.
|
|
17
|
+
|
|
18
|
+
Two react-x11-specific rules:
|
|
19
|
+
|
|
20
|
+
1. **`<Trans>` component maps must use react-x11 elements.**
|
|
21
|
+
`components={{ 1: <span/> }}` throws
|
|
22
|
+
`react-x11: <span> is not allowed inside <text>` — inside a `<text>`, only
|
|
23
|
+
strings and nested `<text>` spans are legal. Use `<text style={...}/>` as
|
|
24
|
+
the wrapper.
|
|
25
|
+
2. **Turn off HTML escaping.** i18next escapes interpolated values for an
|
|
26
|
+
HTML sink by default, and react-x11 draws raw glyphs, so `O'Brien` renders
|
|
27
|
+
as `O'Brien`. Set `interpolation: { escapeValue: false }`.
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
import React from 'react';
|
|
31
|
+
import i18next from 'i18next';
|
|
32
|
+
import {
|
|
33
|
+
I18nextProvider,
|
|
34
|
+
initReactI18next,
|
|
35
|
+
useTranslation,
|
|
36
|
+
Trans,
|
|
37
|
+
} from 'react-i18next';
|
|
38
|
+
|
|
39
|
+
const i18n = i18next.createInstance();
|
|
40
|
+
await i18n.use(initReactI18next).init({
|
|
41
|
+
lng: 'en',
|
|
42
|
+
resources: {
|
|
43
|
+
en: {
|
|
44
|
+
translation: {
|
|
45
|
+
greeting: 'Hello, {{name}}!',
|
|
46
|
+
items_one: '{{count}} item',
|
|
47
|
+
items_other: '{{count}} items',
|
|
48
|
+
rich: 'Press <1>Enter</1> to save',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
interpolation: { escapeValue: false }, // no HTML sink to escape for
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function Status() {
|
|
56
|
+
const { t } = useTranslation();
|
|
57
|
+
return (
|
|
58
|
+
<box style={{ flexDirection: 'column' }}>
|
|
59
|
+
<text>{t('greeting', { name: 'X11' })}</text>
|
|
60
|
+
<text>{t('items', { count: 2 })}</text>
|
|
61
|
+
<text>
|
|
62
|
+
<Trans
|
|
63
|
+
i18nKey="rich"
|
|
64
|
+
components={{ 1: <text style={{ color: '#e00' }} /> }}
|
|
65
|
+
/>
|
|
66
|
+
</text>
|
|
67
|
+
</box>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// mount under <I18nextProvider i18n={i18n}> inside your <window>;
|
|
72
|
+
// i18n.changeLanguage('de') re-renders live
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For loading translations from disk, `i18next-fs-backend` is the natural
|
|
76
|
+
desktop choice; `i18next-http-backend` also works, since Node has `fetch`.
|
|
77
|
+
|
|
78
|
+
## What is missing below the string
|
|
79
|
+
|
|
80
|
+
These are react-x11 gaps rather than i18next ones, and they matter more than
|
|
81
|
+
the library choice:
|
|
82
|
+
|
|
83
|
+
- **There is no X input method.** Dead keys and the Compose key are handled
|
|
84
|
+
in the renderer ([events.md](../events.md#composition)), but there is no
|
|
85
|
+
XIM/IBus integration, so a language that needs a real input method — CJK —
|
|
86
|
+
can be _displayed_ but not typed.
|
|
87
|
+
- `<Trans>` with the default `<strong>`- and `<em>`-style numbered tags in
|
|
88
|
+
translations is fine as long as every referenced component is a `<text>`
|
|
89
|
+
span. An accidental DOM tag fails loudly at render, which is the good case.
|