@codenhub/theme 0.0.2 → 0.1.0
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 -201
- package/README.md +164 -62
- package/dist/index.d.ts +116 -42
- package/dist/index.js +1 -198
- package/package.json +12 -4
package/LICENSE
CHANGED
|
@@ -1,201 +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.
|
|
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,14 @@
|
|
|
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
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
pnpm add @codenhub/theme
|
|
9
|
+
npm install @codenhub/theme
|
|
10
|
+
yarn add @codenhub/theme
|
|
11
|
+
bun add @codenhub/theme
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
## Usage
|
|
@@ -13,9 +16,9 @@ pnpm add @codenhub/theme
|
|
|
13
16
|
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
17
|
|
|
15
18
|
```ts
|
|
16
|
-
import {
|
|
19
|
+
import { createTheme } from "@codenhub/theme";
|
|
17
20
|
|
|
18
|
-
const theme =
|
|
21
|
+
const theme = createTheme({ isTailwindCss: false, shouldApplyClass: true });
|
|
19
22
|
|
|
20
23
|
theme.init();
|
|
21
24
|
theme.set("dark");
|
|
@@ -24,6 +27,50 @@ theme.toggle();
|
|
|
24
27
|
|
|
25
28
|
Call `destroy()` during app or test cleanup when the instance is no longer used.
|
|
26
29
|
|
|
30
|
+
### Preventing Flash of Unstyled Content (FOUC)
|
|
31
|
+
|
|
32
|
+
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:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script>
|
|
36
|
+
(function () {
|
|
37
|
+
try {
|
|
38
|
+
const key = "app-theme-preference";
|
|
39
|
+
const attribute = "data-theme";
|
|
40
|
+
let theme = "light";
|
|
41
|
+
try {
|
|
42
|
+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
43
|
+
theme = "dark";
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
try {
|
|
47
|
+
const stored = localStorage.getItem(key);
|
|
48
|
+
// Add all your configured theme names here to validate the stored value.
|
|
49
|
+
const allowed = ["light", "dark"];
|
|
50
|
+
if (stored && allowed.includes(stored)) {
|
|
51
|
+
theme = stored;
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
document.documentElement.setAttribute(attribute, theme);
|
|
55
|
+
|
|
56
|
+
// List the names of all your dark-scheme themes here.
|
|
57
|
+
// Using an explicit set avoids false matches from substring checks (e.g. "midnight" is dark
|
|
58
|
+
// but would not match "dark"; "darkroom" would match but may not be a dark theme).
|
|
59
|
+
const darkThemes = new Set(["dark"]);
|
|
60
|
+
const isDark = darkThemes.has(theme);
|
|
61
|
+
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
|
|
62
|
+
document.documentElement.classList.add("theme-" + theme);
|
|
63
|
+
|
|
64
|
+
// If using Tailwind CSS, also toggle "dark" class:
|
|
65
|
+
// document.documentElement.classList.toggle("dark", isDark);
|
|
66
|
+
} catch (_) {}
|
|
67
|
+
})();
|
|
68
|
+
</script>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> [!WARNING]
|
|
72
|
+
> 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.
|
|
73
|
+
|
|
27
74
|
## Reference
|
|
28
75
|
|
|
29
76
|
### `@codenhub/theme`
|
|
@@ -31,9 +78,10 @@ Call `destroy()` during app or test cleanup when the instance is no longer used.
|
|
|
31
78
|
Primary entrypoint for the theme preference API.
|
|
32
79
|
|
|
33
80
|
```ts
|
|
34
|
-
import {
|
|
81
|
+
import { createTheme, DARK_THEME, LIGHT_THEME, THEME_CHANGE_EVENT } from "@codenhub/theme";
|
|
35
82
|
import type {
|
|
36
83
|
SystemThemeMap,
|
|
84
|
+
Theme,
|
|
37
85
|
ThemeChangeDetail,
|
|
38
86
|
ThemeChangeListener,
|
|
39
87
|
ThemeChangeSource,
|
|
@@ -49,63 +97,72 @@ Supported import paths:
|
|
|
49
97
|
| ----------------- | ----------------------------------- |
|
|
50
98
|
| `@codenhub/theme` | Main JavaScript and TypeScript API. |
|
|
51
99
|
|
|
100
|
+
#### `createTheme()`
|
|
101
|
+
|
|
102
|
+
Factory function that returns a `Theme` manager instance.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
function createTheme<TSchema extends Record<string, string> = Record<string, string>>(
|
|
106
|
+
options?: ThemeOptions<TSchema>,
|
|
107
|
+
): Theme<TSchema>;
|
|
108
|
+
```
|
|
109
|
+
|
|
52
110
|
#### `Theme`
|
|
53
111
|
|
|
54
|
-
Manages the active theme, storage preference, DOM attribute, `colorScheme` style, classes, system preference listener, and change notifications.
|
|
112
|
+
Manages the active theme, storage preference, DOM attribute, `colorScheme` style, classes, system preference listener, dynamic tokens, and change notifications.
|
|
55
113
|
|
|
56
114
|
```ts
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
clearPreference(): ThemeDefinition;
|
|
115
|
+
interface Theme<TSchema extends Record<string, string> = Record<string, string>> {
|
|
116
|
+
init(tokens?: Partial<Record<keyof TSchema, string>>): this;
|
|
117
|
+
get(): ThemeDefinition<TSchema>;
|
|
118
|
+
set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
119
|
+
toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
120
|
+
clearPreference(): ThemeDefinition<TSchema>;
|
|
64
121
|
getStored(): string | null;
|
|
65
|
-
getSystem(): ThemeDefinition
|
|
66
|
-
subscribe(listener: ThemeChangeListener): () => void;
|
|
122
|
+
getSystem(): ThemeDefinition<TSchema>;
|
|
123
|
+
subscribe(listener: ThemeChangeListener<TSchema>): () => void;
|
|
67
124
|
destroy(): void;
|
|
68
125
|
}
|
|
69
126
|
```
|
|
70
127
|
|
|
71
128
|
Import from `@codenhub/theme`.
|
|
72
129
|
|
|
73
|
-
The
|
|
130
|
+
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
131
|
|
|
75
132
|
##### `init()`
|
|
76
133
|
|
|
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.
|
|
134
|
+
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
135
|
|
|
81
136
|
```ts
|
|
82
|
-
function init(): this;
|
|
137
|
+
function init(tokens?: Partial<Record<keyof TSchema, string>>): this;
|
|
83
138
|
```
|
|
84
139
|
|
|
140
|
+
Repeated calls do not register duplicate system preference listeners.
|
|
141
|
+
|
|
85
142
|
##### `get()`
|
|
86
143
|
|
|
87
|
-
Returns the active theme definition.
|
|
144
|
+
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
145
|
|
|
89
146
|
```ts
|
|
90
|
-
function get(): ThemeDefinition
|
|
147
|
+
function get(): ThemeDefinition<TSchema>;
|
|
91
148
|
```
|
|
92
149
|
|
|
93
150
|
##### `set()`
|
|
94
151
|
|
|
95
|
-
Activates a configured theme by name and stores the explicit preference when browser storage is available.
|
|
152
|
+
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
153
|
|
|
97
154
|
```ts
|
|
98
|
-
function set(name: string): ThemeDefinition
|
|
155
|
+
function set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
99
156
|
```
|
|
100
157
|
|
|
101
158
|
Throws `Error` when `name` is not configured.
|
|
102
159
|
|
|
103
160
|
##### `toggle()`
|
|
104
161
|
|
|
105
|
-
Toggles between the configured system light and dark theme
|
|
162
|
+
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
163
|
|
|
107
164
|
```ts
|
|
108
|
-
function toggle(): ThemeDefinition
|
|
165
|
+
function toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
109
166
|
```
|
|
110
167
|
|
|
111
168
|
##### `clearPreference()`
|
|
@@ -113,7 +170,7 @@ function toggle(): ThemeDefinition;
|
|
|
113
170
|
Removes the stored preference and activates the current system theme.
|
|
114
171
|
|
|
115
172
|
```ts
|
|
116
|
-
function clearPreference(): ThemeDefinition
|
|
173
|
+
function clearPreference(): ThemeDefinition<TSchema>;
|
|
117
174
|
```
|
|
118
175
|
|
|
119
176
|
##### `getStored()`
|
|
@@ -131,7 +188,7 @@ Returns `null` during SSR, when storage is unavailable, when storage access thro
|
|
|
131
188
|
Returns the configured theme for the current `prefers-color-scheme` value.
|
|
132
189
|
|
|
133
190
|
```ts
|
|
134
|
-
function getSystem(): ThemeDefinition
|
|
191
|
+
function getSystem(): ThemeDefinition<TSchema>;
|
|
135
192
|
```
|
|
136
193
|
|
|
137
194
|
Returns the default theme during SSR or when `matchMedia` is unavailable.
|
|
@@ -141,14 +198,14 @@ Returns the default theme during SSR or when `matchMedia` is unavailable.
|
|
|
141
198
|
Registers an in-process listener for theme changes.
|
|
142
199
|
|
|
143
200
|
```ts
|
|
144
|
-
function subscribe(listener: ThemeChangeListener): () => void;
|
|
201
|
+
function subscribe(listener: ThemeChangeListener<TSchema>): () => void;
|
|
145
202
|
```
|
|
146
203
|
|
|
147
204
|
Returns an unsubscribe function.
|
|
148
205
|
|
|
149
206
|
##### `destroy()`
|
|
150
207
|
|
|
151
|
-
Removes the system preference listener
|
|
208
|
+
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
209
|
|
|
153
210
|
```ts
|
|
154
211
|
function destroy(): void;
|
|
@@ -159,42 +216,46 @@ Call this during app or test cleanup when the instance is no longer used.
|
|
|
159
216
|
#### `ThemeOptions`
|
|
160
217
|
|
|
161
218
|
```ts
|
|
162
|
-
interface ThemeOptions {
|
|
163
|
-
themes?: readonly ThemeDefinition[];
|
|
219
|
+
interface ThemeOptions<TSchema extends Record<string, string> = Record<string, string>> {
|
|
220
|
+
themes?: readonly ThemeDefinition<TSchema>[];
|
|
164
221
|
defaultTheme?: string;
|
|
165
222
|
systemTheme?: SystemThemeMap;
|
|
166
223
|
storageKey?: string;
|
|
167
224
|
attribute?: string;
|
|
168
|
-
|
|
169
|
-
|
|
225
|
+
isTailwindCss?: boolean;
|
|
226
|
+
shouldApplyClass?: boolean | ThemeClassResolver<TSchema>;
|
|
227
|
+
tokenSchema?: TSchema;
|
|
170
228
|
}
|
|
171
229
|
```
|
|
172
230
|
|
|
173
|
-
| Option
|
|
174
|
-
|
|
|
175
|
-
| `themes`
|
|
176
|
-
| `defaultTheme`
|
|
177
|
-
| `systemTheme`
|
|
178
|
-
| `storageKey`
|
|
179
|
-
| `attribute`
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
231
|
+
| Option | Type | Default | Description |
|
|
232
|
+
| ------------------ | ------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
233
|
+
| `themes` | `readonly ThemeDefinition[]` | `[LIGHT_THEME, DARK_THEME]` | Defines available themes. |
|
|
234
|
+
| `defaultTheme` | `string` | `"light"` | Theme used before init and when browser APIs are unavailable. |
|
|
235
|
+
| `systemTheme` | `SystemThemeMap` | `{ light: "light", dark: "dark" }` | Maps OS light and dark preferences to configured theme names. |
|
|
236
|
+
| `storageKey` | `string` | `"app-theme-preference"` | Key used for `localStorage`. |
|
|
237
|
+
| `attribute` | `string` | `"data-theme"` | Attribute set on `document.documentElement`. |
|
|
238
|
+
| `isTailwindCss` | `boolean` | `false` | Toggles the `dark` class when the active theme has `colorScheme: "dark"`. |
|
|
239
|
+
| `shouldApplyClass` | `boolean` or `(theme: ThemeDefinition) => string` | `true` | Adds `theme-${name}`, no class, or a resolver-provided class to `document.documentElement`. |
|
|
240
|
+
| `tokenSchema` | `TSchema` | `undefined` | Schema mapping theme token names to their corresponding CSS Custom Property names. |
|
|
182
241
|
|
|
183
242
|
When class application is enabled, each theme application removes classes for all configured themes, then adds the class for the active theme.
|
|
184
243
|
|
|
185
244
|
#### `ThemeDefinition`
|
|
186
245
|
|
|
187
246
|
```ts
|
|
188
|
-
interface ThemeDefinition {
|
|
247
|
+
interface ThemeDefinition<TSchema extends Record<string, string> = Record<string, string>> {
|
|
189
248
|
name: string;
|
|
190
249
|
colorScheme: "light" | "dark";
|
|
250
|
+
tokens?: Partial<Record<keyof TSchema, string>>;
|
|
191
251
|
}
|
|
192
252
|
```
|
|
193
253
|
|
|
194
|
-
| Field | Type
|
|
195
|
-
| ------------- |
|
|
196
|
-
| `name` | `string`
|
|
197
|
-
| `colorScheme` | `"light" \| "dark"`
|
|
254
|
+
| Field | Type | Description |
|
|
255
|
+
| ------------- | ---------------------------------------- | ---------------------------------------------------------------- |
|
|
256
|
+
| `name` | `string` | Unique theme name used for storage, attributes, and class names. |
|
|
257
|
+
| `colorScheme` | `"light" \| "dark"` | Browser color scheme applied through `style.colorScheme`. |
|
|
258
|
+
| `tokens` | `Partial<Record<keyof TSchema, string>>` | Optional theme-specific static token values. |
|
|
198
259
|
|
|
199
260
|
#### `SystemThemeMap`
|
|
200
261
|
|
|
@@ -215,7 +276,9 @@ interface SystemThemeMap {
|
|
|
215
276
|
Returns the class name applied to `document.documentElement` for a theme.
|
|
216
277
|
|
|
217
278
|
```ts
|
|
218
|
-
type ThemeClassResolver =
|
|
279
|
+
type ThemeClassResolver<TSchema extends Record<string, string> = Record<string, string>> = (
|
|
280
|
+
theme: ThemeDefinition<TSchema>,
|
|
281
|
+
) => string;
|
|
219
282
|
```
|
|
220
283
|
|
|
221
284
|
The returned class name must be a single non-empty class token without whitespace.
|
|
@@ -227,7 +290,9 @@ The returned class name must be a single non-empty class token without whitespac
|
|
|
227
290
|
Listener passed to `theme.subscribe()`.
|
|
228
291
|
|
|
229
292
|
```ts
|
|
230
|
-
type ThemeChangeListener =
|
|
293
|
+
type ThemeChangeListener<TSchema extends Record<string, string> = Record<string, string>> = (
|
|
294
|
+
detail: ThemeChangeDetail<TSchema>,
|
|
295
|
+
) => void;
|
|
231
296
|
```
|
|
232
297
|
|
|
233
298
|
#### `THEME_CHANGE_EVENT`
|
|
@@ -241,10 +306,10 @@ const THEME_CHANGE_EVENT = "themechange";
|
|
|
241
306
|
#### `ThemeChangeDetail`
|
|
242
307
|
|
|
243
308
|
```ts
|
|
244
|
-
interface ThemeChangeDetail {
|
|
309
|
+
interface ThemeChangeDetail<TSchema extends Record<string, string> = Record<string, string>> {
|
|
245
310
|
name: string;
|
|
246
|
-
theme: ThemeDefinition
|
|
247
|
-
source:
|
|
311
|
+
theme: ThemeDefinition<TSchema>;
|
|
312
|
+
source: ThemeChangeSource;
|
|
248
313
|
}
|
|
249
314
|
```
|
|
250
315
|
|
|
@@ -275,12 +340,46 @@ type ThemeChangeSource = "init" | "set" | "toggle" | "clearPreference" | "system
|
|
|
275
340
|
Built-in theme definitions.
|
|
276
341
|
|
|
277
342
|
```ts
|
|
278
|
-
const
|
|
279
|
-
const
|
|
343
|
+
const LIGHT_THEME: ThemeDefinition = { name: "light", colorScheme: "light" };
|
|
344
|
+
const DARK_THEME: ThemeDefinition = { name: "dark", colorScheme: "dark" };
|
|
280
345
|
```
|
|
281
346
|
|
|
282
347
|
## Examples
|
|
283
348
|
|
|
349
|
+
### Dynamic CSS Tokens
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { createTheme } from "@codenhub/theme";
|
|
353
|
+
|
|
354
|
+
// 1. Define schema
|
|
355
|
+
const tokenSchema = {
|
|
356
|
+
primary: "--color-primary",
|
|
357
|
+
background: "--color-bg",
|
|
358
|
+
} as const;
|
|
359
|
+
|
|
360
|
+
// 2. Initialize with schema and optional theme static token values
|
|
361
|
+
const theme = createTheme({
|
|
362
|
+
tokenSchema,
|
|
363
|
+
themes: [
|
|
364
|
+
{
|
|
365
|
+
name: "light",
|
|
366
|
+
colorScheme: "light",
|
|
367
|
+
tokens: { primary: "#0070f3", background: "#ffffff" },
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: "dark",
|
|
371
|
+
colorScheme: "dark",
|
|
372
|
+
tokens: { primary: "#3291ff", background: "#000000" },
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
theme.init();
|
|
378
|
+
|
|
379
|
+
// 3. Switch theme and pass runtime overrides (e.g. from dynamic branding API)
|
|
380
|
+
theme.set("dark", { primary: "#ff007f" });
|
|
381
|
+
```
|
|
382
|
+
|
|
284
383
|
### Define CSS Tokens
|
|
285
384
|
|
|
286
385
|
```css
|
|
@@ -304,12 +403,12 @@ body {
|
|
|
304
403
|
### Add More Themes
|
|
305
404
|
|
|
306
405
|
```ts
|
|
307
|
-
import {
|
|
406
|
+
import { createTheme, DARK_THEME, LIGHT_THEME } from "@codenhub/theme";
|
|
308
407
|
|
|
309
|
-
const theme =
|
|
310
|
-
themes: [
|
|
408
|
+
const theme = createTheme({
|
|
409
|
+
themes: [LIGHT_THEME, DARK_THEME, { name: "high-contrast", colorScheme: "dark" }],
|
|
311
410
|
systemTheme: { light: "light", dark: "high-contrast" },
|
|
312
|
-
|
|
411
|
+
shouldApplyClass: (definition) => `mode-${definition.name}`,
|
|
313
412
|
});
|
|
314
413
|
|
|
315
414
|
theme.init();
|
|
@@ -319,9 +418,9 @@ theme.set("high-contrast");
|
|
|
319
418
|
### Listen For Changes
|
|
320
419
|
|
|
321
420
|
```ts
|
|
322
|
-
import {
|
|
421
|
+
import { createTheme, THEME_CHANGE_EVENT, type ThemeChangeDetail } from "@codenhub/theme";
|
|
323
422
|
|
|
324
|
-
const theme =
|
|
423
|
+
const theme = createTheme().init();
|
|
325
424
|
|
|
326
425
|
const unsubscribe = theme.subscribe((detail) => {
|
|
327
426
|
console.log(detail.name, detail.theme, detail.source);
|
|
@@ -342,7 +441,7 @@ theme.destroy();
|
|
|
342
441
|
- Browser integration uses `document.documentElement`, `document.documentElement.style.colorScheme`, `window.matchMedia`, `localStorage`, and `CustomEvent`.
|
|
343
442
|
- SSR is supported; DOM, storage, media query, and event work is skipped when browser APIs are unavailable.
|
|
344
443
|
- System preference changes update the active theme only when there is no valid stored preference.
|
|
345
|
-
- `localStorage` read, write, and remove errors are
|
|
444
|
+
- `localStorage` read, write, and remove errors are logged to the console via `console.error` and treated as unavailable storage.
|
|
346
445
|
- Consumers own CSS variables, selectors, visual tokens, and persistence consent requirements.
|
|
347
446
|
- No CSS file, design tokens, framework adapter, or peer dependency is provided.
|
|
348
447
|
|
|
@@ -351,5 +450,8 @@ theme.destroy();
|
|
|
351
450
|
- Does not provide design tokens or generated CSS.
|
|
352
451
|
- Does not provide React, Vue, or other framework bindings.
|
|
353
452
|
- Does not provide server-side persistence.
|
|
354
|
-
- Does not synchronize theme changes across tabs.
|
|
355
453
|
- Does not manage user consent requirements for storage.
|
|
454
|
+
|
|
455
|
+
## License
|
|
456
|
+
|
|
457
|
+
This project is licensed under the [Apache-2.0](LICENSE) license.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
//#region src/
|
|
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 `
|
|
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
|
-
|
|
46
|
+
isTailwindCss?: boolean;
|
|
47
47
|
/** Whether and how to apply a theme-specific class to `document.documentElement`. */
|
|
48
|
-
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,198 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/** Window event name dispatched with `ThemeChangeDetail` after a theme change is applied in browser environments. */
|
|
3
|
-
const THEME_CHANGE_EVENT = "themechange";
|
|
4
|
-
const DEFAULT_STORAGE_KEY = "app-theme-preference";
|
|
5
|
-
const DEFAULT_ATTRIBUTE = "data-theme";
|
|
6
|
-
const DARK_CLASS = "dark";
|
|
7
|
-
const PREFERS_DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
8
|
-
const CLASS_TOKEN_WHITESPACE = /\s/;
|
|
9
|
-
/** Built-in light theme used by default and available for custom theme lists. */
|
|
10
|
-
const lightTheme = {
|
|
11
|
-
name: "light",
|
|
12
|
-
colorScheme: "light"
|
|
13
|
-
};
|
|
14
|
-
/** Built-in dark theme used by default and available for custom theme lists. */
|
|
15
|
-
const darkTheme = {
|
|
16
|
-
name: "dark",
|
|
17
|
-
colorScheme: "dark"
|
|
18
|
-
};
|
|
19
|
-
const defaultOptions = {
|
|
20
|
-
themes: [lightTheme, darkTheme],
|
|
21
|
-
defaultTheme: lightTheme.name,
|
|
22
|
-
systemTheme: {
|
|
23
|
-
light: lightTheme.name,
|
|
24
|
-
dark: darkTheme.name
|
|
25
|
-
},
|
|
26
|
-
storageKey: DEFAULT_STORAGE_KEY,
|
|
27
|
-
attribute: DEFAULT_ATTRIBUTE,
|
|
28
|
-
tailwindcss: false,
|
|
29
|
-
applyClass: true
|
|
30
|
-
};
|
|
31
|
-
const isBrowser = () => {
|
|
32
|
-
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
33
|
-
};
|
|
34
|
-
const getThemeClass = (theme, applyClass) => {
|
|
35
|
-
if (applyClass === false) return null;
|
|
36
|
-
if (typeof applyClass === "function") {
|
|
37
|
-
const className = applyClass(theme);
|
|
38
|
-
assertClassToken(className, `Theme class resolver returned an invalid class for theme: ${theme.name}.`);
|
|
39
|
-
return className;
|
|
40
|
-
}
|
|
41
|
-
return `theme-${theme.name}`;
|
|
42
|
-
};
|
|
43
|
-
const assertClassToken = (className, message) => {
|
|
44
|
-
if (className.length === 0 || CLASS_TOKEN_WHITESPACE.test(className)) throw new Error(message);
|
|
45
|
-
};
|
|
46
|
-
const assertThemeConfig = (options) => {
|
|
47
|
-
const names = /* @__PURE__ */ new Set();
|
|
48
|
-
for (const theme of options.themes) {
|
|
49
|
-
if (theme.name.trim().length === 0) throw new Error("Theme names must be non-empty.");
|
|
50
|
-
if (names.has(theme.name)) throw new Error(`Duplicate theme name: ${theme.name}.`);
|
|
51
|
-
names.add(theme.name);
|
|
52
|
-
if (options.applyClass === true) assertClassToken(`theme-${theme.name}`, `Theme name cannot be used as a default theme class: ${theme.name}.`);
|
|
53
|
-
}
|
|
54
|
-
if (!names.has(options.defaultTheme)) throw new Error(`Default theme is not configured: ${options.defaultTheme}.`);
|
|
55
|
-
if (!names.has(options.systemTheme.light)) throw new Error(`System light theme is not configured: ${options.systemTheme.light}.`);
|
|
56
|
-
if (!names.has(options.systemTheme.dark)) throw new Error(`System dark theme is not configured: ${options.systemTheme.dark}.`);
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* Manages theme preference, DOM application, system preference changes, and change notifications.
|
|
60
|
-
*
|
|
61
|
-
* The constructor validates configured theme names, default and system mappings, and default class tokens.
|
|
62
|
-
* Theme application throws `Error` when a requested theme is missing or a class resolver returns an invalid class token.
|
|
63
|
-
*/
|
|
64
|
-
var Theme = class {
|
|
65
|
-
#options;
|
|
66
|
-
#activeName;
|
|
67
|
-
#listeners = /* @__PURE__ */ new Set();
|
|
68
|
-
#mediaQueryList = null;
|
|
69
|
-
#handleSystemChange = (event) => {
|
|
70
|
-
if (this.getStored() !== null) return;
|
|
71
|
-
const name = event.matches ? this.#options.systemTheme.dark : this.#options.systemTheme.light;
|
|
72
|
-
this.#activate(name, "system", { shouldStore: false });
|
|
73
|
-
};
|
|
74
|
-
/** Creates a theme manager with default light/dark themes unless overridden. */
|
|
75
|
-
constructor(options = {}) {
|
|
76
|
-
this.#options = {
|
|
77
|
-
...defaultOptions,
|
|
78
|
-
...options,
|
|
79
|
-
systemTheme: {
|
|
80
|
-
...defaultOptions.systemTheme,
|
|
81
|
-
...options.systemTheme
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
assertThemeConfig(this.#options);
|
|
85
|
-
this.#activeName = this.#options.defaultTheme;
|
|
86
|
-
}
|
|
87
|
-
/** Registers system preference handling, applies the initial theme, emits an `init` change, and returns this instance. */
|
|
88
|
-
init() {
|
|
89
|
-
this.#registerSystemListener();
|
|
90
|
-
this.#activate(this.getStored() ?? this.getSystem().name, "init", { shouldStore: false });
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
/** Returns the currently active theme definition. */
|
|
94
|
-
get() {
|
|
95
|
-
return this.#getTheme(this.#activeName) ?? this.#getTheme(this.#options.defaultTheme);
|
|
96
|
-
}
|
|
97
|
-
/** Applies a configured theme by name, stores it when possible, emits a `set` change, and throws `Error` for unknown names. */
|
|
98
|
-
set(name) {
|
|
99
|
-
return this.#activate(name, "set", { shouldStore: true });
|
|
100
|
-
}
|
|
101
|
-
/** Toggles between the configured system light and dark themes, stores the preference when possible, and emits a `toggle` change. */
|
|
102
|
-
toggle() {
|
|
103
|
-
const nextName = this.get().name === this.#options.systemTheme.dark ? this.#options.systemTheme.light : this.#options.systemTheme.dark;
|
|
104
|
-
return this.#activate(nextName, "toggle", { shouldStore: true });
|
|
105
|
-
}
|
|
106
|
-
/** Removes the stored preference when possible, applies the current system theme, and emits a `clearPreference` change. */
|
|
107
|
-
clearPreference() {
|
|
108
|
-
this.#removeStored();
|
|
109
|
-
return this.#activate(this.getSystem().name, "clearPreference", { shouldStore: false });
|
|
110
|
-
}
|
|
111
|
-
/** Returns the stored configured theme name, or `null` during SSR, storage failures, or invalid stored preferences. */
|
|
112
|
-
getStored() {
|
|
113
|
-
if (!isBrowser()) return null;
|
|
114
|
-
try {
|
|
115
|
-
const storedName = window.localStorage.getItem(this.#options.storageKey);
|
|
116
|
-
return storedName !== null && this.#getTheme(storedName) !== null ? storedName : null;
|
|
117
|
-
} catch {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/** Returns the configured theme for the current OS color-scheme preference, or the default theme without browser support. */
|
|
122
|
-
getSystem() {
|
|
123
|
-
if (!isBrowser() || typeof window.matchMedia !== "function") return this.#getTheme(this.#options.defaultTheme);
|
|
124
|
-
const name = window.matchMedia(PREFERS_DARK_QUERY).matches ? this.#options.systemTheme.dark : this.#options.systemTheme.light;
|
|
125
|
-
return this.#getTheme(name);
|
|
126
|
-
}
|
|
127
|
-
/** Registers a listener for in-process theme changes and returns an unsubscribe function. */
|
|
128
|
-
subscribe(listener) {
|
|
129
|
-
this.#listeners.add(listener);
|
|
130
|
-
return () => {
|
|
131
|
-
this.#listeners.delete(listener);
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
/** Removes the system preference listener and clears in-process subscribers. */
|
|
135
|
-
destroy() {
|
|
136
|
-
if (this.#mediaQueryList !== null) {
|
|
137
|
-
this.#mediaQueryList.removeEventListener("change", this.#handleSystemChange);
|
|
138
|
-
this.#mediaQueryList = null;
|
|
139
|
-
}
|
|
140
|
-
this.#listeners.clear();
|
|
141
|
-
}
|
|
142
|
-
#activate(name, source, options) {
|
|
143
|
-
const theme = this.#getTheme(name);
|
|
144
|
-
this.#activeName = theme.name;
|
|
145
|
-
if (options.shouldStore) this.#store(theme.name);
|
|
146
|
-
this.#apply(theme);
|
|
147
|
-
this.#emit({
|
|
148
|
-
name: theme.name,
|
|
149
|
-
theme,
|
|
150
|
-
source
|
|
151
|
-
});
|
|
152
|
-
return theme;
|
|
153
|
-
}
|
|
154
|
-
#apply(theme) {
|
|
155
|
-
if (!isBrowser()) return;
|
|
156
|
-
const root = document.documentElement;
|
|
157
|
-
const nextClass = getThemeClass(theme, this.#options.applyClass);
|
|
158
|
-
const configuredClasses = this.#options.themes.map((configuredTheme) => getThemeClass(configuredTheme, this.#options.applyClass)).filter((configuredClass) => configuredClass !== null);
|
|
159
|
-
root.setAttribute(this.#options.attribute, theme.name);
|
|
160
|
-
root.style.colorScheme = theme.colorScheme;
|
|
161
|
-
for (const configuredClass of configuredClasses) root.classList.remove(configuredClass);
|
|
162
|
-
if (nextClass !== null) root.classList.add(nextClass);
|
|
163
|
-
if (this.#options.tailwindcss) root.classList.toggle(DARK_CLASS, theme.colorScheme === "dark");
|
|
164
|
-
}
|
|
165
|
-
#emit(detail) {
|
|
166
|
-
for (const listener of this.#listeners) listener(detail);
|
|
167
|
-
if (!isBrowser()) return;
|
|
168
|
-
window.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, { detail }));
|
|
169
|
-
}
|
|
170
|
-
#getTheme(name) {
|
|
171
|
-
const theme = this.#options.themes.find((candidate) => candidate.name === name);
|
|
172
|
-
if (theme === void 0) throw new Error(`Theme is not configured: ${name}.`);
|
|
173
|
-
return theme;
|
|
174
|
-
}
|
|
175
|
-
#registerSystemListener() {
|
|
176
|
-
if (!isBrowser() || typeof window.matchMedia !== "function" || this.#mediaQueryList !== null) return;
|
|
177
|
-
this.#mediaQueryList = window.matchMedia(PREFERS_DARK_QUERY);
|
|
178
|
-
this.#mediaQueryList.addEventListener("change", this.#handleSystemChange);
|
|
179
|
-
}
|
|
180
|
-
#store(name) {
|
|
181
|
-
if (!isBrowser()) return;
|
|
182
|
-
try {
|
|
183
|
-
window.localStorage.setItem(this.#options.storageKey, name);
|
|
184
|
-
} catch {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
#removeStored() {
|
|
189
|
-
if (!isBrowser()) return;
|
|
190
|
-
try {
|
|
191
|
-
window.localStorage.removeItem(this.#options.storageKey);
|
|
192
|
-
} catch {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
//#endregion
|
|
198
|
-
export { THEME_CHANGE_EVENT, Theme, darkTheme, lightTheme };
|
|
1
|
+
const e=`themechange`,t=/\s/,n=Object.freeze({name:`light`,colorScheme:`light`}),r=Object.freeze({name:`dark`,colorScheme:`dark`}),i=Object.freeze({themes:Object.freeze([n,r]),defaultTheme:n.name,systemTheme:Object.freeze({light:n.name,dark:r.name}),storageKey:`app-theme-preference`,attribute:`data-theme`,isTailwindCss:!1,shouldApplyClass:!0}),a=(e,t)=>{if(t===!1)return null;if(typeof t==`function`){let n=t(e);return o(n,`Theme class resolver returned an invalid class for theme: ${e.name}.`),n}let n=`theme-${e.name}`;return o(n,`Theme name cannot be used as a default theme class: ${e.name}.`),n},o=(e,n)=>{if(typeof e!=`string`||e.length===0||t.test(e))throw Error(n)},s=e=>{let{theme:t,options:n,activeTokens:r,resolvedClasses:i,nextClass:a}=e;if(typeof document>`u`)return;let o=document.documentElement;o.setAttribute(n.attribute,t.name),o.style.colorScheme=t.colorScheme;for(let e of i)e!==a&&o.classList.remove(e);if(a!==null&&o.classList.add(a),n.isTailwindCss&&o.classList.toggle(`dark`,t.colorScheme===`dark`),n.tokenSchema){let e=n.tokenSchema,i={...t.tokens,...r};for(let t of Object.keys(e)){let n=e[t],r=i[t];r==null?o.style.removeProperty(n):o.style.setProperty(n,r)}}},c=e=>{let{theme:t,options:n,activeTokens:r}=e,i={};if(typeof window>`u`||typeof window.getComputedStyle!=`function`||typeof document>`u`||!n.tokenSchema)return i;let a=document.documentElement;try{let e=n.tokenSchema,o={...t.tokens,...r};if(!Object.keys(e).some(e=>o[e]===void 0))return i;let s=window.getComputedStyle(a);if(!s)return i;for(let t of Object.keys(e))if(o[t]===void 0){let n=s.getPropertyValue(e[t]).trim();n&&(i[t]=n)}}catch(e){console.error(`[theme] Failed to read computed token styles:`,e)}return i},l=t=>{typeof window>`u`||typeof window.dispatchEvent!=`function`||typeof window.CustomEvent!=`function`||window.dispatchEvent(new CustomEvent(e,{detail:t}))},u=(e,t)=>{if(typeof window>`u`)return null;try{let n=window.localStorage.getItem(e);return n===null?null:t.some(e=>e.name===n)?n:null}catch(e){return console.error(`[theme] Failed to read from localStorage:`,e),null}},d=(e,t)=>{if(!(typeof window>`u`))try{window.localStorage.setItem(e,t)}catch(e){console.error(`[theme] Failed to write to localStorage:`,e)}},f=e=>{if(!(typeof window>`u`))try{window.localStorage.removeItem(e)}catch(e){console.error(`[theme] Failed to remove from localStorage:`,e)}},p=e=>{let{defaultTheme:t,systemTheme:n,themes:r}=e,i=e=>{let t=r.find(t=>t.name===e);if(t===void 0)throw Error(`Theme is not configured: ${e}.`);return t};if(typeof window>`u`||typeof window.matchMedia!=`function`)return i(t);try{return i(window.matchMedia(`(prefers-color-scheme: dark)`).matches?n.dark:n.light)}catch{return i(t)}},m=e=>{if(typeof window>`u`||typeof window.matchMedia!=`function`)return()=>{};let t=(()=>{try{return window.matchMedia(`(prefers-color-scheme: dark)`)}catch{return null}})(),n=!1;if(t)try{typeof t.addEventListener==`function`?t.addEventListener(`change`,e):typeof t.addListener==`function`&&(n=!0,t.addListener(e))}catch{}return()=>{if(t!==null)try{n&&typeof t.removeListener==`function`?t.removeListener(e):typeof t.removeEventListener==`function`&&t.removeEventListener(`change`,e)}catch{}}},h=e=>typeof window>`u`||typeof window.addEventListener!=`function`?()=>{}:(window.addEventListener(`storage`,e),()=>{window.removeEventListener(`storage`,e)}),g=e=>{if(typeof e.attribute!=`string`||e.attribute.trim().length===0)throw Error(`Theme attribute option must be a non-empty string.`);if(/[\s"'/>=]/.test(e.attribute))throw Error(`Theme attribute option must be a valid HTML attribute name.`);if(typeof e.storageKey!=`string`||e.storageKey.trim().length===0)throw Error(`Theme storageKey option must be a non-empty string.`);if(!Array.isArray(e.themes))throw Error(`Theme options.themes must be an array.`);let t=new Set;if(e.tokenSchema){for(let[t,n]of Object.entries(e.tokenSchema))if(typeof n!=`string`||!n.startsWith(`--`))throw Error(`Token schema key "${t}" must map to a CSS custom property starting with "--". Received: "${n}".`)}for(let n of e.themes){if(typeof n!=`object`||!n)throw Error(`Theme definitions must be objects.`);if(typeof n.name!=`string`||n.name.trim().length===0)throw Error(`Theme names must be non-empty strings.`);if(n.colorScheme!==`light`&&n.colorScheme!==`dark`)throw Error(`Theme "${n.name}" has an invalid colorScheme: ${n.colorScheme}. Must be "light" or "dark".`);if(t.has(n.name))throw Error(`Duplicate theme name: ${n.name}.`);if(t.add(n.name),typeof e.shouldApplyClass!=`function`&&a(n,e.shouldApplyClass),n.tokens){if(!e.tokenSchema)throw Error(`Theme "${n.name}" defines tokens but no tokenSchema is configured.`);for(let t of Object.keys(n.tokens))if(!Object.hasOwn(e.tokenSchema,t))throw Error(`Theme "${n.name}" defines token "${t}" which is not present in tokenSchema.`)}}if(!t.has(e.defaultTheme))throw Error(`Default theme is not configured: ${e.defaultTheme}.`);if(!t.has(e.systemTheme.light))throw Error(`System light theme is not configured: ${e.systemTheme.light}.`);if(!t.has(e.systemTheme.dark))throw Error(`System dark theme is not configured: ${e.systemTheme.dark}.`)},_=(e,t)=>{if(e!=null){if(typeof e!=`object`||Array.isArray(e))throw Error(`Runtime tokens must be an object.`);if(t===void 0)throw Error(`Runtime tokens provided but no tokenSchema is configured.`);for(let n of Object.keys(e))if(!Object.hasOwn(t,n))throw Error(`Runtime token override "${n}" is not present in tokenSchema.`)}};var v=class{#e;#t;#n={};#r=new Set;#i=null;#a=null;#o=!1;#s=null;#c=null;#l(){if(this.#s===null){this.#s=new Map;for(let e of this.#e.themes)this.#s.set(e.name,a(e,this.#e.shouldApplyClass))}return this.#s}#u(){if(this.#c===null){let e=this.#l();this.#c=Array.from(e.values()).filter(e=>e!==null)}return this.#c}#d=e=>{if(u(this.#e.storageKey,this.#e.themes)!==null)return;let t=e.matches?this.#e.systemTheme.dark:this.#e.systemTheme.light;this.#t!==t&&this.#p(t,{source:`system`,shouldStore:!1})};#f=e=>{if(e.key===this.#e.storageKey)if(e.newValue===null){let e=this.getSystem().name;this.#p(e,{source:`clearPreference`,shouldStore:!1})}else this.#e.themes.some(t=>t.name===e.newValue)&&this.#t!==e.newValue&&this.#p(e.newValue,{source:`set`,shouldStore:!1})};constructor(e={}){this.#e={...i,...e,systemTheme:{...i.systemTheme,...e.systemTheme}},g(this.#e),this.#t=this.#e.defaultTheme}init(e){return this.#o?this:(this.#i=m(this.#d),this.#a=h(this.#f),this.#p(u(this.#e.storageKey,this.#e.themes)??this.getSystem().name,{source:`init`,shouldStore:!1,tokens:e}),this.#o=!0,this)}get(){let e=this.#h(this.#t),t=this.#e,n=this.#n,r=null;return{...e,get tokens(){return r===null&&(r={...c({theme:e,options:t,activeTokens:n}),...e.tokens,...n}),r}}}set(e,t){return this.#p(e,{source:`set`,shouldStore:!0,tokens:t})}toggle(e){let t=this.#h(this.#t).colorScheme===`dark`?this.#e.systemTheme.light:this.#e.systemTheme.dark;return this.#p(t,{source:`toggle`,shouldStore:!0,tokens:e})}clearPreference(){return f(this.#e.storageKey),this.#p(this.getSystem().name,{source:`clearPreference`,shouldStore:!1})}getStored(){return u(this.#e.storageKey,this.#e.themes)}getSystem(){return p({defaultTheme:this.#e.defaultTheme,systemTheme:this.#e.systemTheme,themes:this.#e.themes})}subscribe(e){return this.#r.add(e),()=>{this.#r.delete(e)}}destroy(){this.#i&&=(this.#i(),null),this.#a&&=(this.#a(),null),this.#r.clear(),this.#n={},this.#t=this.#e.defaultTheme,this.#s=null,this.#c=null,this.#o=!1}#p(e,t){_(t.tokens,this.#e.tokenSchema);let n=this.#h(e);this.#t=n.name,t.tokens!==void 0&&(this.#n=t.tokens),t.shouldStore&&d(this.#e.storageKey,n.name);let r=this.#l();s({theme:n,options:this.#e,activeTokens:this.#n,resolvedClasses:this.#u(),nextClass:r.get(n.name)??null});let i=this.get();return this.#m({name:i.name,theme:i,source:t.source}),i}#m(e){for(let t of this.#r)try{t(e)}catch(e){console.error(`Error in theme change listener:`,e)}l(e)}#h(e){let t=this.#e.themes.find(t=>t.name===e);if(t===void 0)throw Error(`Theme is not configured: ${e}.`);return t}};function y(e={}){return new v(e)}export{r as DARK_THEME,n as LIGHT_THEME,e as THEME_CHANGE_EVENT,y as createTheme};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codenhub/theme",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Zero-dependency browser theme preference helper for TypeScript apps.",
|
|
6
6
|
"homepage": "https://github.com/codenhub/codenhub/tree/main/packages/theme",
|
|
@@ -27,17 +27,25 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
+
"@playwright/test": "^1.61.1",
|
|
30
31
|
"jsdom": "^29.1.1",
|
|
32
|
+
"tsdown": "^0.22.3",
|
|
31
33
|
"typescript": "^6.0.3",
|
|
32
|
-
"vitest": "^4.
|
|
34
|
+
"vitest": "^4.1.10",
|
|
35
|
+
"@codenhub/styles": "0.0.4",
|
|
36
|
+
"@codenhub/vite-plugin-icons": "0.0.1"
|
|
33
37
|
},
|
|
34
38
|
"scripts": {
|
|
35
|
-
"
|
|
39
|
+
"dev": "pnpm --filter=@codenhub/theme-dev dev",
|
|
40
|
+
"debug": "pnpm build && pnpm --filter=@codenhub/theme-debug dev",
|
|
41
|
+
"build": "tsdown src/index.ts --format esm --dts --clean --no-fixed-extension --minify",
|
|
36
42
|
"status:npm": "npm view @codenhub/theme version dist-tags time --json && npm dist-tag ls @codenhub/theme && npm access get status @codenhub/theme",
|
|
37
43
|
"status:pack": "npm pack --dry-run",
|
|
38
|
-
"test": "vitest run",
|
|
44
|
+
"test": "vitest run && pnpm build && pnpm test:visual",
|
|
39
45
|
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"test:visual": "playwright test",
|
|
40
47
|
"test:watch": "vitest",
|
|
48
|
+
"test:visual:watch": "playwright test --ui",
|
|
41
49
|
"typecheck": "tsc --noEmit"
|
|
42
50
|
}
|
|
43
51
|
}
|