@nxgt/material 3.7.5 → 3.8.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 (176) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +39 -6
  3. package/bin/nxgt-theme.js +6 -0
  4. package/dist/{async-slice-o_fpqeGX.js → async-slice-DDjaJY_F.js} +2 -2
  5. package/dist/background--Bf_EbFR.js +159 -0
  6. package/dist/backgrounds/backgrounds.css +2 -0
  7. package/dist/backgrounds/css-react/moon.css +97 -0
  8. package/dist/backgrounds.js +3 -139
  9. package/dist/browser-only-DQ94lWF8.js +18 -0
  10. package/dist/bubble-input-B7BVd9fX.js +237 -0
  11. package/dist/{button--cF4gY14.js → button-DbfdITBB.js} +51 -57
  12. package/dist/charts.js +3 -3
  13. package/dist/{chip-D1B1dL77.js → chip-DpySRqCy.js} +2 -2
  14. package/dist/{client-only.utils-lQ7GqP8w.js → client-only.utils-3nHNzCki.js} +1 -1
  15. package/dist/{code-editor-0CdGwKDg.js → code-editor-BiA14xua.js} +7 -7
  16. package/dist/code-editor.js +6 -6
  17. package/dist/{code-language-items-CGARRbVm.js → code-language-items-C_dJ6GVs.js} +2 -2
  18. package/dist/components/theme-tokens.css +34 -35
  19. package/dist/components/tones/app-tones.css +2 -1
  20. package/dist/components/tones/containers.css +4 -1
  21. package/dist/components/tones/index.css +2 -0
  22. package/dist/components/tones/surface.css +1 -0
  23. package/dist/components/tones/tokens.css +8 -3
  24. package/dist/components/tones/zone-paint.css +15 -0
  25. package/dist/components/ui/feedback/toast/toast-tokens.css +1 -0
  26. package/dist/components/ui/layout/app-shell/activity-backdrop.css +26 -0
  27. package/dist/components/ui/layout/sidebar/sidebar-tokens.css +5 -2
  28. package/dist/components/ui/media/media-player/nxgt-media-player.css +7 -32
  29. package/dist/components/ui/primitives/field-box/field-box-tokens.css +5 -2
  30. package/dist/{components-CDrQCOsU.js → components-B9EIfMdd.js} +18622 -18531
  31. package/dist/components.js +24 -22
  32. package/dist/{dialog-DdJ1C4BR.js → dialog-CkIYpJXk.js} +55 -53
  33. package/dist/dist-B7RjY7SL.js +158 -0
  34. package/dist/dist-C4wtXCjf.js +593 -0
  35. package/dist/dist-DVRQkaTK.js +111 -0
  36. package/dist/dist-lmHmz_bu.js +419 -0
  37. package/dist/dnd.js +1 -1
  38. package/dist/dropdown-menu-BC5Tbhb9.js +908 -0
  39. package/dist/editor.js +527 -527
  40. package/dist/empty-state-s2CSjKXJ.js +126 -0
  41. package/dist/{escape-owner-B8k7DvFa.js → escape-owner-CripBce_.js} +2 -1
  42. package/dist/{field-blur-DFUazxsL.js → field-blur-DPLsMufs.js} +1 -1
  43. package/dist/{field-blur-context-Dw9ZAgBr.js → field-blur-context-1ZwJgsHE.js} +2 -2
  44. package/dist/has-text-content-Ds2dkHX_.js +10 -0
  45. package/dist/hooks.js +35 -33
  46. package/dist/{hover-card-D5iHS9q1.js → hover-card-Cox-tPDz.js} +41 -39
  47. package/dist/{i18n-provider-C6SBK_nO.js → i18n-provider-C6HhBw34.js} +1 -1
  48. package/dist/i18n.js +6 -6
  49. package/dist/{icon-badge-Bb5-73XP.js → icon-badge-q3Ve8gra.js} +3 -3
  50. package/dist/{icon-button-5A6Mrj6w.js → icon-button-DJcWpIhu.js} +2 -1
  51. package/dist/{in-dialog-AiwTD3DK.js → in-dialog-Vwkyp-M5.js} +6 -5
  52. package/dist/lib/backgrounds/background/available.d.ts +3 -3
  53. package/dist/lib/backgrounds/background/background.d.ts +7 -3
  54. package/dist/lib/backgrounds/background/types.d.ts +7 -1
  55. package/dist/lib/backgrounds/index.d.ts +1 -1
  56. package/dist/lib/components/ui/layout/app-shell/activity-chrome.d.ts +17 -0
  57. package/dist/lib/components/ui/layout/app-shell/activity-layout.d.ts +6 -4
  58. package/dist/lib/components/ui/layout/app-shell/backdrop-layer.d.ts +11 -0
  59. package/dist/lib/components/ui/layout/app-shell/keep-shapes-inside.d.ts +17 -0
  60. package/dist/lib/components/ui/layout/app-shell/theme-backdrop.d.ts +6 -0
  61. package/dist/lib/components/ui/layout/app-shell/use-activity-backdrop.d.ts +18 -0
  62. package/dist/lib/components/ui/layout/app-shell/use-keep-shapes-inside.d.ts +9 -0
  63. package/dist/lib/hooks/resolve-theme.d.ts +46 -1
  64. package/dist/lib/hooks/theme-store.d.ts +35 -4
  65. package/dist/lib/hooks/use-theme.d.ts +20 -4
  66. package/dist/lib/i18n/resources/en/en.d.ts +4 -0
  67. package/dist/lib/i18n/resources/en/material.json.d.ts +4 -0
  68. package/dist/lib/i18n/resources/fr/fr.d.ts +4 -0
  69. package/dist/lib/i18n/resources/fr/material.json.d.ts +4 -0
  70. package/dist/lib/i18n/resources/resources.d.ts +8 -0
  71. package/dist/lib/i18n/translator.d.ts +1 -1
  72. package/dist/lib/theme-picker/index.d.ts +7 -0
  73. package/dist/lib/theme-picker/partials/mini-background.d.ts +10 -0
  74. package/dist/lib/theme-picker/partials/mode-control.d.ts +5 -0
  75. package/dist/lib/theme-picker/partials/theme-tile.d.ts +12 -0
  76. package/dist/lib/theme-picker/theme-label.d.ts +7 -0
  77. package/dist/lib/theme-picker/theme-picker.d.ts +25 -0
  78. package/dist/lib/themes/index.d.ts +14 -0
  79. package/dist/lib/themes/nxgt-theme-provider.d.ts +29 -0
  80. package/dist/lib/themes/nxgt-theme.d.ts +22 -0
  81. package/dist/lib/themes/theme-context.d.ts +28 -0
  82. package/dist/lib/themes/theme-value.d.ts +5 -0
  83. package/dist/lib/vite/index.d.ts +1 -1
  84. package/dist/lib.js +3 -3
  85. package/dist/main.js +43 -39
  86. package/dist/map.js +8 -8
  87. package/dist/material.css +1 -1
  88. package/dist/nxgt-theme-CgSb9Obl.js +37 -0
  89. package/dist/pdf-reader.js +1 -1
  90. package/dist/print.js +2 -2
  91. package/dist/{public-SsiazLEv.js → public-Ce30uvp0.js} +591 -587
  92. package/dist/read-storage-C2J90bNm.js +10 -0
  93. package/dist/{rich-code-block-CwNwQs5z.js → rich-code-block-C2Fp_FDU.js} +5 -5
  94. package/dist/{sortable-provider-BvD98s8Y.js → sortable-provider-pVaEZzty.js} +3 -3
  95. package/dist/{spinner-CtGCPza-.js → spinner-CsHcmcpF.js} +2 -2
  96. package/dist/{stored-language-Bb7yX5uE.js → stored-language-BeIKw3-K.js} +15 -15
  97. package/dist/styles.css +8 -1
  98. package/dist/theme-context-BagPvRcz.js +16 -0
  99. package/dist/theme-picker.js +118 -0
  100. package/dist/theme-store-CSl51uNH.js +91 -0
  101. package/dist/theme-toggle-C8os82b4.js +48 -0
  102. package/dist/themes.js +42 -0
  103. package/dist/toggle-group-j8zaxEg1.js +69 -0
  104. package/dist/{toolbar-items-DsDDD5AY.js → toolbar-items-CqHvlTU8.js} +443 -589
  105. package/dist/tooltip-rhbqNmr3.js +1064 -0
  106. package/dist/{translate-material-Bsjc4Tow.js → translate-material-CGZljjp1.js} +2 -2
  107. package/dist/{translator-or2y0JJB.js → translator-DCYO4Ij3.js} +3 -3
  108. package/dist/{use-background-activity-B8jLHNop.js → use-background-activity-KxQgelFF.js} +1 -1
  109. package/dist/{use-chat-composer-DZ01a16D.js → use-chat-composer-CDrgWGMM.js} +4684 -4886
  110. package/dist/{use-heading-spy-ByOQNnFZ.js → use-heading-spy-DXo6IOX9.js} +52 -121
  111. package/dist/{use-material-translation-hLQzOjF0.js → use-material-translation-DkxcLbLM.js} +2 -2
  112. package/dist/use-slice-reducer-BGkeGtML.js +42 -0
  113. package/dist/use-theme-Cd-aiDJo.js +31 -0
  114. package/docs/README.md +2 -1
  115. package/docs/components/foundations.md +39 -0
  116. package/docs/components/navigation-and-layout.md +3 -3
  117. package/docs/getting-started.md +1 -1
  118. package/docs/guides/app-shell.md +41 -6
  119. package/docs/guides/backgrounds.md +43 -7
  120. package/docs/guides/i18n.md +1 -1
  121. package/docs/guides/themes.md +103 -0
  122. package/docs/guides/theming.md +288 -84
  123. package/docs/guides/vite-plugin.md +26 -0
  124. package/docs/previews/backgrounds-background-from-theme.webp +0 -0
  125. package/docs/previews/backgrounds-geometry-more.webp +0 -0
  126. package/docs/previews/backgrounds-geometry.webp +0 -0
  127. package/docs/previews/backgrounds-illustrated-scenes.webp +0 -0
  128. package/docs/previews/backgrounds-light-more.webp +0 -0
  129. package/docs/previews/backgrounds-light.webp +0 -0
  130. package/docs/previews/backgrounds-material-more.webp +0 -0
  131. package/docs/previews/backgrounds-matrix.webp +0 -0
  132. package/docs/previews/backgrounds-moon.webp +0 -0
  133. package/docs/previews/backgrounds-motif-shapes.webp +0 -0
  134. package/docs/previews/backgrounds-motifs.webp +0 -0
  135. package/docs/previews/foundations-themes-themepicker.webp +0 -0
  136. package/docs/roadmap.md +25 -0
  137. package/docs/troubleshooting.md +35 -7
  138. package/lib/backgrounds/backgrounds.css +2 -0
  139. package/lib/backgrounds/css-react/moon.css +97 -0
  140. package/lib/components/theme-tokens.css +34 -35
  141. package/lib/components/tones/app-tones.css +2 -1
  142. package/lib/components/tones/containers.css +4 -1
  143. package/lib/components/tones/index.css +2 -0
  144. package/lib/components/tones/surface.css +1 -0
  145. package/lib/components/tones/tokens.css +8 -3
  146. package/lib/components/tones/zone-paint.css +15 -0
  147. package/lib/components/ui/feedback/toast/toast-tokens.css +1 -0
  148. package/lib/components/ui/layout/app-shell/activity-backdrop.css +26 -0
  149. package/lib/components/ui/layout/sidebar/sidebar-tokens.css +5 -2
  150. package/lib/components/ui/media/media-player/nxgt-media-player.css +7 -32
  151. package/lib/components/ui/primitives/field-box/field-box-tokens.css +5 -2
  152. package/lib/styles.css +8 -1
  153. package/lib/theme-picker/theme-picker-list.css +29 -0
  154. package/lib/theme-picker/theme-picker.css +192 -0
  155. package/lib/theme-picker/theme-tile-background.css +77 -0
  156. package/lib/theme-picker/theme-tile-textures-night.css +63 -0
  157. package/lib/theme-picker/theme-tile-textures.css +153 -0
  158. package/lib/themes/ambre.css +134 -0
  159. package/lib/themes/ardoise.css +133 -0
  160. package/lib/themes/bleumono.css +136 -0
  161. package/lib/themes/bluemoon.css +136 -0
  162. package/lib/themes/corail.css +136 -0
  163. package/lib/themes/foret.css +133 -0
  164. package/lib/themes/lavande.css +133 -0
  165. package/lib/themes/menthe.css +136 -0
  166. package/lib/themes/ocean.css +136 -0
  167. package/lib/themes/pink.css +134 -0
  168. package/lib/themes/prune.css +133 -0
  169. package/lib/themes/sable.css +133 -0
  170. package/lib/themes/slate.css +133 -0
  171. package/lib/themes/teal.css +137 -0
  172. package/package.json +30 -2
  173. package/dist/dropdown-menu-DWtgUVW7.js +0 -1490
  174. package/dist/empty-state-CRWLQ1sM.js +0 -191
  175. package/dist/tooltip-tmFoTU_d.js +0 -1582
  176. package/dist/use-slice-reducer-0r5dXnJe.js +0 -58
