A configured theme is written before the application starts. A **runtime theme** is written while it
runs: a reader picks colours, names the palette, and expects every component to change without a
rebuild.

Both providers accept the definitions through one controlled prop:

```tsx
<AsheeUIProvider config={config} themes={themes}>
  <App />
</AsheeUIProvider>
```

`themes` is an `ExternalColorConfig`, the same shape as `config.color`. The provider merges it over the
configured map **before** resolution, so a runtime palette goes through the same inheritance,
validation and painting as a configured one.

```tsx
import type { ExternalColorConfig } from "@asheeui/core";

const themes: ExternalColorConfig = {
  ocean: {
    extends: "dark",
    background: "#08111f",
    primary: "#38bdf8",
    border: "#1e3a5f",
  },
};
```

`ocean` now appears in `useTheme().availableThemes`, can be selected with `setTheme("ocean")`, paints
as `.theme-ocean` on the web and as the resolved palette on a device.

## What it may state

| Rule | Detail |
| --- | --- |
| A name is a slug | Lowercase letters, digits and hyphens; starts with a letter; up to 64 characters |
| Built-ins are reserved | `light`, `dark` and `system` are rejected; retune the built-ins in `config.color` |
| Colours are portable | Hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) or `rgb()`/`rgba()`; alpha is ignored |
| `extends` names something declared | A built-in, a configured theme or another runtime theme |
| It is colours only | Radii, spacing, typography and component config remain in the static configuration |

An invalid map throws while the provider renders, naming the first issue. The editor package exposes the
non-throwing validator through its field messages, so a form can point at the field instead of crashing.

## Add, select, remove

Keep the map in application state and pass it down:

```tsx
const [themes, setThemes] = useState<ExternalColorConfig>({});

const addOcean = () => {
  setThemes((current) => ({
    ...current,
    ocean: { extends: "dark", primary: "#38bdf8", background: "#08111f" },
  }));
};
```

The provider must see a name before the controller can select it. A handler that adds a theme and calls
`setTheme` in the same event is selecting a name the current provider does not know:

```tsx
// This can be dropped: `ocean` is not in availableThemes yet.
setThemes(next);
setTheme("ocean");
```

Select it after the render that carries the new map. The fitness reference application uses a pending
name and an effect:

```tsx
useEffect(() => {
  if (pending && themes[pending]) {
    setTheme(pending);
    setPending(undefined);
  }
}, [pending, themes, setTheme]);
```

Removing a name removes it from the picker and the stylesheet. If it overrode a configured theme, the
configured definition comes back. If it was the selection, the controller falls back to the default and
clears the stored selection; the same stale-value rule that handles a renamed configured theme.

## Persistence

The definitions belong to the application. The library only accepts the map; it does not choose a
storage key, schema or migration policy.

Store the map next to the rest of the reader's settings, then load it **before** the provider's first
render when a stored selection may name a runtime theme:

```tsx
const [themes, setThemes] = useState(() => readThemes());
const [ready, setReady] = useState(false);

useEffect(() => {
  setThemes(readThemes());
  setReady(true);
}, []);
```

The selection itself is still `ashee-theme`, written by `setTheme`. The definitions need their own key;
the reference applications use `pulse.themes`.

Two rules keep a reload predictable:

- **Validate before passing stored data.** A bad edit or an older version must not make the provider
  throw during render. Validate the map and fall back to `{}` when it is not acceptable.
