@pshah-lab/themeswitcher 0.1.1 → 0.1.3

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.cjs ADDED
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const isBrowser = typeof window !== "undefined" &&
4
+ typeof document !== "undefined";
5
+ class ThemeToggle {
6
+ constructor(config = {}) {
7
+ var _a, _b, _c;
8
+ this.handleSystemChange = () => {
9
+ if (this.currentTheme === "auto") {
10
+ this.applyTheme("auto");
11
+ }
12
+ };
13
+ this.storageKey = (_a = config.storageKey) !== null && _a !== void 0 ? _a : "app-theme";
14
+ this.useSystemPreference = (_b = config.useSystemPreference) !== null && _b !== void 0 ? _b : true;
15
+ this.onChange = config.onChange;
16
+ const stored = this.getStoredTheme();
17
+ this.currentTheme = (_c = stored !== null && stored !== void 0 ? stored : config.defaultTheme) !== null && _c !== void 0 ? _c : "auto";
18
+ this.init();
19
+ }
20
+ init() {
21
+ if (!isBrowser)
22
+ return;
23
+ if (this.useSystemPreference) {
24
+ this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
25
+ this.mediaQuery.addEventListener("change", this.handleSystemChange);
26
+ }
27
+ this.applyTheme(this.currentTheme);
28
+ }
29
+ getStoredTheme() {
30
+ if (!isBrowser)
31
+ return null;
32
+ try {
33
+ const stored = localStorage.getItem(this.storageKey);
34
+ return stored === "light" || stored === "dark" || stored === "auto"
35
+ ? stored
36
+ : null;
37
+ }
38
+ catch (_a) {
39
+ return null;
40
+ }
41
+ }
42
+ setStoredTheme(theme) {
43
+ if (!isBrowser)
44
+ return;
45
+ try {
46
+ localStorage.setItem(this.storageKey, theme);
47
+ }
48
+ catch (_a) {
49
+ /* noop */
50
+ }
51
+ }
52
+ getEffectiveTheme() {
53
+ var _a;
54
+ if (this.currentTheme === "auto") {
55
+ return ((_a = this.mediaQuery) === null || _a === void 0 ? void 0 : _a.matches) ? "dark" : "light";
56
+ }
57
+ return this.currentTheme;
58
+ }
59
+ applyTheme(theme) {
60
+ var _a;
61
+ if (!isBrowser)
62
+ return;
63
+ const effective = this.getEffectiveTheme();
64
+ document.documentElement.setAttribute("data-theme", effective);
65
+ document.documentElement.classList.remove("light", "dark");
66
+ document.documentElement.classList.add(effective);
67
+ (_a = this.onChange) === null || _a === void 0 ? void 0 : _a.call(this, theme, effective);
68
+ }
69
+ setTheme(theme) {
70
+ this.currentTheme = theme;
71
+ this.setStoredTheme(theme);
72
+ this.applyTheme(theme);
73
+ }
74
+ toggle() {
75
+ const next = this.getEffectiveTheme() === "dark" ? "light" : "dark";
76
+ this.setTheme(next);
77
+ }
78
+ getTheme() {
79
+ return this.currentTheme;
80
+ }
81
+ destroy() {
82
+ if (this.mediaQuery) {
83
+ this.mediaQuery.removeEventListener("change", this.handleSystemChange);
84
+ }
85
+ }
86
+ }
87
+ function createThemeToggle(config) {
88
+ return new ThemeToggle(config);
89
+ }
90
+
91
+ exports.ThemeToggle = ThemeToggle;
92
+ exports.createThemeToggle = createThemeToggle;
93
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["export type Theme = \"light\" | \"dark\" | \"auto\";\n\nexport interface ThemeConfig {\n storageKey?: string;\n defaultTheme?: Theme;\n useSystemPreference?: boolean;\n onChange?: (theme: Theme, effective: \"light\" | \"dark\") => void;\n}\n\nconst isBrowser =\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\";\n\nexport class ThemeToggle {\n private storageKey: string;\n private currentTheme: Theme;\n private useSystemPreference: boolean;\n private onChange?: ThemeConfig[\"onChange\"];\n private mediaQuery?: MediaQueryList;\n\n private handleSystemChange = () => {\n if (this.currentTheme === \"auto\") {\n this.applyTheme(\"auto\");\n }\n };\n\n constructor(config: ThemeConfig = {}) {\n this.storageKey = config.storageKey ?? \"app-theme\";\n this.useSystemPreference = config.useSystemPreference ?? true;\n this.onChange = config.onChange;\n\n const stored = this.getStoredTheme();\n this.currentTheme = stored ?? config.defaultTheme ?? \"auto\";\n\n this.init();\n }\n\n private init(): void {\n if (!isBrowser) return;\n\n if (this.useSystemPreference) {\n this.mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n this.mediaQuery.addEventListener(\"change\", this.handleSystemChange);\n }\n\n this.applyTheme(this.currentTheme);\n }\n\n private getStoredTheme(): Theme | null {\n if (!isBrowser) return null;\n\n try {\n const stored = localStorage.getItem(this.storageKey);\n return stored === \"light\" || stored === \"dark\" || stored === \"auto\"\n ? stored\n : null;\n } catch {\n return null;\n }\n }\n\n private setStoredTheme(theme: Theme): void {\n if (!isBrowser) return;\n\n try {\n localStorage.setItem(this.storageKey, theme);\n } catch {\n /* noop */\n }\n }\n\n public getEffectiveTheme(): \"light\" | \"dark\" {\n if (this.currentTheme === \"auto\") {\n return this.mediaQuery?.matches ? \"dark\" : \"light\";\n }\n return this.currentTheme;\n }\n\n private applyTheme(theme: Theme): void {\n if (!isBrowser) return;\n\n const effective = this.getEffectiveTheme();\n\n document.documentElement.setAttribute(\"data-theme\", effective);\n document.documentElement.classList.remove(\"light\", \"dark\");\n document.documentElement.classList.add(effective);\n\n this.onChange?.(theme, effective);\n }\n\n public setTheme(theme: Theme): void {\n this.currentTheme = theme;\n this.setStoredTheme(theme);\n this.applyTheme(theme);\n }\n\n public toggle(): void {\n const next =\n this.getEffectiveTheme() === \"dark\" ? \"light\" : \"dark\";\n this.setTheme(next);\n }\n\n public getTheme(): Theme {\n return this.currentTheme;\n }\n\n public destroy(): void {\n if (this.mediaQuery) {\n this.mediaQuery.removeEventListener(\n \"change\",\n this.handleSystemChange\n );\n }\n }\n}\n\nexport function createThemeToggle(\n config?: ThemeConfig\n): ThemeToggle {\n return new ThemeToggle(config);\n}"],"names":[],"mappings":";;AASA,MAAM,SAAS,GACb,OAAO,MAAM,KAAK,WAAW;IAC7B,OAAO,QAAQ,KAAK,WAAW;MAEpB,WAAW,CAAA;AAatB,IAAA,WAAA,CAAY,SAAsB,EAAE,EAAA;;QAN5B,IAAA,CAAA,kBAAkB,GAAG,MAAK;AAChC,YAAA,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM,EAAE;AAChC,gBAAA,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YACzB;AACF,QAAA,CAAC;QAGC,IAAI,CAAC,UAAU,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,WAAW;QAClD,IAAI,CAAC,mBAAmB,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,mBAAmB,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,IAAI;AAC7D,QAAA,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ;AAE/B,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE;AACpC,QAAA,IAAI,CAAC,YAAY,GAAG,CAAA,EAAA,GAAA,MAAM,KAAA,IAAA,IAAN,MAAM,KAAA,MAAA,GAAN,MAAM,GAAI,MAAM,CAAC,YAAY,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,MAAM;QAE3D,IAAI,CAAC,IAAI,EAAE;IACb;IAEQ,IAAI,GAAA;AACV,QAAA,IAAI,CAAC,SAAS;YAAE;AAEhB,QAAA,IAAI,IAAI,CAAC,mBAAmB,EAAE;YAC5B,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,8BAA8B,CAAC;YACnE,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,kBAAkB,CAAC;QACrE;AAEA,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC;IACpC;IAEQ,cAAc,GAAA;AACpB,QAAA,IAAI,CAAC,SAAS;AAAE,YAAA,OAAO,IAAI;AAE3B,QAAA,IAAI;YACF,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;YACpD,OAAO,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK;AAC3D,kBAAE;kBACA,IAAI;QACV;AAAE,QAAA,OAAA,EAAA,EAAM;AACN,YAAA,OAAO,IAAI;QACb;IACF;AAEQ,IAAA,cAAc,CAAC,KAAY,EAAA;AACjC,QAAA,IAAI,CAAC,SAAS;YAAE;AAEhB,QAAA,IAAI;YACF,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;QAC9C;AAAE,QAAA,OAAA,EAAA,EAAM;;QAER;IACF;IAEO,iBAAiB,GAAA;;AACtB,QAAA,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM,EAAE;AAChC,YAAA,OAAO,CAAA,CAAA,EAAA,GAAA,IAAI,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,MAAA,GAAA,EAAA,CAAE,OAAO,IAAG,MAAM,GAAG,OAAO;QACpD;QACA,OAAO,IAAI,CAAC,YAAY;IAC1B;AAEQ,IAAA,UAAU,CAAC,KAAY,EAAA;;AAC7B,QAAA,IAAI,CAAC,SAAS;YAAE;AAEhB,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,iBAAiB,EAAE;QAE1C,QAAQ,CAAC,eAAe,CAAC,YAAY,CAAC,YAAY,EAAE,SAAS,CAAC;QAC9D,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC;QAC1D,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC;QAEjD,CAAA,EAAA,GAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,MAAA,GAAA,EAAA,CAAA,IAAA,CAAA,IAAA,EAAG,KAAK,EAAE,SAAS,CAAC;IACnC;AAEO,IAAA,QAAQ,CAAC,KAAY,EAAA;AAC1B,QAAA,IAAI,CAAC,YAAY,GAAG,KAAK;AACzB,QAAA,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;AAC1B,QAAA,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;IACxB;IAEO,MAAM,GAAA;AACX,QAAA,MAAM,IAAI,GACR,IAAI,CAAC,iBAAiB,EAAE,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM;AACxD,QAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IACrB;IAEO,QAAQ,GAAA;QACb,OAAO,IAAI,CAAC,YAAY;IAC1B;IAEO,OAAO,GAAA;AACZ,QAAA,IAAI,IAAI,CAAC,UAAU,EAAE;YACnB,IAAI,CAAC,UAAU,CAAC,mBAAmB,CACjC,QAAQ,EACR,IAAI,CAAC,kBAAkB,CACxB;QACH;IACF;AACD;AAEK,SAAU,iBAAiB,CAC/B,MAAoB,EAAA;AAEpB,IAAA,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC;AAChC;;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,44 +1,25 @@
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;
1
+ type Theme = "light" | "dark" | "auto";
2
+
3
+ interface ThemeConfig {
4
+ storageKey?: string;
5
+ defaultTheme?: Theme;
6
+ useSystemPreference?: boolean;
7
+ onChange?: (theme: Theme, effective: "light" | "dark") => void;
25
8
  }
