first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.