<!-- Source: README.md -->

# @codenhub/theme

Zero-dependency browser theme preference, persistence, DOM, and token manager.

## Installation

```sh
pnpm add @codenhub/theme
```

## Usage

```ts
import { createTheme } from "@codenhub/theme";

// In the browser:
const theme = createTheme().init();

document.querySelector("#theme-toggle")?.addEventListener("click", theme.toggle);
theme.subscribe(({ name }) => console.log(`theme is now ${name}`));

// Remove media-query/storage listeners and subscribers on teardown.
theme.destroy({ revertDom: true });

// On the server or at build time, with the same options: inline this string in a
// blocking <head> script so the theme applies before the first paint.
const script = createTheme().getPrePaintScript();
```

## Documentation

- [Documentation overview](docs/index.md)
- [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)
- [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md)
- [API reference](docs/reference/index.md)

## Requirements

- Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`.
- SSR is supported by skipping unavailable browser work and using the configured default theme.
- Consumers provide CSS selectors, variables, and visual tokens, and inline the script from `getPrePaintScript()` to prevent a theme flash.
- TypeScript 5.4 or newer, for the types, which use `NoInfer`.

## Notes

Construction validates theme names, mappings, attributes, classes, and token schemas and throws on invalid configuration. Storage failures are reported to the console and treated as unavailable storage.

## License

Licensed under Apache-2.0.

<!-- Source: docs/index.md -->

# Manage Browser Themes

`@codenhub/theme` owns one thing for a browser application: deciding which theme is active, keeping it in sync with `localStorage` and the OS `prefers-color-scheme` setting, and reflecting it onto the DOM as an attribute, a class, a `color-scheme`, and CSS custom properties. It fits applications that want one place to own that decision instead of scattering `matchMedia` listeners and `localStorage` reads across the codebase, while keeping selectors, visual design, and CSS entirely in application code.

## Setup

### Installation

```sh
pnpm add @codenhub/theme
```

### Quick start

```ts
import { createTheme } from "@codenhub/theme";

const theme = createTheme().init();

document.querySelector("#theme-toggle")?.addEventListener("click", theme.toggle);
```

`createTheme()` with no arguments configures the built-in `light` and `dark` themes. `init()` resolves the theme to apply — a stored preference if one is valid, otherwise the OS preference — and applies it to `document.documentElement`. Every method is bound to its manager, so `theme.toggle` and `theme.subscribe` can be passed as callbacks directly. Call `destroy()` when the manager's owner is torn down, such as in a framework component's cleanup hook; it stops listening but leaves the DOM and stored preference as they were unless told otherwise (see [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md)).

### Reading state

| Member             | Value                                                                                     |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `theme.name`       | Active theme name.                                                                        |
| `theme.get()`      | Active theme — `name`, `colorScheme`, `tokens`, `pairedTheme` — as a frozen object.       |
| `theme.preference` | Explicit preference stored in `localStorage` when it names a configured theme, or `null`. |
| `theme.system`     | Theme the OS preference maps to right now.                                                |

`theme.preference` reflects what storage holds, so it is also `null` while storage cannot be read. When storing a choice made with `set()` or `toggle()` fails, the choice still holds over OS changes, while `preference` keeps reporting storage: `null` when reading fails too, or the previously stored theme when only writing fails, as with a full quota; see [Persistence](docs/tokens-and-persistence.md#persistence).

`get()` returns the same object until the next change, so it works directly as a store snapshot. In React:

```ts
import { useSyncExternalStore } from "react";

function useActiveTheme() {
  return useSyncExternalStore(theme.subscribe, theme.get);
}
```

A server-rendered app also passes `getServerSnapshot`, which must return the theme the server rendered with. Passing `theme.get` there makes hydration read the theme `init()` resolved in the browser, and React reports a mismatch when it differs. A server that renders with this package alone renders `defaultTheme`, since it has no stored or OS preference, so take the snapshot before `init()` runs; the component then renders the default theme while hydrating and updates right after:

```ts
const serverSnapshot = theme.get(); // before theme.init()

function useActiveTheme() {
  return useSyncExternalStore(theme.subscribe, theme.get, () => serverSnapshot);
}
```

A server that resolves the theme per request instead, such as from a cookie, renders with that theme, so `getServerSnapshot` must return the same one, passed to the client with the page.

### Configuration

Most applications configure their themes and a `tokenSchema` up front:

```ts
import { createTheme } from "@codenhub/theme";