26
9
 
27
- declare function createThemeManager(options?: ThemeManagerOptions): ThemeManager;
10
+ declare class ThemeToggle {
11
+ constructor(config?: ThemeConfig);
12
+
13
+ setTheme(theme: Theme): void;
14
+ toggle(): void;
15
+ getTheme(): Theme;
16
+ getEffectiveTheme(): "light" | "dark";
17
+ destroy(): void;
18
+ }
28
19
 
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;
20
+ declare function createThemeToggle(
21
+ config?: ThemeConfig
22
+ ): ThemeToggle;
43
23
 
44
- export { type Theme, type ThemeManager, type ThemeManagerOptions, type ThemeMode, type ThemeSubscriber, createThemeManager, createUseTheme };
24
+ export { ThemeToggle, createThemeToggle };
25
+ export type { Theme, ThemeConfig };
package/dist/index.mjs CHANGED
@@ -1,209 +1,90 @@
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]);
1
+ const isBrowser = typeof window !== "undefined" &&
2
+ typeof document !== "undefined";
3
+ class ThemeToggle {
4
+ constructor(config = {}) {
5
+ var _a, _b, _c;
6
+ this.handleSystemChange = () => {
7
+ if (this.currentTheme === "auto") {
8
+ this.applyTheme("auto");
9
+ }
10
+ };
11
+ this.storageKey = (_a = config.storageKey) !== null && _a !== void 0 ? _a : "app-theme";
12
+ this.useSystemPreference = (_b = config.useSystemPreference) !== null && _b !== void 0 ? _b : true;
13
+ this.onChange = config.onChange;
14
+ const stored = this.getStoredTheme();
15
+ this.currentTheme = (_c = stored !== null && stored !== void 0 ? stored : config.defaultTheme) !== null && _c !== void 0 ? _c : "auto";
16
+ this.init();
14
17
  }
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);
18
+ init() {
19
+ if (!isBrowser)
20
+ return;
21
+ if (this.useSystemPreference) {
22
+ this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
23
+ this.mediaQuery.addEventListener("change", this.handleSystemChange);
24
+ }
25
+ this.applyTheme(this.currentTheme);
49
26
  }
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;
27
+ getStoredTheme() {
28
+ if (!isBrowser)
29
+ return null;
30
+ try {
31
+ const stored = localStorage.getItem(this.storageKey);
32
+ return stored === "light" || stored === "dark" || stored === "auto"
33
+ ? stored
34
+ : null;
35
+ }
36
+ catch (_a) {
37
+ return null;
38
+ }
63
39
  }
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;
40
+ setStoredTheme(theme) {
41
+ if (!isBrowser)
42
+ return;
43
+ try {
44
+ localStorage.setItem(this.storageKey, theme);
45
+ }
46
+ catch (_a) {
47
+ /* noop */
48
+ }
49
+ }
50
+ getEffectiveTheme() {
51
+ var _a;
52
+ if (this.currentTheme === "auto") {
53
+ return ((_a = this.mediaQuery) === null || _a === void 0 ? void 0 : _a.matches) ? "dark" : "light";
54
+ }
55
+ return this.currentTheme;
56
+ }
57
+ applyTheme(theme) {
58
+ var _a;
59
+ if (!isBrowser)
60
+ return;
61
+ const effective = this.getEffectiveTheme();
62
+ document.documentElement.setAttribute("data-theme", effective);
63
+ document.documentElement.classList.remove("light", "dark");
64
+ document.documentElement.classList.add(effective);
65
+ (_a = this.onChange) === null || _a === void 0 ? void 0 : _a.call(this, theme, effective);
145
66
  }
