@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 +201 -0
- package/README.md +171 -62
- package/dist/index.d.ts +116 -42
- package/dist/index.js +397 -112
- package/package.json +12 -3
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
|
|
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 {
|
|
26
|
+
import { createTheme } from "@codenhub/theme";
|
|
17
27
|
|
|
18
|
-
const theme =
|
|
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 {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
232
|
+
isTailwindCss?: boolean;
|
|
233
|
+
shouldApplyClass?: boolean | ThemeClassResolver<TSchema>;
|
|
234
|
+
tokenSchema?: TSchema;
|
|
170
235
|
}
|
|
171
236
|
```
|
|
172
237
|
|
|
173
|
-
| Option
|
|
174
|
-
|
|
|
175
|
-
| `themes`
|
|
176
|
-
| `defaultTheme`
|
|
177
|
-
| `systemTheme`
|
|
178
|
-
| `storageKey`
|
|
179
|
-
| `attribute`
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
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
|
|
195
|
-
| ------------- |
|
|
196
|
-
| `name` | `string`
|
|
197
|
-
| `colorScheme` | `"light" \| "dark"`
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
279
|
-
const
|
|
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 {
|
|
413
|
+
import { createTheme, DARK_THEME, LIGHT_THEME } from "@codenhub/theme";
|
|
308
414
|
|
|
309
|
-
const theme =
|
|
310
|
-
themes: [
|
|
415
|
+
const theme = createTheme({
|
|
416
|
+
themes: [LIGHT_THEME, DARK_THEME, { name: "high-contrast", colorScheme: "dark" }],
|
|
311
417
|
systemTheme: { light: "light", dark: "high-contrast" },
|
|
312
|
-
|
|
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 {
|
|
428
|
+
import { createTheme, THEME_CHANGE_EVENT, type ThemeChangeDetail } from "@codenhub/theme";
|
|
323
429
|
|
|
324
|
-
const theme =
|
|
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
|
|
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.
|