ssr-themes 0.0.1-alpha.1
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.md +201 -0
- package/README.md +607 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/dist/server-CcYvjBJ6.d.mts +87 -0
- package/dist/server-CcYvjBJ6.d.ts +87 -0
- package/dist/server.d.mts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +1 -0
- package/dist/server.mjs +1 -0
- package/package.json +45 -0
package/LICENSE.md
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 [yyyy] [name of copyright owner]
|
|
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
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# ssr-themes  [](https://www.npmjs.com/package/ssr-themes)
|
|
2
|
+
|
|
3
|
+
Themes for your React SSR app.
|
|
4
|
+
|
|
5
|
+
- ✅ Perfect dark mode with no flashing
|
|
6
|
+
- ✅ System setting with `prefers-color-scheme`
|
|
7
|
+
- ✅ Themed browser UI with color-scheme
|
|
8
|
+
- ✅ SSR support for all frameworks (including TanStack Start/Next)
|
|
9
|
+
- ✅ Easy Tailwind integration
|
|
10
|
+
- ✅ Sync theme across tabs
|
|
11
|
+
- ✅ `useTheme` hook
|
|
12
|
+
|
|
13
|
+
Check out the live examples:
|
|
14
|
+
|
|
15
|
+
- Tanstack Start: []
|
|
16
|
+
- Next: []
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install ssr-themes
|
|
22
|
+
# or
|
|
23
|
+
bun add ssr-themes
|
|
24
|
+
# or
|
|
25
|
+
pnpm install ssr-themes
|
|
26
|
+
# or
|
|
27
|
+
yarn add ssr-themes
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Use
|
|
31
|
+
|
|
32
|
+
### With TanStack Start
|
|
33
|
+
|
|
34
|
+
Add `ThemeProvider` to your root route and render `HeadContent`/`Scripts` in the document:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// src/routes/__root.tsx
|
|
38
|
+
import {
|
|
39
|
+
HeadContent,
|
|
40
|
+
Outlet,
|
|
41
|
+
Scripts,
|
|
42
|
+
createRootRoute,
|
|
43
|
+
} from '@tanstack/react-router';
|
|
44
|
+
import type {ReactNode} from 'react';
|
|
45
|
+
import {ThemeProvider} from 'ssr-themes';
|
|
46
|
+
import appCss from '../styles.css?url';
|
|
47
|
+
|
|
48
|
+
function RootDocument({children}: {children: ReactNode}) {
|
|
49
|
+
return (
|
|
50
|
+
<html lang="en" suppressHydrationWarning>
|
|
51
|
+
<head>
|
|
52
|
+
<HeadContent />
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<ThemeProvider attribute="class">{children}</ThemeProvider>
|
|
56
|
+
<Scripts />
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Route = createRootRoute({
|
|
63
|
+
head: () => ({
|
|
64
|
+
links: [{rel: 'stylesheet', href: appCss}],
|
|
65
|
+
}),
|
|
66
|
+
component: function RootComponent() {
|
|
67
|
+
return (
|
|
68
|
+
<RootDocument>
|
|
69
|
+
<Outlet />
|
|
70
|
+
</RootDocument>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> **Note!** If you do not add [suppressHydrationWarning](https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning:~:text=It%20only%20works%20one%20level%20deep) to your `<html>` you will get warnings because `ssr-themes` updates that element. This property only applies one level deep, so it won't block hydration warnings on other elements.
|
|
77
|
+
|
|
78
|
+
### SSR / RSC
|
|
79
|
+
|
|
80
|
+
`ssr-themes` uses cookies to set the theme, so the server can use `registerTheme` to read cookies and apply the right value directly to `<html>`. The inline script will reuse that value and skip cookie reads when the theme attribute/class is already present on the `<html>` element.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import {registerTheme, ThemeProvider} from 'ssr-themes';
|
|
84
|
+
import {cookies} from 'next/headers';
|
|
85
|
+
|
|
86
|
+
const cookieStore = await cookies();
|
|
87
|
+
const theme = cookieStore.get('theme'); // 'dark' | 'light' | undefined
|
|
88
|
+
const htmlProps = registerTheme({
|
|
89
|
+
theme,
|
|
90
|
+
attribute: 'class',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<html suppressHydrationWarning {...htmlProps}>
|
|
95
|
+
<body>
|
|
96
|
+
<ThemeProvider attribute="class">{children}</ThemeProvider>
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### HTML & CSS
|
|
103
|
+
|
|
104
|
+
That's it, your app fully supports dark mode, including System preference with `prefers-color-scheme`. The theme is also immediately synced between tabs. By default, ssr-themes modifies the `data-theme` attribute on the `html` element, which you can easily use to style your app:
|
|
105
|
+
|
|
106
|
+
```css
|
|
107
|
+
:root {
|
|
108
|
+
/* Your default theme */
|
|
109
|
+
--background: white;
|
|
110
|
+
--foreground: black;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[data-theme='dark'] {
|
|
114
|
+
--background: black;
|
|
115
|
+
--foreground: white;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **Note!** If you set the attribute of your Theme Provider to class for Tailwind ssr-themes will modify the `class` attribute on the `html` element. See [With TailwindCSS](#with-tailwindcss).
|
|
120
|
+
|
|
121
|
+
### useTheme
|
|
122
|
+
|
|
123
|
+
Your UI will need to know the current theme and be able to change it. The `useTheme` hook provides theme information:
|
|
124
|
+
|
|
125
|
+
```jsx
|
|
126
|
+
import {useTheme} from 'ssr-themes';
|
|
127
|
+
|
|
128
|
+
const ThemeChanger = () => {
|
|
129
|
+
const {theme, setTheme} = useTheme();
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
The current theme is: {theme}
|
|
134
|
+
<button onClick={() => setTheme('light')}>Light Mode</button>
|
|
135
|
+
<button onClick={() => setTheme('dark')}>Dark Mode</button>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
> **Warning!** The above code is hydration _unsafe_ and will throw a hydration mismatch warning when rendering with SSG or SSR. This is because we cannot know the `theme` on the server, so it will always be `undefined` until mounted on the client.
|
|
142
|
+
>
|
|
143
|
+
> You should delay rendering any theme toggling UI until mounted on the client. See the [example](#avoid-hydration-mismatch).
|
|
144
|
+
|
|
145
|
+
## API
|
|
146
|
+
|
|
147
|
+
Let's dig into the details.
|
|
148
|
+
|
|
149
|
+
### ThemeProvider
|
|
150
|
+
|
|
151
|
+
All your theme configuration is passed to ThemeProvider.
|
|
152
|
+
|
|
153
|
+
- `cookie = { name: 'theme', path: '/', maxAge: 31536000, sameSite: 'lax' }`: Cookie configuration for the theme cookie
|
|
154
|
+
- `defaultTheme = 'system'`: Default theme name (for v0.0.12 and lower the default was `light`). If `enableSystem` is false, the default theme is `light`
|
|
155
|
+
- `forcedTheme`: Forced theme name for the current page (does not modify saved theme settings)
|
|
156
|
+
- `enableSystem = true`: Whether to switch between `dark` and `light` based on `prefers-color-scheme`
|
|
157
|
+
- `enableColorScheme = true`: Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons
|
|
158
|
+
- `disableTransitionOnChange = false`: Optionally disable all CSS transitions when switching themes ([example](#disable-transitions-on-theme-change))
|
|
159
|
+
- `themes = ['light', 'dark']`: List of theme names
|
|
160
|
+
- `attribute = 'data-theme'`: HTML attribute modified based on the active theme
|
|
161
|
+
- accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) ([example](#class-instead-of-data-attribute))
|
|
162
|
+
- `value`: Optional mapping of theme name to attribute value
|
|
163
|
+
- value is an `object` where key is the theme name and value is the attribute value ([example](#differing-dom-attribute-and-theme-name))
|
|
164
|
+
- `nonce`: Optional nonce passed to the injected `script` tag, used to allow-list the ssr-themes script in your CSP
|
|
165
|
+
- `scriptProps`: Optional props to pass to the injected `script` tag ([example](#using-with-cloudflare-rocket-loader))
|
|
166
|
+
|
|
167
|
+
### registerTheme
|
|
168
|
+
|
|
169
|
+
Use `registerTheme` to generate props for your `<html>` element on the server. When a theme is provided, it applies the theme attribute/class and sets `color-scheme` (if enabled), so the client script treats the existing HTML value as authoritative.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
const htmlProps = registerTheme({
|
|
173
|
+
theme: 'dark',
|
|
174
|
+
attribute: 'class',
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If `theme` is `undefined` or `'system'`, it returns an empty object.
|
|
179
|
+
|
|
180
|
+
### useTheme
|
|
181
|
+
|
|
182
|
+
useTheme takes no parameters, but returns:
|
|
183
|
+
|
|
184
|
+
- `theme`: Active theme name
|
|
185
|
+
- `setTheme(name)`: Function to update the theme. The API is identical to the [set function](https://react.dev/reference/react/useState#setstate) returned by `useState`-hook. Pass the new theme value or use a callback to set the new theme based on the current theme.
|
|
186
|
+
- `forcedTheme`: Forced page theme or falsy. If `forcedTheme` is set, you should disable any theme switching UI
|
|
187
|
+
- `resolvedTheme`: If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme`
|
|
188
|
+
- `systemTheme`: If `enableSystem` is true, represents the System theme preference ("dark" or "light"), regardless what the active theme is
|
|
189
|
+
- `themes`: The list of themes passed to `ThemeProvider` (with "system" appended, if `enableSystem` is true)
|
|
190
|
+
|
|
191
|
+
Not too bad, right? Let's see how to use these properties with examples:
|
|
192
|
+
|
|
193
|
+
## Examples
|
|
194
|
+
|
|
195
|
+
The [Live Example](https://ssr-themes-example.vercel.app/) shows ssr-themes in action, with dark, light, system themes and pages with forced themes.
|
|
196
|
+
|
|
197
|
+
### Use System preference by default
|
|
198
|
+
|
|
199
|
+
For versions above v0.0.12, the `defaultTheme` is automatically set to "system", so to use System preference you can simply use:
|
|
200
|
+
|
|
201
|
+
```jsx
|
|
202
|
+
<ThemeProvider>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Ignore System preference
|
|
206
|
+
|
|
207
|
+
If you don't want a System theme, disable it via `enableSystem`:
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
<ThemeProvider enableSystem={false}>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Class instead of data attribute
|
|
214
|
+
|
|
215
|
+
If your app uses a class to style the page based on the theme, change the attribute prop to `class`:
|
|
216
|
+
|
|
217
|
+
```jsx
|
|
218
|
+
<ThemeProvider attribute="class">
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Now, setting the theme to "dark" will set `class="dark"` on the `html` element.
|
|
222
|
+
|
|
223
|
+
### Force page to a theme
|
|
224
|
+
|
|
225
|
+
Let's say your cool new marketing page is dark mode only. The page should always use the dark theme, and changing the theme should have no effect. In TanStack Start, add static data to the route and read it in the root route:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// src/routes/dark.tsx
|
|
229
|
+
import {createFileRoute} from '@tanstack/react-router';
|
|
230
|
+
|
|
231
|
+
export const Route = createFileRoute('/dark')({
|
|
232
|
+
staticData: {theme: 'dark'},
|
|
233
|
+
component: DarkPage,
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// src/routes/__root.tsx
|
|
239
|
+
import { useMatches } from '@tanstack/react-router'
|
|
240
|
+
|
|
241
|
+
const matches = useMatches()
|
|
242
|
+
const forcedTheme = matches.reduce<string | undefined>((theme, match) => {
|
|
243
|
+
const staticData = match.staticData as { theme?: string } | undefined
|
|
244
|
+
return staticData?.theme ?? theme
|
|
245
|
+
}, undefined)
|
|
246
|
+
|
|
247
|
+
<ThemeProvider forcedTheme={forcedTheme} attribute="class">
|
|
248
|
+
{children}
|
|
249
|
+
</ThemeProvider>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Done! Your page is always dark theme (regardless of user preference), and calling `setTheme` from `useTheme` is now a no-op. However, you should make sure to disable any of your UI that would normally change the theme:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const {forcedTheme} = useTheme();
|
|
256
|
+
|
|
257
|
+
// Theme is forced, we shouldn't allow user to change the theme
|
|
258
|
+
const disabled = !!forcedTheme;
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Disable transitions on theme change
|
|
262
|
+
|
|
263
|
+
I wrote about [this technique here](https://paco.sh/blog/disable-theme-transitions). We can forcefully disable all CSS transitions before the theme is changed, and re-enable them immediately afterwards. This ensures your UI with different transition durations won't feel inconsistent when changing the theme.
|
|
264
|
+
|
|
265
|
+
To enable this behavior, pass the `disableTransitionOnChange` prop:
|
|
266
|
+
|
|
267
|
+
```jsx
|
|
268
|
+
<ThemeProvider disableTransitionOnChange>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Differing DOM attribute and theme name
|
|
272
|
+
|
|
273
|
+
The name of the active theme is used as both the cookie value and the value of the DOM attribute. If the theme name is "pink", the cookie will contain `theme=pink` and the DOM will be `data-theme="pink"`. You **cannot** modify the cookie value, but you **can** modify the DOM value.
|
|
274
|
+
|
|
275
|
+
If we want the DOM to instead render `data-theme="my-pink-theme"` when the theme is "pink", pass the `value` prop:
|
|
276
|
+
|
|
277
|
+
```jsx
|
|
278
|
+
<ThemeProvider value={{ pink: 'my-pink-theme' }}>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Done! To be extra clear, this affects only the DOM. Here's how all the values will look:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
const {theme} = useTheme();
|
|
285
|
+
// => "pink"
|
|
286
|
+
|
|
287
|
+
document.cookie;
|
|
288
|
+
// => "theme=pink"
|
|
289
|
+
|
|
290
|
+
document.documentElement.getAttribute('data-theme');
|
|
291
|
+
// => "my-pink-theme"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Using with Cloudflare Rocket Loader
|
|
295
|
+
|
|
296
|
+
[Rocket Loader](https://developers.cloudflare.com/fundamentals/speed/rocket-loader/) is a Cloudflare optimization that defers the loading of inline and external scripts to prioritize the website content. Since ssr-themes relies on a script injection to avoid screen flashing on page load, Rocket Loader breaks this functionality. Individual scripts [can be ignored](https://developers.cloudflare.com/fundamentals/speed/rocket-loader/ignore-javascripts/) by adding the `data-cfasync="false"` attribute to the script tag:
|
|
297
|
+
|
|
298
|
+
```jsx
|
|
299
|
+
<ThemeProvider scriptProps={{ 'data-cfasync': 'false' }}>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### More than light and dark mode
|
|
303
|
+
|
|
304
|
+
ssr-themes is designed to support any number of themes! Simply pass a list of themes:
|
|
305
|
+
|
|
306
|
+
```jsx
|
|
307
|
+
<ThemeProvider themes={['pink', 'red', 'blue']}>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
> **Note!** When you pass `themes`, the default set of themes ("light" and "dark") are overridden. Make sure you include those if you still want your light and dark themes:
|
|
311
|
+
|
|
312
|
+
```jsx
|
|
313
|
+
<ThemeProvider themes={['pink', 'red', 'blue', 'light', 'dark']}>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
For a starting point, check out `examples/example`.
|
|
317
|
+
|
|
318
|
+
### Without CSS variables
|
|
319
|
+
|
|
320
|
+
This library does not rely on your theme styling using CSS variables. You can hard-code the values in your CSS, and everything will work as expected (without any flashing):
|
|
321
|
+
|
|
322
|
+
```css
|
|
323
|
+
html,
|
|
324
|
+
body {
|
|
325
|
+
color: #000;
|
|
326
|
+
background: #fff;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
[data-theme='dark'],
|
|
330
|
+
[data-theme='dark'] body {
|
|
331
|
+
color: #fff;
|
|
332
|
+
background: #000;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### With Styled Components and any CSS-in-JS
|
|
337
|
+
|
|
338
|
+
SSR Themes is completely CSS independent, it will work with any library. For example, with Styled Components you can wire `createGlobalStyle` in your root route:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
// src/routes/__root.tsx
|
|
342
|
+
import {Outlet, createRootRoute} from '@tanstack/react-router';
|
|
343
|
+
import {createGlobalStyle} from 'styled-components';
|
|
344
|
+
import {ThemeProvider} from 'ssr-themes';
|
|
345
|
+
|
|
346
|
+
// Your themeing variables
|
|
347
|
+
const GlobalStyle = createGlobalStyle`
|
|
348
|
+
:root {
|
|
349
|
+
--fg: #000;
|
|
350
|
+
--bg: #fff;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
[data-theme="dark"] {
|
|
354
|
+
--fg: #fff;
|
|
355
|
+
--bg: #000;
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
|
|
359
|
+
function RootComponent() {
|
|
360
|
+
return (
|
|
361
|
+
<>
|
|
362
|
+
<GlobalStyle />
|
|
363
|
+
<ThemeProvider>
|
|
364
|
+
<Outlet />
|
|
365
|
+
</ThemeProvider>
|
|
366
|
+
</>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export const Route = createRootRoute({
|
|
371
|
+
component: RootComponent,
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Avoid Hydration Mismatch
|
|
376
|
+
|
|
377
|
+
Because we cannot know the `theme` on the server, many of the values returned from `useTheme` will be `undefined` until mounted on the client. This means if you try to render UI based on the current theme before mounting on the client, you will see a hydration mismatch error.
|
|
378
|
+
|
|
379
|
+
The following code sample is **unsafe**:
|
|
380
|
+
|
|
381
|
+
```jsx
|
|
382
|
+
import {useTheme} from 'ssr-themes';
|
|
383
|
+
|
|
384
|
+
// Do NOT use this! It will throw a hydration mismatch error.
|
|
385
|
+
const ThemeSwitch = () => {
|
|
386
|
+
const {theme, setTheme} = useTheme();
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
390
|
+
<option value="system">System</option>
|
|
391
|
+
<option value="dark">Dark</option>
|
|
392
|
+
<option value="light">Light</option>
|
|
393
|
+
</select>
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export default ThemeSwitch;
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
To fix this, make sure you only render UI that uses the current theme when the page is mounted on the client:
|
|
401
|
+
|
|
402
|
+
```jsx
|
|
403
|
+
import {useState, useEffect} from 'react';
|
|
404
|
+
import {useTheme} from 'ssr-themes';
|
|
405
|
+
|
|
406
|
+
const ThemeSwitch = () => {
|
|
407
|
+
const [mounted, setMounted] = useState(false);
|
|
408
|
+
const {theme, setTheme} = useTheme();
|
|
409
|
+
|
|
410
|
+
// useEffect only runs on the client, so now we can safely show the UI
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
setMounted(true);
|
|
413
|
+
}, []);
|
|
414
|
+
|
|
415
|
+
if (!mounted) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
421
|
+
<option value="system">System</option>
|
|
422
|
+
<option value="dark">Dark</option>
|
|
423
|
+
<option value="light">Light</option>
|
|
424
|
+
</select>
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export default ThemeSwitch;
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Alternatively, you could lazy load the component on the client side with `React.lazy`:
|
|
432
|
+
|
|
433
|
+
```jsx
|
|
434
|
+
import {Suspense, lazy} from 'react';
|
|
435
|
+
|
|
436
|
+
const ThemeSwitch = lazy(() => import('./ThemeSwitch'));
|
|
437
|
+
|
|
438
|
+
const ThemePage = () => {
|
|
439
|
+
return (
|
|
440
|
+
<Suspense fallback={null}>
|
|
441
|
+
<ThemeSwitch />
|
|
442
|
+
</Suspense>
|
|
443
|
+
);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
export default ThemePage;
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
To avoid [Layout Shift](https://web.dev/cls/), consider rendering a skeleton/placeholder until mounted on the client side.
|
|
450
|
+
|
|
451
|
+
#### Images
|
|
452
|
+
|
|
453
|
+
Showing different images based on the current theme also suffers from the hydration mismatch problem. You can use a placeholder image until the theme is resolved:
|
|
454
|
+
|
|
455
|
+
```jsx
|
|
456
|
+
import {useTheme} from 'ssr-themes';
|
|
457
|
+
|
|
458
|
+
function ThemedImage() {
|
|
459
|
+
const {resolvedTheme} = useTheme();
|
|
460
|
+
let src;
|
|
461
|
+
|
|
462
|
+
switch (resolvedTheme) {
|
|
463
|
+
case 'light':
|
|
464
|
+
src = '/light.png';
|
|
465
|
+
break;
|
|
466
|
+
case 'dark':
|
|
467
|
+
src = '/dark.png';
|
|
468
|
+
break;
|
|
469
|
+
default:
|
|
470
|
+
src =
|
|
471
|
+
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return <img src={src} width={400} height={400} alt="" />;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export default ThemedImage;
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### CSS
|
|
482
|
+
|
|
483
|
+
You can also use CSS to hide or show content based on the current theme. To avoid the hydration mismatch, you'll need to render _both_ versions of the UI, with CSS hiding the unused version. For example:
|
|
484
|
+
|
|
485
|
+
```jsx
|
|
486
|
+
function ThemedImage() {
|
|
487
|
+
return (
|
|
488
|
+
<>
|
|
489
|
+
{/* When the theme is dark, hide this div */}
|
|
490
|
+
<div data-hide-on-theme="dark">
|
|
491
|
+
<img src="light.png" width={400} height={400} alt="" />
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
{/* When the theme is light, hide this div */}
|
|
495
|
+
<div data-hide-on-theme="light">
|
|
496
|
+
<img src="dark.png" width={400} height={400} alt="" />
|
|
497
|
+
</div>
|
|
498
|
+
</>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export default ThemedImage;
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```css
|
|
506
|
+
[data-theme='dark'] [data-hide-on-theme='dark'],
|
|
507
|
+
[data-theme='light'] [data-hide-on-theme='light'] {
|
|
508
|
+
display: none;
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### With TailwindCSS
|
|
513
|
+
|
|
514
|
+
The example in `examples/example` uses Tailwind v4 with a class-based dark mode.
|
|
515
|
+
|
|
516
|
+
Add the dark variant in your CSS:
|
|
517
|
+
|
|
518
|
+
```css
|
|
519
|
+
@import 'tailwindcss';
|
|
520
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Set the attribute for your Theme Provider to `class`:
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
<ThemeProvider attribute="class">
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
When the theme is dark, ssr-themes applies the `dark` class to the `html` element:
|
|
530
|
+
|
|
531
|
+
```html
|
|
532
|
+
<html class="dark">
|
|
533
|
+
<body>
|
|
534
|
+
<div class="bg-white dark:bg-black">
|
|
535
|
+
<!-- ... -->
|
|
536
|
+
</div>
|
|
537
|
+
</body>
|
|
538
|
+
</html>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
That's it! Now you can use dark-mode specific classes:
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
<h1 className="text-black dark:text-white">
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Discussion
|
|
548
|
+
|
|
549
|
+
### The Flash
|
|
550
|
+
|
|
551
|
+
ThemeProvider automatically injects a script to update the `html` element with the correct attributes before the rest of your page loads. This means the page will not flash under any circumstances, including forced themes, system theme, multiple themes, and incognito. No `noflash.js` required.
|
|
552
|
+
|
|
553
|
+
## FAQ
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
**Why is my page still flashing?**
|
|
558
|
+
|
|
559
|
+
In dev mode, the page may still flash. When you build your app in production mode, there will be no flashing.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
**Why do I get server/client mismatch error?**
|
|
564
|
+
|
|
565
|
+
When using `useTheme`, you will use see a hydration mismatch error when rendering UI that relies on the current theme. This is because many of the values returned by `useTheme` are undefined on the server, since we can't read cookies until mounting on the client. See the [example](#avoid-hydration-mismatch) for how to fix this error.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
**Do I need to use CSS variables with this library?**
|
|
570
|
+
|
|
571
|
+
Nope. See the [example](#without-css-variables).
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
**Can I set the class or data attribute on the body or another element?**
|
|
576
|
+
|
|
577
|
+
Nope. If you have a good reason for supporting this feature, please open an issue.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
**Can I use this package with Gatsby or CRA?**
|
|
582
|
+
|
|
583
|
+
Yes, starting from the 0.3.0 version.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
**Is the injected script minified?**
|
|
588
|
+
|
|
589
|
+
Yes.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
**Why is `resolvedTheme` necessary?**
|
|
594
|
+
|
|
595
|
+
When supporting the System theme preference, you want to make sure that's reflected in your UI. This means your buttons, selects, dropdowns, or whatever you use to indicate the current theme should say "System" when the System theme preference is active.
|
|
596
|
+
|
|
597
|
+
If we didn't distinguish between `theme` and `resolvedTheme`, the UI would show "Dark" or "Light", when it should really be "System".
|
|
598
|
+
|
|
599
|
+
`resolvedTheme` is then useful for modifying behavior or styles at runtime:
|
|
600
|
+
|
|
601
|
+
```jsx
|
|
602
|
+
const { resolvedTheme } = useTheme()
|
|
603
|
+
|
|
604
|
+
<div style={{ color: resolvedTheme === 'dark' ? 'white' : 'black' }}>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
If we didn't have `resolvedTheme` and only used `theme`, you'd lose information about the state of your UI (you would only know the theme is "system", and not what it resolved to).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { S as SystemThemeDefinition, T as ThemeProviderProps, U as UseThemeProps } from './server-CcYvjBJ6.mjs';
|
|
3
|
+
export { A as Attribute, C as CookieConfig, a as CookieOptions, R as RegisterThemeOptions, b as SystemTheme, c as ThemeHtmlProps, r as registerTheme } from './server-CcYvjBJ6.mjs';
|
|
4
|
+
|
|
5
|
+
declare const useTheme: () => UseThemeProps<SystemThemeDefinition>;
|
|
6
|
+
declare const ThemeProvider: <TThemes extends readonly string[] = SystemThemeDefinition>(props: ThemeProviderProps<TThemes>) => React.JSX.Element;
|
|
7
|
+
declare const ThemeScript: React.MemoExoticComponent<(<TThemes extends readonly string[]>({ forcedTheme, cookieName, attribute, enableSystem, enableColorScheme, defaultTheme, value, themes, nonce, scriptProps, }: Omit<ThemeProviderProps<TThemes>, "children" | "cookie"> & {
|
|
8
|
+
cookieName: string;
|
|
9
|
+
defaultTheme: string;
|
|
10
|
+
}) => React.JSX.Element)>;
|
|
11
|
+
|
|
12
|
+
export { ThemeProvider, ThemeProviderProps, ThemeScript, UseThemeProps, useTheme };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { S as SystemThemeDefinition, T as ThemeProviderProps, U as UseThemeProps } from './server-CcYvjBJ6.js';
|
|
3
|
+
export { A as Attribute, C as CookieConfig, a as CookieOptions, R as RegisterThemeOptions, b as SystemTheme, c as ThemeHtmlProps, r as registerTheme } from './server-CcYvjBJ6.js';
|
|
4
|
+
|
|
5
|
+
declare const useTheme: () => UseThemeProps<SystemThemeDefinition>;
|
|
6
|
+
declare const ThemeProvider: <TThemes extends readonly string[] = SystemThemeDefinition>(props: ThemeProviderProps<TThemes>) => React.JSX.Element;
|
|
7
|
+
declare const ThemeScript: React.MemoExoticComponent<(<TThemes extends readonly string[]>({ forcedTheme, cookieName, attribute, enableSystem, enableColorScheme, defaultTheme, value, themes, nonce, scriptProps, }: Omit<ThemeProviderProps<TThemes>, "children" | "cookie"> & {
|
|
8
|
+
cookieName: string;
|
|
9
|
+
defaultTheme: string;
|
|
10
|
+
}) => React.JSX.Element)>;
|
|
11
|
+
|
|
12
|
+
export { ThemeProvider, ThemeProviderProps, ThemeScript, UseThemeProps, useTheme };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";var F=Object.create;var w=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var J=Object.getPrototypeOf,q=Object.prototype.hasOwnProperty;var G=(e,t)=>{for(var n in t)w(e,n,{get:t[n],enumerable:!0})},D=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of z(t))!q.call(e,i)&&i!==n&&w(e,i,{get:()=>t[i],enumerable:!(r=W(t,i))||r.enumerable});return e};var K=(e,t,n)=>(n=e!=null?F(J(e)):{},D(t||!e||!e.__esModule?w(n,"default",{value:e,enumerable:!0}):n,e)),X=e=>D(w({},"__esModule",{value:!0}),e);var ue={};G(ue,{ThemeProvider:()=>oe,ThemeScript:()=>j,registerTheme:()=>U,useTheme:()=>ne});module.exports=X(ue);var o=K(require("react"));var $=(e,t,n,r,i,s,a,u)=>{let d=document.documentElement,C=Array.isArray(e)?e:[e],g=[];for(let m of i){let h=s?s[m]:m;h&&g.push({theme:m,value:h})}let N=g.map(m=>m.value),p=m=>{let h=document.cookie.match(new RegExp(`(?:^|; )${m}=([^;]*)`));return h?decodeURIComponent(h[1]):void 0},y=()=>{for(let m of C){if(m==="class"){for(let l of g)if(d.classList.contains(l.value))return l.theme;continue}let h=d.getAttribute(m);if(h){for(let l of g)if(l.value===h)return l.theme}}};function k(m){let h=s?s[m]:m;for(let l of C)l==="class"?(d.classList.remove(...N),h&&d.classList.add(h)):h?d.setAttribute(l,h):d.removeAttribute(l);u&&(m==="light"||m==="dark")&&(d.style.colorScheme=m)}let S=()=>window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";if(r){k(r);return}let R=m=>a&&m==="system"?S():m,v=y();if(v){k(R(v));return}try{let m=p(t)||n;k(R(m))}catch(m){}};var U=({theme:e,attribute:t="class",value:n,enableColorScheme:r=!0}={})=>{if(!e||e==="system")return{};let i=n?n[e]:e;if(!i)return{};let s={},a=Array.isArray(t)?t:[t];for(let u of a)u==="class"?s.className=i:s[u]=i;return r&&(e==="light"||e==="dark")&&(s.style={colorScheme:e}),s};var V=["light","dark"],I="(prefers-color-scheme: dark)",Q=typeof window=="undefined",O=o.createContext(void 0),Y={setTheme:e=>{},themes:[]},Z={path:"/",maxAge:31536e3,sameSite:"lax"},ee=e=>{var t;return(t=e==null?void 0:e.name)!=null?t:"theme"},te=e=>{if(Q)return;let t=document.cookie?document.cookie.split("; "):[];for(let n of t){let[r,...i]=n.split("=");if(r===e)return decodeURIComponent(i.join("="))}},se=e=>{if(e)return`${e.charAt(0).toUpperCase()}${e.slice(1)}`},H=(e,t,n)=>{try{let r={...Z,...n!=null?n:{}},i=encodeURIComponent(t),s=[`${e}=${i}`];r.path&&s.push(`Path=${r.path}`),r.domain&&s.push(`Domain=${r.domain}`),typeof r.maxAge=="number"&&s.push(`Max-Age=${r.maxAge}`),r.expires&&s.push(`Expires=${r.expires.toUTCString()}`);let a=se(r.sameSite);a&&s.push(`SameSite=${a}`),r.secure&&s.push("Secure"),document.cookie=s.join("; ")}catch(r){}},ne=()=>{var e;return(e=o.useContext(O))!=null?e:Y},oe=e=>o.useContext(O)?o.createElement(o.Fragment,null,e.children):o.createElement(ie,{...e}),re=["dark","light"],ie=({forcedTheme:e,disableTransitionOnChange:t=!1,enableSystem:n=!0,enableColorScheme:r=!0,cookie:i,themes:s=re,defaultTheme:a=n?"system":"light",attribute:u="class",value:d,children:C,nonce:g,scriptProps:N})=>{let p=ee(i),[y,k]=o.useState(()=>ae(p,a,u,d,s)),[S,R]=o.useState(()=>y==="system"?E():y),v=d?Object.values(d):s,m=o.useRef(null),h=o.useCallback(c=>{let f=c;if(!f)return;c==="system"&&n&&(f=E());let T=d?d[f]:f,M=t?ce(g):null,x=document.documentElement,L=A=>{A==="class"?(x.classList.remove(...v),T&&x.classList.add(T)):A.startsWith("data-")&&(T?x.setAttribute(A,T):x.removeAttribute(A))};if(Array.isArray(u)?u.forEach(L):L(u),r){let A=V.includes(a)?a:null,_=V.includes(f)?f:A;x.style.colorScheme=_}M==null||M()},[g]),l=o.useCallback(c=>{var f;(f=m.current)==null||f.postMessage({key:p,value:c})},[p]),b=o.useCallback(c=>{typeof c=="function"?k(f=>{let T=c(f);return H(p,T,i),l(T),T}):(k(c),H(p,c,i),l(c))},[p,i,l]),P=o.useCallback(c=>{let f=E(c);R(f),y==="system"&&n&&!e&&h("system")},[y,e]);o.useEffect(()=>{let c=window.matchMedia(I);return c.addListener(P),P(c),()=>c.removeListener(P)},[P]),o.useEffect(()=>{if(typeof window=="undefined"||typeof window.BroadcastChannel=="undefined")return;let c=new BroadcastChannel(`ssr-themes:${p}`);m.current=c;let f=T=>{if(!(!T.data||T.data.key!==p)){if(!T.data.value){k(a);return}k(T.data.value)}};return c.addEventListener("message",f),()=>{c.removeEventListener("message",f),c.close(),m.current===c&&(m.current=null)}},[p,a]),o.useEffect(()=>{h(e!=null?e:y)},[e,y]);let B=o.useMemo(()=>({theme:y,setTheme:b,forcedTheme:e,resolvedTheme:y==="system"?S:y,themes:n?[...s,"system"]:s,systemTheme:n?S:void 0}),[y,b,e,S,n,s]);return o.createElement(O.Provider,{value:B},o.createElement(j,{forcedTheme:e,cookieName:p,attribute:u,enableSystem:n,enableColorScheme:r,defaultTheme:a,value:d,themes:s,nonce:g,scriptProps:N}),C)},j=o.memo(({forcedTheme:e,cookieName:t,attribute:n,enableSystem:r,enableColorScheme:i,defaultTheme:s,value:a,themes:u,nonce:d,scriptProps:C})=>{let g=JSON.stringify([n,t,s,e,u,a,r,i]).slice(1,-1);return o.createElement("script",{...C,suppressHydrationWarning:!0,nonce:typeof window=="undefined"?d:"",dangerouslySetInnerHTML:{__html:`(${$.toString()})(${g})`}})});function me(e,t,n){let r=Array.isArray(e)?e:[e],i=[];for(let s of n){let a=t?t[s]:s;a&&i.push({theme:s,value:a})}for(let s of r){if(s==="class"){for(let u of i)if(document.documentElement.classList.contains(u.value))return u.theme;continue}let a=document.documentElement.getAttribute(s);if(a){for(let u of i)if(u.value===a)return u.theme}}}function ae(e,t,n,r,i){if(Q)return;let s=me(n,r,i);if(s)return s;let a;try{a=te(e)}catch(u){}return a||t}var ce=e=>{let t=document.createElement("style");return e&&t.setAttribute("nonce",e),t.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(t),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(t)},1)}},E=e=>(e||(e=window.matchMedia(I)),e.matches?"dark":"light");0&&(module.exports={ThemeProvider,ThemeScript,registerTheme,useTheme});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import*as s from"react";var L=(e,n,i,o,a,t,m,u)=>{let d=document.documentElement,C=Array.isArray(e)?e:[e],g=[];for(let r of a){let h=t?t[r]:r;h&&g.push({theme:r,value:h})}let w=g.map(r=>r.value),p=r=>{let h=document.cookie.match(new RegExp(`(?:^|; )${r}=([^;]*)`));return h?decodeURIComponent(h[1]):void 0},y=()=>{for(let r of C){if(r==="class"){for(let l of g)if(d.classList.contains(l.value))return l.theme;continue}let h=d.getAttribute(r);if(h){for(let l of g)if(l.value===h)return l.theme}}};function k(r){let h=t?t[r]:r;for(let l of C)l==="class"?(d.classList.remove(...w),h&&d.classList.add(h)):h?d.setAttribute(l,h):d.removeAttribute(l);u&&(r==="light"||r==="dark")&&(d.style.colorScheme=r)}let S=()=>window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";if(o){k(o);return}let R=r=>m&&r==="system"?S():r,v=y();if(v){k(R(v));return}try{let r=p(n)||i;k(R(r))}catch(r){}};var Q=({theme:e,attribute:n="class",value:i,enableColorScheme:o=!0}={})=>{if(!e||e==="system")return{};let a=i?i[e]:e;if(!a)return{};let t={},m=Array.isArray(n)?n:[n];for(let u of m)u==="class"?t.className=a:t[u]=a;return o&&(e==="light"||e==="dark")&&(t.style={colorScheme:e}),t};var D=["light","dark"],U="(prefers-color-scheme: dark)",V=typeof window=="undefined",E=s.createContext(void 0),j={setTheme:e=>{},themes:[]},B={path:"/",maxAge:31536e3,sameSite:"lax"},_=e=>{var n;return(n=e==null?void 0:e.name)!=null?n:"theme"},F=e=>{if(V)return;let n=document.cookie?document.cookie.split("; "):[];for(let i of n){let[o,...a]=i.split("=");if(o===e)return decodeURIComponent(a.join("="))}},W=e=>{if(e)return`${e.charAt(0).toUpperCase()}${e.slice(1)}`},$=(e,n,i)=>{try{let o={...B,...i!=null?i:{}},a=encodeURIComponent(n),t=[`${e}=${a}`];o.path&&t.push(`Path=${o.path}`),o.domain&&t.push(`Domain=${o.domain}`),typeof o.maxAge=="number"&&t.push(`Max-Age=${o.maxAge}`),o.expires&&t.push(`Expires=${o.expires.toUTCString()}`);let m=W(o.sameSite);m&&t.push(`SameSite=${m}`),o.secure&&t.push("Secure"),document.cookie=t.join("; ")}catch(o){}},te=()=>{var e;return(e=s.useContext(E))!=null?e:j},se=e=>s.useContext(E)?s.createElement(s.Fragment,null,e.children):s.createElement(J,{...e}),z=["dark","light"],J=({forcedTheme:e,disableTransitionOnChange:n=!1,enableSystem:i=!0,enableColorScheme:o=!0,cookie:a,themes:t=z,defaultTheme:m=i?"system":"light",attribute:u="class",value:d,children:C,nonce:g,scriptProps:w})=>{let p=_(a),[y,k]=s.useState(()=>K(p,m,u,d,t)),[S,R]=s.useState(()=>y==="system"?M():y),v=d?Object.values(d):t,r=s.useRef(null),h=s.useCallback(c=>{let f=c;if(!f)return;c==="system"&&i&&(f=M());let T=d?d[f]:f,N=n?X(g):null,x=document.documentElement,b=A=>{A==="class"?(x.classList.remove(...v),T&&x.classList.add(T)):A.startsWith("data-")&&(T?x.setAttribute(A,T):x.removeAttribute(A))};if(Array.isArray(u)?u.forEach(b):b(u),o){let A=D.includes(m)?m:null,I=D.includes(f)?f:A;x.style.colorScheme=I}N==null||N()},[g]),l=s.useCallback(c=>{var f;(f=r.current)==null||f.postMessage({key:p,value:c})},[p]),O=s.useCallback(c=>{typeof c=="function"?k(f=>{let T=c(f);return $(p,T,a),l(T),T}):(k(c),$(p,c,a),l(c))},[p,a,l]),P=s.useCallback(c=>{let f=M(c);R(f),y==="system"&&i&&!e&&h("system")},[y,e]);s.useEffect(()=>{let c=window.matchMedia(U);return c.addListener(P),P(c),()=>c.removeListener(P)},[P]),s.useEffect(()=>{if(typeof window=="undefined"||typeof window.BroadcastChannel=="undefined")return;let c=new BroadcastChannel(`ssr-themes:${p}`);r.current=c;let f=T=>{if(!(!T.data||T.data.key!==p)){if(!T.data.value){k(m);return}k(T.data.value)}};return c.addEventListener("message",f),()=>{c.removeEventListener("message",f),c.close(),r.current===c&&(r.current=null)}},[p,m]),s.useEffect(()=>{h(e!=null?e:y)},[e,y]);let H=s.useMemo(()=>({theme:y,setTheme:O,forcedTheme:e,resolvedTheme:y==="system"?S:y,themes:i?[...t,"system"]:t,systemTheme:i?S:void 0}),[y,O,e,S,i,t]);return s.createElement(E.Provider,{value:H},s.createElement(q,{forcedTheme:e,cookieName:p,attribute:u,enableSystem:i,enableColorScheme:o,defaultTheme:m,value:d,themes:t,nonce:g,scriptProps:w}),C)},q=s.memo(({forcedTheme:e,cookieName:n,attribute:i,enableSystem:o,enableColorScheme:a,defaultTheme:t,value:m,themes:u,nonce:d,scriptProps:C})=>{let g=JSON.stringify([i,n,t,e,u,m,o,a]).slice(1,-1);return s.createElement("script",{...C,suppressHydrationWarning:!0,nonce:typeof window=="undefined"?d:"",dangerouslySetInnerHTML:{__html:`(${L.toString()})(${g})`}})});function G(e,n,i){let o=Array.isArray(e)?e:[e],a=[];for(let t of i){let m=n?n[t]:t;m&&a.push({theme:t,value:m})}for(let t of o){if(t==="class"){for(let u of a)if(document.documentElement.classList.contains(u.value))return u.theme;continue}let m=document.documentElement.getAttribute(t);if(m){for(let u of a)if(u.value===m)return u.theme}}}function K(e,n,i,o,a){if(V)return;let t=G(i,o,a);if(t)return t;let m;try{m=F(e)}catch(u){}return m||n}var X=e=>{let n=document.createElement("style");return e&&n.setAttribute("nonce",e),n.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(n),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(n)},1)}},M=e=>(e||(e=window.matchMedia(U)),e.matches?"dark":"light");export{se as ThemeProvider,q as ThemeScript,Q as registerTheme,te as useTheme};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ValueObject {
|
|
4
|
+
[themeName: string]: string;
|
|
5
|
+
}
|
|
6
|
+
interface CookieOptions {
|
|
7
|
+
/** Cookie path attribute */
|
|
8
|
+
path?: string;
|
|
9
|
+
/** Cookie max-age attribute in seconds */
|
|
10
|
+
maxAge?: number;
|
|
11
|
+
/** Cookie expires attribute */
|
|
12
|
+
expires?: Date;
|
|
13
|
+
/** Cookie SameSite attribute */
|
|
14
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
15
|
+
/** Cookie domain attribute */
|
|
16
|
+
domain?: string;
|
|
17
|
+
/** Cookie secure attribute */
|
|
18
|
+
secure?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface CookieConfig extends CookieOptions {
|
|
21
|
+
/** Cookie name used to store theme preference */
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
type SystemThemeDefinition = readonly ['dark', 'light'];
|
|
25
|
+
type SystemTheme = SystemThemeDefinition[number];
|
|
26
|
+
type DataAttribute = `data-${string}`;
|
|
27
|
+
interface ScriptProps extends React.DetailedHTMLProps<React.ScriptHTMLAttributes<HTMLScriptElement>, HTMLScriptElement> {
|
|
28
|
+
[dataAttribute: DataAttribute]: any;
|
|
29
|
+
}
|
|
30
|
+
interface UseThemeProps<TThemes extends readonly string[] = SystemThemeDefinition> {
|
|
31
|
+
/** List of all available theme names */
|
|
32
|
+
themes: TThemes;
|
|
33
|
+
/** Forced theme name for the current page */
|
|
34
|
+
forcedTheme?: ThemeName<TThemes> | undefined;
|
|
35
|
+
/** Update the theme */
|
|
36
|
+
setTheme: React.Dispatch<React.SetStateAction<string>>;
|
|
37
|
+
/** Active theme name */
|
|
38
|
+
theme?: ThemeName<TThemes> | undefined;
|
|
39
|
+
/** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
|
|
40
|
+
resolvedTheme?: ThemeName<TThemes> | undefined;
|
|
41
|
+
/** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
|
|
42
|
+
systemTheme?: SystemTheme | undefined;
|
|
43
|
+
}
|
|
44
|
+
type Attribute = DataAttribute | 'class';
|
|
45
|
+
type ThemeName<T extends readonly string[]> = T[number];
|
|
46
|
+
type ThemeHtmlProps = {
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: React.CSSProperties;
|
|
49
|
+
} & Partial<Record<DataAttribute, string>>;
|
|
50
|
+
interface RegisterThemeOptions<TThemes extends readonly string[] = SystemThemeDefinition> {
|
|
51
|
+
/** Resolved theme name to apply to the html element */
|
|
52
|
+
theme?: ThemeName<TThemes> | undefined;
|
|
53
|
+
/** HTML attribute modified based on the active theme */
|
|
54
|
+
attribute?: Attribute | Attribute[] | undefined;
|
|
55
|
+
/** Mapping of theme name to HTML attribute value */
|
|
56
|
+
value?: ValueObject | undefined;
|
|
57
|
+
/** Whether to indicate to browsers which color scheme is used */
|
|
58
|
+
enableColorScheme?: boolean | undefined;
|
|
59
|
+
}
|
|
60
|
+
interface ThemeProviderProps<TThemes extends readonly string[] = SystemThemeDefinition> extends React.PropsWithChildren<unknown> {
|
|
61
|
+
/** List of all available theme names */
|
|
62
|
+
themes?: TThemes | undefined;
|
|
63
|
+
/** Forced theme name for the current page */
|
|
64
|
+
forcedTheme?: ThemeName<TThemes> | undefined;
|
|
65
|
+
/** Whether to switch between dark and light themes based on prefers-color-scheme */
|
|
66
|
+
enableSystem?: boolean | undefined;
|
|
67
|
+
/** Disable all CSS transitions when switching themes */
|
|
68
|
+
disableTransitionOnChange?: boolean | undefined;
|
|
69
|
+
/** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
|
|
70
|
+
enableColorScheme?: boolean | undefined;
|
|
71
|
+
/** Cookie configuration used to store theme preference */
|
|
72
|
+
cookie?: CookieConfig | undefined;
|
|
73
|
+
/** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
|
|
74
|
+
defaultTheme?: ThemeName<TThemes> | undefined;
|
|
75
|
+
/** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
|
|
76
|
+
attribute?: Attribute | Attribute[] | undefined;
|
|
77
|
+
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
|
|
78
|
+
value?: ValueObject | undefined;
|
|
79
|
+
/** Nonce string to pass to the inline script and style elements for CSP headers */
|
|
80
|
+
nonce?: string;
|
|
81
|
+
/** Props to pass the inline script */
|
|
82
|
+
scriptProps?: ScriptProps;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
declare const registerTheme: <TThemes extends readonly string[] = SystemThemeDefinition>({ theme, attribute, value, enableColorScheme, }?: RegisterThemeOptions<TThemes>) => ThemeHtmlProps;
|
|
86
|
+
|
|
87
|
+
export { type Attribute as A, type CookieConfig as C, type RegisterThemeOptions as R, type SystemThemeDefinition as S, type ThemeProviderProps as T, type UseThemeProps as U, type CookieOptions as a, type SystemTheme as b, type ThemeHtmlProps as c, registerTheme as r };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ValueObject {
|
|
4
|
+
[themeName: string]: string;
|
|
5
|
+
}
|
|
6
|
+
interface CookieOptions {
|
|
7
|
+
/** Cookie path attribute */
|
|
8
|
+
path?: string;
|
|
9
|
+
/** Cookie max-age attribute in seconds */
|
|
10
|
+
maxAge?: number;
|
|
11
|
+
/** Cookie expires attribute */
|
|
12
|
+
expires?: Date;
|
|
13
|
+
/** Cookie SameSite attribute */
|
|
14
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
15
|
+
/** Cookie domain attribute */
|
|
16
|
+
domain?: string;
|
|
17
|
+
/** Cookie secure attribute */
|
|
18
|
+
secure?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface CookieConfig extends CookieOptions {
|
|
21
|
+
/** Cookie name used to store theme preference */
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
type SystemThemeDefinition = readonly ['dark', 'light'];
|
|
25
|
+
type SystemTheme = SystemThemeDefinition[number];
|
|
26
|
+
type DataAttribute = `data-${string}`;
|
|
27
|
+
interface ScriptProps extends React.DetailedHTMLProps<React.ScriptHTMLAttributes<HTMLScriptElement>, HTMLScriptElement> {
|
|
28
|
+
[dataAttribute: DataAttribute]: any;
|
|
29
|
+
}
|
|
30
|
+
interface UseThemeProps<TThemes extends readonly string[] = SystemThemeDefinition> {
|
|
31
|
+
/** List of all available theme names */
|
|
32
|
+
themes: TThemes;
|
|
33
|
+
/** Forced theme name for the current page */
|
|
34
|
+
forcedTheme?: ThemeName<TThemes> | undefined;
|
|
35
|
+
/** Update the theme */
|
|
36
|
+
setTheme: React.Dispatch<React.SetStateAction<string>>;
|
|
37
|
+
/** Active theme name */
|
|
38
|
+
theme?: ThemeName<TThemes> | undefined;
|
|
39
|
+
/** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
|
|
40
|
+
resolvedTheme?: ThemeName<TThemes> | undefined;
|
|
41
|
+
/** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
|
|
42
|
+
systemTheme?: SystemTheme | undefined;
|
|
43
|
+
}
|
|
44
|
+
type Attribute = DataAttribute | 'class';
|
|
45
|
+
type ThemeName<T extends readonly string[]> = T[number];
|
|
46
|
+
type ThemeHtmlProps = {
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: React.CSSProperties;
|
|
49
|
+
} & Partial<Record<DataAttribute, string>>;
|
|
50
|
+
interface RegisterThemeOptions<TThemes extends readonly string[] = SystemThemeDefinition> {
|
|
51
|
+
/** Resolved theme name to apply to the html element */
|
|
52
|
+
theme?: ThemeName<TThemes> | undefined;
|
|
53
|
+
/** HTML attribute modified based on the active theme */
|
|
54
|
+
attribute?: Attribute | Attribute[] | undefined;
|
|
55
|
+
/** Mapping of theme name to HTML attribute value */
|
|
56
|
+
value?: ValueObject | undefined;
|
|
57
|
+
/** Whether to indicate to browsers which color scheme is used */
|
|
58
|
+
enableColorScheme?: boolean | undefined;
|
|
59
|
+
}
|
|
60
|
+
interface ThemeProviderProps<TThemes extends readonly string[] = SystemThemeDefinition> extends React.PropsWithChildren<unknown> {
|
|
61
|
+
/** List of all available theme names */
|
|
62
|
+
themes?: TThemes | undefined;
|
|
63
|
+
/** Forced theme name for the current page */
|
|
64
|
+
forcedTheme?: ThemeName<TThemes> | undefined;
|
|
65
|
+
/** Whether to switch between dark and light themes based on prefers-color-scheme */
|
|
66
|
+
enableSystem?: boolean | undefined;
|
|
67
|
+
/** Disable all CSS transitions when switching themes */
|
|
68
|
+
disableTransitionOnChange?: boolean | undefined;
|
|
69
|
+
/** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
|
|
70
|
+
enableColorScheme?: boolean | undefined;
|
|
71
|
+
/** Cookie configuration used to store theme preference */
|
|
72
|
+
cookie?: CookieConfig | undefined;
|
|
73
|
+
/** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
|
|
74
|
+
defaultTheme?: ThemeName<TThemes> | undefined;
|
|
75
|
+
/** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
|
|
76
|
+
attribute?: Attribute | Attribute[] | undefined;
|
|
77
|
+
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
|
|
78
|
+
value?: ValueObject | undefined;
|
|
79
|
+
/** Nonce string to pass to the inline script and style elements for CSP headers */
|
|
80
|
+
nonce?: string;
|
|
81
|
+
/** Props to pass the inline script */
|
|
82
|
+
scriptProps?: ScriptProps;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
declare const registerTheme: <TThemes extends readonly string[] = SystemThemeDefinition>({ theme, attribute, value, enableColorScheme, }?: RegisterThemeOptions<TThemes>) => ThemeHtmlProps;
|
|
86
|
+
|
|
87
|
+
export { type Attribute as A, type CookieConfig as C, type RegisterThemeOptions as R, type SystemThemeDefinition as S, type ThemeProviderProps as T, type UseThemeProps as U, type CookieOptions as a, type SystemTheme as b, type ThemeHtmlProps as c, registerTheme as r };
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var m=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var l=(e,s)=>{for(var o in s)m(e,o,{get:s[o],enumerable:!0})},c=(e,s,o,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of h(s))!y.call(e,r)&&r!==o&&m(e,r,{get:()=>s[r],enumerable:!(t=f(s,r))||t.enumerable});return e};var a=e=>c(m({},"__esModule",{value:!0}),e);var g={};l(g,{registerTheme:()=>p});module.exports=a(g);var p=({theme:e,attribute:s="class",value:o,enableColorScheme:t=!0}={})=>{if(!e||e==="system")return{};let r=o?o[e]:e;if(!r)return{};let i={},T=Array.isArray(s)?s:[s];for(let n of T)n==="class"?i.className=r:i[n]=r;return t&&(e==="light"||e==="dark")&&(i.style={colorScheme:e}),i};0&&(module.exports={registerTheme});
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=({theme:e,attribute:r="class",value:t,enableColorScheme:m=!0}={})=>{if(!e||e==="system")return{};let o=t?t[e]:e;if(!o)return{};let s={},n=Array.isArray(r)?r:[r];for(let i of n)i==="class"?s.className=o:s[i]=o;return m&&(e==="light"||e==="dark")&&(s.style={colorScheme:e}),s};export{p as registerTheme};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ssr-themes",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"LICENSE.md",
|
|
11
|
+
"package.json"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"react-server": "./dist/server.mjs",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"types": "./dist/server.d.ts",
|
|
22
|
+
"import": "./dist/server.mjs",
|
|
23
|
+
"require": "./dist/server.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"prepublish": "bun run build",
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"test": "vitest run __tests__",
|
|
31
|
+
"clean": "rm -rf dist node_modules"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^19.2.3",
|
|
35
|
+
"react-dom": "^19.2.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"react": "^19.2.3",
|
|
39
|
+
"react-dom": "^19.2.3"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/0xcadams/ssr-themes.git"
|
|
44
|
+
}
|
|
45
|
+
}
|