@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 +107 -2
- package/dist/index.d.ts +23 -1
- package/dist/index.js +31 -3
- package/package.json +2 -1
- package/LICENSE +0 -21
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
|
-
|
|
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
|
+
"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.
|