# Theme — providers, switches, and forced themes

The runtime side of the design system. `styles/` ships the **tokens** (CSS
variables) and the Tailwind utilities that read them; `theme/` is the **React**
layer that decides *which* token set is live (light / dark / a preset) and lets
you switch or pin it.

> Read [`../styles/README.md`](../styles/README.md) first for the **token
> contract** — it is load-bearing for everything here. The one rule that bites:
> **base tokens are full CSS colors (`hsl(...)`), never bare triplets.**

## Exports

| Export | What it does |
|---|---|
| `ThemeProvider` / `useThemeContext` | Wraps `next-themes`. Owns the `html.dark` class + the user's light/dark/system choice. Mount once at the app root (via `BaseApp` in `@djangocfg/layouts`, which mounts it for you). |
| `ThemeToggle` | A light/dark toggle button. |
| `ThemeSegmented` | A segmented light/dark/system control. |
| `ThemeOverride` | **Pathname-aware forced theme.** Mutates the real `next-themes` value while the route matches a rule, restores the user's pick when it leaves. Globs (`*`, `**`) supported. This is the **correct** way to force a route's theme. |
| `resolveForcedTheme` | Pure helper — first matching rule → forced theme (or `null`). Pair with `ForcedThemeProvider`. |
| `ForcedThemeProvider` / `useForcedTheme` | Lets descendants read the currently-forced theme. |
| `ForceTheme` | **Scoped, inline-var theme for a subtree.** A `<div class={theme}>` that re-declares the token vars inline. Use sparingly — see the trap below. |

## Choosing: `ThemeOverride` vs `ForceTheme`

They look similar; they are not interchangeable.

| | `ThemeOverride` (preferred) | `ForceTheme` (last resort) |
|---|---|---|
| How | mutates the real `next-themes` value → toggles `html.dark` | wraps children in `<div class={theme} style={inline vars}>` |
| Scope | the **whole app** while the route matches | just that **subtree** |
| Tokens | the real preset/`theme.css` tokens cascade — **always valid** | re-declares a hardcoded token map inline |
| Active preset | **respected** (macos/ios/django-cfg/…) | **ignored** — ships its own generic palette |
| Risk | none | bare-triplet trap (below); also overrides your brand accent |

**Default to `ThemeOverride`** for "this route is always dark" (the common case —
a marketing landing, a docs section). It's how the cmdop site forces its homepage
dark:

```tsx
// in the app shell (RootAppLayout)
<BaseApp theme={{ defaultTheme: 'dark' }}>
  <ThemeOverride pathname={pathnameWithoutLocale} rules={[{ path: '/', theme: 'dark' }]} />
  {children}
</BaseApp>
```

Reach for `ForceTheme` **only** when you need a single subtree in the opposite
theme of the rest of the page (e.g. a dark preview card on a light page) and you
cannot drive it through the router.

## ⚠️ The `ForceTheme` bare-triplet trap

`ForceTheme` re-declares tokens inline. Under the **Tailwind v4 token contract**,
base tokens are **full colors** and the utilities read them raw:

```css
.bg-muted    { background-color: var(--muted); }
.border-border { border-color: var(--border); }
```

So a token set to a **bare HSL triplet** is a broken color:

```css
--muted: 0 0% 10%;     /* ❌ var(--muted) → "0 0% 10%" → not a color → declaration dropped */
--muted: hsl(0 0% 10%);/* ✅ valid color */
```

The failure is **silent and confusing**: inside the `ForceTheme` subtree
`bg-muted` renders **transparent** and `border-border` renders the inherited
fallback (≈ white), while `border-divider` (a token `ForceTheme` doesn't
re-declare) still works — so it looks like "random borders are white and some
fills vanished." It is NOT a Tailwind content-scan problem; the utilities exist,
their *input* is invalid. Full write-up + how to diagnose:
[`TROUBLESHOOTING.md`](TROUBLESHOOTING.md).

`ForceTheme`'s token map is wrapped in `hsl(...)` as of this version, so the trap
is closed for the values it ships. But the lesson stands for **any** inline token
override you write: wrap in `hsl(...)`, or prefer `ThemeOverride` so the real
(already-valid) preset tokens cascade and you never hand-maintain a palette.

## Maintenance rule

After changing a component or the token map here, update this README and bump the
package patch version (consumers pin npm versions; this file is the changelog).
