@codenhub/theme 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,22 +26,19 @@ theme.destroy({ revertDom: true });
26
26
  ## Documentation
27
27
 
28
28
  - [Documentation overview](docs/index.md)
29
- - [API, persistence, DOM, and SSR behavior](docs/reference.md)
29
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)
30
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md)
31
+ - [API reference](docs/reference/index.md)
30
32
 
31
33
  ## Requirements
32
34
 
33
- - Browser integration uses `document.documentElement`, `localStorage`,
34
- `matchMedia`, `storage` events, and `CustomEvent`.
35
- - SSR is supported by skipping unavailable browser work and using the configured
36
- default theme.
37
- - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint
38
- script needed to prevent a theme flash.
35
+ - Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`.
36
+ - SSR is supported by skipping unavailable browser work and using the configured default theme.
37
+ - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint script needed to prevent a theme flash.
39
38
 
40
39
  ## Notes
41
40
 
42
- Construction validates theme names, mappings, attributes, classes, and token
43
- schemas and throws on invalid configuration. Storage failures are reported to
44
- the console and treated as unavailable storage.
41
+ 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.
45
42
 
46
43
  ## License
47
44
 
package/dist/index.d.ts CHANGED
@@ -94,11 +94,10 @@ interface Theme<TSchema extends Record<string, string> = Record<string, string>>
94
94
  */
95
95
  set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
96
96
  /**
97
- * Switches the theme between the configured system light and dark themes and persists the change.
98
- * The next theme is always selected from `systemTheme.light` or `systemTheme.dark` based on the
99
- * active theme's `colorScheme`, not by cycling the active theme name. In multi-theme setups where
100
- * the active theme is not one of the system themes, `toggle()` still targets `systemTheme.light`
101
- * or `systemTheme.dark`.
97
+ * Switches away from the active theme and persists the change. If the active theme configures a
98
+ * `pairedTheme`, that theme is activated. Otherwise the next theme is selected from
99
+ * `systemTheme.light` or `systemTheme.dark` based on the active theme's `colorScheme`, not by
100
+ * cycling the active theme name.
102
101
  *
103
102
  * @param tokens - Optional runtime override token values to apply. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
104
103
  * @returns The activated `ThemeDefinition` with merged and resolved tokens.
@@ -140,9 +139,10 @@ interface Theme<TSchema extends Record<string, string> = Record<string, string>>
140
139
  */
141
140
  getPrePaintScript(): string;
142
141
  /**
143
- * Cleans up the theme instance by removing all in-process change listeners and the system
144
- * preference media query listener. Resets active tokens and the active theme name to the
145
- * configured `defaultTheme` so the instance can be safely re-initialized with `init()`.
142
+ * Cleans up the theme instance by removing all in-process change listeners, the system
143
+ * preference media query listener, and the cross-tab storage listener. Resets active tokens
144
+ * and the active theme name to the configured `defaultTheme` so the instance can be safely
145
+ * re-initialized with `init()`.
146
146
  *
147
147
  * @param options - Optional cleanup options. Set `revertDom: true` to remove configured DOM attributes, classes, and CSS custom properties applied to `document.documentElement`.
148
148
  * @sideEffect Removes event listeners from `window` and clears internal subscriber sets. If `revertDom: true`, removes theme attributes, classes, and custom properties from the DOM root.
@@ -166,7 +166,8 @@ declare const DARK_THEME: ThemeDefinition;
166
166
  *
167
167
  * @param options - Configuration options for theme definitions, persistence keys, DOM attributes, custom class resolvers, and dynamic token schemas.
168
168
  * @returns A `Theme` instance.
169
- * @throws {Error} If configured theme names are empty, duplicated, invalid for CSS class application, or if the default/system themes are not present in the configured list.
169
+ * @throws {Error} If configured theme names are empty, duplicated, invalid for CSS class application, have an
170
+ * invalid `colorScheme`, or if the default/system themes are not present in the configured list.
170
171
  */
171
172
  declare function createTheme<TSchema extends Record<string, string> = Record<string, string>>(options?: ThemeOptions<TSchema>): Theme<TSchema>;
172
173
  //#endregion
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: 0.1.2
3
+ date: 2026-09-15
4
+ description: A generated API reference and reorganized documentation.
5
+ ---
6
+
7
+ # 0.1.2
8
+
9
+ ## Added
10
+
11
+ - 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.
12
+
13
+ ## Changed
14
+
15
+ - 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`.
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Changelog
3
+ curated: true
4
+ group: Changelog
5
+ ---
6
+
7
+ # Changelog
8
+
9
+ - [0.1.2](0.1.2.md)
package/docs/index.md CHANGED
@@ -4,13 +4,7 @@ title: Overview
4
4
 
5
5
  # Manage Browser Themes
6
6
 
7
- `@codenhub/theme` resolves stored and system theme preferences, applies theme
8
- state to the document root, synchronizes browser tabs, exposes change events,
9
- and maps typed tokens to CSS custom properties.
10
-
11
- It fits browser applications that need one owner for theme preference and DOM
12
- synchronization while keeping selectors, visual design, and CSS in application
13
- code.
7
+ `@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, 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.
14
8
 
15
9
  ## Setup
16
10
 
@@ -29,24 +23,38 @@ const theme = createTheme().init();
29
23
  theme.set("dark");
30
24
  ```
31
25
 
32
- Call `destroy()` when the manager's owner is removed. The package persists an
33
- explicit preference and follows system preference when appropriate.
26
+ `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`. 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 [Tokens, persistence, and cross-tab sync](tokens-and-persistence.md)).
34
27
 
35
- ## Requirements
28
+ ### Configuration
29
+
30
+ Most applications configure at least `themes` and `tokenSchema` up front:
31
+
32
+ ```ts
33
+ import { createTheme } from "@codenhub/theme";
36
34
 
37
- - Browser integration uses `document.documentElement`, `localStorage`,
38
- `matchMedia`, `storage` events, and `CustomEvent`.
39
- - SSR is supported by skipping unavailable browser work and using the configured
40
- default theme.
41
- - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint
42
- script needed to prevent a theme flash.
35
+ const theme = createTheme({
36
+ themes: [
37
+ { name: "light", colorScheme: "light", tokens: { primary: "#171717" } },
38
+ { name: "dark", colorScheme: "dark", tokens: { primary: "#f9fafb" } },
39
+ ],
40
+ tokenSchema: { primary: "--color-primary" },
41
+ storageKey: "app-theme-preference",
42
+ isTailwindCss: true,
43
+ }).init();
44
+ ```
45
+
46
+ `themes` replaces the built-in list entirely — include `light` and `dark` (or whatever names `defaultTheme` and `systemTheme` point at) when defining custom themes. `tokenSchema` maps the typed token names used in `ThemeDefinition.tokens` and `get()` to the CSS custom property each one writes to `document.documentElement`. See [Tokens, persistence, and cross-tab sync](tokens-and-persistence.md) for how token values are resolved and stored, and [SSR, pre-paint, and Tailwind](ssr-and-pre-paint.md) for flash-of-unstyled-content prevention and OS-preference-only applications like `isTailwindCss`.
47
+
48
+ Construction validates every option and throws on invalid configuration: empty or duplicate theme names, a `defaultTheme` or `systemTheme` name that is not in `themes`, an invalid `colorScheme`, or a `shouldApplyClass` resolver that would produce an empty or whitespace-containing class.
49
+
50
+ ## Requirements
43
51
 
