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,540 @@
|
|
|
1
|
+
# Theming, palettes and colour
|
|
2
|
+
|
|
3
|
+
react-x11's theme is a plain object of hex strings and numbers — see
|
|
4
|
+
[components](../components.md#theming) for the token list and
|
|
5
|
+
[styling](../styling.md#theme-tokens) for how `$token` resolves. So every
|
|
6
|
+
palette package on this page is used the same way: pick keys at
|
|
7
|
+
theme-build time, hand the result to `<ThemeProvider>` and `<window theme>`,
|
|
8
|
+
and the renderer never knows the package exists.
|
|
9
|
+
|
|
10
|
+
One rule governs the whole page: **ntk's colour parser accepts hex, `rgb()`,
|
|
11
|
+
`rgba()`, `hsl()` and CSS colour names, and returns null for anything else —
|
|
12
|
+
and a null colour paints nothing, silently.** Modern token sets are
|
|
13
|
+
increasingly `oklch()`-first, so conversion is not optional. That is the
|
|
14
|
+
single sharpest edge here.
|
|
15
|
+
|
|
16
|
+
CSS-in-JS is not on this page at all. `styled-components` and
|
|
17
|
+
`@emotion/styled` both fail, in interestingly different ways, and are written
|
|
18
|
+
up in the [negative-results register](../ecosystem.md#what-does-not-work).
|
|
19
|
+
Use `createStyles` plus one of the palettes below.
|
|
20
|
+
|
|
21
|
+
## `@radix-ui/colors` {#radix-colors}
|
|
22
|
+
|
|
23
|
+
**Out of the box.** @radix-ui/colors@3.0.0.
|
|
24
|
+
|
|
25
|
+
Thirty named 12-step colour scales (plus dark, alpha and P3 variants) shipped
|
|
26
|
+
as plain JS objects of hex strings — no React, no DOM, no CSS. Each step has
|
|
27
|
+
a documented job: 1–2 app backgrounds, 3–5 component backgrounds, 6–8
|
|
28
|
+
borders, 9–10 solid fills, 11–12 text. That semantic contract is what makes
|
|
29
|
+
it a theme _generator_ rather than a swatch book.
|
|
30
|
+
|
|
31
|
+
Because every scale has identical structure, one function produces any
|
|
32
|
+
light/dark pairing.
|
|
33
|
+
|
|
34
|
+
Radix's ladder and the token set line up almost step for step: 1–2 are the
|
|
35
|
+
`background`, 3–5 are the `surface` and its hover, 6–8 the `border`, 9–10 the
|
|
36
|
+
solid fills, 11–12 the ink.
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
import {
|
|
40
|
+
slate,
|
|
41
|
+
slateDark,
|
|
42
|
+
grass,
|
|
43
|
+
grassDark,
|
|
44
|
+
red,
|
|
45
|
+
redDark,
|
|
46
|
+
green,
|
|
47
|
+
greenDark,
|
|
48
|
+
amber,
|
|
49
|
+
amberDark,
|
|
50
|
+
blue,
|
|
51
|
+
blueDark,
|
|
52
|
+
} from '@radix-ui/colors';
|
|
53
|
+
import { ThemeProvider, Button } from 'react-x11';
|
|
54
|
+
|
|
55
|
+
// step 11 is the readable-on-its-own-background text step of every scale,
|
|
56
|
+
// which is exactly what a status colour has to be here: ink as well as fill
|
|
57
|
+
const ink = (scale) => Object.values(scale)[10];
|
|
58
|
+
|
|
59
|
+
function radixTheme({ gray, accent, status }) {
|
|
60
|
+
const g = Object.values(gray);
|
|
61
|
+
const a = Object.values(accent);
|
|
62
|
+
return {
|
|
63
|
+
background: g[0],
|
|
64
|
+
surface: g[2],
|
|
65
|
+
text: g[11],
|
|
66
|
+
textMuted: g[10],
|
|
67
|
+
border: g[6],
|
|
68
|
+
borderFocus: a[7],
|
|
69
|
+
accent: a[8],
|
|
70
|
+
accentHover: a[9],
|
|
71
|
+
hoverBackground: a[8],
|
|
72
|
+
surfaceHover: g[3],
|
|
73
|
+
track: g[5],
|
|
74
|
+
danger: ink(status.red),
|
|
75
|
+
dangerHover: Object.values(status.red)[11],
|
|
76
|
+
success: ink(status.green),
|
|
77
|
+
warning: ink(status.amber),
|
|
78
|
+
info: ink(status.blue),
|
|
79
|
+
radius: 6,
|
|
80
|
+
radiusSmall: 4,
|
|
81
|
+
borderWidth: 1,
|
|
82
|
+
fontSize: 14,
|
|
83
|
+
paddingX: 16,
|
|
84
|
+
paddingY: 10,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const status = { red, green, amber, blue };
|
|
89
|
+
const statusDark = {
|
|
90
|
+
red: redDark,
|
|
91
|
+
green: greenDark,
|
|
92
|
+
amber: amberDark,
|
|
93
|
+
blue: blueDark,
|
|
94
|
+
};
|
|
95
|
+
const light = radixTheme({ gray: slate, accent: grass, status });
|
|
96
|
+
const dark = radixTheme({
|
|
97
|
+
gray: slateDark,
|
|
98
|
+
accent: grassDark,
|
|
99
|
+
status: statusDark,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
<ThemeProvider value={light}>
|
|
103
|
+
<window>
|
|
104
|
+
<Button primary>OK</Button>
|
|
105
|
+
</window>
|
|
106
|
+
</ThemeProvider>;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- The dark scales are _separate exports_ (`slateDark`, not `slate.dark`).
|
|
110
|
+
- `Object.values()` ordering relies on the package's insertion order
|
|
111
|
+
(`blue1`…`blue12`). It holds in 3.0.0, but indexing by key
|
|
112
|
+
(``accent[`${name}9`]``) is sturdier if the scale name is handy.
|
|
113
|
+
- Nothing here names `accentText`, `hoverText` or any of the `…Text` four:
|
|
114
|
+
each is derived from the fill it goes on as the more legible of this
|
|
115
|
+
palette's `text` and `background`. That is what used to bite here — steps 9
|
|
116
|
+
and 10 of yellow, lime, amber, sky and mint are light enough that a
|
|
117
|
+
hard-coded `'#ffffff'` label fails contrast on them, and now the ratio
|
|
118
|
+
decides. Name one to overrule it.
|
|
119
|
+
- The window needs no `backgroundColor`: with no style of its own it paints
|
|
120
|
+
`background`, which is the ground this palette just named.
|
|
121
|
+
- The alpha variants (`slateA`) are `#rrggbbaa`, which parses fine, but
|
|
122
|
+
remember a translucent widget composites against the window background, not
|
|
123
|
+
against whatever is behind the window.
|
|
124
|
+
- The P3 variants (`blueP3`) are `color(display-p3 …)` strings. The parser
|
|
125
|
+
returns null and nothing is painted. Stick to the sRGB exports.
|
|
126
|
+
|
|
127
|
+
## `@catppuccin/palette` {#catppuccin}
|
|
128
|
+
|
|
129
|
+
**Out of the box.** @catppuccin/palette@1.8.0.
|
|
130
|
+
|
|
131
|
+
Four flavours (latte is light; frappé, macchiato and mocha are dark), each
|
|
132
|
+
with 14 accents and a 12-step neutral ladder whose names _are_ UI roles —
|
|
133
|
+
`base`/`mantle`/`crust` for backgrounds, `surface0-2`, `overlay0-2`,
|
|
134
|
+
`subtext0-1`, `text`. Of all the palette packages this one maps most directly
|
|
135
|
+
onto the theme shape.
|
|
136
|
+
|
|
137
|
+
`flavor.dark` decides whether accent-coloured fills get dark or light text.
|
|
138
|
+
Catppuccin accents are pastel, so on dark flavours the readable text on an
|
|
139
|
+
accent is _dark_, not white.
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
import { flavors } from '@catppuccin/palette';
|
|
143
|
+
import { ThemeProvider, Button } from 'react-x11';
|
|
144
|
+
|
|
145
|
+
function catppuccinTheme(flavor, accent = 'mauve') {
|
|
146
|
+
const c = Object.fromEntries(
|
|
147
|
+
Object.entries(flavor.colors).map(([k, v]) => [k, v.hex]),
|
|
148
|
+
);
|
|
149
|
+
const onAccent = flavor.dark ? c.crust : c.base;
|
|
150
|
+
return {
|
|
151
|
+
background: c.base,
|
|
152
|
+
surface: c.surface0,
|
|
153
|
+
text: c.text,
|
|
154
|
+
textMuted: c.subtext0,
|
|
155
|
+
border: c.surface1,
|
|
156
|
+
borderFocus: c[accent],
|
|
157
|
+
accent: c[accent],
|
|
158
|
+
accentHover: c.lavender,
|
|
159
|
+
accentText: onAccent,
|
|
160
|
+
hoverBackground: c[accent],
|
|
161
|
+
hoverText: onAccent,
|
|
162
|
+
surfaceHover: c.surface1,
|
|
163
|
+
track: c.surface2,
|
|
164
|
+
// the four are named colours here, which is the whole appeal of this
|
|
165
|
+
// package: `red` means the same thing in every flavour
|
|
166
|
+
danger: c.red,
|
|
167
|
+
dangerHover: c.maroon,
|
|
168
|
+
success: c.green,
|
|
169
|
+
warning: c.yellow,
|
|
170
|
+
info: c.sapphire,
|
|
171
|
+
radius: 6,
|
|
172
|
+
radiusSmall: 4,
|
|
173
|
+
borderWidth: 1,
|
|
174
|
+
fontSize: 14,
|
|
175
|
+
paddingX: 16,
|
|
176
|
+
paddingY: 10,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const mocha = catppuccinTheme(flavors.mocha);
|
|
181
|
+
const latte = catppuccinTheme(flavors.latte, 'blue');
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- `accentHover` has no canonical Catppuccin answer — the palette defines flat
|
|
185
|
+
accents, not interaction ramps. Picking a neighbouring accent, as above, is
|
|
186
|
+
a taste call; deriving a real hover shade is a one-liner with
|
|
187
|
+
[colord](#colord) or [culori](#culori).
|
|
188
|
+
- The package is ESM-first with an `exports` map, so un-exported subpaths
|
|
189
|
+
fail. The root import is all you need.
|
|
190
|
+
- Latte's accents sit around 4:1 contrast on `base` — fine for fills,
|
|
191
|
+
marginal for small accent-coloured _text_. Same caveat as every pastel
|
|
192
|
+
scheme.
|
|
193
|
+
|
|
194
|
+
## `@material/material-color-utilities` {#material-color-utilities}
|
|
195
|
+
|
|
196
|
+
**Out of the box from a source colour.**
|
|
197
|
+
@material/material-color-utilities@0.4.0.
|
|
198
|
+
|
|
199
|
+
Google's reference implementation of the Material 3 colour system: the HCT
|
|
200
|
+
colour space, tonal palettes, and `themeFromSourceColor(argb)`, which turns a
|
|
201
|
+
single seed colour into complete light and dark schemes. It is the only
|
|
202
|
+
package here that _generates_ a coherent scheme from one input, which is
|
|
203
|
+
exactly the shape `<window theme>` wants.
|
|
204
|
+
|
|
205
|
+
M3 roles map nearly 1:1 onto the theme shape: `background → background`,
|
|
206
|
+
`surface → surface`, `onSurface → text`, `outlineVariant → border`,
|
|
207
|
+
`primary → accent`, `onPrimary → accentText` — and `error`, which is the one
|
|
208
|
+
status colour M3 defines, is `danger`.
|
|
209
|
+
|
|
210
|
+
```jsx
|
|
211
|
+
import {
|
|
212
|
+
themeFromSourceColor,
|
|
213
|
+
argbFromHex,
|
|
214
|
+
hexFromArgb,
|
|
215
|
+
} from '@material/material-color-utilities';
|
|
216
|
+
import { ThemeProvider, Button } from 'react-x11';
|
|
217
|
+
|
|
218
|
+
function materialTheme(scheme) {
|
|
219
|
+
const c = Object.fromEntries(
|
|
220
|
+
Object.entries(scheme.toJSON()).map(([k, v]) => [k, hexFromArgb(v)]),
|
|
221
|
+
);
|
|
222
|
+
return {
|
|
223
|
+
background: c.background,
|
|
224
|
+
surface: c.surface,
|
|
225
|
+
text: c.onSurface,
|
|
226
|
+
textMuted: c.onSurfaceVariant,
|
|
227
|
+
border: c.outlineVariant,
|
|
228
|
+
borderFocus: c.primary,
|
|
229
|
+
accent: c.primary,
|
|
230
|
+
accentHover: c.inversePrimary,
|
|
231
|
+
accentText: c.onPrimary,
|
|
232
|
+
hoverBackground: c.primary,
|
|
233
|
+
hoverText: c.onPrimary,
|
|
234
|
+
surfaceHover: c.surfaceVariant,
|
|
235
|
+
danger: c.error,
|
|
236
|
+
dangerText: c.onError,
|
|
237
|
+
track: c.outlineVariant,
|
|
238
|
+
radius: 8,
|
|
239
|
+
radiusSmall: 4,
|
|
240
|
+
borderWidth: 1,
|
|
241
|
+
fontSize: 14,
|
|
242
|
+
paddingX: 16,
|
|
243
|
+
paddingY: 10,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const m3 = themeFromSourceColor(argbFromHex('#6750A4'));
|
|
248
|
+
const light = materialTheme(m3.schemes.light); // accent #6750a4
|
|
249
|
+
const dark = materialTheme(m3.schemes.dark); // accent #cfbcff
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- **The published ESM uses extensionless relative imports**, so plain `node`
|
|
253
|
+
fails with `ERR_MODULE_NOT_FOUND`. Under tsx — which is how react-x11 JSX
|
|
254
|
+
runs anyway — or any bundler, it resolves fine.
|
|
255
|
+
- `themeFromImage`/`sourceColorFromImage` call `document.createElement('canvas')`
|
|
256
|
+
and are dead here. `sourceColorFromImageBytes(rgbaBytes)` is exported and
|
|
257
|
+
pure: decode a PNG or JPEG yourself with pngjs or jpeg-js and hand it the
|
|
258
|
+
RGBA array. Downscale first — the Celebi quantizer walks every opaque
|
|
259
|
+
pixel, and it skips pixels with alpha below 255.
|
|
260
|
+
- `accentHover` has no M3 role. `inversePrimary` is serviceable; a tone step
|
|
261
|
+
via `theme.palettes.primary.tone(n)` is more faithful.
|
|
262
|
+
- 0.4.0 has sat unchanged for a while. The upstream repo is active but npm
|
|
263
|
+
releases are slow — pin it.
|
|
264
|
+
|
|
265
|
+
## colord {#colord}
|
|
266
|
+
|
|
267
|
+
**Out of the box.** colord@2.9.3.
|
|
268
|
+
|
|
269
|
+
A tiny immutable colour-manipulation library: parse, `darken`/`lighten`/
|
|
270
|
+
`alpha`/`mix`, `isDark()`, output as hex/rgb/hsl. The `a11y` plugin adds WCAG
|
|
271
|
+
`contrast()` and `isReadable()`.
|
|
272
|
+
|
|
273
|
+
The theme shape wants colours no palette ships — `accentHover`,
|
|
274
|
+
`surfaceHover`, a `dangerHover`. colord answers each in a line, which turns
|
|
275
|
+
a single brand colour into a complete theme without hand-tuning.
|
|
276
|
+
|
|
277
|
+
The `…Text` inks are the one thing you no longer need it for: `resolveTheme`
|
|
278
|
+
already picks the more legible of the palette's own `text` and `background`
|
|
279
|
+
for every fill that has one. The version below is still worth reading as the
|
|
280
|
+
explicit form — and it is what to write when the two candidates the palette
|
|
281
|
+
happens to hold are not the two you want.
|
|
282
|
+
|
|
283
|
+
```jsx
|
|
284
|
+
import { colord, extend } from 'colord';
|
|
285
|
+
import a11yPlugin from 'colord/plugins/a11y';
|
|
286
|
+
extend([a11yPlugin]);
|
|
287
|
+
|
|
288
|
+
/** One brand colour -> the interactive slice of a react-x11 theme. */
|
|
289
|
+
function accentSlice(accent) {
|
|
290
|
+
const c = colord(accent);
|
|
291
|
+
const onAccent = c.contrast('#ffffff') >= 4.5 ? '#ffffff' : '#1b1b1b';
|
|
292
|
+
return {
|
|
293
|
+
accent,
|
|
294
|
+
accentHover: c.darken(0.06).toHex(),
|
|
295
|
+
borderFocus: accent,
|
|
296
|
+
hoverBackground: accent,
|
|
297
|
+
accentText: onAccent,
|
|
298
|
+
hoverText: onAccent,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const theme = { ...baseTheme, ...accentSlice('#1f883d') };
|
|
303
|
+
// colord('#1f883d').contrast('#ffffff') === 4.51 -> white text, just barely
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- Plugins must be `extend`-ed before use; forgetting yields
|
|
307
|
+
`c.contrast is not a function` at theme-build time.
|
|
308
|
+
- `darken()` works in HSL, which shifts saturated hues perceptibly. For a 6%
|
|
309
|
+
hover nudge nobody notices; for accent _ramps_ that must stay on-hue, use
|
|
310
|
+
culori's OKLCH interpolation.
|
|
311
|
+
- Maintenance is quiet (2.9.3 since 2023) but the library is complete and
|
|
312
|
+
dependency-free. Low risk.
|
|
313
|
+
|
|
314
|
+
## culori {#culori}
|
|
315
|
+
|
|
316
|
+
**Out of the box.** culori@4.0.2.
|
|
317
|
+
|
|
318
|
+
Parses every CSS Color 4 syntax, converts between about thirty colour spaces
|
|
319
|
+
(OKLCH and OKLab included), interpolates in any of them, builds scales, and
|
|
320
|
+
computes WCAG contrast. Two distinct jobs here:
|
|
321
|
+
|
|
322
|
+
1. **Gateway for modern colour strings.** Growing amounts of design-token
|
|
323
|
+
output — Tailwind v4, Open Props' OKLCH variants, design systems
|
|
324
|
+
exporting CSS Color 4 — are `oklch()`-first, and ntk's parser returns null
|
|
325
|
+
for those. `formatHex(str)` is the one-call fix.
|
|
326
|
+
2. **Perceptual ramps.** Hover, active and disabled shades interpolated in
|
|
327
|
+
OKLCH keep hue and perceived lightness honest where HSL manipulation
|
|
328
|
+
drifts.
|
|
329
|
+
|
|
330
|
+
```jsx
|
|
331
|
+
import { formatHex, interpolate, wcagContrast } from 'culori';
|
|
332
|
+
|
|
333
|
+
// A perceptual ramp from one accent: 0 = accent, higher = lighter.
|
|
334
|
+
const ramp = interpolate(['#0969da', '#ffffff'], 'oklch');
|
|
335
|
+
const accent = '#0969da';
|
|
336
|
+
|
|
337
|
+
const theme = {
|
|
338
|
+
...baseTheme,
|
|
339
|
+
accent,
|
|
340
|
+
accentHover: formatHex(ramp(0.15)), // #3d81e2 — stays on-hue
|
|
341
|
+
borderFocus: accent,
|
|
342
|
+
track: formatHex(ramp(0.75)),
|
|
343
|
+
accentText: wcagContrast(accent, '#fff') >= 4.5 ? '#ffffff' : '#1b1b1b',
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Gateway use: any CSS Color 4 string -> something ntk can paint.
|
|
347
|
+
formatHex('oklch(54.6% 0.245 262.881)'); // '#155dfc'
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
- It is the heavyweight of the pair. Prefer colord for "darken this a bit",
|
|
351
|
+
culori when you need OKLCH, CSS Color 4 parsing, or scales. Using both is
|
|
352
|
+
fine — neither touches the render path.
|
|
353
|
+
- `formatHex` clips out-of-sRGB-gamut results channelwise; for scales from
|
|
354
|
+
vivid P3-ish seeds, `clampChroma` first gives cleaner ramps.
|
|
355
|
+
- Dual-published: ESM source plus a bundled CJS. The `/fn` and `/css`
|
|
356
|
+
subpaths for tree-shaken builds are ESM-only.
|
|
357
|
+
|
|
358
|
+
## `open-props` {#open-props}
|
|
359
|
+
|
|
360
|
+
**Out of the box for colours.** open-props@1.7.23.
|
|
361
|
+
|
|
362
|
+
Open Props is a CSS-custom-properties design system, but it does have a JS
|
|
363
|
+
export: the root import resolves to a flat frozen object of about 1,830
|
|
364
|
+
entries keyed by custom-property name (`'--gray-0'`, `'--indigo-7'`), values
|
|
365
|
+
as strings. No DOM, no dependencies.
|
|
366
|
+
|
|
367
|
+
Only the colour half is worth having here, so **prefer
|
|
368
|
+
[`@radix-ui/colors`](#radix-colors)** if all you want is a palette — it is
|
|
369
|
+
hex throughout, has documented step semantics, and ships separate dark
|
|
370
|
+
scales. Open Props earns its place when a project already uses it and you
|
|
371
|
+
want the same swatches on both sides.
|
|
372
|
+
|
|
373
|
+
The colour tokens — 13 hues × 13 steps — are plain hex, so building a theme
|
|
374
|
+
is pure key-picking. The step convention is roughly: 0–2 backgrounds, 3–5
|
|
375
|
+
borders and muted, 6–9 solids, 10–12 deep text.
|
|
376
|
+
|
|
377
|
+
```jsx
|
|
378
|
+
import OP from 'open-props';
|
|
379
|
+
|
|
380
|
+
const rem = (s) => Math.round(parseFloat(s) * 16); // '1.1rem' -> 18
|
|
381
|
+
|
|
382
|
+
const theme = {
|
|
383
|
+
background: OP['--gray-0'],
|
|
384
|
+
surface: OP['--gray-1'],
|
|
385
|
+
text: OP['--gray-9'],
|
|
386
|
+
textMuted: OP['--gray-6'],
|
|
387
|
+
border: OP['--gray-3'],
|
|
388
|
+
borderFocus: OP['--indigo-7'],
|
|
389
|
+
accent: OP['--indigo-7'],
|
|
390
|
+
accentHover: OP['--indigo-8'],
|
|
391
|
+
accentText: '#ffffff',
|
|
392
|
+
hoverBackground: OP['--indigo-7'],
|
|
393
|
+
hoverText: '#ffffff',
|
|
394
|
+
surfaceHover: OP['--gray-2'],
|
|
395
|
+
track: OP['--gray-3'],
|
|
396
|
+
danger: OP['--red-8'],
|
|
397
|
+
dangerHover: OP['--red-9'],
|
|
398
|
+
success: OP['--green-8'],
|
|
399
|
+
warning: OP['--yellow-9'],
|
|
400
|
+
info: OP['--blue-8'],
|
|
401
|
+
radius: parseInt(OP['--radius-2']),
|
|
402
|
+
radiusSmall: parseInt(OP['--radius-1']),
|
|
403
|
+
borderWidth: 1,
|
|
404
|
+
fontSize: rem(OP['--font-size-1']),
|
|
405
|
+
paddingX: 16,
|
|
406
|
+
paddingY: 10,
|
|
407
|
+
};
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
- **The sizes read in a style; the palette's own entries are numbers.**
|
|
411
|
+
Open Props sizes are `rem` strings, which a style takes as written —
|
|
412
|
+
`padding: OP['--size-3']` is `1rem` — and `rem` here is the palette's
|
|
413
|
+
`fontSize`, which the theme above sets to Open Props' own 16, so the scale
|
|
414
|
+
comes out as it does on the web. The palette entries are another matter:
|
|
415
|
+
the widgets do arithmetic with `radius` and `fontSize`, so those are
|
|
416
|
+
converted to numbers, as above. Easings are `cubic-bezier()` strings where
|
|
417
|
+
react-x11 takes named easings, and shadows reference `var(--shadow-color)`
|
|
418
|
+
and have no equivalent at all; ignore the animation and shadow tokens.
|
|
419
|
+
- Key names include the `--` prefix, so it is bracket access everywhere;
|
|
420
|
+
camelCase aliases exist too (`OP.gray0`).
|
|
421
|
+
- Unlike Radix there are no separate dark scales — a dark theme is your own
|
|
422
|
+
key-picking (`--gray-12` background, `--gray-9` borders).
|
|
423
|
+
|
|
424
|
+
## `tailwindcss/colors` {#tailwind-colors}
|
|
425
|
+
|
|
426
|
+
**Adapter, ~3 lines — and the failure without it is silent.**
|
|
427
|
+
tailwindcss@4.3.3.
|
|
428
|
+
|
|
429
|
+
Tailwind-the-framework is a non-starter here: it emits CSS class utilities
|
|
430
|
+
for a CSSOM, and react-x11's style channel is a validated object system. The
|
|
431
|
+
_palette_ is a different story — 22 named scales × 11 steps, importable as
|
|
432
|
+
plain JS from `tailwindcss/colors` without touching any of the CSS
|
|
433
|
+
machinery, and it is the palette half your users already think in. The
|
|
434
|
+
package has zero dependencies, so installing it purely for the colours costs
|
|
435
|
+
about 770 KB unpacked and pulls in nothing else.
|
|
436
|
+
|
|
437
|
+
Since v4 every value is an `oklch()` string. ntk's parser returns null for
|
|
438
|
+
those, and a null colour paints **nothing** — no error, just an invisible
|
|
439
|
+
widget. Convert at theme-build time:
|
|
440
|
+
|
|
441
|
+
```jsx
|
|
442
|
+
import colors from 'tailwindcss/colors';
|
|
443
|
+
import { formatHex } from 'culori';
|
|
444
|
+
|
|
445
|
+
// The whole adapter: one call per value.
|
|
446
|
+
const hex = (c) => formatHex(c); // 'oklch(54.6% …)' -> '#155dfc'
|
|
447
|
+
|
|
448
|
+
const theme = {
|
|
449
|
+
background: hex(colors.slate[50]),
|
|
450
|
+
surface: hex(colors.slate[100]),
|
|
451
|
+
text: hex(colors.slate[900]),
|
|
452
|
+
textMuted: hex(colors.slate[500]),
|
|
453
|
+
border: hex(colors.slate[300]),
|
|
454
|
+
borderFocus: hex(colors.blue[600]),
|
|
455
|
+
accent: hex(colors.blue[600]),
|
|
456
|
+
accentHover: hex(colors.blue[700]),
|
|
457
|
+
accentText: '#ffffff',
|
|
458
|
+
hoverBackground: hex(colors.blue[600]),
|
|
459
|
+
hoverText: '#ffffff',
|
|
460
|
+
surfaceHover: hex(colors.slate[200]),
|
|
461
|
+
track: hex(colors.slate[300]),
|
|
462
|
+
danger: hex(colors.red[600]),
|
|
463
|
+
dangerHover: hex(colors.red[700]),
|
|
464
|
+
success: hex(colors.green[700]),
|
|
465
|
+
warning: hex(colors.amber[700]),
|
|
466
|
+
info: hex(colors.blue[700]),
|
|
467
|
+
radius: 6,
|
|
468
|
+
radiusSmall: 4,
|
|
469
|
+
borderWidth: 1,
|
|
470
|
+
fontSize: 14,
|
|
471
|
+
paddingX: 16,
|
|
472
|
+
paddingY: 10,
|
|
473
|
+
};
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
- **Do not pass the raw values through.** This is the sharpest edge on the
|
|
477
|
+
page; everything else here is smooth.
|
|
478
|
+
- `formatHex` gamut-clips, but Tailwind's vivid 500s are chosen to survive
|
|
479
|
+
sRGB conversion, so in practice the clipped hex closely matches what
|
|
480
|
+
Tailwind v3 shipped.
|
|
481
|
+
- Installing a whole CSS framework for a data file is aesthetically odd; if
|
|
482
|
+
that grates, `@radix-ui/colors` does the same job natively in hex. Zero
|
|
483
|
+
dependencies means it is only odd, not costly.
|
|
484
|
+
|
|
485
|
+
## `daltonize` {#daltonize}
|
|
486
|
+
|
|
487
|
+
**Adapter, ~4 lines. Niche — reach for it only if you are shipping an
|
|
488
|
+
explicit colour-vision variant; for everyday contrast work
|
|
489
|
+
[colord](#colord)'s `a11y` plugin is the tool.** daltonize@1.0.2.
|
|
490
|
+
|
|
491
|
+
A single-function package: `daltonize([r, g, b], type)` where `type` is
|
|
492
|
+
`'protanope' | 'deuteranope' | 'tritanope'`, returning a corrected triplet.
|
|
493
|
+
It implements the classic LMS-space daltonization algorithm — simulate the
|
|
494
|
+
deficiency, take the error against the original, redistribute it into
|
|
495
|
+
channels the viewer _can_ see. Pure arithmetic, 9 KB, zero dependencies;
|
|
496
|
+
despite the README's image-oriented pitch, nothing in it touches a canvas.
|
|
497
|
+
|
|
498
|
+
Themes are small closed sets of colours, which makes whole-theme
|
|
499
|
+
daltonization trivial: map the correction over every value and offer it as an
|
|
500
|
+
accessibility variant next to light and dark. The adapter is only hex ↔
|
|
501
|
+
triplet plumbing.
|
|
502
|
+
|
|
503
|
+
```jsx
|
|
504
|
+
import { daltonize } from 'daltonize';
|
|
505
|
+
import { colord } from 'colord';
|
|
506
|
+
|
|
507
|
+
const toRgb = (hex) => {
|
|
508
|
+
const { r, g, b } = colord(hex).toRgb();
|
|
509
|
+
return [r, g, b];
|
|
510
|
+
};
|
|
511
|
+
const toHex = (rgb) => colord({ r: rgb[0], g: rgb[1], b: rgb[2] }).toHex();
|
|
512
|
+
|
|
513
|
+
/** Colourblind-corrected variant of any react-x11 theme. */
|
|
514
|
+
function daltonizeTheme(theme, type = 'deuteranope') {
|
|
515
|
+
return Object.fromEntries(
|
|
516
|
+
Object.entries(theme).map(([k, v]) => [
|
|
517
|
+
k,
|
|
518
|
+
typeof v === 'string' && v.startsWith('#')
|
|
519
|
+
? toHex(daltonize(toRgb(v), type))
|
|
520
|
+
: v, // radius, fontSize, … pass through
|
|
521
|
+
]),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const accessible = daltonizeTheme(githubLight);
|
|
526
|
+
// '#1f883d' (GitHub green) -> '#1f7206' for deuteranopia
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
- **Correction, not validation.** It shifts colours so information carried by
|
|
530
|
+
problem hues survives; it does not check or fix contrast. A low-contrast
|
|
531
|
+
theme stays low-contrast. Pair it with colord's `a11y` plugin or culori's
|
|
532
|
+
`wcagContrast`.
|
|
533
|
+
- Only the three dichromatic extremes are modelled — anomalous trichromacy,
|
|
534
|
+
which is the common mild case, is not. Corrections can look aggressive to a
|
|
535
|
+
mildly affected viewer, so offer it as an opt-in variant rather than a
|
|
536
|
+
default.
|
|
537
|
+
- Handles opaque triplets only. Run it on the RGB part and reattach alpha
|
|
538
|
+
yourself if the theme uses `#rrggbbaa`.
|
|
539
|
+
- The package is small and finished rather than maintained (last release 2023) and its `repository` field is a placeholder. Vendoring the ~40 lines
|
|
540
|
+
is a fair alternative to the dependency.
|