@codenhub/theme 0.0.2 → 0.0.3

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.
Files changed (5) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +171 -62
  3. package/dist/index.d.ts +116 -42
  4. package/dist/index.js +397 -112
  5. package/package.json +10 -3
package/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # @codenhub/theme
2
2
 
3
- Small zero-dependency theme preference helper for browser apps. It applies a theme name to the document, updates `document.documentElement.style.colorScheme`, and leaves tokens, variables, and visual styles to your CSS.
3
+ Small zero-dependency theme preference helper for browser apps. It applies a theme name to the document, updates `document.documentElement.style.colorScheme`, and supports managing dynamic CSS tokens.
4
+
5
+ ## Features
6
+
7
+ - **Factory API**: Instantiate with `createTheme()`.
8
+ - **Dynamic CSS Tokens**: Define a `tokenSchema` at initialization for type-safe, dynamic inline CSS custom property styling.
9
+ - **Zero Dependencies**: Tiny footprint and pure TypeScript.
10
+ - **Flexible Styling**: Works with standard CSS variables, class toggles, or Tailwind CSS.
4
11
 
5
12
  ## Installation
6
13
 
7
14
  ```sh
8
15
  pnpm add @codenhub/theme
16
+ npm install @codenhub/theme
17
+ yarn add @codenhub/theme
18
+ bun add @codenhub/theme
9
19
  ```
10
20
 
11
21
  ## Usage
@@ -13,9 +23,9 @@ pnpm add @codenhub/theme
13
23
  By default, `init()` uses a valid stored preference first. If there is no valid stored preference, it maps the OS color scheme to `light` or `dark`.
14
24
 
15
25
  ```ts
16
- import { Theme } from "@codenhub/theme";
26
+ import { createTheme } from "@codenhub/theme";
17
27
 
18
- const theme = new Theme({ tailwindcss: false, applyClass: true });
28
+ const theme = createTheme({ isTailwindCss: false, shouldApplyClass: true });
19
29
 
20
30
  theme.init();
21
31
  theme.set("dark");
@@ -24,6 +34,50 @@ theme.toggle();
24
34
 
25
35
  Call `destroy()` during app or test cleanup when the instance is no longer used.
26
36
 
37
+ ### Preventing Flash of Unstyled Content (FOUC)
38
+
39
+ Because the client-side JS bundle loads asynchronously, there can be a brief flash of the default theme before the theme manager initializes. To prevent this, inject a tiny blocking script in your HTML `<head>` before any stylesheet or content:
40
+
41
+ ```html
42
+ <script>
43
+ (function () {
44
+ try {
45
+ const key = "app-theme-preference";
46
+ const attribute = "data-theme";
47
+ let theme = "light";
48
+ try {
49
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
50
+ theme = "dark";
51
+ }
52
+ } catch (_) {}
53
+ try {
54
+ const stored = localStorage.getItem(key);
55
+ // Add all your configured theme names here to validate the stored value.
56
+ const allowed = ["light", "dark"];
57
+ if (stored && allowed.includes(stored)) {
58
+ theme = stored;
59
+ }
60
+ } catch (_) {}
61
+ document.documentElement.setAttribute(attribute, theme);
62
+
63
+ // List the names of all your dark-scheme themes here.
64
+ // Using an explicit set avoids false matches from substring checks (e.g. "midnight" is dark
65
+ // but would not match "dark"; "darkroom" would match but may not be a dark theme).
66
+ const darkThemes = new Set(["dark"]);
67
+ const isDark = darkThemes.has(theme);
68
+ document.documentElement.style.colorScheme = isDark ? "dark" : "light";
69
+ document.documentElement.classList.add("theme-" + theme);
70
+
71
+ // If using Tailwind CSS, also toggle "dark" class:
72
+ // document.documentElement.classList.toggle("dark", isDark);
73
+ } catch (_) {}
74
+ })();
75
+ </script>
76
+ ```
77
+
78
+ > [!WARNING]
79
+ > If you customize `systemTheme`, `themes`, `storageKey`, or `attribute` in `createTheme`, make sure to update the inline FOUC script to match those values. Mismatched configurations will cause a flash of the wrong theme. Also, if you set `shouldApplyClass` to `false` or use a custom class resolver, update or remove the FOUC class application block (`classList.add`) accordingly to prevent visual shifts on initialization.
80
+
27
81
  ## Reference
28
82
 
29
83
  ### `@codenhub/theme`
@@ -31,9 +85,10 @@ Call `destroy()` during app or test cleanup when the instance is no longer used.
31
85
  Primary entrypoint for the theme preference API.
32
86
 
33
87
  ```ts
