@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 CHANGED
@@ -1,2 +1,52 @@
1
- declare function Button(props: React.ComponentProps<"button">): React.ReactNode;
2
- export { Button };
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
- // src/components/button.tsx
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
- function Button(props) {
4
- return /* @__PURE__ */ jsxDEV("button", {
5
- type: "button",
6
- "data-slot": "button",
7
- ...props
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
- Button
260
+ useTheme,
261
+ ThemeProvider
12
262
  };
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@wrksz/themes",
3
- "version": "0.1.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
- }