/** What a person picked. `"system"` means "follow the operating system", live. */
export type ThemeMode = "light" | "dark" | "system";
/** What is on screen right now. */
export type ResolvedTheme = "light" | "dark";
export interface ThemeOptions {
/**
* The `localStorage` key holding the choice. Keep the key your app already uses: a new key
* resets everybody's choice once.
*/
key: string;
/**
* What an empty, unreadable or unrecognised key means. Defaults to `"system"`.
*/
defaultMode?: ThemeMode;
/**
* Where the resolved theme goes on ``. `"class"` toggles `dark`, which is what
* Tailwind's `dark:` variant and `@gusnips/tokens` read. `"data-theme"` writes `"light"` or
* `"dark"` into that attribute, for a stylesheet whose `@custom-variant` reads it. Defaults
* to `"class"`.
*/
attribute?: "class" | "data-theme";
/**
* The browser toolbar colour for each theme, written to every ``.
* Leave it out and the tags are not touched. It is needed whenever the choice can differ from
* the OS setting: a `media` query on the tag follows the OS, not your app.
*/
themeColor?: Readonly>;
}
export interface ThemeSnapshot {
readonly mode: ThemeMode;
readonly resolved: ResolvedTheme;
}
export interface ThemeController {
getSnapshot(): ThemeSnapshot;
/** Applies at once, and is remembered when storage allows it. */
setMode(mode: ThemeMode): void;
subscribe(listener: () => void): () => void;
}
declare global {
interface Window {
/** The page's one theme controller. See {@link startTheme}. */
__frontkitTheme?: ThemeController;
}
}
/**
* Reads the choice, paints it, and keeps it painted: when the OS switches (a laptop that goes
* dark at sunset), when another tab picks a theme, and when the page comes back from the
* back-forward cache.
*
* **This function is serialized.** `@gusnips/vite/theme` writes `startTheme.toString()` into the
* pre-paint file, so it must not reference anything outside its own body: no imports, no helpers
* or constants from this module. It would still pass here and throw a `ReferenceError` in that
* file, before first paint. The vite package's test runs the serialized copy in a bare context
* to catch that. Comments go up here rather than in the body, because the body ships.
*
* **One per page.** The first call stores the controller on `window` and later calls return it,
* whatever options they pass. Two would both follow the OS, and the one that missed a `setMode`
* would repaint its stale choice on the next change. So the pre-paint script and the app must
* pass the same options, which is easiest when both import them from one module.
*
* **Blocked storage is not an empty key.** When storage throws, the default applies at boot, and
* a choice made afterwards lasts until the page is left. A resync never replaces that choice
* with the default, because a read that failed says nothing about what was chosen.
*
* "system" is stored as the word, not as an empty key. An empty key means the default, and the
* default is not always "system".
*
* **It writes to the page when the theme changes, and at no other time.** A page may override the
* class for its own lifetime, as a public booking page that is always light does, and keeps that
* override until the theme really changes. Repainting on every resync would undo it a moment later,
* because `pageshow` fires on every load, not only on a back-forward restore.
*/
export declare function startTheme(options: ThemeOptions): ThemeController;
export interface Theme {
/** The choice and what it resolves to, re-rendering when either changes. */
useTheme(): ThemeSnapshot & {
setMode(mode: ThemeMode): void;
};
/** For code outside React: a keyboard shortcut, a command palette. Browser only. */
setMode(mode: ThemeMode): void;
/** For code outside React. Answers the prerender snapshot where there is no window. */
getSnapshot(): ThemeSnapshot;
}
/**
* The React side of {@link startTheme}. Nothing starts at import, so a module calling this at
* its top level is safe to prerender. The controller starts on first use, or is found if the
* pre-paint script already started it.
*
* During a prerender, and while hydrating, `useTheme` answers the default. `"system"` cannot be
* resolved without a window, so it reads as light, as `color-scheme` does before the pre-paint
* script runs. React then re-renders with the real snapshot, so a toggle hydrates cleanly and
* shows the right choice a frame later instead of failing to hydrate.
*/
export declare function createTheme(options: ThemeOptions): Theme;
//# sourceMappingURL=theme.d.ts.map