theme-watcher 0.1.0 → 0.1.2

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 CHANGED
@@ -38,8 +38,10 @@ Mount once near your app root.
38
38
  Props:
39
39
  - `theme?: "light" | "dark"` controlled override
40
40
  - `storageKey?: string` default: `"theme-watcher"`
41
- - `attribute?: "data-theme" | "class"` default: `"data-theme"`
41
+ - `attribute?: "data-theme" | "class" | "both"` default: `"both"`
42
42
  - `defaultTheme?: "light" | "dark" | "system"` default: `"system"`
43
+ - `enableColorScheme?: boolean` default: `true`
44
+ - `variables?: { light?: Record<string, string>; dark?: Record<string, string> }`
43
45
 
44
46
  ### `useTheme()`
45
47
 
@@ -48,6 +50,7 @@ Returns:
48
50
  - `resolvedTheme` active applied theme (`"light" | "dark"`)
49
51
  - `source` where the current value came from (`"prop" | "storage" | "default" | "system"`)
50
52
  - `set(theme)` set and persist preference
53
+ - `setTheme(theme)` alias for compatibility with next-themes-style usage
51
54
  - `get()` get persisted preference
52
55
 
53
56
  ## Behavior notes
@@ -55,8 +58,35 @@ Returns:
55
58
  - Priority order: `theme prop` -> `localStorage` -> `defaultTheme` -> system theme.
56
59
  - `system` mode updates live when `prefers-color-scheme` changes.
57
60
  - Cross-tab updates are synced through `storage` events.
61
+ - By default, it applies both `data-theme` and `html.dark` so Tailwind/shadcn and CSS-var setups both work.
58
62
  - Package is ESM-only.
59
63
 
64
+ ## CSS variable support
65
+
66
+ ```tsx
67
+ <ThemeWatcher
68
+ variables={{
69
+ light: {
70
+ "--background": "#ffffff",
71
+ "--foreground": "#111111"
72
+ },
73
+ dark: {
74
+ "--background": "#111111",
75
+ "--foreground": "#ffffff"
76
+ }
77
+ }}
78
+ />
79
+ ```
80
+
81
+ Then in CSS:
82
+
83
+ ```css
84
+ body {
85
+ background: var(--background);
86
+ color: var(--foreground);
87
+ }
88
+ ```
89
+
60
90
  ## Development
61
91
 
62
92
  This repo uses Bun for package management and scripts, but the published package has no Bun runtime dependency.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  type Theme = "light" | "dark";
2
2
  type ThemePreference = Theme | "system";
3
3
  type ThemeSource = "prop" | "storage" | "default" | "system";