146
- theme = resolveTheme(mode);
147
- applyTheme(theme, config.attribute);
148
- if (mode === "system") {
149
- unsubscribeSystem = subscribeSystemTheme(handleSystemChange);
67
+ setTheme(theme) {
68
+ this.currentTheme = theme;
69
+ this.setStoredTheme(theme);
70
+ this.applyTheme(theme);
150
71
  }
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
72
  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
- };
73
+ const next = this.getEffectiveTheme() === "dark" ? "light" : "dark";
74
+ this.setTheme(next);
75
+ }
76
+ getTheme() {
77
+ return this.currentTheme;
78
+ }
79
+ destroy() {
80
+ if (this.mediaQuery) {
81
+ this.mediaQuery.removeEventListener("change", this.handleSystemChange);
82
+ }
179
83
  }
180
- };
181
84
  }
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
- };
85
+ function createThemeToggle(config) {
86
+ return new ThemeToggle(config);
204
87
  }
205
- export {
206
- createThemeManager,
207
- createUseTheme
208
- };
209
- //# sourceMappingURL=index.mjs.map
88
+
89
+ export { ThemeToggle, createThemeToggle };
90
+ //# sourceMappingURL=index.mjs.map
@@ -1 +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":"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","names":[]}
1
+ {"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["export type Theme = \"light\" | \"dark\" | \"auto\";\n\nexport interface ThemeConfig {\n storageKey?: string;\n defaultTheme?: Theme;\n useSystemPreference?: boolean;\n onChange?: (theme: Theme, effective: \"light\" | \"dark\") => void;\n}\n\nconst isBrowser =\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\";\n\nexport class ThemeToggle {\n private storageKey: string;\n private currentTheme: Theme;\n private useSystemPreference: boolean;\n private onChange?: ThemeConfig[\"onChange\"];\n private mediaQuery?: MediaQueryList;\n\n private handleSystemChange = () => {\n if (this.currentTheme === \"auto\") {\n this.applyTheme(\"auto\");\n }\n };\n\n constructor(config: ThemeConfig = {}) {\n this.storageKey = config.storageKey ?? \"app-theme\";\n this.useSystemPreference = config.useSystemPreference ?? true;\n this.onChange = config.onChange;\n\n const stored = this.getStoredTheme();\n this.currentTheme = stored ?? config.defaultTheme ?? \"auto\";\n\n this.init();\n }\n\n private init(): void {\n if (!isBrowser) return;\n\n if (this.useSystemPreference) {\n this.mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n this.mediaQuery.addEventListener(\"change\", this.handleSystemChange);\n }\n\n this.applyTheme(this.currentTheme);\n }\n\n private getStoredTheme(): Theme | null {\n if (!isBrowser) return null;\n\n try {\n const stored = localStorage.getItem(this.storageKey);\n return stored === \"light\" || stored === \"dark\" || stored === \"auto\"\n ? stored\n : null;\n } catch {\n return null;\n }\n }\n\n private setStoredTheme(theme: Theme): void {\n if (!isBrowser) return;\n\n try {\n localStorage.setItem(this.storageKey, theme);\n } catch {\n /* noop */\n }\n }\n\n public getEffectiveTheme(): \"light\" | \"dark\" {\n if (this.currentTheme === \"auto\") {\n return this.mediaQuery?.matches ? \"dark\" : \"light\";\n }\n return this.currentTheme;\n }\n\n private applyTheme(theme: Theme): void {\n if (!isBrowser) return;\n\n const effective = this.getEffectiveTheme();\n\n document.documentElement.setAttribute(\"data-theme\", effective);\n document.documentElement.classList.remove(\"light\", \"dark\");\n document.documentElement.classList.add(effective);\n\n this.onChange?.(theme, effective);\n }\n\n public setTheme(theme: Theme): void {\n this.currentTheme = theme;\n this.setStoredTheme(theme);\n this.applyTheme(theme);\n }\n\n public toggle(): void {\n const next =\n this.getEffectiveTheme() === \"dark\" ? \"light\" : \"dark\";\n this.setTheme(next);\n }\n\n public getTheme(): Theme {\n return this.currentTheme;\n }\n\n public destroy(): void {\n if (this.mediaQuery) {\n this.mediaQuery.removeEventListener(\n \"change\",\n this.handleSystemChange\n );\n }\n }\n}\n\nexport function createThemeToggle(\n config?: ThemeConfig\n): ThemeToggle {\n return new ThemeToggle(config);\n}"],"names":[],"mappings":"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"}
package/dist/react.cjs ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var index = require('./index.cjs');
5
+
6
+ function _interopNamespaceDefault(e) {
7
+ var n = Object.create(null);
8
+ if (e) {
9
+ Object.keys(e).forEach(function (k) {
10
+ if (k !== 'default') {
11
+ var d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: function () { return e[k]; }
15
+ });
16
+ }
17
+ });
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+
23
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
24
+
25
+ /**
26
+ * Create a React hook bound to a single ThemeToggle instance.
27
+ *
28
+ * IMPORTANT:
29
+ * - Call this ONCE at module scope
30
+ * - Do NOT call inside components
31
+ */
32
+ function createUseTheme(config) {
33
+ const manager = index.createThemeToggle(config);
34
+ return function useTheme() {
35
+ const [theme, setTheme] = React__namespace.useState(manager.getTheme());
36
+ const [effectiveTheme, setEffectiveTheme] = React__namespace.useState(manager.getEffectiveTheme());
37
+ React__namespace.useEffect(() => {
38
+ const prev = manager["onChange"];
39
+ manager["onChange"] = (t, e) => {
40
+ setTheme(t);
41
+ setEffectiveTheme(e);
42
+ };
43
+ return () => {
44
+ manager["onChange"] = prev;
45
+ };
46
+ }, []);
47
+ return {
48
+ theme,
49
+ effectiveTheme,
50
+ setTheme: (t) => manager.setTheme(t),
51
+ toggleTheme: () => manager.toggle(),
52
+ };
53
+ };
54
+ }
55
+
56
+ exports.createUseTheme = createUseTheme;
57
+ //# sourceMappingURL=react.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.cjs","sources":["../src/react-adapter.ts"],"sourcesContent":["import * as React from \"react\";\nimport type { Theme, ThemeConfig } from \"./index\";\nimport { createThemeToggle } from \"./index\";\n\ntype UseThemeResult = {\n theme: Theme;\n effectiveTheme: \"light\" | \"dark\";\n setTheme: (theme: Theme) => void;\n toggleTheme: () => void;\n};\n\n/**\n * Create a React hook bound to a single ThemeToggle instance.\n *\n * IMPORTANT:\n * - Call this ONCE at module scope\n * - Do NOT call inside components\n */\nexport function createUseTheme(config?: ThemeConfig) {\n const manager = createThemeToggle(config);\n\n return function useTheme(): UseThemeResult {\n const [theme, setTheme] = React.useState<Theme>(\n manager.getTheme()\n );\n const [effectiveTheme, setEffectiveTheme] = React.useState<\n \"light\" | \"dark\"\n >(manager.getEffectiveTheme());\n\n React.useEffect(() => {\n const prev = manager[\"onChange\"];\n\n manager[\"onChange\"] = (t, e) => {\n setTheme(t);\n setEffectiveTheme(e);\n };\n\n return () => {\n manager[\"onChange\"] = prev;\n };\n }, []);\n\n return {\n theme,\n effectiveTheme,\n setTheme: (t) => manager.setTheme(t),\n toggleTheme: () => manager.toggle(),\n };\n };\n}"],"names":["createThemeToggle","React"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAWA;;;;;;AAMG;AACG,SAAU,cAAc,CAAC,MAAoB,EAAA;AACjD,IAAA,MAAM,OAAO,GAAGA,uBAAiB,CAAC,MAAM,CAAC;AAEzC,IAAA,OAAO,SAAS,QAAQ,GAAA;AACtB,QAAA,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAGC,gBAAK,CAAC,QAAQ,CACtC,OAAO,CAAC,QAAQ,EAAE,CACnB;AACD,QAAA,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAGA,gBAAK,CAAC,QAAQ,CAExD,OAAO,CAAC,iBAAiB,EAAE,CAAC;AAE9B,QAAAA,gBAAK,CAAC,SAAS,CAAC,MAAK;AACnB,YAAA,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC;YAEhC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAI;gBAC7B,QAAQ,CAAC,CAAC,CAAC;gBACX,iBAAiB,CAAC,CAAC,CAAC;AACtB,YAAA,CAAC;AAED,YAAA,OAAO,MAAK;AACV,gBAAA,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI;AAC5B,YAAA,CAAC;QACH,CAAC,EAAE,EAAE,CAAC;QAEN,OAAO;YACL,KAAK;YACL,cAAc;YACd,QAAQ,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AACpC,YAAA,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE;SACpC;AACH,IAAA,CAAC;AACH;;;;"}
@@ -0,0 +1,18 @@
1
+ import { ThemeConfig, Theme } from './index.js';
2
+
3
+ type UseThemeResult = {
4
+ theme: Theme;
5
+ effectiveTheme: "light" | "dark";
6
+ setTheme: (theme: Theme) => void;
7
+ toggleTheme: () => void;
8
+ };
9
+ /**
10
+ * Create a React hook bound to a single ThemeToggle instance.
11
+ *
12
+ * IMPORTANT:
13
+ * - Call this ONCE at module scope
14
+ * - Do NOT call inside components
15
+ */
16
+ declare function createUseTheme(config?: ThemeConfig): () => UseThemeResult;
17
+
18
+ export { createUseTheme };
package/dist/react.mjs ADDED
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import { createThemeToggle } from './index.mjs';
3
+
4
+ /**
5
+ * Create a React hook bound to a single ThemeToggle instance.
6
+ *
7
+ * IMPORTANT:
8
+ * - Call this ONCE at module scope
9
+ * - Do NOT call inside components
10
+ */
11
+ function createUseTheme(config) {
12
+ const manager = createThemeToggle(config);
13
+ return function useTheme() {
14
+ const [theme, setTheme] = React.useState(manager.getTheme());
15
+ const [effectiveTheme, setEffectiveTheme] = React.useState(manager.getEffectiveTheme());
16
+ React.useEffect(() => {
17
+ const prev = manager["onChange"];
18
+ manager["onChange"] = (t, e) => {
19
+ setTheme(t);
20
+ setEffectiveTheme(e);
21
+ };
22
+ return () => {
23
+ manager["onChange"] = prev;
24
+ };
25
+ }, []);
26
+ return {
27
+ theme,
28
+ effectiveTheme,
29
+ setTheme: (t) => manager.setTheme(t),
30
+ toggleTheme: () => manager.toggle(),
31
+ };
32
+ };
33
+ }
34
+
35
+ export { createUseTheme };
36
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","sources":["../src/react-adapter.ts"],"sourcesContent":["import * as React from \"react\";\nimport type { Theme, ThemeConfig } from \"./index\";\nimport { createThemeToggle } from \"./index\";\n\ntype UseThemeResult = {\n theme: Theme;\n effectiveTheme: \"light\" | \"dark\";\n setTheme: (theme: Theme) => void;\n toggleTheme: () => void;\n};\n\n/**\n * Create a React hook bound to a single ThemeToggle instance.\n *\n * IMPORTANT:\n * - Call this ONCE at module scope\n * - Do NOT call inside components\n */\nexport function createUseTheme(config?: ThemeConfig) {\n const manager = createThemeToggle(config);\n\n return function useTheme(): UseThemeResult {\n const [theme, setTheme] = React.useState<Theme>(\n manager.getTheme()\n );\n const [effectiveTheme, setEffectiveTheme] = React.useState<\n \"light\" | \"dark\"\n >(manager.getEffectiveTheme());\n\n React.useEffect(() => {\n const prev = manager[\"onChange\"];\n\n manager[\"onChange\"] = (t, e) => {\n setTheme(t);\n setEffectiveTheme(e);\n };\n\n return () => {\n manager[\"onChange\"] = prev;\n };\n }, []);\n\n return {\n theme,\n effectiveTheme,\n setTheme: (t) => manager.setTheme(t),\n toggleTheme: () => manager.toggle(),\n };\n };\n}"],"names":[],"mappings":";;;AAWA;;;;;;AAMG;AACG,SAAU,cAAc,CAAC,MAAoB,EAAA;AACjD,IAAA,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,CAAC;AAEzC,IAAA,OAAO,SAAS,QAAQ,GAAA;AACtB,QAAA,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,QAAQ,CACtC,OAAO,CAAC,QAAQ,EAAE,CACnB;AACD,QAAA,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAExD,OAAO,CAAC,iBAAiB,EAAE,CAAC;AAE9B,QAAA,KAAK,CAAC,SAAS,CAAC,MAAK;AACnB,YAAA,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC;YAEhC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAI;gBAC7B,QAAQ,CAAC,CAAC,CAAC;gBACX,iBAAiB,CAAC,CAAC,CAAC;AACtB,YAAA,CAAC;AAED,YAAA,OAAO,MAAK;AACV,gBAAA,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI;AAC5B,YAAA,CAAC;QACH,CAAC,EAAE,EAAE,CAAC;QAEN,OAAO;YACL,KAAK;YACL,cAAc;YACd,QAAQ,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AACpC,YAAA,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE;SACpC;AACH,IAAA,CAAC;AACH;;;;"}
package/package.json CHANGED
@@ -1,47 +1,56 @@
1
1
  {
2
- "name": "@pshah-lab/themeswitcher",
3
- "version": "0.1.1",
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"
2
+ "name": "@pshah-lab/themeswitcher",
3
+ "version": "0.1.3",
4
+ "description": "A lightweight, framework-agnostic theme manager with SSR-safe dark/light/system mode support",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./react": {
16
+ "import": "./dist/react.mjs",
17
+ "require": "./dist/react.cjs",
18
+ "types": "./dist/react.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "rollup -c",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "peerDependencies": {
29
+ "react": ">=16.8"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "react": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "devDependencies": {
37
+ "@rollup/plugin-typescript": "^11.1.6",
38
+ "@types/react": "^19.2.8",
39
+ "react": "^19.2.3",
40
+ "rollup": "^4.55.1",
41
+ "rollup-plugin-dts": "^6.3.0",
42
+ "typescript": "^5.9.3"
43
+ },
44
+ "keywords": [
45
+ "dark-mode",
46
+ "light-mode",
47
+ "theme",
48
+ "toggle",
49
+ "nextjs",
50
+ "react",
51
+ "ssr"
52
+ ],
53
+ "dependencies": {
54
+ "tslib": "^2.8.1"
13
55
  }
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
56
  }