44
- The package cannot produce server HTML attributes or prevent a flash before
45
- initialization. Applications that need pre-paint consistency must apply matching
46
- theme logic before rendering.
52
+ - 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.
53
+ - 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](ssr-and-pre-paint.md).
54
+ - Consumers provide CSS selectors, variables, and visual tokens. The package only ever writes an attribute, optional classes, and CSS custom properties — it ships no CSS or token values beyond the built-in `light`/`dark` definitions' names.
47
55
 
48
56
  ## Next steps
49
57
 
50
- - [API, persistence, DOM, and SSR behavior](reference.md): Complete exports,
51
- configuration, tokens, browser synchronization, validation, pre-paint
52
- concerns, and cleanup.
58
+ - [Tokens, persistence, and cross-tab sync](tokens-and-persistence.md): How `get()` resolves token values, how preferences persist to `localStorage` and sync across tabs, and how to read or clear the stored preference.
59
+ - [SSR, pre-paint, and Tailwind](ssr-and-pre-paint.md): Preventing a flash of unstyled content before hydration, server-rendering considerations, and toggling Tailwind's `dark` class.
60
+ - [API reference](reference/index.md): Every exported function, type, and interface member with its full signature.
@@ -0,0 +1,381 @@
1
+ ---
2
+ title: /
3
+ group: Reference
4
+ ---
5
+
6
+ <!-- Generated by `pnpm generate` from packages/theme/src. Do not edit. -->
7
+
8
+ # @codenhub/theme
9
+
10
+ ## Functions
11
+
12
+ ### createTheme
13
+
14
+ ```ts
15
+ export declare function createTheme<TSchema extends Record<string, string> = Record<string, string>>(options?: ThemeOptions<TSchema>): Theme<TSchema>;
16
+ ```
17
+
18
+ Factory function that creates and returns a `Theme` instance.
19
+
20
+ **Parameters**
21
+
22
+ - `options` — Configuration options for theme definitions, persistence keys, DOM attributes, custom class resolvers, and dynamic token schemas.
23
+
24
+ **Returns** — A `Theme` instance.
25
+
26
+ **Throws** — If configured theme names are empty, duplicated, invalid for CSS class application, have an invalid `colorScheme`, or if the default/system themes are not present in the configured list.
27
+
28
+ ### getPrePaintScript
29
+
30
+ ```ts
31
+ export declare function getPrePaintScript<TSchema extends Record<string, string> = Record<string, string>>(options?: ThemeOptions<TSchema>): string;
32
+ ```
33
+
34
+ Generates a synchronous inline IIFE script string to inject into document `<head>`. Prevents Flash of Unstyled Content (FOUC) by applying storage or system theme before render.
35
+
36
+ **Parameters**
37
+
38
+ - `options` — Configuration options used to determine storage keys, attributes, default/system themes, custom class resolvers, and token schemas.
39
+
40
+ **Returns** — Minified JavaScript script string.
41
+
42
+ ## Interfaces
43
+
44
+ ### SystemThemeMap
45
+
46
+ ```ts
47
+ export interface SystemThemeMap
48
+ ```
49
+
50
+ Mapping from OS color-scheme preferences to configured theme names.
51
+
52
+ #### dark
53
+
54
+ ```ts
55
+ dark: string;
56
+ ```
57
+
58
+ Configured theme name used when the OS preference is dark.
59
+
60
+ #### light
61
+
62
+ ```ts
63
+ light: string;
64
+ ```
65
+
66
+ Configured theme name used when the OS preference is light or no dark preference is detected.
67
+
68
+ ### Theme
69
+
70
+ ```ts
71
+ export interface Theme<TSchema extends Record<string, string> = Record<string, string>>
72
+ ```
73
+
74
+ Core theme preference handler. Manages initialization, switching themes, persistence to localStorage, synchronizing with the OS prefers-color-scheme preference, dynamic token mapping to CSS Custom Properties, and dispatching change events.
75
+
76
+ #### clearPreference
77
+
78
+ ```ts
79
+ clearPreference(): ThemeDefinition<TSchema>;
80
+ ```
81
+
82
+ Removes the explicit user theme preference from storage and resets the theme to match the OS system preference.
83
+
84
+ #### destroy
85
+
86
+ ```ts
87
+ destroy(options?: { revertDom?: boolean; }): void;
88
+ ```
89
+
90
+ Cleans up the theme instance by removing all in-process change listeners, the system preference media query listener, and the cross-tab storage listener. Resets active tokens and the active theme name to the configured `defaultTheme` so the instance can be safely re-initialized with `init()`.
91
+
92
+ **Parameters**
93
+
94
+ - `options` — Optional cleanup options. Set `revertDom: true` to remove configured DOM attributes, classes, and CSS custom properties applied to `document.documentElement`.
95
+
96
+ #### get
97
+
98
+ ```ts
99
+ get(options?: { skipComputed?: boolean; }): ThemeDefinition<TSchema>;
100
+ ```
101
+
102
+ Retrieves the active theme configuration including static and computed tokens.
103
+
104
+ Token values are merged in this priority order (last wins):
105
+
106
+ 1. CSS computed style — values read from `window.getComputedStyle` for tokens not defined in JS (unless `options.skipComputed` is true).
107
+ 2. Theme static tokens — values defined in `ThemeDefinition.tokens` for the active theme.
108
+ 3. Runtime overrides — values passed to `init()`, `set()`, `toggle()`, or other methods.
109
+
110
+ **Parameters**
111
+
112
+ - `options` — Optional getter options. Set `skipComputed: true` to avoid reading computed styles from the DOM via `window.getComputedStyle`, preventing potential layout reflows.
113
+
114
+ #### getPrePaintScript
115
+
116
+ ```ts
117
+ getPrePaintScript(): string;
118
+ ```
119
+
120
+ Generates a synchronous inline IIFE script string to inject into document `<head>`. Prevents Flash of Unstyled Content (FOUC) by applying storage or system theme before render.
121
+
122
+ #### getStored
123
+
124
+ ```ts
125
+ getStored(): string | null;
126
+ ```
127
+
128
+ Retrieves the currently stored theme preference name from `localStorage`.
129
+
130
+ #### getSystem
131
+
132
+ ```ts
133
+ getSystem(): ThemeDefinition<TSchema>;
134
+ ```
135
+
136
+ Resolves the configured theme that matches the active OS color-scheme preference.
137
+
138
+ #### init
139
+
140
+ ```ts
141
+ init(tokens?: Partial<Record<keyof TSchema, string>>): this;
142
+ ```
143
+
144
+ Initializes the theme manager. Resolves the active theme (using the stored preference if valid, falling back to the current OS color-scheme preference), applies classes/attributes to the DOM, and registers the media query listener for automatic system preference updates.
145
+
146
+ **Parameters**
147
+
148
+ - `tokens` — Optional runtime override token values to merge and apply.
149
+
150
+ #### set
151
+
152
+ ```ts
153
+ set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
154
+ ```
155
+
156
+ Activates a configured theme by name and updates the stored preference in `localStorage`.
157
+
158
+ **Parameters**
159
+
160
+ - `name` — The name of the configured theme to activate.
161
+ - `tokens` — Optional runtime override token values to apply. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
162
+
163
+ #### subscribe
164
+
165
+ ```ts
166
+ subscribe(listener: ThemeChangeListener<TSchema>): () => void;
167
+ ```
168
+
169
+ Registers a callback listener to receive notifications when the theme or its tokens change.
170
+
171
+ **Parameters**
172
+
173
+ - `listener` — Callback function invoked on theme changes.
174
+
175
+ #### toggle
176
+
177
+ ```ts
178
+ toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
179
+ ```
180
+
181
+ Switches away from the active theme and persists the change. If the active theme configures a `pairedTheme`, that theme is activated. Otherwise the next theme is selected from `systemTheme.light` or `systemTheme.dark` based on the active theme's `colorScheme`, not by cycling the active theme name.
182
+
183
+ **Parameters**
184
+
185
+ - `tokens` — Optional runtime override token values to apply. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
186
+
187
+ ### ThemeChangeDetail
188
+
189
+ ```ts
190
+ export interface ThemeChangeDetail<TSchema extends Record<string, string> = Record<string, string>>
191
+ ```
192
+
193
+ Payload passed to subscribers and the browser `themechange` event after a theme change.
194
+
195
+ #### name
196
+
197
+ ```ts
198
+ name: string;
199
+ ```
200
+
201
+ Active theme name after the change.
202
+
203
+ #### source
204
+
205
+ ```ts
206
+ source: ThemeChangeSource;
207
+ ```
208
+
209
+ Operation or browser signal that caused the change notification.
210
+
211
+ #### theme
212
+
213
+ ```ts
214
+ theme: ThemeDefinition<TSchema>;
215
+ ```
216
+
217
+ Active theme definition after the change.
218
+
219
+ ### ThemeDefinition
220
+
221
+ ```ts
222
+ export interface ThemeDefinition<TSchema extends Record<string, string> = Record<string, string>>
223
+ ```
224
+
225
+ Theme option stored, applied to the configured DOM attribute, and mapped to the browser color scheme.
226
+
227
+ #### colorScheme
228
+
229
+ ```ts
230
+ colorScheme: "light" | "dark";
231
+ ```
232
+
233
+ Browser color scheme applied to `document.documentElement.style.colorScheme`.
234
+
235
+ #### name
236
+
237
+ ```ts
238
+ name: string;
239
+ ```
240
+
241
+ Unique configured theme name used for storage, DOM attributes, and generated default classes.
242
+
243
+ #### pairedTheme
244
+
245
+ ```ts
246
+ pairedTheme?: string;
247
+ ```
248
+
249
+ Optional theme name activated when calling `toggle()` from this theme.
250
+
251
+ #### tokens
252
+
253
+ ```ts
254
+ tokens?: Partial<Record<keyof TSchema, string>>;
255
+ ```
256
+
257
+ Optional theme-specific static token values.
258
+
259
+ ### ThemeOptions
260
+
261
+ ```ts
262
+ export interface ThemeOptions<TSchema extends Record<string, string> = Record<string, string>>
263
+ ```
264
+
265
+ Configuration for theme definitions, persistence, DOM application, and system preference mapping.
266
+
267
+ #### attribute
268
+
269
+ ```ts
270
+ attribute?: string;
271
+ ```
272
+
273
+ Attribute set on `document.documentElement` with the active theme name.
274
+
275
+ #### defaultTheme
276
+
277
+ ```ts
278
+ defaultTheme?: string;
279
+ ```
280
+
281
+ Configured theme name used before initialization and when browser APIs are unavailable.
282
+
283
+ #### isTailwindCss
284
+
285
+ ```ts
286
+ isTailwindCss?: boolean;
287
+ ```
288
+
289
+ Whether to toggle Tailwind CSS's `dark` class for themes with `colorScheme: "dark"`.
290
+
291
+ #### shouldApplyClass
292
+
293
+ ```ts
294
+ shouldApplyClass?: boolean | ThemeClassResolver<TSchema>;
295
+ ```
296
+
297
+ Whether and how to apply a theme-specific class to `document.documentElement`.
298
+
299
+ #### storageKey
300
+
301
+ ```ts
302
+ storageKey?: string;
303
+ ```
304
+
305
+ `localStorage` key used for explicit user preferences.
306
+
307
+ #### systemTheme
308
+
309
+ ```ts
310
+ systemTheme?: SystemThemeMap;
311
+ ```
312
+
313
+ Configured theme names selected for OS light and dark color-scheme preferences.
314
+
315
+ #### themes
316
+
317
+ ```ts
318
+ themes?: readonly ThemeDefinition<TSchema>[];
319
+ ```
320
+
321
+ Available themes. Names must be unique, non-empty, and valid default class tokens when `shouldApplyClass` is `true`.
322
+
323
+ #### tokenSchema
324
+
325
+ ```ts
326
+ tokenSchema?: TSchema;
327
+ ```
328
+
329
+ Schema mapping theme token names to their corresponding CSS Custom Property names.
330
+
331
+ ## Type aliases
332
+
333
+ ### ThemeChangeListener
334
+
335
+ ```ts
336
+ export type ThemeChangeListener<TSchema extends Record<string, string> = Record<string, string>> = (detail: ThemeChangeDetail<TSchema>) => void;
337
+ ```
338
+
339
+ In-process callback registered with `Theme.subscribe()` for applied theme changes.
340
+
341
+ ### ThemeChangeSource
342
+
343
+ ```ts
344
+ export type ThemeChangeSource = "init" | "set" | "toggle" | "clearPreference" | "system";
345
+ ```
346
+
347
+ Reason a theme change notification was emitted.
348
+
349
+ ### ThemeClassResolver
350
+
351
+ ```ts
352
+ export type ThemeClassResolver<TSchema extends Record<string, string> = Record<string, string>> = (theme: ThemeDefinition<TSchema>) => string;
353
+ ```
354
+
355
+ Resolves the single DOM class token applied for a theme when custom class application is enabled.
356
+
357
+ ## Variables
358
+
359
+ ### DARK_THEME
360
+
361
+ ```ts
362
+ export declare const DARK_THEME: ThemeDefinition;
363
+ ```
364
+
365
+ Built-in dark theme used by default and available for custom theme lists.
366
+
367
+ ### LIGHT_THEME
368
+
369
+ ```ts
370
+ export declare const LIGHT_THEME: ThemeDefinition;
371
+ ```
372
+
373
+ Built-in light theme used by default and available for custom theme lists.
374
+
375
+ ### THEME_CHANGE_EVENT
376
+
377
+ ```ts
378
+ export declare const THEME_CHANGE_EVENT = "themechange";
379
+ ```
380
+
381
+ Window event name dispatched with `ThemeChangeDetail` after a theme change is applied in browser environments.
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: SSR, Pre-Paint, and Tailwind
3
+ ---
4
+
5
+ # SSR, Pre-Paint, and Tailwind
6
+
7
+ ## Preventing a flash of unstyled content
8
+
9
+ 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()` — available as a method on an existing manager, or as the standalone `getPrePaintScript(options)` export when no manager exists yet — returns a synchronous, minified inline IIFE string that duplicates the same stored-preference-then-system-preference resolution, and applies the configured attribute, classes, and any static tokens before the browser paints:
10
+
11
+ ```ts
12
+ import { getPrePaintScript } from "@codenhub/theme";
13
+
14
+ const script = getPrePaintScript({ storageKey: "app-theme-preference" });
15
+ ```
16
+
17
+ Inject the returned string into a blocking `<script>` in the document `<head>`, before any stylesheet or content that depends on the theme:
18
+
19
+ ```html
20
+ <head>
21
+ <script>
22
+ /* inline the string returned by getPrePaintScript() here */
23
+ </script>
24
+ </head>
25
+ ```
26
+
27
+ Pass the same `themes`, `tokenSchema`, `defaultTheme`, `systemTheme`, `storageKey`, `attribute`, `shouldApplyClass`, and `isTailwindCss` options used to construct the manager, so the pre-paint script and the later `init()` call resolve to the same theme. `getPrePaintScript()` only serializes **static** tokens from each theme's `ThemeDefinition.tokens` — and only when `tokenSchema` is passed to it — runtime overrides passed to `init(tokens)` are applied after hydration, during client-side execution, and cannot be part of a script that runs before any application code.
28
+
29
+ ## Server-side rendering
30
+
31
+ Without browser APIs, `Theme` methods skip storage reads/writes, DOM updates, and listener registration; `getSystem()` falls back to `defaultTheme`. Every method call remains safe to make 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:
32
+
33
+ - 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
34
+ - 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.
35
+
36
+ Keep any server-side resolution logic — stored names, system mapping, storage key, attribute, classes, color scheme — aligned with the options passed to `createTheme()` and `getPrePaintScript()`; a mismatch reintroduces the flash it exists to prevent.
37
+
38
+ ## Tailwind's `dark` class
39
+
40
+ Set `isTailwindCss: true` to toggle Tailwind CSS's `dark` class on `document.documentElement` alongside the configured attribute, for every theme whose `colorScheme` is `"dark"`. This is independent of `shouldApplyClass` (below) — a Tailwind app commonly wants both the `dark` class for Tailwind's `dark:` variant and, when `shouldApplyClass` is left at its default, a `theme-${name}` class for custom per-theme CSS.
41
+
42
+ ## Classes and cleanup
43
+
44
+ `shouldApplyClass` controls whether and how a class is applied to `document.documentElement` alongside the attribute: `true` (the default) applies `theme-${name}`, `false` applies no class, and a `ThemeClassResolver` function receives the active `ThemeDefinition` and returns one custom class token — throwing if it returns an empty or whitespace-containing string.
45
+
46
+ `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, 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.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Tokens, Persistence, and Cross-Tab Sync
3
+ ---
4
+
5
+ # Tokens, Persistence, and Cross-Tab Sync
6
+
7
+ ## Token resolution
8
+
9
+ `tokenSchema` maps typed token names to the CSS custom property each one writes, such as `{ primary: "--color-primary" }`. Once configured, `get()` returns the active `ThemeDefinition` with every schema key resolved from three sources, in this priority order (last wins):
10
+
11
+ 1. **Computed style** — for a token not defined in JS, the value is read from `window.getComputedStyle(document.documentElement)` in browser environments.
12
+ 2. **Theme static tokens** — values set on the active theme's own `ThemeDefinition.tokens`.
13
+ 3. **Runtime overrides** — values passed to `init()`, `set()`, or `toggle()`.
14
+
15
+ ```ts
16
+ const theme = createTheme({
17
+ themes: [{ name: "brand", colorScheme: "light", tokens: { accent: "#ff6600" } }],
18
+ tokenSchema: { accent: "--color-accent" },
19
+ }).init();
20
+
21
+ theme.set("brand", { accent: "#00c853" }); // runtime override wins over the static "#ff6600"
22
+ theme.get().tokens?.accent; // "#00c853"
23
+ ```
24
+
25
+ Reading computed values can force a synchronous layout reflow. Pass `{ skipComputed: true }` to `get()` to skip that source and avoid the reflow when only the JS-known values are needed.
26
+
27
+ Runtime overrides persist across subsequent theme changes until explicitly replaced — pass a new object, including `{}`, to `init()`, `set()`, or `toggle()` to clear or replace them. Passing a token key that is not in `tokenSchema` throws.
28
+
29
+ ## Persistence
30
+
31
+ An explicit preference set through `set()` or `toggle()` is written to `localStorage` under `storageKey` (default `"app-theme-preference"`). `getStored()` returns that value when it names a currently configured theme, or `null` when it is unset, invalid, or storage is unavailable (for example during SSR). `clearPreference()` removes the stored value and re-applies whichever theme `getSystem()` currently resolves to.
32
+
33
+ Storage reads, writes, and removals that throw — a full quota, a disabled storage API, a private-browsing restriction — are logged with `console.error` and treated the same as unavailable storage; they never throw out of `Theme` methods.
34
+
35
+ ## Cross-tab sync
36
+
37
+ Every tab running `createTheme().init()` against the same `storageKey` stays in sync: a `storage` event fires in every other tab when one tab writes a new preference, and each one applies the change if it names a valid configured theme. A tab with its own runtime token overrides keeps them — only the active theme name changes from cross-tab sync, not tokens passed in-process.
38
+
39
+ 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.
40
+
41
+ ## Reacting to changes
42
+
43
+ `subscribe(listener)` registers a callback invoked after every applied change — from `init()`, `set()`, `toggle()`, `clearPreference()`, or an accepted `system` or cross-tab update — and returns an unsubscribe function:
44
+
45
+ ```ts
46
+ const unsubscribe = theme.subscribe(({ name, source }) => {
47
+ console.log(`theme is now "${name}" (${source})`);
48
+ });
49
+ ```
50
+
51
+ A subscriber that throws is logged and does not stop other subscribers from running. Browsers also receive a `window` `CustomEvent` named by the exported `THEME_CHANGE_EVENT` constant (`"themechange"`) carrying the same detail, for code that prefers DOM events over `subscribe()`.
package/llms-full.txt CHANGED
@@ -1,204 +1,213 @@
1
- <!-- Source: README.md -->
2
-
3
- # @codenhub/theme
4
-
5
- Zero-dependency browser theme preference, persistence, DOM, and token manager.
6
-
7
- ## Installation
8
-
9
- ```sh
10
- pnpm add @codenhub/theme
11
- ```
12
-
13
- ## Usage
14
-
15
- ```ts
16
- import { createTheme, getPrePaintScript } from "@codenhub/theme";
17
-
18
- const theme = createTheme().init();
19
- theme.set("dark");
20
-
21
- // Get inline IIFE script string for <head> to prevent FOUC:
22
- const script = theme.getPrePaintScript(); // or standalone getPrePaintScript()
23
-
24
- // Remove media-query/storage listeners and subscribers on teardown.
25
- theme.destroy({ revertDom: true });
26
- ```
27
-
28
- ## Documentation
29
-
30
- - [Documentation overview](docs/index.md)
31
- - [API, persistence, DOM, and SSR behavior](docs/reference.md)
32
-
33
- ## Requirements
34
-
35
- - Browser integration uses `document.documentElement`, `localStorage`,
36
- `matchMedia`, `storage` events, and `CustomEvent`.
37
- - SSR is supported by skipping unavailable browser work and using the configured
38
- default theme.
39
- - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint
40
- script needed to prevent a theme flash.
41
-
42
- ## Notes
43
-
44
- Construction validates theme names, mappings, attributes, classes, and token
45
- schemas and throws on invalid configuration. Storage failures are reported to
46
- the console and treated as unavailable storage.
47
-
48
- ## License
49
-
50
- Licensed under Apache-2.0.
51
-
52
- <!-- Source: docs/index.md -->
53
-
54
- # Manage Browser Themes
55
-
56
- `@codenhub/theme` resolves stored and system theme preferences, applies theme
57
- state to the document root, synchronizes browser tabs, exposes change events,
58
- and maps typed tokens to CSS custom properties.
59
-
60
- It fits browser applications that need one owner for theme preference and DOM
61
- synchronization while keeping selectors, visual design, and CSS in application
62
- code.
63
-
64
- ## Setup
65
-
66
- ### Installation
67
-
68
- ```sh
69
- pnpm add @codenhub/theme
70
- ```
71
-
72
- ### Quick start
73
-
74
- ```ts
75
- import { createTheme } from "@codenhub/theme";
76
-
77
- const theme = createTheme().init();
78
- theme.set("dark");
79
- ```
80
-
81
- Call `destroy()` when the manager's owner is removed. The package persists an
82
- explicit preference and follows system preference when appropriate.
83
-
84
- ## Requirements
85
-
86
- - Browser integration uses `document.documentElement`, `localStorage`,
87
- `matchMedia`, `storage` events, and `CustomEvent`.
88
- - SSR is supported by skipping unavailable browser work and using the configured
89
- default theme.
90
- - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint
91
- script needed to prevent a theme flash.
92
-
93
- The package cannot produce server HTML attributes or prevent a flash before
94
- initialization. Applications that need pre-paint consistency must apply matching
95
- theme logic before rendering.
96
-
97
- ## Next steps
98
-
99
- - [API, persistence, DOM, and SSR behavior](docs/reference.md): Complete exports,
100
- configuration, tokens, browser synchronization, validation, pre-paint
101
- concerns, and cleanup.
102
-
103
- <!-- Source: docs/reference.md -->
104
-
105
- # Theme API, Persistence, DOM, and SSR Behavior
106
-
107
- ## Create and Initialize
108
-
109
- `createTheme<TSchema>(options?): Theme<TSchema>` validates configuration and
110
- creates a manager. `ThemeOptions` contains:
111
-
112
- - `themes`: definitions, defaulting to `LIGHT_THEME` and `DARK_THEME`.
113
- - `defaultTheme`: pre-init and SSR fallback, default `"light"`.
114
- - `systemTheme`: configured names for light/dark OS preference.
115
- - `storageKey`: `localStorage` key, default `"app-theme-preference"`.
116
- - `attribute`: document-root attribute, default `"data-theme"`.
117
- - `isTailwindCss`: toggle the root `dark` class, default `false`.
118
- - `shouldApplyClass`: add `theme-${name}` by default, disable classes, or return
119
- one custom class token with a `ThemeClassResolver`.
120
- - `tokenSchema`: maps typed token names to CSS custom-property names.
121
-
122
- `ThemeDefinition` has a unique `name`, `colorScheme` (`light` or `dark`),
123
- optional static tokens, and optional `pairedTheme` target for `toggle()`.
124
- `SystemThemeMap` maps OS light and dark preferences.
125
-
126
- `init(tokens?)` is idempotent until `destroy()`. It registers media-query and
127
- cross-tab storage listeners, selects valid stored preference before system
128
- preference, applies the theme, and emits an `"init"` change.
129
-
130
- ## Theme Operations
131
-
132
- - `get(options?)` returns the active definition. Tokens merge computed CSS values,
133
- static theme values, then runtime overrides. Reading computed values can force
134
- style calculation; pass `{ skipComputed: true }` to bypass reading computed DOM styles.
135
- - `set(name, tokens?)` applies and persists a configured theme; unknown names
136
- throw.
137
- - `toggle(tokens?)` switches to `pairedTheme` if defined on the active theme, or
138
- otherwise by active `colorScheme` between the names in `systemTheme`, then
139
- persists the choice.
140
- - `clearPreference()` removes storage and applies the current system theme.
141
- - `getStored()` returns a valid configured stored name or `null`.
142
- - `getSystem()` returns the mapped system theme, or `defaultTheme` when media
143
- queries are unavailable.
144
- - `getPrePaintScript()` returns a synchronous inline IIFE script string to inject
145
- into document `<head>` to prevent flash of unstyled content (FOUC). Supports custom
146
- class resolvers and static token custom properties. Also available as standalone export
147
- `getPrePaintScript(options?)`.
148
-
149
- Runtime token overrides persist across theme changes. Passing a new object,
150
- including `{}`, replaces them. Tokens require a schema and unknown token keys
151
- throw.
152
-
153
- ## DOM, Persistence, and Events
154
-
155
- Applying a theme sets the configured root attribute and
156
- `document.documentElement.style.colorScheme`, updates configured theme classes,
157
- toggles Tailwind's `dark` class when enabled, and writes mapped token values as
158
- CSS custom properties. The package supplies no CSS or token values beyond the
159
- built-in light/dark definitions.
160
-
161
- Explicit preferences use `localStorage`. `storage` events synchronize valid
162
- changes from other tabs. OS preference changes apply only while no valid stored
163
- preference exists. Storage read/write/remove failures are logged with
164
- `console.error` and treated as unavailable.
165
-
166
- `subscribe(ThemeChangeListener)` returns an unsubscribe function. Subscribers
167
- receive `ThemeChangeDetail` with `name`, `theme`, and `ThemeChangeSource`:
168
- `"init"`, `"set"`, `"toggle"`, `"clearPreference"`, or `"system"`. Subscriber
169
- errors are logged and do not stop later listeners. Browsers also receive a
170
- window `CustomEvent` named by `THEME_CHANGE_EVENT` (`"themechange"`).
171
-
172
- ## Validation and Cleanup
173
-
174
- Creation throws for invalid attributes/storage keys, malformed or duplicate
175
- themes, invalid color schemes/classes/token schemas, and missing default/system
176
- theme names. Applying a custom class resolver can throw if it returns an empty
177
- or whitespace-containing class. Runtime token calls throw without a matching
178
- schema.
179
-
180
- `destroy(options?)` removes media-query and storage listeners, clears
181
- in-process subscribers and runtime tokens, and resets internal active state.
182
- By default, it does not remove persisted preference or revert attributes,
183
- classes, custom properties, or `colorScheme` already applied to the document;
184
- pass `{ revertDom: true }` to remove configured DOM attributes, classes, and
185
- CSS custom properties from `document.documentElement`.
186
-
187
- ## SSR and Pre-Paint Behavior
188
-
189
- Without browser APIs, storage, DOM, events, and listeners are skipped;
190
- `getSystem()` uses `defaultTheme`. Calls remain usable but cannot produce server
191
- HTML attributes. To avoid a flash, applications must apply equivalent validated
192
- storage/system logic in a blocking pre-paint script or render matching server
193
- markup. Keep that logic aligned with custom names, mappings, storage key,
194
- attribute, classes, and color scheme. Note that `getPrePaintScript()` serializes
195
- static tokens defined on configured theme definitions (`ThemeDefinition.tokens`);
196
- runtime token overrides passed to `init(tokens)` or `set(name, tokens)` are applied
197
- during client-side execution after hydration.
198
-
199
- ## Public Exports
200
-
201
- The root exports `createTheme`, `getPrePaintScript`, `THEME_CHANGE_EVENT`,
202
- `LIGHT_THEME`, and `DARK_THEME`, plus `Theme`, `ThemeDefinition`, `SystemThemeMap`,
203
- `ThemeClassResolver`, `ThemeChangeSource`, `ThemeChangeDetail`,
204
- `ThemeChangeListener`, and `ThemeOptions` types.
1
+ <!-- Source: README.md -->
2
+
3
+ # @codenhub/theme
4
+
5
+ Zero-dependency browser theme preference, persistence, DOM, and token manager.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @codenhub/theme
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createTheme, getPrePaintScript } from "@codenhub/theme";
17
+
18
+ const theme = createTheme().init();
19
+ theme.set("dark");
20
+
21
+ // Get inline IIFE script string for <head> to prevent FOUC:
22
+ const script = theme.getPrePaintScript(); // or standalone getPrePaintScript()
23
+
24
+ // Remove media-query/storage listeners and subscribers on teardown.
25
+ theme.destroy({ revertDom: true });
26
+ ```
27
+
28
+ ## Documentation
29
+
30
+ - [Documentation overview](docs/index.md)
31
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)
32
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md)
33
+ - [API reference](docs/reference/index.md)
34
+
35
+ ## Requirements
36
+
37
+ - Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`.
38
+ - SSR is supported by skipping unavailable browser work and using the configured default theme.
39
+ - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint script needed to prevent a theme flash.
40
+
41
+ ## Notes
42
+
43
+ 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.
44
+
45
+ ## License
46
+
47
+ Licensed under Apache-2.0.
48
+
49
+ <!-- Source: docs/index.md -->
50
+
51
+ # Manage Browser Themes
52
+
53
+ `@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, 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.
54
+
55
+ ## Setup
56
+
57
+ ### Installation
58
+
59
+ ```sh
60
+ pnpm add @codenhub/theme
61
+ ```
62
+
63
+ ### Quick start
64
+
65
+ ```ts
66
+ import { createTheme } from "@codenhub/theme";
67
+
68
+ const theme = createTheme().init();
69
+ theme.set("dark");
70
+ ```
71
+
72
+ `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`. 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 [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)).
73
+
74
+ ### Configuration
75
+
76
+ Most applications configure at least `themes` and `tokenSchema` up front:
77
+
78
+ ```ts
79
+ import { createTheme } from "@codenhub/theme";
80
+
81
+ const theme = createTheme({
82
+ themes: [
83
+ { name: "light", colorScheme: "light", tokens: { primary: "#171717" } },
84
+ { name: "dark", colorScheme: "dark", tokens: { primary: "#f9fafb" } },
85
+ ],
86
+ tokenSchema: { primary: "--color-primary" },
87
+ storageKey: "app-theme-preference",
88
+ isTailwindCss: true,
89
+ }).init();
90
+ ```
91
+
92
+ `themes` replaces the built-in list entirely — include `light` and `dark` (or whatever names `defaultTheme` and `systemTheme` point at) when defining custom themes. `tokenSchema` maps the typed token names used in `ThemeDefinition.tokens` and `get()` 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 and OS-preference-only applications like `isTailwindCss`.
93
+
94
+ Construction validates every option and throws on invalid configuration: empty or duplicate theme names, a `defaultTheme` or `systemTheme` name that is not in `themes`, an invalid `colorScheme`, or a `shouldApplyClass` resolver that would produce an empty or whitespace-containing class.
95
+
96
+ ## Requirements
97
+
98
+ - 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.
99
+ - 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).
100
+ - Consumers provide CSS selectors, variables, and visual tokens. The package only ever writes an attribute, optional classes, and CSS custom properties — it ships no CSS or token values beyond the built-in `light`/`dark` definitions' names.
101
+
102
+ ## Next steps
103
+
104
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md): How `get()` resolves token values, how preferences persist to `localStorage` and sync across tabs, and how to read or clear the stored preference.
105
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md): Preventing a flash of unstyled content before hydration, server-rendering considerations, and toggling Tailwind's `dark` class.
106
+ - [API reference](docs/reference/index.md): Every exported function, type, and interface member with its full signature.
107
+
108
+ <!-- Source: docs/ssr-and-pre-paint.md -->
109
+
110
+ # SSR, Pre-Paint, and Tailwind
111
+
112
+ ## Preventing a flash of unstyled content
113
+
114
+ 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()` — available as a method on an existing manager, or as the standalone `getPrePaintScript(options)` export when no manager exists yet — returns a synchronous, minified inline IIFE string that duplicates the same stored-preference-then-system-preference resolution, and applies the configured attribute, classes, and any static tokens before the browser paints:
115
+
116
+ ```ts
117
+ import { getPrePaintScript } from "@codenhub/theme";
118
+
119
+ const script = getPrePaintScript({ storageKey: "app-theme-preference" });
120
+ ```
121
+
122
+ Inject the returned string into a blocking `<script>` in the document `<head>`, before any stylesheet or content that depends on the theme:
123
+
124
+ ```html
125
+ <head>
126
+ <script>
127
+ /* inline the string returned by getPrePaintScript() here */
128
+ </script>
129
+ </head>
130
+ ```
131
+
132
+ Pass the same `themes`, `tokenSchema`, `defaultTheme`, `systemTheme`, `storageKey`, `attribute`, `shouldApplyClass`, and `isTailwindCss` options used to construct the manager, so the pre-paint script and the later `init()` call resolve to the same theme. `getPrePaintScript()` only serializes **static** tokens from each theme's `ThemeDefinition.tokens` — and only when `tokenSchema` is passed to it — runtime overrides passed to `init(tokens)` are applied after hydration, during client-side execution, and cannot be part of a script that runs before any application code.
133
+
134
+ ## Server-side rendering
135
+
136
+ Without browser APIs, `Theme` methods skip storage reads/writes, DOM updates, and listener registration; `getSystem()` falls back to `defaultTheme`. Every method call remains safe to make 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:
137
+
138
+ - 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
139
+ - 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.
140
+
141
+ Keep any server-side resolution logic — stored names, system mapping, storage key, attribute, classes, color scheme — aligned with the options passed to `createTheme()` and `getPrePaintScript()`; a mismatch reintroduces the flash it exists to prevent.
142
+
143
+ ## Tailwind's `dark` class
144
+
145
+ Set `isTailwindCss: true` to toggle Tailwind CSS's `dark` class on `document.documentElement` alongside the configured attribute, for every theme whose `colorScheme` is `"dark"`. This is independent of `shouldApplyClass` (below) a Tailwind app commonly wants both the `dark` class for Tailwind's `dark:` variant and, when `shouldApplyClass` is left at its default, a `theme-${name}` class for custom per-theme CSS.
146
+
147
+ ## Classes and cleanup
148
+
149
+ `shouldApplyClass` controls whether and how a class is applied to `document.documentElement` alongside the attribute: `true` (the default) applies `theme-${name}`, `false` applies no class, and a `ThemeClassResolver` function receives the active `ThemeDefinition` and returns one custom class token — throwing if it returns an empty or whitespace-containing string.
150
+
151
+ `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, 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.
152
+
153
+ <!-- Source: docs/tokens-and-persistence.md -->
154
+
155
+ # Tokens, Persistence, and Cross-Tab Sync
156
+
157
+ ## Token resolution
158
+
159
+ `tokenSchema` maps typed token names to the CSS custom property each one writes, such as `{ primary: "--color-primary" }`. Once configured, `get()` returns the active `ThemeDefinition` with every schema key resolved from three sources, in this priority order (last wins):
160
+
161
+ 1. **Computed style** for a token not defined in JS, the value is read from `window.getComputedStyle(document.documentElement)` in browser environments.
162
+ 2. **Theme static tokens** values set on the active theme's own `ThemeDefinition.tokens`.
163
+ 3. **Runtime overrides** values passed to `init()`, `set()`, or `toggle()`.
164
+
165
+ ```ts
166
+ const theme = createTheme({
167
+ themes: [{ name: "brand", colorScheme: "light", tokens: { accent: "#ff6600" } }],
168
+ tokenSchema: { accent: "--color-accent" },
169
+ }).init();
170
+
171
+ theme.set("brand", { accent: "#00c853" }); // runtime override wins over the static "#ff6600"
172
+ theme.get().tokens?.accent; // "#00c853"
173
+ ```
174
+
175
+ Reading computed values can force a synchronous layout reflow. Pass `{ skipComputed: true }` to `get()` to skip that source and avoid the reflow when only the JS-known values are needed.
176
+
177
+ Runtime overrides persist across subsequent theme changes until explicitly replaced — pass a new object, including `{}`, to `init()`, `set()`, or `toggle()` to clear or replace them. Passing a token key that is not in `tokenSchema` throws.
178
+
179
+ ## Persistence
180
+
181
+ An explicit preference set through `set()` or `toggle()` is written to `localStorage` under `storageKey` (default `"app-theme-preference"`). `getStored()` returns that value when it names a currently configured theme, or `null` when it is unset, invalid, or storage is unavailable (for example during SSR). `clearPreference()` removes the stored value and re-applies whichever theme `getSystem()` currently resolves to.
182
+
183
+ Storage reads, writes, and removals that throw — a full quota, a disabled storage API, a private-browsing restriction — are logged with `console.error` and treated the same as unavailable storage; they never throw out of `Theme` methods.
184
+
185
+ ## Cross-tab sync
186
+
187
+ Every tab running `createTheme().init()` against the same `storageKey` stays in sync: a `storage` event fires in every other tab when one tab writes a new preference, and each one applies the change if it names a valid configured theme. A tab with its own runtime token overrides keeps them — only the active theme name changes from cross-tab sync, not tokens passed in-process.
188
+
189
+ 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.
190
+
191
+ ## Reacting to changes
192
+
193
+ `subscribe(listener)` registers a callback invoked after every applied change from `init()`, `set()`, `toggle()`, `clearPreference()`, or an accepted `system` or cross-tab update — and returns an unsubscribe function:
194
+
195
+ ```ts
196
+ const unsubscribe = theme.subscribe(({ name, source }) => {
197
+ console.log(`theme is now "${name}" (${source})`);
198
+ });
199
+ ```
200
+
201
+ A subscriber that throws is logged and does not stop other subscribers from running. Browsers also receive a `window` `CustomEvent` named by the exported `THEME_CHANGE_EVENT` constant (`"themechange"`) carrying the same detail, for code that prefers DOM events over `subscribe()`.
202
+
203
+ <!-- Source: docs/changelog/0.1.2.md -->
204
+
205
+ # 0.1.2
206
+
207
+ ## Added
208
+
209
+ - 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.
210
+
211
+ ## Changed
212
+
213
+ - 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`.
package/llms.txt CHANGED
@@ -1,13 +1,19 @@
1
- # @codenhub/theme
2
-
3
- > Zero-dependency browser theme preference, persistence, DOM, cross-tab, event,
4
- > and token manager with SSR-safe fallbacks.
5
-
6
- Call `init()` in the browser, keep pre-paint logic aligned with configuration,
7
- and call `destroy()` to release media-query/storage listeners and subscribers.
8
-
9
- ## Documentation
10
-
11
- - [Documentation overview](docs/index.md): Purpose, status, and starting points.
12
- - [API, persistence, DOM, and SSR behavior](docs/reference.md): Complete exports,
13
- configuration, tokens, listeners, failures, pre-paint behavior, and cleanup.
1
+ # @codenhub/theme
2
+
3
+ > Zero-dependency browser theme preference, persistence, DOM, cross-tab, event,
4
+ > and token manager with SSR-safe fallbacks.
5
+
6
+ Call `init()` in the browser, keep pre-paint logic aligned with configuration,
7
+ and call `destroy()` to release media-query/storage listeners and subscribers.
8
+
9
+ ## Documentation
10
+
11
+ - [Documentation overview](docs/index.md): Purpose, status, and starting points.
12
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md): Token
13
+ resolution priority, `localStorage` persistence, and cross-tab sync.
14
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md): FOUC prevention, SSR
15
+ fallbacks, and Tailwind `dark` class toggling.
16
+
17
+ ## Optional
18
+
19
+ - [API reference](docs/reference/index.md): Generated per-symbol signatures and prose.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codenhub/theme",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Zero-dependency browser theme preference helper for TypeScript apps.",
6
6
  "homepage": "https://github.com/codenhub/codenhub/tree/main/packages/theme",