@@ -37,8 +37,10 @@ at the top are needed is in [getting started](../getting-started.md#styles).
37
37
  `lib/components/`);
38
38
  - the **tokens**: CSS variables on `:root`, and their dark values on `.dark`
39
39
  and `[data-theme="dark"]`;
40
- - a `@theme` block that maps each token to a Tailwind colour, so `bg-primary`,
41
- `text-error-foreground`, `border-border` are real utilities;
40
+ - an `@theme inline` block that maps each token to a Tailwind colour, so
41
+ `bg-primary`, `text-error-foreground`, `border-border` are real utilities
42
+ that read `var(--primary)`… where they are used (a wrapper or a theme zone
43
+ that sets the token is followed);
42
44
  - the **background presets' base, gradient and motion CSS** (`lib/backgrounds/css/`),
43
45
  which `Background` from `/components` draws with: plain CSS keyed on
44
46
  `data-background`, in `@layer components`, so a utility on the element wins.
@@ -65,7 +67,7 @@ at the top are needed is in [getting started](../getting-started.md#styles).
65
67
  | `--nxgt-accent` | `[--nxgt-accent:…]` | the mark colour of the selection controls: the fill, handle, state layer and stop dots of `Slider`, the on state of `Switch`, the box and dot of `Checkbox` and `Radio`. It defaults to `--primary` (`--primary-text` in dark) and is set per control, so override it on the control itself, not on `:root` |
66
68
  | `--error`, `--success`, `--info`, `--warning` (+ `-foreground`) | `bg-error`, `text-success`, … | the status colours of `Button`, `Alert`, `Badge`, toasts |
67
69
  | `--primary-text` | `text-primary-text` | primary drawn as text or an icon on a page, card or popover surface: `Typography tone="primary"`, links, the `Alert` primary icon, selected labels. Equal to `--primary` in light; in dark, `--primary` mixed 60 % with white (`color-mix(in oklab, var(--primary) 40%, white)`), well over 4.5:1 on `--background` and `--card` |
68
- | `--primary-indicator` | `bg-primary-indicator`, `border-primary-indicator`, `ring-primary-indicator`, `stroke-primary-indicator` | primary drawn as a non-text mark that carries a state (WCAG 1.4.11, 3:1): an active bar or dot, a selected or focused border, a ring, a spinner arc. Equal to `--primary` in light; in dark, `--primary` mixed 25 % with white (`color-mix(in oklab, var(--primary) 75%, white)`), 4.5:1 on `--background`, 4.1:1 on `--card`, 3.7:1 on `--muted` (`--primary` itself gives 2.5, 2.3 and 2.1:1) |
70
+ | `--primary-indicator` | `bg-primary-indicator`, `border-primary-indicator`, `ring-primary-indicator`, `stroke-primary-indicator` | primary drawn as a non-text mark that carries a state (WCAG 1.4.11, 3:1): an active bar or dot, a selected or focused border, a ring, a spinner arc. Equal to `--primary` in light; in dark, `--primary` mixed 25 % with white (`color-mix(in oklab, var(--primary) 75%, white)`), 4.8:1 on `--background`, 4.5:1 on `--card`, 4.2:1 on `--muted` (`--primary` itself gives 2.7, 2.5 and 2.3:1) |
69
71
  | `--error-text` | `text-error-text` | error drawn as text: field messages, `HelperText` in its error state, form and dialog errors, `variant="error"` menu items |
70
72
  | `--destructive` | `bg-destructive`, `text-destructive` | deprecated alias of `--error`: the light theme sets it to `var(--error)`, the dark theme keeps its own value; use `--error` |
71
73
  | `--card`, `--popover`, `--tooltip` (+ `-foreground`) | `bg-card`, `bg-popover`, `bg-tooltip` | surfaces |
@@ -93,17 +95,22 @@ Put `text-foreground` on a container; it stays well above 4.5:1 in both themes.
93
95
 
94
96
  ## The neutral palette
95
97
 
96
- The neutrals of both themes are softly tinted toward violet (hue 290, low
97
- chroma), to sit with the primary indigo instead of fighting it. The values:
98
+ The default theme's page is plain: pure white in light and the one dark night
99
+ every theme shares (`oklch(0.15 0.026 273)`), whatever the colours. Its other
100
+ neutrals (cards in dark, muted, borders, text) are softly tinted toward violet
101
+ (hue 290, low chroma), to sit with the primary indigo instead of fighting it.
102
+ A theme can tint its page too with `neutrals: 'tinted'` in `defineTheme`
103
+ (`@nxgt/material-core` theme guide), which is how the page looked before 3.8
104
+ (`oklch(0.973 0.008 293.9)` and `oklch(0.192 0.016 290.5)` for Lavande). The values:
98
105
 
99
106
  | Token | Light | Dark |
100
107
  | --- | --- | --- |
101
- | `--background` | `oklch(0.973 0.008 293.9)` | `oklch(0.192 0.016 290.5)` |
102
- | `--card`, `--popover` / `--sidebar` | white / `oklch(0.985 0.006 293.9)` | `oklch(0.232 0.019 289.4)` |
108
+ | `--background` | `oklch(1 0 0)` | `oklch(0.15 0.026 273)` |
109
+ | `--card`, `--popover` / `--sidebar` | white / `oklch(0.985 0.006 293.9)` | `oklch(0.19 0.019 289.4)` |
103
110
  | `--foreground` | `oklch(0.227 0.017 290)` | `oklch(0.943 0.012 291.3)` |
104
- | `--muted`, `--accent` | `oklch(0.955 0.012 292)`, `oklch(0.91 0.016 293)` | `oklch(0.27 0.022 290)` |
111
+ | `--muted`, `--accent` | `oklch(0.955 0.012 292)`, `oklch(0.91 0.016 293)` | `oklch(0.228 0.022 290)`, `oklch(0.25 0.022 290)` |
105
112
  | `--muted-foreground` | `oklch(0.478 0.032 292)` | `oklch(0.725 0.03 292.2)` |
106
- | `--border`, `--input` | `oklch(0.912 0.016 293.7)` | `oklch(0.301 0.025 291.4)`, `oklch(0.34 0.027 291.4)` |
113
+ | `--border`, `--input` | `oklch(0.912 0.016 293.7)` | `oklch(0.259 0.025 291.4)`, `oklch(0.298 0.027 291.4)` |
107
114
 
108
115
  Earlier versions used a blue-slate neutral (hue 247 to 266; the dark
109
116
  background `oklch(0.14 0.036 258.53)` was a deep navy). To bring it back,
@@ -246,7 +253,7 @@ the element overrides them.
246
253
  | `--nxgt-tint-hover`, `-selected`, `-strong` | `color-mix(in oklab, var(--primary) var(--nxgt-tint-hover), transparent)` | the 8 %, 12 % and 18 % tints of hover, selected and strong states. For your own components: no component of the library reads them |
247
254
  | `--nxgt-tint-container` | | the share of the tone in a container: 20 % in light, 45 % in dark (of the dark base below; 24 % of the tone's text colour in a browser without relative colour syntax) |
248
255
  | `--nxgt-tone-container-from` | | where a tone container is mixed from, the dark rule stated once: 100 % mixes from the tone itself, 0 % from the tone held at a container's lightness (the dark base, next row). It is 100 % in light and 0 % in dark, where the raw tone is too deep (the indigo primary) or too pale (an M3 dark primary, `#cfbcff`) to show over the surfaces. It applies to the channel's container, the opaque `--<tone>-container` tokens and the status `Chip` alike. Set it under `.dark, [data-theme="dark"]` (the dark selector of `tones/tokens.css`) to bring the dark containers back to the raw tone: `.dark, [data-theme="dark"] { --nxgt-tone-container-from: 100%; }` |
249
- | `--nxgt-tone-container-min`, `-max` | | the dark base: the tone with its lightness held between these two (`0.35` and `0.4`, on `:root`), its hue and chroma kept, `oklch(from <tone> clamp(min, l, max) c h)`. 45 % of it over the surface gives a container of lightness 0.25 to 0.32 that shows the tone's colour. In the default theme the text of every named tone is 5.2:1 or more on it, an app tone's 5.7:1 or more, on the page and on a card; with an M3 dark palette (`#141218` page, `#211f26` card), 4.9:1 or more. Without relative colour syntax (before Chrome 119, Safari 18, Firefox 128) the base is the tone's text colour at 24 %, the earlier rule |
256
+ | `--nxgt-tone-container-min`, `-max` | | the dark base: the tone with its lightness held between these two (`0.35` and `0.4`, on `:root`), its hue and chroma kept, `oklch(from <tone> clamp(min, l, max) c h)`. 45 % of it over the surface gives a container of lightness 0.25 to 0.32 that shows the tone's colour. In the default theme the text of every named tone is 4.5:1 or more on it, and so is an app tone's, on the page and on a card; with an M3 dark palette (`#141218` page, `#211f26` card), 4.9:1 or more. Without relative colour syntax (before Chrome 119, Safari 18, Firefox 128) the base is the tone's text colour at 24 %, the earlier rule |
250
257
  | `--nxgt-tone-container-base` | `color-mix(in oklab, var(--nxgt-tone-container-base) 45%, <surface>)` | what the channel's container mixes from, set on every element whose `data-tone` is a named tone or yours (not the library's other `data-tone` values, which inherit it): the tone in light, the dark base in dark. Read it for a tint of your own that should match the containers |
251
258
  | `--nxgt-tint-status` | the tonal `Chip` with a `color` (`color-mix(in oklab, <tone> var(--nxgt-tint-status), <surface>)`) | the share of the tone in a status: 20 % in light, 45 % in dark (of the dark base there, as above; 24 % of the text colour without relative colour syntax). Separate from `--nxgt-tint-container`, so changing that one does not change chips. Override it under `:root` and `.dark, [data-theme="dark"]` like the other tokens |
252
259
  | `--nxgt-calm-hides`, `--nxgt-expressive-hides` | `var(--nxgt-calm-hides, <tone>) var(--nxgt-expressive-hides, <surface>)` | the flags of the [calm zone](#calm-zone): set by the zone (`data-density`), empty or invalid, never by hand, because setting one inverts the behaviour. Your own component reads them to be the tone outside a zone and the surface inside one |
@@ -679,19 +686,23 @@ Redefine the variables **after** the import, for light and dark separately:
679
686
  }
680
687
  ```
681
688
 
682
- Override the variable, not the `@theme` entry: `--color-primary` is defined as
683
- `var(--primary)`, so one change reaches both modes and every derived colour.
689
+ Override the variable, not the `@theme` entry (for a brand palette, prefer a
690
+ [theme](#from-token-overrides-to-a-theme)): the utilities read `var(--primary)`
691
+ where they are used, so one change reaches both modes and every derived colour.
684
692
 
685
- For a single area of the page, set the `--color-*` variables on a wrapper
686
- (the `@theme` entries are resolved on the root, so setting `--primary` on an
687
- element below it changes nothing):
693
+ For a single area of the page, set the token on a wrapper. Utilities inside it
694
+ follow, since they read the token where they sit (before 3.8 they read a
695
+ `--color-*` computed on the root, and the wrapper had to set that one; setting
696
+ `--color-primary` on a wrapper no longer changes `bg-primary`). Derived tokens
697
+ such as `--primary-text` are computed where they are declared, so for a full
698
+ recolour use a [theme zone](#themes), which re-declares them:
688
699
 
689
700
  ```tsx
690
701
  import { Button } from '@nxgt/material/components';
691
702
 
692
703
  export function DangerZone() {
693
704
  return (
694
- <section style={{ ['--color-primary' as string]: 'var(--error)' }}>
705
+ <section style={{ ['--primary' as string]: 'var(--error)' }}>
695
706
  <Button>Delete workspace</Button>
696
707
  </section>
697
708
  );
@@ -787,20 +798,20 @@ the rest yourself. Without the class the anchor keeps the prose style.
787
798
  **Contrast.** The shipped light palette keeps the text of every filled status
788
799
  colour at 4.5:1 or more (WCAG 1.4.3): `--secondary` 6.2:1, `--success` 6.1:1,
789
800
  `--info` 6.3:1 and `--warning` 5.5:1 against their `-foreground`, and
790
- `--secondary` as text (a `tonal`, `outlined` or `link` `Button`) 6.0:1 on
791
- `--background`, 4.8:1 on its own 15 % `tonal` tint.
801
+ `--secondary` as text (a `tonal`, `outlined` or `link` `Button`) 6.5:1 on
802
+ `--background`, 5.2:1 on its own 15 % `tonal` tint.
792
803
 
793
804
  The dark theme keeps brighter values, read as text on the dark surfaces: the
794
- dark `--tertiary` (`oklch(0.7 0.15 310.75)`) is 6.5:1 on `--background` and
795
- 5.9:1 on `--card`, with a dark `--tertiary-foreground` that keeps 6.5:1 on
805
+ dark `--tertiary` (`oklch(0.7 0.15 310.75)`) is 6.9:1 on `--background` and
806
+ 6.5:1 on `--card`, with a dark `--tertiary-foreground` that keeps 6.9:1 on
796
807
  a filled tertiary surface (light: 7.6:1 under near-white). The dark
797
- `--secondary` (`oklch(0.675 0.1135 245.65)`) is 6.3:1 on `--background`
798
- and 5.8:1 on `--card` (a `tonal` secondary `Button` 5.1:1 and 4.6:1). Under
808
+ `--secondary` (`oklch(0.675 0.1135 245.65)`) is 6.8:1 on `--background`
809
+ and 6.4:1 on `--card` (a `tonal` secondary `Button` 5.6:1 and 5.1:1). Under
799
810
  near-white text those brights are too light, so every filled surface built on
800
811
  them is tinted in the dark theme: a filled `secondary` `Button`, `IconButton`
801
- or `Badge` at 70 % (4.9 to 5.2:1 on `--background`, 4.7 to 4.9:1 on `--card`), a filled
802
- `success`, `info` or `warning` one at 60 % (7.1:1, 5.6:1 and 5.8:1 on
803
- `--background`; 6.8:1, 5.3:1 and 5.5:1 on `--card`; the `warning` `Button` 5.9:1). Their hover, focus and
812
+ or `Badge` at 70 % (5.1 to 5.4:1 on `--background`, 4.9:1 or more on `--card`), a filled
813
+ `success`, `info` or `warning` one at 60 % (7.6:1, 5.8:1 and 6.1:1 on
814
+ `--background`; 7.3:1, 5.5:1 and 5.8:1 on `--card`; the `warning` `Button` 4.5:1 or more). Their hover, focus and
804
815
  pressed states are tinted further down, so they only gain contrast.
805
816
 
806
817
  In the light theme the filled `primary`, `secondary`, `success`, `info`,
@@ -816,11 +827,11 @@ The `tonal`, `outlined` and `link` variants of `Button` and `IconButton` draw
816
827
  their text and icon in one tone per colour, read on `--background`, on
817
828
  `--card` and on the variant's own tint (15 % at most, at rest or focused). In
818
829
  the light theme `primary`, `secondary`, `info` and `success` use the token
819
- itself (5.4, 4.8, 4.9 and 4.7:1 at their lowest) and `warning` and `error` the
820
- token mixed with 15 % black (6.0 and 5.0:1). In the dark theme `primary` is
821
- mixed with 60 % white, the `--primary-text` token (7.7:1 at its lowest: the token itself is a dark indigo,
822
- 2.2:1), `success` and `warning` with 15 % white (4.8 and 5.8:1), `error` with
823
- 30 % white (5.5:1), and `secondary` and `info` use the token (4.6 and 5.0:1).
830
+ itself (5.8, 5.2, 5.2 and 5.0:1 at their lowest) and `warning` and `error` the
831
+ token mixed with 15 % black (6.4 and 5.4:1). In the dark theme `primary` is
832
+ mixed with 60 % white, the `--primary-text` token (8.5:1 at its lowest: the token itself is a dark indigo,
833
+ 2.3:1), `success` and `warning` with 15 % white (5.3 and 6.5:1), `error` with
834
+ 30 % white (6.0:1), and `secondary` and `info` use the token (5.1 and 5.6:1).
824
835
  The tones are mixed from the tokens (`color-mix(in oklab, var(--color-error)
825
836
  85%, black)`), so an override of a token moves its tone with it; a text colour
826
837
  of your own on one of those buttons (`className="text-primary-text"`) replaces the tone
@@ -829,7 +840,7 @@ and is yours to check.
829
840
  A `Badge` used as a link (`asChild` around an `<a>`) darkens on hover like a filled
830
841
  button (mixed with 10 % black; in the dark theme its tint drops by ten
831
842
  points) rather than fading toward the page. A selected `ListTile` draws its
832
- subtitle in `text-foreground/70` (7.0:1 on the selected tint, where the muted
843
+ subtitle in `text-foreground/70` (7.5:1 on the selected tint, where the muted
833
844
  colour gave 4.4:1). The light `--error` is `oklch(0.56 0.245 27.325)`, a shade
834
845
  darker than the dark theme's: a filled `error` button's near-white text reaches
835
846
  4.8:1 (it was 4.6:1). The dark `--error` keeps `0.577`: a lighter value drops
@@ -839,32 +850,32 @@ the filled surface's near-white text below 4.5:1 (`0.62` gives 3.85:1).
839
850
  the light theme, 70 in the dark one), so a warning `Alert`, `Banner`, `Badge`
840
851
  or toast does not read as a failure. Light `oklch(0.52 0.13 62)` is 5.5:1
841
852
  under its near-white `-foreground`; dark `oklch(0.7 0.15 70)` reads as text at
842
- 6.7:1 on `--background` and 6.1:1 on `--card` (5.9 and 5.3:1 on its 10 % tint). An app that overrides `--warning` keeps its own
853
+ 7.2:1 on `--background` and 6.7:1 on `--card` (6.4 and 5.9:1 on its 10 % tint). An app that overrides `--warning` keeps its own
843
854
  value; one that keeps the shipped value sees its `bg-warning` and
844
855
  `text-warning` surfaces change hue, and re-checks its custom pairs.
845
856
 
846
857
  **A selected `DataTable` row** (`TableRow`, `data-state="selected"`) is a 15 %
847
858
  primary tint with `foreground` text, a bar on the inline-start edge of its
848
859
  first cell (`before:`, primary in the light theme, primary mixed with 60 %
849
- white in the dark one: 5.4:1 in light and 7.7:1 or more in dark against the
850
- row), and the checkbox, drawn in the same dark colour (7.7:1 or more). The
851
- tint is too light to be told from its neighbour alone (1.25:1 in light, 1.10 to
852
- 1.11:1 in dark), which is what keeps the text at 4.5:1 or more on a page and on
853
- a `Card`: 12.6:1 in light, and in dark 14.1:1 on a page, 12.9:1 in a `DataTable`
860
+ white in the dark one: 5.8:1 in light and 8.5:1 or more in dark against the
861
+ row), and the checkbox, drawn in the same dark colour (8.5:1 or more). The
862
+ tint is too light to be told from its neighbour alone (1.26:1 in light, 1.09 to
863
+ 1.10:1 in dark), which is what keeps the text at 4.5:1 or more on a page and on
864
+ a `Card`: 13.6:1 in light, and in dark 15.3:1 on a page, 14.2:1 in a `DataTable`
854
865
  on a `Card` and for a bare `TableRow` on a `Card`. Links and
855
866
  `text-muted-foreground` texts (drawn as `foreground` at 70 %) are 5.6:1 in
856
- light and 7.0:1 in dark for a bare row on a `Card`. A `text-error-text` text
867
+ light and 7.5:1 in dark for a bare row on a `Card`. A `text-error-text` text
857
868
  keeps its colour. Do not
858
869
  put `text-primary-foreground` in a cell for a selected row: it is near-white
859
870
  on that tint.
860
871
 
861
872
  **Error text has a token of its own**, `--error-text` (`text-error-text`), so
862
873
  the fill can stay dark while the text is light enough to read. The light value
863
- is `color-mix(in oklab, var(--error) 92%, black)` (5.6:1 on `--background`,
874
+ is `color-mix(in oklab, var(--error) 92%, black)` (6.0:1 on `--background`,
864
875
  6.0:1 on `--card`, 5.0:1 on the `bg-error/10` tint of a focused menu item, where
865
876
  `--error` gives 5.0 and 4.2:1); the dark one is
866
- `color-mix(in oklab, var(--error) 76%, white)`, 5.85:1 on `--background` and
867
- 5.35:1 on `--card` (4.78:1 on a focused error menu item), where `--error` itself gives 3.9 and 3.5:1. Field messages,
877
+ `color-mix(in oklab, var(--error) 76%, white)`, 6.25:1 on `--background` and
878
+ 5.9:1 on `--card` (5.2:1 on a focused error menu item), where `--error` itself gives 4.0 and 3.8:1. Field messages,
868
879
  `HelperText` in its error state, form, dialog and upload errors, a textarea's
869
880
  counter over its limit, the error lines of `Terminal`, a
870
881
  `variant="error"` menu item and the falling values of the metric cards use it.
@@ -879,18 +890,18 @@ Which one to use:
879
890
  need 3:1 (WCAG 1.4.11), which `--error` reaches in both themes. Text on a
880
891
  filled error surface is `text-error-foreground`.
881
892
 
882
- `--error-text` is computed where it is declared, on the root, and `@theme`
883
- resolves `--color-error-text` there too: an override of `--error` under `:root`
884
- and `.dark, [data-theme="dark"]` moves it, but setting `--error-text` or
885
- `--error` on an element below the root changes nothing. Below the root, set
886
- `--color-error-text` (and `--color-error` for fills) on that element, or put
887
- the override on the root.
893
+ `--error-text` is computed where it is declared, on the root: an override of
894
+ `--error` under `:root` and `.dark, [data-theme="dark"]` moves it. Utilities
895
+ read the token where they are used, so setting `--error-text` itself on an
896
+ element below the root works, but setting only `--error` there does not move
897
+ the text colour (it stays the root's). Set both on that element, put the
898
+ override on the root, or use a [theme zone](#themes), which re-declares them.
888
899
 
889
900
  **Primary text has a token of its own**, `--primary-text` (`text-primary-text`),
890
901
  for the same reason: the brand colour is a dark fill (it keeps near-white text
891
902
  readable on a button) and reads at only about 2:1 as text on the dark surfaces.
892
903
  The light value is `color-mix(in oklab, var(--primary) 96%, black)` (primary
893
- 4 % toward black, 7.3:1 on the violet page); the dark one is
904
+ 4 % toward black, 7.9:1 on the white page); the dark one is
894
905
  `color-mix(in oklab, var(--primary) 40%, white)` (primary mixed 60 % with
895
906
  white, the recipe of the tonal `Button` labels). `Typography tone="primary"`,
896
907
  the `Alert` primary icon, links, selected labels and the other primary text of
@@ -904,7 +915,8 @@ the catalogue use it.
904
915
  Like `--error-text`, it is computed where it is declared, on the root: an
905
916
  override of `--primary` under `:root` and `.dark, [data-theme="dark"]` moves it
906
917
  (override `--primary-text` itself after it to pick another dark colour); below
907
- the root, set `--color-primary-text` on the element.
918
+ the root, set `--primary-text` on the element (setting only `--primary` there
919
+ does not move it; a [theme zone](#themes) does).
908
920
 
909
921
  Storybook's `Guides/Theming/Contrast` measures all of this in both themes:
910
922
  the surfaces' text, the error text (`HelperText` and a field's message on
@@ -915,33 +927,33 @@ at rest, hovered, focused and pressed, the tracks and the tabs;
915
927
  tokens after an override.
916
928
 
917
929
  **Tabs.** An `underline` tab's active label is `--primary` in the light
918
- theme (6.8:1 on `--background`, 7.3:1 on `--card`, 6.4:1 on `--muted`) and
919
- `--primary` mixed with 60 % white in the dark one (9.3:1, 8.5:1 and 7.6:1);
930
+ theme (7.3:1 on `--background`, 7.3:1 on `--card`, 6.4:1 on `--muted`) and
931
+ `--primary` mixed with 60 % white in the dark one (9.9:1, 9.3:1 and 8.6:1);
920
932
  its bar and every tab's focus outline are `--primary` in the light theme and
921
- `--track-fill` in the dark one (at least 6.7:1 and 9.4:1 on those surfaces).
933
+ `--track-fill` in the dark one (at least 6.4:1 and 10.5:1 on those surfaces).
922
934
  An inactive label is `--muted-foreground`, and turns to `--foreground` when
923
935
  hovered. A `segmented` tab's active label is `--foreground` on the raised
924
- surface (17.1:1 light, 14.2:1 dark); that surface is only 1.1:1 to 1.2:1 on
925
- the track, so it has a `--muted-foreground` border (5.8:1 light, 6.2:1 dark
936
+ surface (17.1:1 light, 15.6:1 dark); that surface is only 1.1:1 to 1.2:1 on
937
+ the track, so it has a `--muted-foreground` border (5.8:1 light, 7.0:1 dark
926
938
  on the track), and the label also changes colour and weight. Storybook's
927
939
  `Guides/Theming/Contrast` story `Tabs` measures each label at rest, hovered
928
940
  and focused, the outline, the bar and the border, on the three surfaces, in
929
941
  both themes.
930
942
 
931
- `--muted-foreground` is 6.2:1 on `--background`, 6.7:1 on `--card`, 5.7:1 on the
943
+ `--muted-foreground` is 6.7:1 on `--background`, 6.7:1 on `--card`, 6.2:1 on the
932
944
  "paper" tint (`bg-background-paper`, 5 % primary) and 5.8:1 on `--muted` in
933
- the light theme; in the dark theme it is 7.5:1 on `--background`, 6.9:1 on
934
- `--card`, 7.3:1 on the paper tint and 6.2:1 on `--muted`. The dark `--muted`
935
- is a dark surface (`oklch(0.27 0.022 290)`, it was the light value, a
936
- near-white block on a dark page) and `--foreground` on it reaches 12.8:1.
945
+ the light theme; in the dark theme it is 8.0:1 on `--background`, 7.6:1 on
946
+ `--card`, 7.8:1 on the paper tint and 7.0:1 on `--muted`. The dark `--muted`
947
+ is a dark surface (`oklch(0.228 0.022 290)`, it was the light value, a
948
+ near-white block on a dark page) and `--foreground` on it reaches 14.4:1.
937
949
 
938
950
  **Tracks.** The empty part of a `Slider`, of the password strength meter and
939
951
  the `Drawer` handle use `--track` (`bg-track`), and the `Slider`'s range and
940
952
  thumb border `--track-fill` (`bg-track-fill`, `border-track-fill`). In the
941
- dark theme the track reaches 3.5:1 on `--background` and 3.2:1 on `--card`
942
- (WCAG 1.4.11; `bg-muted`, a dark surface now, gave 1.4:1 and 1.2:1), and the
953
+ dark theme the track reaches 3.7:1 on `--background` and 3.5:1 on `--card`
954
+ (WCAG 1.4.11; `bg-muted`, a dark surface now, gives only 1.2:1 and 1.1:1), and the
943
955
  fill 3.3:1 on the track. The light theme keeps a pale track (`--track` is the
944
- light `--muted`) and `--track-fill` is `--primary`: the thumb, 6.8:1 on the
956
+ light `--muted`) and `--track-fill` is `--primary`: the thumb, 7.3:1 on the
945
957
  page, shows the value. A track, bar, handle or divider of your own drawn with
946
958
  `bg-muted` is near invisible in the dark theme now that `--muted` is a dark
947
959
  surface: draw it with `bg-track`.
@@ -1052,6 +1064,158 @@ are, without growing what is painted (nothing changes for a mouse):
1052
1064
  A toast covering a control is out of scope: it physically hides what is under
1053
1065
  it.
1054
1066
 
1067
+ ## Themes
1068
+
1069
+ Fourteen ready-made themes ship as stylesheets, each generated from a preset of
1070
+ `@nxgt/material-core/theme/presets` with every colour token in light and dark,
1071
+ the radius and the font: `lavande` (the default), `ocean`, `foret`, `ambre`,
1072
+ `ardoise`, `corail`, `menthe`, `prune`, `sable`, `slate`, `pink`, `bleumono`,
1073
+ `bluemoon` and `teal`.
1074
+
1075
+ ```css
1076
+ @import "tailwindcss";
1077
+ @import "@nxgt/material/styles.css";
1078
+ @import "@nxgt/material/themes/ocean.css"; /* after styles.css */
1079
+ ```
1080
+
1081
+ ```html
1082
+ <html data-nxgt-theme="ocean">
1083
+ ```
1084
+
1085
+ ```tsx
1086
+ <section data-nxgt-theme="ocean" data-theme="dark">
1087
+ {/* a dark Ocean zone, in a page of any theme */}
1088
+ </section>
1089
+ ```
1090
+
1091
+ A theme applies to the element that carries `data-nxgt-theme`: `<html>` for the
1092
+ whole page, or any element for a **zone** (a card in another theme, a preview).
1093
+ Nothing to change for an app that does not use one: `styles.css` keeps the
1094
+ Lavande tokens on `:root`.
1095
+
1096
+ - **Dark mode.** The theme is dark when the element has `.dark` or
1097
+ `data-theme="dark"`, or sits inside an element that has. A zone with
1098
+ `data-theme="dark"` is dark in a light page. A light zone inside a dark page
1099
+ is not supported.
1100
+ - **A zone paints its own text.** The shipped CSS gives `[data-nxgt-theme]`
1101
+ `color: var(--foreground)`, and a zone with a forced mode
1102
+ (`data-theme="dark"`, which is what `NxgtTheme mode="dark"` sets) also
1103
+ `background: var(--background)`, so a dark zone in a light page is dark with
1104
+ no class of yours. Both sit in `@layer base`: a utility on the zone
1105
+ (`text-primary`, `bg-card`) still wins.
1106
+ - **Zones are self-contained.** The theme file declares every token on the zone,
1107
+ and the derived ones (`--primary-text`, the tone texts and containers, the
1108
+ field box) are computed there, so a zone never shows its parent's colours.
1109
+ Utilities such as `bg-primary` read the token where they are used.
1110
+ - **Cascade.** The theme block beats `:root` by source order, so import it after
1111
+ `styles.css`. Once an app opts into a theme, its `:root { --primary: … }`
1112
+ override placed before the theme file no longer wins. Override on the same
1113
+ selector after the theme (`[data-nxgt-theme="ocean"] { --primary: … }`).
1114
+ - **Fonts.** The theme sets `font-family` on its own element; a font stack that
1115
+ is not installed falls back to the system face.
1116
+
1117
+ ### The provider and zones
1118
+
1119
+ `@nxgt/material/themes` holds the React side: the provider that keeps the chosen
1120
+ theme, and the zone.
1121
+
1122
+ ```tsx
1123
+ import { NxgtTheme, NxgtThemeProvider } from '@nxgt/material/themes';
1124
+
1125
+ <NxgtThemeProvider defaultTheme="ocean">
1126
+ <App />
1127
+ <NxgtTheme theme="prune" mode="dark" className="rounded p-4">
1128
+ {/* a dark Prune zone */}
1129
+ </NxgtTheme>
1130
+ </NxgtThemeProvider>
1131
+ ```
1132
+
1133
+ - **`NxgtThemeProvider`** (props `themes`, default `THEME_PRESETS`; `defaultTheme`, default `'lavande'`; `defaultMode`, default the active theme's `colorScheme`) goes at the root and renders no element. It holds the
1134
+ registry (`themes`, default the 14 presets; add your own `defineTheme`
1135
+ results), reads and writes the name stored under `nxgt-theme`
1136
+ (`useTheme().setThemeName`) and is **the one writer of `<html
1137
+ data-nxgt-theme>`** while it is mounted (without a provider the store
1138
+ writes it). A stored name the registry does not know resolves to
1139
+ `defaultTheme` (default `lavande`), and so does a key another tab clears;
1140
+ `setThemeName` of a name it does not know is ignored, with a warning in
1141
+ development. On the server and while hydrating the name is `defaultTheme`;
1142
+ the boot script has already put a stored name on `<html>`, and the provider
1143
+ leaves it alone until the stored name has been read. `useTheme().themeName`
1144
+ is the nearest provider's (or zone's) name, so it agrees with
1145
+ `useThemeContext()`; without either it is the stored name or `lavande`.
1146
+ - **`NxgtTheme`** is a zone: a `div` with `data-nxgt-theme` (and
1147
+ `data-theme="dark"` for `mode="dark"`; a `data-theme` you pass yourself is
1148
+ kept when `mode` is left out), whose children read that theme.
1149
+ `theme` is a `Theme` or the name of one the provider knows; a bare name with
1150
+ no provider has no registry, so its `Background` falls back to the defaults.
1151
+ `asChild` puts the attributes on your element. Import the theme's stylesheet
1152
+ yourself. Zones nest; a light zone inside a dark page is not supported.
1153
+ The nearest zone's tokens win, with one limit: in A > B > A, a `.dark` element
1154
+ *below* the inner A (not on it) takes Lavande's dark tokens, not A's. Put
1155
+ `mode="dark"` on the inner `NxgtTheme` instead.
1156
+ - **`useThemeContext()`** returns `{ name, theme, background, defaultMode }` for the nearest
1157
+ zone or provider, and `null` outside both.
1158
+ - **The background follows the theme.** A `Background` with no props, and
1159
+ `ActivityLayout`, draw the nearest theme's `background`; see
1160
+ [Defaults from the theme](./backgrounds.md#defaults-from-the-theme) and
1161
+ [The theme's background](./app-shell.md#the-themes-background).
1162
+ - **Nothing is added without it.** An app with no provider and no zone, or on
1163
+ Lavande, renders the same markup, apart from one attribute and the page
1164
+ background (white in light, the shared night in dark; `neutrals: 'tinted'`
1165
+ keeps a tinted page).
1166
+
1167
+ ### Let users choose
1168
+
1169
+ `ThemePicker` (`@nxgt/material/theme-picker`, its own entry so `/themes` stays small) is the chooser: a radiogroup of tiles, each previewing its own theme
1170
+ (swatches and a mini background), with the light / dark / system control. Mount it
1171
+ under the provider; it reads and writes the provider's name.
1172
+
1173
+ ```css
1174
+ @import "@nxgt/material/theme-picker.css"; /* after styles.css */
1175
+ ```
1176
+
1177
+ ```tsx
1178
+ import { ThemePicker } from '@nxgt/material/theme-picker';
1179
+
1180
+ <ThemePicker layout="list" showBackground={false} />;
1181
+ ```
1182
+
1183
+ The tiles' stylesheet is opt-in (about 3.3 KB gzipped), so an app that does not
1184
+ use the picker does not ship it; `styles.css` does not carry it.
1185
+
1186
+ Pass `themes` to choose among fewer or to add an app theme, `value` and
1187
+ `onValueChange` to hold the choice yourself, `showMode={false}` (default `true`) when the mode has
1188
+ its own control. Labels come from each theme's `label` and the
1189
+ `material.theme-picker.*` keys.
1190
+
1191
+ ### From token overrides to a theme
1192
+
1193
+ Before 3.8 an app recoloured itself by overriding `--color-*` or `--primary` on
1194
+ `:root`. Both still work on `:root`, but a **theme** is the way to customise:
1195
+ it solves a whole palette (light and dark, every derived colour) from one to
1196
+ three seed colours, checks it against AA, and applies to a page or a zone.
1197
+
1198
+ | You wrote | Write |
1199
+ | --- | --- |
1200
+ | `:root { --primary: …; --secondary: … }` and the matching `.dark` block | `defineTheme({ name: 'brand', colors: { primary: '#0b6bcb' } })`, generated by `nxgtMaterial({ themes })` ([Themes for your app](./themes.md)) |
1201
+ | a wrapper that sets `--color-primary` | `<NxgtTheme theme="brand">`, or `--primary` on the wrapper for a one-off |
1202
+ | `backdrop={false}` on `ActivityLayout` for the gradient | leave `backdrop` out (the theme's background); `false` is now a plain page |
1203
+
1204
+ A wrapper that overrides `--color-*` **no longer recolours utilities**:
1205
+ `bg-primary` reads `var(--primary)` where it is used. Set `--primary` on the
1206
+ wrapper (its derived tokens, such as `--primary-text`, are computed where they
1207
+ are declared, so they do not follow), or use a theme zone, which re-declares
1208
+ them. A `:root` override placed before an imported theme file stops winning
1209
+ (see Cascade above).
1210
+
1211
+ A third change comes with the same `@theme inline`: **`class="dark"`,
1212
+ `data-theme="dark"` or a token such as `--primary:` set on a sub-element (not
1213
+ `<html>`) now recolours the utilities below it.** Utilities used to read a
1214
+ colour computed once on `:root`, so such a wrapper changed nothing for
1215
+ `bg-primary` or `text-foreground`; now it does. Search your app for those three
1216
+ on a selector or element other than `<html>`/`:root`: a nested dark card or a
1217
+ scoped brand colour will now show.
1218
+
1055
1219
  ## Dark mode
1056
1220
 
1057
1221
  Dark values apply under a `.dark` class **or** a `data-theme="dark"`
@@ -1095,45 +1259,83 @@ function useTheme(): {
1095
1259
  theme: ResolvedTheme; // what is shown: 'system' resolved through prefers-color-scheme
1096
1260
  preference: ThemePreference; // what the user chose
1097
1261
  updateTheme(next: ThemePreference): void; // stores it and applies it to <html>
1262
+ themeName: ThemeName; // the nearest provider's or zone's; else the stored name, 'lavande' when none (and on the server)
1263
+ setThemeName(next: ThemeName): void; // stores it; sets <html data-nxgt-theme> unless a provider does
1098
1264
  };
1265
+
1266
+ // useTheme({ defaultMode }): the mode when none is stored. Default: the provider's
1267
+ // defaultMode, else the active theme's colorScheme, else 'system'.
1099
1268
  ```
1100
1269
 
1101
1270
  - The preference is stored in `localStorage` under `theme`, **JSON-encoded**
1102
1271
  (`"dark"`, with the quotes, as `react-use`'s `useLocalStorage` writes it).
1103
- Choosing `system` removes the key.
1104
- - `updateTheme` sets the `dark` class and `color-scheme` on `<html>`. Reading
1105
- `useTheme()` alone does not apply anything.
1272
+ Choosing `system` removes the key, unless a first visit would not be `system`
1273
+ (a theme suggests `dark`): then `"system"` is stored, or clearing it would
1274
+ bring the suggestion back.
1275
+ - **The first-visit mode.** A theme may suggest one (`colorScheme` in
1276
+ `defineTheme`: `bluemoon` suggests `dark`). With no stored mode, the boot
1277
+ script, `useTheme()` (so `theme`, `preference`, `CodeBlock` and `ThemeToggle`)
1278
+ all use it: under `NxgtThemeProvider` it is the active theme's, or the
1279
+ provider's `defaultMode`. With no provider, the stored name of a library
1280
+ preset still gives its `colorScheme` (`bluemoon`: dark); an app's own theme
1281
+ needs the provider, or `useTheme({ defaultMode })`. A zone never changes the
1282
+ page's mode. The class on `<html>` comes from the boot script before the
1283
+ first paint, and then from `NxgtThemeProvider`, which applies the resolved
1284
+ mode whenever the theme name or the mode changes (so `setThemeName('bluemoon')`
1285
+ turns the page dark with no `ThemeToggle` mounted); with no provider, a
1286
+ mounted `ThemeToggle` or `updateTheme` does.
1287
+ - The theme name is stored apart, under `nxgt-theme`, also JSON-encoded, so
1288
+ the mode and the name change independently. `setThemeName` sets
1289
+ `data-nxgt-theme` on `<html>` (through the provider, when there is one); Lavande, the default, takes the attribute off
1290
+ (the boot script never sets it for Lavande either), so an app that never
1291
+ calls it gets no attribute. A name that is not a well-formed theme name
1292
+ (`[a-z][a-z0-9-]*`) reads as `lavande`.
1293
+ - `ThemeName` is augmentable, like `AppTones`: an app adds its own themes with
1294
+ `declare module '@nxgt/material' { interface NxgtThemes { brand: true } }`.
1295
+ Out of the box it types the 14 presets; an app theme needs the same
1296
+ augmentation to pass `setThemeName`. `DEFAULT_THEME` (`'lavande'`) is exported too.
1297
+ - `resolveThemeName(stored, known, fallback)` and `applyThemeName(name)` are
1298
+ exported beside `resolveTheme` and `applyResolvedTheme`, for a bundled entry.
1106
1299
  - Every `useTheme()` call reads one shared store, so a component that reads
1107
1300
  `theme` (the code blocks and the code editor pick their highlighting theme
1108
1301
  this way) re-renders when any other component calls `updateTheme`. A change
1109
- made in another tab arrives through the `storage` event.
1302
+ made in another tab arrives through the `storage` event (for the name, it
1303
+ also sets `data-nxgt-theme` there).
1110
1304
  - On the server and during hydration the preference reads as `system` and
1111
1305
  `prefers-color-scheme` as light, so `theme` is `light`; the stored value and
1112
1306
  the real color scheme take over right after, without a hydration mismatch.
1307
+ The name reads as `lavande` the same way, and the attribute is only ever
1308
+ written on the client (by the boot script, or `setThemeName`), never rendered
1309
+ by the server: put `suppressHydrationWarning` on `<html>`.
1113
1310
 
1114
1311
  ### No flash on load: a boot script
1115
1312
 
1116
1313
  `useTheme` and `ThemeToggle` run after React mounts, so a stored `dark`
1117
- preference would first paint light. Apply it before the first paint with an
1118
- inline script in `<head>`. It is the same logic as `resolveTheme` +
1119
- `applyResolvedTheme`, written without imports because an inline script cannot
1120
- have any:
1314
+ preference or a stored theme name would first paint the default. Apply both
1315
+ before the first paint with an inline script in `<head>`. It sets the `dark`
1316
+ class and `color-scheme` from the mode (key `theme`) and `data-nxgt-theme`
1317
+ from the name (key `nxgt-theme`; Lavande sets no attribute), without imports,
1318
+ because an inline script cannot have any. `themeBootScript` from
1319
+ `@nxgt/material/themes` generates it, and takes the app's own theme names and
1320
+ defaults:
1121
1321
 
1122
- ```html
1123
- <script>
1124
- (function () {
1125
- var stored = null;
1126
- try { stored = JSON.parse(localStorage.getItem('theme') || 'null'); } catch (e) {}
1127
- var preference = stored === 'light' || stored === 'dark' ? stored : 'system';
1128
- var dark = preference === 'dark' ||
1129
- (preference === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
1130
- document.documentElement.classList.toggle('dark', dark);
1131
- document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
1132
- })();
1133
- </script>
1322
+ ```tsx
1323
+ import { themeBootScript } from '@nxgt/material/themes';
1324
+
1325
+ // <head>: add the nonce when the CSP uses one, or hash the script.
1326
+ <script dangerouslySetInnerHTML={{ __html: themeBootScript({ themes: ['lavande', 'ocean', 'brand'] }) }} />
1134
1327
  ```
1135
1328
 
1136
- Read the value with `JSON.parse`: `localStorage.getItem('theme')` returns
1329
+ The constant `THEME_BOOT_SCRIPT` is the same script for the 14 presets, with a
1330
+ stable CSP hash. A stored name the script does not know falls back to
1331
+ Lavande. When no mode is stored, the first visit takes `defaultMode` if you
1332
+ pass one, else the **active theme's `colorScheme`** (`schemes` maps a theme
1333
+ name to it; the presets' are built in, so a first visit on `bluemoon` opens
1334
+ dark; `nxgtMaterial({ themes })` fills it from your themes), else `system`. A
1335
+ stored mode always wins. Put `suppressHydrationWarning` on `<html>`: the script changes its
1336
+ `class`, `style` and `data-nxgt-theme` before React hydrates.
1337
+
1338
+ Read a stored value with `JSON.parse`: `localStorage.getItem('theme')` returns
1137
1339
  `"dark"` with its quotes, which a plain comparison never matches.
1138
1340
 
1139
1341
  In a bundled client entry that runs first (an SPA's `main.tsx`), the exported
@@ -1152,11 +1354,13 @@ applyResolvedTheme(resolveTheme(stored, matchMedia('(prefers-color-scheme: dark)
1152
1354
  ```
1153
1355
 
1154
1356
  ```ts
1155
- function resolveTheme(stored: string | null | undefined, prefersDark: boolean): {
1357
+ function resolveTheme(stored: string | null | undefined, prefersDark: boolean, defaultMode?: ThemePreference): {
1156
1358
  theme: ResolvedTheme;
1157
1359
  preference: ThemePreference;
1158
1360
  };
1159
1361
  function applyResolvedTheme(theme: ResolvedTheme): void; // no-op without `document`
1362
+ function resolveThemeName(stored: string | null | undefined, known: readonly string[] | undefined, fallback: string): string;
1363
+ function applyThemeName(name: string): void; // sets <html data-nxgt-theme>, removes it for 'lavande'; no-op without `document`
1160
1364
  ```
1161
1365
 
1162
1366
  In a server-rendered app, keep the inline script: the server does not know the
@@ -32,6 +32,7 @@ Every part is on by default and has an opt-out. It needs `vite` 5.2 or later (6,
32
32
  | `fonts.katex` | `true` | Never inline a font as a `data:` URI (`cspAssetsInlineLimit`), composed with your own `build.assetsInlineLimit` |
33
33
  | `fonts.sans` | `'inter'` | Loads self-hosted Inter when `@fontsource-variable/inter` is installed (otherwise warns and keeps the system stack), in your root stylesheet, and sets `--font-sans`; `false` to keep the system stack and silence the warning. `fonts: false` turns both off |
34
34
  | `styles.check` | `true` | Fails `vite build` when the library's stylesheet is never imported (`@nxgt/material/styles.css`, or the compiled one through `@import "@nxgt/material";`), or `styles.css` is imported without the `@source` line Tailwind needs |
35
+ | `themes` | off | Generates your themes' stylesheets and types and injects the boot script: see [Themes](#themes) |
35
36
  | `pdf` | on | pdfkit's standard fonts (`pdfkitStandardFonts()`): a no-op unless pdfkit goes through the bundle; and a dev-server route for the pdf.js worker (the `pdf.worker` row). `pdf: false` turns both off |
36
37
  | `pdf.bundle` | `false` | Also adds the renderer to `ssr.noExternal`, for a server whose runtime image has no `node_modules` (see [Rendering PDFs on a bundled server](#rendering-pdfs-on-a-bundled-server)) |
37
38
  | `pdf.worker` | `true` | Dev server only: answers `pdf.worker.min.mjs` in Vite's pre-bundle folder (`…/deps/`, any `cacheDir`, so Storybook and Vitest browser mode too) with the installed `@nxgt/material/dist/pdf.worker.min.mjs` (else `pdfjs-dist/build/…`). Vite 5 to 7 pre-bundle the library without the worker, so the readers' worker URL is a 404 there; Vite 8 needs no route, and `@nxgt/material-vue` none (it imports the worker with `?url`). `pdf: { worker: false }` drops it; then set `configurePdfReader({ workerSrc })` instead (see [the PDF reader guide](pdf-reader.md#bundlers)) |
@@ -91,6 +92,31 @@ Pick nonce mode when a server renders or serves your HTML and can run
91
92
  strict option for styles: `style-src-elem 'self' 'nonce-…'`, no
92
93
  `'unsafe-inline'`.
93
94
 
95
+ ## Themes
96
+
97
+ `themes` (off unless given) generates the stylesheets and types of your own
98
+ `defineTheme()` modules (see [Themes for your app](themes.md)) and puts the boot
99
+ script that restores the stored mode and theme name first in `<head>`.
100
+
101
+ ```ts
102
+ nxgtMaterial({ themes: { entries: 'src/themes/*.ts' } });
103
+ ```
104
+
105
+ | Option | Default | What it does |
106
+ | --- | --- | --- |
107
+ | `themes.entries` | required | Paths or globs of the theme modules; each module's default or named exports are themes |
108
+ | `themes.outDir` | `'src/themes/generated'` | Where `<name>.css` and `nxgt-themes.d.ts` are written, relative to the project root. Commit them |
109
+ | `themes.types` | `true` | Write `nxgt-themes.d.ts`, which merges your tones into `AppTones` and your theme names into `NxgtThemes` |
110
+ | `themes.boot` | `true` | The boot script, with the presets' and your theme names. `{ defaultTheme, defaultMode }` sets a first visit's theme and mode; without `defaultMode`, a first visit takes the active theme's `colorScheme` (yours included), then `system` |
111
+ | `themes.onWarning` | `'warn'` | Contrast and tone warnings go to Vite's logger; `'error'` fails the build |
112
+
113
+ The modules are loaded with Vite's module runner (Vite 6.1 or later), so a `.ts`
114
+ theme file needs no loader. A file is rewritten only when its bytes change, so HMR
115
+ does not loop; in dev the plugin watches the modules and what they import. The boot
116
+ script is an inline script: in hash mode its hash goes to `nxgt-csp.json` with the
117
+ others, in nonce mode it carries the placeholder. The dev report-only header lists
118
+ it as a violation, like Vite's own dev scripts.
119
+
94
120
  ## The dev server
95
121
 
96
122
  `csp.dev` sends `Content-Security-Policy-Report-Only` (hash mode: no nonce,