first commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# `<ThemeProvider>` — chassis-level theme registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Mounts the active theme registry, hands it down through React context,
|
||||
applies the active theme's tokens to `<html>` as inline CSS variables,
|
||||
listens for `prefers-reduced-motion` changes, and prevents flash-of-
|
||||
unstyled-content via an inline FOUC-prevention `<script>` tag rendered
|
||||
into the `<head>`. The Provider is the only piece of the chassis that
|
||||
mutates `document.documentElement` directly — every other consumer reads
|
||||
through the `useTheme` / `useThemeTokens` / `useThemedVariants` hooks.
|
||||
|
||||
## Features
|
||||
|
||||
- Validates every supplied `ThemePlugin` against `ThemePluginSchema` at
|
||||
mount; throws `ReactUiError(REACT_UI.THEME_TOKEN_MISSING)` on failure
|
||||
- Resolves the initial theme from `localStorage[storageKey]` first,
|
||||
then `defaultTheme`; throws
|
||||
`ReactUiError(REACT_UI.THEME_NOT_REGISTERED)` when the resolved name
|
||||
is not registered
|
||||
- Inline FOUC-prevention `<script>` injected before children render so
|
||||
`<html data-theme="...">` is set before React hydrates; CSP nonce
|
||||
honoured via `scriptNonce`
|
||||
- Live `prefers-reduced-motion` listener that updates the live
|
||||
`--motion-duration-*` variables according to the active theme's
|
||||
`respectReducedMotion` policy (`'strict'` collapses to `0ms`,
|
||||
`'soft'` halves, `'off'` ignores)
|
||||
- `setTheme` wrapped in `useTransition()` so theme switches are
|
||||
low-priority React work that does not block urgent input
|
||||
- OTel telemetry: counter `react_ui.theme.switch_total{from, to}` and
|
||||
histogram `react_ui.theme.switch_duration_ms`
|
||||
|
||||
## Usage: Basic example
|
||||
|
||||
```tsx
|
||||
import { ThemeProvider } from "@suaveplan/react-ui/theme/theme-provider";
|
||||
import { darkTheme, lightTheme } from "@suaveplan/react-ui/theme";
|
||||
|
||||
<ThemeProvider themes={[lightTheme, darkTheme]} defaultTheme="light">
|
||||
<App />
|
||||
</ThemeProvider>;
|
||||
```
|
||||
|
||||
## Usage: Advanced example
|
||||
|
||||
```tsx
|
||||
import { ThemeProvider } from "@suaveplan/react-ui/theme/theme-provider";
|
||||
import {
|
||||
darkTheme,
|
||||
highContrastTheme,
|
||||
lightTheme,
|
||||
} from "@suaveplan/react-ui/theme";
|
||||
|
||||
<ThemeProvider
|
||||
themes={[lightTheme, darkTheme, highContrastTheme]}
|
||||
defaultTheme="system"
|
||||
storageKey="my-app-theme"
|
||||
enableSystem={true}
|
||||
attribute="data-theme"
|
||||
scriptNonce={cspNonce}
|
||||
>
|
||||
<App />
|
||||
</ThemeProvider>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `<ThemeProvider>` — props per `ThemeProviderProps`:
|
||||
- `themes: ReadonlyArray<ThemePlugin>` — required
|
||||
- `defaultTheme: string` — required; matched against `themes[i].name`
|
||||
or the literal `'system'` (when `enableSystem`)
|
||||
- `storageKey?: string` — defaults to `'suaveplan-theme'`
|
||||
- `enableSystem?: boolean` — when true, allows `defaultTheme: 'system'`
|
||||
to map onto `prefers-color-scheme`
|
||||
- `attribute?: string` — defaults to `'data-theme'`
|
||||
- `scriptNonce?: string` — CSP nonce for the inline FOUC script
|
||||
- `children: ReactNode`
|
||||
- `buildFoucScript(input)` — pure builder for the inline script body;
|
||||
exposed so tests can assert without rendering
|
||||
- `halveDuration(input)` — pure helper that halves an `Xms` / `Ys`
|
||||
duration string; returns `'0ms'` on parse fail
|
||||
- `toCssVar(name)` — pure helper converting `cardForeground` →
|
||||
`--card-foreground`
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The FOUC script is intentionally compact, self-contained, and uses
|
||||
`String.prototype.replace` for all escaping rather than `JSON.stringify`
|
||||
so the emitted body cannot smuggle a stray `</script>` close tag. The
|
||||
`useTransition()` wrapper around `setTheme` is the reason the
|
||||
`isPending` flag exists on `useTheme()` — consumers that show a tiny
|
||||
spinner during a theme transition read that flag. The Provider tracks
|
||||
the previous theme name in a `useRef` so the OTel `from` attribute is
|
||||
accurate even when several `setTheme` calls land back-to-back.
|
||||
Reference in New Issue
Block a user