@@ -29,32 +29,35 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
- "devDependencies": {
33
- "@playwright/test": "^1.61.1",
34
- "jsdom": "^29.1.1",
35
- "tsdown": "^0.22.3",
36
- "typescript": "^6.0.3",
37
- "vitest": "^4.1.10",
38
- "@codenhub/styles": "0.1.0",
39
- "@codenhub/vite-plugin-icons": "0.0.1"
40
- },
41
- "codenhub": {
42
- "docs": {
43
- "label": "Theme",
44
- "status": "active"
45
- }
46
- },
47
32
  "scripts": {
48
33
  "dev": "pnpm --filter=@codenhub/theme-dev dev",
49
34
  "debug": "pnpm build && pnpm --filter=@codenhub/theme-debug dev",
50
35
  "build": "tsdown src/index.ts --format esm --dts --clean --no-fixed-extension --minify",
36
+ "prepublishOnly": "pnpm build && pnpm typecheck",
51
37
  "status:npm": "npm view @codenhub/theme version dist-tags time --json && npm dist-tag ls @codenhub/theme && npm access get status @codenhub/theme",
52
- "status:pack": "npm pack --dry-run",
53
- "test": "vitest run && pnpm build && pnpm test:visual",
38
+ "status:pack": "npm pack --dry-run --ignore-scripts",
39
+ "test": "vitest run",
40
+ "test:browser": "playwright test",
41
+ "test:browser:watch": "playwright test --ui",
54
42
  "test:coverage": "vitest run --coverage",
55
- "test:visual": "playwright test",
56
43
  "test:watch": "vitest",
57
- "test:visual:watch": "playwright test --ui",
58
- "typecheck": "tsc --noEmit"
44
+ "typecheck": "tsc -b"
45
+ },
46
+ "devDependencies": {
47
+ "@playwright/test": "catalog:",
48
+ "@vitest/coverage-v8": "catalog:",
49
+ "jsdom": "catalog:",
50
+ "tsdown": "catalog:",
51
+ "typescript": "catalog:",
52
+ "vitest": "catalog:"
53
+ },
54
+ "codenhub": {
55
+ "docs": {
56
+ "label": "ThemeSystem",
57
+ "status": "active",
58
+ "reference": {
59
+ "prose": true
60
+ }
61
+ }
59
62
  }
