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.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. 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&#39;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.