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