@wrksz/themes 0.3.0 → 0.3.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 +59 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +31 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ export function ThemeToggle() {
|
|
|
58
58
|
| `themes` | `string[]` | `["light", "dark"]` | Available themes |
|
|
59
59
|
| `defaultTheme` | `string` | `"system"` | Theme used when no preference is stored |
|
|
60
60
|
| `forcedTheme` | `string` | - | Force a specific theme, ignoring user preference |
|
|
61
|
+
| `initialTheme` | `string` | - | Server-provided theme that overrides storage on mount. User can still call `setTheme` to change it |
|
|
61
62
|
| `enableSystem` | `boolean` | `true` | Detect system preference via `prefers-color-scheme` |
|
|
62
63
|
| `enableColorScheme` | `boolean` | `true` | Set native `color-scheme` CSS property |
|
|
63
64
|
| `attribute` | `string \| string[]` | `"class"` | HTML attribute(s) to set on target element (`"class"`, `"data-theme"`, etc.) |
|
|
@@ -132,16 +133,27 @@ const { theme, setTheme } = useTheme<AppTheme>();
|
|
|
132
133
|
</ThemeProvider>
|
|
133
134
|
```
|
|
134
135
|
|
|
135
|
-
###
|
|
136
|
+
### Multiple classes per theme
|
|
137
|
+
|
|
138
|
+
Map a theme to multiple CSS classes by using a space-separated value:
|
|
136
139
|
|
|
137
140
|
```tsx
|
|
138
141
|
<ThemeProvider
|
|
139
|
-
|
|
142
|
+
themes={["light", "dark", "dim"]}
|
|
143
|
+
value={{ light: "light", dark: "dark high-contrast", dim: "dark dim" }}
|
|
140
144
|
>
|
|
141
145
|
{children}
|
|
142
146
|
</ThemeProvider>
|
|
143
147
|
```
|
|
144
148
|
|
|
149
|
+
### Meta theme-color (Safari / PWA)
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<ThemeProvider themeColor={{ light: "#ffffff", dark: "#0a0a0a" }}>
|
|
153
|
+
{children}
|
|
154
|
+
</ThemeProvider>
|
|
155
|
+
```
|
|
156
|
+
|
|
145
157
|
Works with CSS variables too:
|
|
146
158
|
|
|
147
159
|
```tsx
|
|
@@ -168,6 +180,48 @@ Works with CSS variables too:
|
|
|
168
180
|
</ThemeProvider>
|
|
169
181
|
```
|
|
170
182
|
|
|
183
|
+
### Server-provided theme
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// app/layout.tsx (server component)
|
|
189
|
+
export default async function RootLayout({ children }) {
|
|
190
|
+
const userTheme = await getUserTheme(); // "light" | "dark" | null
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<html lang="en" suppressHydrationWarning>
|
|
194
|
+
<body>
|
|
195
|
+
<ThemeProvider
|
|
196
|
+
initialTheme={userTheme ?? undefined}
|
|
197
|
+
onThemeChange={saveUserTheme}
|
|
198
|
+
>
|
|
199
|
+
{children}
|
|
200
|
+
</ThemeProvider>
|
|
201
|
+
</body>
|
|
202
|
+
</html>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Nested provider in a Client Component
|
|
208
|
+
|
|
209
|
+
`ThemeProvider` renders an inline `<script>` and must be used in a Server Component. For nested providers inside Client Components, use `ClientThemeProvider` instead:
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
"use client";
|
|
213
|
+
|
|
214
|
+
import { ClientThemeProvider } from "@wrksz/themes";
|
|
215
|
+
|
|
216
|
+
export function AdminShell({ children }: { children: React.ReactNode }) {
|
|
217
|
+
return (
|
|
218
|
+
<ClientThemeProvider forcedTheme="dark">
|
|
219
|
+
{children}
|
|
220
|
+
</ClientThemeProvider>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
171
225
|
### Class on body instead of html
|
|
172
226
|
|
|
173
227
|
```tsx
|
|
@@ -183,9 +237,12 @@ Works with CSS variables too:
|
|
|
183
237
|
| React 19 script warning | Yes | Fixed (RSC split) |
|
|
184
238
|
| `__name` minification bug | Yes | Fixed |
|
|
185
239
|
| React 19 Activity/cacheComponents stale theme | Yes | Fixed (`useSyncExternalStore`) |
|
|
240
|
+
| Multiple classes per theme | No | Yes (`value` map with spaces) |
|
|
241
|
+
| Nested providers | No | Yes (per-instance store) |
|
|
186
242
|
| `sessionStorage` support | No | Yes |
|
|
187
243
|
| Disable storage | No | Yes (`storage: "none"`) |
|
|
188
244
|
| `meta theme-color` support | No | Yes (`themeColor` prop) |
|
|
245
|
+
| Server-provided theme | No | Yes (`initialTheme` prop) |
|
|
189
246
|
| Generic types | No | Yes (`useTheme<AppTheme>()`) |
|
|
190
247
|
| Zero runtime dependencies | Yes | Yes |
|
|
191
248
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
1
2
|
import { ReactNode } from "react";
|
|
2
3
|
type DefaultTheme = "light" | "dark" | "system";
|
|
3
4
|
type Attribute = "class" | `data-${string}`;
|
|
@@ -37,6 +38,8 @@ type ThemeProviderProps<Themes extends string = DefaultTheme> = {
|
|
|
37
38
|
themeColor?: ThemeColor;
|
|
38
39
|
/** Always follow system preference changes, even after setTheme was called */
|
|
39
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";
|
|
40
43
|
};
|
|
41
44
|
type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
42
45
|
/** Current theme (may be "system") */
|
|
@@ -52,7 +55,8 @@ type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
|
52
55
|
/** Set theme */
|
|
53
56
|
setTheme: (theme: Themes | "system" | ((current: Themes | "system" | undefined) => Themes | "system")) => void;
|
|
54
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;
|
|
55
59
|
declare function useTheme<Themes extends string = DefaultTheme>(): ThemeContextValue<Themes>;
|
|
56
|
-
import { ReactElement } from "react";
|
|
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>):
|
|
58
|
-
export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, Attribute };
|
|
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
|
+
export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, ClientThemeProvider, Attribute };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
// src/client-provider.tsx
|
|
3
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
|
|
2
5
|
// src/context.ts
|
|
3
6
|
import { createContext, useContext } from "react";
|
|
4
7
|
var ThemeContext = createContext(undefined);
|
|
@@ -9,8 +12,6 @@ function useTheme() {
|
|
|
9
12
|
}
|
|
10
13
|
return ctx;
|
|
11
14
|
}
|
|
12
|
-
// src/client-provider.tsx
|
|
13
|
-
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
14
15
|
|
|
15
16
|
// src/store.ts
|
|
16
17
|
var SERVER_SNAPSHOT = { theme: undefined, systemTheme: undefined };
|
|
@@ -84,7 +85,8 @@ function ClientThemeProvider({
|
|
|
84
85
|
enableColorScheme = true,
|
|
85
86
|
themeColor,
|
|
86
87
|
followSystem = false,
|
|
87
|
-
onThemeChange
|
|
88
|
+
onThemeChange,
|
|
89
|
+
initialTheme
|
|
88
90
|
}) {
|
|
89
91
|
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
90
92
|
const storeRef = useRef(createThemeStore());
|
|
@@ -117,9 +119,9 @@ function ClientThemeProvider({
|
|
|
117
119
|
}
|
|
118
120
|
for (const attr of attrs) {
|
|
119
121
|
if (attr === "class") {
|
|
120
|
-
const toRemove = themes.
|
|
122
|
+
const toRemove = themes.flatMap((t) => (valueMap?.[t] ?? t).split(" "));
|
|
121
123
|
el.classList.remove(...toRemove);
|
|
122
|
-
el.classList.add(attrValue);
|
|
124
|
+
el.classList.add(...attrValue.split(" "));
|
|
123
125
|
} else {
|
|
124
126
|
el.setAttribute(attr, attrValue);
|
|
125
127
|
}
|
|
@@ -147,6 +149,15 @@ function ClientThemeProvider({
|
|
|
147
149
|
if (forcedTheme) {
|
|
148
150
|
setStoreTheme(forcedTheme);
|
|
149
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 {}
|
|
150
161
|
} else {
|
|
151
162
|
let stored = null;
|
|
152
163
|
try {
|
|
@@ -177,6 +188,7 @@ function ClientThemeProvider({
|
|
|
177
188
|
return () => mq.removeEventListener("change", handler);
|
|
178
189
|
}, [
|
|
179
190
|
forcedTheme,
|
|
191
|
+
initialTheme,
|
|
180
192
|
resolvedDefault,
|
|
181
193
|
storage,
|
|
182
194
|
storageKey,
|
|
@@ -247,12 +259,13 @@ function ClientThemeProvider({
|
|
|
247
259
|
children
|
|
248
260
|
}, undefined, false, undefined, this);
|
|
249
261
|
}
|
|
250
|
-
|
|
251
262
|
// src/script.ts
|
|
252
|
-
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage, themeColors) {
|
|
263
|
+
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage, themeColors, initialTheme) {
|
|
253
264
|
let theme;
|
|
254
265
|
if (forcedTheme) {
|
|
255
266
|
theme = forcedTheme;
|
|
267
|
+
} else if (initialTheme && themes.includes(initialTheme)) {
|
|
268
|
+
theme = initialTheme;
|
|
256
269
|
} else {
|
|
257
270
|
let stored = null;
|
|
258
271
|
try {
|
|
@@ -273,9 +286,9 @@ function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableCo
|
|
|
273
286
|
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
274
287
|
for (const attr of attrs) {
|
|
275
288
|
if (attr === "class") {
|
|
276
|
-
const toRemove = themes.
|
|
289
|
+
const toRemove = themes.flatMap((t) => (value?.[t] || t).split(" "));
|
|
277
290
|
el.classList.remove(...toRemove);
|
|
278
|
-
el.classList.add(attrValue);
|
|
291
|
+
el.classList.add(...attrValue.split(" "));
|
|
279
292
|
} else {
|
|
280
293
|
el.setAttribute(attr, attrValue);
|
|
281
294
|
}
|
|
@@ -309,7 +322,8 @@ function getScript(config) {
|
|
|
309
322
|
JSON.stringify(config.value ?? null),
|
|
310
323
|
JSON.stringify(config.target),
|
|
311
324
|
JSON.stringify(config.storage),
|
|
312
|
-
JSON.stringify(config.themeColors ?? null)
|
|
325
|
+
JSON.stringify(config.themeColors ?? null),
|
|
326
|
+
JSON.stringify(config.initialTheme ?? null)
|
|
313
327
|
].join(",");
|
|
314
328
|
return `(${fn})(${args})`;
|
|
315
329
|
}
|
|
@@ -333,7 +347,8 @@ function ThemeProvider({
|
|
|
333
347
|
nonce,
|
|
334
348
|
onThemeChange,
|
|
335
349
|
themeColor,
|
|
336
|
-
followSystem = false
|
|
350
|
+
followSystem = false,
|
|
351
|
+
initialTheme
|
|
337
352
|
}) {
|
|
338
353
|
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
339
354
|
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
@@ -351,7 +366,8 @@ function ThemeProvider({
|
|
|
351
366
|
value: valueMap,
|
|
352
367
|
target,
|
|
353
368
|
storage,
|
|
354
|
-
themeColors: themeColor
|
|
369
|
+
themeColors: themeColor,
|
|
370
|
+
initialTheme
|
|
355
371
|
})
|
|
356
372
|
},
|
|
357
373
|
nonce
|
|
@@ -371,6 +387,7 @@ function ThemeProvider({
|
|
|
371
387
|
themeColor,
|
|
372
388
|
followSystem,
|
|
373
389
|
onThemeChange,
|
|
390
|
+
initialTheme,
|
|
374
391
|
children
|
|
375
392
|
}, undefined, false, undefined, this)
|
|
376
393
|
]
|
|
@@ -378,5 +395,6 @@ function ThemeProvider({
|
|
|
378
395
|
}
|
|
379
396
|
export {
|
|
380
397
|
useTheme,
|
|
381
|
-
ThemeProvider
|
|
398
|
+
ThemeProvider,
|
|
399
|
+
ClientThemeProvider
|
|
382
400
|
};
|