@nxgt/material 3.7.4 → 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.
- package/CHANGELOG.md +56 -0
- package/README.md +39 -6
- package/bin/nxgt-theme.js +6 -0
- package/dist/{async-slice-o_fpqeGX.js → async-slice-DDjaJY_F.js} +2 -2
- package/dist/background--Bf_EbFR.js +159 -0
- package/dist/backgrounds/backgrounds.css +2 -0
- package/dist/backgrounds/css-react/moon.css +97 -0
- package/dist/backgrounds.js +3 -139
- package/dist/browser-only-DQ94lWF8.js +18 -0
- package/dist/bubble-input-B7BVd9fX.js +237 -0
- package/dist/{button--cF4gY14.js → button-DbfdITBB.js} +51 -57
- package/dist/charts.js +3 -3
- package/dist/{chip-D1B1dL77.js → chip-DpySRqCy.js} +2 -2
- package/dist/{client-only.utils-lQ7GqP8w.js → client-only.utils-3nHNzCki.js} +1 -1
- package/dist/{code-editor-0CdGwKDg.js → code-editor-BiA14xua.js} +7 -7
- package/dist/code-editor.js +6 -6
- package/dist/{code-language-items-CGARRbVm.js → code-language-items-C_dJ6GVs.js} +2 -2
- package/dist/components/theme-tokens.css +34 -35
- package/dist/components/tones/app-tones.css +2 -1
- package/dist/components/tones/containers.css +4 -1
- package/dist/components/tones/index.css +2 -0
- package/dist/components/tones/surface.css +1 -0
- package/dist/components/tones/tokens.css +8 -3
- package/dist/components/tones/zone-paint.css +15 -0
- package/dist/components/ui/feedback/toast/toast-tokens.css +1 -0
- package/dist/components/ui/layout/app-shell/activity-backdrop.css +62 -1
- package/dist/components/ui/layout/sidebar/sidebar-tokens.css +5 -2
- package/dist/components/ui/media/media-player/nxgt-media-player.css +7 -32
- package/dist/components/ui/primitives/field-box/field-box-tokens.css +5 -2
- package/dist/{components-DD7_NI6U.js → components-B9EIfMdd.js} +18652 -18553
- package/dist/components.js +24 -22
- package/dist/{dialog-DdJ1C4BR.js → dialog-CkIYpJXk.js} +55 -53
- package/dist/dist-B7RjY7SL.js +158 -0
- package/dist/dist-C4wtXCjf.js +593 -0
- package/dist/dist-DVRQkaTK.js +111 -0
- package/dist/dist-lmHmz_bu.js +419 -0
- package/dist/dnd.js +1 -1
- package/dist/dropdown-menu-BC5Tbhb9.js +908 -0
- package/dist/editor.js +527 -527
- package/dist/empty-state-s2CSjKXJ.js +126 -0
- package/dist/{escape-owner-B8k7DvFa.js → escape-owner-CripBce_.js} +2 -1
- package/dist/{field-blur-DFUazxsL.js → field-blur-DPLsMufs.js} +1 -1
- package/dist/{field-blur-context-Dw9ZAgBr.js → field-blur-context-1ZwJgsHE.js} +2 -2
- package/dist/has-text-content-Ds2dkHX_.js +10 -0
- package/dist/hooks.js +35 -33
- package/dist/{hover-card-D5iHS9q1.js → hover-card-Cox-tPDz.js} +41 -39
- package/dist/{i18n-provider-C6SBK_nO.js → i18n-provider-C6HhBw34.js} +1 -1
- package/dist/i18n.js +6 -6
- package/dist/{icon-badge-Bb5-73XP.js → icon-badge-q3Ve8gra.js} +3 -3
- package/dist/{icon-button-5A6Mrj6w.js → icon-button-DJcWpIhu.js} +2 -1
- package/dist/{in-dialog-AiwTD3DK.js → in-dialog-Vwkyp-M5.js} +6 -5
- package/dist/lib/backgrounds/background/available.d.ts +3 -3
- package/dist/lib/backgrounds/background/background.d.ts +7 -3
- package/dist/lib/backgrounds/background/types.d.ts +7 -1
- package/dist/lib/backgrounds/index.d.ts +1 -1
- package/dist/lib/components/ui/layout/app-shell/activity-chrome.d.ts +17 -0
- package/dist/lib/components/ui/layout/app-shell/activity-layout.d.ts +6 -4
- package/dist/lib/components/ui/layout/app-shell/backdrop-layer.d.ts +11 -0
- package/dist/lib/components/ui/layout/app-shell/keep-shapes-inside.d.ts +17 -0
- package/dist/lib/components/ui/layout/app-shell/theme-backdrop.d.ts +6 -0
- package/dist/lib/components/ui/layout/app-shell/use-activity-backdrop.d.ts +18 -0
- package/dist/lib/components/ui/layout/app-shell/use-keep-shapes-inside.d.ts +9 -0
- package/dist/lib/hooks/resolve-theme.d.ts +46 -1
- package/dist/lib/hooks/theme-store.d.ts +35 -4
- package/dist/lib/hooks/use-theme.d.ts +20 -4
- package/dist/lib/i18n/resources/en/en.d.ts +4 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +4 -0
- package/dist/lib/i18n/resources/fr/fr.d.ts +4 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +4 -0
- package/dist/lib/i18n/resources/resources.d.ts +8 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib/theme-picker/index.d.ts +7 -0
- package/dist/lib/theme-picker/partials/mini-background.d.ts +10 -0
- package/dist/lib/theme-picker/partials/mode-control.d.ts +5 -0
- package/dist/lib/theme-picker/partials/theme-tile.d.ts +12 -0
- package/dist/lib/theme-picker/theme-label.d.ts +7 -0
- package/dist/lib/theme-picker/theme-picker.d.ts +25 -0
- package/dist/lib/themes/index.d.ts +14 -0
- package/dist/lib/themes/nxgt-theme-provider.d.ts +29 -0
- package/dist/lib/themes/nxgt-theme.d.ts +22 -0
- package/dist/lib/themes/theme-context.d.ts +28 -0
- package/dist/lib/themes/theme-value.d.ts +5 -0
- package/dist/lib/vite/index.d.ts +1 -1
- package/dist/lib.js +3 -3
- package/dist/main.js +43 -39
- package/dist/map.js +8 -8
- package/dist/material.css +1 -1
- package/dist/nxgt-theme-CgSb9Obl.js +37 -0
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +2 -2
- package/dist/{public-SsiazLEv.js → public-Ce30uvp0.js} +591 -587
- package/dist/read-storage-C2J90bNm.js +10 -0
- package/dist/{rich-code-block-CwNwQs5z.js → rich-code-block-C2Fp_FDU.js} +5 -5
- package/dist/{sortable-provider-BvD98s8Y.js → sortable-provider-pVaEZzty.js} +3 -3
- package/dist/{spinner-CtGCPza-.js → spinner-CsHcmcpF.js} +2 -2
- package/dist/{stored-language-Bb7yX5uE.js → stored-language-BeIKw3-K.js} +15 -15
- package/dist/styles.css +8 -1
- package/dist/theme-context-BagPvRcz.js +16 -0
- package/dist/theme-picker.js +118 -0
- package/dist/theme-store-CSl51uNH.js +91 -0
- package/dist/theme-toggle-C8os82b4.js +48 -0
- package/dist/themes.js +42 -0
- package/dist/toggle-group-j8zaxEg1.js +69 -0
- package/dist/{toolbar-items-DsDDD5AY.js → toolbar-items-CqHvlTU8.js} +443 -589
- package/dist/tooltip-rhbqNmr3.js +1064 -0
- package/dist/{translate-material-Bsjc4Tow.js → translate-material-CGZljjp1.js} +2 -2
- package/dist/{translator-or2y0JJB.js → translator-DCYO4Ij3.js} +3 -3
- package/dist/{use-background-activity-B8jLHNop.js → use-background-activity-KxQgelFF.js} +1 -1
- package/dist/{use-chat-composer-DZ01a16D.js → use-chat-composer-CDrgWGMM.js} +4684 -4886
- package/dist/{use-heading-spy-ByOQNnFZ.js → use-heading-spy-DXo6IOX9.js} +52 -121
- package/dist/{use-material-translation-hLQzOjF0.js → use-material-translation-DkxcLbLM.js} +2 -2
- package/dist/use-slice-reducer-BGkeGtML.js +42 -0
- package/dist/use-theme-Cd-aiDJo.js +31 -0
- package/docs/README.md +2 -1
- package/docs/components/foundations.md +39 -0
- package/docs/components/navigation-and-layout.md +3 -3
- package/docs/getting-started.md +1 -1
- package/docs/guides/app-shell.md +46 -7
- package/docs/guides/backgrounds.md +43 -7
- package/docs/guides/i18n.md +1 -1
- package/docs/guides/themes.md +103 -0
- package/docs/guides/theming.md +288 -84
- package/docs/guides/vite-plugin.md +26 -0
- package/docs/previews/backgrounds-background-from-theme.webp +0 -0
- package/docs/previews/backgrounds-geometry-more.webp +0 -0
- package/docs/previews/backgrounds-geometry.webp +0 -0
- package/docs/previews/backgrounds-illustrated-scenes.webp +0 -0
- package/docs/previews/backgrounds-light-more.webp +0 -0
- package/docs/previews/backgrounds-light.webp +0 -0
- package/docs/previews/backgrounds-material-more.webp +0 -0
- package/docs/previews/backgrounds-matrix.webp +0 -0
- package/docs/previews/backgrounds-moon.webp +0 -0
- package/docs/previews/backgrounds-motif-shapes.webp +0 -0
- package/docs/previews/backgrounds-motifs.webp +0 -0
- package/docs/previews/foundations-themes-themepicker.webp +0 -0
- package/docs/roadmap.md +25 -0
- package/docs/troubleshooting.md +35 -7
- package/lib/backgrounds/backgrounds.css +2 -0
- package/lib/backgrounds/css-react/moon.css +97 -0
- package/lib/components/theme-tokens.css +34 -35
- package/lib/components/tones/app-tones.css +2 -1
- package/lib/components/tones/containers.css +4 -1
- package/lib/components/tones/index.css +2 -0
- package/lib/components/tones/surface.css +1 -0
- package/lib/components/tones/tokens.css +8 -3
- package/lib/components/tones/zone-paint.css +15 -0
- package/lib/components/ui/feedback/toast/toast-tokens.css +1 -0
- package/lib/components/ui/layout/app-shell/activity-backdrop.css +62 -1
- package/lib/components/ui/layout/sidebar/sidebar-tokens.css +5 -2
- package/lib/components/ui/media/media-player/nxgt-media-player.css +7 -32
- package/lib/components/ui/primitives/field-box/field-box-tokens.css +5 -2
- package/lib/styles.css +8 -1
- package/lib/theme-picker/theme-picker-list.css +29 -0
- package/lib/theme-picker/theme-picker.css +192 -0
- package/lib/theme-picker/theme-tile-background.css +77 -0
- package/lib/theme-picker/theme-tile-textures-night.css +63 -0
- package/lib/theme-picker/theme-tile-textures.css +153 -0
- package/lib/themes/ambre.css +134 -0
- package/lib/themes/ardoise.css +133 -0
- package/lib/themes/bleumono.css +136 -0
- package/lib/themes/bluemoon.css +136 -0
- package/lib/themes/corail.css +136 -0
- package/lib/themes/foret.css +133 -0
- package/lib/themes/lavande.css +133 -0
- package/lib/themes/menthe.css +136 -0
- package/lib/themes/ocean.css +136 -0
- package/lib/themes/pink.css +134 -0
- package/lib/themes/prune.css +133 -0
- package/lib/themes/sable.css +133 -0
- package/lib/themes/slate.css +133 -0
- package/lib/themes/teal.css +137 -0
- package/package.json +30 -2
- package/dist/dropdown-menu-DWtgUVW7.js +0 -1490
- package/dist/empty-state-CRWLQ1sM.js +0 -191
- package/dist/tooltip-tmFoTU_d.js +0 -1582
- package/dist/use-slice-reducer-0r5dXnJe.js +0 -58
package/docs/guides/theming.md
CHANGED
|
@@ -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
|
-
-
|
|
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.
|
|
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
|
|
97
|
-
|
|
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
|
|
102
|
-
| `--card`, `--popover` / `--sidebar` | white / `oklch(0.985 0.006 293.9)` | `oklch(0.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
683
|
-
`var(--primary)
|
|
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
|
|
686
|
-
|
|
687
|
-
|
|
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={{ ['--
|
|
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.
|
|
791
|
-
`--background`,
|
|
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.
|
|
795
|
-
5
|
|
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.
|
|
798
|
-
and
|
|
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 % (
|
|
802
|
-
`success`, `info` or `warning` one at 60 % (7.
|
|
803
|
-
`--background`;
|
|
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.
|
|
820
|
-
token mixed with 15 % black (6.
|
|
821
|
-
mixed with 60 % white, the `--primary-text` token (
|
|
822
|
-
2.
|
|
823
|
-
30 % white (
|
|
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.
|
|
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
|
-
|
|
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.
|
|
850
|
-
row), and the checkbox, drawn in the same dark colour (
|
|
851
|
-
tint is too light to be told from its neighbour alone (1.
|
|
852
|
-
1.
|
|
853
|
-
a `Card`:
|
|
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.
|
|
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)` (
|
|
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)`,
|
|
867
|
-
5.
|
|
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
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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.
|
|
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 `--
|
|
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 (
|
|
919
|
-
`--primary` mixed with 60 % white in the dark one (9.
|
|
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.
|
|
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,
|
|
925
|
-
the track, so it has a `--muted-foreground` border (5.8:1 light,
|
|
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.
|
|
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
|
|
934
|
-
`--card`, 7.
|
|
935
|
-
is a dark surface (`oklch(0.
|
|
936
|
-
near-white block on a dark page) and `--foreground` on it reaches
|
|
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.
|
|
942
|
-
(WCAG 1.4.11; `bg-muted`, a dark surface now,
|
|
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,
|
|
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
|
-
|
|
1105
|
-
|
|
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
|
|
1118
|
-
inline script in `<head>`. It
|
|
1119
|
-
`
|
|
1120
|
-
|
|
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
|
-
```
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
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,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|