- **Load the definitions before validating the selection.** A stored `ocean` looks stale while the
  definitions are still loading. On the server there is no localStorage, so a web application usually
  starts with `{}`, loads the definitions in an effect, and restores a captured selection once. The
  [server rendering](#server-rendering) section states the trade-off.

On a device, AsyncStorage is asynchronous while the controller's storage contract is synchronous. The
reference application loads both values, caches the selection in memory for the controller's synchronous
`get`, and mirrors writes back to AsyncStorage.

## Server rendering

A theme known at render time is included in the pre-paint script and the first paint, exactly like a
configured theme. A theme that only exists in the browser arrives after hydration: the page paints the
configured default first, then the provider registers and applies the runtime palette.

That is a real trade-off, not a bug. The alternatives are to render the definitions on the server
(cookies or a request-scoped store) or to accept a short default frame. Loading `themes` from an effect
keeps the server HTML and the first client render identical, which is what prevents a hydration mismatch
on the stylesheet.

If a stored selection names a runtime theme, read the selection **before** the provider's first layout
effect. The controller may drop it while the definitions are absent. Once the definitions are present,
`setTheme(stored)` restores it.

## The editor package

`@asheeui/theme` is optional. It imports `asheeui` and core, has no renderer imports, and works on both
platforms without a second implementation.

```bash
pnpm add @asheeui/theme
```

```tsx
import { ThemeEditor, ThemeSwitcher } from "@asheeui/theme";

<ThemeSwitcher showSystem label="Theme" />

<ThemeEditor
  themes={themes}
  onChange={setThemes}
  activeTheme={theme}
  onApply={setTheme}
/>;
```

The package contains:

| Component | Use |
| --- | --- |
| `ThemeSwitcher` | Pick system, a built-in or a runtime theme |
| `ColorField` | A hex/rgb input with a swatch, presets and a validation message |
| `ThemeCard` | A palette preview with select/edit/delete actions |
| `ThemeEditor` | Name, slug, base, eight roles, contrast checks and save/apply/delete |
| `ThemeGallery` | Saved themes, a create action and optional starter presets |
| `ContrastBadge` | The WCAG ratio and level for two colours |

Every component is controlled. `ThemeEditor` never wraps the provider, never writes storage and never
calls `setTheme` except through `onApply`. That makes it usable in a settings drawer, a modal, a test or
a documentation demo.

```tsx demo="guides/runtime-theme-editor"
```

The live demo edits an in-memory map. It does not apply the theme to this page: a docs demo that called
`setTheme` would change the site's own theme, and a preview that needs a scoped palette is a product
decision rather than an editor default.

### Starter presets

`THEME_PRESETS` is a small set of ready definitions (Ocean, Forest, Sunset and Mono). The gallery can
show them without saving anything itself:

```tsx
import { THEME_PRESETS, ThemeGallery } from "@asheeui/theme";

<ThemeGallery
  themes={themes}
  presets={THEME_PRESETS}
  onPresetSelect={(preset) => {
    setThemes((current) => ({
      ...current,
      [preset.slug]: preset.definition,
    }));
    setPending(preset.slug);
  }}
/>;
```

`onPresetSelect` is the application's decision: replace an existing preset, skip it, or add it beside
the reader's themes. Select the name on the next render, after the provider knows it, using the same
pending-name pattern as above.

```tsx demo="guides/runtime-theme-presets"
```

The demo adds presets to its own map. It does not apply them to the documentation page.

## Contrast

A palette is not valid just because it parses. `ThemeEditor` computes the ratio for body text and for a
primary action, and shows `AA`/`AAA` or a failing ratio. It warns rather than blocks: a deliberately
low-contrast palette is the application's choice.

The same calculation is exported from core for an application that builds its own form:

```ts
import { contrastRatio, contrastLevel } from "@asheeui/core";

contrastRatio("#ffffff", "#0b1220"); // about 17.9
contrastLevel(17.9); // "AAA"
```

## Native

The device receives the same definitions. It paints the eight portable roles through its palette
variables; `scrollbarThumb` and `scrollbarTrack` are web-only and are ignored. Use hex or `rgb()` values;
an alpha channel is discarded because the framework's classes carry their own.

## Next

- [A custom theme](/guides/custom-theme) for a theme that ships with the configuration.
- [Theming](/concepts/theming) for the controller, the resolved palette and server rendering.
- [ColorSwatch](/components/data-display/color-swatch) for the primitive the editor builds on.
- [Configuration](/concepts/configuration) for where `config` and `themes` meet.