const theme = createTheme({
  themes: {
    light: { colorScheme: "light", tokens: { primary: "#171717" } },
    dark: { colorScheme: "dark", tokens: { primary: "#f9fafb" } },
    dim: { colorScheme: "dark", pairedTheme: "light" },
  },
  tokenSchema: { primary: "--color-primary" },
  storageKey: "app-theme-preference",
  darkClass: true,
}).init();

theme.set("dim");
```

`themes` is keyed by theme name and replaces the built-in list entirely — include `light` and `dark`, or set `defaultTheme` and `systemTheme` to names you do define. Theme names are inferred from its keys, so `defaultTheme`, `systemTheme`, `pairedTheme`, and `set()` accept only configured names at compile time. The types do not make `defaultTheme` and `systemTheme` required when `themes` lacks `light` or `dark`; `createTheme()` throws instead, naming the missing theme. A conditional parameter type would catch it at compile time, but it was rejected because it breaks generic wrappers that forward `ThemeOptions<N>` to `createTheme()`. `tokenSchema` maps the typed token names used in `tokens` to the CSS custom property each one writes to `document.documentElement`. See [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md) for how token values are resolved and stored, and [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md) for flash-of-unstyled-content prevention, `darkClass`, and `className`.

Construction validates the options and throws on invalid configuration, such as a `themes` or `systemTheme` value that is not a plain object, an empty theme name, a `defaultTheme`, `systemTheme`, or `pairedTheme` name that is not in `themes`, an invalid `colorScheme`, theme tokens outside `tokenSchema`, a `darkClass` that is not a boolean, an `attribute` of `class` or `style`, or, while `className` is `true`, a theme name that cannot form the default `theme-<name>` class. The [API reference](docs/reference/index.md) lists every rule.

An option set to `undefined` uses its default, exactly as an omitted one does. The option types accept `undefined`, so a wrapper that forwards an unset option, such as `storageKey: props.storageKey`, gets the default instead of a construction error. Rejecting `undefined` was the behavior before 0.2.0; it was dropped because it failed at runtime on code the types accept.

## Requirements

- Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`. Every one of these is read defensively: an unavailable or throwing API is treated as absent rather than crashing the caller.
- SSR is supported by skipping unavailable browser work and using the configured `defaultTheme`. The package cannot produce server HTML attributes or prevent a flash on its own — see [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md).
- Consumers provide CSS selectors, variables, and visual tokens. The package only ever writes an attribute, optional classes, `color-scheme`, and CSS custom properties — it ships no CSS or token values.
- TypeScript 5.4 or newer, for the types, which use `NoInfer` to infer theme names from `themes` alone. Older compilers report `Cannot find name 'NoInfer'`, or, with `skipLibCheck`, accept unconfigured theme names.

## Next steps

- [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md): How token values are resolved, how preferences persist to `localStorage` and sync across tabs, and how to react to changes.
- [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md): Preventing a flash of unstyled content before hydration, server-rendering considerations, and the classes the package applies.
- [API reference](docs/reference/index.md): Every exported function, type, and interface member with its full signature.

<!-- Source: docs/ssr-and-pre-paint.md -->

# SSR, Pre-Paint, and Tailwind

## Preventing a flash of unstyled content

A client-side `init()` call runs after the page has already painted once, which is visible as a flash from the default theme to the resolved one. `getPrePaintScript()` returns a synchronous, minified inline IIFE string that repeats the same stored-preference-then-system-preference resolution and applies the configured attribute, `color-scheme`, classes, and static tokens before the browser paints:

```ts
import { createTheme } from "@codenhub/theme";

import { themeOptions } from "./theme-options";

const script = createTheme(themeOptions).getPrePaintScript();
```

`createTheme()` is safe to call on the server, so one options module can build the script there and the manager in the browser, keeping the two in agreement. Inject the returned string into a blocking `<script>` in the document `<head>`, before any stylesheet or content that depends on the theme:

```html
<head>
  <script>
    /* inline the string returned by getPrePaintScript() here */
  </script>
</head>
```

