react-x11 2.51.1 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- 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/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -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/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Headless components
|
|
2
|
+
|
|
3
|
+
A library that returns DOM prop bags — `useButton()` and the like — is
|
|
4
|
+
DOM-coupled with the rendering deferred, not headless in a sense that helps
|
|
5
|
+
here. The two packages below own state and collections, return plain values,
|
|
6
|
+
and never touch a ref. Headless UI and the Radix primitives render DOM hosts
|
|
7
|
+
and fail ([register](../ecosystem.md#what-does-not-work)); downshift's
|
|
8
|
+
`getInputProps()` works on a `<textinput>` ([forms](forms.md)).
|
|
9
|
+
|
|
10
|
+
For finished widgets react-x11 ships its own: `Button`, `Checkbox`,
|
|
11
|
+
`Radio`/`RadioGroup`, `Switch`, `ProgressBar`, `Select`, `Slider`, `Tooltip`,
|
|
12
|
+
`Dialog`, `MenuBar`/`ContextMenu`, `Tabs`, `Table`, `SplitPane` — see
|
|
13
|
+
[components](../components.md).
|
|
14
|
+
|
|
15
|
+
## react-stately {#react-stately}
|
|
16
|
+
|
|
17
|
+
**Out of the box.** react-stately@3.48.0.
|
|
18
|
+
|
|
19
|
+
The state-machine half of React Aria: `useListState`, `useTreeState`,
|
|
20
|
+
`useTableState`, `useSelectState`, `useToggleState`, `useSliderState`,
|
|
21
|
+
`useComboBoxState`, `useMenuTriggerState`, `useNumberFieldState` and about
|
|
22
|
+
thirty-five more, plus the `Item`/`Section` collection API. Each hook owns
|
|
23
|
+
selection, disabled-key traversal, expansion and typeahead-ready collections
|
|
24
|
+
— everything but rendering and event binding, which react-x11 supplies. No
|
|
25
|
+
hook references `document`, `window` or `getBoundingClientRect` at runtime,
|
|
26
|
+
and the dependencies (`@internationalized/*`, `@react-types/shared`,
|
|
27
|
+
`use-sync-external-store`) are DOM-free.
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
import React from 'react';
|
|
31
|
+
import { useListState, Item } from 'react-stately';
|
|
32
|
+
|
|
33
|
+
function Picker() {
|
|
34
|
+
const state = useListState({
|
|
35
|
+
children: [
|
|
36
|
+
<Item key="alpha">Alpha</Item>,
|
|
37
|
+
<Item key="beta">Beta</Item>,
|
|
38
|
+
<Item key="gamma">Gamma</Item>,
|
|
39
|
+
],
|
|
40
|
+
selectionMode: 'multiple',
|
|
41
|
+
});
|
|
42
|
+
const mgr = state.selectionManager;
|
|
43
|
+
return (
|
|
44
|
+
<box
|
|
45
|
+
style={{ flexDirection: 'column' }}
|
|
46
|
+
focusable
|
|
47
|
+
onKeyDown={(ev) => {
|
|
48
|
+
if (ev.key === 'ArrowDown')
|
|
49
|
+
mgr.setFocusedKey(
|
|
50
|
+
state.collection.getKeyAfter(mgr.focusedKey) ?? mgr.focusedKey,
|
|
51
|
+
);
|
|
52
|
+
if (ev.codepoint === 32) mgr.toggleSelection(mgr.focusedKey);
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{[...state.collection].map((item) => (
|
|
56
|
+
<box
|
|
57
|
+
key={item.key}
|
|
58
|
+
style={{ height: 20, justifyContent: 'center' }}
|
|
59
|
+
onClick={() => mgr.toggleSelection(item.key)}
|
|
60
|
+
>
|
|
61
|
+
<text>
|
|
62
|
+
{(mgr.isSelected(item.key) ? '* ' : ' ') + item.rendered}
|
|
63
|
+
</text>
|
|
64
|
+
</box>
|
|
65
|
+
))}
|
|
66
|
+
</box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **Only `react-stately` is portable.** `react-aria` (hooks that produce DOM
|
|
72
|
+
props and bind DOM events) and `react-aria-components` are not — do not
|
|
73
|
+
follow Adobe's docs into `useListBox` or `useButton`.
|
|
74
|
+
- Since 3.48 the old `@react-stately/*` packages are bundled in, with
|
|
75
|
+
per-hook subpath exports (`react-stately/useListState`) for lean imports.
|
|
76
|
+
- The collection API wants `<Item>`/`<Section>` elements, or `items` plus a
|
|
77
|
+
render function — ceremonious for a three-entry menu.
|
|
78
|
+
- `useAsyncList` carries fetch-based loading state — fine under Node, the
|
|
79
|
+
one hook with I/O opinions.
|
|
80
|
+
- Drag-and-drop hooks (`useDraggableCollectionState`) hold state, but
|
|
81
|
+
everything that would _feed_ them events is DOM-side; write the event
|
|
82
|
+
plumbing yourself.
|
|
83
|
+
|
|
84
|
+
## `@tanstack/react-table` {#tanstack-table}
|
|
85
|
+
|
|
86
|
+
**Out of the box.** @tanstack/react-table@9.1.2 (table-core 9.1.2,
|
|
87
|
+
react-store 0.11.1).
|
|
88
|
+
|
|
89
|
+
A headless table engine: column defs, row models, sorting, filtering,
|
|
90
|
+
grouping, pagination, column sizing, visibility and pinning, as pure state
|
|
91
|
+
plus derived row models. You map `getHeaderGroups()` and
|
|
92
|
+
`getRowModel().rows` to react-x11 elements.
|
|
93
|
+
|
|
94
|
+
v9's "adapters" are per framework, not per renderer: `@tanstack/react-table`
|
|
95
|
+
_is_ the React adapter, so there is nothing to write, and it never touches
|
|
96
|
+
`react-dom` or `document` at runtime (but see the react-dom and `flexRender`
|
|
97
|
+
bullets). `column.getToggleSortingHandler()` goes straight onto a header
|
|
98
|
+
`onClick`. Features and row models are registered up front through
|
|
99
|
+
`tableFeatures`, and the hook is `useTable`.
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
import React from 'react';
|
|
103
|
+
import {
|
|
104
|
+
useTable,
|
|
105
|
+
tableFeatures,
|
|
106
|
+
rowSortingFeature,
|
|
107
|
+
createCoreRowModel,
|
|
108
|
+
createSortedRowModel,
|
|
109
|
+
createColumnHelper,
|
|
110
|
+
flexRender,
|
|
111
|
+
} from '@tanstack/react-table';
|
|
112
|
+
|
|
113
|
+
// Registered once, outside the component. A feature that is not listed here
|
|
114
|
+
// does not exist: its state slice, its column methods, its row model.
|
|
115
|
+
const features = tableFeatures({
|
|
116
|
+
rowSortingFeature,
|
|
117
|
+
coreRowModel: createCoreRowModel(),
|
|
118
|
+
sortedRowModel: createSortedRowModel(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const col = createColumnHelper();
|
|
122
|
+
const columns = [
|
|
123
|
+
col.accessor('name', { header: 'Name' }),
|
|
124
|
+
col.accessor('port', { header: 'Port', cell: (i) => `:${i.getValue()}` }),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
function Servers({ data }) {
|
|
128
|
+
const table = useTable({ features, columns, data });
|
|
129
|
+
return (
|
|
130
|
+
<box style={{ flexDirection: 'column' }}>
|
|
131
|
+
{table.getHeaderGroups().map((hg) => (
|
|
132
|
+
<box key={hg.id} style={{ flexDirection: 'row', height: 20 }}>
|
|
133
|
+
{hg.headers.map((h) => (
|
|
134
|
+
<box
|
|
135
|
+
key={h.id}
|
|
136
|
+
style={{ width: 120 }}
|
|
137
|
+
onClick={h.column.getToggleSortingHandler()}
|
|
138
|
+
>
|
|
139
|
+
<text>
|
|
140
|
+
{flexRender(h.column.columnDef.header, h.getContext())}
|
|
141
|
+
</text>
|
|
142
|
+
</box>
|
|
143
|
+
))}
|
|
144
|
+
</box>
|
|
145
|
+
))}
|
|
146
|
+
{table.getRowModel().rows.map((row) => (
|
|
147
|
+
<box key={row.id} style={{ flexDirection: 'row', height: 20 }}>
|
|
148
|
+
{row.getAllCells().map((cell) => (
|
|
149
|
+
<text key={cell.id} style={{ width: 120 }}>
|
|
150
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
151
|
+
</text>
|
|
152
|
+
))}
|
|
153
|
+
</box>
|
|
154
|
+
))}
|
|
155
|
+
</box>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Sorting is uncontrolled here — the table's store holds it, and the header
|
|
161
|
+
click re-renders through the adapter's subscription. Pass
|
|
162
|
+
`state`/`onSortingChange` to control it, or a selector as `useTable`'s
|
|
163
|
+
second argument to subscribe a component to only some slices. Render into
|
|
164
|
+
the built-in `Table` component or raw `<box style={{ flexDirection: 'row' }}>`
|
|
165
|
+
rows.
|
|
166
|
+
|
|
167
|
+
- **`coreRowModel` is opt-in, and forgetting it fails silently.** Without
|
|
168
|
+
`coreRowModel: createCoreRowModel()` in `tableFeatures`, headers render
|
|
169
|
+
and `getRowModel().rows` is `[]`, with no error. The official examples get
|
|
170
|
+
it from framework presets; register it yourself.
|
|
171
|
+
- **npm installs a `react-dom` you must not keep.** `@tanstack/react-store`
|
|
172
|
+
declares a `react-dom` _peer_, which npm 7+ auto-installs. The shipped
|
|
173
|
+
code never imports it (test 2 finds one `"button"`, in a doc comment, and
|
|
174
|
+
the table works with `node_modules/react-dom` deleted), but a resident
|
|
175
|
+
react-dom is the [silent-failures](../ecosystem.md#silent-failures)
|
|
176
|
+
hazard. Install with `--legacy-peer-deps`, or delete it so
|
|
177
|
+
`npm ls react-dom` stays empty.
|
|
178
|
+
- **`flexRender` returns a React element even for a plain string cell** (v8
|
|
179
|
+
returned the string). Inside `<text>` it paints identically, but a test or
|
|
180
|
+
a11y walk reading a `<text>` node's `children` prop sees an element
|
|
181
|
+
object; read the rendered chunks. A `cell` that returns DOM elements
|
|
182
|
+
throws the unknown-element error; return strings or react-x11 elements.
|
|
183
|
+
- Column _resizing_ helpers (`header.getResizeHandler`) expect DOM mouse or
|
|
184
|
+
touch events with `clientX` and fall back to a `document`-listener drag
|
|
185
|
+
loop (injectable as `_contextDocument`). Simpler: handle the drag off
|
|
186
|
+
`onMouseDown`/`onMouseMove` (react-x11 events carry `x`/`y`) and call
|
|
187
|
+
`table.setColumnSizing` (needs `columnSizingFeature` registered).
|
|
188
|
+
- `useTable` probes `typeof window` to pick `useLayoutEffect` and gets
|
|
189
|
+
`useEffect` under Node — a harmless test-3 hit (timing, not a feature
|
|
190
|
+
gate).
|
|
191
|
+
- For thousands of rows, pair it with a
|
|
192
|
+
[virtualizer](layout.md#tanstack-virtual).
|
|
193
|
+
- State updates driven from outside a React event land one frame later than
|
|
194
|
+
the call, as with any react-x11 update.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Internationalization
|
|
2
|
+
|
|
3
|
+
## i18next + react-i18next
|
|
4
|
+
|
|
5
|
+
**Out of the box.** i18next@26.3.6, react-i18next@17.0.11.
|
|
6
|
+
|
|
7
|
+
i18next (resources, interpolation, plurals, namespaces, backends) with
|
|
8
|
+
react-i18next's `useTranslation`, `<Trans>` and provider-driven re-render
|
|
9
|
+
runs unmodified: interpolation, count-based plurals, `<Trans>`, and
|
|
10
|
+
`i18n.changeLanguage('de')` re-rendering the mounted tree in place. Node
|
|
11
|
+
ships full ICU, so `Intl.NumberFormat` and `Intl.DateTimeFormat` work too.
|
|
12
|
+
|
|
13
|
+
Two react-x11-specific rules:
|
|
14
|
+
|
|
15
|
+
1. **`<Trans>` component maps must use react-x11 elements.**
|
|
16
|
+
`components={{ 1: <span/> }}` throws
|
|
17
|
+
`react-x11: <span> is not allowed inside <text>` — inside a `<text>`, only
|
|
18
|
+
strings and nested `<text>` spans are legal. Use `<text style={...}/>` as
|
|
19
|
+
the wrapper.
|
|
20
|
+
2. **Turn off HTML escaping.** i18next escapes interpolated values for an
|
|
21
|
+
HTML sink by default, and react-x11 draws raw glyphs, so `O'Brien` renders
|
|
22
|
+
as `O'Brien`. Set `interpolation: { escapeValue: false }`.
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
import React from 'react';
|
|
26
|
+
import i18next from 'i18next';
|
|
27
|
+
import {
|
|
28
|
+
I18nextProvider,
|
|
29
|
+
initReactI18next,
|
|
30
|
+
useTranslation,
|
|
31
|
+
Trans,
|
|
32
|
+
} from 'react-i18next';
|
|
33
|
+
|
|
34
|
+
const i18n = i18next.createInstance();
|
|
35
|
+
await i18n.use(initReactI18next).init({
|
|
36
|
+
lng: 'en',
|
|
37
|
+
resources: {
|
|
38
|
+
en: {
|
|
39
|
+
translation: {
|
|
40
|
+
greeting: 'Hello, {{name}}!',
|
|
41
|
+
items_one: '{{count}} item',
|
|
42
|
+
items_other: '{{count}} items',
|
|
43
|
+
rich: 'Press <1>Enter</1> to save',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
interpolation: { escapeValue: false }, // no HTML sink to escape for
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function Status() {
|
|
51
|
+
const { t } = useTranslation();
|
|
52
|
+
return (
|
|
53
|
+
<box style={{ flexDirection: 'column' }}>
|
|
54
|
+
<text>{t('greeting', { name: 'X11' })}</text>
|
|
55
|
+
<text>{t('items', { count: 2 })}</text>
|
|
56
|
+
<text>
|
|
57
|
+
<Trans
|
|
58
|
+
i18nKey="rich"
|
|
59
|
+
components={{ 1: <text style={{ color: '#e00' }} /> }}
|
|
60
|
+
/>
|
|
61
|
+
</text>
|
|
62
|
+
</box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// mount under <I18nextProvider i18n={i18n}> inside your <window>;
|
|
67
|
+
// i18n.changeLanguage('de') re-renders live
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To load translations from disk use `i18next-fs-backend`;
|
|
71
|
+
`i18next-http-backend` also works, since Node has `fetch`.
|
|
72
|
+
|
|
73
|
+
## What is missing below the string
|
|
74
|
+
|
|
75
|
+
React-x11 gaps rather than i18next ones, and they matter more than the
|
|
76
|
+
library choice:
|
|
77
|
+
|
|
78
|
+
- **No input method on X11 or Cocoa.** Dead keys and the Compose key are
|
|
79
|
+
handled in the renderer ([events.md](../events.md#composition)), but
|
|
80
|
+
there is no XIM/IBus/Fcitx client on X11 and no `NSTextInputClient` on
|
|
81
|
+
Cocoa, so a language that needs one — CJK — can be _displayed_ but not
|
|
82
|
+
typed there. The Wayland and Windows backends drive the system input
|
|
83
|
+
method.
|
|
84
|
+
- `<Trans>` with `<strong>`/`<em>`-style numbered tags is fine as long as
|
|
85
|
+
every referenced component is a `<text>` span. An accidental DOM tag fails
|
|
86
|
+
loudly at render.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Icons
|
|
2
|
+
|
|
3
|
+
react-x11 has a real `<svg>` host, so icons get further than most DOM
|
|
4
|
+
libraries. Two renderer facts shape every entry on this page:
|
|
5
|
+
|
|
6
|
+
1. **ntk's `SvgView`, which rasterizes `<svg>`, ignores presentation
|
|
7
|
+
attributes on the root element.** Inheritance starts from its own
|
|
8
|
+
defaults, so an icon with `fill="none" stroke="currentColor"
|
|
9
|
+
stroke-width="2"` on the root renders as solid black blobs — wrong
|
|
10
|
+
glyphs, not just the wrong colour. Moving those attributes onto an inner
|
|
11
|
+
`<g>`, which does inherit, fixes it.
|
|
12
|
+
2. **Flat style props on `<svg>` throw.** `<svg width={24}>` is
|
|
13
|
+
`react-x11: <svg width=…> is a style property — pass it in style`. Size
|
|
14
|
+
with `style={{ width: 24 }}`; the `viewBox` keeps the aspect ratio.
|
|
15
|
+
|
|
16
|
+
Fact 2 is why `lucide-react`, `@tabler/icons-react` and
|
|
17
|
+
`@phosphor-icons/react` cannot work
|
|
18
|
+
([register](../ecosystem.md#render-time)); `@heroicons/react` is the
|
|
19
|
+
exception.
|
|
20
|
+
|
|
21
|
+
## The `iconSource` wrapper {#iconsource}
|
|
22
|
+
|
|
23
|
+
Four of the packages below ship whole SVG _strings_ with paint attributes on
|
|
24
|
+
the root. They all need the same twelve lines:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// icon-source.js — re-root an icon's paint attributes onto a <g> so they
|
|
28
|
+
// survive SvgView, and pin `color` there so currentColor resolves.
|
|
29
|
+
const ROOT = /<svg([^>]*)>([\s\S]*)<\/svg>\s*$/;
|
|
30
|
+
|
|
31
|
+
export function iconSource(svgText, color = '#000') {
|
|
32
|
+
const [, root, body] = ROOT.exec(svgText);
|
|
33
|
+
const viewBox = /viewBox="[^"]*"/.exec(root)?.[0] ?? '';
|
|
34
|
+
const paint = (
|
|
35
|
+
root.match(
|
|
36
|
+
/(?:fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule)="[^"]*"/g,
|
|
37
|
+
) ?? []
|
|
38
|
+
).join(' ');
|
|
39
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" ${viewBox}><g color="${color}" ${paint}>${body}</g></svg>`;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`currentColor` resolves to the inherited `color`, which defaults to `#000`,
|
|
44
|
+
so the `color` attribute on the `<g>` is what makes recolouring work.
|
|
45
|
+
`svgText.replaceAll('currentColor', color)` alone is _not_ enough: it fixes
|
|
46
|
+
the colour but not the dropped `fill="none"`, so stroke icons still paint as
|
|
47
|
+
filled blobs. Cache the wrapped strings per (name, style, colour).
|
|
48
|
+
|
|
49
|
+
## Iconify {#iconify}
|
|
50
|
+
|
|
51
|
+
**Out of the box.** @iconify/utils@3.1.4 with any `@iconify-json/*` set.
|
|
52
|
+
|
|
53
|
+
Iconify publishes every major icon set as JSON data — `@iconify-json/tabler`,
|
|
54
|
+
`@iconify-json/mdi`, `@iconify-json/ph`, `@iconify-json/lucide`,
|
|
55
|
+
`@iconify-json/heroicons` — plus `@iconify/utils`, which looks an icon up and
|
|
56
|
+
builds SVG markup from it. The highest-leverage route: one pipeline, 200,000+
|
|
57
|
+
icons, and the only string-SVG route that needs **no re-rooting adapter**,
|
|
58
|
+
because `iconToSVG` returns the body separately from the root attributes and
|
|
59
|
+
the paint attributes live inside the body.
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
import { getIconData, iconToSVG, iconToHTML, replaceIDs } from '@iconify/utils';
|
|
63
|
+
import { icons as tabler } from '@iconify-json/tabler';
|
|
64
|
+
|
|
65
|
+
function iconSvg(name, color = '#000') {
|
|
66
|
+
const data = getIconData(tabler, name); // null if unknown
|
|
67
|
+
const built = iconToSVG(data, { height: 24 });
|
|
68
|
+
return iconToHTML(replaceIDs(built.body), built.attributes).replaceAll(
|
|
69
|
+
'currentColor',
|
|
70
|
+
color,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Icon({ name, size = 24, color }) {
|
|
75
|
+
return <svg source={iconSvg(name, color)} style={{ width: size }} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// <Icon name="home" color="#7c3aed" />
|
|
79
|
+
// <Icon name="settings" size={16} color="#0891b2" />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `replaceIDs` matters for icons with gradients or clip paths, whose `id`s
|
|
83
|
+
would collide inside one rasterized document. Harmless across separate
|
|
84
|
+
`<svg>` nodes, but free to keep.
|
|
85
|
+
- Pick per-set packages (`@iconify-json/tabler` is about 5,000 icons in one
|
|
86
|
+
JSON) rather than the monolithic `@iconify/json`, which is all 200,000+ and
|
|
87
|
+
roughly 100 MB.
|
|
88
|
+
- There are no events inside a rasterized SVG. Wrap it in a
|
|
89
|
+
`<box onClick>`.
|
|
90
|
+
|
|
91
|
+
## `@heroicons/react` {#heroicons-react}
|
|
92
|
+
|
|
93
|
+
**Out of the box — size it with `style`.** @heroicons/react@2.2.0.
|
|
94
|
+
|
|
95
|
+
The exception among the `*-react` icon packages: its components size
|
|
96
|
+
themselves with a Tailwind class name rather than flat `width`/`height`
|
|
97
|
+
attributes, so the `<svg>` host accepts them. lucide-react,
|
|
98
|
+
`@tabler/icons-react` and `@phosphor-icons/react` all throw.
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
import { BeakerIcon } from '@heroicons/react/24/solid';
|
|
102
|
+
|
|
103
|
+
<box style={{ flexGrow: 1 }}>
|
|
104
|
+
<BeakerIcon style={{ width: 24, height: 24 }} />
|
|
105
|
+
</box>;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The renderer ignores the `className`, so pass `style={{ width, height }}`
|
|
109
|
+
yourself — an icon with no size lays out at its natural size or collapses,
|
|
110
|
+
depending on the surrounding flex box. There is no recolour channel; for a
|
|
111
|
+
themed icon use the asset package below, or `@iconify-json/heroicons`.
|
|
112
|
+
|
|
113
|
+
## `lucide` {#lucide}
|
|
114
|
+
|
|
115
|
+
**Adapter, ~10 lines.** lucide@1.28.0.
|
|
116
|
+
|
|
117
|
+
Lucide's core package exports every icon as plain data — an array of
|
|
118
|
+
`[tag, attrs]` pairs — alongside the DOM-oriented `createIcons`.
|
|
119
|
+
Tree-shakeable per icon, and the best fit for react-x11: no string parsing,
|
|
120
|
+
each pair becomes a JSX child of `<svg>` (camelCase props serialize as
|
|
121
|
+
kebab-case attributes).
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
import { createElement } from 'react';
|
|
125
|
+
import { House, Sun } from 'lucide';
|
|
126
|
+
|
|
127
|
+
function Icon({ icon, size = 24, color = '#000', strokeWidth = 2 }) {
|
|
128
|
+
return (
|
|
129
|
+
<svg viewBox="0 0 24 24" style={{ width: size, height: size }}>
|
|
130
|
+
<g
|
|
131
|
+
fill="none"
|
|
132
|
+
stroke={color}
|
|
133
|
+
strokeWidth={strokeWidth}
|
|
134
|
+
strokeLinecap="round"
|
|
135
|
+
strokeLinejoin="round"
|
|
136
|
+
>
|
|
137
|
+
{icon.map(([tag, attrs], i) =>
|
|
138
|
+
createElement(tag, { key: i, ...attrs }),
|
|
139
|
+
)}
|
|
140
|
+
</g>
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// <Icon icon={House} color="#16a34a" />
|
|
146
|
+
// <Icon icon={theme === 'dark' ? Sun : House} color={accent} />
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Swapping `icon` or `color` in state repaints the icon.
|
|
150
|
+
|
|
151
|
+
- Omitting the `<g>` and putting `stroke`/`fill` on the `<svg>` silently
|
|
152
|
+
renders black filled blobs.
|
|
153
|
+
- Function props on SVG children are dropped by the serializer, so there are
|
|
154
|
+
no per-child events. Wrap the `<svg>` in a `<box onClick>` for a clickable
|
|
155
|
+
icon.
|
|
156
|
+
- `className` on a child serializes to a `class-name` attribute — harmless,
|
|
157
|
+
but do not rely on classes for styling.
|
|
158
|
+
|
|
159
|
+
## `lucide-static` {#lucide-static}
|
|
160
|
+
|
|
161
|
+
**Adapter — the [`iconSource` wrapper](#iconsource).** lucide-static@1.28.0.
|
|
162
|
+
|
|
163
|
+
Every Lucide icon as a named export holding an SVG markup string: 24×24,
|
|
164
|
+
stroke-based, `stroke="currentColor"` with round caps and joins — all on the
|
|
165
|
+
root element, hence the wrapper.
|
|
166
|
+
|
|
167
|
+
```jsx
|
|
168
|
+
import * as lucide from 'lucide-static';
|
|
169
|
+
import { iconSource } from './icon-source.js';
|
|
170
|
+
|
|
171
|
+
const Icon = ({ name, size = 24, color = '#000' }) => (
|
|
172
|
+
<svg source={iconSource(lucide[name], color)} style={{ width: size }} />
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// <Icon name="House" color="#e11d48" />
|
|
176
|
+
// <Icon name="Sun" size={16} color="#eab308" />
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Prefer the `lucide` data package; use this one when you want the markup
|
|
180
|
+
string itself (asset pipelines, a pre-built sheet).
|
|
181
|
+
|
|
182
|
+
## `react-icons` {#react-icons}
|
|
183
|
+
|
|
184
|
+
**Adapter, ~15 lines.** react-icons@5.7.0.
|
|
185
|
+
|
|
186
|
+
The React icon aggregator — Font Awesome, Material, Lucide, Bootstrap,
|
|
187
|
+
Simple Icons and about fifty more behind one API. Unadapted it throws,
|
|
188
|
+
because `IconBase` always sets `height`/`width` (default `"1em"`) as flat
|
|
189
|
+
props on `<svg>`.
|
|
190
|
+
|
|
191
|
+
The adapter sidesteps `IconBase`. An icon component called with empty props
|
|
192
|
+
returns the `IconBase` element, whose props carry `attr` (the set's root
|
|
193
|
+
attributes — `viewBox`, and for stroke sets the paint attributes) and
|
|
194
|
+
`children` (the paths as React elements). Re-rooting `attr` onto an inner
|
|
195
|
+
`<g>` handles fact 1.
|
|
196
|
+
|
|
197
|
+
```jsx
|
|
198
|
+
import { FaHeart } from 'react-icons/fa';
|
|
199
|
+
import { LuHouse } from 'react-icons/lu';
|
|
200
|
+
|
|
201
|
+
// Works for both fill sets (fa, md, …) and stroke sets (lu, fi, …).
|
|
202
|
+
function RIcon({ icon: I, size = 24, color = '#000' }) {
|
|
203
|
+
const { attr = {}, children } = I({}).props; // IconBase element
|
|
204
|
+
const { viewBox = '0 0 24 24', ...paint } = attr;
|
|
205
|
+
return (
|
|
206
|
+
<svg viewBox={viewBox} style={{ width: size, height: size }}>
|
|
207
|
+
<g
|
|
208
|
+
color={color}
|
|
209
|
+
fill="currentColor"
|
|
210
|
+
stroke="currentColor"
|
|
211
|
+
strokeWidth={0}
|
|
212
|
+
{...paint}
|
|
213
|
+
>
|
|
214
|
+
{children}
|
|
215
|
+
</g>
|
|
216
|
+
</svg>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// <RIcon icon={FaHeart} color="#e11d48" /> fill-based
|
|
221
|
+
// <RIcon icon={LuHouse} color="#2563eb" /> stroke-based
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- `I({})` relies on each icon being a plain function component wrapping
|
|
225
|
+
`GenIcon` — react-icons' shape since v4, but an internal one. `lucide` and
|
|
226
|
+
the Iconify pipeline have no such seam.
|
|
227
|
+
- `IconContext` (global size/colour config) is bypassed; pass props instead.
|
|
228
|
+
- The `title` prop emits an SVG `<title>` child, which reaches no tooltip or
|
|
229
|
+
assistive technology from a rasterized SVG. Omit it.
|
|
230
|
+
- react-icons bundles every set and is heavy on disk; for one style,
|
|
231
|
+
`lucide` or `@iconify-json/<set>` is a much smaller install.
|
|
232
|
+
|
|
233
|
+
## `@tabler/icons` {#tabler-icons}
|
|
234
|
+
|
|
235
|
+
**Adapter — the [`iconSource` wrapper](#iconsource).** @tabler/icons@3.46.0.
|
|
236
|
+
|
|
237
|
+
5,000+ MIT icons as SVG files under `icons/outline/` (stroke-based, 24×24)
|
|
238
|
+
and `icons/filled/`, with an `exports` map that resolves file paths, plus
|
|
239
|
+
`tabler-nodes-outline.json`/`-filled.json` — per-icon node arrays like
|
|
240
|
+
`lucide`'s data exports.
|
|
241
|
+
|
|
242
|
+
```jsx
|
|
243
|
+
import fs from 'node:fs';
|
|
244
|
+
import { iconSource } from './icon-source.js';
|
|
245
|
+
|
|
246
|
+
const tabler = (name, style = 'outline') =>
|
|
247
|
+
fs.readFileSync(
|
|
248
|
+
new URL(import.meta.resolve(`@tabler/icons/${style}/${name}.svg`)),
|
|
249
|
+
'utf8',
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const Icon = ({ name, style, size = 24, color = '#000' }) => (
|
|
253
|
+
<svg
|
|
254
|
+
source={iconSource(tabler(name, style), color)}
|
|
255
|
+
style={{ width: size }}
|
|
256
|
+
/>
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// <Icon name="home" color="#dc2626" />
|
|
260
|
+
// <Icon name="settings" style="filled" size={16} />
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Prefer `@iconify-json/tabler` + `@iconify/utils`**: identical art, no
|
|
264
|
+
adapter. Use this package when you want the files themselves or the JSON
|
|
265
|
+
node arrays.
|
|
266
|
+
|
|
267
|
+
- Outline icons start with a
|
|
268
|
+
`<path stroke="none" d="M0 0h24v24H0z" fill="none"/>` bounds rect, which
|
|
269
|
+
paints nothing. Harmless.
|
|
270
|
+
- `@tabler/icons-react` throws ([register](../ecosystem.md#render-time)).
|
|
271
|
+
|
|
272
|
+
## `@phosphor-icons/core` {#phosphor-icons}
|
|
273
|
+
|
|
274
|
+
**Adapter — the [`iconSource` wrapper](#iconsource).**
|
|
275
|
+
@phosphor-icons/core@2.1.1.
|
|
276
|
+
|
|
277
|
+
Raw SVG files under `assets/<weight>/<name>.svg` for six weights (`thin`,
|
|
278
|
+
`light`, `regular`, `bold`, `fill`, `duotone`), 256×256 viewBox,
|
|
279
|
+
fill-based. Because the default fill matches, the raw file renders legible
|
|
280
|
+
black glyphs; the wrapper is only needed for colour. `duotone`'s per-path
|
|
281
|
+
opacity survives either way.
|
|
282
|
+
|
|
283
|
+
```jsx
|
|
284
|
+
import fs from 'node:fs';
|
|
285
|
+
import { iconSource } from './icon-source.js';
|
|
286
|
+
|
|
287
|
+
const phosphor = (name, weight = 'regular') => {
|
|
288
|
+
const file = weight === 'regular' ? name : `${name}-${weight}`;
|
|
289
|
+
return fs.readFileSync(
|
|
290
|
+
new URL(
|
|
291
|
+
import.meta.resolve(`@phosphor-icons/core/assets/${weight}/${file}.svg`),
|
|
292
|
+
),
|
|
293
|
+
'utf8',
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const Icon = ({ name, weight, size = 24, color = '#000' }) => (
|
|
298
|
+
<svg
|
|
299
|
+
source={iconSource(phosphor(name, weight), color)}
|
|
300
|
+
style={{ width: size }}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// <Icon name="house" color="#0ea5e9" />
|
|
305
|
+
// <Icon name="house" weight="fill" size={32} />
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- Non-`regular` weights use suffixed filenames
|
|
309
|
+
(`assets/bold/house-bold.svg`); the helper above handles it.
|
|
310
|
+
- `@iconify-json/ph` serves the same set with no adapter and no filesystem
|
|
311
|
+
access. Prefer it unless you want the files.
|
|
312
|
+
- `@phosphor-icons/react` throws ([register](../ecosystem.md#render-time)).
|
|
313
|
+
|
|
314
|
+
## `heroicons` (the asset package) {#heroicons-assets}
|
|
315
|
+
|
|
316
|
+
**Adapter — the [`iconSource` wrapper](#iconsource).** heroicons@2.2.0.
|
|
317
|
+
|
|
318
|
+
The Tailwind set as plain SVG files, no JS: `24/outline/*.svg`
|
|
319
|
+
(stroke-based), `24/solid/`, `20/solid/` and `16/solid/`.
|
|
320
|
+
|
|
321
|
+
```jsx
|
|
322
|
+
import fs from 'node:fs';
|
|
323
|
+
import { iconSource } from './icon-source.js';
|
|
324
|
+
|
|
325
|
+
const heroicon = (name, style = '24/outline') =>
|
|
326
|
+
fs.readFileSync(
|
|
327
|
+
new URL(import.meta.resolve(`heroicons/${style}/${name}.svg`)),
|
|
328
|
+
'utf8',
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const Icon = ({ name, style, size = 24, color = '#000' }) => (
|
|
332
|
+
<svg
|
|
333
|
+
source={iconSource(heroicon(name, style), color)}
|
|
334
|
+
style={{ width: size }}
|
|
335
|
+
/>
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// <Icon name="home" color="#f59e0b" />
|
|
339
|
+
// <Icon name="home" style="24/solid" color="#0ea5e9" />
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Use this rather than `@heroicons/react` when you need to recolour. There is
|
|
343
|
+
no JS index, so enumerate the directory for a picker. **Prefer
|
|
344
|
+
`@iconify-json/heroicons`** through the [Iconify](#iconify) pipeline — same
|
|
345
|
+
art, no adapter, no filesystem access — unless you want the files.
|
|
346
|
+
|
|
347
|
+
## `feather-icons` {#feather-icons}
|
|
348
|
+
|
|
349
|
+
**Adapter, ~5 lines.** feather-icons@4.29.2.
|
|
350
|
+
|
|
351
|
+
The classic 24×24 stroke set, 287 icons. `.contents` is the inner SVG
|
|
352
|
+
markup without a root element, so the adapter is a template literal: build
|
|
353
|
+
the root and the `<g>` yourself and fact 1 never applies.
|
|
354
|
+
|
|
355
|
+
```jsx
|
|
356
|
+
import feather from 'feather-icons';
|
|
357
|
+
|
|
358
|
+
function featherSvg(name, color = '#000', strokeWidth = 2) {
|
|
359
|
+
return `<svg viewBox="0 0 24 24"><g fill="none" stroke="${color}"
|
|
360
|
+
stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round"
|
|
361
|
+
>${feather.icons[name].contents}</g></svg>`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const Icon = ({ name, size = 24, color, strokeWidth }) => (
|
|
365
|
+
<svg source={featherSvg(name, color, strokeWidth)} style={{ width: size }} />
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// <Icon name="alert-circle" color="#f59e0b" />
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- **Prefer Lucide**, Feather's actively developed fork with five times the
|
|
372
|
+
coverage and the same look; Feather has had no new icons in years. Use it
|
|
373
|
+
only to match an existing design.
|
|
374
|
+
- `feather.icons.x.toSvg()` into `<svg source>` renders wrong: `toSvg()`
|
|
375
|
+
puts every paint attribute, and `toSvg({ color })`'s `color`, on the root,
|
|
376
|
+
where they are ignored. Always go through `.contents`.
|
|
377
|
+
- `feather.replace()` (the DOM `<i data-feather>` swapper) is browser-only.
|