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.
Files changed (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. 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.