@wrksz/themes 0.1.0 → 0.2.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 +193 -1
- package/dist/index.d.ts +56 -2
- package/dist/index.js +338 -7
- package/package.json +6 -2
- package/dist/index.css +0 -29
package/README.md
CHANGED
|
@@ -1 +1,193 @@
|
|
|
1
|
-
|
|
1
|
+
<img src=".github/images/banner.png" alt="@wrksz/themes" width="100%" />
|
|
2
|
+
|
|
3
|
+
# @wrksz/themes
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@wrksz/themes)
|
|
6
|
+
|
|
7
|
+
Modern theme management for Next.js 16+ and React 19+. Drop-in replacement for `next-themes` - fixes every known bug and adds missing features.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @wrksz/themes
|
|
11
|
+
# or
|
|
12
|
+
npm install @wrksz/themes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Add the provider to your root layout. Add `suppressHydrationWarning` to `<html>` to prevent hydration warnings caused by the inline theme script running before React hydrates.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/layout.tsx
|
|
21
|
+
import { ThemeProvider } from "@wrksz/themes";
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
24
|
+
return (
|
|
25
|
+
<html lang="en" suppressHydrationWarning>
|
|
26
|
+
<body>
|
|
27
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
"use client";
|
|
38
|
+
|
|
39
|
+
import { useTheme } from "@wrksz/themes";
|
|
40
|
+
|
|
41
|
+
export function ThemeToggle() {
|
|
42
|
+
const { theme, setTheme } = useTheme();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
|
46
|
+
Toggle theme
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `ThemeProvider`
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
|------|------|---------|-------------|
|
|
58
|
+
| `themes` | `string[]` | `["light", "dark"]` | Available themes |
|
|
59
|
+
| `defaultTheme` | `string` | `"system"` | Theme used when no preference is stored |
|
|
60
|
+
| `forcedTheme` | `string` | - | Force a specific theme, ignoring user preference |
|
|
61
|
+
| `enableSystem` | `boolean` | `true` | Detect system preference via `prefers-color-scheme` |
|
|
62
|
+
| `enableColorScheme` | `boolean` | `true` | Set native `color-scheme` CSS property |
|
|
63
|
+
| `attribute` | `string \| string[]` | `"class"` | HTML attribute(s) to set on target element (`"class"`, `"data-theme"`, etc.) |
|
|
64
|
+
| `value` | `Record<string, string>` | - | Map theme names to attribute values |
|
|
65
|
+
| `target` | `string` | `"html"` | Element to apply theme to (`"html"`, `"body"`, or a CSS selector) |
|
|
66
|
+
| `storageKey` | `string` | `"theme"` | Key used for storage |
|
|
67
|
+
| `storage` | `"localStorage" \| "sessionStorage" \| "none"` | `"localStorage"` | Where to persist the theme |
|
|
68
|
+
| `disableTransitionOnChange` | `boolean` | `false` | Disable CSS transitions when switching themes |
|
|
69
|
+
| `themeColor` | `string \| Record<string, string>` | - | Update `<meta name="theme-color">` on theme change |
|
|
70
|
+
| `nonce` | `string` | - | CSP nonce for the inline script |
|
|
71
|
+
| `onThemeChange` | `(theme: string) => void` | - | Called whenever the resolved theme changes |
|
|
72
|
+
|
|
73
|
+
### `useTheme`
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
const {
|
|
77
|
+
theme, // Current theme - may be "system"
|
|
78
|
+
resolvedTheme, // Actual theme - never "system"
|
|
79
|
+
systemTheme, // System preference: "light" | "dark" | undefined
|
|
80
|
+
forcedTheme, // Forced theme if set
|
|
81
|
+
themes, // Available themes
|
|
82
|
+
setTheme, // Set theme
|
|
83
|
+
} = useTheme();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Supports generics for full type safety:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
type AppTheme = "light" | "dark" | "high-contrast";
|
|
90
|
+
|
|
91
|
+
const { theme, setTheme } = useTheme<AppTheme>();
|
|
92
|
+
// theme: AppTheme | "system" | undefined
|
|
93
|
+
// setTheme: (theme: AppTheme | "system") => void
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
### Custom themes with Tailwind
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<ThemeProvider
|
|
102
|
+
themes={["light", "dark", "high-contrast"]}
|
|
103
|
+
attribute="class"
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</ThemeProvider>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Data attribute instead of class
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<ThemeProvider attribute="data-theme">
|
|
113
|
+
{children}
|
|
114
|
+
</ThemeProvider>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
[data-theme="dark"] { --bg: #000; }
|
|
119
|
+
[data-theme="light"] { --bg: #fff; }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Custom attribute values
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<ThemeProvider
|
|
126
|
+
themes={["light", "dark"]}
|
|
127
|
+
attribute="data-mode"
|
|
128
|
+
value={{ light: "light-mode", dark: "dark-mode" }}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
</ThemeProvider>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Meta theme-color (Safari / PWA)
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<ThemeProvider
|
|
138
|
+
themeColor={{ light: "#ffffff", dark: "#0a0a0a" }}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</ThemeProvider>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Works with CSS variables too:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<ThemeProvider themeColor="var(--color-background)">
|
|
148
|
+
{children}
|
|
149
|
+
</ThemeProvider>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Disable storage
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// No persistence - always uses defaultTheme or system preference
|
|
156
|
+
<ThemeProvider storage="none" defaultTheme="dark">
|
|
157
|
+
{children}
|
|
158
|
+
</ThemeProvider>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Forced theme per page
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// app/dashboard/layout.tsx
|
|
165
|
+
<ThemeProvider forcedTheme="dark">
|
|
166
|
+
{children}
|
|
167
|
+
</ThemeProvider>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Class on body instead of html
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<ThemeProvider target="body">
|
|
174
|
+
{children}
|
|
175
|
+
</ThemeProvider>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Why not `next-themes`?
|
|
179
|
+
|
|
180
|
+
| Issue | next-themes | @wrksz/themes |
|
|
181
|
+
|-------|-------------|---------------|
|
|
182
|
+
| React 19 script warning | Yes | Fixed (RSC split) |
|
|
183
|
+
| `__name` minification bug | Yes | Fixed |
|
|
184
|
+
| React 19 Activity/cacheComponents stale theme | Yes | Fixed (`useSyncExternalStore`) |
|
|
185
|
+
| `sessionStorage` support | No | Yes |
|
|
186
|
+
| Disable storage | No | Yes (`storage: "none"`) |
|
|
187
|
+
| `meta theme-color` support | No | Yes (`themeColor` prop) |
|
|
188
|
+
| Generic types | No | Yes (`useTheme<AppTheme>()`) |
|
|
189
|
+
| Zero runtime dependencies | Yes | Yes |
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
type DefaultTheme = "light" | "dark" | "system";
|
|
3
|
+
type Attribute = "class" | `data-${string}`;
|
|
4
|
+
type ValueObject = Record<string, string>;
|
|
5
|
+
type StorageType = "localStorage" | "sessionStorage" | "none";
|
|
6
|
+
/** Per-theme colors for meta theme-color, or a single string for all themes */
|
|
7
|
+
type ThemeColor = string | Partial<Record<string, string>>;
|
|
8
|
+
type ThemeProviderProps<Themes extends string = DefaultTheme> = {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/** All available themes */
|
|
11
|
+
themes?: Themes[];
|
|
12
|
+
/** Forced theme, overrides everything */
|
|
13
|
+
forcedTheme?: Themes;
|
|
14
|
+
/** Enable system preference via prefers-color-scheme */
|
|
15
|
+
enableSystem?: boolean;
|
|
16
|
+
/** Default theme when no preference stored */
|
|
17
|
+
defaultTheme?: Themes | "system";
|
|
18
|
+
/** HTML attribute(s) to set on target element */
|
|
19
|
+
attribute?: Attribute | Attribute[];
|
|
20
|
+
/** Map theme name to attribute value */
|
|
21
|
+
value?: ValueObject;
|
|
22
|
+
/** Target element to apply theme to, defaults to <html> */
|
|
23
|
+
target?: "html" | "body" | string;
|
|
24
|
+
/** Disable CSS transitions on theme change */
|
|
25
|
+
disableTransitionOnChange?: boolean;
|
|
26
|
+
/** Where to persist theme */
|
|
27
|
+
storage?: StorageType;
|
|
28
|
+
/** Storage key */
|
|
29
|
+
storageKey?: string;
|
|
30
|
+
/** Set native color-scheme CSS property */
|
|
31
|
+
enableColorScheme?: boolean;
|
|
32
|
+
/** Nonce for CSP */
|
|
33
|
+
nonce?: string;
|
|
34
|
+
/** Called when theme changes */
|
|
35
|
+
onThemeChange?: (theme: Themes) => void;
|
|
36
|
+
/** Colors for meta theme-color tag, per theme or a single value */
|
|
37
|
+
themeColor?: ThemeColor;
|
|
38
|
+
};
|
|
39
|
+
type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
40
|
+
/** Current theme (may be "system") */
|
|
41
|
+
theme: Themes | "system" | undefined;
|
|
42
|
+
/** Resolved theme - never "system" */
|
|
43
|
+
resolvedTheme: Themes | undefined;
|
|
44
|
+
/** System preference */
|
|
45
|
+
systemTheme: "light" | "dark" | undefined;
|
|
46
|
+
/** Forced theme if set */
|
|
47
|
+
forcedTheme: Themes | undefined;
|
|
48
|
+
/** All available themes */
|
|
49
|
+
themes: Themes[];
|
|
50
|
+
/** Set theme */
|
|
51
|
+
setTheme: (theme: Themes | "system" | ((current: Themes | "system" | undefined) => Themes | "system")) => void;
|
|
52
|
+
};
|
|
53
|
+
declare function useTheme<Themes extends string = DefaultTheme>(): ThemeContextValue<Themes>;
|
|
54
|
+
import { ReactElement } from "react";
|
|
55
|
+
declare function ThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, nonce, onThemeChange, themeColor }: ThemeProviderProps<Themes>): ReactElement;
|
|
56
|
+
export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, ThemeColor, StorageType, DefaultTheme, Attribute };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,343 @@
|
|
|
1
|
-
|
|
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
|
+
// src/client-provider.tsx
|
|
13
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
14
|
+
|
|
15
|
+
// src/store.ts
|
|
16
|
+
var state = { theme: undefined, systemTheme: undefined };
|
|
17
|
+
var listeners = new Set;
|
|
18
|
+
function emit() {
|
|
19
|
+
for (const listener of listeners)
|
|
20
|
+
listener();
|
|
21
|
+
}
|
|
22
|
+
var SERVER_SNAPSHOT = { theme: undefined, systemTheme: undefined };
|
|
23
|
+
var themeStore = {
|
|
24
|
+
subscribe(listener) {
|
|
25
|
+
listeners.add(listener);
|
|
26
|
+
return () => {
|
|
27
|
+
listeners.delete(listener);
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
getSnapshot() {
|
|
31
|
+
return state;
|
|
32
|
+
},
|
|
33
|
+
getServerSnapshot() {
|
|
34
|
+
return SERVER_SNAPSHOT;
|
|
35
|
+
},
|
|
36
|
+
setTheme(theme) {
|
|
37
|
+
if (state.theme === theme)
|
|
38
|
+
return;
|
|
39
|
+
state = { ...state, theme };
|
|
40
|
+
emit();
|
|
41
|
+
},
|
|
42
|
+
setSystemTheme(systemTheme) {
|
|
43
|
+
if (state.systemTheme === systemTheme)
|
|
44
|
+
return;
|
|
45
|
+
state = { ...state, systemTheme };
|
|
46
|
+
emit();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/client-provider.tsx
|
|
2
51
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
52
|
+
|
|
53
|
+
var DEFAULT_THEMES = ["light", "dark"];
|
|
54
|
+
function resolveThemeColor(themeColor, resolved) {
|
|
55
|
+
if (typeof themeColor === "string")
|
|
56
|
+
return themeColor;
|
|
57
|
+
return themeColor[resolved];
|
|
58
|
+
}
|
|
59
|
+
function updateMetaThemeColor(color) {
|
|
60
|
+
if (!color)
|
|
61
|
+
return;
|
|
62
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
63
|
+
if (!meta) {
|
|
64
|
+
meta = document.createElement("meta");
|
|
65
|
+
meta.name = "theme-color";
|
|
66
|
+
document.head.appendChild(meta);
|
|
67
|
+
}
|
|
68
|
+
meta.content = color;
|
|
69
|
+
}
|
|
70
|
+
function ClientThemeProvider({
|
|
71
|
+
children,
|
|
72
|
+
themes = DEFAULT_THEMES,
|
|
73
|
+
forcedTheme,
|
|
74
|
+
enableSystem = true,
|
|
75
|
+
defaultTheme,
|
|
76
|
+
attribute = "class",
|
|
77
|
+
value: valueMap,
|
|
78
|
+
target = "html",
|
|
79
|
+
disableTransitionOnChange = false,
|
|
80
|
+
storage = "localStorage",
|
|
81
|
+
storageKey = "theme",
|
|
82
|
+
enableColorScheme = true,
|
|
83
|
+
themeColor,
|
|
84
|
+
onThemeChange
|
|
85
|
+
}) {
|
|
86
|
+
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
87
|
+
const { theme, systemTheme } = useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot, themeStore.getServerSnapshot);
|
|
88
|
+
const resolvedTheme = forcedTheme ?? (theme === "system" || theme === undefined ? systemTheme : theme);
|
|
89
|
+
const onThemeChangeRef = useRef(onThemeChange);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
onThemeChangeRef.current = onThemeChange;
|
|
92
|
+
});
|
|
93
|
+
const getTargetEl = useCallback(() => {
|
|
94
|
+
if (target === "html")
|
|
95
|
+
return document.documentElement;
|
|
96
|
+
if (target === "body")
|
|
97
|
+
return document.body;
|
|
98
|
+
return document.querySelector(target);
|
|
99
|
+
}, [target]);
|
|
100
|
+
const applyToDom = useCallback((resolved) => {
|
|
101
|
+
const el = getTargetEl();
|
|
102
|
+
if (!el)
|
|
103
|
+
return;
|
|
104
|
+
const attrValue = valueMap?.[resolved] ?? resolved;
|
|
105
|
+
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
106
|
+
if (disableTransitionOnChange) {
|
|
107
|
+
const style = document.createElement("style");
|
|
108
|
+
style.textContent = "*,*::before,*::after{transition:none!important}";
|
|
109
|
+
document.head.appendChild(style);
|
|
110
|
+
requestAnimationFrame(() => requestAnimationFrame(() => document.head.removeChild(style)));
|
|
111
|
+
}
|
|
112
|
+
for (const attr of attrs) {
|
|
113
|
+
if (attr === "class") {
|
|
114
|
+
const toRemove = themes.map((t) => valueMap?.[t] ?? t);
|
|
115
|
+
el.classList.remove(...toRemove);
|
|
116
|
+
el.classList.add(attrValue);
|
|
117
|
+
} else {
|
|
118
|
+
el.setAttribute(attr, attrValue);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (enableColorScheme && (resolved === "light" || resolved === "dark")) {
|
|
122
|
+
el.style.colorScheme = resolved;
|
|
123
|
+
}
|
|
124
|
+
if (themeColor) {
|
|
125
|
+
updateMetaThemeColor(resolveThemeColor(themeColor, resolved));
|
|
126
|
+
}
|
|
127
|
+
}, [
|
|
128
|
+
attribute,
|
|
129
|
+
disableTransitionOnChange,
|
|
130
|
+
enableColorScheme,
|
|
131
|
+
getTargetEl,
|
|
132
|
+
themes,
|
|
133
|
+
valueMap,
|
|
134
|
+
themeColor
|
|
135
|
+
]);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (forcedTheme) {
|
|
138
|
+
themeStore.setTheme(forcedTheme);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let stored = null;
|
|
142
|
+
try {
|
|
143
|
+
if (storage !== "none") {
|
|
144
|
+
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
145
|
+
stored = store.getItem(storageKey);
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
const initial = stored && themes.includes(stored) ? stored : resolvedDefault;
|
|
149
|
+
themeStore.setTheme(initial);
|
|
150
|
+
}, [forcedTheme, resolvedDefault, storage, storageKey, themes]);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!enableSystem)
|
|
153
|
+
return;
|
|
154
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
155
|
+
themeStore.setSystemTheme(mq.matches ? "dark" : "light");
|
|
156
|
+
const handler = (e) => {
|
|
157
|
+
const next = e.matches ? "dark" : "light";
|
|
158
|
+
themeStore.setSystemTheme(next);
|
|
159
|
+
const current = themeStore.getSnapshot().theme;
|
|
160
|
+
if (current === "system" || current === undefined) {
|
|
161
|
+
applyToDom(next);
|
|
162
|
+
onThemeChangeRef.current?.(next);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
mq.addEventListener("change", handler);
|
|
166
|
+
return () => mq.removeEventListener("change", handler);
|
|
167
|
+
}, [enableSystem, applyToDom]);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (storage === "none")
|
|
170
|
+
return;
|
|
171
|
+
const handler = (e) => {
|
|
172
|
+
if (e.key !== storageKey || !e.newValue)
|
|
173
|
+
return;
|
|
174
|
+
if (themes.includes(e.newValue)) {
|
|
175
|
+
const newTheme = e.newValue;
|
|
176
|
+
const resolved = newTheme === "system" ? themeStore.getSnapshot().systemTheme ?? "light" : newTheme;
|
|
177
|
+
themeStore.setTheme(newTheme);
|
|
178
|
+
applyToDom(resolved);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
window.addEventListener("storage", handler);
|
|
182
|
+
return () => window.removeEventListener("storage", handler);
|
|
183
|
+
}, [storage, storageKey, themes, applyToDom]);
|
|
184
|
+
const setTheme = useCallback((next) => {
|
|
185
|
+
if (forcedTheme)
|
|
186
|
+
return;
|
|
187
|
+
const current = themeStore.getSnapshot().theme;
|
|
188
|
+
const newTheme = typeof next === "function" ? next(current) : next;
|
|
189
|
+
const resolved = newTheme === "system" ? themeStore.getSnapshot().systemTheme ?? "light" : newTheme;
|
|
190
|
+
themeStore.setTheme(newTheme);
|
|
191
|
+
applyToDom(resolved);
|
|
192
|
+
onThemeChangeRef.current?.(resolved);
|
|
193
|
+
try {
|
|
194
|
+
if (storage !== "none") {
|
|
195
|
+
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
196
|
+
store.setItem(storageKey, newTheme);
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}, [applyToDom, forcedTheme, storage, storageKey]);
|
|
200
|
+
const contextValue = {
|
|
201
|
+
theme: forcedTheme ?? theme,
|
|
202
|
+
resolvedTheme,
|
|
203
|
+
systemTheme,
|
|
204
|
+
forcedTheme,
|
|
205
|
+
themes,
|
|
206
|
+
setTheme
|
|
207
|
+
};
|
|
208
|
+
return /* @__PURE__ */ jsxDEV(ThemeContext.Provider, {
|
|
209
|
+
value: contextValue,
|
|
210
|
+
children
|
|
8
211
|
}, undefined, false, undefined, this);
|
|
9
212
|
}
|
|
213
|
+
|
|
214
|
+
// src/script.ts
|
|
215
|
+
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage, themeColors) {
|
|
216
|
+
let theme;
|
|
217
|
+
if (forcedTheme) {
|
|
218
|
+
theme = forcedTheme;
|
|
219
|
+
} else {
|
|
220
|
+
let stored = null;
|
|
221
|
+
try {
|
|
222
|
+
if (storage !== "none") {
|
|
223
|
+
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
224
|
+
stored = store.getItem(storageKey);
|
|
225
|
+
}
|
|
226
|
+
} catch {}
|
|
227
|
+
theme = stored && themes.includes(stored) ? stored : defaultTheme;
|
|
228
|
+
}
|
|
229
|
+
if (theme === "system") {
|
|
230
|
+
theme = enableSystem ? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : defaultTheme;
|
|
231
|
+
}
|
|
232
|
+
const attrValue = value?.[theme] || theme;
|
|
233
|
+
const el = target === "html" ? document.documentElement : target === "body" ? document.body : document.querySelector(target);
|
|
234
|
+
if (!el)
|
|
235
|
+
return;
|
|
236
|
+
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
237
|
+
for (const attr of attrs) {
|
|
238
|
+
if (attr === "class") {
|
|
239
|
+
const toRemove = themes.map((t) => value?.[t] || t);
|
|
240
|
+
el.classList.remove(...toRemove);
|
|
241
|
+
el.classList.add(attrValue);
|
|
242
|
+
} else {
|
|
243
|
+
el.setAttribute(attr, attrValue);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (enableColorScheme && (theme === "light" || theme === "dark")) {
|
|
247
|
+
el.style.colorScheme = theme;
|
|
248
|
+
}
|
|
249
|
+
if (themeColors) {
|
|
250
|
+
const color = typeof themeColors === "string" ? themeColors : themeColors[theme];
|
|
251
|
+
if (color) {
|
|
252
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
253
|
+
if (!meta) {
|
|
254
|
+
meta = document.createElement("meta");
|
|
255
|
+
meta.setAttribute("name", "theme-color");
|
|
256
|
+
document.head.appendChild(meta);
|
|
257
|
+
}
|
|
258
|
+
meta.setAttribute("content", color);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function getScript(config) {
|
|
263
|
+
const fn = themeScript.toString().replace(/\s*__name\s*\([^)]*\)\s*;?\s*/g, "");
|
|
264
|
+
const args = [
|
|
265
|
+
JSON.stringify(config.storageKey),
|
|
266
|
+
JSON.stringify(config.attribute),
|
|
267
|
+
JSON.stringify(config.defaultTheme),
|
|
268
|
+
String(config.enableSystem),
|
|
269
|
+
String(config.enableColorScheme),
|
|
270
|
+
JSON.stringify(config.forcedTheme ?? null),
|
|
271
|
+
JSON.stringify(config.themes),
|
|
272
|
+
JSON.stringify(config.value ?? null),
|
|
273
|
+
JSON.stringify(config.target),
|
|
274
|
+
JSON.stringify(config.storage),
|
|
275
|
+
JSON.stringify(config.themeColors ?? null)
|
|
276
|
+
].join(",");
|
|
277
|
+
return `(${fn})(${args})`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/provider.tsx
|
|
281
|
+
import { jsxDEV as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
|
|
282
|
+
var DEFAULT_THEMES2 = ["light", "dark"];
|
|
283
|
+
function ThemeProvider({
|
|
284
|
+
children,
|
|
285
|
+
themes = DEFAULT_THEMES2,
|
|
286
|
+
forcedTheme,
|
|
287
|
+
enableSystem = true,
|
|
288
|
+
defaultTheme,
|
|
289
|
+
attribute = "class",
|
|
290
|
+
value: valueMap,
|
|
291
|
+
target = "html",
|
|
292
|
+
disableTransitionOnChange = false,
|
|
293
|
+
storage = "localStorage",
|
|
294
|
+
storageKey = "theme",
|
|
295
|
+
enableColorScheme = true,
|
|
296
|
+
nonce,
|
|
297
|
+
onThemeChange,
|
|
298
|
+
themeColor
|
|
299
|
+
}) {
|
|
300
|
+
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
301
|
+
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ jsxDEV2("script", {
|
|
304
|
+
dangerouslySetInnerHTML: {
|
|
305
|
+
__html: getScript({
|
|
306
|
+
storageKey,
|
|
307
|
+
attribute,
|
|
308
|
+
defaultTheme: resolvedDefault,
|
|
309
|
+
enableSystem,
|
|
310
|
+
enableColorScheme,
|
|
311
|
+
forcedTheme,
|
|
312
|
+
themes,
|
|
313
|
+
value: valueMap,
|
|
314
|
+
target,
|
|
315
|
+
storage,
|
|
316
|
+
themeColors: themeColor
|
|
317
|
+
})
|
|
318
|
+
},
|
|
319
|
+
nonce
|
|
320
|
+
}, undefined, false, undefined, this),
|
|
321
|
+
/* @__PURE__ */ jsxDEV2(ClientThemeProvider, {
|
|
322
|
+
themes,
|
|
323
|
+
forcedTheme,
|
|
324
|
+
enableSystem,
|
|
325
|
+
defaultTheme,
|
|
326
|
+
attribute,
|
|
327
|
+
value: valueMap,
|
|
328
|
+
target,
|
|
329
|
+
disableTransitionOnChange,
|
|
330
|
+
storage,
|
|
331
|
+
storageKey,
|
|
332
|
+
enableColorScheme,
|
|
333
|
+
themeColor,
|
|
334
|
+
onThemeChange,
|
|
335
|
+
children
|
|
336
|
+
}, undefined, false, undefined, this)
|
|
337
|
+
]
|
|
338
|
+
}, undefined, true, undefined, this);
|
|
339
|
+
}
|
|
10
340
|
export {
|
|
11
|
-
|
|
341
|
+
useTheme,
|
|
342
|
+
ThemeProvider
|
|
12
343
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrksz/themes",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A modern, fully-featured theme management library for Next.js",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/jakubwarkusz/themes"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"files": [
|
|
7
11
|
"dist"
|
|
@@ -13,7 +17,7 @@
|
|
|
13
17
|
"lint": "biome check src",
|
|
14
18
|
"lint:fix": "biome check --write src",
|
|
15
19
|
"format": "biome format --write src",
|
|
16
|
-
"prepare": ""
|
|
20
|
+
"prepare": "[ \"$CI\" = \"true\" ] || lefthook install"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
23
|
"@biomejs/biome": "^2.4.8",
|
package/dist/index.css
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/* src/styles.css */
|
|
2
|
-
:root {
|
|
3
|
-
--button-bg: #007bff;
|
|
4
|
-
--button-bg-hover: #006fe6;
|
|
5
|
-
--button-text: #fff;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
@media (prefers-color-scheme: dark) {
|
|
9
|
-
:root {
|
|
10
|
-
--button-bg: #3089e8;
|
|
11
|
-
--button-bg-hover: #4796eb;
|
|
12
|
-
--button-text: #fff;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
[data-slot="button"] {
|
|
17
|
-
background: var(--button-bg);
|
|
18
|
-
color: var(--button-text);
|
|
19
|
-
cursor: pointer;
|
|
20
|
-
border: none;
|
|
21
|
-
border-radius: .5rem;
|
|
22
|
-
padding: .6rem 1.2rem;
|
|
23
|
-
font-size: .875rem;
|
|
24
|
-
font-weight: 500;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
[data-slot="button"]:hover {
|
|
28
|
-
background: var(--button-bg-hover);
|
|
29
|
-
}
|