@wrksz/themes 0.4.1 → 0.5.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 +5 -5
- package/dist/client.d.ts +82 -0
- package/dist/client.js +40 -0
- package/dist/index.d.ts +2 -27
- package/dist/index.js +10 -297
- package/dist/shared/chunk-ea7hc1e9.js +263 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
36
36
|
```tsx
|
|
37
37
|
"use client";
|
|
38
38
|
|
|
39
|
-
import { useTheme } from "@wrksz/themes";
|
|
39
|
+
import { useTheme } from "@wrksz/themes/client";
|
|
40
40
|
|
|
41
41
|
export function ThemeToggle() {
|
|
42
42
|
const { theme, setTheme } = useTheme();
|
|
@@ -243,7 +243,7 @@ export default async function RootLayout({ children }) {
|
|
|
243
243
|
```tsx
|
|
244
244
|
"use client";
|
|
245
245
|
|
|
246
|
-
import { ClientThemeProvider } from "@wrksz/themes";
|
|
246
|
+
import { ClientThemeProvider } from "@wrksz/themes/client";
|
|
247
247
|
|
|
248
248
|
export function AdminShell({ children }: { children: React.ReactNode }) {
|
|
249
249
|
return (
|
|
@@ -261,7 +261,7 @@ Returns the value from a map that matches the current resolved theme. Returns `u
|
|
|
261
261
|
```tsx
|
|
262
262
|
"use client";
|
|
263
263
|
|
|
264
|
-
import { useThemeValue } from "@wrksz/themes";
|
|
264
|
+
import { useThemeValue } from "@wrksz/themes/client";
|
|
265
265
|
|
|
266
266
|
// strings
|
|
267
267
|
const label = useThemeValue({ light: "Switch to dark", dark: "Switch to light" });
|
|
@@ -278,7 +278,7 @@ const icon = useThemeValue({ light: <SunIcon />, dark: <MoonIcon /> });
|
|
|
278
278
|
Showing different images per theme has a hydration mismatch problem - `resolvedTheme` is always `undefined` on the server. Use the built-in `ThemedImage` component which shows a transparent placeholder until the theme resolves on the client:
|
|
279
279
|
|
|
280
280
|
```tsx
|
|
281
|
-
import { ThemedImage } from "@wrksz/themes";
|
|
281
|
+
import { ThemedImage } from "@wrksz/themes/client";
|
|
282
282
|
|
|
283
283
|
<ThemedImage
|
|
284
284
|
src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
|
|
@@ -309,7 +309,7 @@ For custom themes or `next/image`, use `resolvedTheme` directly with a fallback:
|
|
|
309
309
|
"use client";
|
|
310
310
|
|
|
311
311
|
import Image from "next/image";
|
|
312
|
-
import { useTheme } from "@wrksz/themes";
|
|
312
|
+
import { useTheme } from "@wrksz/themes/client";
|
|
313
313
|
|
|
314
314
|
export function Logo() {
|
|
315
315
|
const { resolvedTheme } = useTheme();
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
type DefaultTheme = "light" | "dark" | "system";
|
|
4
|
+
type Attribute = "class" | `data-${string}`;
|
|
5
|
+
type ValueObject = Record<string, string>;
|
|
6
|
+
type StorageType = "localStorage" | "sessionStorage" | "none";
|
|
7
|
+
/** Per-theme colors for meta theme-color, or a single string for all themes */
|
|
8
|
+
type ThemeColor = string | Partial<Record<string, string>>;
|
|
9
|
+
type ThemeProviderProps<Themes extends string = DefaultTheme> = {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** All available themes */
|
|
12
|
+
themes?: Themes[];
|
|
13
|
+
/** Forced theme, overrides everything */
|
|
14
|
+
forcedTheme?: Themes;
|
|
15
|
+
/** Enable system preference via prefers-color-scheme */
|
|
16
|
+
enableSystem?: boolean;
|
|
17
|
+
/** Default theme when no preference stored */
|
|
18
|
+
defaultTheme?: Themes | "system";
|
|
19
|
+
/** HTML attribute(s) to set on target element */
|
|
20
|
+
attribute?: Attribute | Attribute[];
|
|
21
|
+
/** Map theme name to attribute value */
|
|
22
|
+
value?: ValueObject;
|
|
23
|
+
/** Target element to apply theme to, defaults to <html> */
|
|
24
|
+
target?: "html" | "body" | string;
|
|
25
|
+
/** Disable CSS transitions on theme change */
|
|
26
|
+
disableTransitionOnChange?: boolean;
|
|
27
|
+
/** Where to persist theme */
|
|
28
|
+
storage?: StorageType;
|
|
29
|
+
/** Storage key */
|
|
30
|
+
storageKey?: string;
|
|
31
|
+
/** Set native color-scheme CSS property */
|
|
32
|
+
enableColorScheme?: boolean;
|
|
33
|
+
/** Nonce for CSP */
|
|
34
|
+
nonce?: string;
|
|
35
|
+
/** Called when theme changes */
|
|
36
|
+
onThemeChange?: (theme: Themes) => void;
|
|
37
|
+
/** Colors for meta theme-color tag, per theme or a single value */
|
|
38
|
+
themeColor?: ThemeColor;
|
|
39
|
+
/** Always follow system preference changes, even after setTheme was called */
|
|
40
|
+
followSystem?: boolean;
|
|
41
|
+
/** Server-provided theme that overrides storage on mount (e.g. from a database). User can still call setTheme to change it. */
|
|
42
|
+
initialTheme?: Themes | "system";
|
|
43
|
+
};
|
|
44
|
+
type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
45
|
+
/** Current theme (may be "system") */
|
|
46
|
+
theme: Themes | "system" | undefined;
|
|
47
|
+
/** Resolved theme - never "system" */
|
|
48
|
+
resolvedTheme: Themes | undefined;
|
|
49
|
+
/** System preference */
|
|
50
|
+
systemTheme: "light" | "dark" | undefined;
|
|
51
|
+
/** Forced theme if set */
|
|
52
|
+
forcedTheme: Themes | undefined;
|
|
53
|
+
/** All available themes */
|
|
54
|
+
themes: Themes[];
|
|
55
|
+
/** Set theme */
|
|
56
|
+
setTheme: (theme: Themes | "system" | ((current: Themes | "system" | undefined) => Themes | "system")) => void;
|
|
57
|
+
};
|
|
58
|
+
declare function ClientThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, themeColor, followSystem, onThemeChange, initialTheme }: ThemeProviderProps<Themes>): ReactElement;
|
|
59
|
+
declare function useTheme<Themes extends string = DefaultTheme>(): ThemeContextValue<Themes>;
|
|
60
|
+
import { ImgHTMLAttributes, ReactElement as ReactElement2 } from "react";
|
|
61
|
+
type ThemedImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt"> & {
|
|
62
|
+
/** Map of theme name to image source */
|
|
63
|
+
src: Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Shown before the theme resolves on the client.
|
|
66
|
+
* Defaults to a transparent 1x1 GIF to avoid hydration mismatch.
|
|
67
|
+
*/
|
|
68
|
+
fallback?: string;
|
|
69
|
+
/** Alt text (required for accessibility) */
|
|
70
|
+
alt: string;
|
|
71
|
+
};
|
|
72
|
+
declare function ThemedImage({ src, fallback, alt,...props }: ThemedImageProps): ReactElement2;
|
|
73
|
+
/**
|
|
74
|
+
* Returns the value from the map that corresponds to the current resolved theme.
|
|
75
|
+
* Returns `undefined` if the theme hasn't resolved yet (e.g. during SSR).
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const label = useThemeValue({ light: "Switch to dark", dark: "Switch to light" });
|
|
79
|
+
* const color = useThemeValue({ light: "#fff", dark: "#000", purple: "#1a0a2e" });
|
|
80
|
+
*/
|
|
81
|
+
declare function useThemeValue<T>(map: Record<string, T>): T | undefined;
|
|
82
|
+
export { useThemeValue, useTheme, ValueObject, ThemedImageProps, ThemedImage, ThemeProviderProps, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, ClientThemeProvider, Attribute };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
ClientThemeProvider,
|
|
4
|
+
useTheme
|
|
5
|
+
} from "./shared/chunk-ea7hc1e9.js";
|
|
6
|
+
// src/themed-image.tsx
|
|
7
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
8
|
+
|
|
9
|
+
var TRANSPARENT_FALLBACK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
10
|
+
function ThemedImage({
|
|
11
|
+
src,
|
|
12
|
+
fallback = TRANSPARENT_FALLBACK,
|
|
13
|
+
alt,
|
|
14
|
+
...props
|
|
15
|
+
}) {
|
|
16
|
+
const { resolvedTheme } = useTheme();
|
|
17
|
+
const resolvedSrc = resolvedTheme && src[resolvedTheme] || fallback;
|
|
18
|
+
return /* @__PURE__ */ jsxDEV("img", {
|
|
19
|
+
src: resolvedSrc,
|
|
20
|
+
alt,
|
|
21
|
+
...props
|
|
22
|
+
}, undefined, false, undefined, this);
|
|
23
|
+
}
|
|
24
|
+
// src/use-theme-value.ts
|
|
25
|
+
|
|
26
|
+
function useThemeValue(map) {
|
|
27
|
+
const { resolvedTheme } = useTheme();
|
|
28
|
+
if (!resolvedTheme)
|
|
29
|
+
return;
|
|
30
|
+
return map[resolvedTheme];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/client.ts
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
useThemeValue,
|
|
37
|
+
useTheme,
|
|
38
|
+
ThemedImage,
|
|
39
|
+
ClientThemeProvider
|
|
40
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -55,30 +55,5 @@ type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
|
55
55
|
/** Set theme */
|
|
56
56
|
setTheme: (theme: Themes | "system" | ((current: Themes | "system" | undefined) => Themes | "system")) => void;
|
|
57
57
|
};
|
|
58
|
-
declare function
|
|
59
|
-
|
|
60
|
-
import { ReactElement as ReactElement2 } from "react";
|
|
61
|
-
declare function ThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, nonce, onThemeChange, themeColor, followSystem, initialTheme }: ThemeProviderProps<Themes>): ReactElement2;
|
|
62
|
-
import { ImgHTMLAttributes, ReactElement as ReactElement3 } from "react";
|
|
63
|
-
type ThemedImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt"> & {
|
|
64
|
-
/** Map of theme name to image source */
|
|
65
|
-
src: Record<string, string>;
|
|
66
|
-
/**
|
|
67
|
-
* Shown before the theme resolves on the client.
|
|
68
|
-
* Defaults to a transparent 1x1 GIF to avoid hydration mismatch.
|
|
69
|
-
*/
|
|
70
|
-
fallback?: string;
|
|
71
|
-
/** Alt text (required for accessibility) */
|
|
72
|
-
alt: string;
|
|
73
|
-
};
|
|
74
|
-
declare function ThemedImage({ src, fallback, alt,...props }: ThemedImageProps): ReactElement3;
|
|
75
|
-
/**
|
|
76
|
-
* Returns the value from the map that corresponds to the current resolved theme.
|
|
77
|
-
* Returns `undefined` if the theme hasn't resolved yet (e.g. during SSR).
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* const label = useThemeValue({ light: "Switch to dark", dark: "Switch to light" });
|
|
81
|
-
* const color = useThemeValue({ light: "#fff", dark: "#000", purple: "#1a0a2e" });
|
|
82
|
-
*/
|
|
83
|
-
declare function useThemeValue<T>(map: Record<string, T>): T | undefined;
|
|
84
|
-
export { useThemeValue, useTheme, ValueObject, ThemedImageProps, ThemedImage, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, ClientThemeProvider, Attribute };
|
|
58
|
+
declare function ThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, nonce, onThemeChange, themeColor, followSystem, initialTheme }: ThemeProviderProps<Themes>): ReactElement;
|
|
59
|
+
export { ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, Attribute };
|
package/dist/index.js
CHANGED
|
@@ -1,264 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// src/context.ts
|
|
6
|
-
import { createContext, useContext } from "react";
|
|
7
|
-
var ThemeContext = createContext(undefined);
|
|
8
|
-
function useTheme() {
|
|
9
|
-
const ctx = useContext(ThemeContext);
|
|
10
|
-
if (!ctx) {
|
|
11
|
-
throw new Error("useTheme must be used within a ThemeProvider");
|
|
12
|
-
}
|
|
13
|
-
return ctx;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// src/store.ts
|
|
17
|
-
var SERVER_SNAPSHOT = { theme: undefined, systemTheme: undefined };
|
|
18
|
-
function createThemeStore() {
|
|
19
|
-
let state = { theme: undefined, systemTheme: undefined };
|
|
20
|
-
const listeners = new Set;
|
|
21
|
-
function emit() {
|
|
22
|
-
for (const listener of listeners)
|
|
23
|
-
listener();
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
subscribe(listener) {
|
|
27
|
-
listeners.add(listener);
|
|
28
|
-
return () => {
|
|
29
|
-
listeners.delete(listener);
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
getSnapshot() {
|
|
33
|
-
return state;
|
|
34
|
-
},
|
|
35
|
-
getServerSnapshot() {
|
|
36
|
-
return SERVER_SNAPSHOT;
|
|
37
|
-
},
|
|
38
|
-
setTheme(theme) {
|
|
39
|
-
if (state.theme === theme)
|
|
40
|
-
return;
|
|
41
|
-
state = { ...state, theme };
|
|
42
|
-
emit();
|
|
43
|
-
},
|
|
44
|
-
setSystemTheme(systemTheme) {
|
|
45
|
-
if (state.systemTheme === systemTheme)
|
|
46
|
-
return;
|
|
47
|
-
state = { ...state, systemTheme };
|
|
48
|
-
emit();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// src/client-provider.tsx
|
|
54
|
-
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
1
|
+
import {
|
|
2
|
+
ClientThemeProvider
|
|
3
|
+
} from "./shared/chunk-ea7hc1e9.js";
|
|
55
4
|
|
|
56
|
-
var DEFAULT_THEMES = ["light", "dark"];
|
|
57
|
-
function resolveThemeColor(themeColor, resolved) {
|
|
58
|
-
if (typeof themeColor === "string")
|
|
59
|
-
return themeColor;
|
|
60
|
-
return themeColor[resolved];
|
|
61
|
-
}
|
|
62
|
-
function updateMetaThemeColor(color) {
|
|
63
|
-
if (!color)
|
|
64
|
-
return;
|
|
65
|
-
let meta = document.querySelector('meta[name="theme-color"]');
|
|
66
|
-
if (!meta) {
|
|
67
|
-
meta = document.createElement("meta");
|
|
68
|
-
meta.name = "theme-color";
|
|
69
|
-
document.head.appendChild(meta);
|
|
70
|
-
}
|
|
71
|
-
meta.content = color;
|
|
72
|
-
}
|
|
73
|
-
function ClientThemeProvider({
|
|
74
|
-
children,
|
|
75
|
-
themes = DEFAULT_THEMES,
|
|
76
|
-
forcedTheme,
|
|
77
|
-
enableSystem = true,
|
|
78
|
-
defaultTheme,
|
|
79
|
-
attribute = "class",
|
|
80
|
-
value: valueMap,
|
|
81
|
-
target = "html",
|
|
82
|
-
disableTransitionOnChange = false,
|
|
83
|
-
storage = "localStorage",
|
|
84
|
-
storageKey = "theme",
|
|
85
|
-
enableColorScheme = true,
|
|
86
|
-
themeColor,
|
|
87
|
-
followSystem = false,
|
|
88
|
-
onThemeChange,
|
|
89
|
-
initialTheme
|
|
90
|
-
}) {
|
|
91
|
-
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
92
|
-
const storeRef = useRef(createThemeStore());
|
|
93
|
-
const store = storeRef.current;
|
|
94
|
-
const { getSnapshot, setTheme: setStoreTheme, setSystemTheme: setStoreSystemTheme } = store;
|
|
95
|
-
const { theme, systemTheme } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
96
|
-
const resolvedTheme = forcedTheme ?? (theme === "system" || theme === undefined ? systemTheme : theme);
|
|
97
|
-
const onThemeChangeRef = useRef(onThemeChange);
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
onThemeChangeRef.current = onThemeChange;
|
|
100
|
-
});
|
|
101
|
-
const getTargetEl = useCallback(() => {
|
|
102
|
-
if (target === "html")
|
|
103
|
-
return document.documentElement;
|
|
104
|
-
if (target === "body")
|
|
105
|
-
return document.body;
|
|
106
|
-
return document.querySelector(target);
|
|
107
|
-
}, [target]);
|
|
108
|
-
const applyToDom = useCallback((resolved) => {
|
|
109
|
-
const el = getTargetEl();
|
|
110
|
-
if (!el)
|
|
111
|
-
return;
|
|
112
|
-
const attrValue = valueMap?.[resolved] ?? resolved;
|
|
113
|
-
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
114
|
-
if (disableTransitionOnChange) {
|
|
115
|
-
const style = document.createElement("style");
|
|
116
|
-
style.textContent = "*,*::before,*::after{transition:none!important}";
|
|
117
|
-
document.head.appendChild(style);
|
|
118
|
-
requestAnimationFrame(() => requestAnimationFrame(() => document.head.removeChild(style)));
|
|
119
|
-
}
|
|
120
|
-
for (const attr of attrs) {
|
|
121
|
-
if (attr === "class") {
|
|
122
|
-
const toRemove = themes.flatMap((t) => (valueMap?.[t] ?? t).split(" "));
|
|
123
|
-
el.classList.remove(...toRemove);
|
|
124
|
-
el.classList.add(...attrValue.split(" "));
|
|
125
|
-
} else {
|
|
126
|
-
el.setAttribute(attr, attrValue);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (enableColorScheme && (resolved === "light" || resolved === "dark")) {
|
|
130
|
-
el.style.colorScheme = resolved;
|
|
131
|
-
}
|
|
132
|
-
if (themeColor) {
|
|
133
|
-
updateMetaThemeColor(resolveThemeColor(themeColor, resolved));
|
|
134
|
-
}
|
|
135
|
-
}, [
|
|
136
|
-
attribute,
|
|
137
|
-
disableTransitionOnChange,
|
|
138
|
-
enableColorScheme,
|
|
139
|
-
getTargetEl,
|
|
140
|
-
themes,
|
|
141
|
-
valueMap,
|
|
142
|
-
themeColor
|
|
143
|
-
]);
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
const mq = enableSystem ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
146
|
-
const sys = mq ? mq.matches ? "dark" : "light" : undefined;
|
|
147
|
-
if (sys)
|
|
148
|
-
setStoreSystemTheme(sys);
|
|
149
|
-
if (forcedTheme) {
|
|
150
|
-
setStoreTheme(forcedTheme);
|
|
151
|
-
applyToDom(forcedTheme);
|
|
152
|
-
} else if (initialTheme) {
|
|
153
|
-
setStoreTheme(initialTheme);
|
|
154
|
-
applyToDom(initialTheme === "system" ? sys ?? "light" : initialTheme);
|
|
155
|
-
try {
|
|
156
|
-
if (storage !== "none") {
|
|
157
|
-
const s = storage === "localStorage" ? localStorage : sessionStorage;
|
|
158
|
-
s.setItem(storageKey, initialTheme);
|
|
159
|
-
}
|
|
160
|
-
} catch {}
|
|
161
|
-
} else {
|
|
162
|
-
let stored = null;
|
|
163
|
-
try {
|
|
164
|
-
if (storage !== "none") {
|
|
165
|
-
const s = storage === "localStorage" ? localStorage : sessionStorage;
|
|
166
|
-
stored = s.getItem(storageKey);
|
|
167
|
-
}
|
|
168
|
-
} catch {}
|
|
169
|
-
const initial = !followSystem && stored && themes.includes(stored) ? stored : resolvedDefault;
|
|
170
|
-
setStoreTheme(initial);
|
|
171
|
-
applyToDom(initial === "system" ? sys ?? "light" : initial);
|
|
172
|
-
}
|
|
173
|
-
if (!mq)
|
|
174
|
-
return;
|
|
175
|
-
const handler = (e) => {
|
|
176
|
-
const next = e.matches ? "dark" : "light";
|
|
177
|
-
setStoreSystemTheme(next);
|
|
178
|
-
const current = getSnapshot().theme;
|
|
179
|
-
if (current === "system" || current === undefined || followSystem) {
|
|
180
|
-
if (followSystem) {
|
|
181
|
-
setStoreTheme("system");
|
|
182
|
-
}
|
|
183
|
-
applyToDom(next);
|
|
184
|
-
onThemeChangeRef.current?.(next);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
mq.addEventListener("change", handler);
|
|
188
|
-
return () => mq.removeEventListener("change", handler);
|
|
189
|
-
}, [
|
|
190
|
-
forcedTheme,
|
|
191
|
-
initialTheme,
|
|
192
|
-
resolvedDefault,
|
|
193
|
-
storage,
|
|
194
|
-
storageKey,
|
|
195
|
-
themes,
|
|
196
|
-
enableSystem,
|
|
197
|
-
followSystem,
|
|
198
|
-
applyToDom,
|
|
199
|
-
getSnapshot,
|
|
200
|
-
setStoreTheme,
|
|
201
|
-
setStoreSystemTheme
|
|
202
|
-
]);
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
const handler = () => {
|
|
205
|
-
const { theme: theme2, systemTheme: systemTheme2 } = getSnapshot();
|
|
206
|
-
const resolved = forcedTheme ?? (theme2 === "system" || theme2 === undefined ? systemTheme2 : theme2);
|
|
207
|
-
if (resolved)
|
|
208
|
-
applyToDom(resolved);
|
|
209
|
-
};
|
|
210
|
-
window.addEventListener("pageshow", handler);
|
|
211
|
-
window.addEventListener("popstate", handler);
|
|
212
|
-
return () => {
|
|
213
|
-
window.removeEventListener("pageshow", handler);
|
|
214
|
-
window.removeEventListener("popstate", handler);
|
|
215
|
-
};
|
|
216
|
-
}, [applyToDom, forcedTheme, getSnapshot]);
|
|
217
|
-
useEffect(() => {
|
|
218
|
-
if (storage === "none" || storage === "sessionStorage")
|
|
219
|
-
return;
|
|
220
|
-
const handler = (e) => {
|
|
221
|
-
if (e.storageArea !== localStorage || e.key !== storageKey || !e.newValue)
|
|
222
|
-
return;
|
|
223
|
-
if (themes.includes(e.newValue)) {
|
|
224
|
-
const newTheme = e.newValue;
|
|
225
|
-
const resolved = newTheme === "system" ? getSnapshot().systemTheme ?? "light" : newTheme;
|
|
226
|
-
setStoreTheme(newTheme);
|
|
227
|
-
applyToDom(resolved);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
window.addEventListener("storage", handler);
|
|
231
|
-
return () => window.removeEventListener("storage", handler);
|
|
232
|
-
}, [storage, storageKey, themes, applyToDom, getSnapshot, setStoreTheme]);
|
|
233
|
-
const setTheme = useCallback((next) => {
|
|
234
|
-
if (forcedTheme)
|
|
235
|
-
return;
|
|
236
|
-
const current = getSnapshot().theme;
|
|
237
|
-
const newTheme = typeof next === "function" ? next(current) : next;
|
|
238
|
-
const resolved = newTheme === "system" ? getSnapshot().systemTheme ?? "light" : newTheme;
|
|
239
|
-
setStoreTheme(newTheme);
|
|
240
|
-
applyToDom(resolved);
|
|
241
|
-
onThemeChangeRef.current?.(resolved);
|
|
242
|
-
try {
|
|
243
|
-
if (storage !== "none") {
|
|
244
|
-
const store2 = storage === "localStorage" ? localStorage : sessionStorage;
|
|
245
|
-
store2.setItem(storageKey, newTheme);
|
|
246
|
-
}
|
|
247
|
-
} catch {}
|
|
248
|
-
}, [applyToDom, forcedTheme, storage, storageKey, getSnapshot, setStoreTheme]);
|
|
249
|
-
const contextValue = {
|
|
250
|
-
theme: forcedTheme ?? theme,
|
|
251
|
-
resolvedTheme,
|
|
252
|
-
systemTheme,
|
|
253
|
-
forcedTheme,
|
|
254
|
-
themes,
|
|
255
|
-
setTheme
|
|
256
|
-
};
|
|
257
|
-
return /* @__PURE__ */ jsxDEV(ThemeContext.Provider, {
|
|
258
|
-
value: contextValue,
|
|
259
|
-
children
|
|
260
|
-
}, undefined, false, undefined, this);
|
|
261
|
-
}
|
|
262
5
|
// src/script.ts
|
|
263
6
|
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage, themeColors, initialTheme) {
|
|
264
7
|
let theme;
|
|
@@ -329,11 +72,11 @@ function getScript(config) {
|
|
|
329
72
|
}
|
|
330
73
|
|
|
331
74
|
// src/provider.tsx
|
|
332
|
-
import { jsxDEV
|
|
333
|
-
var
|
|
75
|
+
import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
|
|
76
|
+
var DEFAULT_THEMES = ["light", "dark"];
|
|
334
77
|
function ThemeProvider({
|
|
335
78
|
children,
|
|
336
|
-
themes =
|
|
79
|
+
themes = DEFAULT_THEMES,
|
|
337
80
|
forcedTheme,
|
|
338
81
|
enableSystem = true,
|
|
339
82
|
defaultTheme,
|
|
@@ -351,9 +94,9 @@ function ThemeProvider({
|
|
|
351
94
|
initialTheme
|
|
352
95
|
}) {
|
|
353
96
|
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
354
|
-
return /* @__PURE__ */
|
|
97
|
+
return /* @__PURE__ */ jsxDEV(Fragment, {
|
|
355
98
|
children: [
|
|
356
|
-
/* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ jsxDEV("script", {
|
|
357
100
|
dangerouslySetInnerHTML: {
|
|
358
101
|
__html: getScript({
|
|
359
102
|
storageKey,
|
|
@@ -372,7 +115,7 @@ function ThemeProvider({
|
|
|
372
115
|
},
|
|
373
116
|
nonce
|
|
374
117
|
}, undefined, false, undefined, this),
|
|
375
|
-
/* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ jsxDEV(ClientThemeProvider, {
|
|
376
119
|
themes,
|
|
377
120
|
forcedTheme,
|
|
378
121
|
enableSystem,
|
|
@@ -393,36 +136,6 @@ function ThemeProvider({
|
|
|
393
136
|
]
|
|
394
137
|
}, undefined, true, undefined, this);
|
|
395
138
|
}
|
|
396
|
-
// src/themed-image.tsx
|
|
397
|
-
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
398
|
-
|
|
399
|
-
var TRANSPARENT_FALLBACK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
400
|
-
function ThemedImage({
|
|
401
|
-
src,
|
|
402
|
-
fallback = TRANSPARENT_FALLBACK,
|
|
403
|
-
alt,
|
|
404
|
-
...props
|
|
405
|
-
}) {
|
|
406
|
-
const { resolvedTheme } = useTheme();
|
|
407
|
-
const resolvedSrc = resolvedTheme && src[resolvedTheme] || fallback;
|
|
408
|
-
return /* @__PURE__ */ jsxDEV3("img", {
|
|
409
|
-
src: resolvedSrc,
|
|
410
|
-
alt,
|
|
411
|
-
...props
|
|
412
|
-
}, undefined, false, undefined, this);
|
|
413
|
-
}
|
|
414
|
-
// src/use-theme-value.ts
|
|
415
|
-
|
|
416
|
-
function useThemeValue(map) {
|
|
417
|
-
const { resolvedTheme } = useTheme();
|
|
418
|
-
if (!resolvedTheme)
|
|
419
|
-
return;
|
|
420
|
-
return map[resolvedTheme];
|
|
421
|
-
}
|
|
422
139
|
export {
|
|
423
|
-
|
|
424
|
-
useTheme,
|
|
425
|
-
ThemedImage,
|
|
426
|
-
ThemeProvider,
|
|
427
|
-
ClientThemeProvider
|
|
140
|
+
ThemeProvider
|
|
428
141
|
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// src/context.ts
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
var ThemeContext = createContext(undefined);
|
|
5
|
+
function useTheme() {
|
|
6
|
+
const ctx = useContext(ThemeContext);
|
|
7
|
+
if (!ctx) {
|
|
8
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
9
|
+
}
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/client-provider.tsx
|
|
14
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
15
|
+
|
|
16
|
+
// src/store.ts
|
|
17
|
+
var SERVER_SNAPSHOT = { theme: undefined, systemTheme: undefined };
|
|
18
|
+
function createThemeStore() {
|
|
19
|
+
let state = { theme: undefined, systemTheme: undefined };
|
|
20
|
+
const listeners = new Set;
|
|
21
|
+
function emit() {
|
|
22
|
+
for (const listener of listeners)
|
|
23
|
+
listener();
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
subscribe(listener) {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return () => {
|
|
29
|
+
listeners.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
getSnapshot() {
|
|
33
|
+
return state;
|
|
34
|
+
},
|
|
35
|
+
getServerSnapshot() {
|
|
36
|
+
return SERVER_SNAPSHOT;
|
|
37
|
+
},
|
|
38
|
+
setTheme(theme) {
|
|
39
|
+
if (state.theme === theme)
|
|
40
|
+
return;
|
|
41
|
+
state = { ...state, theme };
|
|
42
|
+
emit();
|
|
43
|
+
},
|
|
44
|
+
setSystemTheme(systemTheme) {
|
|
45
|
+
if (state.systemTheme === systemTheme)
|
|
46
|
+
return;
|
|
47
|
+
state = { ...state, systemTheme };
|
|
48
|
+
emit();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/client-provider.tsx
|
|
54
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
55
|
+
|
|
56
|
+
var DEFAULT_THEMES = ["light", "dark"];
|
|
57
|
+
function resolveThemeColor(themeColor, resolved) {
|
|
58
|
+
if (typeof themeColor === "string")
|
|
59
|
+
return themeColor;
|
|
60
|
+
return themeColor[resolved];
|
|
61
|
+
}
|
|
62
|
+
function updateMetaThemeColor(color) {
|
|
63
|
+
if (!color)
|
|
64
|
+
return;
|
|
65
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
66
|
+
if (!meta) {
|
|
67
|
+
meta = document.createElement("meta");
|
|
68
|
+
meta.name = "theme-color";
|
|
69
|
+
document.head.appendChild(meta);
|
|
70
|
+
}
|
|
71
|
+
meta.content = color;
|
|
72
|
+
}
|
|
73
|
+
function ClientThemeProvider({
|
|
74
|
+
children,
|
|
75
|
+
themes = DEFAULT_THEMES,
|
|
76
|
+
forcedTheme,
|
|
77
|
+
enableSystem = true,
|
|
78
|
+
defaultTheme,
|
|
79
|
+
attribute = "class",
|
|
80
|
+
value: valueMap,
|
|
81
|
+
target = "html",
|
|
82
|
+
disableTransitionOnChange = false,
|
|
83
|
+
storage = "localStorage",
|
|
84
|
+
storageKey = "theme",
|
|
85
|
+
enableColorScheme = true,
|
|
86
|
+
themeColor,
|
|
87
|
+
followSystem = false,
|
|
88
|
+
onThemeChange,
|
|
89
|
+
initialTheme
|
|
90
|
+
}) {
|
|
91
|
+
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
92
|
+
const storeRef = useRef(createThemeStore());
|
|
93
|
+
const store = storeRef.current;
|
|
94
|
+
const { getSnapshot, setTheme: setStoreTheme, setSystemTheme: setStoreSystemTheme } = store;
|
|
95
|
+
const { theme, systemTheme } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
96
|
+
const resolvedTheme = forcedTheme ?? (theme === "system" || theme === undefined ? systemTheme : theme);
|
|
97
|
+
const onThemeChangeRef = useRef(onThemeChange);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
onThemeChangeRef.current = onThemeChange;
|
|
100
|
+
});
|
|
101
|
+
const getTargetEl = useCallback(() => {
|
|
102
|
+
if (target === "html")
|
|
103
|
+
return document.documentElement;
|
|
104
|
+
if (target === "body")
|
|
105
|
+
return document.body;
|
|
106
|
+
return document.querySelector(target);
|
|
107
|
+
}, [target]);
|
|
108
|
+
const applyToDom = useCallback((resolved) => {
|
|
109
|
+
const el = getTargetEl();
|
|
110
|
+
if (!el)
|
|
111
|
+
return;
|
|
112
|
+
const attrValue = valueMap?.[resolved] ?? resolved;
|
|
113
|
+
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
114
|
+
if (disableTransitionOnChange) {
|
|
115
|
+
const style = document.createElement("style");
|
|
116
|
+
style.textContent = "*,*::before,*::after{transition:none!important}";
|
|
117
|
+
document.head.appendChild(style);
|
|
118
|
+
requestAnimationFrame(() => requestAnimationFrame(() => document.head.removeChild(style)));
|
|
119
|
+
}
|
|
120
|
+
for (const attr of attrs) {
|
|
121
|
+
if (attr === "class") {
|
|
122
|
+
const toRemove = themes.flatMap((t) => (valueMap?.[t] ?? t).split(" "));
|
|
123
|
+
el.classList.remove(...toRemove);
|
|
124
|
+
el.classList.add(...attrValue.split(" "));
|
|
125
|
+
} else {
|
|
126
|
+
el.setAttribute(attr, attrValue);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (enableColorScheme && (resolved === "light" || resolved === "dark")) {
|
|
130
|
+
el.style.colorScheme = resolved;
|
|
131
|
+
}
|
|
132
|
+
if (themeColor) {
|
|
133
|
+
updateMetaThemeColor(resolveThemeColor(themeColor, resolved));
|
|
134
|
+
}
|
|
135
|
+
}, [
|
|
136
|
+
attribute,
|
|
137
|
+
disableTransitionOnChange,
|
|
138
|
+
enableColorScheme,
|
|
139
|
+
getTargetEl,
|
|
140
|
+
themes,
|
|
141
|
+
valueMap,
|
|
142
|
+
themeColor
|
|
143
|
+
]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const mq = enableSystem ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
146
|
+
const sys = mq ? mq.matches ? "dark" : "light" : undefined;
|
|
147
|
+
if (sys)
|
|
148
|
+
setStoreSystemTheme(sys);
|
|
149
|
+
if (forcedTheme) {
|
|
150
|
+
setStoreTheme(forcedTheme);
|
|
151
|
+
applyToDom(forcedTheme);
|
|
152
|
+
} else if (initialTheme) {
|
|
153
|
+
setStoreTheme(initialTheme);
|
|
154
|
+
applyToDom(initialTheme === "system" ? sys ?? "light" : initialTheme);
|
|
155
|
+
try {
|
|
156
|
+
if (storage !== "none") {
|
|
157
|
+
const s = storage === "localStorage" ? localStorage : sessionStorage;
|
|
158
|
+
s.setItem(storageKey, initialTheme);
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
} else {
|
|
162
|
+
let stored = null;
|
|
163
|
+
try {
|
|
164
|
+
if (storage !== "none") {
|
|
165
|
+
const s = storage === "localStorage" ? localStorage : sessionStorage;
|
|
166
|
+
stored = s.getItem(storageKey);
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
const initial = !followSystem && stored && themes.includes(stored) ? stored : resolvedDefault;
|
|
170
|
+
setStoreTheme(initial);
|
|
171
|
+
applyToDom(initial === "system" ? sys ?? "light" : initial);
|
|
172
|
+
}
|
|
173
|
+
if (!mq)
|
|
174
|
+
return;
|
|
175
|
+
const handler = (e) => {
|
|
176
|
+
const next = e.matches ? "dark" : "light";
|
|
177
|
+
setStoreSystemTheme(next);
|
|
178
|
+
const current = getSnapshot().theme;
|
|
179
|
+
if (current === "system" || current === undefined || followSystem) {
|
|
180
|
+
if (followSystem) {
|
|
181
|
+
setStoreTheme("system");
|
|
182
|
+
}
|
|
183
|
+
applyToDom(next);
|
|
184
|
+
onThemeChangeRef.current?.(next);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
mq.addEventListener("change", handler);
|
|
188
|
+
return () => mq.removeEventListener("change", handler);
|
|
189
|
+
}, [
|
|
190
|
+
forcedTheme,
|
|
191
|
+
initialTheme,
|
|
192
|
+
resolvedDefault,
|
|
193
|
+
storage,
|
|
194
|
+
storageKey,
|
|
195
|
+
themes,
|
|
196
|
+
enableSystem,
|
|
197
|
+
followSystem,
|
|
198
|
+
applyToDom,
|
|
199
|
+
getSnapshot,
|
|
200
|
+
setStoreTheme,
|
|
201
|
+
setStoreSystemTheme
|
|
202
|
+
]);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const handler = () => {
|
|
205
|
+
const { theme: theme2, systemTheme: systemTheme2 } = getSnapshot();
|
|
206
|
+
const resolved = forcedTheme ?? (theme2 === "system" || theme2 === undefined ? systemTheme2 : theme2);
|
|
207
|
+
if (resolved)
|
|
208
|
+
applyToDom(resolved);
|
|
209
|
+
};
|
|
210
|
+
window.addEventListener("pageshow", handler);
|
|
211
|
+
window.addEventListener("popstate", handler);
|
|
212
|
+
return () => {
|
|
213
|
+
window.removeEventListener("pageshow", handler);
|
|
214
|
+
window.removeEventListener("popstate", handler);
|
|
215
|
+
};
|
|
216
|
+
}, [applyToDom, forcedTheme, getSnapshot]);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (storage === "none" || storage === "sessionStorage")
|
|
219
|
+
return;
|
|
220
|
+
const handler = (e) => {
|
|
221
|
+
if (e.storageArea !== localStorage || e.key !== storageKey || !e.newValue)
|
|
222
|
+
return;
|
|
223
|
+
if (themes.includes(e.newValue)) {
|
|
224
|
+
const newTheme = e.newValue;
|
|
225
|
+
const resolved = newTheme === "system" ? getSnapshot().systemTheme ?? "light" : newTheme;
|
|
226
|
+
setStoreTheme(newTheme);
|
|
227
|
+
applyToDom(resolved);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
window.addEventListener("storage", handler);
|
|
231
|
+
return () => window.removeEventListener("storage", handler);
|
|
232
|
+
}, [storage, storageKey, themes, applyToDom, getSnapshot, setStoreTheme]);
|
|
233
|
+
const setTheme = useCallback((next) => {
|
|
234
|
+
if (forcedTheme)
|
|
235
|
+
return;
|
|
236
|
+
const current = getSnapshot().theme;
|
|
237
|
+
const newTheme = typeof next === "function" ? next(current) : next;
|
|
238
|
+
const resolved = newTheme === "system" ? getSnapshot().systemTheme ?? "light" : newTheme;
|
|
239
|
+
setStoreTheme(newTheme);
|
|
240
|
+
applyToDom(resolved);
|
|
241
|
+
onThemeChangeRef.current?.(resolved);
|
|
242
|
+
try {
|
|
243
|
+
if (storage !== "none") {
|
|
244
|
+
const store2 = storage === "localStorage" ? localStorage : sessionStorage;
|
|
245
|
+
store2.setItem(storageKey, newTheme);
|
|
246
|
+
}
|
|
247
|
+
} catch {}
|
|
248
|
+
}, [applyToDom, forcedTheme, storage, storageKey, getSnapshot, setStoreTheme]);
|
|
249
|
+
const contextValue = {
|
|
250
|
+
theme: forcedTheme ?? theme,
|
|
251
|
+
resolvedTheme,
|
|
252
|
+
systemTheme,
|
|
253
|
+
forcedTheme,
|
|
254
|
+
themes,
|
|
255
|
+
setTheme
|
|
256
|
+
};
|
|
257
|
+
return /* @__PURE__ */ jsxDEV(ThemeContext.Provider, {
|
|
258
|
+
value: contextValue,
|
|
259
|
+
children
|
|
260
|
+
}, undefined, false, undefined, this);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { useTheme, ClientThemeProvider };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrksz/themes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A modern, fully-featured theme management library for Next.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "bunup",
|
|
15
|
-
"dev": "bunup --watch",
|
|
14
|
+
"build": "bunup src/index.ts src/client.ts",
|
|
15
|
+
"dev": "bunup src/index.ts src/client.ts --watch",
|
|
16
16
|
"type-check": "tsc --noEmit",
|
|
17
17
|
"lint": "biome check src",
|
|
18
18
|
"lint:fix": "biome check --write src",
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
"default": "./dist/index.js"
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
|
+
"./client": {
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./dist/client.d.ts",
|
|
57
|
+
"default": "./dist/client.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
54
60
|
"./styles.css": "./dist/index.css",
|
|
55
61
|
"./package.json": "./package.json"
|
|
56
62
|
},
|