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,148 @@
1
+ # Permissions
2
+
3
+ May this app use the camera, the microphone, the screen, the accessibility
4
+ APIs — and how does it ask?
5
+
6
+ ```jsx
7
+ import { usePermission } from 'react-x11';
8
+
9
+ function RecordButton() {
10
+ const mic = usePermission('microphone');
11
+
12
+ if (!mic.available) return <Button label="Record" onPress={record} />;
13
+ if (mic.status === 'granted')
14
+ return <Button label="Record" onPress={record} />;
15
+ return (
16
+ <Button
17
+ label={mic.status === 'prompt' ? 'Allow microphone' : 'Open Settings'}
18
+ onPress={() =>
19
+ mic.status === 'prompt' ? mic.request() : mic.openSettings()
20
+ }
21
+ />
22
+ );
23
+ }
24
+ ```
25
+
26
+ ## The ladder
27
+
28
+ The [file dialog](filedialog.md)'s shape again, with one rung that answers and
29
+ one that only points:
30
+
31
+ | | | |
32
+ | --- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | 1 | **the platform's authorization** | The [cocoa backend](macos.md): macOS's privacy (TCC) status per kind, and the system's own prompt where a framework offers one — `AVCaptureDevice` for the camera and microphone, `CoreLocation`, and the rest. |
34
+ | 2 | **the Settings pane** | `open x-apple.systempreferences:…` — any Mac, bridge or not. It cannot read a status or raise a prompt; it puts the user in front of the switch, which is all `openPrivacySettings` promises. |
35
+
36
+ **On Linux nothing answers yet.** The freedesktop counterparts are the
37
+ per-device portals (`org.freedesktop.portal.Camera`, `.Location`, …), which
38
+ need no bridge and are
39
+ [#466](https://github.com/sidorares/react-x11/issues/466)'s remaining half.
40
+ Until then a status query says `'unknown'` and a request rejects with a
41
+ **typed** `NoPermissionServiceError` — the signal to keep the feature behind
42
+ the answer, not a crash. `usePermission().available` is that branch as render
43
+ state.
44
+
45
+ The top rung is never chosen by naming a backend: the call asks the connection
46
+ the tree renders through whether it has an authorization API, and the cocoa
47
+ backend says so by carrying one.
48
+
49
+ ## The vocabulary
50
+
51
+ ```ts
52
+ type PermissionKind =
53
+ | 'camera'
54
+ | 'microphone'
55
+ | 'screen-recording'
56
+ | 'accessibility'
57
+ | 'input-monitoring'
58
+ | 'automation'
59
+ | 'location'
60
+ | 'calendars'
61
+ | 'reminders';
62
+
63
+ type PermissionStatus =
64
+ 'granted' | 'denied' | 'restricted' | 'prompt' | 'write-only' | 'unknown';
65
+ ```
66
+
67
+ A status is one of six words, and two of them are not the platform's:
68
+
69
+ | | |
70
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
+ | `'granted'` | use it |
72
+ | `'denied'` | the user said no; `openPrivacySettings()` is the way back |
73
+ | `'restricted'` | MDM or parental controls — the user **cannot** grant it, so no Settings button |
74
+ | `'prompt'` | not decided yet — `request()` would ask |
75
+ | `'write-only'` | macOS 14's partial grant for `calendars` and `reminders`: the app may **save** an item it cannot read. A grant to a writer and a refusal to a reader, so it crosses as its own word rather than being flattened into one of them |
76
+ | `'unknown'` | nothing here can say. A fact about the machine, not the permission, and the reason a status query never throws |
77
+
78
+ A request answers with the **status after the user answered**, never a bare
79
+ boolean, because `'denied'` and `'restricted'` want different UI.
80
+
81
+ ## `usePermission()`
82
+
83
+ ```ts
84
+ const { status, available, request, refresh, openSettings } = usePermission(kind, { target? });
85
+ ```
86
+
87
+ `status` starts `'unknown'` and settles once read. `request()` resolves with
88
+ the status after the user answered and updates `status` with it; a second call
89
+ while one is in flight returns the same promise, so a double-clicked button
90
+ asks once. There is **no change notification** behind a status on any
91
+ platform — the status is what was last read, on mount and after each
92
+ `request()`; `refresh()` re-reads it on your own cue, a window coming back to
93
+ the front being the usual one.
94
+
95
+ ## The bare functions
96
+
97
+ ```ts
98
+ import {
99
+ permissionStatus,
100
+ requestPermission,
101
+ openPrivacySettings,
102
+ permissionBackend,
103
+ } from 'react-x11';
104
+
105
+ await permissionStatus('camera'); // never rejects for anything about the machine
106
+ await requestPermission('camera'); // rejects with NoPermissionServiceError where nothing can ask
107
+ await openPrivacySettings('camera'); // true if something opened; false off macOS
108
+ permissionBackend(); // 'cocoa' | null, synchronously
109
+ ```
110
+
111
+ ## What each kind actually does on macOS
112
+
113
+ Not every kind has a prompt of its own, and the shape of the request follows
114
+ the framework's:
115
+
116
+ | kind | status | the "prompt" |
117
+ | ---------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `camera`, `microphone` | all five | the system's in-process dialog; the answer arrives when the user clicks |
119
+ | `screen-recording` | never `'prompt'` | the system's go-to-Settings dialog, shown once; the request resolves at once, and a new grant needs a **restart** |
120
+ | `accessibility` | never `'prompt'` | the go-to-Settings dialog; resolves at once, and the grant applies live |
121
+ | `input-monitoring` | `'prompt'` while open | posts the prompt and resolves at once |
122
+ | `automation` | per **target** | `{ target: 'com.apple.finder' }` — the bundle id of a **running** app, or the call throws; asking blocks until answered |
123
+ | `location` | all five | `requestWhenInUseAuthorization`, answered through the run loop |
124
+ | `calendars` | plus `'write-only'` | EventKit's own prompt. `{ access: 'write-only' }` asks the narrower one; the grant is what [desktop-calendar.md](desktop-calendar.md) reads through |
125
+ | `reminders` | plus `'write-only'` | the same store, a separate grant. `{ access: 'write-only' }` is a `TypeError` here: reminders have no such grant |
126
+ | `files-and-folders` | — | no API: reading the folder _is_ the prompt and `EPERM` the denial; `openPrivacySettings` reaches the pane |
127
+ | `full-disk-access` | — | no API; `openPrivacySettings('full-disk-access')` |
128
+
129
+ **Attribution is the thing to know.** A bare `node` process is attributed to
130
+ its _responsible process_ — the terminal, an IDE — or to `node` itself, and
131
+ prompts with no usage-description strings. A bundled app must carry the keys
132
+ (`NSCameraUsageDescription`, `NSMicrophoneUsageDescription`,
133
+ `NSLocationUsageDescription`, `NSAppleEventsUsageDescription`,
134
+ `NSCalendarsFullAccessUsageDescription`,
135
+ `NSCalendarsWriteOnlyAccessUsageDescription`,
136
+ `NSRemindersFullAccessUsageDescription`) in its `Info.plist` — without them
137
+ the request never prompts, or TCC ends the process. See
138
+ [packaging.md](packaging.md).
139
+
140
+ ## What differs between the rungs
141
+
142
+ - **The Settings rung reads nothing.** `openPrivacySettings` opens the pane
143
+ on any Mac — through the bridge on the cocoa backend, by `open` under
144
+ XQuartz — and that is all it does; the status stays `'unknown'` on the X11
145
+ backend because nothing there can read TCC.
146
+ - **`'unknown'` is not `'prompt'`.** An app that treats them alike will show
147
+ an "Allow" button on Linux that rejects when pressed. Branch on `available`
148
+ first.
@@ -0,0 +1,430 @@
1
+ # React features
2
+
3
+ You already know React. This page is about the parts where that knowledge
4
+ transfers unchanged, the parts where it transfers with a caveat, and the
5
+ four places where a habit from the DOM will quietly not work.
6
+
7
+ The short version: **everything in React core behaves exactly as documented.**
8
+ Hooks, context, error boundaries, `<Suspense>`, `lazy`, `use`, transitions,
9
+ `<Profiler>`, `<StrictMode>` — all of it. react-x11 is a normal React 19
10
+ renderer, and where React's behaviour does not depend on the DOM, it is the
11
+ same behaviour.
12
+
13
+ Everything on this page is also something you can drive by hand — in the
14
+ apps that depend on it rather than in a panel of its own:
15
+
16
+ ```sh
17
+ npm run examples:monitor # priority: a filter over ~780 live rows
18
+ npm run examples:chat # <Activity>, useOptimistic, <Suspense>
19
+ npm run examples:timer # error boundaries, and where a crash is logged
20
+ ```
21
+
22
+ Those replaced an `examples/react-features.jsx` that had a panel per feature.
23
+ A panel proves the API exists; it cannot show you the choice, because nothing
24
+ in it costs anything. `monitor`'s field is expensive because a process list
25
+ is expensive, and that is the only condition under which deferring it is a
26
+ decision rather than a demonstration.
27
+
28
+ What differs is anything the DOM used to define for you:
29
+
30
+ - **Paint timing.** A commit does not put pixels on screen. Frames go out on
31
+ a clock, so "before paint" is a real, observable boundary.
32
+ - **Measuring.** There is no `getBoundingClientRect()` that forces layout.
33
+ Layout has already happened, or it has not.
34
+ - **Hiding.** `<Suspense>` and `<Activity>` hide a `<box>` cheaply, but
35
+ hiding a `<window>` genuinely unmaps it, and the window manager notices.
36
+ - **Portal containers.** There is no document to portal into.
37
+
38
+ ## At a glance
39
+
40
+ | | |
41
+ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
42
+ | `useState`, `useReducer`, `useContext`, `useMemo`, `useCallback`, `useRef` | unchanged |
43
+ | `useImperativeHandle`, `forwardRef`, `ref` as a prop, ref cleanup | unchanged |
44
+ | `use(promise)`, `use(context)`, `React.lazy` | unchanged |
45
+ | `useOptimistic`, `useActionState`, `<Profiler>`, custom hooks | unchanged |
46
+ | `useSyncExternalStore` | unchanged, and the best way to bridge non-React code |
47
+ | `useTransition`, `startTransition`, `useDeferredValue` | work, and really do stay responsive |
48
+ | `useLayoutEffect` vs `useEffect` | the difference is real — one paint versus two |
49
+ | `<Suspense>` | works; read [Suspense and Activity](#suspense-and-activity) for `<window>` |
50
+ | `<Activity>` | works; `mode="hidden"` around a `<window>` unmaps it |
51
+ | Error boundaries | work — but **where you put one** decides if the window survives |
52
+ | `<StrictMode>` | safe to use; effects are not double-invoked on the first mount |
53
+ | `useId` | works; there is very little here to use it for |
54
+ | `createPortal` | use [`<popup>`](elements.md) instead — see [Portals](#portals) |
55
+ | `flushSync` | not exported; you almost certainly do not need it |
56
+ | `useFormStatus` | not wired up |
57
+ | React Server Components | not available |
58
+ | anything imported from `react-dom` | not available — see [Not available](#not-available) |
59
+
60
+ ## Effects, and the paint boundary
61
+
62
+ Put an effect that **changes what the user sees** in `useLayoutEffect`. Put
63
+ everything else in `useEffect`. That is the same rule as the DOM, and here
64
+ it has a directly observable consequence:
65
+
66
+ | you call `setState` from | what the user sees |
67
+ | ------------------------ | ------------------------------------------------------- |
68
+ | `useLayoutEffect` | one frame, with the corrected value |
69
+ | `useEffect` | one frame with the old value, then a frame with the new |
70
+
71
+ So an adjustment made in `useEffect` flickers. This is not a timing race you
72
+ can get lucky with — updates from a layout effect are folded into the frame
73
+ being prepared, and updates from a passive effect are not.
74
+
75
+ That holds for a correction computed from React state. One computed from
76
+ **geometry** is a layout behind: a layout effect runs after the commit and
77
+ before the layout pass, so `abs` there is still the previous frame's rect
78
+ (zeros, on mount), and anything measured from it describes the frame
79
+ before. For a size, let the element report it with `onLayout`
80
+ ([below](#measuring-a-node)); for a style that depends on one, write a
81
+ container query ([styling.md](styling.md#container-queries)), which the
82
+ pass answers in the same frame. `scrollIntoView` measures nothing when you
83
+ call it, and neither does the part of a `scrollTo` past the extent the last
84
+ pass measured, which is what `scrollTo(Infinity)` asks for when it follows
85
+ rows this commit added. The pass answers both against the geometry it
86
+ produces, so they are safe here ([elements.md](elements.md#scrolling)).
87
+
88
+ The same boundary is why an event handler feels instant: react-x11 lands the
89
+ React update caused by a click or a keystroke **before** the frame goes out,
90
+ so the response and the default action paint together rather than a frame
91
+ apart.
92
+
93
+ ```jsx
94
+ // good — `tooTall` and `fits` are React state, so the corrected rows paint
95
+ // in the same frame as the state that asked for them
96
+ useLayoutEffect(() => {
97
+ if (tooTall) setRows(fits);
98
+ }, [tooTall, fits]);
99
+ ```
100
+
101
+ ## Measuring a node
102
+
103
+ **There is no forced synchronous layout.** Nothing you can call will make
104
+ layout run so you can read the result. This is the DOM habit most likely to
105
+ break on the way over, because in a browser `getBoundingClientRect()` both
106
+ reads _and_ triggers.
107
+
108
+ A node's `abs` rect (and `getClientRects()`) reports what the **last** layout
109
+ produced. On the very first render of a node, that is nothing yet.
110
+
111
+ What to do instead:
112
+
113
+ - **Let the element tell you.** `onLayout` fires on any element after a
114
+ layout pass moved or resized it, with `width`/`height` (and `x`/`y`
115
+ within the parent) in logical pixels — once after the first layout, then
116
+ on change. `<box onViewport>` adds a scroller's content size, and
117
+ `<window onResize>` fires when the window is resized. These are the
118
+ supported way to react to a size.
119
+ - **Read `abs` from a ref in a handler or a later effect**, not during the
120
+ render that created the node.
121
+ - **Take a getter, not a value.** `useWindowId(ref)` and `useAnchor()` return
122
+ getters for exactly this reason — a value captured during render would be
123
+ `null` on the render where you needed it.
124
+
125
+ ```jsx
126
+ const [cols, setCols] = useState(1);
127
+
128
+ <box onLayout={({ width }) => setCols(Math.max(1, (width / 200) | 0))}>
129
+ {items.map(…)}
130
+ </box>;
131
+ ```
132
+
133
+ When what depends on the size is a _style_ rather than a decision about what
134
+ to render, write it as a container query block instead
135
+ ([styling.md](styling.md#container-queries)): it applies in the same frame,
136
+ where a state set from `onLayout` re-renders a frame later.
137
+
138
+ If you are porting a component that measures itself and adjusts, this is the
139
+ part to rewrite. See [elements.md](elements.md) for `onLayout`, `onViewport`
140
+ and `onResize`, and [components.md](components.md) for `useAnchor`.
141
+
142
+ ## What a ref gives you
143
+
144
+ Two different kinds of object, depending on the element:
145
+
146
+ - **`<window>` and `<popup>`** hand back the live **window object** of
147
+ whichever backend is drawing — on X11 the ntk window, which is what
148
+ [`windowIdOf()`](elements.md) resolves to an XID. Both spell
149
+ `getContext('2d')` and `requestAnimationFrame` the same way; anything
150
+ past that is the backend's own surface, so a component that reaches
151
+ further should say which backend it is for.
152
+ - **every other element** hands back its **node**: `focus()`, `blur()`,
153
+ `focused`, `hitTest()`, `containsPoint()`, `getClientRects()`, and `abs`
154
+ for its position and size. `<textinput>`/`<textarea>` additionally match
155
+ the DOM closely enough that libraries like react-hook-form drive them
156
+ through a ref.
157
+
158
+ A window ref is `null` until that window exists, and refs attach after the
159
+ commit — so read them in effects and handlers, never during render.
160
+
161
+ For `<canvas>`, note that the **node is not a drawing surface**. There is no
162
+ `getContext()` on it; you draw in `onDraw`, which the renderer calls with a
163
+ context whenever the canvas needs repainting:
164
+
165
+ ```jsx
166
+ <canvas onDraw={(ctx, { width, height }) => { …}} />
167
+ ```
168
+
169
+ To make it repaint, change something React can see — a prop, state, or the
170
+ `cacheKey`. That is the supported path, and it is usually what you want.
171
+ There is an imperative escape hatch for element authors in
172
+ [extending.md](extending.md), but note that it lives on the **owning
173
+ window**, not on the drawn node, despite what the bundled types currently
174
+ say (see [Known gaps](#known-gaps)).
175
+
176
+ ## Transitions, and what stays responsive
177
+
178
+ `useTransition`, `startTransition` and `useDeferredValue` all work, and they
179
+ really do keep the UI responsive — React will interrupt a transition render
180
+ to handle an incoming click or keystroke.
181
+
182
+ What is worth knowing is that **transitions are the mechanism that does
183
+ this**. An ordinary `setState` — from a handler, a timer, a socket callback —
184
+ renders in one uninterruptible pass, however much work it is. If a render is
185
+ big enough to drop a frame, wrapping it in `startTransition` (or deferring
186
+ its input with `useDeferredValue`) is not a micro-optimisation, it is the
187
+ difference between a responsive window and a frozen one.
188
+
189
+ ```jsx
190
+ const [pending, startTransition] = useTransition();
191
+
192
+ // typing stays instant; the expensive list catches up
193
+ const onChange = (e) => {
194
+ setQuery(e.value);
195
+ startTransition(() => setResults(search(e.value)));
196
+ };
197
+ ```
198
+
199
+ Two smaller notes:
200
+
201
+ - `root.render()` is **synchronous** — the tree is committed before the call
202
+ returns. Wrapping it in `startTransition` does nothing.
203
+ - A `startTransition` inside a click handler is still a transition. It does
204
+ not inherit the handler's urgency.
205
+
206
+ react-x11 already classifies X11 input for you: presses, releases, keys,
207
+ focus changes, window close and drops are urgent; pointer motion and
208
+ drag-over are not, which is what keeps a motion burst from flooding the
209
+ renderer. You do not have to do anything to get this.
210
+
211
+ [`examples/monitor.jsx`](../examples/monitor.jsx) is this paragraph with a
212
+ keyboard on it: a filter over hundreds of live rows, where one keystroke
213
+ changes every row on screen. Without the deferral the caret waits for the
214
+ table; with it the field stays under the fingers and the rows arrive a beat
215
+ later. Two things there are worth copying, and both are one-line mistakes in
216
+ the other direction: the expensive subtree is
217
+ `memo`ised (or the urgent render rebuilds it anyway, and deferring buys
218
+ nothing), and no prop that changes on every update — not even a "stale"
219
+ border — is passed into it.
220
+
221
+ ### Bridging non-React code
222
+
223
+ `useSyncExternalStore` is the right tool for anything that lives outside
224
+ React — a timer, a D-Bus signal, a socket, an ntk event. It is also faster
225
+ here than a hand-rolled `setState` bridge: a store notification is applied
226
+ urgently and lands in the next microtask, where the equivalent `setState`
227
+ waits a turn longer.
228
+
229
+ Every mainstream store (zustand, jotai, valtio, redux, XState) works with no
230
+ adapter for this reason. The [window-manager
231
+ example](../examples/wm.jsx) is the worked case: the WM core is a plain
232
+ store, and the UI subscribes to it.
233
+
234
+ ## Suspense and Activity
235
+
236
+ `<Suspense>` works. The one thing to plan for is layout: a hidden subtree
237
+ gives up its space entirely, so the fallback lays out as if the real content
238
+ were not there. **Size your fallback deliberately**, or the window collapses
239
+ to the fallback and jumps back on reveal.
240
+
241
+ ```jsx
242
+ <Suspense fallback={<box style={{ height: 240 }} />}>
243
+ <Report />
244
+ </Suspense>
245
+ ```
246
+
247
+ **Focus goes with the hidden tree and comes back with it.** A field inside
248
+ the boundary gives up the keyboard when the fallback appears — nothing
249
+ invisible collects keystrokes — and gets it back on the reveal unless
250
+ something else took focus meanwhile, so a boundary that re-suspends
251
+ mid-edit does not drop the user out of what they were typing. See
252
+ [events.md](events.md#focus-and-visibility) for the rules and the way out.
253
+
254
+ **Scroll positions come back too.** What a hidden boundary holds is not
255
+ placed while it is hidden — the same as under `display: 'none'` — so a scroll
256
+ pane in it is not clamped to the start of content it no longer lays out,
257
+ reports no `onViewport` or `onScroll` until it is shown, and is shown at the
258
+ offset it was left at. A tab kept mounted under `<Activity>` reopens on the
259
+ row the user was reading.
260
+
261
+ If a `<window>` is inside the boundary, hiding it **unmaps the real window**.
262
+ The window manager treats the reveal as a new window: it may re-place it,
263
+ restack it, or apply its own geometry, and anything the user did to it can be
264
+ lost. Prefer suspending _inside_ a window over suspending the window itself.
265
+
266
+ `React.lazy` works. If you ship a single-file bundle, note that dynamic
267
+ `import()` gets inlined — you keep lazy _evaluation_ but get no code
268
+ splitting; see [packaging.md](packaging.md).
269
+
270
+ `<Activity>` works, and `mode="hidden"` around a toplevel `<window>` reads
271
+ the same way `<Suspense>` does: the window is unmapped, and one mounted
272
+ hidden is never mapped at all — it is created, laid out and kept, but the
273
+ window manager only ever hears about it on the reveal. The same caveat
274
+ applies: what the user did to the window before it was hidden is the window
275
+ manager's to remember, and it may not.
276
+
277
+ **Testing Suspense**: a promise that settles outside `act()` is not always
278
+ picked up by an `await act()`, because React throttles the commit when a
279
+ fallback was shown very recently. Use `waitFor` from `react-x11/test` —
280
+ see [testing.md](testing.md).
281
+
282
+ ## Portals
283
+
284
+ **Use `<popup>`.** It is what you want in almost every case where the DOM
285
+ would reach for `createPortal`: a `<popup>` is written as an ordinary child
286
+ in your JSX — so it sees context, state and props exactly where you wrote it
287
+ — but it is a real top-level X window, so it escapes its parent's clipping
288
+ and can extend past the window edge.
289
+
290
+ ```jsx
291
+ <box>
292
+ <Button onClick={(e) => setAt({ x: e.screenX, y: e.screenY })}>
293
+ Options…
294
+ </Button>
295
+ {at && (
296
+ <popup
297
+ x={at.x}
298
+ y={at.y}
299
+ width={180}
300
+ height={120}
301
+ grab
302
+ onDismiss={() => setAt(null)}
303
+ >
304
+ <Menu />
305
+ </popup>
306
+ )}
307
+ </box>
308
+ ```
309
+
310
+ Its position in the JSX has no effect on where it appears — you place it in
311
+ screen coordinates, and `useAnchor()` does that math for you when you are
312
+ anchoring to another node.
313
+
314
+ `Dialog`, `Tooltip`, `ContextMenu`, `MenuBar` and `Select` are all built this
315
+ way — see [components.md](components.md).
316
+
317
+ There is no `createPortal` export. The reconciler's own is reachable through
318
+ the `Renderer` escape hatch, but it is not a supported surface: the container
319
+ has to be an X connection or an X window rather than a node, and only
320
+ `<window>`/`<popup>` can be the portal's immediate child. If you find
321
+ yourself wanting it, you almost certainly want a second `<window>` or a
322
+ `<popup>`.
323
+
324
+ ## Where to put an error boundary
325
+
326
+ Boundaries work exactly as documented. The X11-specific decision is
327
+ **placement**, and it decides whether the user's window survives:
328
+
329
+ ```jsx
330
+ // good — the window and everything the WM knows about it survives
331
+ <window title="Editor">
332
+ <ErrorBoundary fallback={<text>Something broke</text>}>
333
+ <Document />
334
+ </ErrorBoundary>
335
+ </window>
336
+
337
+ // the window itself is destroyed and recreated on a throw
338
+ <ErrorBoundary fallback={<window title="Editor">…</window>}>
339
+ <window title="Editor">
340
+ <Document />
341
+ </window>
342
+ </ErrorBoundary>
343
+ ```
344
+
345
+ A recreated window is a **new** window: its position, size, stacking,
346
+ maximized state and anything else the user or the window manager did to it
347
+ are gone. Put the boundary inside the window unless you genuinely want the
348
+ window replaced.
349
+
350
+ An **uncaught** error unmounts the whole tree, which here means every window
351
+ disappears while the process stays alive. `createRoot` takes
352
+ `onUncaughtError`, `onCaughtError` and `onRecoverableError` so you can decide
353
+ what that should mean for your app — the default logs and sets a failing exit
354
+ code. See [events.md](events.md).
355
+
356
+ One thing React does not cover: **a throw inside an event handler never
357
+ reaches a boundary.** (That is true in React DOM too.) react-x11 routes those
358
+ to `onUncaughtError` instead, so they are at least reportable rather than
359
+ silent.
360
+
361
+ ## Smaller notes
362
+
363
+ **`useId`** works, but there is little use for it: no element takes an `id`,
364
+ there is no hydration, and labels are associated structurally (`<Checkbox
365
+ label>`, `<Radio>children</Radio>`). If you want a window's real identity,
366
+ that is `useWindowId()`. Avoid putting an `id` prop on `<window>` — unknown
367
+ props there are forwarded to the underlying window as creation attributes.
368
+
369
+ **`<StrictMode>`** is safe. A discarded render costs no X11 traffic, so
370
+ double-rendering never produces double windows. Be aware that effects are
371
+ **not** double-invoked on the initial mount, so it will not catch a missing
372
+ cleanup at mount the way it does in React DOM.
373
+
374
+ **`memo` / `useMemo`** save CPU, not bandwidth. Rebuilding a `style` object
375
+ or array every render is free — styles are compared by value, so an
376
+ identical style produces no layout work, no repaint and no frame. Passing
377
+ `style={[base, active && on]}` inline is the documented idiom, not a leak.
378
+ See [styling.md](styling.md).
379
+
380
+ **Keys** matter more for `<window>` children than for drawn ones. Reordering
381
+ `<box>` children is cheap. Reordering sibling `<window>`s restacks real
382
+ windows, and a missing key destroys and recreates them — with the same loss
383
+ of window-manager state described above. To keep a window on top, use
384
+ `alwaysOnTop` rather than ordering.
385
+
386
+ **DevTools** works — set `REACT_X11_DEVTOOLS=1` and the component tree,
387
+ props and highlight-on-hover all behave normally, with the highlight drawn
388
+ into the real window. The Profiler **Timeline** tab is unavailable; the
389
+ commit list and flamegraph work. See [devtools.md](devtools.md).
390
+
391
+ **Fast Refresh** works without a bundler: `npx react-x11 dev app.jsx` runs
392
+ a file with its default export mounted and every save applied in place, over
393
+ the `react-x11/refresh` entry point, which a tool that hosts the loader
394
+ itself registers with `--import`. Keep anything
395
+ whose identity must survive a reload — contexts, stores — in a module you
396
+ are not editing. See [dev tooling](ecosystem/dev-tooling.md#react-refresh)
397
+ and the [hot-reload example](../examples/tasks.jsx), which is
398
+ `npm run examples:tasks:hot`.
399
+
400
+ ## Not available
401
+
402
+ **`react-dom`.** It is not a dependency, so `ReactDOM.createPortal`,
403
+ `flushSync`, `findDOMNode`, `hydrateRoot` and `unstable_batchedUpdates` are
404
+ not part of react-x11.
405
+
406
+ The trap is not the import error — it is that `react-dom` is a required peer
407
+ of some libraries, so it can end up installed anyway. Then the import
408
+ resolves, and `flushSync()` runs your callback **without flushing anything**,
409
+ because it is talking to a renderer you are not using. If a library depends
410
+ on synchronous flushing, check `npm ls react-dom`. The
411
+ [ecosystem register](ecosystem.md) records which packages hit this.
412
+
413
+ **Server Components** are not available: there is no Flight client wired up.
414
+
415
+ **`useFormStatus`** is not wired up. `useOptimistic` and `useActionState` are
416
+ unaffected and work normally.
417
+
418
+ ## Known gaps
419
+
420
+ Open bugs where a React feature does not yet mean here what it should:
421
+
422
+ - A `<popup>` written inside a subtree that `<Suspense>` or `<Activity>`
423
+ hides stays on screen: React hides the topmost host instance of the branch
424
+ and the popup is its own X window below it, so nothing unmaps it. Focus
425
+ follows what is actually visible, so the popup keeps the keyboard — but
426
+ the popup itself has to be conditional in your JSX rather than left to the
427
+ boundary.
428
+ - The bundled types declare `invalidate()` on every node; at runtime only the
429
+ owning `<window>` has it. Reach it as `ref.current.root` — or better, drive
430
+ repaints through props and `cacheKey`.