60
- }
63
+ }
package/docs/reference.md DELETED
@@ -1,104 +0,0 @@
1
- ---
2
- title: Reference
3
- ---
4
-
5
- # Theme API, Persistence, DOM, and SSR Behavior
6
-
7
- ## Create and Initialize
8
-
9
- `createTheme<TSchema>(options?): Theme<TSchema>` validates configuration and
10
- creates a manager. `ThemeOptions` contains:
11
-
12
- - `themes`: definitions, defaulting to `LIGHT_THEME` and `DARK_THEME`.
13
- - `defaultTheme`: pre-init and SSR fallback, default `"light"`.
14
- - `systemTheme`: configured names for light/dark OS preference.
15
- - `storageKey`: `localStorage` key, default `"app-theme-preference"`.
16
- - `attribute`: document-root attribute, default `"data-theme"`.
17
- - `isTailwindCss`: toggle the root `dark` class, default `false`.
18
- - `shouldApplyClass`: add `theme-${name}` by default, disable classes, or return
19
- one custom class token with a `ThemeClassResolver`.
20
- - `tokenSchema`: maps typed token names to CSS custom-property names.
21
-
22
- `ThemeDefinition` has a unique `name`, `colorScheme` (`light` or `dark`),
23
- optional static tokens, and optional `pairedTheme` target for `toggle()`.
24
- `SystemThemeMap` maps OS light and dark preferences.
25
-
26
- `init(tokens?)` is idempotent until `destroy()`. It registers media-query and
27
- cross-tab storage listeners, selects valid stored preference before system
28
- preference, applies the theme, and emits an `"init"` change.
29
-
30
- ## Theme Operations
31
-
32
- - `get(options?)` returns the active definition. Tokens merge computed CSS values,
33
- static theme values, then runtime overrides. Reading computed values can force
34
- style calculation; pass `{ skipComputed: true }` to bypass reading computed DOM styles.
35
- - `set(name, tokens?)` applies and persists a configured theme; unknown names
36
- throw.
37
- - `toggle(tokens?)` switches to `pairedTheme` if defined on the active theme, or
38
- otherwise by active `colorScheme` between the names in `systemTheme`, then
39
- persists the choice.
40
- - `clearPreference()` removes storage and applies the current system theme.
41
- - `getStored()` returns a valid configured stored name or `null`.
42
- - `getSystem()` returns the mapped system theme, or `defaultTheme` when media
43
- queries are unavailable.
44
- - `getPrePaintScript()` returns a synchronous inline IIFE script string to inject
45
- into document `<head>` to prevent flash of unstyled content (FOUC). Supports custom
46
- class resolvers and static token custom properties. Also available as standalone export
47
- `getPrePaintScript(options?)`.
48
-
49
- Runtime token overrides persist across theme changes. Passing a new object,
50
- including `{}`, replaces them. Tokens require a schema and unknown token keys
51
- throw.
52
-
53
- ## DOM, Persistence, and Events
54
-
55
- Applying a theme sets the configured root attribute and
56
- `document.documentElement.style.colorScheme`, updates configured theme classes,
57
- toggles Tailwind's `dark` class when enabled, and writes mapped token values as
58
- CSS custom properties. The package supplies no CSS or token values beyond the
59
- built-in light/dark definitions.
60
-
61
- Explicit preferences use `localStorage`. `storage` events synchronize valid
62
- changes from other tabs. OS preference changes apply only while no valid stored
63
- preference exists. Storage read/write/remove failures are logged with
64
- `console.error` and treated as unavailable.
65
-
66
- `subscribe(ThemeChangeListener)` returns an unsubscribe function. Subscribers
67
- receive `ThemeChangeDetail` with `name`, `theme`, and `ThemeChangeSource`:
68
- `"init"`, `"set"`, `"toggle"`, `"clearPreference"`, or `"system"`. Subscriber
69
- errors are logged and do not stop later listeners. Browsers also receive a
70
- window `CustomEvent` named by `THEME_CHANGE_EVENT` (`"themechange"`).
71
-
72
- ## Validation and Cleanup
73
-
74
- Creation throws for invalid attributes/storage keys, malformed or duplicate
75
- themes, invalid color schemes/classes/token schemas, and missing default/system
76
- theme names. Applying a custom class resolver can throw if it returns an empty
77
- or whitespace-containing class. Runtime token calls throw without a matching
78
- schema.
79
-
80
- `destroy(options?)` removes media-query and storage listeners, clears
81
- in-process subscribers and runtime tokens, and resets internal active state.
82
- By default, it does not remove persisted preference or revert attributes,
83
- classes, custom properties, or `colorScheme` already applied to the document;
84
- pass `{ revertDom: true }` to remove configured DOM attributes, classes, and
85
- CSS custom properties from `document.documentElement`.
86
-
87
- ## SSR and Pre-Paint Behavior
88
-
89
- Without browser APIs, storage, DOM, events, and listeners are skipped;
90
- `getSystem()` uses `defaultTheme`. Calls remain usable but cannot produce server
91
- HTML attributes. To avoid a flash, applications must apply equivalent validated
92
- storage/system logic in a blocking pre-paint script or render matching server
93
- markup. Keep that logic aligned with custom names, mappings, storage key,
94
- attribute, classes, and color scheme. Note that `getPrePaintScript()` serializes
95
- static tokens defined on configured theme definitions (`ThemeDefinition.tokens`);
96
- runtime token overrides passed to `init(tokens)` or `set(name, tokens)` are applied
97
- during client-side execution after hydration.
98
-
99
- ## Public Exports
100
-
101
- The root exports `createTheme`, `getPrePaintScript`, `THEME_CHANGE_EVENT`,
102
- `LIGHT_THEME`, and `DARK_THEME`, plus `Theme`, `ThemeDefinition`, `SystemThemeMap`,
103
- `ThemeClassResolver`, `ThemeChangeSource`, `ThemeChangeDetail`,
104
- `ThemeChangeListener`, and `ThemeOptions` types.