react-x11 2.51.1 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +9 -2
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -0,0 +1,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.