34
- import { Theme, darkTheme, lightTheme, THEME_CHANGE_EVENT } from "@codenhub/theme";
88
+ import { createTheme, DARK_THEME, LIGHT_THEME, THEME_CHANGE_EVENT } from "@codenhub/theme";
35
89
  import type {
36
90
  SystemThemeMap,
91
+ Theme,
37
92
  ThemeChangeDetail,
38
93
  ThemeChangeListener,
39
94
  ThemeChangeSource,
@@ -49,63 +104,72 @@ Supported import paths:
49
104
  | ----------------- | ----------------------------------- |
50
105
  | `@codenhub/theme` | Main JavaScript and TypeScript API. |
51
106
 
107
+ #### `createTheme()`
108
+
109
+ Factory function that returns a `Theme` manager instance.
110
+
111
+ ```ts
112
+ function createTheme<TSchema extends Record<string, string> = Record<string, string>>(
113
+ options?: ThemeOptions<TSchema>,
114
+ ): Theme<TSchema>;
115
+ ```
116
+
52
117
  #### `Theme`
53
118
 
54
- Manages the active theme, storage preference, DOM attribute, `colorScheme` style, classes, system preference listener, and change notifications.
119
+ Manages the active theme, storage preference, DOM attribute, `colorScheme` style, classes, system preference listener, dynamic tokens, and change notifications.
55
120
 
56
121
  ```ts
57
- class Theme {
58
- constructor(options?: ThemeOptions);
59
- init(): this;
60
- get(): ThemeDefinition;
61
- set(name: string): ThemeDefinition;
62
- toggle(): ThemeDefinition;
63
- clearPreference(): ThemeDefinition;
122
+ interface Theme<TSchema extends Record<string, string> = Record<string, string>> {
123
+ init(tokens?: Partial<Record<keyof TSchema, string>>): this;
124
+ get(): ThemeDefinition<TSchema>;
125
+ set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
126
+ toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
127
+ clearPreference(): ThemeDefinition<TSchema>;
64
128
  getStored(): string | null;
65
- getSystem(): ThemeDefinition;
66
- subscribe(listener: ThemeChangeListener): () => void;
129
+ getSystem(): ThemeDefinition<TSchema>;
130
+ subscribe(listener: ThemeChangeListener<TSchema>): () => void;
67
131
  destroy(): void;
68
132
  }
69
133
  ```
70
134
 
71
135
  Import from `@codenhub/theme`.
72
136
 
73
- The constructor throws `Error` when configured theme names are empty, duplicated, invalid for CSS class application, or referenced by `defaultTheme` or `systemTheme` without being configured.
137
+ The factory throws `Error` when configured theme names are empty, duplicated, invalid for CSS class application, or referenced by `defaultTheme` or `systemTheme` without being configured.
74
138
 
75
139
  ##### `init()`
76
140
 
77
- Registers the system preference listener, resolves the initial theme, applies it, and emits a change with source `"init"`.
78
-
79
- Repeated calls do not register duplicate system preference listeners.
141
+ Registers the system preference listener, resolves the initial theme, applies it, and emits a change with source `"init"`. Can optionally accept initial token value overrides.
80
142
 
81
143
  ```ts
82
- function init(): this;
144
+ function init(tokens?: Partial<Record<keyof TSchema, string>>): this;
83
145
  ```
84
146
 
147
+ Repeated calls do not register duplicate system preference listeners.
148
+
85
149
  ##### `get()`
86
150
 
87
- Returns the active theme definition.
151
+ Returns the active theme definition including any active merged tokens. If a token in `tokenSchema` is not defined in JS for the active theme, its value is dynamically resolved from the computed styles of the DOM in browser environments.
88
152
 
89
153
  ```ts
90
- function get(): ThemeDefinition;
154
+ function get(): ThemeDefinition<TSchema>;
91
155
  ```
92
156
 
93
157
  ##### `set()`
94
158
 
95
- Activates a configured theme by name and stores the explicit preference when browser storage is available.
159
+ Activates a configured theme by name, applies any dynamic token overrides, and stores the explicit preference when browser storage is available. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
96
160
 
97
161
  ```ts
98
- function set(name: string): ThemeDefinition;
162
+ function set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
99
163
  ```
100
164
 
101
165
  Throws `Error` when `name` is not configured.
102
166
 
103
167
  ##### `toggle()`
104
168
 
105
- Toggles between the configured system light and dark theme names, then stores the explicit preference when browser storage is available.
169
+ Toggles between the configured system light and dark themes based on the active theme's `colorScheme`, applies any dynamic token overrides, then stores the explicit preference when browser storage is available. The target name is always taken from `systemTheme.light` or `systemTheme.dark`, not by cycling the active theme name. In multi-theme setups where the active theme is not one of the system themes, `toggle()` still targets `systemTheme.light` or `systemTheme.dark`. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
106
170
 
107
171
  ```ts
108
- function toggle(): ThemeDefinition;
172
+ function toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
109
173
  ```
110
174
 
111
175
  ##### `clearPreference()`
@@ -113,7 +177,7 @@ function toggle(): ThemeDefinition;
113
177
  Removes the stored preference and activates the current system theme.
114
178
 
115
179
  ```ts
116
- function clearPreference(): ThemeDefinition;
180
+ function clearPreference(): ThemeDefinition<TSchema>;
117
181
  ```
118
182
 
119
183
  ##### `getStored()`
@@ -131,7 +195,7 @@ Returns `null` during SSR, when storage is unavailable, when storage access thro
131
195
  Returns the configured theme for the current `prefers-color-scheme` value.
132
196
 
133
197
  ```ts
134
- function getSystem(): ThemeDefinition;
198
+ function getSystem(): ThemeDefinition<TSchema>;
135
199
  ```
136
200
 
137
201
  Returns the default theme during SSR or when `matchMedia` is unavailable.
@@ -141,14 +205,14 @@ Returns the default theme during SSR or when `matchMedia` is unavailable.
141
205
  Registers an in-process listener for theme changes.
142
206
 
143
207
  ```ts
144
- function subscribe(listener: ThemeChangeListener): () => void;
208
+ function subscribe(listener: ThemeChangeListener<TSchema>): () => void;
145
209
  ```
146
210
 
147
211
  Returns an unsubscribe function.
148
212
 
149
213
  ##### `destroy()`
150
214
 
151
- Removes the system preference listener and clears in-process subscribers.
215
+ Removes the system preference listener, clears in-process subscribers, and resets active tokens and the active theme name to `defaultTheme`. Safe to call before re-initializing with `init()`.
152
216
 
153
217
  ```ts
154
218
  function destroy(): void;
@@ -159,42 +223,46 @@ Call this during app or test cleanup when the instance is no longer used.
159
223
  #### `ThemeOptions`
160
224
 
161
225
  ```ts
162
- interface ThemeOptions {
163
- themes?: readonly ThemeDefinition[];
226
+ interface ThemeOptions<TSchema extends Record<string, string> = Record<string, string>> {
227
+ themes?: readonly ThemeDefinition<TSchema>[];
164
228
  defaultTheme?: string;
165
229
  systemTheme?: SystemThemeMap;
166
230
  storageKey?: string;
167
231
  attribute?: string;
168
- tailwindcss?: boolean;
169
- applyClass?: boolean | ThemeClassResolver;
232
+ isTailwindCss?: boolean;
233
+ shouldApplyClass?: boolean | ThemeClassResolver<TSchema>;
234
+ tokenSchema?: TSchema;
170
235
  }
171
236
  ```
172
237
 
173
- | Option | Type | Default | Description |
174
- | -------------- | ------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------- |
175
- | `themes` | `readonly ThemeDefinition[]` | `[lightTheme, darkTheme]` | Defines available themes. |
176
- | `defaultTheme` | `string` | `"light"` | Theme used before init and when browser APIs are unavailable. |
177
- | `systemTheme` | `SystemThemeMap` | `{ light: "light", dark: "dark" }` | Maps OS light and dark preferences to configured theme names. |
178
- | `storageKey` | `string` | `"app-theme-preference"` | Key used for `localStorage`. |
179
- | `attribute` | `string` | `"data-theme"` | Attribute set on `document.documentElement`. |
180
- | `tailwindcss` | `boolean` | `false` | Toggles the `dark` class when the active theme has `colorScheme: "dark"`. |
181
- | `applyClass` | `boolean` or `(theme: ThemeDefinition) => string` | `true` | Adds `theme-${name}`, no class, or a resolver-provided class to `document.documentElement`. |
238
+ | Option | Type | Default | Description |
239
+ | ------------------ | ------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------- |
240
+ | `themes` | `readonly ThemeDefinition[]` | `[LIGHT_THEME, DARK_THEME]` | Defines available themes. |
241
+ | `defaultTheme` | `string` | `"light"` | Theme used before init and when browser APIs are unavailable. |
242
+ | `systemTheme` | `SystemThemeMap` | `{ light: "light", dark: "dark" }` | Maps OS light and dark preferences to configured theme names. |
243
+ | `storageKey` | `string` | `"app-theme-preference"` | Key used for `localStorage`. |
244
+ | `attribute` | `string` | `"data-theme"` | Attribute set on `document.documentElement`. |
245
+ | `isTailwindCss` | `boolean` | `false` | Toggles the `dark` class when the active theme has `colorScheme: "dark"`. |
246
+ | `shouldApplyClass` | `boolean` or `(theme: ThemeDefinition) => string` | `true` | Adds `theme-${name}`, no class, or a resolver-provided class to `document.documentElement`. |
247
+ | `tokenSchema` | `TSchema` | `undefined` | Schema mapping theme token names to their corresponding CSS Custom Property names. |
182
248
 
183
249
  When class application is enabled, each theme application removes classes for all configured themes, then adds the class for the active theme.
184
250
 
185
251
  #### `ThemeDefinition`
186
252
 
187
253
  ```ts
188
- interface ThemeDefinition {
254
+ interface ThemeDefinition<TSchema extends Record<string, string> = Record<string, string>> {
189
255
  name: string;
190
256
  colorScheme: "light" | "dark";
257
+ tokens?: Partial<Record<keyof TSchema, string>>;
191
258
  }
192
259
  ```
193
260
 
194
- | Field | Type | Description |
195
- | ------------- | ------------------- | ---------------------------------------------------------------- |
196
- | `name` | `string` | Unique theme name used for storage, attributes, and class names. |
197
- | `colorScheme` | `"light" \| "dark"` | Browser color scheme applied through `style.colorScheme`. |
261
+ | Field | Type | Description |
262
+ | ------------- | ---------------------------------------- | ---------------------------------------------------------------- |
263
+ | `name` | `string` | Unique theme name used for storage, attributes, and class names. |
264
+ | `colorScheme` | `"light" \| "dark"` | Browser color scheme applied through `style.colorScheme`. |
265
+ | `tokens` | `Partial<Record<keyof TSchema, string>>` | Optional theme-specific static token values. |
198
266
 
199
267
  #### `SystemThemeMap`
200
268
 
@@ -215,7 +283,9 @@ interface SystemThemeMap {
215
283
  Returns the class name applied to `document.documentElement` for a theme.
216
284
 
217
285
  ```ts
218
- type ThemeClassResolver = (theme: ThemeDefinition) => string;
286
+ type ThemeClassResolver<TSchema extends Record<string, string> = Record<string, string>> = (
287
+ theme: ThemeDefinition<TSchema>,
288
+ ) => string;
219
289
  ```
220
290
 
221
291
  The returned class name must be a single non-empty class token without whitespace.
@@ -227,7 +297,9 @@ The returned class name must be a single non-empty class token without whitespac
227
297
  Listener passed to `theme.subscribe()`.
228
298
 
229
299
  ```ts
230
- type ThemeChangeListener = (detail: ThemeChangeDetail) => void;
300
+ type ThemeChangeListener<TSchema extends Record<string, string> = Record<string, string>> = (
301
+ detail: ThemeChangeDetail<TSchema>,
302
+ ) => void;
231
303
  ```
232
304
 
233
305
  #### `THEME_CHANGE_EVENT`
@@ -241,10 +313,10 @@ const THEME_CHANGE_EVENT = "themechange";
241
313
  #### `ThemeChangeDetail`
242
314
 
243
315
  ```ts
244
- interface ThemeChangeDetail {
316
+ interface ThemeChangeDetail<TSchema extends Record<string, string> = Record<string, string>> {
245
317
  name: string;
246
- theme: ThemeDefinition;
247
- source: "init" | "set" | "toggle" | "clearPreference" | "system";
318
+ theme: ThemeDefinition<TSchema>;
319
+ source: ThemeChangeSource;
248
320
  }
249
321
  ```
250
322
 
@@ -275,12 +347,46 @@ type ThemeChangeSource = "init" | "set" | "toggle" | "clearPreference" | "system
275
347
  Built-in theme definitions.
276
348
 
277
349
  ```ts
278
- const lightTheme: ThemeDefinition = { name: "light", colorScheme: "light" };
279
- const darkTheme: ThemeDefinition = { name: "dark", colorScheme: "dark" };
350
+ const LIGHT_THEME: ThemeDefinition = { name: "light", colorScheme: "light" };
351
+ const DARK_THEME: ThemeDefinition = { name: "dark", colorScheme: "dark" };
280
352
  ```
281
353
 
282
354
  ## Examples
283
355
 
356
+ ### Dynamic CSS Tokens
357
+
358
+ ```ts
359
+ import { createTheme } from "@codenhub/theme";
360
+
361
+ // 1. Define schema
362
+ const tokenSchema = {
363
+ primary: "--color-primary",
364
+ background: "--color-bg",
365
+ } as const;
366
+
367
+ // 2. Initialize with schema and optional theme static token values
368
+ const theme = createTheme({
369
+ tokenSchema,
370
+ themes: [
371
+ {
372
+ name: "light",
373
+ colorScheme: "light",
374
+ tokens: { primary: "#0070f3", background: "#ffffff" },
375
+ },
376
+ {
377
+ name: "dark",
378
+ colorScheme: "dark",
379
+ tokens: { primary: "#3291ff", background: "#000000" },
380
+ },
381
+ ],
382
+ });
383
+
384
+ theme.init();
385
+
386
+ // 3. Switch theme and pass runtime overrides (e.g. from dynamic branding API)
387
+ theme.set("dark", { primary: "#ff007f" });
388
+ ```
389
+
284
390
  ### Define CSS Tokens
285
391
 
286
392
  ```css
@@ -304,12 +410,12 @@ body {
304
410
  ### Add More Themes
305
411
 
306
412
  ```ts
307
- import { Theme, darkTheme, lightTheme } from "@codenhub/theme";
413
+ import { createTheme, DARK_THEME, LIGHT_THEME } from "@codenhub/theme";
308
414
 
309
- const theme = new Theme({
310
- themes: [lightTheme, darkTheme, { name: "high-contrast", colorScheme: "dark" }],
415
+ const theme = createTheme({
416
+ themes: [LIGHT_THEME, DARK_THEME, { name: "high-contrast", colorScheme: "dark" }],
311
417
  systemTheme: { light: "light", dark: "high-contrast" },
312
- applyClass: (definition) => `mode-${definition.name}`,
418
+ shouldApplyClass: (definition) => `mode-${definition.name}`,
313
419
  });
314
420
 
315
421
  theme.init();
@@ -319,9 +425,9 @@ theme.set("high-contrast");
319
425
  ### Listen For Changes
320
426
 
321
427
  ```ts
322
- import { Theme, THEME_CHANGE_EVENT, type ThemeChangeDetail } from "@codenhub/theme";
428
+ import { createTheme, THEME_CHANGE_EVENT, type ThemeChangeDetail } from "@codenhub/theme";
323
429
 
324
- const theme = new Theme().init();
430
+ const theme = createTheme().init();
325
431
 
326
432
  const unsubscribe = theme.subscribe((detail) => {
327
433
  console.log(detail.name, detail.theme, detail.source);
@@ -342,7 +448,7 @@ theme.destroy();
342
448
  - Browser integration uses `document.documentElement`, `document.documentElement.style.colorScheme`, `window.matchMedia`, `localStorage`, and `CustomEvent`.
343
449
  - SSR is supported; DOM, storage, media query, and event work is skipped when browser APIs are unavailable.
344
450
  - System preference changes update the active theme only when there is no valid stored preference.
345
- - `localStorage` read, write, and remove errors are ignored and treated as unavailable storage.
451
+ - `localStorage` read, write, and remove errors are logged to the console via `console.error` and treated as unavailable storage.
346
452
  - Consumers own CSS variables, selectors, visual tokens, and persistence consent requirements.
347
453
  - No CSS file, design tokens, framework adapter, or peer dependency is provided.
348
454
 
@@ -351,5 +457,8 @@ theme.destroy();
351
457
  - Does not provide design tokens or generated CSS.
352
458
  - Does not provide React, Vue, or other framework bindings.
353
459
  - Does not provide server-side persistence.
354
- - Does not synchronize theme changes across tabs.
355
460
  - Does not manage user consent requirements for storage.
461
+
462
+ ## License
463
+
464
+ This project is licensed under the [Apache-2.0](LICENSE) license.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- //#region src/index.d.ts
2
- /** Window event name dispatched with `ThemeChangeDetail` after a theme change is applied in browser environments. */
3
- declare const THEME_CHANGE_EVENT = "themechange";
1
+ //#region src/types.d.ts
4
2
  /** Theme option stored, applied to the configured DOM attribute, and mapped to the browser color scheme. */
5
- interface ThemeDefinition {
3
+ interface ThemeDefinition<TSchema extends Record<string, string> = Record<string, string>> {
6
4
  /** Unique configured theme name used for storage, DOM attributes, and generated default classes. */
7
5
  name: string;
8
6
  /** Browser color scheme applied to `document.documentElement.style.colorScheme`. */
9
7
  colorScheme: "light" | "dark";
8
+ /** Optional theme-specific static token values. */
9
+ tokens?: Partial<Record<keyof TSchema, string>>;
10
10
  }
11
11
  /** Mapping from OS color-scheme preferences to configured theme names. */
12
12
  interface SystemThemeMap {
@@ -16,24 +16,24 @@ interface SystemThemeMap {
16
16
  dark: string;
17
17
  }
18
18
  /** Resolves the single DOM class token applied for a theme when custom class application is enabled. */
19
- type ThemeClassResolver = (theme: ThemeDefinition) => string;
19
+ type ThemeClassResolver<TSchema extends Record<string, string> = Record<string, string>> = (theme: ThemeDefinition<TSchema>) => string;
20
20
  /** Reason a theme change notification was emitted. */
21
21
  type ThemeChangeSource = "init" | "set" | "toggle" | "clearPreference" | "system";
22
22
  /** Payload passed to subscribers and the browser `themechange` event after a theme change. */
23
- interface ThemeChangeDetail {
23
+ interface ThemeChangeDetail<TSchema extends Record<string, string> = Record<string, string>> {
24
24
  /** Active theme name after the change. */
25
25
  name: string;
26
26
  /** Active theme definition after the change. */
27
- theme: ThemeDefinition;
27
+ theme: ThemeDefinition<TSchema>;
28
28
  /** Operation or browser signal that caused the change notification. */
29
29
  source: ThemeChangeSource;
30
30
  }
31
31
  /** In-process callback registered with `Theme.subscribe()` for applied theme changes. */
32
- type ThemeChangeListener = (detail: ThemeChangeDetail) => void;
32
+ type ThemeChangeListener<TSchema extends Record<string, string> = Record<string, string>> = (detail: ThemeChangeDetail<TSchema>) => void;
33
33
  /** Configuration for theme definitions, persistence, DOM application, and system preference mapping. */
34
- interface ThemeOptions {
35
- /** Available themes. Names must be unique, non-empty, and valid default class tokens when `applyClass` is `true`. */
36
- themes?: readonly ThemeDefinition[];
34
+ interface ThemeOptions<TSchema extends Record<string, string> = Record<string, string>> {
35
+ /** Available themes. Names must be unique, non-empty, and valid default class tokens when `shouldApplyClass` is `true`. */
36
+ themes?: readonly ThemeDefinition<TSchema>[];
37
37
  /** Configured theme name used before initialization and when browser APIs are unavailable. */
38
38
  defaultTheme?: string;
39
39
  /** Configured theme names selected for OS light and dark color-scheme preferences. */
@@ -43,42 +43,116 @@ interface ThemeOptions {
43
43
  /** Attribute set on `document.documentElement` with the active theme name. */
44
44
  attribute?: string;
45
45
  /** Whether to toggle Tailwind CSS's `dark` class for themes with `colorScheme: "dark"`. */
46
- tailwindcss?: boolean;
46
+ isTailwindCss?: boolean;
47
47
  /** Whether and how to apply a theme-specific class to `document.documentElement`. */
48
- applyClass?: boolean | ThemeClassResolver;
48
+ shouldApplyClass?: boolean | ThemeClassResolver<TSchema>;
49
+ /** Schema mapping theme token names to their corresponding CSS Custom Property names. */
50
+ tokenSchema?: TSchema;
49
51
  }
50
- /** Built-in light theme used by default and available for custom theme lists. */
51
- declare const lightTheme: ThemeDefinition;
52
- /** Built-in dark theme used by default and available for custom theme lists. */
53
- declare const darkTheme: ThemeDefinition;
54
52
  /**
55
- * Manages theme preference, DOM application, system preference changes, and change notifications.
56
- *
57
- * The constructor validates configured theme names, default and system mappings, and default class tokens.
58
- * Theme application throws `Error` when a requested theme is missing or a class resolver returns an invalid class token.
53
+ * Core theme preference handler. Manages initialization, switching themes,
54
+ * persistence to localStorage, synchronizing with the OS prefers-color-scheme preference,
55
+ * dynamic token mapping to CSS Custom Properties, and dispatching change events.
59
56
  */
60
- declare class Theme {
61
- #private;
62
- /** Creates a theme manager with default light/dark themes unless overridden. */
63
- constructor(options?: ThemeOptions);
64
- /** Registers system preference handling, applies the initial theme, emits an `init` change, and returns this instance. */
65
- init(): this;
66
- /** Returns the currently active theme definition. */
67
- get(): ThemeDefinition;
68
- /** Applies a configured theme by name, stores it when possible, emits a `set` change, and throws `Error` for unknown names. */
69
- set(name: string): ThemeDefinition;
70
- /** Toggles between the configured system light and dark themes, stores the preference when possible, and emits a `toggle` change. */
71
- toggle(): ThemeDefinition;
72
- /** Removes the stored preference when possible, applies the current system theme, and emits a `clearPreference` change. */
73
- clearPreference(): ThemeDefinition;
74
- /** Returns the stored configured theme name, or `null` during SSR, storage failures, or invalid stored preferences. */
57
+ interface Theme<TSchema extends Record<string, string> = Record<string, string>> {
58
+ /**
59
+ * Initializes the theme manager. Resolves the active theme (using the stored preference if valid,
60
+ * falling back to the current OS color-scheme preference), applies classes/attributes to the DOM,
61
+ * and registers the media query listener for automatic system preference updates.
62
+ *
63
+ * @param tokens - Optional runtime override token values to merge and apply.
64
+ * @returns The current `Theme` manager instance for method chaining.
65
+ * @sideEffect Registers a media query event listener on `window` and updates root DOM element attributes/styles. Dispatches a "themechange" event.
66
+ */
67
+ init(tokens?: Partial<Record<keyof TSchema, string>>): this;
68
+ /**
69
+ * Retrieves the active theme configuration including static and computed tokens.
70
+ *
71
+ * Token values are merged in this priority order (last wins):
72
+ * 1. CSS computed style — values read from `window.getComputedStyle` for tokens not defined in JS.
73
+ * 2. Theme static tokens — values defined in `ThemeDefinition.tokens` for the active theme.
74
+ * 3. Runtime overrides — values passed to `init()`, `set()`, `toggle()`, or other methods.
75
+ *
76
+ * @returns The active `ThemeDefinition` object. If `tokenSchema` is configured and a token is not
77
+ * explicitly defined in JS, its value is dynamically resolved from the computed style of the root DOM element in browser environments.
78
+ * @warning Reading computed styles from the DOM via `window.getComputedStyle` can trigger a synchronous layout reflow. Avoid calling `get()` frequently or inside high-performance loops.
79
+ */
80
+ get(): ThemeDefinition<TSchema>;
81
+ /**
82
+ * Activates a configured theme by name and updates the stored preference in `localStorage`.
83
+ *
84
+ * @param name - The name of the configured theme to activate.
85
+ * @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).
86
+ * @returns The activated `ThemeDefinition` with merged and resolved tokens.
87
+ * @throws {Error} If the specified theme name is not found in the configured themes list.
88
+ * @sideEffect Updates root DOM attributes, colorscheme styles, classes, and saves preference to `localStorage`. Dispatches a "themechange" event.
89
+ */
90
+ set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
91
+ /**
92
+ * Switches the theme between the configured system light and dark themes and persists the change.
93
+ * The next theme is always selected from `systemTheme.light` or `systemTheme.dark` based on the
94
+ * active theme's `colorScheme`, not by cycling the active theme name. In multi-theme setups where
95
+ * the active theme is not one of the system themes, `toggle()` still targets `systemTheme.light`
96
+ * or `systemTheme.dark`.
97
+ *
98
+ * @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).
99
+ * @returns The activated `ThemeDefinition` with merged and resolved tokens.
100
+ * @sideEffect Updates root DOM attributes, colorscheme styles, classes, and saves preference to `localStorage`. Dispatches a "themechange" event.
101
+ */
102
+ toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
103
+ /**
104
+ * Removes the explicit user theme preference from storage and resets the theme to match the OS system preference.
105
+ *
106
+ * @returns The activated system `ThemeDefinition` with merged and resolved tokens.
107
+ * @sideEffect Deletes the storage key from `localStorage`, updates root DOM attributes, styles, classes. Dispatches a "themechange" event.
108
+ */
109
+ clearPreference(): ThemeDefinition<TSchema>;
110
+ /**
111
+ * Retrieves the currently stored theme preference name from `localStorage`.
112
+ *
113
+ * @returns The stored theme name if valid and currently configured; otherwise `null` (e.g. during SSR, if storage is empty/unavailable, or if the stored theme name is not configured).
114
+ */
75
115
  getStored(): string | null;
76
- /** Returns the configured theme for the current OS color-scheme preference, or the default theme without browser support. */
77
- getSystem(): ThemeDefinition;
78
- /** Registers a listener for in-process theme changes and returns an unsubscribe function. */
79
- subscribe(listener: ThemeChangeListener): () => void;
80
- /** Removes the system preference listener and clears in-process subscribers. */
116
+ /**
117
+ * Resolves the configured theme that matches the active OS color-scheme preference.
118
+ *
119
+ * @returns The matching `ThemeDefinition`. Falls back to the default theme during SSR or if `matchMedia` is unavailable.
120
+ */
121
+ getSystem(): ThemeDefinition<TSchema>;
122
+ /**
123
+ * Registers a callback listener to receive notifications when the theme or its tokens change.
124
+ *
125
+ * @param listener - Callback function invoked on theme changes.
126
+ * @returns An unsubscribe function to remove the registered listener.
127
+ * @sideEffect Adds the listener to the internal callbacks registry.
128
+ */
129
+ subscribe(listener: ThemeChangeListener<TSchema>): () => void;
130
+ /**
131
+ * Cleans up the theme instance by removing all in-process change listeners and the system
132
+ * preference media query listener. Resets active tokens and the active theme name to the
133
+ * configured `defaultTheme` so the instance can be safely re-initialized with `init()`.
134
+ *
135
+ * @sideEffect Removes event listeners from `window` and clears internal subscriber sets.
136
+ */
81
137
  destroy(): void;
82
138
  }
83
139
  //#endregion
84
- export { SystemThemeMap, THEME_CHANGE_EVENT, Theme, ThemeChangeDetail, ThemeChangeListener, ThemeChangeSource, ThemeClassResolver, ThemeDefinition, ThemeOptions, darkTheme, lightTheme };
140
+ //#region src/constants.d.ts
141
+ /** Window event name dispatched with `ThemeChangeDetail` after a theme change is applied in browser environments. */
142
+ declare const THEME_CHANGE_EVENT = "themechange";
143
+ /** Built-in light theme used by default and available for custom theme lists. */
144
+ declare const LIGHT_THEME: ThemeDefinition;
145
+ /** Built-in dark theme used by default and available for custom theme lists. */
146
+ declare const DARK_THEME: ThemeDefinition;
147
+ //#endregion
148
+ //#region src/theme.d.ts
149
+ /**
150
+ * Factory function that creates and returns a `Theme` instance.
151
+ *
152
+ * @param options - Configuration options for theme definitions, persistence keys, DOM attributes, custom class resolvers, and dynamic token schemas.
153
+ * @returns A `Theme` instance.
154
+ * @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.
155
+ */
156
+ declare function createTheme<TSchema extends Record<string, string> = Record<string, string>>(options?: ThemeOptions<TSchema>): Theme<TSchema>;
157
+ //#endregion
158
+ export { DARK_THEME, LIGHT_THEME, type SystemThemeMap, THEME_CHANGE_EVENT, type Theme, type ThemeChangeDetail, type ThemeChangeListener, type ThemeChangeSource, type ThemeClassResolver, type ThemeDefinition, type ThemeOptions, createTheme };