@wrksz/themes 0.1.0 → 0.2.0
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/dist/index.d.ts +52 -2
- package/dist/index.js +257 -7
- package/package.json +6 -2
- package/dist/index.css +0 -29
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,52 @@
|
|
|
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
|
+
type ThemeProviderProps<Themes extends string = DefaultTheme> = {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** All available themes */
|
|
9
|
+
themes?: Themes[];
|
|
10
|
+
/** Forced theme, overrides everything */
|
|
11
|
+
forcedTheme?: Themes;
|
|
12
|
+
/** Enable system preference via prefers-color-scheme */
|
|
13
|
+
enableSystem?: boolean;
|
|
14
|
+
/** Default theme when no preference stored */
|
|
15
|
+
defaultTheme?: Themes | "system";
|
|
16
|
+
/** HTML attribute(s) to set on target element */
|
|
17
|
+
attribute?: Attribute | Attribute[];
|
|
18
|
+
/** Map theme name to attribute value */
|
|
19
|
+
value?: ValueObject;
|
|
20
|
+
/** Target element to apply theme to, defaults to <html> */
|
|
21
|
+
target?: "html" | "body" | string;
|
|
22
|
+
/** Disable CSS transitions on theme change */
|
|
23
|
+
disableTransitionOnChange?: boolean;
|
|
24
|
+
/** Where to persist theme */
|
|
25
|
+
storage?: StorageType;
|
|
26
|
+
/** Storage key */
|
|
27
|
+
storageKey?: string;
|
|
28
|
+
/** Set native color-scheme CSS property */
|
|
29
|
+
enableColorScheme?: boolean;
|
|
30
|
+
/** Nonce for CSP */
|
|
31
|
+
nonce?: string;
|
|
32
|
+
/** Called when theme changes */
|
|
33
|
+
onThemeChange?: (theme: Themes) => void;
|
|
34
|
+
};
|
|
35
|
+
type ThemeContextValue<Themes extends string = DefaultTheme> = {
|
|
36
|
+
/** Current theme (may be "system") */
|
|
37
|
+
theme: Themes | "system" | undefined;
|
|
38
|
+
/** Resolved theme - never "system" */
|
|
39
|
+
resolvedTheme: Themes | undefined;
|
|
40
|
+
/** System preference */
|
|
41
|
+
systemTheme: "light" | "dark" | undefined;
|
|
42
|
+
/** Forced theme if set */
|
|
43
|
+
forcedTheme: Themes | undefined;
|
|
44
|
+
/** All available themes */
|
|
45
|
+
themes: Themes[];
|
|
46
|
+
/** Set theme */
|
|
47
|
+
setTheme: (theme: Themes | "system" | ((current: Themes | "system" | undefined) => Themes | "system")) => void;
|
|
48
|
+
};
|
|
49
|
+
declare function useTheme<Themes extends string = DefaultTheme>(): ThemeContextValue<Themes>;
|
|
50
|
+
import { ReactElement } from "react";
|
|
51
|
+
declare function ThemeProvider<Themes extends string = DefaultTheme>({ children, themes, forcedTheme, enableSystem, defaultTheme, attribute, value: valueMap, target, disableTransitionOnChange, storage, storageKey, enableColorScheme, nonce, onThemeChange }: ThemeProviderProps<Themes>): ReactElement;
|
|
52
|
+
export { useTheme, ValueObject, ThemeProviderProps, ThemeProvider, ThemeContextValue, StorageType, DefaultTheme, Attribute };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,262 @@
|
|
|
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, useState } from "react";
|
|
2
14
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
15
|
+
|
|
16
|
+
var DEFAULT_THEMES = ["light", "dark"];
|
|
17
|
+
function ClientThemeProvider({
|
|
18
|
+
children,
|
|
19
|
+
themes = DEFAULT_THEMES,
|
|
20
|
+
forcedTheme,
|
|
21
|
+
enableSystem = true,
|
|
22
|
+
defaultTheme,
|
|
23
|
+
attribute = "class",
|
|
24
|
+
value: valueMap,
|
|
25
|
+
target = "html",
|
|
26
|
+
disableTransitionOnChange = false,
|
|
27
|
+
storage = "localStorage",
|
|
28
|
+
storageKey = "theme",
|
|
29
|
+
enableColorScheme = true,
|
|
30
|
+
onThemeChange
|
|
31
|
+
}) {
|
|
32
|
+
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
33
|
+
const [theme, setThemeState] = useState(undefined);
|
|
34
|
+
const [systemTheme, setSystemTheme] = useState(undefined);
|
|
35
|
+
const resolvedTheme = forcedTheme ?? (theme === "system" || theme === undefined ? systemTheme : theme);
|
|
36
|
+
const onThemeChangeRef = useRef(onThemeChange);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
onThemeChangeRef.current = onThemeChange;
|
|
39
|
+
});
|
|
40
|
+
const getTargetEl = useCallback(() => {
|
|
41
|
+
if (target === "html")
|
|
42
|
+
return document.documentElement;
|
|
43
|
+
if (target === "body")
|
|
44
|
+
return document.body;
|
|
45
|
+
return document.querySelector(target);
|
|
46
|
+
}, [target]);
|
|
47
|
+
const applyToDom = useCallback((resolved) => {
|
|
48
|
+
const el = getTargetEl();
|
|
49
|
+
if (!el)
|
|
50
|
+
return;
|
|
51
|
+
const attrValue = valueMap?.[resolved] ?? resolved;
|
|
52
|
+
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
53
|
+
if (disableTransitionOnChange) {
|
|
54
|
+
const style = document.createElement("style");
|
|
55
|
+
style.textContent = "*,*::before,*::after{transition:none!important}";
|
|
56
|
+
document.head.appendChild(style);
|
|
57
|
+
requestAnimationFrame(() => requestAnimationFrame(() => document.head.removeChild(style)));
|
|
58
|
+
}
|
|
59
|
+
for (const attr of attrs) {
|
|
60
|
+
if (attr === "class") {
|
|
61
|
+
const toRemove = themes.map((t) => valueMap?.[t] ?? t);
|
|
62
|
+
el.classList.remove(...toRemove);
|
|
63
|
+
el.classList.add(attrValue);
|
|
64
|
+
} else {
|
|
65
|
+
el.setAttribute(attr, attrValue);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (enableColorScheme && (resolved === "light" || resolved === "dark")) {
|
|
69
|
+
el.style.colorScheme = resolved;
|
|
70
|
+
}
|
|
71
|
+
}, [attribute, disableTransitionOnChange, enableColorScheme, getTargetEl, themes, valueMap]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (forcedTheme) {
|
|
74
|
+
setThemeState(forcedTheme);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let stored = null;
|
|
78
|
+
try {
|
|
79
|
+
if (storage !== "none") {
|
|
80
|
+
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
81
|
+
stored = store.getItem(storageKey);
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
const initial = stored && themes.includes(stored) ? stored : resolvedDefault;
|
|
85
|
+
setThemeState(initial);
|
|
86
|
+
}, [forcedTheme, resolvedDefault, storage, storageKey, themes]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!enableSystem)
|
|
89
|
+
return;
|
|
90
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
91
|
+
const sys = mq.matches ? "dark" : "light";
|
|
92
|
+
setSystemTheme(sys);
|
|
93
|
+
const handler = (e) => {
|
|
94
|
+
const next = e.matches ? "dark" : "light";
|
|
95
|
+
setSystemTheme(next);
|
|
96
|
+
if (theme === "system" || theme === undefined) {
|
|
97
|
+
applyToDom(next);
|
|
98
|
+
onThemeChangeRef.current?.(next);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
mq.addEventListener("change", handler);
|
|
102
|
+
return () => mq.removeEventListener("change", handler);
|
|
103
|
+
}, [enableSystem, theme, applyToDom]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (storage === "none")
|
|
106
|
+
return;
|
|
107
|
+
const handler = (e) => {
|
|
108
|
+
if (e.key !== storageKey || !e.newValue)
|
|
109
|
+
return;
|
|
110
|
+
if (themes.includes(e.newValue)) {
|
|
111
|
+
const newTheme = e.newValue;
|
|
112
|
+
const resolved = newTheme === "system" ? systemTheme ?? "light" : newTheme;
|
|
113
|
+
setThemeState(newTheme);
|
|
114
|
+
applyToDom(resolved);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
window.addEventListener("storage", handler);
|
|
118
|
+
return () => window.removeEventListener("storage", handler);
|
|
119
|
+
}, [storage, storageKey, themes, systemTheme, applyToDom]);
|
|
120
|
+
const setTheme = useCallback((next) => {
|
|
121
|
+
if (forcedTheme)
|
|
122
|
+
return;
|
|
123
|
+
const newTheme = typeof next === "function" ? next(theme) : next;
|
|
124
|
+
const resolved = newTheme === "system" ? systemTheme ?? "light" : newTheme;
|
|
125
|
+
setThemeState(newTheme);
|
|
126
|
+
applyToDom(resolved);
|
|
127
|
+
onThemeChangeRef.current?.(resolved);
|
|
128
|
+
try {
|
|
129
|
+
if (storage !== "none") {
|
|
130
|
+
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
131
|
+
store.setItem(storageKey, newTheme);
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
}, [applyToDom, forcedTheme, storage, storageKey, systemTheme, theme]);
|
|
135
|
+
const contextValue = {
|
|
136
|
+
theme: forcedTheme ?? theme,
|
|
137
|
+
resolvedTheme,
|
|
138
|
+
systemTheme,
|
|
139
|
+
forcedTheme,
|
|
140
|
+
themes,
|
|
141
|
+
setTheme
|
|
142
|
+
};
|
|
143
|
+
return /* @__PURE__ */ jsxDEV(ThemeContext.Provider, {
|
|
144
|
+
value: contextValue,
|
|
145
|
+
children
|
|
8
146
|
}, undefined, false, undefined, this);
|
|
9
147
|
}
|
|
148
|
+
|
|
149
|
+
// src/script.ts
|
|
150
|
+
function themeScript(storageKey, attribute, defaultTheme, enableSystem, enableColorScheme, forcedTheme, themes, value, target, storage) {
|
|
151
|
+
let theme;
|
|
152
|
+
if (forcedTheme) {
|
|
153
|
+
theme = forcedTheme;
|
|
154
|
+
} else {
|
|
155
|
+
let stored = null;
|
|
156
|
+
try {
|
|
157
|
+
if (storage !== "none") {
|
|
158
|
+
const store = storage === "localStorage" ? localStorage : sessionStorage;
|
|
159
|
+
stored = store.getItem(storageKey);
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
theme = stored && themes.includes(stored) ? stored : defaultTheme;
|
|
163
|
+
}
|
|
164
|
+
if (theme === "system") {
|
|
165
|
+
theme = enableSystem ? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : defaultTheme;
|
|
166
|
+
}
|
|
167
|
+
const attrValue = value?.[theme] || theme;
|
|
168
|
+
const el = target === "html" ? document.documentElement : target === "body" ? document.body : document.querySelector(target);
|
|
169
|
+
if (!el)
|
|
170
|
+
return;
|
|
171
|
+
const attrs = Array.isArray(attribute) ? attribute : [attribute];
|
|
172
|
+
for (const attr of attrs) {
|
|
173
|
+
if (attr === "class") {
|
|
174
|
+
const toRemove = themes.map((t) => value?.[t] || t);
|
|
175
|
+
el.classList.remove(...toRemove);
|
|
176
|
+
el.classList.add(attrValue);
|
|
177
|
+
} else {
|
|
178
|
+
el.setAttribute(attr, attrValue);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (enableColorScheme && (theme === "light" || theme === "dark")) {
|
|
182
|
+
el.style.colorScheme = theme;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function getScript(config) {
|
|
186
|
+
const fn = themeScript.toString().replace(/\s*__name\s*\([^)]*\)\s*;?\s*/g, "");
|
|
187
|
+
const args = [
|
|
188
|
+
JSON.stringify(config.storageKey),
|
|
189
|
+
JSON.stringify(config.attribute),
|
|
190
|
+
JSON.stringify(config.defaultTheme),
|
|
191
|
+
String(config.enableSystem),
|
|
192
|
+
String(config.enableColorScheme),
|
|
193
|
+
JSON.stringify(config.forcedTheme ?? null),
|
|
194
|
+
JSON.stringify(config.themes),
|
|
195
|
+
JSON.stringify(config.value ?? null),
|
|
196
|
+
JSON.stringify(config.target),
|
|
197
|
+
JSON.stringify(config.storage)
|
|
198
|
+
].join(",");
|
|
199
|
+
return `(${fn})(${args})`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/provider.tsx
|
|
203
|
+
import { jsxDEV as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
|
|
204
|
+
var DEFAULT_THEMES2 = ["light", "dark"];
|
|
205
|
+
function ThemeProvider({
|
|
206
|
+
children,
|
|
207
|
+
themes = DEFAULT_THEMES2,
|
|
208
|
+
forcedTheme,
|
|
209
|
+
enableSystem = true,
|
|
210
|
+
defaultTheme,
|
|
211
|
+
attribute = "class",
|
|
212
|
+
value: valueMap,
|
|
213
|
+
target = "html",
|
|
214
|
+
disableTransitionOnChange = false,
|
|
215
|
+
storage = "localStorage",
|
|
216
|
+
storageKey = "theme",
|
|
217
|
+
enableColorScheme = true,
|
|
218
|
+
nonce,
|
|
219
|
+
onThemeChange
|
|
220
|
+
}) {
|
|
221
|
+
const resolvedDefault = defaultTheme ?? (enableSystem ? "system" : "light");
|
|
222
|
+
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
223
|
+
children: [
|
|
224
|
+
/* @__PURE__ */ jsxDEV2("script", {
|
|
225
|
+
dangerouslySetInnerHTML: {
|
|
226
|
+
__html: getScript({
|
|
227
|
+
storageKey,
|
|
228
|
+
attribute,
|
|
229
|
+
defaultTheme: resolvedDefault,
|
|
230
|
+
enableSystem,
|
|
231
|
+
enableColorScheme,
|
|
232
|
+
forcedTheme,
|
|
233
|
+
themes,
|
|
234
|
+
value: valueMap,
|
|
235
|
+
target,
|
|
236
|
+
storage
|
|
237
|
+
})
|
|
238
|
+
},
|
|
239
|
+
nonce
|
|
240
|
+
}, undefined, false, undefined, this),
|
|
241
|
+
/* @__PURE__ */ jsxDEV2(ClientThemeProvider, {
|
|
242
|
+
themes,
|
|
243
|
+
forcedTheme,
|
|
244
|
+
enableSystem,
|
|
245
|
+
defaultTheme,
|
|
246
|
+
attribute,
|
|
247
|
+
value: valueMap,
|
|
248
|
+
target,
|
|
249
|
+
disableTransitionOnChange,
|
|
250
|
+
storage,
|
|
251
|
+
storageKey,
|
|
252
|
+
enableColorScheme,
|
|
253
|
+
onThemeChange,
|
|
254
|
+
children
|
|
255
|
+
}, undefined, false, undefined, this)
|
|
256
|
+
]
|
|
257
|
+
}, undefined, true, undefined, this);
|
|
258
|
+
}
|
|
10
259
|
export {
|
|
11
|
-
|
|
260
|
+
useTheme,
|
|
261
|
+
ThemeProvider
|
|
12
262
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrksz/themes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
-
}
|