@codenhub/theme 0.0.1 → 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.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Coden Agency
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
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.