@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 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
- ### Meta theme-color (Safari / PWA)
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
- themeColor={{ light: "#ffffff", dark: "#0a0a0a" }}
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>): ReactElement;
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.map((t) => valueMap?.[t] ?? t);
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.map((t) => value?.[t] || t);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrksz/themes",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A modern, fully-featured theme management library for Next.js",
5
5
  "repository": {
6
6
  "type": "git",