package/README.md DELETED
@@ -1,207 +0,0 @@
1
- # @pshah-lab/themeswitcher
2
-
3
- A lightweight, framework-agnostic theme manager for **light**, **dark**, and **system** modes — with **SSR-safe no-flash support**.
4
-
5
- Designed to be:
6
- - Minimal
7
- - Predictable
8
- - Copy-paste friendly
9
- - Framework independent
10
-
11
- ---
12
-
13
- ## Features
14
-
15
- - Light / Dark / System theme support
16
- - Prevents flash of incorrect theme (FOUC)
17
- - SSR-safe (works before JS bundle loads)
18
- - No dependencies
19
- - Fully typed (TypeScript)
20
- - Works with plain JS, React, Vue, or any framework
21
-
22
- ---
23
-
24
- ## Installation
25
- ```bash
26
- npm install @pshah-lab/themeswitcher
27
- ```
28
-
29
- ---
30
-
31
- ## Core Usage (Framework-agnostic)
32
- ```javascript
33
- import { createThemeManager } from "@pshah-lab/themeswitcher";
34
-
35
- const theme = createThemeManager();
36
-
37
- theme.set("dark"); // "light" | "dark" | "system"
38
- theme.toggle(); // toggle light ↔ dark
39
- theme.get(); // resolved theme: "light" | "dark"
40
- theme.getMode(); // selected mode
41
- ```
42
-
43
- The theme is applied to the document using a data attribute:
44
- ```html
45
- <html data-theme="dark">
46
- ```
47
-
48
- Style your app using this attribute:
49
- ```css
50
- [data-theme="dark"] {
51
- background: #000;
52
- color: #fff;
53
- }
54
- ```
55
-
56
- ---
57
-
58
- ## React Usage
59
-
60
- Create the hook once at module scope.
61
- ```typescript
62
- // theme.ts
63
- import { createUseTheme } from "@pshah-lab/themeswitcher";
64
-
65
- export const useTheme = createUseTheme();
66
- ```
67
-
68
- Use it anywhere in your app:
69
- ```jsx
70
- function ThemeToggle() {
71
- const { theme, toggleTheme } = useTheme();
72
-
73
- return (
74
- <button onClick={toggleTheme}>
75
- Current theme: {theme}
76
- </button>
77
- );
78
- }
79
- ```
80
-
81
- ⚠️ **Do not call `createUseTheme()` inside components.**
82
- It must be created once and reused.
83
-
84
- ---
85
-
86
- ## Prevent Theme Flash (Recommended)
87
-
88
- When using light/dark themes, browsers may briefly render the page in the default theme before JavaScript loads. This causes a visible flash of incorrect theme.
89
-
90
- To prevent this, add the following script inline in your HTML `<head>`, before any CSS is loaded.
91
-
92
- ### Why this is needed
93
-
94
- - Runs before first paint
95
- - Applies the correct theme synchronously
96
- - Works before your JavaScript bundle loads
97
- - Required for SSR
98
- - Strongly recommended for all setups
99
-
100
- ### Add this to `<head>`
101
- ```html
102
- <script>
103
- (function () {
104
- try {
105
- var key = "theme-mode";
106
- var attribute = "data-theme";
107
-
108
- var stored = localStorage.getItem(key);
109
- var mode =
110
- stored === "light" || stored === "dark" || stored === "system"
111
- ? stored
112
- : "system";
113
-
114
- var isDark =
115
- window.matchMedia &&
116
- window.matchMedia("(prefers-color-scheme: dark)").matches;
117
-
118
- var theme =
119
- mode === "system"
120
- ? isDark
121
- ? "dark"
122
- : "light"
123
- : mode;
124
-
125
- document.documentElement.setAttribute(attribute, theme);
126
- } catch (e) {
127
- // fail silently
128
- }
129
- })();
130
- </script>
131
- ```
132
-
133
- Place it before your CSS:
134
- ```html
135
- <head>
136
- <!-- Prevent theme flash -->
137
- <script>/* pasted theme script */</script>
138
-
139
- <link rel="stylesheet" href="styles.css" />
140
- </head>
141
- ```
142
-
143
- ### Important Notes
144
-
145
- - Do not import this script from the package
146
- - Do not bundle it
147
- - Copy-paste is intentional and required for correct timing
148
- - The script logic matches the internal theme manager
149
-
150
- ---
151
-
152
- ## Configuration
153
- ```javascript
154
- createThemeManager({
155
- defaultMode: "system", // default
156
- storageKey: "theme-mode", // localStorage key
157
- attribute: "data-theme", // HTML attribute
158
- });
159
- ```
160
-
161
- ---
162
-
163
- ## API Reference
164
-
165
- ### `set(mode)`
166
-
167
- Set the theme mode.
168
- ```javascript
169
- theme.set("dark");
170
- ```
171
-
172
- ### `toggle()`
173
-
174
- Toggle between light and dark.
175
- ```javascript
176
- theme.toggle();
177
- ```
178
-
179
- ### `get()`
180
-
181
- Get the resolved theme.
182
- ```javascript
183
- theme.get(); // "light" | "dark"
184
- ```
185
-
186
- ### `getMode()`
187
-
188
- Get the selected mode.
189
- ```javascript
190
- theme.getMode(); // "light" | "dark" | "system"
191
- ```
192
-
193
- ---
194
-
195
- ## Design Philosophy
196
-
197
- - Explicit over magical
198
- - Copy-paste over runtime hacks
199
- - No global side effects
200
- - No framework lock-in
201
- - Predictable behavior
202
-
203
- ---
204
-
205
- ## License
206
-
207
- MIT
package/dist/index.d.mts DELETED
@@ -1,44 +0,0 @@
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 DELETED
@@ -1,234 +0,0 @@
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
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
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":[]}
@@ -1,22 +0,0 @@
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
- })();