@wrksz/themes 0.4.0 → 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 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();
@@ -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 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 { 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
- "use client";
2
- // src/client-provider.tsx
3
- import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
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 as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
333
- var DEFAULT_THEMES2 = ["light", "dark"];
75
+ import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
76
+ var DEFAULT_THEMES = ["light", "dark"];
334
77
  function ThemeProvider({
335
78
  children,
336
- themes = DEFAULT_THEMES2,
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__ */ jsxDEV2(Fragment, {
97
+ return /* @__PURE__ */ jsxDEV(Fragment, {
355
98
  children: [
356
- /* @__PURE__ */ jsxDEV2("script", {
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__ */ jsxDEV2(ClientThemeProvider, {
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
- useThemeValue,
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.4.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
  },
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jakub Warkusz
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.