@pshah-lab/themeswitcher 0.1.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/README.md +63 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +209 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
- package/scripts/no-flash-theme.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Prevent Theme Flash (Recommended)
|
|
2
|
+
|
|
3
|
+
When using light/dark themes, browsers may briefly render the page in the default theme before JavaScript loads. This results in a visible flash of incorrect theme.
|
|
4
|
+
|
|
5
|
+
To prevent this, add the following inline script to your HTML before any CSS is loaded.
|
|
6
|
+
|
|
7
|
+
Why this is needed
|
|
8
|
+
• The script runs before first paint
|
|
9
|
+
• It applies the correct theme synchronously
|
|
10
|
+
• It works even before your JavaScript bundle loads
|
|
11
|
+
• This is required for SSR and strongly recommended for all setups
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Add this to <head>
|
|
15
|
+
|
|
16
|
+
<script>
|
|
17
|
+
(function () {
|
|
18
|
+
try {
|
|
19
|
+
var key = "theme-mode";
|
|
20
|
+
var attribute = "data-theme";
|
|
21
|
+
|
|
22
|
+
var stored = localStorage.getItem(key);
|
|
23
|
+
var mode =
|
|
24
|
+
stored === "light" || stored === "dark" || stored === "system"
|
|
25
|
+
? stored
|
|
26
|
+
: "system";
|
|
27
|
+
|
|
28
|
+
var isDark =
|
|
29
|
+
window.matchMedia &&
|
|
30
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
31
|
+
|
|
32
|
+
var theme =
|
|
33
|
+
mode === "system"
|
|
34
|
+
? isDark
|
|
35
|
+
? "dark"
|
|
36
|
+
: "light"
|
|
37
|
+
: mode;
|
|
38
|
+
|
|
39
|
+
document.documentElement.setAttribute(attribute, theme);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// fail silently
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
Place this before your CSS:
|
|
47
|
+
|
|
48
|
+
<head>
|
|
49
|
+
<!-- Prevent theme flash -->
|
|
50
|
+
<script>/* theme script */</script>
|
|
51
|
+
|
|
52
|
+
<link rel="stylesheet" href="styles.css" />
|
|
53
|
+
</head>
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Notes
|
|
60
|
+
• Do not import this script from the package
|
|
61
|
+
• Do not bundle it
|
|
62
|
+
• Copy-paste is intentional and required for correct timing
|
|
63
|
+
• The script logic matches the internal theme manager
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type Theme = "light" | "dark";
|
|
2
|
+
type ThemeMode = Theme | "system";
|
|
3
|
+
interface ThemeManagerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Attribute applied to the root element
|
|
6
|
+
* Example: data-theme="dark"
|
|
7
|
+
*/
|
|
8
|
+
attribute?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Storage key for persistence
|
|
11
|
+
*/
|
|
12
|
+
storageKey?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Default mode when nothing is stored
|
|
15
|
+
*/
|
|
16
|
+
defaultMode?: ThemeMode;
|
|
17
|
+
}
|
|
18
|
+
type ThemeSubscriber = (theme: Theme, mode: ThemeMode) => void;
|
|
19
|
+
interface ThemeManager {
|
|
20
|
+
get(): Theme;
|
|
21
|
+
getMode(): ThemeMode;
|
|
22
|
+
set(mode: ThemeMode): void;
|
|
23
|
+
toggle(): void;
|
|
24
|
+
subscribe(fn: ThemeSubscriber): () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare function createThemeManager(options?: ThemeManagerOptions): ThemeManager;
|
|
28
|
+
|
|
29
|
+
type UseThemeResult = {
|
|
30
|
+
theme: Theme;
|
|
31
|
+
mode: ThemeMode;
|
|
32
|
+
setTheme: (mode: ThemeMode) => void;
|
|
33
|
+
toggleTheme: () => void;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Create a React hook bound to a ThemeManager instance.
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT:
|
|
39
|
+
* - Call this once (e.g. in a module or provider)
|
|
40
|
+
* - Do NOT call inside components repeatedly
|
|
41
|
+
*/
|
|
42
|
+
declare function createUseTheme(options?: ThemeManagerOptions): () => UseThemeResult;
|
|
43
|
+
|
|
44
|
+
export { type Theme, type ThemeManager, type ThemeManagerOptions, type ThemeMode, type ThemeSubscriber, createThemeManager, createUseTheme };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type Theme = "light" | "dark";
|
|
2
|
+
type ThemeMode = Theme | "system";
|
|
3
|
+
interface ThemeManagerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Attribute applied to the root element
|
|
6
|
+
* Example: data-theme="dark"
|
|
7
|
+
*/
|
|
8
|
+
attribute?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Storage key for persistence
|
|
11
|
+
*/
|
|
12
|
+
storageKey?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Default mode when nothing is stored
|
|
15
|
+
*/
|
|
16
|
+
defaultMode?: ThemeMode;
|
|
17
|
+
}
|
|
18
|
+
type ThemeSubscriber = (theme: Theme, mode: ThemeMode) => void;
|
|
19
|
+
interface ThemeManager {
|
|
20
|
+
get(): Theme;
|
|
21
|
+
getMode(): ThemeMode;
|
|
22
|
+
set(mode: ThemeMode): void;
|
|
23
|
+
toggle(): void;
|
|
24
|
+
subscribe(fn: ThemeSubscriber): () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare function createThemeManager(options?: ThemeManagerOptions): ThemeManager;
|
|
28
|
+
|
|
29
|
+
type UseThemeResult = {
|
|
30
|
+
theme: Theme;
|
|
31
|
+
mode: ThemeMode;
|
|
32
|
+
setTheme: (mode: ThemeMode) => void;
|
|
33
|
+
toggleTheme: () => void;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Create a React hook bound to a ThemeManager instance.
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT:
|
|
39
|
+
* - Call this once (e.g. in a module or provider)
|
|
40
|
+
* - Do NOT call inside components repeatedly
|
|
41
|
+
*/
|
|
42
|
+
declare function createUseTheme(options?: ThemeManagerOptions): () => UseThemeResult;
|
|
43
|
+
|
|
44
|
+
export { type Theme, type ThemeManager, type ThemeManagerOptions, type ThemeMode, type ThemeSubscriber, createThemeManager, createUseTheme };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
var __copyProps = (to, from, except, desc) => {
|
|
25
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
26
|
+
for (let key of __getOwnPropNames(from))
|
|
27
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
28
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
29
|
+
}
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/index.ts
|
|
35
|
+
var index_exports = {};
|
|
36
|
+
__export(index_exports, {
|
|
37
|
+
createThemeManager: () => createThemeManager,
|
|
38
|
+
createUseTheme: () => createUseTheme
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/storage.ts
|
|
43
|
+
function createPersistence(key) {
|
|
44
|
+
if (typeof window === "undefined") {
|
|
45
|
+
return memoryStorage();
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const testKey = "__theme_test__";
|
|
49
|
+
window.localStorage.setItem(testKey, testKey);
|
|
50
|
+
window.localStorage.removeItem(testKey);
|
|
51
|
+
return localStorageAdapter(key);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return memoryStorage();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function localStorageAdapter(key) {
|
|
57
|
+
return {
|
|
58
|
+
get() {
|
|
59
|
+
const value = window.localStorage.getItem(key);
|
|
60
|
+
if (value === "light" || value === "dark" || value === "system") {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
if (value !== null) {
|
|
64
|
+
window.localStorage.removeItem(key);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
},
|
|
68
|
+
set(value) {
|
|
69
|
+
window.localStorage.setItem(key, value);
|
|
70
|
+
},
|
|
71
|
+
clear() {
|
|
72
|
+
window.localStorage.removeItem(key);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function memoryStorage() {
|
|
77
|
+
let value = null;
|
|
78
|
+
return {
|
|
79
|
+
get() {
|
|
80
|
+
return value;
|
|
81
|
+
},
|
|
82
|
+
set(v) {
|
|
83
|
+
value = v;
|
|
84
|
+
},
|
|
85
|
+
clear() {
|
|
86
|
+
value = null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/system.ts
|
|
92
|
+
var QUERY = "(prefers-color-scheme: dark)";
|
|
93
|
+
function getSystemTheme() {
|
|
94
|
+
if (typeof window === "undefined") {
|
|
95
|
+
return "light";
|
|
96
|
+
}
|
|
97
|
+
if (!window.matchMedia) {
|
|
98
|
+
return "light";
|
|
99
|
+
}
|
|
100
|
+
return window.matchMedia(QUERY).matches ? "dark" : "light";
|
|
101
|
+
}
|
|
102
|
+
function subscribeSystemTheme(callback) {
|
|
103
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
104
|
+
return () => {
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const media = window.matchMedia(QUERY);
|
|
108
|
+
const handler = (event) => {
|
|
109
|
+
callback(event.matches ? "dark" : "light");
|
|
110
|
+
};
|
|
111
|
+
if (media.addEventListener) {
|
|
112
|
+
media.addEventListener("change", handler);
|
|
113
|
+
return () => media.removeEventListener("change", handler);
|
|
114
|
+
}
|
|
115
|
+
media.addListener(handler);
|
|
116
|
+
return () => media.removeListener(handler);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/dom.ts
|
|
120
|
+
function getRoot() {
|
|
121
|
+
if (typeof document === "undefined") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return document.documentElement;
|
|
125
|
+
}
|
|
126
|
+
function applyTheme(theme, attribute) {
|
|
127
|
+
const root = getRoot();
|
|
128
|
+
if (!root) return;
|
|
129
|
+
const current = root.getAttribute(attribute);
|
|
130
|
+
if (current === theme) return;
|
|
131
|
+
root.setAttribute(attribute, theme);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/manager.ts
|
|
135
|
+
var DEFAULT_OPTIONS = {
|
|
136
|
+
attribute: "data-theme",
|
|
137
|
+
storageKey: "theme-mode",
|
|
138
|
+
defaultMode: "system"
|
|
139
|
+
};
|
|
140
|
+
function createThemeManager(options = {}) {
|
|
141
|
+
var _a;
|
|
142
|
+
const config = __spreadValues(__spreadValues({}, DEFAULT_OPTIONS), options);
|
|
143
|
+
const storage = createPersistence(config.storageKey);
|
|
144
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
145
|
+
let mode = (_a = storage.get()) != null ? _a : config.defaultMode;
|
|
146
|
+
let theme = resolveTheme(mode);
|
|
147
|
+
applyTheme(theme, config.attribute);
|
|
148
|
+
let unsubscribeSystem = null;
|
|
149
|
+
if (mode === "system") {
|
|
150
|
+
unsubscribeSystem = subscribeSystemTheme(handleSystemChange);
|
|
151
|
+
}
|
|
152
|
+
function resolveTheme(m) {
|
|
153
|
+
return m === "system" ? getSystemTheme() : m;
|
|
154
|
+
}
|
|
155
|
+
let notifying = false;
|
|
156
|
+
function notify() {
|
|
157
|
+
if (notifying) return;
|
|
158
|
+
notifying = true;
|
|
159
|
+
subscribers.forEach((fn) => fn(theme, mode));
|
|
160
|
+
notifying = false;
|
|
161
|
+
}
|
|
162
|
+
function setMode(nextMode) {
|
|
163
|
+
if (nextMode === mode) return;
|
|
164
|
+
mode = nextMode;
|
|
165
|
+
storage.set(mode);
|
|
166
|
+
if (unsubscribeSystem) {
|
|
167
|
+
unsubscribeSystem();
|
|
168
|
+
unsubscribeSystem = null;
|
|
169
|
+
}
|
|
170
|
+
theme = resolveTheme(mode);
|
|
171
|
+
applyTheme(theme, config.attribute);
|
|
172
|
+
if (mode === "system") {
|
|
173
|
+
unsubscribeSystem = subscribeSystemTheme(handleSystemChange);
|
|
174
|
+
}
|
|
175
|
+
notify();
|
|
176
|
+
}
|
|
177
|
+
function handleSystemChange(nextTheme) {
|
|
178
|
+
if (mode !== "system") return;
|
|
179
|
+
if (theme === nextTheme) return;
|
|
180
|
+
theme = nextTheme;
|
|
181
|
+
applyTheme(theme, config.attribute);
|
|
182
|
+
notify();
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
get() {
|
|
186
|
+
return theme;
|
|
187
|
+
},
|
|
188
|
+
getMode() {
|
|
189
|
+
return mode;
|
|
190
|
+
},
|
|
191
|
+
set(nextMode) {
|
|
192
|
+
setMode(nextMode);
|
|
193
|
+
},
|
|
194
|
+
toggle() {
|
|
195
|
+
setMode(theme === "dark" ? "light" : "dark");
|
|
196
|
+
},
|
|
197
|
+
subscribe(fn) {
|
|
198
|
+
subscribers.add(fn);
|
|
199
|
+
fn(theme, mode);
|
|
200
|
+
return () => {
|
|
201
|
+
subscribers.delete(fn);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/react.ts
|
|
208
|
+
var import_react = require("react");
|
|
209
|
+
function createUseTheme(options) {
|
|
210
|
+
const manager = createThemeManager(options);
|
|
211
|
+
return function useTheme() {
|
|
212
|
+
const [theme, setThemeState] = (0, import_react.useState)(manager.get());
|
|
213
|
+
const [mode, setModeState] = (0, import_react.useState)(manager.getMode());
|
|
214
|
+
(0, import_react.useEffect)(() => {
|
|
215
|
+
const unsubscribe = manager.subscribe((t, m) => {
|
|
216
|
+
setThemeState(t);
|
|
217
|
+
setModeState(m);
|
|
218
|
+
});
|
|
219
|
+
return unsubscribe;
|
|
220
|
+
}, []);
|
|
221
|
+
return {
|
|
222
|
+
theme,
|
|
223
|
+
mode,
|
|
224
|
+
setTheme: (nextMode) => manager.set(nextMode),
|
|
225
|
+
toggleTheme: () => manager.toggle()
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
230
|
+
0 && (module.exports = {
|
|
231
|
+
createThemeManager,
|
|
232
|
+
createUseTheme
|
|
233
|
+
});
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/storage.ts","../src/system.ts","../src/dom.ts","../src/manager.ts","../src/react.ts"],"sourcesContent":["export type {\n Theme,\n ThemeMode,\n ThemeManager,\n ThemeManagerOptions,\n ThemeSubscriber,\n } from \"./types\";\n \n export { createThemeManager } from \"./manager\";\n export { createUseTheme } from \"./react\";","// src/storage.ts\n\nimport type { ThemeMode } from \"./types\";\n\nexport interface StorageAdapter {\n get(): ThemeMode | null;\n set(value: ThemeMode): void;\n clear(): void;\n}\n\nexport function createPersistence(key: string): StorageAdapter {\n // SSR / non-browser guard\n if (typeof window === \"undefined\") {\n return memoryStorage();\n }\n\n try {\n const testKey = \"__theme_test__\";\n window.localStorage.setItem(testKey, testKey);\n window.localStorage.removeItem(testKey);\n\n return localStorageAdapter(key);\n } catch {\n return memoryStorage();\n }\n}\n\nfunction localStorageAdapter(key: string): StorageAdapter {\n return {\n get() {\n const value = window.localStorage.getItem(key);\n\n if (value === \"light\" || value === \"dark\" || value === \"system\") {\n return value;\n }\n\n // clear corrupted or unknown values\n if (value !== null) {\n window.localStorage.removeItem(key);\n }\n\n return null;\n },\n\n set(value) {\n window.localStorage.setItem(key, value);\n },\n\n clear() {\n window.localStorage.removeItem(key);\n },\n };\n}\n\nfunction memoryStorage(): StorageAdapter {\n let value: ThemeMode | null = null;\n\n return {\n get() {\n return value;\n },\n set(v) {\n value = v;\n },\n clear() {\n value = null;\n },\n };\n}\n","// src/system.ts\n\nimport type { Theme } from \"./types\";\n\nconst QUERY = \"(prefers-color-scheme: dark)\";\n\nexport function getSystemTheme(): Theme {\n if (typeof window === \"undefined\") {\n return \"light\"; // safe default for SSR\n }\n\n if (!window.matchMedia) {\n return \"light\";\n }\n\n return window.matchMedia(QUERY).matches ? \"dark\" : \"light\";\n}\n\nexport function subscribeSystemTheme(\n callback: (theme: Theme) => void\n): () => void {\n if (typeof window === \"undefined\" || !window.matchMedia) {\n return () => {};\n }\n\n const media = window.matchMedia(QUERY);\n\n const handler = (event: MediaQueryListEvent) => {\n callback(event.matches ? \"dark\" : \"light\");\n };\n\n // Modern browsers\n if (media.addEventListener) {\n media.addEventListener(\"change\", handler);\n return () => media.removeEventListener(\"change\", handler);\n }\n\n // Legacy Safari fallback\n media.addListener(handler);\n return () => media.removeListener(handler);\n}","// src/dom.ts\n\nimport type { Theme } from \"./types\";\n\nfunction getRoot(): HTMLElement | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n return document.documentElement;\n}\n\nexport function applyTheme(theme: Theme, attribute: string): void {\n const root = getRoot();\n if (!root) return;\n\n const current = root.getAttribute(attribute);\n if (current === theme) return; // avoid unnecessary DOM writes\n\n root.setAttribute(attribute, theme);\n}\n\nexport function getAppliedTheme(attribute: string): Theme | null {\n const root = getRoot();\n if (!root) return null;\n\n const value = root.getAttribute(attribute);\n if (value === \"light\" || value === \"dark\") {\n return value;\n }\n\n return null;\n}","// src/manager.ts\n\nimport type {\n Theme,\n ThemeMode,\n ThemeManager,\n ThemeManagerOptions,\n ThemeSubscriber,\n} from \"./types\";\n\nimport { createPersistence } from \"./storage\";\nimport { getSystemTheme, subscribeSystemTheme } from \"./system\";\nimport { applyTheme } from \"./dom\";\n\nconst DEFAULT_OPTIONS: Required<ThemeManagerOptions> = {\n attribute: \"data-theme\",\n storageKey: \"theme-mode\",\n defaultMode: \"system\",\n};\n\nexport function createThemeManager(\n options: ThemeManagerOptions = {}\n): ThemeManager {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n const storage = createPersistence(config.storageKey);\n const subscribers = new Set<ThemeSubscriber>();\n\n let mode: ThemeMode = storage.get() ?? config.defaultMode;\n\n let theme: Theme = resolveTheme(mode);\n\n // Apply initial theme immediately\n applyTheme(theme, config.attribute);\n\n // Listen to OS theme changes only when needed\n let unsubscribeSystem: (() => void) | null = null;\n if (mode === \"system\") {\n unsubscribeSystem = subscribeSystemTheme(handleSystemChange);\n }\n\n function resolveTheme(m: ThemeMode): Theme {\n return m === \"system\" ? getSystemTheme() : m;\n }\n let notifying = false;\n\n function notify() {\n if (notifying) return;\n\n notifying = true;\n subscribers.forEach((fn) => fn(theme, mode));\n notifying = false;\n }\n\n function setMode(nextMode: ThemeMode) {\n if (nextMode === mode) return;\n\n mode = nextMode;\n storage.set(mode);\n\n if (unsubscribeSystem) {\n unsubscribeSystem();\n unsubscribeSystem = null;\n }\n\n theme = resolveTheme(mode);\n applyTheme(theme, config.attribute);\n\n if (mode === \"system\") {\n unsubscribeSystem = subscribeSystemTheme(handleSystemChange);\n }\n\n notify();\n }\n\n function handleSystemChange(nextTheme: Theme) {\n if (mode !== \"system\") return;\n if (theme === nextTheme) return;\n\n theme = nextTheme;\n applyTheme(theme, config.attribute);\n notify();\n }\n\n return {\n get() {\n return theme;\n },\n\n getMode() {\n return mode;\n },\n\n set(nextMode) {\n setMode(nextMode);\n },\n toggle() {\n setMode(theme === \"dark\" ? \"light\" : \"dark\");\n },\n\n subscribe(fn) {\n subscribers.add(fn);\n fn(theme, mode); // immediate sync\n return () => {\n subscribers.delete(fn);\n };\n },\n };\n}\n","import { useEffect, useState } from \"react\";\nimport type { Theme, ThemeMode, ThemeManagerOptions } from \"./types\";\nimport { createThemeManager } from \"./manager\";\n\ntype UseThemeResult = {\n theme: Theme;\n mode: ThemeMode;\n setTheme: (mode: ThemeMode) => void;\n toggleTheme: () => void;\n};\n\n/**\n * Create a React hook bound to a ThemeManager instance.\n *\n * IMPORTANT:\n * - Call this once (e.g. in a module or provider)\n * - Do NOT call inside components repeatedly\n */\nexport function createUseTheme(options?: ThemeManagerOptions) {\n const manager = createThemeManager(options);\n\n return function useTheme(): UseThemeResult {\n const [theme, setThemeState] = useState<Theme>(manager.get());\n const [mode, setModeState] = useState<ThemeMode>(manager.getMode());\n\n useEffect(() => {\n const unsubscribe = manager.subscribe((t, m) => {\n setThemeState(t);\n setModeState(m);\n });\n\n return unsubscribe;\n }, []);\n\n return {\n theme,\n mode,\n setTheme: (nextMode: ThemeMode) => manager.set(nextMode),\n toggleTheme: () => manager.toggle(),\n };\n };\n}"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/storage.ts
|
|
19
|
+
function createPersistence(key) {
|
|
20
|
+
if (typeof window === "undefined") {
|
|
21
|
+
return memoryStorage();
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const testKey = "__theme_test__";
|
|
25
|
+
window.localStorage.setItem(testKey, testKey);
|
|
26
|
+
window.localStorage.removeItem(testKey);
|
|
27
|
+
return localStorageAdapter(key);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return memoryStorage();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function localStorageAdapter(key) {
|
|
33
|
+
return {
|
|
34
|
+
get() {
|
|
35
|
+
const value = window.localStorage.getItem(key);
|
|
36
|
+
if (value === "light" || value === "dark" || value === "system") {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (value !== null) {
|
|
40
|
+
window.localStorage.removeItem(key);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
},
|
|
44
|
+
set(value) {
|
|
45
|
+
window.localStorage.setItem(key, value);
|
|
46
|
+
},
|
|
47
|
+
clear() {
|
|
48
|
+
window.localStorage.removeItem(key);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function memoryStorage() {
|
|
53
|
+
let value = null;
|
|
54
|
+
return {
|
|
55
|
+
get() {
|
|
56
|
+
return value;
|
|
57
|
+
},
|
|
58
|
+
set(v) {
|
|
59
|
+
value = v;
|
|
60
|
+
},
|
|
61
|
+
clear() {
|
|
62
|
+
value = null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/system.ts
|
|
68
|
+
var QUERY = "(prefers-color-scheme: dark)";
|
|
69
|
+
function getSystemTheme() {
|
|
70
|
+
if (typeof window === "undefined") {
|
|
71
|
+
return "light";
|
|
72
|
+
}
|
|
73
|
+
if (!window.matchMedia) {
|
|
74
|
+
return "light";
|
|
75
|
+
}
|
|
76
|
+
return window.matchMedia(QUERY).matches ? "dark" : "light";
|
|
77
|
+
}
|
|
78
|
+
function subscribeSystemTheme(callback) {
|
|
79
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
80
|
+
return () => {
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const media = window.matchMedia(QUERY);
|
|
84
|
+
const handler = (event) => {
|
|
85
|
+
callback(event.matches ? "dark" : "light");
|
|
86
|
+
};
|
|
87
|
+
if (media.addEventListener) {
|
|
88
|
+
media.addEventListener("change", handler);
|
|
89
|
+
return () => media.removeEventListener("change", handler);
|
|
90
|
+
}
|
|
91
|
+
media.addListener(handler);
|
|
92
|
+
return () => media.removeListener(handler);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/dom.ts
|
|
96
|
+
function getRoot() {
|
|
97
|
+
if (typeof document === "undefined") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return document.documentElement;
|
|
101
|
+
}
|
|
102
|
+
function applyTheme(theme, attribute) {
|
|
103
|
+
const root = getRoot();
|
|
104
|
+
if (!root) return;
|
|
105
|
+
const current = root.getAttribute(attribute);
|
|
106
|
+
if (current === theme) return;
|
|
107
|
+
root.setAttribute(attribute, theme);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/manager.ts
|
|
111
|
+
var DEFAULT_OPTIONS = {
|
|
112
|
+
attribute: "data-theme",
|
|
113
|
+
storageKey: "theme-mode",
|
|
114
|
+
defaultMode: "system"
|
|
115
|
+
};
|
|
116
|
+
function createThemeManager(options = {}) {
|
|
117
|
+
var _a;
|
|
118
|
+
const config = __spreadValues(__spreadValues({}, DEFAULT_OPTIONS), options);
|
|
119
|
+
const storage = createPersistence(config.storageKey);
|
|
120
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
121
|
+
let mode = (_a = storage.get()) != null ? _a : config.defaultMode;
|
|
122
|
+
let theme = resolveTheme(mode);
|
|
123
|
+
applyTheme(theme, config.attribute);
|
|
124
|
+
let unsubscribeSystem = null;
|
|
125
|
+
if (mode === "system") {
|
|
126
|
+
unsubscribeSystem = subscribeSystemTheme(handleSystemChange);
|
|
127
|
+
}
|
|
128
|
+
function resolveTheme(m) {
|
|
129
|
+
return m === "system" ? getSystemTheme() : m;
|
|
130
|
+
}
|
|
131
|
+
let notifying = false;
|
|
132
|
+
function notify() {
|
|
133
|
+
if (notifying) return;
|
|
134
|
+
notifying = true;
|
|
135
|
+
subscribers.forEach((fn) => fn(theme, mode));
|
|
136
|
+
notifying = false;
|
|
137
|
+
}
|
|
138
|
+
function setMode(nextMode) {
|
|
139
|
+
if (nextMode === mode) return;
|
|
140
|
+
mode = nextMode;
|
|
141
|
+
storage.set(mode);
|
|
142
|
+
if (unsubscribeSystem) {
|
|
143
|
+
unsubscribeSystem();
|
|
144
|
+
unsubscribeSystem = null;
|
|
145
|
+
}
|
|
146
|
+
theme = resolveTheme(mode);
|
|
147
|
+
applyTheme(theme, config.attribute);
|
|
148
|
+
if (mode === "system") {
|
|
149
|
+
unsubscribeSystem = subscribeSystemTheme(handleSystemChange);
|
|
150
|
+
}
|
|
151
|
+
notify();
|
|
152
|
+
}
|
|
153
|
+
function handleSystemChange(nextTheme) {
|
|
154
|
+
if (mode !== "system") return;
|
|
155
|
+
if (theme === nextTheme) return;
|
|
156
|
+
theme = nextTheme;
|
|
157
|
+
applyTheme(theme, config.attribute);
|
|
158
|
+
notify();
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
get() {
|
|
162
|
+
return theme;
|
|
163
|
+
},
|
|
164
|
+
getMode() {
|
|
165
|
+
return mode;
|
|
166
|
+
},
|
|
167
|
+
set(nextMode) {
|
|
168
|
+
setMode(nextMode);
|
|
169
|
+
},
|
|
170
|
+
toggle() {
|
|
171
|
+
setMode(theme === "dark" ? "light" : "dark");
|
|
172
|
+
},
|
|
173
|
+
subscribe(fn) {
|
|
174
|
+
subscribers.add(fn);
|
|
175
|
+
fn(theme, mode);
|
|
176
|
+
return () => {
|
|
177
|
+
subscribers.delete(fn);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/react.ts
|
|
184
|
+
import { useEffect, useState } from "react";
|
|
185
|
+
function createUseTheme(options) {
|
|
186
|
+
const manager = createThemeManager(options);
|
|
187
|
+
return function useTheme() {
|
|
188
|
+
const [theme, setThemeState] = useState(manager.get());
|
|
189
|
+
const [mode, setModeState] = useState(manager.getMode());
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const unsubscribe = manager.subscribe((t, m) => {
|
|
192
|
+
setThemeState(t);
|
|
193
|
+
setModeState(m);
|
|
194
|
+
});
|
|
195
|
+
return unsubscribe;
|
|
196
|
+
}, []);
|
|
197
|
+
return {
|
|
198
|
+
theme,
|
|
199
|
+
mode,
|
|
200
|
+
setTheme: (nextMode) => manager.set(nextMode),
|
|
201
|
+
toggleTheme: () => manager.toggle()
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
createThemeManager,
|
|
207
|
+
createUseTheme
|
|
208
|
+
};
|
|
209
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage.ts","../src/system.ts","../src/dom.ts","../src/manager.ts","../src/react.ts"],"sourcesContent":["// src/storage.ts\n\nimport type { ThemeMode } from \"./types\";\n\nexport interface StorageAdapter {\n get(): ThemeMode | null;\n set(value: ThemeMode): void;\n clear(): void;\n}\n\nexport function createPersistence(key: string): StorageAdapter {\n // SSR / non-browser guard\n if (typeof window === \"undefined\") {\n return memoryStorage();\n }\n\n try {\n const testKey = \"__theme_test__\";\n window.localStorage.setItem(testKey, testKey);\n window.localStorage.removeItem(testKey);\n\n return localStorageAdapter(key);\n } catch {\n return memoryStorage();\n }\n}\n\nfunction localStorageAdapter(key: string): StorageAdapter {\n return {\n get() {\n const value = window.localStorage.getItem(key);\n\n if (value === \"light\" || value === \"dark\" || value === \"system\") {\n return value;\n }\n\n // clear corrupted or unknown values\n if (value !== null) {\n window.localStorage.removeItem(key);\n }\n\n return null;\n },\n\n set(value) {\n window.localStorage.setItem(key, value);\n },\n\n clear() {\n window.localStorage.removeItem(key);\n },\n };\n}\n\nfunction memoryStorage(): StorageAdapter {\n let value: ThemeMode | null = null;\n\n return {\n get() {\n return value;\n },\n set(v) {\n value = v;\n },\n clear() {\n value = null;\n },\n };\n}\n","// src/system.ts\n\nimport type { Theme } from \"./types\";\n\nconst QUERY = \"(prefers-color-scheme: dark)\";\n\nexport function getSystemTheme(): Theme {\n if (typeof window === \"undefined\") {\n return \"light\"; // safe default for SSR\n }\n\n if (!window.matchMedia) {\n return \"light\";\n }\n\n return window.matchMedia(QUERY).matches ? \"dark\" : \"light\";\n}\n\nexport function subscribeSystemTheme(\n callback: (theme: Theme) => void\n): () => void {\n if (typeof window === \"undefined\" || !window.matchMedia) {\n return () => {};\n }\n\n const media = window.matchMedia(QUERY);\n\n const handler = (event: MediaQueryListEvent) => {\n callback(event.matches ? \"dark\" : \"light\");\n };\n\n // Modern browsers\n if (media.addEventListener) {\n media.addEventListener(\"change\", handler);\n return () => media.removeEventListener(\"change\", handler);\n }\n\n // Legacy Safari fallback\n media.addListener(handler);\n return () => media.removeListener(handler);\n}","// src/dom.ts\n\nimport type { Theme } from \"./types\";\n\nfunction getRoot(): HTMLElement | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n return document.documentElement;\n}\n\nexport function applyTheme(theme: Theme, attribute: string): void {\n const root = getRoot();\n if (!root) return;\n\n const current = root.getAttribute(attribute);\n if (current === theme) return; // avoid unnecessary DOM writes\n\n root.setAttribute(attribute, theme);\n}\n\nexport function getAppliedTheme(attribute: string): Theme | null {\n const root = getRoot();\n if (!root) return null;\n\n const value = root.getAttribute(attribute);\n if (value === \"light\" || value === \"dark\") {\n return value;\n }\n\n return null;\n}","// src/manager.ts\n\nimport type {\n Theme,\n ThemeMode,\n ThemeManager,\n ThemeManagerOptions,\n ThemeSubscriber,\n} from \"./types\";\n\nimport { createPersistence } from \"./storage\";\nimport { getSystemTheme, subscribeSystemTheme } from \"./system\";\nimport { applyTheme } from \"./dom\";\n\nconst DEFAULT_OPTIONS: Required<ThemeManagerOptions> = {\n attribute: \"data-theme\",\n storageKey: \"theme-mode\",\n defaultMode: \"system\",\n};\n\nexport function createThemeManager(\n options: ThemeManagerOptions = {}\n): ThemeManager {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n const storage = createPersistence(config.storageKey);\n const subscribers = new Set<ThemeSubscriber>();\n\n let mode: ThemeMode = storage.get() ?? config.defaultMode;\n\n let theme: Theme = resolveTheme(mode);\n\n // Apply initial theme immediately\n applyTheme(theme, config.attribute);\n\n // Listen to OS theme changes only when needed\n let unsubscribeSystem: (() => void) | null = null;\n if (mode === \"system\") {\n unsubscribeSystem = subscribeSystemTheme(handleSystemChange);\n }\n\n function resolveTheme(m: ThemeMode): Theme {\n return m === \"system\" ? getSystemTheme() : m;\n }\n let notifying = false;\n\n function notify() {\n if (notifying) return;\n\n notifying = true;\n subscribers.forEach((fn) => fn(theme, mode));\n notifying = false;\n }\n\n function setMode(nextMode: ThemeMode) {\n if (nextMode === mode) return;\n\n mode = nextMode;\n storage.set(mode);\n\n if (unsubscribeSystem) {\n unsubscribeSystem();\n unsubscribeSystem = null;\n }\n\n theme = resolveTheme(mode);\n applyTheme(theme, config.attribute);\n\n if (mode === \"system\") {\n unsubscribeSystem = subscribeSystemTheme(handleSystemChange);\n }\n\n notify();\n }\n\n function handleSystemChange(nextTheme: Theme) {\n if (mode !== \"system\") return;\n if (theme === nextTheme) return;\n\n theme = nextTheme;\n applyTheme(theme, config.attribute);\n notify();\n }\n\n return {\n get() {\n return theme;\n },\n\n getMode() {\n return mode;\n },\n\n set(nextMode) {\n setMode(nextMode);\n },\n toggle() {\n setMode(theme === \"dark\" ? \"light\" : \"dark\");\n },\n\n subscribe(fn) {\n subscribers.add(fn);\n fn(theme, mode); // immediate sync\n return () => {\n subscribers.delete(fn);\n };\n },\n };\n}\n","import { useEffect, useState } from \"react\";\nimport type { Theme, ThemeMode, ThemeManagerOptions } from \"./types\";\nimport { createThemeManager } from \"./manager\";\n\ntype UseThemeResult = {\n theme: Theme;\n mode: ThemeMode;\n setTheme: (mode: ThemeMode) => void;\n toggleTheme: () => void;\n};\n\n/**\n * Create a React hook bound to a ThemeManager instance.\n *\n * IMPORTANT:\n * - Call this once (e.g. in a module or provider)\n * - Do NOT call inside components repeatedly\n */\nexport function createUseTheme(options?: ThemeManagerOptions) {\n const manager = createThemeManager(options);\n\n return function useTheme(): UseThemeResult {\n const [theme, setThemeState] = useState<Theme>(manager.get());\n const [mode, setModeState] = useState<ThemeMode>(manager.getMode());\n\n useEffect(() => {\n const unsubscribe = manager.subscribe((t, m) => {\n setThemeState(t);\n setModeState(m);\n });\n\n return unsubscribe;\n }, []);\n\n return {\n theme,\n mode,\n setTheme: (nextMode: ThemeMode) => manager.set(nextMode),\n toggleTheme: () => manager.toggle(),\n };\n };\n}"],"mappings":";;;;;;;;;;;;;;;;;;AAUO,SAAS,kBAAkB,KAA6B;AAE7D,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,cAAc;AAAA,EACvB;AAEA,MAAI;AACF,UAAM,UAAU;AAChB,WAAO,aAAa,QAAQ,SAAS,OAAO;AAC5C,WAAO,aAAa,WAAW,OAAO;AAEtC,WAAO,oBAAoB,GAAG;AAAA,EAChC,SAAQ;AACN,WAAO,cAAc;AAAA,EACvB;AACF;AAEA,SAAS,oBAAoB,KAA6B;AACxD,SAAO;AAAA,IACL,MAAM;AACJ,YAAM,QAAQ,OAAO,aAAa,QAAQ,GAAG;AAE7C,UAAI,UAAU,WAAW,UAAU,UAAU,UAAU,UAAU;AAC/D,eAAO;AAAA,MACT;AAGA,UAAI,UAAU,MAAM;AAClB,eAAO,aAAa,WAAW,GAAG;AAAA,MACpC;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,IAAI,OAAO;AACT,aAAO,aAAa,QAAQ,KAAK,KAAK;AAAA,IACxC;AAAA,IAEA,QAAQ;AACN,aAAO,aAAa,WAAW,GAAG;AAAA,IACpC;AAAA,EACF;AACF;AAEA,SAAS,gBAAgC;AACvC,MAAI,QAA0B;AAE9B,SAAO;AAAA,IACL,MAAM;AACJ,aAAO;AAAA,IACT;AAAA,IACA,IAAI,GAAG;AACL,cAAQ;AAAA,IACV;AAAA,IACA,QAAQ;AACN,cAAQ;AAAA,IACV;AAAA,EACF;AACF;;;AChEA,IAAM,QAAQ;AAEP,SAAS,iBAAwB;AACtC,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,OAAO,YAAY;AACtB,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,WAAW,KAAK,EAAE,UAAU,SAAS;AACrD;AAEO,SAAS,qBACd,UACY;AACZ,MAAI,OAAO,WAAW,eAAe,CAAC,OAAO,YAAY;AACvD,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AAEA,QAAM,QAAQ,OAAO,WAAW,KAAK;AAErC,QAAM,UAAU,CAAC,UAA+B;AAC9C,aAAS,MAAM,UAAU,SAAS,OAAO;AAAA,EAC3C;AAGA,MAAI,MAAM,kBAAkB;AAC1B,UAAM,iBAAiB,UAAU,OAAO;AACxC,WAAO,MAAM,MAAM,oBAAoB,UAAU,OAAO;AAAA,EAC1D;AAGA,QAAM,YAAY,OAAO;AACzB,SAAO,MAAM,MAAM,eAAe,OAAO;AAC3C;;;ACpCA,SAAS,UAA8B;AACrC,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO;AAAA,EACT;AACA,SAAO,SAAS;AAClB;AAEO,SAAS,WAAW,OAAc,WAAyB;AAChE,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,KAAM;AAEX,QAAM,UAAU,KAAK,aAAa,SAAS;AAC3C,MAAI,YAAY,MAAO;AAEvB,OAAK,aAAa,WAAW,KAAK;AACpC;;;ACLA,IAAM,kBAAiD;AAAA,EACrD,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,aAAa;AACf;AAEO,SAAS,mBACd,UAA+B,CAAC,GAClB;AAtBhB;AAuBE,QAAM,SAAS,kCAAK,kBAAoB;AAExC,QAAM,UAAU,kBAAkB,OAAO,UAAU;AACnD,QAAM,cAAc,oBAAI,IAAqB;AAE7C,MAAI,QAAkB,aAAQ,IAAI,MAAZ,YAAiB,OAAO;AAE9C,MAAI,QAAe,aAAa,IAAI;AAGpC,aAAW,OAAO,OAAO,SAAS;AAGlC,MAAI,oBAAyC;AAC7C,MAAI,SAAS,UAAU;AACrB,wBAAoB,qBAAqB,kBAAkB;AAAA,EAC7D;AAEA,WAAS,aAAa,GAAqB;AACzC,WAAO,MAAM,WAAW,eAAe,IAAI;AAAA,EAC7C;AACA,MAAI,YAAY;AAEhB,WAAS,SAAS;AAChB,QAAI,UAAW;AAEf,gBAAY;AACZ,gBAAY,QAAQ,CAAC,OAAO,GAAG,OAAO,IAAI,CAAC;AAC3C,gBAAY;AAAA,EACd;AAEA,WAAS,QAAQ,UAAqB;AACpC,QAAI,aAAa,KAAM;AAEvB,WAAO;AACP,YAAQ,IAAI,IAAI;AAEhB,QAAI,mBAAmB;AACrB,wBAAkB;AAClB,0BAAoB;AAAA,IACtB;AAEA,YAAQ,aAAa,IAAI;AACzB,eAAW,OAAO,OAAO,SAAS;AAElC,QAAI,SAAS,UAAU;AACrB,0BAAoB,qBAAqB,kBAAkB;AAAA,IAC7D;AAEA,WAAO;AAAA,EACT;AAEA,WAAS,mBAAmB,WAAkB;AAC5C,QAAI,SAAS,SAAU;AACvB,QAAI,UAAU,UAAW;AAEzB,YAAQ;AACR,eAAW,OAAO,OAAO,SAAS;AAClC,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM;AACJ,aAAO;AAAA,IACT;AAAA,IAEA,UAAU;AACR,aAAO;AAAA,IACT;AAAA,IAEA,IAAI,UAAU;AACZ,cAAQ,QAAQ;AAAA,IAClB;AAAA,IACA,SAAS;AACP,cAAQ,UAAU,SAAS,UAAU,MAAM;AAAA,IAC7C;AAAA,IAEA,UAAU,IAAI;AACZ,kBAAY,IAAI,EAAE;AAClB,SAAG,OAAO,IAAI;AACd,aAAO,MAAM;AACX,oBAAY,OAAO,EAAE;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;;;AC5GA,SAAS,WAAW,gBAAgB;AAkB7B,SAAS,eAAe,SAA+B;AAC5D,QAAM,UAAU,mBAAmB,OAAO;AAE1C,SAAO,SAAS,WAA2B;AACzC,UAAM,CAAC,OAAO,aAAa,IAAI,SAAgB,QAAQ,IAAI,CAAC;AAC5D,UAAM,CAAC,MAAM,YAAY,IAAI,SAAoB,QAAQ,QAAQ,CAAC;AAElE,cAAU,MAAM;AACd,YAAM,cAAc,QAAQ,UAAU,CAAC,GAAG,MAAM;AAC9C,sBAAc,CAAC;AACf,qBAAa,CAAC;AAAA,MAChB,CAAC;AAED,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAEL,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,UAAU,CAAC,aAAwB,QAAQ,IAAI,QAAQ;AAAA,MACvD,aAAa,MAAM,QAAQ,OAAO;AAAA,IACpC;AAAA,EACF;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pshah-lab/themeswitcher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Light, dark, and system theme manager with SSR-safe no-flash support",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"scripts"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"theme",
|
|
26
|
+
"dark-mode",
|
|
27
|
+
"light-mode",
|
|
28
|
+
"theme-switcher",
|
|
29
|
+
"css-theme",
|
|
30
|
+
"ssr"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^19.2.7",
|
|
34
|
+
"jsdom": "^27.4.0",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vitest": "^4.0.16"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=16.8"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"react": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
try {
|
|
3
|
+
var key = "theme-mode";
|
|
4
|
+
var attribute = "data-theme";
|
|
5
|
+
|
|
6
|
+
var stored = localStorage.getItem(key);
|
|
7
|
+
var mode =
|
|
8
|
+
stored === "light" || stored === "dark" || stored === "system"
|
|
9
|
+
? stored
|
|
10
|
+
: "system";
|
|
11
|
+
|
|
12
|
+
var isDark =
|
|
13
|
+
window.matchMedia &&
|
|
14
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
15
|
+
|
|
16
|
+
var theme = mode === "system" ? (isDark ? "dark" : "light") : mode;
|
|
17
|
+
|
|
18
|
+
document.documentElement.setAttribute(attribute, theme);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// fail silently
|
|
21
|
+
}
|
|
22
|
+
})();
|