The script handles storage and `matchMedia` failures the same way `init()` does: blocked storage still applies the system theme, and a missing `matchMedia` applies `defaultTheme`. It removes the classes of other configured themes, so a class rendered by the server is replaced rather than joined. It serializes only **static** tokens from each theme's `tokens`; runtime overrides from `setTokens()` exist only in the running page and cannot be part of a script that runs before any application code.

A framework that renders `<html>` itself and hydrates it, such as Next.js or Remix, finds the attribute, class, and `style` the script added and warns that they do not match the server markup. The difference is intended, so mark the element with React's `suppressHydrationWarning`, which silences attribute warnings on that element only:

```tsx
<html lang="en" suppressHydrationWarning>
```

### Content Security Policy

A `script-src` policy without `'unsafe-inline'` blocks the inline script, and the flash returns with only a console error to show for it. Allow it in either of two ways:

- **Nonce:** render the element as `<script nonce="…">` with the per-request nonce your policy already names.
- **Hash:** add `'sha256-…'` of the script text to `script-src`. The script is the same for the same options, so the hash can be computed once at build time, such as with `createHash("sha256").update(script).digest("base64")` from `node:crypto`. Inline the string byte for byte: any change, including whitespace around it inside the element, changes the hash.

## Server-side rendering

Without browser APIs, `Theme` methods skip storage reads/writes, DOM updates, and listener registration; `theme.system` falls back to `defaultTheme`. Every member remains safe to use during SSR — nothing throws for a missing `window`, `document`, or `localStorage` — but the package cannot produce server-rendered HTML attributes on its own. To avoid a flash on a server-rendered page, either:

- Render the server markup with the same attribute/class the pre-paint script would apply (requiring the server to read the same cookie or header a real theme service would use), or
- Rely on the pre-paint script above to correct the DOM before first paint, accepting that server-rendered markup itself does not carry theme state.

Keep any server-side resolution logic — stored names, system mapping, storage key, attribute, classes, color scheme — aligned with the options passed to `createTheme()`; a mismatch reintroduces the flash it exists to prevent.

## Tailwind's `dark` class

Set `darkClass: true` to add a `dark` class to `document.documentElement` while a theme whose `colorScheme` is `"dark"` is active, as Tailwind CSS's class-based `dark:` variant expects. This is independent of `className` (below) — a Tailwind app commonly wants both the `dark` class and, when `className` is left at its default, a `theme-<name>` class for custom per-theme CSS.

## Classes and cleanup

`className` controls the class applied to `document.documentElement` alongside the attribute: `true` (the default) applies `theme-<name>`, `false` applies no class, and a `ThemeClassResolver` function receives the theme with its static tokens and returns one custom class token. Code moving from a library configured with `attribute: "class"` wants `className: (theme) => theme.name` instead, which works while every theme name is a single class token without whitespace: `attribute` cannot be `class` or `style`, in any letter case, because writing the theme name there would replace every class or inline style the page put on `<html>`. A resolver runs once per theme, on first use rather than at construction, and the operation that triggered it throws if it returns an empty or whitespace-containing string.

`destroy(options?)` removes the media-query and storage listeners and clears in-process subscribers and runtime tokens, leaving the instance safe to `init()` again. By default it does **not** touch the DOM or stored preference. Pass `{ revertDom: true }` to also remove the configured attribute, classes, `color-scheme`, and CSS custom properties from `document.documentElement` — useful when unmounting a scoped theme manager that should leave no trace, such as in a component test or a preview pane.

<!-- Source: docs/tokens-and-persistence.md -->

# Tokens, Persistence, and Cross-Tab Sync

## Token resolution

`tokenSchema` maps typed token names to the CSS custom property each one writes, such as `{ primary: "--color-primary" }`. Each token can come from three sources, in this priority order (last wins):

1. **Computed style** — the value CSS gives the property on `document.documentElement`, for a token not defined in JS.
2. **Theme static tokens** — values set on the active theme's `tokens`.
3. **Runtime overrides** — values passed to `setTokens()`.

`get().tokens` holds the JS sources, 2 and 3, and never reads the DOM. `readTokens()` adds the computed values of the remaining tokens at the time of the call:

```ts
const theme = createTheme({
  themes: {
    light: { colorScheme: "light", tokens: { accent: "#ff6600" } },
    dark: { colorScheme: "dark" },
  },
  tokenSchema: { accent: "--color-accent", surface: "--color-surface" },
}).init();

theme.setTokens({ accent: "#00c853" }); // runtime override wins over the static "#ff6600"
theme.get().tokens; // { accent: "#00c853" }
theme.readTokens(); // { accent: "#00c853", surface: "<computed --color-surface>" }
```

