react-x11 2.51.1 → 2.53.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 (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. package/src/xvideo.js +189 -0
@@ -0,0 +1,360 @@
1
+ # Animation
2
+
3
+ react-x11's built-in `transition` style property has a fixed easing curve
4
+ ([styling](../styling.md#transitions)). This page covers what it cannot
5
+ express: springs with velocity carry-over, interruption, arbitrary easing
6
+ curves, shape morphing, and keeping an element mounted while it animates
7
+ out.
8
+
9
+ **There is no `opacity` style property**, and `validateStyle` throws on
10
+ unknown properties, so the classic fade does not port; animate a colour, a
11
+ size or a position instead.
12
+
13
+ Two patterns recur. The cheap one is `onUpdate → setState → style prop`:
14
+ each frame is a React render plus a commit, which react-x11 turns into a
15
+ partial repaint of the changed node. Keep the animated component a leaf — a
16
+ `<box>`, not the window root — and it holds spring frame rates. The other is
17
+ the [react-spring host](#react-spring): more setup, but it applies frames
18
+ imperatively and bypasses React.
19
+
20
+ Node has no global `requestAnimationFrame`. In an app, use the ntk window's
21
+ `requestAnimationFrame` (through a ref on `<window>`), which paces frames to
22
+ X connection latency; a 16 ms `setInterval` works everywhere else.
23
+
24
+ `framer-motion` fails at both its component and its value layer — see the
25
+ [negative-results register](../ecosystem.md#render-time).
26
+
27
+ ## popmotion {#popmotion}
28
+
29
+ **Out of the box.** popmotion@11.0.5.
30
+
31
+ `animate()` tweens or springs a plain JS value and calls `onUpdate(v)`
32
+ every frame. The smallest way to get real springs, with velocity and
33
+ overshoot, without the react-spring host.
34
+
35
+ ```jsx
36
+ import React, { useEffect, useState } from 'react';
37
+ import { animate } from 'popmotion';
38
+
39
+ function Bar() {
40
+ const [w, setW] = useState(20);
41
+ useEffect(() => {
42
+ const anim = animate({
43
+ from: 20,
44
+ to: 200,
45
+ stiffness: 300,
46
+ damping: 20,
47
+ onUpdate: setW,
48
+ });
49
+ return () => anim.stop();
50
+ }, []);
51
+ return <box style={{ width: w, height: 30, backgroundColor: '#3366ff' }} />;
52
+ }
53
+ ```
54
+
55
+ With no `requestAnimationFrame`, popmotion falls back to a 16.7 ms
56
+ `setTimeout`, so it needs no configuration. For latency-adaptive pacing,
57
+ pass `animate()` a custom `driver` backed by the ntk window's clock.
58
+
59
+ - Springs on layout properties (width, margin) cost a yoga layout pass per
60
+ frame, as react-x11's own layout transitions do.
61
+ - `animate` interpolates colour strings (`'#f00' → '#00f'`), and the
62
+ `rgba(...)` strings it emits paint as-is.
63
+ - Use only the value APIs — `animate`, `spring`, `decay`, `mix` — not
64
+ `styler` or the DOM helpers.
65
+
66
+ ## d3-ease {#d3-ease}
67
+
68
+ **Out of the box.** d3-ease@3.0.1.
69
+
70
+ Pure easing functions — `ease(t)` maps normalized time to progress — with
71
+ thirty-plus curves (cubic, elastic, bounce, back) the built-in `transition`
72
+ lacks. Compute `t` from wall time, pass `ease(t)` to an interpolator, push
73
+ the result into a style prop.
74
+
75
+ ```jsx
76
+ import React, { useEffect, useState } from 'react';
77
+ import { easeCubicOut } from 'd3-ease';
78
+ import { interpolateNumber } from 'd3-interpolate';
79
+
80
+ function useTimeline(duration, ease, onFrame) {
81
+ useEffect(() => {
82
+ const start = Date.now();
83
+ const timer = setInterval(() => {
84
+ const t = Math.min(1, (Date.now() - start) / duration);
85
+ onFrame(ease(t));
86
+ if (t >= 1) clearInterval(timer);
87
+ }, 16);
88
+ return () => clearInterval(timer);
89
+ }, []);
90
+ }
91
+
92
+ const width = interpolateNumber(10, 200);
93
+
94
+ function Gauge() {
95
+ const [f, setF] = useState(0);
96
+ useTimeline(250, easeCubicOut, setF);
97
+ return (
98
+ <box style={{ width: width(f), height: 24, backgroundColor: '#204080' }} />
99
+ );
100
+ }
101
+ ```
102
+
103
+ - ESM-only since v3 — `import`, not `require()`.
104
+ - It is only the curve; you own the clock and the interpolation.
105
+ - Overshooting curves (elastic, back) leave the from/to range; clamp before
106
+ feeding anything react-x11 validates as non-negative.
107
+
108
+ ## d3-interpolate {#d3-interpolate}
109
+
110
+ **Out of the box.** d3-interpolate@3.0.1.
111
+
112
+ `interpolate(a, b)` returns `t => value` for numbers, colours in any CSS
113
+ space, strings with embedded numbers, dates, arrays and nested objects.
114
+ `interpolateRgb` emits `'rgb(r, g, b)'` strings, which paint as-is. (The
115
+ interpolator behind react-x11's `transition` is not exported.)
116
+
117
+ ```jsx
118
+ import { easeCubicOut } from 'd3-ease';
119
+ import { interpolateNumber, interpolateRgb } from 'd3-interpolate';
120
+
121
+ const width = interpolateNumber(10, 200);
122
+ const color = interpolateRgb('#204080', '#e04040');
123
+
124
+ function Gauge() {
125
+ const [f, setF] = useState(0);
126
+ useTimeline(250, easeCubicOut, setF); // see d3-ease above
127
+ return (
128
+ <box style={{ width: width(f), height: 24, backgroundColor: color(f) }} />
129
+ );
130
+ }
131
+ ```
132
+
133
+ - ESM-only since v3.
134
+ - Generic `interpolate(a, b)` on objects interpolates _every_ numeric-looking
135
+ key, giving fractional values to properties that want integers.
136
+ Interpolate only the properties you mean.
137
+ - There is no `opacity`; to fade, interpolate a colour's alpha into
138
+ `backgroundColor: 'rgba(...)'`.
139
+ - `interpolateArray` over polygon points feeding `<canvas onDraw>` is a good
140
+ use of the general form.
141
+
142
+ ## `@tweenjs/tween.js` {#tweenjs}
143
+
144
+ **Out of the box.** @tweenjs/tween.js@25.0.0.
145
+
146
+ `new Tween(obj).to(target, ms).easing(...).onUpdate(...).start()`, then
147
+ _you_ call `tween.update(time)` on your own clock — there is no implicit
148
+ global loop. In an app, pump from the ntk window's `requestAnimationFrame`;
149
+ with many tweens, one `Group` per window lets `group.update(now)` step them
150
+ all from the same tick.
151
+
152
+ ```jsx
153
+ import React, { useEffect, useState } from 'react';
154
+ import { Tween, Easing } from '@tweenjs/tween.js';
155
+
156
+ function Panel() {
157
+ const [pos, setPos] = useState({ x: 0, h: 10 });
158
+ useEffect(() => {
159
+ const state = { x: 0, h: 10 };
160
+ const tween = new Tween(state)
161
+ .to({ x: 150, h: 60 }, 300)
162
+ .easing(Easing.Cubic.Out)
163
+ .onUpdate(() => setPos({ ...state }))
164
+ .start();
165
+ const timer = setInterval(() => {
166
+ if (!tween.update()) clearInterval(timer); // update() is false when done
167
+ }, 16);
168
+ return () => clearInterval(timer);
169
+ }, []);
170
+ return (
171
+ <box
172
+ style={{
173
+ marginLeft: pos.x,
174
+ width: 40,
175
+ height: pos.h,
176
+ backgroundColor: '#00aa55',
177
+ }}
178
+ />
179
+ );
180
+ }
181
+ ```
182
+
183
+ - Numbers only, including nested in objects and arrays. For colours use
184
+ d3-interpolate or popmotion.
185
+ - Nothing animates until something calls `update()` — the classic first
186
+ bug.
187
+ - `onUpdate` mutates the source object, so spread it into fresh state
188
+ (`setPos({ ...state })`) or React skips the re-render on identity.
189
+
190
+ ## flubber {#flubber}
191
+
192
+ **Out of the box.** flubber@0.4.2.
193
+
194
+ Shape morphing for SVG paths: `interpolate(from, to)` returns
195
+ `t => d-string`, handling ring alignment, point insertion and topology
196
+ changes so intermediate frames do not self-intersect.
197
+
198
+ ```jsx
199
+ import React, { useEffect, useState } from 'react';
200
+ import flubber from 'flubber'; // CJS: default import, no named ESM exports
201
+ const { interpolate } = flubber;
202
+
203
+ const morph = interpolate(
204
+ 'M50,10 L90,90 L10,90 Z',
205
+ 'M10,10 L90,10 L90,90 L10,90 Z',
206
+ );
207
+ const svgFor = (d) =>
208
+ `<svg viewBox="0 0 100 100"><path d="${d}" fill="#cc3355"/></svg>`;
209
+
210
+ function Morph() {
211
+ const [t, setT] = useState(0);
212
+ useEffect(() => {
213
+ const start = Date.now();
214
+ const timer = setInterval(() => {
215
+ const p = Math.min(1, (Date.now() - start) / 200);
216
+ setT(p);
217
+ if (p >= 1) clearInterval(timer);
218
+ }, 16);
219
+ return () => clearInterval(timer);
220
+ }, []);
221
+ return <svg source={svgFor(morph(t))} style={{ width: 100, height: 100 }} />;
222
+ }
223
+ ```
224
+
225
+ - `import { interpolate } from 'flubber'` throws — it is a CJS package, so
226
+ use the default-import form above.
227
+ - Build the interpolator once (module scope or `useMemo`); construction is
228
+ the expensive part.
229
+ - Every frame is a full SVG parse and rasterize of the `source` string. Fine
230
+ for icons and small figures; for large scenes draw the interpolated points
231
+ in `<canvas onDraw>`.
232
+ - 0.4.2 since 2017 — finished rather than unmaintained.
233
+
234
+ ## react-transition-group {#react-transition-group}
235
+
236
+ **Out of the box — with `nodeRef`.** react-transition-group@4.4.5.
237
+
238
+ A transition _state machine_, not an animator: `<Transition in={bool}
239
+ timeout={ms}>` walks its child through `entering`/`entered`/`exiting`/
240
+ `exited` on a timer and keeps it mounted during exit — the way to keep an
241
+ element mounted while it animates out, which react-x11 has no built-in for.
242
+
243
+ Always pass `nodeRef` — without it, v4 falls back to `ReactDOM.findDOMNode`,
244
+ which cannot handle a react-x11 node. Map states to styles yourself, with the
245
+ built-in `transition` style so the flip _tweens_ instead of snapping.
246
+
247
+ ```jsx
248
+ import React, { useRef } from 'react';
249
+ import { Transition } from 'react-transition-group';
250
+
251
+ function Panel({ shown }) {
252
+ const nodeRef = useRef(null);
253
+ return (
254
+ <Transition
255
+ nodeRef={nodeRef}
256
+ in={shown}
257
+ timeout={150}
258
+ mountOnEnter
259
+ unmountOnExit
260
+ >
261
+ {(state) => (
262
+ <box
263
+ ref={nodeRef}
264
+ style={{
265
+ transition: 150, // let react-x11 tween the flip
266
+ width: 100,
267
+ height: 50,
268
+ backgroundColor: state === 'entered' ? '#ff0000' : '#0000ff',
269
+ }}
270
+ />
271
+ )}
272
+ </Transition>
273
+ );
274
+ }
275
+ ```
276
+
277
+ - **`react-dom` must be in `node_modules`.** `cjs/Transition.js` requires it
278
+ at module scope for the `findDOMNode` fallback, even when `nodeRef` means
279
+ it is never called. Install the peer; nothing renders through it.
280
+ - `CSSTransition` toggles class names, and there are none. Use plain
281
+ `Transition`.
282
+ - `timeout` and your transition duration are independent; keep them equal,
283
+ or the state flips mid-tween.
284
+ - `addEndListener` works; call the provided `done` yourself.
285
+
286
+ ## `@react-spring/core` {#react-spring}
287
+
288
+ **Adapter, ~20 lines.** @react-spring/core@10.1.2,
289
+ @react-spring/animated@10.1.2.
290
+
291
+ The platform-independent half of react-spring: `useSpring`,
292
+ `useTransition`, `useTrail`, `SpringValue`, `Controller` — springs,
293
+ interruption with velocity carry-over, `api.start()`.
294
+ `@react-spring/animated` provides `createHost`, the documented seam for a
295
+ non-DOM target (how `@react-spring/native` and `/three` exist). Two public
296
+ seams do the work:
297
+
298
+ - `Globals.assign({ now, requestAnimationFrame })` points the frame loop at
299
+ a clock.
300
+ - `createHost(['box', …], { applyAnimatedValues })` produces `animated.box`.
301
+ Each frame, react-spring calls `applyAnimatedValues(instance, props)` with
302
+ the react-x11 node, and `node.applyProps(next, prev)` is the imperative
303
+ path the reconciler's `commitUpdate` uses. A component renders once while
304
+ hundreds of frames apply.
305
+
306
+ ```jsx
307
+ // spring-x11.js — the adapter
308
+ import { Globals } from '@react-spring/core';
309
+ import { createHost } from '@react-spring/animated';
310
+
311
+ Globals.assign({
312
+ now: Date.now,
313
+ // real app: cb => ntkWindow.requestAnimationFrame(cb) (ref on <window>)
314
+ requestAnimationFrame: (cb) => setTimeout(() => cb(Date.now()), 16),
315
+ });
316
+
317
+ const host = createHost(['box', 'text', 'window'], {
318
+ applyAnimatedValues(instance, props) {
319
+ if (!instance || typeof instance.applyProps !== 'function') return false;
320
+ const prev = instance.props;
321
+ const next = { ...prev, ...props };
322
+ // frames carry only animated leaves — keep the static style keys
323
+ if (props.style) next.style = { ...prev.style, ...props.style };
324
+ instance.applyProps(next, prev);
325
+ },
326
+ });
327
+
328
+ export const animated = host.animated;
329
+ ```
330
+
331
+ ```jsx
332
+ import React, { useEffect } from 'react';
333
+ import { useSpring } from '@react-spring/core';
334
+ import { animated } from './spring-x11.js';
335
+
336
+ function Bar() {
337
+ const [style, api] = useSpring(() => ({
338
+ from: { width: 20, backgroundColor: '#204080' },
339
+ }));
340
+ useEffect(() => {
341
+ api.start({ width: 180, backgroundColor: '#e04040' });
342
+ }, []);
343
+ return <animated.box style={{ ...style, height: 30 }} />;
344
+ }
345
+ ```
346
+
347
+ - **Merge `style` one level deep in `applyAnimatedValues`** — the
348
+ number-one silent failure. Frames carry _animated leaves only_
349
+ (`props.getValue(true)`), so static keys (`height: 30` beside an animated
350
+ `width`) are absent, and a naive `{...prev, ...props}` merge wipes them:
351
+ the box paints nothing, with no error.
352
+ - Hex, `rgb()` and `rgba()` strings interpolate; named CSS colours are not
353
+ enabled by default in `@react-spring/shared`.
354
+ - Do not use `useScroll`, `useResize`, `useInView` or `useReducedMotion` from
355
+ core; they observe the DOM `window` and `matchMedia`.
356
+ `Globals.assign({ skipAnimation })` is the manual reduced-motion switch.
357
+ - Style arrays (`style={[a, b]}`) defeat the shallow merge. Flatten first —
358
+ react-x11 exports `flattenStyle`.
359
+ - A `setImmediate` clock runs frames as fast as the event loop turns — fine
360
+ for tests, wasteful for apps. Use the ntk window clock or a 16 ms timeout.
@@ -0,0 +1,186 @@
1
+ # Data fetching
2
+
3
+ A react-x11 app is an ordinary Node process, so `fetch`, undici, `ky` and
4
+ `graphql-request` work unmodified. The _cache_ layer on top needs care:
5
+ stale-while-revalidate libraries decide whether they are "on a server" by
6
+ looking for a global `window`, and a react-x11 process has none. Get it
7
+ wrong and nothing throws; the app just stops refetching.
8
+
9
+ ## TanStack Query {#tanstack-query}
10
+
11
+ **Adapter, ~12 lines.** @tanstack/react-query@5.101.4 (query-core 5.101.4).
12
+
13
+ `useQuery` deduplicates, caches, retries and background-refetches;
14
+ `useMutation` handles writes with invalidation. The environment coupling
15
+ lives in three replaceable managers — `focusManager`, `onlineManager`,
16
+ `environmentManager`. The trap is query-core's probe:
17
+
18
+ ```js
19
+ // @tanstack/query-core src/utils.ts
20
+ export const isServer = typeof window === 'undefined' || 'Deno' in globalThis;
21
+ ```
22
+
23
+ With no `window`, query-core decides it is doing SSR:
24
+
25
+ - `retry` defaults to **0** instead of 3;
26
+ - stale timers are never scheduled and `refetchInterval` is disabled;
27
+ - `gcTime` defaults to **Infinity** instead of 5 minutes — the cache never
28
+ collects;
29
+ - the default `focusManager` setup wants
30
+ `window.addEventListener('visibilitychange')`, so focus refetching is
31
+ inert.
32
+
33
+ The fix is `environmentManager.setIsServer()`, re-exported from
34
+ `@tanstack/react-query`, paired with react-x11's window focus events:
35
+
36
+ ```jsx
37
+ // query-x11.js — the whole adapter
38
+ import {
39
+ environmentManager,
40
+ focusManager,
41
+ onlineManager,
42
+ } from '@tanstack/react-query';
43
+
44
+ environmentManager.setIsServer(() => false); // we are a client, not SSR
45
+ focusManager.setEventListener(() => () => {}); // drop the visibilitychange probe
46
+ onlineManager.setEventListener(() => () => {}); // assume online (or probe for real)
47
+
48
+ export const windowFocusProps = {
49
+ onFocus: () => focusManager.setFocused(true),
50
+ onBlur: () => focusManager.setFocused(false),
51
+ };
52
+ ```
53
+
54
+ ```jsx
55
+ import React from 'react';
56
+ import { createRoot } from 'react-x11';
57
+ import {
58
+ QueryClient,
59
+ QueryClientProvider,
60
+ useQuery,
61
+ } from '@tanstack/react-query';
62
+ import { windowFocusProps } from './query-x11.js';
63
+
64
+ const client = new QueryClient();
65
+
66
+ function Weather() {
67
+ const { data, status } = useQuery({
68
+ queryKey: ['weather'],
69
+ queryFn: () => fetch('https://wttr.in/?format=3').then((r) => r.text()),
70
+ });
71
+ return <text>{status === 'pending' ? 'loading…' : data}</text>;
72
+ }
73
+
74
+ (await createRoot()).render(
75
+ <window width={360} height={120} title="weather" {...windowFocusProps}>
76
+ <QueryClientProvider client={client}>
77
+ <Weather />
78
+ </QueryClientProvider>
79
+ </window>,
80
+ );
81
+ ```
82
+
83
+ Focusing the window now refetches stale queries, like refocusing a browser
84
+ tab.
85
+
86
+ - **Call `setIsServer` before creating the `QueryClient`** — the managers
87
+ are module singletons.
88
+ - Switching between two of your own windows fires blur-then-focus, which
89
+ refetches stale queries; raise `staleTime` if that is too eager.
90
+ - `onlineManager` defaults to online under Node (no `navigator.onLine`).
91
+ Wire it to NetworkManager over D-Bus for real offline handling.
92
+ - `@tanstack/react-query-devtools` is DOM-rendered and not usable.
93
+
94
+ ## SWR {#swr}
95
+
96
+ **Adapter, 1 line — but read the dead end first.** swr@2.4.2.
97
+
98
+ `useSWR` fetches on mount, caches, dedupes and re-renders unmodified. Focus
99
+ and reconnect revalidation fail silently: SWR computes its environment once
100
+ at import time, with no override:
101
+
102
+ ```js
103
+ // swr/dist/_internal/config-context-*.mjs
104
+ const isWindowDefined = typeof window != 'undefined';
105
+ const IS_SERVER = !isWindowDefined || isLegacyDeno;
106
+ ```
107
+
108
+ Revalidation event sources are registered only `if (!IS_SERVER)` —
109
+ _including_ a custom `initFocus`/`initReconnect` passed through the
110
+ cache-provider config, so the provider approach from SWR's docs is dead code
111
+ under Node and swr registers zero listeners. There is no `setIsServer`.
112
+
113
+ Two ways out. The simple one: drive revalidation with `mutate`, whose key
114
+ filter `() => true` revalidates every cached key, from `<window onFocus>`:
115
+
116
+ ```jsx
117
+ import React from 'react';
118
+ import { createRoot } from 'react-x11';
119
+ import useSWR, { mutate } from 'swr';
120
+
121
+ const fetcher = (url) => fetch(url).then((r) => r.text());
122
+
123
+ function Weather() {
124
+ const { data } = useSWR('https://wttr.in/?format=3', fetcher);
125
+ return <text>{data ?? 'loading…'}</text>;
126
+ }
127
+
128
+ (await createRoot()).render(
129
+ // the adapter is this one prop: revalidate everything on window focus
130
+ <window
131
+ width={360}
132
+ height={120}
133
+ title="weather"
134
+ onFocus={() => mutate(() => true)}
135
+ >
136
+ <Weather />
137
+ </window>,
138
+ );
139
+ ```
140
+
141
+ The thorough one: make `IS_SERVER` false, which turns swr's own machinery
142
+ back on. It has to happen **before the first import of swr**:
143
+
144
+ ```js
145
+ // swr-env.js — imported first, before anything that imports swr
146
+ const listeners = new Map();
147
+ const target = {
148
+ addEventListener: (t, fn) =>
149
+ listeners.set(t, [...(listeners.get(t) ?? []), fn]),
150
+ removeEventListener: (t, fn) =>
151
+ listeners.set(
152
+ t,
153
+ (listeners.get(t) ?? []).filter((f) => f !== fn),
154
+ ),
155
+ };
156
+ globalThis.window ??= target;
157
+ globalThis.document ??= target;
158
+
159
+ /** Call from your <window onFocus> handler. */
160
+ export const fireFocus = () =>
161
+ (listeners.get('focus') ?? []).forEach((fn) => fn());
162
+ ```
163
+
164
+ swr then registers `visibilitychange`, `focus`, `online` and `offline`
165
+ listeners and `revalidateOnFocus` behaves. Two gotchas:
166
+
167
+ - Fire only `'focus'`. An `'offline'` event sets swr's internal online flag
168
+ false and silently suppresses the deferred revalidation.
169
+ - `focusThrottleInterval` defaults to 5000 ms, so a focus within five
170
+ seconds of the last revalidation is dropped — which looks exactly like the
171
+ shim not working. Lower it while testing the wiring.
172
+
173
+ Other caveats:
174
+
175
+ - **`revalidateOnFocus` and `revalidateOnReconnect` are inert** unless you
176
+ define the globals. Nothing warns you.
177
+ - The global `mutate` targets the _default_ cache, so with a custom
178
+ `provider` via `<SWRConfig>` it silently misses; use
179
+ `useSWRConfig().mutate` from inside the tree.
180
+ - Suspense mode throws
181
+ `Fallback data is required when using Suspense in SSR.` because SWR
182
+ believes it is on a server. Provide `fallbackData`, or avoid
183
+ `suspense: true`.
184
+ - `preload()` is an SSR-gated no-op under Node.
185
+ - For focus revalidation with retries and cache GC done properly, prefer
186
+ TanStack Query — it ships a supported environment override.