@wrksz/themes 0.3.1 → 0.4.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/README.md CHANGED
@@ -67,9 +67,9 @@ export function ThemeToggle() {
67
67
  | `storageKey` | `string` | `"theme"` | Key used for storage |
68
68
  | `storage` | `"localStorage" \| "sessionStorage" \| "none"` | `"localStorage"` | Where to persist the theme |
69
69
  | `disableTransitionOnChange` | `boolean` | `false` | Disable CSS transitions when switching themes |
70
- | `followSystem` | `boolean` | `false` | Always follow system preference changes, even after `setTheme` was called |
70
+ | `followSystem` | `boolean` | `false` | Always follow system preference changes, even after `setTheme` was called. Also ignores stored value on mount in favor of current system preference |
71
71
  | `themeColor` | `string \| Record<string, string>` | - | Update `<meta name="theme-color">` on theme change |
72
- | `nonce` | `string` | - | CSP nonce for the inline script |
72
+ | `nonce` | `string` | - | CSP nonce for the inline script (`ThemeProvider` only - `ClientThemeProvider` renders no script) |
73
73
  | `onThemeChange` | `(theme: string) => void` | - | Called whenever the resolved theme changes |
74
74
 
75
75
  ### `useTheme`
@@ -180,6 +180,38 @@ Works with CSS variables too:
180
180
  </ThemeProvider>
181
181
  ```
182
182
 
183
+ ### Different theme per section (scoped theming)
184
+
185
+ Apply the theme to a specific element instead of `<html>` using the `target` prop. This lets different sections of your app have independent themes simultaneously.
186
+
187
+ ```tsx
188
+ // app/landing/layout.tsx
189
+ export default function LandingLayout({ children }) {
190
+ return (
191
+ <ThemeProvider forcedTheme="dark" target="#landing-root" storage="none">
192
+ <div id="landing-root">{children}</div>
193
+ </ThemeProvider>
194
+ );
195
+ }
196
+
197
+ // app/dashboard/layout.tsx
198
+ export default function DashboardLayout({ children }) {
199
+ return (
200
+ <ThemeProvider forcedTheme="light" target="#dashboard-root" storage="none">
201
+ <div id="dashboard-root">{children}</div>
202
+ </ThemeProvider>
203
+ );
204
+ }
205
+ ```
206
+
207
+ ```css
208
+ /* scope your CSS variables to the target element */
209
+ #landing-root { --bg: #0a0a0a; --fg: #fafafa; }
210
+ #dashboard-root { --bg: #ffffff; --fg: #0a0a0a; }
211
+ ```
212
+
213
+ Use `storage="none"` when the theme is forced - there's nothing to persist.
214
+
183
215
  ### Server-provided theme
184
216
 
185
217
  Use `initialTheme` to initialize from a server-side source (database, session, cookie) on every mount, overriding any locally stored value. The user can still call `setTheme` to change it - use `onThemeChange` to persist the change back.
@@ -222,6 +254,79 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
222
254
  }
223
255
  ```
224
256
 
257
+ ### `useThemeValue`
258
+
259
+ Returns the value from a map that matches the current resolved theme. Returns `undefined` before the theme resolves on the client.
260
+
261
+ ```tsx
262
+ "use client";
263
+
264
+ import { useThemeValue } from "@wrksz/themes";
265
+
266
+ // strings
267
+ const label = useThemeValue({ light: "Switch to dark", dark: "Switch to light" });
268
+
269
+ // CSS values
270
+ const bg = useThemeValue({ light: "#ffffff", dark: "#0a0a0a", purple: "#6633ff" });
271
+
272
+ // any type
273
+ const icon = useThemeValue({ light: <SunIcon />, dark: <MoonIcon /> });
274
+ ```
275
+
276
+ ### Theme-aware images
277
+
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
+
280
+ ```tsx
281
+ import { ThemedImage } from "@wrksz/themes";
282
+
283
+ <ThemedImage
284
+ src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
285
+ alt="Logo"
286
+ width={200}
287
+ height={50}
288
+ />
289
+ ```
290
+
291
+ Works with any custom themes too:
292
+
293
+ ```tsx
294
+ <ThemedImage
295
+ src={{
296
+ light: "/logo-light.png",
297
+ dark: "/logo-dark.png",
298
+ purple: "/logo-purple.png",
299
+ }}
300
+ alt="Logo"
301
+ width={200}
302
+ height={50}
303
+ />
304
+ ```
305
+
306
+ For custom themes or `next/image`, use `resolvedTheme` directly with a fallback:
307
+
308
+ ```tsx
309
+ "use client";
310
+
311
+ import Image from "next/image";
312
+ import { useTheme } from "@wrksz/themes";
313
+
314
+ export function Logo() {
315
+ const { resolvedTheme } = useTheme();
316
+
317
+ return (
318
+ <Image
319
+ src={resolvedTheme === "dark" ? "/logo-dark.png" : "/logo-light.png"}
320
+ alt="Logo"
321
+ width={200}
322
+ height={50}
323
+ // avoids layout shift while theme is resolving
324
+ style={{ visibility: resolvedTheme ? "visible" : "hidden" }}
325
+ />
326
+ );
327
+ }
328
+ ```
329
+
225
330
  ### Class on body instead of html
226
331
 
227
332
  ```tsx
package/dist/index.d.ts CHANGED
@@ -59,4 +59,26 @@ declare function ClientThemeProvider<Themes extends string = DefaultTheme>({ chi
59
59
  declare function useTheme<Themes extends string = DefaultTheme>(): ThemeContextValue<Themes>;
60
60
  import { ReactElement as ReactElement2 } from "react";
61
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
- export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, ClientThemeProvider, Attribute };
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 };
package/dist/index.js CHANGED
@@ -166,7 +166,7 @@ function ClientThemeProvider({
166
166
  stored = s.getItem(storageKey);
167
167
  }
168
168
  } catch {}
169
- const initial = stored && themes.includes(stored) ? stored : resolvedDefault;
169
+ const initial = !followSystem && stored && themes.includes(stored) ? stored : resolvedDefault;
170
170
  setStoreTheme(initial);
171
171
  applyToDom(initial === "system" ? sys ?? "light" : initial);
172
172
  }
@@ -215,10 +215,10 @@ function ClientThemeProvider({
215
215
  };
216
216
  }, [applyToDom, forcedTheme, getSnapshot]);
217
217
  useEffect(() => {
218
- if (storage === "none")
218
+ if (storage === "none" || storage === "sessionStorage")
219
219
  return;
220
220
  const handler = (e) => {
221
- if (e.key !== storageKey || !e.newValue)
221
+ if (e.storageArea !== localStorage || e.key !== storageKey || !e.newValue)
222
222
  return;
223
223
  if (themes.includes(e.newValue)) {
224
224
  const newTheme = e.newValue;
@@ -393,8 +393,36 @@ function ThemeProvider({
393
393
  ]
394
394
  }, undefined, true, undefined, this);
395
395
  }
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
+ }
396
422
  export {
423
+ useThemeValue,
397
424
  useTheme,
425
+ ThemedImage,
398
426
  ThemeProvider,
399
427
  ClientThemeProvider
400
428
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrksz/themes",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "A modern, fully-featured theme management library for Next.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "^2.4.8",
25
+ "@testing-library/react": "^16.3.2",
25
26
  "@types/bun": "^1.3.11",
26
27
  "@types/react": "^19.2.14",
27
28
  "@types/react-dom": "^19.2.3",
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.