Reading computed values can force a synchronous layout, so prefer `get().tokens` when tokens defined only in CSS are not needed. `readTokens()` skips the read when every token is known in JS.

Token values are strings. A finite number passed from JavaScript, such as a value read from a JSON config, is written as its string, so `get().tokens` reports `"4"` for `4`. An `undefined` or empty value counts as omitted, so `setTokens({ primary: undefined })` and `setTokens({ primary: "" })` leave the theme's static `primary` in place. An empty value is omitted rather than reported, because the browser removes a custom property set to `""` instead of applying it; a cleared color-picker input therefore falls back to the theme's value. Any other value, including `NaN` and `Infinity`, throws.

Token values are not parsed as CSS. When the browser rejects a value as a custom property value, such as `"red !important"` or `"red; color: blue"`, it is not applied and the property keeps whatever value it had, such as the previous theme's, while `get().tokens` still reports the rejected value, since `get()` never reads the DOM. Check values that come from user input, such as a color picker, before passing them to `setTokens()`.

Each `tokenSchema` value must be a custom property a stylesheet can name without escapes: `--` followed by ASCII letters, digits, `-`, `_`, or non-ASCII characters. Anything else throws from `createTheme()`, including `"--"` alone, which browsers do not apply, and a name with a space or `;`. Two keys cannot map to the same property, because applying a theme would write one key's value and then remove it for the other.

Runtime overrides apply to every theme and persist across theme changes until `setTokens()` replaces them — pass `{}` to remove them. They are not stored, so they last until the page unloads or `destroy()` runs, and they are not part of the pre-paint script. Passing a token key that is not in `tokenSchema` throws.

Before any theme is applied — before `init()`, `set()`, `toggle()`, or `clearPreference()` first runs, or after `destroy()` — `setTokens()` only records the overrides and returns `get()` with them; it leaves the DOM alone and notifies no one, and the next change applies them. Applying right away would mean applying `defaultTheme`, the only theme the manager knows before `init()`, which replaced the theme the pre-paint script had applied and flashed the wrong theme when `init()` ran later, such as in a component's effect. Resolving the stored or system theme at that point instead was rejected because it would be `init()` without its listeners, and its `"setTokens"` notification would announce a theme change.

## Persistence

An explicit preference set through `set()` or `toggle()` is written to `localStorage` under `storageKey` (default `"app-theme-preference"`). `theme.preference` returns that value when it names a configured theme, or `null` when it is unset, invalid, or storage is unavailable (for example during SSR). `clearPreference()` removes the stored value and applies `theme.system`, the theme the OS preference currently maps to.

Storage reads, writes, and removals that throw — a full quota, a disabled storage API, a private-browsing restriction — are treated the same as unavailable storage; they never throw out of `Theme` methods. Each kind of operation is logged with `console.error` the first time it fails for a manager, so a page with blocked storage logs one error per kind rather than one per read.

When storing a choice made with `set()` or `toggle()` fails, the manager still remembers the choice for as long as it runs, including one made before `init()`: OS color-scheme changes do not override it until `clearPreference()` or `destroy()` runs. `theme.preference` reports only what storage holds: `null` while storage is unavailable, or the previously stored theme when only writing fails, as with a full quota.

## Cross-tab sync

Every tab running `createTheme().init()` against the same `storageKey` stays in sync: when another tab sets, clears, or invalidates the stored preference — including through `localStorage.clear()` — each tab re-resolves its theme from storage and the OS preference and notifies with `source: "storage"`. `storage` events for `sessionStorage`, which a same-origin frame in the same tab fires, are ignored. A tab with its own runtime token overrides keeps them; only the active theme changes.

The OS `prefers-color-scheme` media query applies automatically only while no valid stored preference exists. Once a tab (or a previous session) has stored an explicit preference, later system-preference changes are ignored until `clearPreference()` runs.

## Reacting to changes

`subscribe(listener)` registers a callback invoked after every applied change and returns an unsubscribe function:

```ts
const unsubscribe = theme.subscribe(({ name, source }) => {
  console.log(`theme is now "${name}" (${source})`);
});
```

