react-x11 2.51.0 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,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.
|