@vueless/storybook-dark-mode 10.0.7 → 10.0.8-beta.1

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/manager.js CHANGED
@@ -86,10 +86,16 @@ updateManager(store());
86
86
 
87
87
  // src/Tool.tsx
88
88
  function DarkMode({ api }) {
89
- const [isDark, setDark] = React.useState(prefersDark.matches);
89
+ const [isDark, setDark] = React.useState(() => {
90
+ const current = store().current;
91
+ return current === "dark";
92
+ });
90
93
  const darkModeParams = useParameter("darkMode", {});
91
94
  const { current: defaultMode, stylePreview, ...params } = darkModeParams;
92
95
  const channel = api.getChannel();
96
+ const darkModeParamsRef = React.useRef(darkModeParams);
97
+ darkModeParamsRef.current = darkModeParams;
98
+ const deferredSysThemeRef = React.useRef(null);
93
99
  const userHasExplicitlySetTheTheme = React.useMemo(
94
100
  () => store(params).userHasExplicitlySetTheTheme,
95
101
  [params]
@@ -116,12 +122,15 @@ function DarkMode({ api }) {
116
122
  },
117
123
  [setMode]
118
124
  );
119
- function prefersDarkUpdate(event) {
120
- if (userHasExplicitlySetTheTheme || defaultMode) {
121
- return;
122
- }
123
- updateMode(event.matches ? "dark" : "light");
124
- }
125
+ const prefersDarkUpdate = React.useCallback(
126
+ (event) => {
127
+ if (userHasExplicitlySetTheTheme || defaultMode) {
128
+ return;
129
+ }
130
+ updateMode(event.matches ? "dark" : "light");
131
+ },
132
+ [userHasExplicitlySetTheTheme, defaultMode, updateMode]
133
+ );
125
134
  const renderTheme = React.useCallback(() => {
126
135
  const { current = "light" } = store();
127
136
  setMode(current);
@@ -132,13 +141,49 @@ function DarkMode({ api }) {
132
141
  updateStore({ ...currentStore2, userHasExplicitlySetTheTheme: true });
133
142
  };
134
143
  React.useEffect(() => {
144
+ if (deferredSysThemeRef.current !== null) {
145
+ clearTimeout(deferredSysThemeRef.current);
146
+ deferredSysThemeRef.current = null;
147
+ }
135
148
  const currentStore2 = store();
136
- updateStore({
149
+ const previewCurrent = darkModeParams.current;
150
+ const persistedCurrent = currentStore2.current;
151
+ const base = {
137
152
  ...currentStore2,
138
- ...darkModeParams,
139
- current: currentStore2.current || darkModeParams.current
140
- });
153
+ ...darkModeParams
154
+ };
155
+ if (previewCurrent !== void 0) {
156
+ updateStore({ ...base, current: previewCurrent });
157
+ renderTheme();
158
+ return;
159
+ }
160
+ if (persistedCurrent) {
161
+ updateStore({ ...base, current: persistedCurrent });
162
+ renderTheme();
163
+ return;
164
+ }
165
+ updateStore({ ...base, current: "light" });
141
166
  renderTheme();
167
+ deferredSysThemeRef.current = setTimeout(() => {
168
+ deferredSysThemeRef.current = null;
169
+ const latestParams = darkModeParamsRef.current;
170
+ if (latestParams.current !== void 0) {
171
+ const cs2 = store();
172
+ updateStore({ ...cs2, ...latestParams, current: latestParams.current });
173
+ renderTheme();
174
+ return;
175
+ }
176
+ const cs = store();
177
+ const nextCurrent = prefersDark.matches ? "dark" : "light";
178
+ updateStore({ ...cs, ...latestParams, current: nextCurrent });
179
+ renderTheme();
180
+ }, 0);
181
+ return () => {
182
+ if (deferredSysThemeRef.current !== null) {
183
+ clearTimeout(deferredSysThemeRef.current);
184
+ deferredSysThemeRef.current = null;
185
+ }
186
+ };
142
187
  }, [darkModeParams, renderTheme]);
143
188
  React.useEffect(() => {
144
189
  channel.on(STORY_CHANGED, renderTheme);
@@ -151,21 +196,19 @@ function DarkMode({ api }) {
151
196
  channel.removeListener(DOCS_RENDERED, renderTheme);
152
197
  prefersDark.removeListener(prefersDarkUpdate);
153
198
  };
154
- });
199
+ }, [channel, renderTheme, prefersDarkUpdate]);
155
200
  React.useEffect(() => {
156
201
  channel.on(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
157
202
  return () => {
158
203
  channel.removeListener(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
159
204
  };
160
- });
205
+ }, [channel, updateMode]);
161
206
  React.useEffect(() => {
162
207
  if (userHasExplicitlySetTheTheme) {
163
208
  return;
164
209
  }
165
210
  if (defaultMode) {
166
211
  updateMode(defaultMode);
167
- } else if (prefersDark.matches) {
168
- updateMode("dark");
169
212
  }
170
213
  }, [defaultMode, updateMode, userHasExplicitlySetTheTheme]);
171
214
  return /* @__PURE__ */ React.createElement(
@@ -185,7 +228,7 @@ var Tool_default = DarkMode;
185
228
 
186
229
  // src/preset/manager.tsx
187
230
  var currentStore = store();
188
- var currentTheme = currentStore.current || prefersDark.matches && "dark" || "light";
231
+ var currentTheme = currentStore.current ?? "light";
189
232
  addons.setConfig({
190
233
  theme: {
191
234
  ...themes[currentTheme],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vueless/storybook-dark-mode",
3
- "version": "10.0.7",
3
+ "version": "10.0.8-beta.1",
4
4
  "description": "Toggle between light and dark mode in Storybook",
5
5
  "author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
6
6
  "repository": {
@@ -25,37 +25,37 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@storybook/global": "^5.0.0",
28
- "lodash-es": "^4.17.23"
28
+ "lodash-es": "^4.18.1"
29
29
  },
30
30
  "devDependencies": {
31
- "@eslint/js": "^9.33.0",
31
+ "@eslint/js": "^9.39.4",
32
32
  "@release-it/bumper": "^7.0.5",
33
- "@release-it/conventional-changelog": "^10.0.4",
34
- "@storybook/builder-vite": "^10.0.4",
35
- "@storybook/icons": "^1.4.0",
36
- "@storybook/react": "^10.0.4",
37
- "@storybook/react-vite": "^10.0.4",
38
- "@stylistic/eslint-plugin": "^5.2.3",
33
+ "@release-it/conventional-changelog": "^11.0.0",
34
+ "@storybook/builder-vite": "^10.3.5",
35
+ "@storybook/icons": "^2.0.1",
36
+ "@storybook/react": "^10.3.5",
37
+ "@storybook/react-vite": "^10.3.5",
38
+ "@stylistic/eslint-plugin": "^5.10.0",
39
39
  "@types/lodash-es": "^4.17.12",
40
- "@types/node": "^24.3.0",
41
- "@types/react": "^18.0.26",
42
- "eslint": "^9.33.0",
40
+ "@types/node": "^25.6.0",
41
+ "@types/react": "^19.2.14",
42
+ "eslint": "^9.39.4",
43
43
  "eslint-config-prettier": "^10.1.8",
44
- "eslint-plugin-prettier": "^5.5.4",
44
+ "eslint-plugin-prettier": "^5.5.5",
45
45
  "eslint-plugin-react": "^7.37.5",
46
- "eslint-plugin-react-hooks": "^5.1.0",
47
- "globals": "^15.14.0",
48
- "prettier": "^3.6.2",
49
- "react": "^18.2.0",
50
- "react-dom": "^18.2.0",
51
- "release-it": "^19.2.4",
52
- "rimraf": "^3.0.2",
53
- "storybook": "^10.0.4",
46
+ "eslint-plugin-react-hooks": "^7.1.1",
47
+ "globals": "^17.5.0",
48
+ "prettier": "^3.8.3",
49
+ "react": "^19.2.5",
50
+ "react-dom": "^19.2.5",
51
+ "release-it": "^20.0.1",
52
+ "rimraf": "^6.1.3",
53
+ "storybook": "^10.3.5",
54
54
  "ts-node": "^10.9.2",
55
- "tsup": "^8.0.0",
56
- "typescript": "^5.9.2",
57
- "typescript-eslint": "^8.40.0",
58
- "vite": "^7.1.11"
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^6.0.3",
57
+ "typescript-eslint": "^8.59.0",
58
+ "vite": "^8.0.10"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "storybook": "^10.0.0"
package/src/Tool.tsx CHANGED
@@ -21,10 +21,18 @@ interface DarkModeProps {
21
21
 
22
22
  /** A toolbar icon to toggle between dark and light themes in storybook */
23
23
  export function DarkMode({ api }: DarkModeProps) {
24
- const [isDark, setDark] = React.useState(prefersDark.matches);
24
+ const [isDark, setDark] = React.useState(() => {
25
+ const current = store().current;
26
+
27
+ return current === "dark";
28
+ });
25
29
  const darkModeParams = useParameter<Partial<DarkModeStore>>("darkMode", {});
26
30
  const { current: defaultMode, stylePreview, ...params } = darkModeParams;
27
31
  const channel = api.getChannel();
32
+ const darkModeParamsRef = React.useRef(darkModeParams);
33
+
34
+ darkModeParamsRef.current = darkModeParams;
35
+ const deferredSysThemeRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
28
36
  // Save custom themes on init
29
37
  const userHasExplicitlySetTheTheme = React.useMemo(
30
38
  () => store(params).userHasExplicitlySetTheTheme,
@@ -60,13 +68,16 @@ export function DarkMode({ api }: DarkModeProps) {
60
68
  );
61
69
 
62
70
  /** Update the theme based on the color preference */
63
- function prefersDarkUpdate(event: MediaQueryListEvent) {
64
- if (userHasExplicitlySetTheTheme || defaultMode) {
65
- return;
66
- }
71
+ const prefersDarkUpdate = React.useCallback(
72
+ (event: MediaQueryListEvent) => {
73
+ if (userHasExplicitlySetTheTheme || defaultMode) {
74
+ return;
75
+ }
67
76
 
68
- updateMode(event.matches ? "dark" : "light");
69
- }
77
+ updateMode(event.matches ? "dark" : "light");
78
+ },
79
+ [userHasExplicitlySetTheTheme, defaultMode, updateMode],
80
+ );
70
81
 
71
82
  /** Render the current theme */
72
83
  const renderTheme = React.useCallback(() => {
@@ -85,17 +96,67 @@ export function DarkMode({ api }: DarkModeProps) {
85
96
 
86
97
  /** When storybook params change update the stored themes */
87
98
  React.useEffect(() => {
99
+ if (deferredSysThemeRef.current !== null) {
100
+ clearTimeout(deferredSysThemeRef.current);
101
+ deferredSysThemeRef.current = null;
102
+ }
103
+
88
104
  const currentStore = store();
105
+ const previewCurrent = darkModeParams.current;
106
+ const persistedCurrent = currentStore.current;
89
107
 
90
- // Ensure we use the stores `current` value first to persist
91
- // themeing between page loads and story changes.
92
- updateStore({
108
+ const base = {
93
109
  ...currentStore,
94
110
  ...darkModeParams,
95
- current: currentStore.current || darkModeParams.current,
96
- });
111
+ };
112
+
113
+ if (previewCurrent !== undefined) {
114
+ updateStore({ ...base, current: previewCurrent });
115
+ renderTheme();
116
+
117
+ return;
118
+ }
119
+
120
+ if (persistedCurrent) {
121
+ updateStore({ ...base, current: persistedCurrent });
122
+ renderTheme();
123
+
124
+ return;
125
+ }
126
+
127
+ // Preview `darkMode.current` can hydrate after the first commit. Avoid applying
128
+ // prefers-color-scheme until then so `current: 'light'` is not overwritten.
129
+ updateStore({ ...base, current: "light" });
97
130
  renderTheme();
131
+
132
+ deferredSysThemeRef.current = setTimeout(() => {
133
+ deferredSysThemeRef.current = null;
134
+ const latestParams = darkModeParamsRef.current;
135
+
136
+ if (latestParams.current !== undefined) {
137
+ const cs = store();
138
+
139
+ updateStore({ ...cs, ...latestParams, current: latestParams.current });
140
+ renderTheme();
141
+
142
+ return;
143
+ }
144
+
145
+ const cs = store();
146
+ const nextCurrent = prefersDark.matches ? "dark" : "light";
147
+
148
+ updateStore({ ...cs, ...latestParams, current: nextCurrent });
149
+ renderTheme();
150
+ }, 0);
151
+
152
+ return () => {
153
+ if (deferredSysThemeRef.current !== null) {
154
+ clearTimeout(deferredSysThemeRef.current);
155
+ deferredSysThemeRef.current = null;
156
+ }
157
+ };
98
158
  }, [darkModeParams, renderTheme]);
159
+
99
160
  React.useEffect(() => {
100
161
  channel.on(STORY_CHANGED, renderTheme);
101
162
  channel.on(SET_STORIES, renderTheme);
@@ -108,14 +169,15 @@ export function DarkMode({ api }: DarkModeProps) {
108
169
  channel.removeListener(DOCS_RENDERED, renderTheme);
109
170
  prefersDark.removeListener(prefersDarkUpdate);
110
171
  };
111
- });
172
+ }, [channel, renderTheme, prefersDarkUpdate]);
173
+
112
174
  React.useEffect(() => {
113
175
  channel.on(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
114
176
 
115
177
  return () => {
116
178
  channel.removeListener(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
117
179
  };
118
- });
180
+ }, [channel, updateMode]);
119
181
  // Storybook's first render doesn't have the global user params loaded so we
120
182
  // need the effect to run whenever defaultMode is updated
121
183
  React.useEffect(() => {
@@ -126,8 +188,6 @@ export function DarkMode({ api }: DarkModeProps) {
126
188
 
127
189
  if (defaultMode) {
128
190
  updateMode(defaultMode);
129
- } else if (prefersDark.matches) {
130
- updateMode("dark");
131
191
  }
132
192
  }, [defaultMode, updateMode, userHasExplicitlySetTheTheme]);
133
193
 
@@ -4,10 +4,10 @@ import { themes } from "storybook/theming";
4
4
  import * as React from "react";
5
5
 
6
6
  import Tool from "../Tool";
7
- import { prefersDark, store } from "../store";
7
+ import { store } from "../store";
8
8
 
9
9
  const currentStore = store();
10
- const currentTheme = currentStore.current || (prefersDark.matches && "dark") || "light";
10
+ const currentTheme = currentStore.current ?? "light";
11
11
 
12
12
  addons.setConfig({
13
13
  theme: {