react-x11 2.51.1 → 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 +9 -2
  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,410 @@
1
+ # Icons
2
+
3
+ react-x11 has a real `<svg>` host, so icons get further than most DOM
4
+ libraries — which is exactly why the failures here are unusual. Two renderer
5
+ facts shape every entry on this page:
6
+
7
+ 1. **ntk's `SvgView` ignores presentation attributes on the root `<svg>`
8
+ element.** Inheritance starts from its own defaults, so an icon that puts
9
+ `fill="none" stroke="currentColor" stroke-width="2"` on the root renders
10
+ as solid black blobs — wrong glyphs, not just the wrong colour. Moving
11
+ those attributes onto an inner `<g>` fixes it, because `<g>` does
12
+ inherit.
13
+ 2. **Flat style props on `<svg>` throw.** `<svg width={24}>` is
14
+ `react-x11: <svg width=…> is a style property — pass it in style`. Size
15
+ with `style={{ width: 24 }}`; the `viewBox` keeps the aspect ratio.
16
+
17
+ Fact 2 is why `lucide-react`, `@tabler/icons-react` and
18
+ `@phosphor-icons/react` cannot work at all — see the
19
+ [negative-results register](../ecosystem.md#render-time). `@heroicons/react`
20
+ is the exception, and it is documented below.
21
+
22
+ ## The `iconSource` wrapper {#iconsource}
23
+
24
+ Four of the packages below ship whole SVG _strings_ with paint attributes on
25
+ the root. They all need the same twelve lines, so write it once:
26
+
27
+ ```js
28
+ // icon-source.js — re-root an icon's paint attributes onto a <g> so they
29
+ // survive SvgView, and pin `color` there so currentColor resolves.
30
+ const ROOT = /<svg([^>]*)>([\s\S]*)<\/svg>\s*$/;
31
+
32
+ export function iconSource(svgText, color = '#000') {
33
+ const [, root, body] = ROOT.exec(svgText);
34
+ const viewBox = /viewBox="[^"]*"/.exec(root)?.[0] ?? '';
35
+ const paint = (
36
+ root.match(
37
+ /(?:fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule)="[^"]*"/g,
38
+ ) ?? []
39
+ ).join(' ');
40
+ return `<svg xmlns="http://www.w3.org/2000/svg" ${viewBox}><g color="${color}" ${paint}>${body}</g></svg>`;
41
+ }
42
+ ```
43
+
44
+ `SvgView` maps `currentColor` to the inherited `color`, which defaults to
45
+ `#000` — so the `color` attribute on the `<g>` is what makes recolouring
46
+ work. Note that `svgText.replaceAll('currentColor', color)` on its own is
47
+ _not_ enough: it fixes the colour but not the dropped `fill="none"`, so
48
+ stroke icons still paint as filled blobs.
49
+
50
+ Cache the wrapped strings per (name, style, colour); parsing per mount is
51
+ avoidable work.
52
+
53
+ ## Iconify {#iconify}
54
+
55
+ **Out of the box.** @iconify/utils@3.1.4 with any `@iconify-json/*` set.
56
+
57
+ Iconify publishes every major icon set as JSON data — `@iconify-json/tabler`,
58
+ `@iconify-json/mdi`, `@iconify-json/ph`, `@iconify-json/lucide`,
59
+ `@iconify-json/heroicons` — plus `@iconify/utils`, a framework-free toolkit
60
+ that looks an icon up and builds SVG markup from it.
61
+
62
+ This is the highest-leverage route: one pipeline, 200,000+ icons across every
63
+ set, no React wrapper to fight. It is also the only string-SVG route that
64
+ needs **no re-rooting adapter**, because `iconToSVG` returns the icon body
65
+ separately from the root attributes, and the paint attributes live inside the
66
+ body rather than on the root.
67
+
68
+ ```jsx
69
+ import { getIconData, iconToSVG, iconToHTML, replaceIDs } from '@iconify/utils';
70
+ import { icons as tabler } from '@iconify-json/tabler';
71
+
72
+ function iconSvg(name, color = '#000') {
73
+ const data = getIconData(tabler, name); // null if unknown
74
+ const built = iconToSVG(data, { height: 24 });
75
+ return iconToHTML(replaceIDs(built.body), built.attributes).replaceAll(
76
+ 'currentColor',
77
+ color,
78
+ );
79
+ }
80
+
81
+ function Icon({ name, size = 24, color }) {
82
+ return <svg source={iconSvg(name, color)} style={{ width: size }} />;
83
+ }
84
+
85
+ // <Icon name="home" color="#7c3aed" />
86
+ // <Icon name="settings" size={16} color="#0891b2" />
87
+ ```
88
+
89
+ - `replaceIDs` matters for icons containing gradients or clip paths;
90
+ duplicate `id`s across two mounted icons would collide inside one
91
+ rasterized document. Harmless across separate `<svg>` nodes, but free to
92
+ keep.
93
+ - Pick per-set packages (`@iconify-json/tabler` is about 5,000 icons in one
94
+ JSON) rather than the monolithic `@iconify/json`, which is all 200,000+ and
95
+ roughly 100 MB.
96
+ - There are no events inside a rasterized SVG. Wrap it in a
97
+ `<box onClick>`.
98
+
99
+ ## `@heroicons/react` {#heroicons-react}
100
+
101
+ **Out of the box — size it with `style`.** @heroicons/react@2.2.0.
102
+
103
+ The exception among the `*-react` icon packages. Heroicons' React components
104
+ size themselves with a Tailwind class name rather than flat `width`/`height`
105
+ attributes, so react-x11's `<svg>` host accepts them and the path paints. Do
106
+ not lump this one in with lucide-react, `@tabler/icons-react` or
107
+ `@phosphor-icons/react`, all three of which throw.
108
+
109
+ ```jsx
110
+ import { BeakerIcon } from '@heroicons/react/24/solid';
111
+
112
+ <box style={{ flexGrow: 1 }}>
113
+ <BeakerIcon style={{ width: 24, height: 24 }} />
114
+ </box>;
115
+ ```
116
+
117
+ The `className` it sets is ignored by the renderer, so you must pass
118
+ `style={{ width, height }}` yourself — an icon with no size will lay out at
119
+ its natural size or collapse, depending on the surrounding flex box.
120
+
121
+ There is no recolour channel through the component, so if you need a themed
122
+ icon use the asset package below, or `@iconify-json/heroicons`.
123
+
124
+ ## `lucide` {#lucide}
125
+
126
+ **Adapter, ~10 lines.** lucide@1.28.0.
127
+
128
+ Lucide's core package. Alongside the DOM-oriented `createIcons`, every icon
129
+ is exported as plain data: an array of `[tag, attrs]` pairs. No DOM, no
130
+ React, tree-shakeable per icon.
131
+
132
+ This is the best-fitting icon package for react-x11, because the data shape
133
+ skips string parsing entirely — each pair becomes a JSX child of `<svg>`,
134
+ which react-x11 serializes through `SvgChildNode` (camelCase to kebab
135
+ attributes) into ntk's `SvgView`.
136
+
137
+ ```jsx
138
+ import { createElement } from 'react';
139
+ import { House, Sun } from 'lucide';
140
+
141
+ function Icon({ icon, size = 24, color = '#000', strokeWidth = 2 }) {
142
+ return (
143
+ <svg viewBox="0 0 24 24" style={{ width: size, height: size }}>
144
+ <g
145
+ fill="none"
146
+ stroke={color}
147
+ strokeWidth={strokeWidth}
148
+ strokeLinecap="round"
149
+ strokeLinejoin="round"
150
+ >
151
+ {icon.map(([tag, attrs], i) =>
152
+ createElement(tag, { key: i, ...attrs }),
153
+ )}
154
+ </g>
155
+ </svg>
156
+ );
157
+ }
158
+
159
+ // <Icon icon={House} color="#16a34a" />
160
+ // <Icon icon={theme === 'dark' ? Sun : House} color={accent} />
161
+ ```
162
+
163
+ Prop changes on SVG children invalidate and repaint correctly — swapping
164
+ `icon` or `color` in state re-renders the icon.
165
+
166
+ - Omitting the `<g>` and putting `stroke`/`fill` on the `<svg>` silently
167
+ renders black filled blobs.
168
+ - Function props on SVG children are dropped by the serializer, so there are
169
+ no per-child events. Wrap the `<svg>` in a `<box onClick>` for a clickable
170
+ icon.
171
+ - `className` on a child serializes to a `class-name` attribute — harmless,
172
+ but do not rely on classes for styling.
173
+
174
+ ## `lucide-static` {#lucide-static}
175
+
176
+ **Adapter — the [`iconSource` wrapper](#iconsource).** lucide-static@1.28.0.
177
+
178
+ The framework-free build of Lucide: every icon is a named export that is just
179
+ an SVG markup string. 24×24 viewBox, stroke-based, `stroke="currentColor"`
180
+ with round caps and joins — all on the root element, which is why it needs
181
+ the wrapper.
182
+
183
+ ```jsx
184
+ import * as lucide from 'lucide-static';
185
+ import { iconSource } from './icon-source.js';
186
+
187
+ const Icon = ({ name, size = 24, color = '#000' }) => (
188
+ <svg source={iconSource(lucide[name], color)} style={{ width: size }} />
189
+ );
190
+
191
+ // <Icon name="House" color="#e11d48" />
192
+ // <Icon name="Sun" size={16} color="#eab308" />
193
+ ```
194
+
195
+ Use the `lucide` data package above where you can; reach for this one when
196
+ you want the markup string itself (asset pipelines, caching a pre-built
197
+ sheet).
198
+
199
+ ## `react-icons` {#react-icons}
200
+
201
+ **Adapter, ~15 lines.** react-icons@5.7.0.
202
+
203
+ The standard React icon aggregator — Font Awesome, Material, Lucide,
204
+ Bootstrap, Simple Icons and about fifty more, tens of thousands of icons
205
+ behind one API. Out of the box it throws, because `IconBase` always sets
206
+ `height`/`width` (default `"1em"`) as flat props on `<svg>`.
207
+
208
+ The adapter sidesteps `IconBase` entirely. An icon component called with
209
+ empty props returns the `IconBase` element, whose props carry exactly what is
210
+ needed: `attr` (the set's root attributes — `viewBox`, and for stroke-based
211
+ sets the paint attributes) and `children` (the icon's paths as ready-made
212
+ React elements). Re-rooting those attributes onto an inner `<g>` handles
213
+ fact 1 above.
214
+
215
+ ```jsx
216
+ import { FaHeart } from 'react-icons/fa';
217
+ import { LuHouse } from 'react-icons/lu';
218
+
219
+ // Works for both fill sets (fa, md, …) and stroke sets (lu, fi, …).
220
+ function RIcon({ icon: I, size = 24, color = '#000' }) {
221
+ const { attr = {}, children } = I({}).props; // IconBase element
222
+ const { viewBox = '0 0 24 24', ...paint } = attr;
223
+ return (
224
+ <svg viewBox={viewBox} style={{ width: size, height: size }}>
225
+ <g
226
+ color={color}
227
+ fill="currentColor"
228
+ stroke="currentColor"
229
+ strokeWidth={0}
230
+ {...paint}
231
+ >
232
+ {children}
233
+ </g>
234
+ </svg>
235
+ );
236
+ }
237
+
238
+ // <RIcon icon={FaHeart} color="#e11d48" /> fill-based
239
+ // <RIcon icon={LuHouse} color="#2563eb" /> stroke-based
240
+ ```
241
+
242
+ - `I({})` inside render leans on each icon being a plain function component
243
+ wrapping `GenIcon`. That has been react-icons' shape since v4, but it is an
244
+ internal one — if it changes, prefer `lucide` or the Iconify pipeline,
245
+ which have no such seam.
246
+ - `IconContext` (global size/colour config) is bypassed; pass props instead.
247
+ - The `title` prop emits an SVG `<title>` child, and there is no tooltip or
248
+ assistive-technology surface behind a rasterized SVG. Omit it.
249
+ - react-icons is heavy on disk — every set is bundled. If you only need one
250
+ style, `lucide` or `@iconify-json/<set>` is a much smaller install.
251
+
252
+ ## `@tabler/icons` {#tabler-icons}
253
+
254
+ **Adapter — the [`iconSource` wrapper](#iconsource).** @tabler/icons@3.46.0.
255
+
256
+ The framework-free Tabler package: 5,000+ MIT icons as SVG files under
257
+ `icons/outline/` (stroke-based, 24×24) and `icons/filled/`, with an `exports`
258
+ map that resolves file paths directly. It also ships
259
+ `tabler-nodes-outline.json`/`-filled.json` — per-icon node arrays, the same
260
+ idea as `lucide`'s data exports.
261
+
262
+ ```jsx
263
+ import fs from 'node:fs';
264
+ import { iconSource } from './icon-source.js';
265
+
266
+ const tabler = (name, style = 'outline') =>
267
+ fs.readFileSync(
268
+ new URL(import.meta.resolve(`@tabler/icons/${style}/${name}.svg`)),
269
+ 'utf8',
270
+ );
271
+
272
+ const Icon = ({ name, style, size = 24, color = '#000' }) => (
273
+ <svg
274
+ source={iconSource(tabler(name, style), color)}
275
+ style={{ width: size }}
276
+ />
277
+ );
278
+
279
+ // <Icon name="home" color="#dc2626" />
280
+ // <Icon name="settings" style="filled" size={16} />
281
+ ```
282
+
283
+ Honestly, though: **prefer `@iconify-json/tabler` + `@iconify/utils`**, which
284
+ serves identical art with no adapter at all. This package earns its place
285
+ when you want the files themselves (design pipelines, asset preprocessing) or
286
+ the JSON node arrays.
287
+
288
+ - Outline icons start with a `<path stroke="none" d="M0 0h24v24H0z"
289
+ fill="none"/>` bounds rect; it inherits nothing and paints nothing.
290
+ Harmless.
291
+ - `@tabler/icons-react` throws. See the
292
+ [register](../ecosystem.md#render-time).
293
+
294
+ ## `@phosphor-icons/core` {#phosphor-icons}
295
+
296
+ **Adapter — the [`iconSource` wrapper](#iconsource).**
297
+ @phosphor-icons/core@2.1.1.
298
+
299
+ Phosphor's framework-free package: raw SVG files under
300
+ `assets/<weight>/<name>.svg` for six weights (`thin`, `light`, `regular`,
301
+ `bold`, `fill`, `duotone`), 256×256 viewBox, fill-based.
302
+
303
+ Because Phosphor is fill-based, the raw file _does_ render legible black
304
+ glyphs — the default fill happens to match — so the wrapper is only
305
+ mandatory for colour. The `duotone` weight relies on per-path opacity, which
306
+ survives fine either way.
307
+
308
+ ```jsx
309
+ import fs from 'node:fs';
310
+ import { iconSource } from './icon-source.js';
311
+
312
+ const phosphor = (name, weight = 'regular') => {
313
+ const file = weight === 'regular' ? name : `${name}-${weight}`;
314
+ return fs.readFileSync(
315
+ new URL(
316
+ import.meta.resolve(`@phosphor-icons/core/assets/${weight}/${file}.svg`),
317
+ ),
318
+ 'utf8',
319
+ );
320
+ };
321
+
322
+ const Icon = ({ name, weight, size = 24, color = '#000' }) => (
323
+ <svg
324
+ source={iconSource(phosphor(name, weight), color)}
325
+ style={{ width: size }}
326
+ />
327
+ );
328
+
329
+ // <Icon name="house" color="#0ea5e9" />
330
+ // <Icon name="house" weight="fill" size={32} />
331
+ ```
332
+
333
+ - Non-`regular` weights use suffixed filenames
334
+ (`assets/bold/house-bold.svg`); the helper above accounts for it.
335
+ - `@iconify-json/ph` serves the same set with no adapter and no filesystem
336
+ access. Prefer it unless you want the files.
337
+ - `@phosphor-icons/react` throws. See the
338
+ [register](../ecosystem.md#render-time).
339
+
340
+ ## `heroicons` (the asset package) {#heroicons-assets}
341
+
342
+ **Adapter — the [`iconSource` wrapper](#iconsource).** heroicons@2.2.0.
343
+
344
+ The Tailwind-house set as plain SVG files: `24/outline/*.svg` (stroke-based),
345
+ `24/solid/`, `20/solid/` and `16/solid/`. No JS at all — the `heroicons` npm
346
+ package is just the files.
347
+
348
+ ```jsx
349
+ import fs from 'node:fs';
350
+ import { iconSource } from './icon-source.js';
351
+
352
+ const heroicon = (name, style = '24/outline') =>
353
+ fs.readFileSync(
354
+ new URL(import.meta.resolve(`heroicons/${style}/${name}.svg`)),
355
+ 'utf8',
356
+ );
357
+
358
+ const Icon = ({ name, style, size = 24, color = '#000' }) => (
359
+ <svg
360
+ source={iconSource(heroicon(name, style), color)}
361
+ style={{ width: size }}
362
+ />
363
+ );
364
+
365
+ // <Icon name="home" color="#f59e0b" />
366
+ // <Icon name="home" style="24/solid" color="#0ea5e9" />
367
+ ```
368
+
369
+ Use this rather than `@heroicons/react` when you need to recolour — the React
370
+ components render fine but give you no colour channel. `heroicons` has no JS
371
+ index, so enumerate the directory if you need a picker.
372
+
373
+ **Prefer `@iconify-json/heroicons`** through the [Iconify](#iconify)
374
+ pipeline: it is the same art, with no adapter and no filesystem access. Reach
375
+ for the asset package when you specifically want the files.
376
+
377
+ ## `feather-icons` {#feather-icons}
378
+
379
+ **Adapter, ~5 lines.** feather-icons@4.29.2.
380
+
381
+ The classic 24×24 stroke set, 287 icons. Feather exposes `.contents` — the
382
+ inner SVG markup, without a root element — which makes the adapter a template
383
+ literal instead of a regex: build the root and the `<g>` yourself and fact 1
384
+ never applies.
385
+
386
+ ```jsx
387
+ import feather from 'feather-icons';
388
+
389
+ function featherSvg(name, color = '#000', strokeWidth = 2) {
390
+ return `<svg viewBox="0 0 24 24"><g fill="none" stroke="${color}"
391
+ stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round"
392
+ >${feather.icons[name].contents}</g></svg>`;
393
+ }
394
+
395
+ const Icon = ({ name, size = 24, color, strokeWidth }) => (
396
+ <svg source={featherSvg(name, color, strokeWidth)} style={{ width: size }} />
397
+ );
398
+
399
+ // <Icon name="alert-circle" color="#f59e0b" />
400
+ ```
401
+
402
+ - **Prefer Lucide.** Feather is in maintenance mode upstream — no new icons
403
+ in years. Lucide is its actively-developed fork with five times the
404
+ coverage and the same visual language. Reach for Feather only if you are
405
+ matching an existing design.
406
+ - Raw `feather.icons.x.toSvg()` into `<svg source>` renders wrong, because
407
+ `toSvg()` puts every paint attribute on the root. `toSvg({ color })` sets
408
+ `color` on the root too, where it is equally ignored. Always go through
409
+ `.contents`.
410
+ - `feather.replace()` (the DOM `<i data-feather>` swapper) is browser-only.
@@ -0,0 +1,216 @@
1
+ # Layout and positioning
2
+
3
+ Layout itself is yoga's job — see [styling](../styling.md). The two packages
4
+ here cover the parts yoga does not: placing a popup relative to a trigger
5
+ across screen edges, and rendering a long list without building a node per
6
+ row.
7
+
8
+ Both are the _core_ half of a library whose React wrapper is DOM-bound. That
9
+ is the recurring shape in this section of the ecosystem: the algorithm is
10
+ portable, the wrapper is not, and the core is published separately precisely
11
+ so that non-DOM hosts can use it.
12
+
13
+ ## `@floating-ui/core` {#floating-ui}
14
+
15
+ **Adapter, ~35 lines (a platform object).** @floating-ui/core@1.8.0.
16
+
17
+ The positioning engine underneath Floating UI, published without any DOM
18
+ code. You hand `computePosition` a `platform` object — `getElementRects`,
19
+ `getDimensions`, `getClippingRect`, `isRTL` — and get back the full
20
+ middleware pipeline (`offset`, `flip`, `shift`, `size`, `arrow`,
21
+ `autoPlacement`, `hide`) with the placement vocabulary every web developer
22
+ already knows.
23
+
24
+ The seam is exactly where `anchorRect()` already works: a drawn node's
25
+ `node.abs` rect plus the owning window's screen origin gives the reference
26
+ rect in screen coordinates, `screenOf(node)` gives the clipping rect, and the
27
+ popup-to-be is just `{width, height}`. The result's `{x, y, placement}` drops
28
+ straight into `<popup x y>`.
29
+
30
+ ```jsx
31
+ import { computePosition, offset, flip, shift } from '@floating-ui/core';
32
+
33
+ // The screen a node is on. Same three lines the built-in anchorRect uses;
34
+ // it is not exported from the package, so keep a copy.
35
+ function screenOf(node) {
36
+ const app = node?.app;
37
+ const screen = (app?.display ?? app?.X?.display)?.screen?.[0];
38
+ return screen?.pixel_width ? screen : null;
39
+ }
40
+
41
+ // reference = a drawn node (post-layout); floating = the popup's {width, height}
42
+ function screenRectOf(node) {
43
+ const win = node.root?.window;
44
+ const origin = win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
45
+ return {
46
+ x: origin.x + node.abs.x,
47
+ y: origin.y + node.abs.y,
48
+ width: node.abs.width,
49
+ height: node.abs.height,
50
+ };
51
+ }
52
+
53
+ const platform = {
54
+ getElementRects: ({ reference, floating }) => ({
55
+ reference: screenRectOf(reference),
56
+ floating: { x: 0, y: 0, width: floating.width, height: floating.height },
57
+ }),
58
+ getDimensions: (el) => el.abs ?? el, // node or plain {width, height}
59
+ getClippingRect: ({ element }) => {
60
+ const s = screenOf(element?.node ?? element);
61
+ return s
62
+ ? { x: 0, y: 0, width: s.pixel_width, height: s.pixel_height }
63
+ : { x: 0, y: 0, width: Infinity, height: Infinity };
64
+ },
65
+ isElement: () => true,
66
+ isRTL: () => false,
67
+ };
68
+
69
+ const { x, y, placement } = await computePosition(
70
+ triggerNode, // from a ref on the trigger <box>
71
+ { width: 200, height: 150, node: triggerNode },
72
+ {
73
+ placement: 'bottom-start',
74
+ middleware: [offset(4), flip(), shift({ padding: 8 })],
75
+ platform,
76
+ },
77
+ );
78
+ // -> <popup x={x} y={y}> ; placement tells you which side won
79
+ ```
80
+
81
+ What this buys over the built-in `anchorRect` is middleware composition
82
+ (`shift` with padding, `size` to clamp long menus, `arrow`) and alignment
83
+ fallback — `flip` with `crossAxis: 'alignment'` switches `bottom-start` to
84
+ `bottom-end` when that is what fits. `anchorRect`'s `placement`, `align` and
85
+ `offset` options map 1:1 onto `placement` plus the `offset()` middleware.
86
+
87
+ - `computePosition` is async — `await` it, even though this platform is fully
88
+ synchronous.
89
+ - `getClippingRect` above is screen 0's full rect, the same single-monitor
90
+ simplification `screenOf` has today. On a dual-head X display, popups clamp
91
+ to the bounding box of both monitors.
92
+ - **`@floating-ui/dom` and `@floating-ui/react` are not usable** — they are
93
+ the DOM platform and DOM interactions respectively. Only `core` and its
94
+ `@floating-ui/utils` dependency belong in an X11 app.
95
+ - The safe-polygon hover logic in `anchor.js` has no equivalent in `core`;
96
+ that lives in `@floating-ui/react`'s `useHover`, which is DOM-bound. Keep
97
+ the built-in one.
98
+
99
+ ## `@tanstack/virtual-core` {#tanstack-virtual}
100
+
101
+ **Adapter, ~40 lines.** @tanstack/virtual-core@3.17.7.
102
+
103
+ The virtualization engine behind `useVirtualizer`: given a count, an
104
+ `estimateSize`, the viewport rect and the scroll offset, it computes which
105
+ items are visible (`getVirtualItems()`), the total scrollable size
106
+ (`getTotalSize()`), and offsets for `scrollToIndex`. All DOM touchpoints are
107
+ constructor options, which is what makes this work.
108
+
109
+ **Do not import `@tanstack/react-virtual`.** Its first line is
110
+ `import { flushSync } from "react-dom"`. Without react-dom installed it fails
111
+ to resolve; _with_ react-dom installed it silently flushes into the wrong
112
+ reconciler — see the
113
+ [silent-failures table](../ecosystem.md#silent-failures). Everything the
114
+ React wrapper adds is the hook below.
115
+
116
+ Three seams wire to a scrolling `<box>`:
117
+
118
+ - `observeElementRect` → `<box onViewport>`, which fires from layout
119
+ with `{width, height, contentWidth, contentHeight}` — including before any
120
+ scroll, which is what a list needs on mount;
121
+ - `observeElementOffset` → `<box onScroll>` (`scrollY`);
122
+ - `scrollToFn` → `ref.scrollTo(offset)`.
123
+
124
+ One non-obvious requirement: `getScrollElement()` must return something with
125
+ `scrollHeight`/`clientHeight`, because virtual-core duck-types Element vs
126
+ Window with `'scrollHeight' in el`. A getter shim over the scroll box node's
127
+ `contentHeight`/`abs.height` satisfies it.
128
+
129
+ ```jsx
130
+ import React from 'react';
131
+ import { Virtualizer } from '@tanstack/virtual-core';
132
+
133
+ function useX11Virtualizer({ count, estimateSize, overscan = 2 }) {
134
+ const ref = React.useRef(null);
135
+ const obs = React.useRef({});
136
+ const [, rerender] = React.useReducer((n) => n + 1, 0);
137
+ const [element] = React.useState(() => ({
138
+ // element-shaped view of the node
139
+ get scrollHeight() {
140
+ return ref.current?.contentHeight ?? 0;
141
+ },
142
+ get clientHeight() {
143
+ return ref.current?.abs.height ?? 0;
144
+ },
145
+ get scrollWidth() {
146
+ return ref.current?.contentWidth ?? 0;
147
+ },
148
+ get clientWidth() {
149
+ return ref.current?.abs.width ?? 0;
150
+ },
151
+ }));
152
+ const [v] = React.useState(
153
+ () =>
154
+ new Virtualizer({
155
+ count,
156
+ estimateSize,
157
+ overscan,
158
+ getScrollElement: () => (ref.current ? element : null),
159
+ observeElementRect: (_i, cb) => {
160
+ obs.current.rect = cb;
161
+ },
162
+ observeElementOffset: (_i, cb) => {
163
+ obs.current.offset = cb;
164
+ cb(0, false);
165
+ },
166
+ scrollToFn: (offset) => ref.current?.scrollTo(offset),
167
+ onChange: () => rerender(),
168
+ }),
169
+ );
170
+ v.setOptions({ ...v.options, count, onChange: () => rerender() });
171
+ React.useEffect(() => v._didMount(), []);
172
+ React.useEffect(() => v._willUpdate());
173
+ return {
174
+ virtualizer: v,
175
+ scrollProps: {
176
+ ref,
177
+ onViewport: (r) => obs.current.rect?.(r),
178
+ onScroll: (s) => obs.current.offset?.(s.scrollY, true),
179
+ },
180
+ };
181
+ }
182
+ ```
183
+
184
+ ```jsx
185
+ function BigList() {
186
+ const { virtualizer, scrollProps } = useX11Virtualizer({
187
+ count: 10000,
188
+ estimateSize: () => 24,
189
+ });
190
+ return (
191
+ <box {...scrollProps} style={{ flexGrow: 1, overflow: 'scroll' }}>
192
+ <box style={{ height: virtualizer.getTotalSize() }}>
193
+ {virtualizer.getVirtualItems().map((item) => (
194
+ <text
195
+ key={item.key}
196
+ style={{ position: 'absolute', top: item.start, height: item.size }}
197
+ >
198
+ {`row ${item.index}`}
199
+ </text>
200
+ ))}
201
+ </box>
202
+ </box>
203
+ );
204
+ }
205
+ ```
206
+
207
+ - **First paint shows zero rows.** Layout runs on the frame clock after the
208
+ mount commit, `onViewport` reports a tick later, and the list fills in on
209
+ the next frame. Fine interactively; tests must tick a few times.
210
+ - `estimateSize` is the only measurement. Dynamic per-item measurement
211
+ (`measureElement`) is built on `ResizeObserver` and
212
+ `getBoundingClientRect`. For variable-height rows, pass exact sizes:
213
+ `estimateSize: (i) => heights[i]`.
214
+ - `_didMount`/`_willUpdate` are underscored but are the documented pattern
215
+ for custom framework adapters — the official React, Vue and Svelte adapters
216
+ call exactly these.