@wrksz/themes 0.2.0 → 0.3.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/README.md +194 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +157 -37
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1 +1,194 @@
|
|
|
1
|
-
|
|
1
|
+
<img src=".github/images/banner.png" alt="@wrksz/themes" width="100%" />
|
|
2
|
+
|
|
3
|
+
# @wrksz/themes
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@wrksz/themes)
|
|
6
|
+
|
|
7
|
+
Modern theme management for Next.js 16+ and React 19+. Drop-in replacement for `next-themes` - fixes every known bug and adds missing features.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @wrksz/themes
|
|
11
|
+
# or
|
|
12
|
+
npm install @wrksz/themes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Add the provider to your root layout. Add `suppressHydrationWarning` to `<html>` to prevent hydration warnings caused by the inline theme script running before React hydrates.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/layout.tsx
|
|
21
|
+
import { ThemeProvider } from "@wrksz/themes";
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
24
|
+
return (
|
|
25
|
+
<html lang="en" suppressHydrationWarning>
|
|
26
|
+
<body>
|
|
27
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
"use client";
|
|
38
|
+
|
|
39
|
+
import { useTheme } from "@wrksz/themes";
|
|
40
|
+
|
|
41
|
+
export function ThemeToggle() {
|
|
42
|
+
const { theme, setTheme } = useTheme();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
|
46
|
+
Toggle theme
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `ThemeProvider`
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
|------|------|---------|-------------|
|
|
58
|
+
| `themes` | `string[]` | `["light", "dark"]` | Available themes |
|
|
59
|
+
| `defaultTheme` | `string` | `"system"` | Theme used when no preference is stored |
|
|
60
|
+
| `forcedTheme` | `string` | - | Force a specific theme, ignoring user preference |
|
|
61
|
+
| `enableSystem` | `boolean` | `true` | Detect system preference via `prefers-color-scheme` |
|
|
62
|
+
| `enableColorScheme` | `boolean` | `true` | Set native `color-scheme` CSS property |
|
|
63
|
+
| `attribute` | `string \| string[]` | `"class"` | HTML attribute(s) to set on target element (`"class"`, `"data-theme"`, etc.) |
|
|
64
|
+
| `value` | `Record<string, string>` | - | Map theme names to attribute values |
|
|
65
|
+
| `target` | `string` | `"html"` | Element to apply theme to (`"html"`, `"body"`, or a CSS selector) |
|
|
66
|
+
| `storageKey` | `string` | `"theme"` | Key used for storage |
|
|
67
|
+
| `storage` | `"localStorage" \| "sessionStorage" \| "none"` | `"localStorage"` | Where to persist the theme |
|
|
68
|
+
| `disableTransitionOnChange` | `boolean` | `false` | Disable CSS transitions when switching themes |
|
|
69
|
+
| `followSystem` | `boolean` | `false` | Always follow system preference changes, even after `setTheme` was called |
|
|
70
|
+
| `themeColor` | `string \| Record<string, string>` | - | Update `<meta name="theme-color">` on theme change |
|
|
71
|
+
| `nonce` | `string` | - | CSP nonce for the inline script |
|
|
72
|
+
| `onThemeChange` | `(theme: string) => void` | - | Called whenever the resolved theme changes |
|
|
73
|
+
|
|
74
|
+
### `useTheme`
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const {
|
|
78
|
+
theme, // Current theme - may be "system"
|
|
79
|
+
resolvedTheme, // Actual theme - never "system"
|
|
80
|
+
systemTheme, // System preference: "light" | "dark" | undefined
|
|
81
|
+
forcedTheme, // Forced theme if set
|
|
82
|
+
themes, // Available themes
|
|
83
|
+
setTheme, // Set theme
|
|
84
|
+
} = useTheme();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Supports generics for full type safety:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
type AppTheme = "light" | "dark" | "high-contrast";
|
|
91
|
+
|
|
92
|
+
const { theme, setTheme } = useTheme<AppTheme>();
|
|
93
|
+
// theme: AppTheme | "system" | undefined
|
|
94
|
+
// setTheme: (theme: AppTheme | "system") => void
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Examples
|
|
98
|
+
|
|
99
|
+
### Custom themes with Tailwind
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<ThemeProvider
|
|
103
|
+
themes={["light", "dark", "high-contrast"]}
|
|
104
|
+
attribute="class"
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</ThemeProvider>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Data attribute instead of class
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<ThemeProvider attribute="data-theme">
|
|
114
|
+
{children}
|
|
115
|
+
</ThemeProvider>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
[data-theme="dark"] { --bg: #000; }
|
|
120
|
+
[data-theme="light"] { --bg: #fff; }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Custom attribute values
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<ThemeProvider
|
|
127
|
+
themes={["light", "dark"]}
|
|
128
|
+
attribute="data-mode"
|
|
129
|
+
value={{ light: "light-mode", dark: "dark-mode" }}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</ThemeProvider>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Meta theme-color (Safari / PWA)
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<ThemeProvider
|
|
139
|
+
themeColor={{ light: "#ffffff", dark: "#0a0a0a" }}
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</ThemeProvider>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Works with CSS variables too:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<ThemeProvider themeColor="var(--color-background)">
|
|
149
|
+
{children}
|
|
150
|
+
</ThemeProvider>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Disable storage
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// No persistence - always uses defaultTheme or system preference
|
|
157
|
+
<ThemeProvider storage="none" defaultTheme="dark">
|
|
158
|
+
{children}
|
|
159
|
+
</ThemeProvider>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Forced theme per page
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// app/dashboard/layout.tsx
|
|
166
|
+
<ThemeProvider forcedTheme="dark">
|
|
167
|
+
{children}
|
|
168
|
+
</ThemeProvider>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Class on body instead of html
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<ThemeProvider target="body">
|
|
175
|
+
{children}
|
|
176
|
+
</ThemeProvider>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Why not `next-themes`?
|
|
180
|
+
|
|
181
|
+
| Issue | next-themes | @wrksz/themes |
|
|
182
|
+
|-------|-------------|---------------|
|
|
183
|
+
| React 19 script warning | Yes | Fixed (RSC split) |
|
|
184
|
+
| `__name` minification bug | Yes | Fixed |
|
|
185
|
+
| React 19 Activity/cacheComponents stale theme | Yes | Fixed (`useSyncExternalStore`) |
|
|
186
|
+
| `sessionStorage` support | No | Yes |
|
|
187
|
+
| Disable storage | No | Yes (`storage: "none"`) |
|
|
188
|
+
| `meta theme-color` support | No | Yes (`themeColor` prop) |
|
|
189
|
+
| Generic types | No | Yes (`useTheme<AppTheme>()`) |
|
|
190
|
+
| Zero runtime dependencies | Yes | Yes |
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ type DefaultTheme = "light" | "dark" | "system";
|
|
|
3
3
|
type Attribute = "class" | `data-${string}`;
|
|
4
4
|
type ValueObject = Record<string, string>;
|
|
5
5
|
type StorageType = "localStorage" | "sessionStorage" | "none";
|
|
6
|
+
/** Per-theme colors for meta theme-color, or a single string for all themes */
|
|
7
|
+
type ThemeColor = string | Partial<Record<string, string>>;
|
|
6
8
|
type ThemeProviderProps<Themes extends string = DefaultTheme> = {
|
|
7
9
|
children: ReactNode;
|
|
8
10
|
/** All available themes */
|
|
@@ -31,6 +33,10 @@ type ThemeProviderProps<Themes extends string = DefaultTheme> = {
|
|
|
31
33
|
nonce?: string;
|
|
32
34
|
/** Called when theme changes */
|
|
33
35
|
onThemeChange?: (theme: Themes) => void;
|
|
36
|
+
/** Colors for meta theme-color tag, per theme or a single value */
|
|
37
|
+
themeColor?: ThemeColor;
|
|
38
|
+
/** Always follow system preference changes, even after setTheme was called */
|
|
39
|
+
followSystem?: boolean;
|
|
34
40
|
};
|
|
35
41
|
type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
36
42
|
/** Current theme (may be "system") */
|
|
@@ -48,5 +54,5 @@ type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
|
48
54
|
};
|
|
49
55
|
declare function useTheme<Themes extends string = DefaultTheme>(): ThemeContextValue<Themes>;
|
|
50
56
|
import { ReactElement } from "react";
|
|
51
|
-
declare function ThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, nonce, onThemeChange }: ThemeProviderProps<Themes>): ReactElement;
|
|
52
|
-
export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, StorageType, DefaultTheme, Attribute };
|
|
57
|
+
declare function ThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, nonce, onThemeChange, themeColor, followSystem }: ThemeProviderProps<Themes>): ReactElement;
|
|
58
|
+
export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, Attribute };
|
package/dist/index.js
CHANGED
|
@@ -10,10 +10,65 @@ function useTheme() {
|
|
|
10
10
|
return ctx;
|
|
11
11
|
}
|
|
12
12
|
// src/client-provider.tsx
|
|
13
|
-
import { useCallback, useEffect, useRef,
|
|
13
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
14
|
+
|
|
15
|
+
// src/store.ts
|
|
16
|
+
var SERVER_SNAPSHOT = { theme: undefined, systemTheme: undefined };
|
|
17
|
+
function createThemeStore() {
|
|
18
|
+
let state = { theme: undefined, systemTheme: undefined };
|
|
19
|
+
const listeners = new Set;
|
|
20
|
+
function emit() {
|
|
21
|
+
for (const listener of listeners)
|
|
22
|
+
listener();
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
subscribe(listener) {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
getSnapshot() {
|
|
32
|
+
return state;
|
|
33
|
+
},
|
|
34
|
+
getServerSnapshot() {
|
|
35
|
+
return SERVER_SNAPSHOT;
|
|
36
|
+
},
|
|
37
|
+
setTheme(theme) {
|
|
38
|
+
if (state.theme === theme)
|
|
39
|
+
return;
|
|
40
|
+
state = { ...state, theme };
|
|
41
|
+
emit();
|
|
42
|
+
},
|
|
43
|
+
setSystemTheme(systemTheme) {
|
|
44
|
+
if (state.systemTheme === systemTheme)
|
|
45
|
+
return;
|
|
46
|
+
state = { ...state, systemTheme };
|
|
47
|
+
emit();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/client-provider.tsx
|
|
14
53
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
15
54
|
|
|
16
55
|
var DEFAULT_THEMES = ["light", "dark"];
|
|
56
|
+
function resolveThemeColor(themeColor, resolved) {
|
|
57
|
+
if (typeof themeColor === "string")
|
|
58
|
+
return themeColor;
|
|
59
|
+
return themeColor[resolved];
|
|
60
|
+
}
|
|
61
|
+
function updateMetaThemeColor(color) {
|
|
62
|
+
if (!color)
|
|
63
|
+
return;
|
|
64
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
65
|
+
if (!meta) {
|
|
66
|
+
meta = document.createElement("meta");
|
|
67
|
+
meta.name = "theme-color";
|
|
68
|
+
document.head.appendChild(meta);
|
|
69
|
+
}
|
|
70
|
+
meta.content = color;
|
|
71
|
+
}
|
|
17
72
|
function ClientThemeProvider({
|
|
18
73
|
children,
|
|
19
74
|
themes = DEFAULT_THEMES,
|
|
@@ -27,11 +82,15 @@ function ClientThemeProvider({
|
|
|
27
82
|
storage = "localStorage",
|
|
28
83
|
storageKey = "theme",
|
|
29
84
|
enableColorScheme = true,
|
|
85
|
+
themeColor,
|
|
86
|
+
followSystem = false,
|
|
30
87
|
onThemeChange
|
|
31
88
|
}) {
|
|
32
89
|
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
33
|
-
const
|
|
34
|
-
const
|
|
90
|
+
const storeRef = useRef(createThemeStore());
|
|
91
|
+
const store = storeRef.current;
|
|
92
|
+
const { getSnapshot, setTheme: setStoreTheme, setSystemTheme: setStoreSystemTheme } = store;
|
|
93
|
+
const { theme, systemTheme } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
35
94
|
const resolvedTheme = forcedTheme ?? (theme === "system" || theme === undefined ? systemTheme : theme);
|
|
36
95
|
const onThemeChangeRef = useRef(onThemeChange);
|
|
37
96
|
useEffect(() => {
|
|
@@ -68,39 +127,81 @@ function ClientThemeProvider({
|
|
|
68
127
|
if (enableColorScheme && (resolved === "light" || resolved === "dark")) {
|
|
69
128
|
el.style.colorScheme = resolved;
|
|
70
129
|
}
|
|
71
|
-
|
|
130
|
+
if (themeColor) {
|
|
131
|
+
updateMetaThemeColor(resolveThemeColor(themeColor, resolved));
|
|
132
|
+
}
|
|
133
|
+
}, [
|
|
134
|
+
attribute,
|
|
135
|
+
disableTransitionOnChange,
|
|
136
|
+
enableColorScheme,
|
|
137
|
+
getTargetEl,
|
|
138
|
+
themes,
|
|
139
|
+
valueMap,
|
|
140
|
+
themeColor
|
|
141
|
+
]);
|
|
72
142
|
useEffect(() => {
|
|
143
|
+
const mq = enableSystem ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
144
|
+
const sys = mq ? mq.matches ? "dark" : "light" : undefined;
|
|
145
|
+
if (sys)
|
|
146
|
+
setStoreSystemTheme(sys);
|
|
73
147
|
if (forcedTheme) {
|
|
74
|
-
|
|
75
|
-
|
|
148
|
+
setStoreTheme(forcedTheme);
|
|
149
|
+
applyToDom(forcedTheme);
|
|
150
|
+
} else {
|
|
151
|
+
let stored = null;
|
|
152
|
+
try {
|
|
153
|
+
if (storage !== "none") {
|
|
154
|
+
const s = storage === "localStorage" ? localStorage : sessionStorage;
|
|
155
|
+
stored = s.getItem(storageKey);
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
const initial = stored && themes.includes(stored) ? stored : resolvedDefault;
|
|
159
|
+
setStoreTheme(initial);
|
|
160
|
+
applyToDom(initial === "system" ? sys ?? "light" : initial);
|
|
76
161
|
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
if (storage !== "none") {
|
|
80
|
-
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
81
|
-
stored = store.getItem(storageKey);
|
|
82
|
-
}
|
|
83
|
-
} catch {}
|
|
84
|
-
const initial = stored && themes.includes(stored) ? stored : resolvedDefault;
|
|
85
|
-
setThemeState(initial);
|
|
86
|
-
}, [forcedTheme, resolvedDefault, storage, storageKey, themes]);
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (!enableSystem)
|
|
162
|
+
if (!mq)
|
|
89
163
|
return;
|
|
90
|
-
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
91
|
-
const sys = mq.matches ? "dark" : "light";
|
|
92
|
-
setSystemTheme(sys);
|
|
93
164
|
const handler = (e) => {
|
|
94
165
|
const next = e.matches ? "dark" : "light";
|
|
95
|
-
|
|
96
|
-
|
|
166
|
+
setStoreSystemTheme(next);
|
|
167
|
+
const current = getSnapshot().theme;
|
|
168
|
+
if (current === "system" || current === undefined || followSystem) {
|
|
169
|
+
if (followSystem) {
|
|
170
|
+
setStoreTheme("system");
|
|
171
|
+
}
|
|
97
172
|
applyToDom(next);
|
|
98
173
|
onThemeChangeRef.current?.(next);
|
|
99
174
|
}
|
|
100
175
|
};
|
|
101
176
|
mq.addEventListener("change", handler);
|
|
102
177
|
return () => mq.removeEventListener("change", handler);
|
|
103
|
-
}, [
|
|
178
|
+
}, [
|
|
179
|
+
forcedTheme,
|
|
180
|
+
resolvedDefault,
|
|
181
|
+
storage,
|
|
182
|
+
storageKey,
|
|
183
|
+
themes,
|
|
184
|
+
enableSystem,
|
|
185
|
+
followSystem,
|
|
186
|
+
applyToDom,
|
|
187
|
+
getSnapshot,
|
|
188
|
+
setStoreTheme,
|
|
189
|
+
setStoreSystemTheme
|
|
190
|
+
]);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const handler = () => {
|
|
193
|
+
const { theme: theme2, systemTheme: systemTheme2 } = getSnapshot();
|
|
194
|
+
const resolved = forcedTheme ?? (theme2 === "system" || theme2 === undefined ? systemTheme2 : theme2);
|
|
195
|
+
if (resolved)
|
|
196
|
+
applyToDom(resolved);
|
|
197
|
+
};
|
|
198
|
+
window.addEventListener("pageshow", handler);
|
|
199
|
+
window.addEventListener("popstate", handler);
|
|
200
|
+
return () => {
|
|
201
|
+
window.removeEventListener("pageshow", handler);
|
|
202
|
+
window.removeEventListener("popstate", handler);
|
|
203
|
+
};
|
|
204
|
+
}, [applyToDom, forcedTheme, getSnapshot]);
|
|
104
205
|
useEffect(() => {
|
|
105
206
|
if (storage === "none")
|
|
106
207
|
return;
|
|
@@ -109,29 +210,30 @@ function ClientThemeProvider({
|
|
|
109
210
|
return;
|
|
110
211
|
if (themes.includes(e.newValue)) {
|
|
111
212
|
const newTheme = e.newValue;
|
|
112
|
-
const resolved = newTheme === "system" ? systemTheme ?? "light" : newTheme;
|
|
113
|
-
|
|
213
|
+
const resolved = newTheme === "system" ? getSnapshot().systemTheme ?? "light" : newTheme;
|
|
214
|
+
setStoreTheme(newTheme);
|
|
114
215
|
applyToDom(resolved);
|
|
115
216
|
}
|
|
116
217
|
};
|
|
117
218
|
window.addEventListener("storage", handler);
|
|
118
219
|
return () => window.removeEventListener("storage", handler);
|
|
119
|
-
}, [storage, storageKey, themes,
|
|
220
|
+
}, [storage, storageKey, themes, applyToDom, getSnapshot, setStoreTheme]);
|
|
120
221
|
const setTheme = useCallback((next) => {
|
|
121
222
|
if (forcedTheme)
|
|
122
223
|
return;
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
224
|
+
const current = getSnapshot().theme;
|
|
225
|
+
const newTheme = typeof next === "function" ? next(current) : next;
|
|
226
|
+
const resolved = newTheme === "system" ? getSnapshot().systemTheme ?? "light" : newTheme;
|
|
227
|
+
setStoreTheme(newTheme);
|
|
126
228
|
applyToDom(resolved);
|
|
127
229
|
onThemeChangeRef.current?.(resolved);
|
|
128
230
|
try {
|
|
129
231
|
if (storage !== "none") {
|
|
130
|
-
const
|
|
131
|
-
|
|
232
|
+
const store2 = storage === "localStorage" ? localStorage : sessionStorage;
|
|
233
|
+
store2.setItem(storageKey, newTheme);
|
|
132
234
|
}
|
|
133
235
|
} catch {}
|
|
134
|
-
}, [applyToDom, forcedTheme, storage, storageKey,
|
|
236
|
+
}, [applyToDom, forcedTheme, storage, storageKey, getSnapshot, setStoreTheme]);
|
|
135
237
|
const contextValue = {
|
|
136
238
|
theme: forcedTheme ?? theme,
|
|
137
239
|
resolvedTheme,
|
|
@@ -147,7 +249,7 @@ function ClientThemeProvider({
|
|
|
147
249
|
}
|
|
148
250
|
|
|
149
251
|
// src/script.ts
|
|
150
|
-
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage) {
|
|
252
|
+
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage, themeColors) {
|
|
151
253
|
let theme;
|
|
152
254
|
if (forcedTheme) {
|
|
153
255
|
theme = forcedTheme;
|
|
@@ -181,6 +283,18 @@ function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableCo
|
|
|
181
283
|
if (enableColorScheme && (theme === "light" || theme === "dark")) {
|
|
182
284
|
el.style.colorScheme = theme;
|
|
183
285
|
}
|
|
286
|
+
if (themeColors) {
|
|
287
|
+
const color = typeof themeColors === "string" ? themeColors : themeColors[theme];
|
|
288
|
+
if (color) {
|
|
289
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
290
|
+
if (!meta) {
|
|
291
|
+
meta = document.createElement("meta");
|
|
292
|
+
meta.setAttribute("name", "theme-color");
|
|
293
|
+
document.head.appendChild(meta);
|
|
294
|
+
}
|
|
295
|
+
meta.setAttribute("content", color);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
184
298
|
}
|
|
185
299
|
function getScript(config) {
|
|
186
300
|
const fn = themeScript.toString().replace(/\s*__name\s*\([^)]*\)\s*;?\s*/g, "");
|
|
@@ -194,7 +308,8 @@ function getScript(config) {
|
|
|
194
308
|
JSON.stringify(config.themes),
|
|
195
309
|
JSON.stringify(config.value ?? null),
|
|
196
310
|
JSON.stringify(config.target),
|
|
197
|
-
JSON.stringify(config.storage)
|
|
311
|
+
JSON.stringify(config.storage),
|
|
312
|
+
JSON.stringify(config.themeColors ?? null)
|
|
198
313
|
].join(",");
|
|
199
314
|
return `(${fn})(${args})`;
|
|
200
315
|
}
|
|
@@ -216,7 +331,9 @@ function ThemeProvider({
|
|
|
216
331
|
storageKey = "theme",
|
|
217
332
|
enableColorScheme = true,
|
|
218
333
|
nonce,
|
|
219
|
-
onThemeChange
|
|
334
|
+
onThemeChange,
|
|
335
|
+
themeColor,
|
|
336
|
+
followSystem = false
|
|
220
337
|
}) {
|
|
221
338
|
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
222
339
|
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
@@ -233,7 +350,8 @@ function ThemeProvider({
|
|
|
233
350
|
themes,
|
|
234
351
|
value: valueMap,
|
|
235
352
|
target,
|
|
236
|
-
storage
|
|
353
|
+
storage,
|
|
354
|
+
themeColors: themeColor
|
|
237
355
|
})
|
|
238
356
|
},
|
|
239
357
|
nonce
|
|
@@ -250,6 +368,8 @@ function ThemeProvider({
|
|
|
250
368
|
storage,
|
|
251
369
|
storageKey,
|
|
252
370
|
enableColorScheme,
|
|
371
|
+
themeColor,
|
|
372
|
+
followSystem,
|
|
253
373
|
onThemeChange,
|
|
254
374
|
children
|
|
255
375
|
}, undefined, false, undefined, this)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrksz/themes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A modern, fully-featured theme management library for Next.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"lint": "biome check src",
|
|
18
18
|
"lint:fix": "biome check --write src",
|
|
19
19
|
"format": "biome format --write src",
|
|
20
|
+
"test": "bun test",
|
|
20
21
|
"prepare": "[ \"$CI\" = \"true\" ] || lefthook install"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"@types/react": "^19.2.14",
|
|
26
27
|
"@types/react-dom": "^19.2.3",
|
|
27
28
|
"bunup": "^0.16.31",
|
|
29
|
+
"happy-dom": "^20.8.4",
|
|
28
30
|
"lefthook": "^2.1.4",
|
|
29
31
|
"react": "^19.2.4",
|
|
30
32
|
"react-dom": "^19.2.4",
|