# Appearance

> The desktop app's runtime theme as a React provider. Themes, light and dark pairing, contrast, accent, fonts, the code theme and the motion level, kept by a host.

`zidane-ui/appearance` is the desktop app's runtime theme. `AppearanceProvider` reads the user's preferences from an `AppearanceHost`, picks the active theme (the OS's appearance under `system`), derives its palette and writes the `--zg-*` variables on `<html>`, with the contrast, the accent override and the fonts. It also puts the tree under the motion preference, and stores a snapshot of the variables so the next page load paints the right theme before any script runs. Each frame below runs on its own fixture host, scoped to the frame.

## Mount it

Wrap the app once. In a web app, the preferences live in `localStorage`.

```tsx
import 'zidane-ui/styles.css'
import { createBrowserAppearanceHost } from 'zidane-ui/adapters'
import { AppearanceProvider, codeThemeTypeOf } from 'zidane-ui/appearance'

const appearance = createBrowserAppearanceHost({ codeThemeType: codeThemeTypeOf })

export function Root() {
  return (
    <AppearanceProvider host={appearance}>
      <App />
    </AppearanceProvider>
  )
}
```

`AppHostProvider` (`zidane-ui/app`) puts `host.appearance` in an `AppearanceProvider` for you.

| Prop | Does |
| - | - |
| `host` | Where the preferences live, the extra themes, the snapshot and the window effects |
| `folder`, `themesKey` | The folder whose extra themes are offered (`host.themes`), and a key that changes when they may have changed |
| `legacyThemeId` | A theme id from zidane's shared settings, read once on the first run to seed the pairing |
| `root` | Where the variables go. Default `<html>`. Anywhere else, no snapshot is written and the window is left alone |
| `motion` | Put the tree under the motion preference (`MotionProvider`). Default on |

## Pick a host

| Host | From | Keeps |
| - | - | - |
| `createBrowserAppearanceHost()` | `zidane-ui/adapters` | `localStorage`, under the desktop app's keys. Another tab's change is adopted. zidane-server keeps no appearance, so the web app offers the built-in themes only. |
| `createFixtureAppearanceHost(options)` | `zidane-ui/appearance/testing` | Memory, for demos and tests. It records the snapshot and the window calls. |
| Your own | `zidane-ui/appearance/headless` | Anything. `createStorageAppearancePrefs` and `createStorageBoot` keep the preferences and the snapshot in any `Storage`. |

## What a host offers

Only `prefs` is required.

| Member | Gives |
| - | - |
| `prefs` | The preferences: `get`, `subscribe`, `set`, `reset`, `isFirstRun` |
| `themes(folder)` | Themes beyond the built-in ones (the desktop app lists the folder's extension themes) |
| `boot.write(snapshot)` | Stores the applied variables for the pre-paint script |
| `window.setBackground`, `window.setZoom`, `window.setNativeTheme` | Native effects: the resize background, the interface scale, the title bar's appearance |

The snapshot is `{ appearance, vars }` under `zg:appearance:boot:v1`. A page replays it before its bundle loads:

```html
<script>
  try {
    const s = JSON.parse(localStorage.getItem('zg:appearance:boot:v1'))
    for (const [k, v] of Object.entries(s.vars)) document.documentElement.style.setProperty(k, v)
    document.documentElement.classList.add(s.appearance)
  } catch {}
</script>
```

## Read it

| Hook | Returns |
| - | - |
| `useAppearance()` | The preferences, the active appearance and theme, every selectable theme, and the setters (`set`, `setMode`, `pickTheme`, `reset`) |
| `useAppearanceState()` | The preferences alone |
| `useTheme()`, `useColors()`, `usePalette()` | The active theme, its colors, the derived palette |
| `useShikiTheme()` | The code theme: the pick for the current appearance, else the theme's own |
| `useAppearanceMotion()`, `useAppearanceReducedMotion()` | The motion level the preference resolves to |

Outside a provider the hooks return the defaults and the setters do nothing. `parseAppearance` validates a stored blob field by field, `LIMITS` holds the bounds, `fontsToCssVars` and `availableCuratedFonts` are the font tokens and the installed-font check, and `codeThemesOf(appearance)` lists the bundled code themes.

## Test your host

```ts
import { describeAppearanceHostContract } from 'zidane-ui/appearance/testing/contract'

describeAppearanceHostContract('my host', backend => makeHarness(backend), ['boot'])
```

| Entry point | Holds |
| - | - |
| `zidane-ui/appearance` | `AppearanceProvider`, the hooks, `applyPalette`, `codeThemesOf` |
| `zidane-ui/appearance/headless` | `AppearanceHost`, the preferences model, the `Storage` stores, the font tokens, `zoomKeyCommands(prefs)` (the zoom keys for `useKeyCommands`) |
| `zidane-ui/appearance/testing` | `createFixtureAppearanceHost` |
| `zidane-ui/appearance/testing/contract` | `describeAppearanceHostContract` |