`source` names what caused the change: the method that ran (`"init"`, `"set"`, `"toggle"`, `"clearPreference"`, `"setTokens"`), `"system"` for an OS color-scheme change, or `"storage"` for a change made in another tab. Code that mirrors the preference elsewhere, such as to a user profile, should act on `"set"`, `"toggle"`, and `"clearPreference"` only, so that every open tab does not repeat the same write.

A subscriber that throws is logged and does not stop other subscribers from running. A subscriber or `themechange` listener that makes another change, such as calling `set()` or `setTokens()`, ends delivery of the change it received: the subscribers and listeners after it receive only the newer change, so none of them is left holding a superseded theme. One that calls `destroy()` ends delivery too, and no `themechange` event follows. A subscriber added during a delivery receives only later changes. Browsers also receive a `window` `CustomEvent` named by the exported `THEME_CHANGE_EVENT` constant (`"themechange"`) carrying the same detail, for code that has no access to the manager. Type it with the exported `ThemeChangeEvent`:

```ts
import { THEME_CHANGE_EVENT, type ThemeChangeEvent } from "@codenhub/theme";

window.addEventListener(THEME_CHANGE_EVENT, (event) => {
  console.log(`theme is now ${(event as ThemeChangeEvent).detail.name}`);
});
```

The event does not identify which manager sent it, so use `subscribe()` when a page runs more than one.

<!-- Source: docs/changelog/0.2.0.md -->

# 0.2.0

## Added

- Theme names are inferred from the keys of `themes`, so `defaultTheme`, `systemTheme`, `pairedTheme`, and `set()` reject unconfigured names at compile time.
- `theme.name`, `theme.preference`, and `theme.system` getters read the active name, the stored preference, and the theme the OS preference maps to.
- `setTokens(tokens)` replaces the runtime token overrides; pass `{}` to remove them. Changes it makes report `source: "setTokens"`. Called before `init()` or any other change, it only records the overrides, so it cannot replace the theme the pre-paint script applied.
- `readTokens()` returns every token, completed with computed CSS values at the time of the call.
- Changes made in another tab report `source: "storage"`, so code that mirrors the preference elsewhere can tell them from local changes.
- The `ThemeChangeEvent` type types the `themechange` window event, whose `detail` was untyped.

## Changed

