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.
- 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 +9 -2
- 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,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.
|