4
- type ThemeAttribute = "data-theme" | "class";
4
+ type ThemeAttribute = "data-theme" | "class" | "both";
5
+ type ThemeVariables = Partial<Record<Theme, Record<string, string>>>;
5
6
  interface ThemeState {
6
7
  theme: ThemePreference;
7
8
  resolvedTheme: Theme;
@@ -12,13 +13,16 @@ interface ThemeWatcherProps {
12
13
  storageKey?: string;
13
14
  attribute?: ThemeAttribute;
14
15
  defaultTheme?: ThemePreference;
16
+ variables?: ThemeVariables;
17
+ enableColorScheme?: boolean;
15
18
  }
16
19
  interface ThemeApi extends ThemeState {
17
20
  set: (theme: ThemePreference) => void;
21
+ setTheme: (theme: ThemePreference) => void;
18
22
  get: () => ThemePreference;
19
23
  }
20
24
 
21
- declare function ThemeWatcher({ theme, storageKey, attribute, defaultTheme }: ThemeWatcherProps): null;
25
+ declare function ThemeWatcher({ theme, storageKey, attribute, defaultTheme, variables, enableColorScheme }: ThemeWatcherProps): null;
22
26
 
23
27
  declare function useTheme(): ThemeApi;
24
28
 
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ import { useEffect } from "react";
4
4
  // src/theme-store.ts
5
5
  var DEFAULT_CONFIG = {
6
6
  storageKey: "theme-watcher",
7
- attribute: "data-theme",
8
- defaultTheme: "system"
7
+ attribute: "both",
8
+ defaultTheme: "system",
9
+ enableColorScheme: true
9
10
  };
10
11
  var VALID_PREFERENCES = /* @__PURE__ */ new Set(["light", "dark", "system"]);
11
12
  var config = { ...DEFAULT_CONFIG };
@@ -64,13 +65,38 @@ function resolveTheme(propTheme, stored, defaultTheme) {
64
65
  function applyDomTheme(resolvedTheme) {
65
66
  if (!isBrowserReady()) return;
66
67
  const root = document.documentElement;
67
- if (config.attribute === "class") {
68
+ if (config.attribute === "class" || config.attribute === "both") {
68
69
  root.classList.toggle("dark", resolvedTheme === "dark");
69
- root.classList.toggle("light", resolvedTheme === "light");
70
+ root.classList.remove("light");
71
+ }
72
+ if (config.attribute === "class") {
70
73
  return;
71
74
  }
72
75
  root.setAttribute("data-theme", resolvedTheme);
73
76
  }
77
+ function applyThemeVariables(resolvedTheme) {
78
+ if (!isBrowserReady()) return;
79
+ const root = document.documentElement;
80
+ const lightVars = config.variables?.light ?? {};
81
+ const darkVars = config.variables?.dark ?? {};
82
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(lightVars), ...Object.keys(darkVars)]);
83
+ for (const key of allKeys) {
84
+ root.style.removeProperty(key);
85
+ }
86
+ const nextVars = config.variables?.[resolvedTheme];
87
+ if (!nextVars) return;
88
+ for (const [key, value] of Object.entries(nextVars)) {
89
+ root.style.setProperty(key, value);
90
+ }
91
+ }
92
+ function applyColorScheme(resolvedTheme) {
93
+ if (!isBrowserReady()) return;
94
+ if (!config.enableColorScheme) {
95
+ document.documentElement.style.removeProperty("color-scheme");
96
+ return;
97
+ }
98
+ document.documentElement.style.setProperty("color-scheme", resolvedTheme);
99
+ }
74
100
  function emit() {
75
101
  for (const callback of subscribers) {
76
102
  callback();
@@ -80,6 +106,8 @@ function recompute() {
80
106
  const stored = readStoredTheme();
81
107
  state = resolveTheme(controlledTheme, stored, config.defaultTheme);
82
108
  applyDomTheme(state.resolvedTheme);
109
+ applyThemeVariables(state.resolvedTheme);
110
+ applyColorScheme(state.resolvedTheme);
83
111
  emit();
84
112
  }
85
113
  function handleMediaChange() {
@@ -137,6 +165,8 @@ function setTheme(theme) {
137
165
  writeStoredTheme(theme);
138
166
  state = resolveTheme(controlledTheme, theme, config.defaultTheme);
139
167
  applyDomTheme(state.resolvedTheme);
168
+ applyThemeVariables(state.resolvedTheme);
169
+ applyColorScheme(state.resolvedTheme);
140
170
  emit();
141
171
  }
142
172
  function getTheme() {
@@ -157,12 +187,14 @@ function getServerSnapshot() {
157
187
  function ThemeWatcher({
158
188
  theme,
159
189
  storageKey = "theme-watcher",
160
- attribute = "data-theme",
161
- defaultTheme = "system"
190
+ attribute = "both",
191
+ defaultTheme = "system",
192
+ variables,
193
+ enableColorScheme = true
162
194
  }) {
163
195
  useEffect(() => {
164
- return mountWatcher({ storageKey, attribute, defaultTheme }, theme);
165
- }, [theme, storageKey, attribute, defaultTheme]);
196
+ return mountWatcher({ storageKey, attribute, defaultTheme, variables, enableColorScheme }, theme);
197
+ }, [theme, storageKey, attribute, defaultTheme, variables, enableColorScheme]);
166
198
  return null;
167
199
  }
168
200
 
@@ -173,6 +205,7 @@ function useTheme() {
173
205
  return {
174
206
  ...state2,
175
207
  set: setTheme,
208
+ setTheme,
176
209
  get: getTheme
177
210
  };
178
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theme-watcher",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Plug-and-play theme watcher for React SPAs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",