- `themes` is an object keyed by theme name instead of an array: `{ light: { colorScheme: "light" } }` replaces `[{ name: "light", colorScheme: "light" }]`.
- Methods are bound to their manager, so `theme.toggle` and `theme.subscribe` can be passed as callbacks directly.
- `get()` returns the same frozen object until the next change and never reads the DOM, so it can serve as a store snapshot such as `useSyncExternalStore(theme.subscribe, theme.get)`. Its `tokens` holds static and runtime tokens only; call `readTokens()` for tokens defined only in CSS.
- `isTailwindCss` is renamed `darkClass`, and `shouldApplyClass` is renamed `className`.
- `Theme`, `ThemeOptions`, and the other generic types take the theme name union as their first type parameter, before the token schema.
- When another tab sets, clears, or invalidates the preference, every tab re-resolves its theme and notifies, even when the active theme stays the same, so a settings screen sees the preference change.
- Listener errors are logged as `[theme] Error in theme change listener:`, matching the package's other messages.
- A storage operation that fails is logged the first time it fails for a manager, not on every call, so a page with blocked storage no longer logs an error each time `theme.preference` is read.
- A theme or runtime token value that is not a string, a finite number, or `undefined` throws; a number is written as its string, which `get().tokens` now reports too. Strings are not parsed as CSS, so one the browser rejects is not applied but still appears in `get().tokens`; see [Token resolution](docs/tokens-and-persistence.md#token-resolution).
- A runtime token set to `undefined` counts as omitted, so `setTokens({ primary: undefined })` keeps the theme's static `primary` instead of removing it.
- A theme or runtime token set to `""` counts as omitted. The browser removed the property while `get().tokens` and `readTokens()` reported `""`; now an emptied override falls back to the theme's static token, and `readTokens()` reports that token, or the value CSS gives the property when the theme has none.
- `tokens` and `setTokens()` are rejected at compile time when `createTheme()` infers its types from options without a `tokenSchema`, since they always threw. `Theme` and `ThemeOptions` keep their broad default schema, so code typed with them, such as a generic wrapper, still compiles them and gets the runtime error.
- `createTheme()` throws `Theme options must be an object.` for options such as `null`, instead of an unrelated `TypeError`.
- `createTheme()` throws for an `attribute` containing NUL, which `setAttribute()` rejects, and for an `attribute` of `class` or `style` in any letter case, which replaced every class or inline style the page put on `<html>` with the theme name. To apply the theme name as a class, use `className: (theme) => theme.name`.
- A `tokenSchema` value must be a custom property name a stylesheet can use without escapes, such as `--color-primary`. `"--"` alone, which browsers ignored, and names with a space or `;` now throw from `createTheme()`. Two keys mapping to the same property also throw: applying a theme wrote one key's value and removed it for the other, so the pre-paint script and `init()` disagreed.

## Removed

- The standalone `getPrePaintScript(options)` export. Use `createTheme(options).getPrePaintScript()`, which is safe to call on the server.
- The `LIGHT_THEME` and `DARK_THEME` exports; the built-in themes are the default `themes` value.
- `getStored()`, `getSystem()`, and `get({ skipComputed })`; use `preference`, `system`, and `get()` with `readTokens()`.
- The `tokens` parameter of `init()`, `set()`, and `toggle()`; use `setTokens()`.

## Fixed

- Calling `theme.toggle` as an event listener no longer throws, and no longer treats the event as token overrides.
- A snapshot taken before a theme change no longer reports the new theme's computed tokens under the old theme's name.
- The pre-paint script applies the OS theme when `localStorage` access throws, applies `defaultTheme` when `matchMedia` is missing as `init()` does, and replaces a server-rendered theme class instead of adding a second one.
- `localStorage.clear()` in another tab now applies the system theme.
- An option set to `undefined`, such as `storageKey: props.storageKey` forwarded by a wrapper, uses its default instead of making `createTheme()` throw. The option, token value, and `destroy()` option types accept `undefined` under TypeScript's `exactOptionalPropertyTypes` too.
- A token named after an `Object.prototype` member, such as `constructor`, is handled as an ordinary token instead of writing the inherited function to the DOM.
- A `systemTheme` that is not an object, or a `darkClass` that is not a boolean, throws from `createTheme()` instead of being silently accepted.
- The pre-paint script falls back as for an unknown name when the stored value is an `Object.prototype` property such as `toString`, and applies a configured theme named `__proto__`. ([#345](https://github.com/codenhub/codenhub/issues/345))
- Invalid options, such as a `defaultTheme` naming an unconfigured theme, throw from `createTheme()` instead of reaching the page through the pre-paint script. ([#364](https://github.com/codenhub/codenhub/issues/364))
- A subscriber or `themechange` listener that changes the theme, such as by calling `set()` or `setTokens()`, no longer leaves the subscribers and `themechange` listeners after it with the superseded change. One that calls `destroy()` ends the delivery too, and a subscriber added during a delivery no longer receives the change being delivered.
- `storage` events for `sessionStorage`, fired when a same-origin frame in the tab changes it, are ignored. A frame calling `sessionStorage.clear()` re-applied the theme with `source: "storage"`, and discarded a choice that could not be stored.
- When storing fails, an explicit choice made with `set()` or `toggle()`, before or after `init()`, is no longer overridden by `init()` or the next OS color-scheme change.
- The `themechange` event is built with the window's own `CustomEvent`, so `init()` and other changes no longer throw when the global `CustomEvent` belongs to another realm, as in a hand-built jsdom setup.

## Security

- Theme names, option values, and token values can no longer close the inline `<script>` element or open an HTML comment in the pre-paint script.

<!-- Source: docs/changelog/0.1.2.md -->

# 0.1.2

## Added

- A generated API reference, `docs/reference/index.md`, covering every exported symbol from its TSDoc directly — regenerated on every release instead of hand-maintained separately, so it can't drift out of sync with the source.

## Changed

- Documentation split from one page, `docs/reference.md`, into three focused ones: `docs/index.md` (setup and configuration), `docs/tokens-and-persistence.md` (token resolution, `skipComputed`, persistence, cross-tab sync), and `docs/ssr-and-pre-paint.md` (`getPrePaintScript()`, SSR fallback, Tailwind's `isTailwindCss`, `shouldApplyClass`, `destroy({ revertDom: true })`), plus the generated reference above. Update any link that pointed at `docs/reference.md`.
