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,394 @@
1
+ # Animation
2
+
3
+ react-x11 has a built-in `transition` style property with a fixed easing
4
+ curve — see [styling](../styling.md#transitions). Everything on this page
5
+ exists to cover what that cannot express: springs with velocity carry-over,
6
+ interruption, arbitrary easing curves, shape morphing, and keeping an element
7
+ mounted while it animates out.
8
+
9
+ There is one constraint every entry shares: **there is no `opacity` style
10
+ property**, and `validateStyle` throws on unknown properties. The classic
11
+ fade does not port literally; animate a colour, a size or a position instead.
12
+
13
+ Two patterns recur. The cheap one is `onUpdate → setState → style prop`:
14
+ every frame is a React render plus a commit, which react-x11 turns into a
15
+ partial repaint of the node whose paint changed. Keep the animated component
16
+ a leaf — a `<box>`, not the window root — and it is fine at spring frame
17
+ rates. The expensive-to-set-up, cheap-to-run one is the
18
+ [react-spring host](#react-spring), which applies frames imperatively and
19
+ bypasses React entirely.
20
+
21
+ Headless Node has no `requestAnimationFrame`. In a real app the right clock
22
+ is the ntk window's `requestAnimationFrame`, reachable through a ref on
23
+ `<window>`, which paces frames to X connection latency; a 16 ms
24
+ `setInterval` works everywhere else.
25
+
26
+ `framer-motion` is not on this page. Both its component layer and its bare
27
+ value layer fail — see the
28
+ [negative-results register](../ecosystem.md#render-time), which also explains
29
+ why reaching past `motion.*` does not help.
30
+
31
+ ## popmotion {#popmotion}
32
+
33
+ **Out of the box.** popmotion@11.0.5.
34
+
35
+ Framer's low-level animation toolkit: `animate()` tweens or springs a plain
36
+ JS value and calls `onUpdate(v)` every frame. No components, no DOM — the
37
+ value goes wherever you point it. This is the smallest way to get real
38
+ springs, with velocity and overshoot, without adopting the react-spring host.
39
+
40
+ ```jsx
41
+ import React, { useEffect, useState } from 'react';
42
+ import { animate } from 'popmotion';
43
+
44
+ function Bar() {
45
+ const [w, setW] = useState(20);
46
+ useEffect(() => {
47
+ const anim = animate({
48
+ from: 20,
49
+ to: 200,
50
+ stiffness: 300,
51
+ damping: 20,
52
+ onUpdate: setW,
53
+ });
54
+ return () => anim.stop();
55
+ }, []);
56
+ return <box style={{ width: w, height: 30, backgroundColor: '#3366ff' }} />;
57
+ }
58
+ ```
59
+
60
+ popmotion's `framesync` detects the missing `requestAnimationFrame` and falls
61
+ back to a 16.7 ms `setTimeout`, so it works headless with no configuration.
62
+ For latency-adaptive pacing, `animate()` accepts a custom `driver` you can
63
+ back with the ntk window's clock.
64
+
65
+ - Springs on layout properties (width, margin) cost a yoga layout pass per
66
+ frame — the same trade react-x11's own layout transitions make.
67
+ - `animate` also interpolates colour strings (`'#f00' → '#00f'`), and the
68
+ `rgba(...)` strings it emits are accepted by the paint path.
69
+ - popmotion's `styler` and DOM helpers are irrelevant here. Use only the
70
+ value APIs: `animate`, `spring`, `decay`, `mix`.
71
+
72
+ ## d3-ease {#d3-ease}
73
+
74
+ **Out of the box.** d3-ease@3.0.1.
75
+
76
+ Pure easing functions: `ease(t)` maps normalized time to normalized progress,
77
+ and nothing else. Thirty-plus curves — cubic, elastic, bounce, back — each a
78
+ few lines of maths with no environment dependencies. This is the zero-cost
79
+ way to get the curves the built-in `transition` does not have.
80
+
81
+ Compute `t` from wall time, pass `ease(t)` to an interpolator, push the
82
+ result into a style prop.
83
+
84
+ ```jsx
85
+ import React, { useEffect, useState } from 'react';
86
+ import { easeCubicOut } from 'd3-ease';
87
+ import { interpolateNumber } from 'd3-interpolate';
88
+
89
+ function useTimeline(duration, ease, onFrame) {
90
+ useEffect(() => {
91
+ const start = Date.now();
92
+ const timer = setInterval(() => {
93
+ const t = Math.min(1, (Date.now() - start) / duration);
94
+ onFrame(ease(t));
95
+ if (t >= 1) clearInterval(timer);
96
+ }, 16);
97
+ return () => clearInterval(timer);
98
+ }, []);
99
+ }
100
+
101
+ const width = interpolateNumber(10, 200);
102
+
103
+ function Gauge() {
104
+ const [f, setF] = useState(0);
105
+ useTimeline(250, easeCubicOut, setF);
106
+ return (
107
+ <box style={{ width: width(f), height: 24, backgroundColor: '#204080' }} />
108
+ );
109
+ }
110
+ ```
111
+
112
+ - ESM-only since v3 — `require()` fails, `import` works. react-x11 apps are
113
+ ESM anyway.
114
+ - It is only the curve; you own the clock and the interpolation.
115
+ - Overshooting curves (elastic, back) produce values outside the from/to
116
+ range. That is the point of them, but clamp before feeding anything
117
+ react-x11 validates as non-negative.
118
+
119
+ ## d3-interpolate {#d3-interpolate}
120
+
121
+ **Out of the box.** d3-interpolate@3.0.1.
122
+
123
+ `interpolate(a, b)` returns `t => value` for numbers, colours in any CSS
124
+ space, strings with embedded numbers, dates, arrays and nested objects. Pure
125
+ maths, no DOM.
126
+
127
+ Worth knowing: `interpolateRgb` emits `'rgb(r, g, b)'` strings and the paint
128
+ path takes them as-is, so colour animation needs no conversion. react-x11 has
129
+ an internal interpolator for its own `transition` styles, but it is not
130
+ exported and handles only what transitions need — d3-interpolate is the
131
+ general tool for hand-rolled timelines.
132
+
133
+ ```jsx
134
+ import { easeCubicOut } from 'd3-ease';
135
+ import { interpolateNumber, interpolateRgb } from 'd3-interpolate';
136
+
137
+ const width = interpolateNumber(10, 200);
138
+ const color = interpolateRgb('#204080', '#e04040');
139
+
140
+ function Gauge() {
141
+ const [f, setF] = useState(0);
142
+ useTimeline(250, easeCubicOut, setF); // see d3-ease above
143
+ return (
144
+ <box style={{ width: width(f), height: 24, backgroundColor: color(f) }} />
145
+ );
146
+ }
147
+ ```
148
+
149
+ - ESM-only since v3.
150
+ - Generic `interpolate(a, b)` on objects interpolates _every_ numeric-looking
151
+ key, so interpolating a whole style object will happily produce fractional
152
+ values for properties that want integers. Interpolate the two or three
153
+ properties you mean.
154
+ - There is no `opacity`; to fade, interpolate a colour's alpha into
155
+ `backgroundColor: 'rgba(...)'`.
156
+ - `interpolateArray` over polygon points feeding `<canvas onDraw>` is a good
157
+ use of the general form.
158
+
159
+ ## `@tweenjs/tween.js` {#tweenjs}
160
+
161
+ **Out of the box.** @tweenjs/tween.js@25.0.0.
162
+
163
+ `new Tween(obj).to(target, ms).easing(...).onUpdate(...).start()`, and then
164
+ _you_ call `tween.update(time)` on whatever clock you own. Since v18 there is
165
+ no implicit global loop — nothing runs until you pump it.
166
+
167
+ The explicit clock is the feature here: in a real app, pump from the ntk
168
+ window's `requestAnimationFrame`, which paces updates to X connection
169
+ latency. Use one `Group` per window if you run many tweens, so
170
+ `group.update(now)` steps them all from the same tick.
171
+
172
+ ```jsx
173
+ import React, { useEffect, useState } from 'react';
174
+ import { Tween, Easing } from '@tweenjs/tween.js';
175
+
176
+ function Panel() {
177
+ const [pos, setPos] = useState({ x: 0, h: 10 });
178
+ useEffect(() => {
179
+ const state = { x: 0, h: 10 };
180
+ const tween = new Tween(state)
181
+ .to({ x: 150, h: 60 }, 300)
182
+ .easing(Easing.Cubic.Out)
183
+ .onUpdate(() => setPos({ ...state }))
184
+ .start();
185
+ const timer = setInterval(() => {
186
+ if (!tween.update()) clearInterval(timer); // update() is false when done
187
+ }, 16);
188
+ return () => clearInterval(timer);
189
+ }, []);
190
+ return (
191
+ <box
192
+ style={{
193
+ marginLeft: pos.x,
194
+ width: 40,
195
+ height: pos.h,
196
+ backgroundColor: '#00aa55',
197
+ }}
198
+ />
199
+ );
200
+ }
201
+ ```
202
+
203
+ - Tweens numbers only, including numbers nested in objects and arrays.
204
+ Colours need pre-decomposition into channels; use d3-interpolate or
205
+ popmotion instead.
206
+ - Nothing animates until something calls `update()`. Forgetting the pump is
207
+ the classic first bug.
208
+ - `onUpdate` mutates the source object, so spread it into fresh state
209
+ (`setPos({ ...state })`) or React skips the re-render on identity.
210
+
211
+ ## flubber {#flubber}
212
+
213
+ **Out of the box.** flubber@0.4.2.
214
+
215
+ Shape-morphing interpolators for SVG paths: `interpolate(from, to)` returns
216
+ `t => d-string`, handling ring alignment, point insertion and topology
217
+ changes so intermediate frames look sensible instead of self-intersecting.
218
+ Shape morphing is the one animation the `<svg>` host makes easy, and flubber
219
+ is the standard tool for it.
220
+
221
+ ```jsx
222
+ import React, { useEffect, useState } from 'react';
223
+ import flubber from 'flubber'; // CJS: default import, no named ESM exports
224
+ const { interpolate } = flubber;
225
+
226
+ const morph = interpolate(
227
+ 'M50,10 L90,90 L10,90 Z',
228
+ 'M10,10 L90,10 L90,90 L10,90 Z',
229
+ );
230
+ const svgFor = (d) =>
231
+ `<svg viewBox="0 0 100 100"><path d="${d}" fill="#cc3355"/></svg>`;
232
+
233
+ function Morph() {
234
+ const [t, setT] = useState(0);
235
+ useEffect(() => {
236
+ const start = Date.now();
237
+ const timer = setInterval(() => {
238
+ const p = Math.min(1, (Date.now() - start) / 200);
239
+ setT(p);
240
+ if (p >= 1) clearInterval(timer);
241
+ }, 16);
242
+ return () => clearInterval(timer);
243
+ }, []);
244
+ return <svg source={svgFor(morph(t))} style={{ width: 100, height: 100 }} />;
245
+ }
246
+ ```
247
+
248
+ - `import { interpolate } from 'flubber'` throws — it is a CJS package, so
249
+ use the default-import form above.
250
+ - Build the interpolator once (module scope or `useMemo`). Construction does
251
+ the expensive ring matching; sampling is cheap.
252
+ - Every frame is a full SVG parse and rasterize of the `source` string. Fine
253
+ for icons and small figures; for large scenes prefer `<canvas onDraw>` and
254
+ draw the interpolated points yourself.
255
+ - Old but stable (0.4.2 since 2017) — finished rather than unmaintained.
256
+
257
+ ## react-transition-group {#react-transition-group}
258
+
259
+ **Out of the box — with `nodeRef`.** react-transition-group@4.4.5.
260
+
261
+ A transition _state machine_, not an animator: `<Transition in={bool}
262
+ timeout={ms}>` walks its child through `entering`/`entered`/`exiting`/
263
+ `exited` on a timer and keeps the child mounted during exit. You decide what
264
+ each state looks like. It is the standard way to keep an element mounted
265
+ while it animates out, which react-x11 has no built-in answer for.
266
+
267
+ Two rules. Always pass `nodeRef` — without it, v4 falls back to
268
+ `ReactDOM.findDOMNode`, which would receive a react-x11 node it cannot
269
+ understand. And map states to styles yourself, pairing with the built-in
270
+ `transition` style so the state flip _tweens_ instead of snapping. That
271
+ combination is the whole "CSSTransition" story here, since there are no class
272
+ names.
273
+
274
+ ```jsx
275
+ import React, { useRef } from 'react';
276
+ import { Transition } from 'react-transition-group';
277
+
278
+ function Panel({ shown }) {
279
+ const nodeRef = useRef(null);
280
+ return (
281
+ <Transition
282
+ nodeRef={nodeRef}
283
+ in={shown}
284
+ timeout={150}
285
+ mountOnEnter
286
+ unmountOnExit
287
+ >
288
+ {(state) => (
289
+ <box
290
+ ref={nodeRef}
291
+ style={{
292
+ transition: 150, // let react-x11 tween the flip
293
+ width: 100,
294
+ height: 50,
295
+ backgroundColor: state === 'entered' ? '#ff0000' : '#0000ff',
296
+ }}
297
+ />
298
+ )}
299
+ </Transition>
300
+ );
301
+ }
302
+ ```
303
+
304
+ - **`react-dom` must be present in `node_modules`.** `cjs/Transition.js`
305
+ requires it at module scope for the `findDOMNode` fallback, even when
306
+ `nodeRef` means it is never called. It is a peer dependency: install it, it
307
+ will not be rendered with.
308
+ - `CSSTransition` is pointless here — it toggles class names, and there are
309
+ none. Use plain `Transition`.
310
+ - `timeout` and your visual transition duration are independent numbers. Keep
311
+ them equal, or the state flips mid-tween.
312
+ - `addEndListener` works; call the provided `done` yourself.
313
+
314
+ ## `@react-spring/core` {#react-spring}
315
+
316
+ **Adapter, ~20 lines.** @react-spring/core@10.1.2,
317
+ @react-spring/animated@10.1.2.
318
+
319
+ The platform-independent half of react-spring: `useSpring`, `useTransition`,
320
+ `useTrail`, `SpringValue`, `Controller`. `@react-spring/animated` provides
321
+ `createHost`, the documented seam for building a non-DOM target — it is how
322
+ `@react-spring/native` and `/three` exist.
323
+
324
+ Springs, interruption with velocity carry-over, and `api.start()` are exactly
325
+ what the built-in duration-only `transition` cannot express. Two public
326
+ seams do the work:
327
+
328
+ - `Globals.assign({ now, requestAnimationFrame })` points react-spring's
329
+ frame loop at a clock.
330
+ - `createHost(['box', …], { applyAnimatedValues })` produces `animated.box`.
331
+ Every frame, react-spring calls `applyAnimatedValues(instance, props)`
332
+ where `instance` is the react-x11 node your ref points at — and
333
+ `node.applyProps(next, prev)` is the same imperative path the reconciler's
334
+ own `commitUpdate` uses. Frames bypass React entirely: a component renders
335
+ once while hundreds of frames apply.
336
+
337
+ ```jsx
338
+ // spring-x11.js — the adapter
339
+ import { Globals } from '@react-spring/core';
340
+ import { createHost } from '@react-spring/animated';
341
+
342
+ Globals.assign({
343
+ now: Date.now,
344
+ // real app: cb => ntkWindow.requestAnimationFrame(cb) (ref on <window>)
345
+ requestAnimationFrame: (cb) => setTimeout(() => cb(Date.now()), 16),
346
+ });
347
+
348
+ const host = createHost(['box', 'text', 'window'], {
349
+ applyAnimatedValues(instance, props) {
350
+ if (!instance || typeof instance.applyProps !== 'function') return false;
351
+ const prev = instance.props;
352
+ const next = { ...prev, ...props };
353
+ // frames carry only animated leaves — keep the static style keys
354
+ if (props.style) next.style = { ...prev.style, ...props.style };
355
+ instance.applyProps(next, prev);
356
+ },
357
+ });
358
+
359
+ export const animated = host.animated;
360
+ ```
361
+
362
+ ```jsx
363
+ import React, { useEffect } from 'react';
364
+ import { useSpring } from '@react-spring/core';
365
+ import { animated } from './spring-x11.js';
366
+
367
+ function Bar() {
368
+ const [style, api] = useSpring(() => ({
369
+ from: { width: 20, backgroundColor: '#204080' },
370
+ }));
371
+ useEffect(() => {
372
+ api.start({ width: 180, backgroundColor: '#e04040' });
373
+ }, []);
374
+ return <animated.box style={{ ...style, height: 30 }} />;
375
+ }
376
+ ```
377
+
378
+ - **Merge `style` one level deep in `applyAnimatedValues`** — this is the
379
+ number-one silent failure. On animation frames react-spring calls
380
+ `props.getValue(true)` for _animated leaves only_, so static keys
381
+ (`height: 30` next to an animated `width`) are absent from the payload and
382
+ a naive `{...prev, ...props}` merge wipes them. The box then paints
383
+ nothing, with no error.
384
+ - Hex, `rgb()` and `rgba()` colour strings interpolate out of the box. Named
385
+ CSS colours are not enabled by default in `@react-spring/shared` — stick to
386
+ hex and rgb.
387
+ - Do not use `useScroll`, `useResize`, `useInView` or `useReducedMotion` from
388
+ core; they observe the DOM `window` and `matchMedia`.
389
+ `Globals.assign({ skipAnimation })` is the manual reduced-motion switch.
390
+ - Style arrays (`style={[a, b]}`) defeat the shallow merge. Flatten first —
391
+ react-x11 exports `flattenStyle`.
392
+ - Driving the clock from `setImmediate` runs frames as fast as the event loop
393
+ turns, which is fine for tests and wasteful for apps. Use the ntk window
394
+ clock or a 16 ms timeout.
@@ -0,0 +1,201 @@
1
+ # Data fetching
2
+
3
+ A react-x11 app is an ordinary Node process, so `fetch`, undici, `ky` and
4
+ `graphql-request` all work unmodified. What needs care is the _cache_ layer
5
+ on top, because every stale-while-revalidate library decides whether it is
6
+ "on a server" by looking for a global `window` — and in a react-x11 process
7
+ there isn't one.
8
+
9
+ That check is the whole story on this page. Get it wrong and nothing throws;
10
+ the app just stops refetching.
11
+
12
+ ## TanStack Query {#tanstack-query}
13
+
14
+ **Adapter, ~12 lines.** @tanstack/react-query@5.101.4 (query-core 5.101.4).
15
+
16
+ Query and mutation cache for server state: `useQuery` deduplicates, caches,
17
+ retries and background-refetches; `useMutation` handles writes with
18
+ invalidation. The core is renderer-agnostic, and the environment coupling
19
+ lives in three small managers — `focusManager`, `onlineManager`,
20
+ `environmentManager` — all replaceable.
21
+
22
+ The trap is query-core's environment probe:
23
+
24
+ ```js
25
+ // @tanstack/query-core src/utils.ts
26
+ export const isServer = typeof window === 'undefined' || 'Deno' in globalThis;
27
+ ```
28
+
29
+ A react-x11 process has no `window`, so query-core decides it is doing SSR.
30
+ Concretely:
31
+
32
+ - `retry` defaults to **0** instead of 3;
33
+ - stale timers are never scheduled and `refetchInterval` is disabled;
34
+ - `gcTime` defaults to **Infinity** instead of 5 minutes — the cache never
35
+ collects;
36
+ - the default `focusManager` setup wants
37
+ `window.addEventListener('visibilitychange')`, so focus refetching is
38
+ inert.
39
+
40
+ The escape hatch shipped in v5: `environmentManager.setIsServer()`,
41
+ re-exported from `@tanstack/react-query`. Pair it with react-x11's window
42
+ focus events, which fire on the X `FocusIn`/`FocusOut` the window manager
43
+ delivers.
44
+
45
+ ```jsx
46
+ // query-x11.js — the whole adapter
47
+ import {
48
+ environmentManager,
49
+ focusManager,
50
+ onlineManager,
51
+ } from '@tanstack/react-query';
52
+
53
+ environmentManager.setIsServer(() => false); // we are a client, not SSR
54
+ focusManager.setEventListener(() => () => {}); // drop the visibilitychange probe
55
+ onlineManager.setEventListener(() => () => {}); // assume online (or probe for real)
56
+
57
+ export const windowFocusProps = {
58
+ onFocus: () => focusManager.setFocused(true),
59
+ onBlur: () => focusManager.setFocused(false),
60
+ };
61
+ ```
62
+
63
+ ```jsx
64
+ import React from 'react';
65
+ import { createRoot } from 'react-x11';
66
+ import {
67
+ QueryClient,
68
+ QueryClientProvider,
69
+ useQuery,
70
+ } from '@tanstack/react-query';
71
+ import { windowFocusProps } from './query-x11.js';
72
+
73
+ const client = new QueryClient();
74
+
75
+ function Weather() {
76
+ const { data, status } = useQuery({
77
+ queryKey: ['weather'],
78
+ queryFn: () => fetch('https://wttr.in/?format=3').then((r) => r.text()),
79
+ });
80
+ return <text>{status === 'pending' ? 'loading…' : data}</text>;
81
+ }
82
+
83
+ (await createRoot()).render(
84
+ <window width={360} height={120} title="weather" {...windowFocusProps}>
85
+ <QueryClientProvider client={client}>
86
+ <Weather />
87
+ </QueryClientProvider>
88
+ </window>,
89
+ );
90
+ ```
91
+
92
+ Focusing the window after being away now refetches stale queries, exactly
93
+ like refocusing a browser tab.
94
+
95
+ - **Call `setIsServer` before creating the `QueryClient`** — the managers
96
+ are module singletons.
97
+ - With several app windows in one process, switching between two of your own
98
+ windows fires blur-then-focus, which counts as a focus event and refetches
99
+ stale queries. Same semantics as switching browser tabs; raise `staleTime`
100
+ if that is too eager.
101
+ - `onlineManager` defaults to online under Node (there is no
102
+ `navigator.onLine`). Wire it to NetworkManager over D-Bus if you want real
103
+ offline handling.
104
+ - `@tanstack/react-query-devtools` is DOM-rendered and not usable.
105
+
106
+ ## SWR {#swr}
107
+
108
+ **Adapter, 1 line — but read the dead end first.** swr@2.4.2.
109
+
110
+ Basic operation works unmodified: `useSWR` fetches on mount, caches, dedupes
111
+ and re-renders through `useSyncExternalStore`.
112
+
113
+ Focus and reconnect revalidation do not, and they fail silently. SWR
114
+ computes its environment once at import time, with no override:
115
+
116
+ ```js
117
+ // swr/dist/_internal/config-context-*.mjs
118
+ const isWindowDefined = typeof window != 'undefined';
119
+ const IS_SERVER = !isWindowDefined || isLegacyDeno;
120
+ ```
121
+
122
+ Revalidation event sources are registered only `if (!IS_SERVER)` —
123
+ _including_ a custom `initFocus`/`initReconnect` passed through the
124
+ cache-provider config. So the "wire your own focus events via the provider
125
+ option" approach from SWR's docs is dead code under Node: your `initFocus`
126
+ is never called, and swr ends up with zero registered listeners. Unlike
127
+ TanStack Query there is no `setIsServer`.
128
+
129
+ Two ways out. The simple one is to drive revalidation yourself with
130
+ `mutate`; a key filter of `() => true` revalidates every key in the cache,
131
+ so hang it off `<window onFocus>`:
132
+
133
+ ```jsx
134
+ import React from 'react';
135
+ import { createRoot } from 'react-x11';
136
+ import useSWR, { mutate } from 'swr';
137
+
138
+ const fetcher = (url) => fetch(url).then((r) => r.text());
139
+
140
+ function Weather() {
141
+ const { data } = useSWR('https://wttr.in/?format=3', fetcher);
142
+ return <text>{data ?? 'loading…'}</text>;
143
+ }
144
+
145
+ (await createRoot()).render(
146
+ // the adapter is this one prop: revalidate everything on window focus
147
+ <window
148
+ width={360}
149
+ height={120}
150
+ title="weather"
151
+ onFocus={() => mutate(() => true)}
152
+ >
153
+ <Weather />
154
+ </window>,
155
+ );
156
+ ```
157
+
158
+ The thorough one is to make `IS_SERVER` false, which turns swr's own
159
+ machinery back on. It has to happen **before the first import of swr**:
160
+
161
+ ```js
162
+ // swr-env.js — imported first, before anything that imports swr
163
+ const listeners = new Map();
164
+ const target = {
165
+ addEventListener: (t, fn) =>
166
+ listeners.set(t, [...(listeners.get(t) ?? []), fn]),
167
+ removeEventListener: (t, fn) =>
168
+ listeners.set(
169
+ t,
170
+ (listeners.get(t) ?? []).filter((f) => f !== fn),
171
+ ),
172
+ };
173
+ globalThis.window ??= target;
174
+ globalThis.document ??= target;
175
+
176
+ /** Call from your <window onFocus> handler. */
177
+ export const fireFocus = () =>
178
+ (listeners.get('focus') ?? []).forEach((fn) => fn());
179
+ ```
180
+
181
+ With that in place swr registers `visibilitychange`, `focus`, `online` and
182
+ `offline` listeners and `revalidateOnFocus` behaves. Two gotchas:
183
+
184
+ - Fire only `'focus'`. An `'offline'` event sets swr's internal online flag
185
+ false and silently suppresses the deferred revalidation.
186
+ - `focusThrottleInterval` defaults to 5000 ms, so a focus that lands within
187
+ five seconds of the last revalidation is dropped — which looks exactly
188
+ like the shim not working. Lower it while you are testing the wiring.
189
+
190
+ - **`revalidateOnFocus` and `revalidateOnReconnect` are inert** unless you
191
+ define the globals. Nothing warns you.
192
+ - The global `mutate` import targets the _default_ cache. If you mount a
193
+ custom `provider` via `<SWRConfig>`, calls to the global `mutate` silently
194
+ miss it — use `useSWRConfig().mutate` from inside the tree.
195
+ - Suspense mode throws
196
+ `Fallback data is required when using Suspense in SSR.` because SWR
197
+ believes it is on a server. Provide `fallbackData`, or avoid
198
+ `suspense: true`.
199
+ - `preload()` is an SSR-gated no-op under Node.
200
+ - If you want focus revalidation with retries and cache GC done properly,
201
+ prefer TanStack Query above — it ships a supported environment override.