@vueless/storybook-dark-mode 9.0.9 → 10.0.1-beta.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 +3 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +86 -0
- package/dist/manager.js +199 -0
- package/dist/preset.js +6 -0
- package/package.json +61 -41
- package/preset.ts +2 -0
- package/src/Tool.tsx +23 -24
- package/src/index.tsx +2 -0
- package/src/preset/manager.tsx +1 -2
- package/src/preset.ts +3 -0
- package/dist/cjs/Tool.js +0 -251
- package/dist/cjs/constants.js +0 -8
- package/dist/cjs/index.js +0 -48
- package/dist/cjs/preset/manager.js +0 -34
- package/dist/esm/Tool.js +0 -242
- package/dist/esm/constants.js +0 -2
- package/dist/esm/index.js +0 -30
- package/dist/esm/preset/manager.js +0 -31
- package/dist/ts/Tool.d.ts +0 -35
- package/dist/ts/constants.d.ts +0 -2
- package/dist/ts/index.d.ts +0 -5
- package/dist/ts/preset/manager.d.ts +0 -1
- package/preset.js +0 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Storybook Dark Mode
|
|
2
2
|
|
|
3
|
-
A Storybook
|
|
3
|
+
A Storybook v10-optimized addon that enables users to toggle between dark and light modes. For support with earlier Storybook versions, see the [original addon](https://github.com/hipstersmoothie/storybook-dark-mode).
|
|
4
4
|
|
|
5
5
|
The project is supported and maintained by the [Vueless UI](https://github.com/vuelessjs/vueless) core team. | [See the demo](https://ui.vueless.com) 🌗
|
|
6
6
|
|
|
@@ -262,6 +262,7 @@ export const decorators = [
|
|
|
262
262
|
Docs have a dedicated container component which will _not_ be themed unless you explicitly configure it:
|
|
263
263
|
|
|
264
264
|
```js
|
|
265
|
+
import { DocsContainer } from '@storybook/addon-docs/blocks';
|
|
265
266
|
import { useIsDarkMode } from './hooks'; // the hook we defined above
|
|
266
267
|
|
|
267
268
|
function ThemedDocsContainer(props) {
|
|
@@ -290,7 +291,7 @@ By editing your `.storybook/preview.js`.
|
|
|
290
291
|
```js
|
|
291
292
|
import React from 'react';
|
|
292
293
|
import { addons } from 'storybook/preview-api';
|
|
293
|
-
import { DocsContainer } from '@storybook/addon-docs';
|
|
294
|
+
import { DocsContainer } from '@storybook/addon-docs/blocks';
|
|
294
295
|
import { themes } from 'storybook/theming';
|
|
295
296
|
|
|
296
297
|
import {
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const DARK_MODE_EVENT_NAME = "DARK_MODE";
|
|
2
|
+
declare const UPDATE_DARK_MODE_EVENT_NAME = "UPDATE_DARK_MODE";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the current state of storybook's dark-mode
|
|
6
|
+
*/
|
|
7
|
+
declare function useDarkMode(): boolean;
|
|
8
|
+
|
|
9
|
+
export { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME, useDarkMode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, useEffect, addons } from 'storybook/preview-api';
|
|
2
|
+
import 'react';
|
|
3
|
+
import { global } from '@storybook/global';
|
|
4
|
+
import { themes } from 'storybook/theming';
|
|
5
|
+
import 'storybook/internal/components';
|
|
6
|
+
import '@storybook/icons';
|
|
7
|
+
import 'storybook/internal/core-events';
|
|
8
|
+
import 'storybook/manager-api';
|
|
9
|
+
import equal from 'fast-deep-equal';
|
|
10
|
+
|
|
11
|
+
// src/index.tsx
|
|
12
|
+
|
|
13
|
+
// src/constants.ts
|
|
14
|
+
var DARK_MODE_EVENT_NAME = "DARK_MODE";
|
|
15
|
+
var UPDATE_DARK_MODE_EVENT_NAME = "UPDATE_DARK_MODE";
|
|
16
|
+
var { document, window } = global;
|
|
17
|
+
var STORAGE_KEY = "sb-addon-themes-3";
|
|
18
|
+
window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
19
|
+
var defaultParams = {
|
|
20
|
+
classTarget: "body",
|
|
21
|
+
dark: themes.dark,
|
|
22
|
+
darkClass: ["dark"],
|
|
23
|
+
light: themes.light,
|
|
24
|
+
lightClass: ["light"],
|
|
25
|
+
stylePreview: false,
|
|
26
|
+
userHasExplicitlySetTheTheme: false
|
|
27
|
+
};
|
|
28
|
+
var updateStore = (newStore) => {
|
|
29
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
|
|
30
|
+
};
|
|
31
|
+
var toggleDarkClass = (el, {
|
|
32
|
+
current,
|
|
33
|
+
darkClass = defaultParams.darkClass,
|
|
34
|
+
lightClass = defaultParams.lightClass
|
|
35
|
+
}) => {
|
|
36
|
+
if (current === "dark") {
|
|
37
|
+
el.classList.remove(...arrayify(lightClass));
|
|
38
|
+
el.classList.add(...arrayify(darkClass));
|
|
39
|
+
} else {
|
|
40
|
+
el.classList.remove(...arrayify(darkClass));
|
|
41
|
+
el.classList.add(...arrayify(lightClass));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var arrayify = (classes) => {
|
|
45
|
+
const arr = [];
|
|
46
|
+
return arr.concat(classes).map((item) => item);
|
|
47
|
+
};
|
|
48
|
+
var updateManager = (store2) => {
|
|
49
|
+
const manager = document.querySelector(store2.classTarget);
|
|
50
|
+
if (!manager) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
toggleDarkClass(manager, store2);
|
|
54
|
+
};
|
|
55
|
+
var store = (userTheme = {}) => {
|
|
56
|
+
const storedItem = window.localStorage.getItem(STORAGE_KEY);
|
|
57
|
+
if (typeof storedItem === "string") {
|
|
58
|
+
const stored = JSON.parse(storedItem);
|
|
59
|
+
if (userTheme) {
|
|
60
|
+
if (userTheme.dark && !equal(stored.dark, userTheme.dark)) {
|
|
61
|
+
stored.dark = userTheme.dark;
|
|
62
|
+
updateStore(stored);
|
|
63
|
+
}
|
|
64
|
+
if (userTheme.light && !equal(stored.light, userTheme.light)) {
|
|
65
|
+
stored.light = userTheme.light;
|
|
66
|
+
updateStore(stored);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return stored;
|
|
70
|
+
}
|
|
71
|
+
return { ...defaultParams, ...userTheme };
|
|
72
|
+
};
|
|
73
|
+
updateManager(store());
|
|
74
|
+
|
|
75
|
+
// src/index.tsx
|
|
76
|
+
function useDarkMode() {
|
|
77
|
+
const [isDark, setIsDark] = useState(() => store().current === "dark");
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const chan = addons.getChannel();
|
|
80
|
+
chan.on(DARK_MODE_EVENT_NAME, setIsDark);
|
|
81
|
+
return () => chan.off(DARK_MODE_EVENT_NAME, setIsDark);
|
|
82
|
+
}, []);
|
|
83
|
+
return isDark;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME, useDarkMode };
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { addons, useParameter } from 'storybook/manager-api';
|
|
2
|
+
import { Addon_TypesEnum } from 'storybook/internal/types';
|
|
3
|
+
import { themes } from 'storybook/theming';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { global } from '@storybook/global';
|
|
6
|
+
import { IconButton } from 'storybook/internal/components';
|
|
7
|
+
import { SunIcon, MoonIcon } from '@storybook/icons';
|
|
8
|
+
import { STORY_CHANGED, SET_STORIES, DOCS_RENDERED } from 'storybook/internal/core-events';
|
|
9
|
+
import equal from 'fast-deep-equal';
|
|
10
|
+
|
|
11
|
+
// src/preset/manager.tsx
|
|
12
|
+
|
|
13
|
+
// src/constants.ts
|
|
14
|
+
var DARK_MODE_EVENT_NAME = "DARK_MODE";
|
|
15
|
+
var UPDATE_DARK_MODE_EVENT_NAME = "UPDATE_DARK_MODE";
|
|
16
|
+
|
|
17
|
+
// src/Tool.tsx
|
|
18
|
+
var { document, window } = global;
|
|
19
|
+
var STORAGE_KEY = "sb-addon-themes-3";
|
|
20
|
+
var prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
21
|
+
var defaultParams = {
|
|
22
|
+
classTarget: "body",
|
|
23
|
+
dark: themes.dark,
|
|
24
|
+
darkClass: ["dark"],
|
|
25
|
+
light: themes.light,
|
|
26
|
+
lightClass: ["light"],
|
|
27
|
+
stylePreview: false,
|
|
28
|
+
userHasExplicitlySetTheTheme: false
|
|
29
|
+
};
|
|
30
|
+
var updateStore = (newStore) => {
|
|
31
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
|
|
32
|
+
};
|
|
33
|
+
var toggleDarkClass = (el, {
|
|
34
|
+
current,
|
|
35
|
+
darkClass = defaultParams.darkClass,
|
|
36
|
+
lightClass = defaultParams.lightClass
|
|
37
|
+
}) => {
|
|
38
|
+
if (current === "dark") {
|
|
39
|
+
el.classList.remove(...arrayify(lightClass));
|
|
40
|
+
el.classList.add(...arrayify(darkClass));
|
|
41
|
+
} else {
|
|
42
|
+
el.classList.remove(...arrayify(darkClass));
|
|
43
|
+
el.classList.add(...arrayify(lightClass));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var arrayify = (classes) => {
|
|
47
|
+
const arr = [];
|
|
48
|
+
return arr.concat(classes).map((item) => item);
|
|
49
|
+
};
|
|
50
|
+
var updatePreview = (store2) => {
|
|
51
|
+
const iframe = document.getElementById("storybook-preview-iframe");
|
|
52
|
+
if (!iframe) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
|
|
56
|
+
const target = iframeDocument?.querySelector(store2.classTarget);
|
|
57
|
+
if (!target) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
toggleDarkClass(target, store2);
|
|
61
|
+
};
|
|
62
|
+
var updateManager = (store2) => {
|
|
63
|
+
const manager = document.querySelector(store2.classTarget);
|
|
64
|
+
if (!manager) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
toggleDarkClass(manager, store2);
|
|
68
|
+
};
|
|
69
|
+
var store = (userTheme = {}) => {
|
|
70
|
+
const storedItem = window.localStorage.getItem(STORAGE_KEY);
|
|
71
|
+
if (typeof storedItem === "string") {
|
|
72
|
+
const stored = JSON.parse(storedItem);
|
|
73
|
+
if (userTheme) {
|
|
74
|
+
if (userTheme.dark && !equal(stored.dark, userTheme.dark)) {
|
|
75
|
+
stored.dark = userTheme.dark;
|
|
76
|
+
updateStore(stored);
|
|
77
|
+
}
|
|
78
|
+
if (userTheme.light && !equal(stored.light, userTheme.light)) {
|
|
79
|
+
stored.light = userTheme.light;
|
|
80
|
+
updateStore(stored);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return stored;
|
|
84
|
+
}
|
|
85
|
+
return { ...defaultParams, ...userTheme };
|
|
86
|
+
};
|
|
87
|
+
updateManager(store());
|
|
88
|
+
function DarkMode({ api }) {
|
|
89
|
+
const [isDark, setDark] = React.useState(prefersDark.matches);
|
|
90
|
+
const darkModeParams = useParameter("darkMode", {});
|
|
91
|
+
const { current: defaultMode, stylePreview, ...params } = darkModeParams;
|
|
92
|
+
const channel = api.getChannel();
|
|
93
|
+
const userHasExplicitlySetTheTheme = React.useMemo(
|
|
94
|
+
() => store(params).userHasExplicitlySetTheTheme,
|
|
95
|
+
[params]
|
|
96
|
+
);
|
|
97
|
+
const setMode = React.useCallback(
|
|
98
|
+
(mode) => {
|
|
99
|
+
const currentStore2 = store();
|
|
100
|
+
api.setOptions({ theme: currentStore2[mode] });
|
|
101
|
+
setDark(mode === "dark");
|
|
102
|
+
api.getChannel().emit(DARK_MODE_EVENT_NAME, mode === "dark");
|
|
103
|
+
updateManager(currentStore2);
|
|
104
|
+
if (stylePreview) {
|
|
105
|
+
updatePreview(currentStore2);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[api, stylePreview]
|
|
109
|
+
);
|
|
110
|
+
const updateMode = React.useCallback(
|
|
111
|
+
(mode) => {
|
|
112
|
+
const currentStore2 = store();
|
|
113
|
+
const current = mode || (currentStore2.current === "dark" ? "light" : "dark");
|
|
114
|
+
updateStore({ ...currentStore2, current });
|
|
115
|
+
setMode(current);
|
|
116
|
+
},
|
|
117
|
+
[setMode]
|
|
118
|
+
);
|
|
119
|
+
function prefersDarkUpdate(event) {
|
|
120
|
+
if (userHasExplicitlySetTheTheme || defaultMode) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
updateMode(event.matches ? "dark" : "light");
|
|
124
|
+
}
|
|
125
|
+
const renderTheme = React.useCallback(() => {
|
|
126
|
+
const { current = "light" } = store();
|
|
127
|
+
setMode(current);
|
|
128
|
+
}, [setMode]);
|
|
129
|
+
const handleIconClick = () => {
|
|
130
|
+
updateMode();
|
|
131
|
+
const currentStore2 = store();
|
|
132
|
+
updateStore({ ...currentStore2, userHasExplicitlySetTheTheme: true });
|
|
133
|
+
};
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
const currentStore2 = store();
|
|
136
|
+
updateStore({
|
|
137
|
+
...currentStore2,
|
|
138
|
+
...darkModeParams,
|
|
139
|
+
current: currentStore2.current || darkModeParams.current
|
|
140
|
+
});
|
|
141
|
+
renderTheme();
|
|
142
|
+
}, [darkModeParams, renderTheme]);
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
channel.on(STORY_CHANGED, renderTheme);
|
|
145
|
+
channel.on(SET_STORIES, renderTheme);
|
|
146
|
+
channel.on(DOCS_RENDERED, renderTheme);
|
|
147
|
+
prefersDark.addListener(prefersDarkUpdate);
|
|
148
|
+
return () => {
|
|
149
|
+
channel.removeListener(STORY_CHANGED, renderTheme);
|
|
150
|
+
channel.removeListener(SET_STORIES, renderTheme);
|
|
151
|
+
channel.removeListener(DOCS_RENDERED, renderTheme);
|
|
152
|
+
prefersDark.removeListener(prefersDarkUpdate);
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
channel.on(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
|
|
157
|
+
return () => {
|
|
158
|
+
channel.removeListener(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
if (userHasExplicitlySetTheTheme) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (defaultMode) {
|
|
166
|
+
updateMode(defaultMode);
|
|
167
|
+
} else if (prefersDark.matches) {
|
|
168
|
+
updateMode("dark");
|
|
169
|
+
}
|
|
170
|
+
}, [defaultMode, updateMode, userHasExplicitlySetTheTheme]);
|
|
171
|
+
return /* @__PURE__ */ React.createElement(
|
|
172
|
+
IconButton,
|
|
173
|
+
{
|
|
174
|
+
key: "dark-mode",
|
|
175
|
+
title: isDark ? "Change theme to light mode" : "Change theme to dark mode",
|
|
176
|
+
onClick: handleIconClick
|
|
177
|
+
},
|
|
178
|
+
isDark ? /* @__PURE__ */ React.createElement(SunIcon, { "aria-hidden": "true" }) : /* @__PURE__ */ React.createElement(MoonIcon, { "aria-hidden": "true" })
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
var Tool_default = DarkMode;
|
|
182
|
+
|
|
183
|
+
// src/preset/manager.tsx
|
|
184
|
+
var currentStore = store();
|
|
185
|
+
var currentTheme = currentStore.current || prefersDark.matches && "dark" || "light";
|
|
186
|
+
addons.setConfig({
|
|
187
|
+
theme: {
|
|
188
|
+
...themes[currentTheme],
|
|
189
|
+
...currentStore[currentTheme]
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
addons.register("storybook/dark-mode", (api) => {
|
|
193
|
+
addons.add("storybook/dark-mode", {
|
|
194
|
+
title: "dark mode",
|
|
195
|
+
type: Addon_TypesEnum.TOOL,
|
|
196
|
+
match: ({ viewMode }) => viewMode === "story" || viewMode === "docs",
|
|
197
|
+
render: () => /* @__PURE__ */ React.createElement(Tool_default, { api })
|
|
198
|
+
});
|
|
199
|
+
});
|
package/dist/preset.js
ADDED
package/package.json
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vueless/storybook-dark-mode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.1-beta.0",
|
|
4
4
|
"description": "Toggle between light and dark mode in Storybook",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./preview": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./preset": "./dist/preset.js",
|
|
16
|
+
"./manager": "./dist/manager.js",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
8
21
|
"files": [
|
|
9
22
|
"src",
|
|
10
23
|
"dist",
|
|
11
|
-
"preset.
|
|
24
|
+
"preset.ts"
|
|
12
25
|
],
|
|
13
26
|
"author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
|
|
14
27
|
"repository": {
|
|
@@ -20,57 +33,64 @@
|
|
|
20
33
|
},
|
|
21
34
|
"scripts": {
|
|
22
35
|
"clean": "rimraf ./dist",
|
|
23
|
-
"buildBabel": "concurrently \"npm run buildBabel:cjs\" \"npm run buildBabel:esm\"",
|
|
24
|
-
"buildBabel:cjs": "babel ./src -d ./dist/cjs --extensions \".js,.jsx,.ts,.tsx\"",
|
|
25
|
-
"buildBabel:esm": "babel ./src -d ./dist/esm --env-name esm --extensions \".js,.jsx,.ts,.tsx\"",
|
|
26
|
-
"buildTsc": "tsc --declaration --emitDeclarationOnly --outDir ./dist/ts -p tsconfig.build.json",
|
|
27
36
|
"prebuild": "npm run clean",
|
|
28
|
-
"build": "
|
|
29
|
-
"build:watch": "
|
|
30
|
-
"lint": "eslint --
|
|
31
|
-
"
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"build:watch": "tsup --watch",
|
|
39
|
+
"lint": "eslint --no-fix src/",
|
|
40
|
+
"lint:fix": "eslint --fix src/",
|
|
41
|
+
"lint:ci": "eslint --no-fix src/ --max-warnings=0",
|
|
42
|
+
"release:beta": "release-it --increment=prerelease --preRelease=beta --ci --no-git.tag --no-github.release",
|
|
43
|
+
"release:patch": "release-it patch --ci",
|
|
44
|
+
"release:minor": "release-it minor --ci",
|
|
45
|
+
"release:major": "release-it major --ci"
|
|
32
46
|
},
|
|
33
47
|
"dependencies": {
|
|
34
48
|
"@storybook/global": "^5.0.0",
|
|
35
|
-
"@storybook/icons": "^1.4.0",
|
|
36
49
|
"fast-deep-equal": "^3.1.3",
|
|
37
50
|
"memoizerific": "^1.11.3"
|
|
38
51
|
},
|
|
39
52
|
"devDependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"@storybook/
|
|
46
|
-
"@storybook/react": "^
|
|
47
|
-
"@
|
|
53
|
+
"@eslint/js": "^9.33.0",
|
|
54
|
+
"@release-it/bumper": "^7.0.5",
|
|
55
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
56
|
+
"@storybook/builder-vite": "^10.0.0",
|
|
57
|
+
"@storybook/icons": "^1.4.0",
|
|
58
|
+
"@storybook/react": "^10.0.0",
|
|
59
|
+
"@storybook/react-vite": "^10.0.0",
|
|
60
|
+
"@stylistic/eslint-plugin": "^5.2.3",
|
|
48
61
|
"@types/node": "^24.3.0",
|
|
49
62
|
"@types/react": "^18.0.26",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"eslint": "8.29.0",
|
|
58
|
-
"eslint-config-prettier": "8.5.0",
|
|
59
|
-
"eslint-config-xo": "0.43.1",
|
|
60
|
-
"eslint-config-xo-react": "0.27.0",
|
|
61
|
-
"eslint-plugin-react": "7.31.11",
|
|
62
|
-
"eslint-plugin-react-hooks": "4.6.0",
|
|
63
|
-
"prettier": "^2.8.0",
|
|
63
|
+
"eslint": "^9.33.0",
|
|
64
|
+
"eslint-config-prettier": "^10.1.8",
|
|
65
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
66
|
+
"eslint-plugin-react": "^7.37.5",
|
|
67
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
68
|
+
"globals": "^15.14.0",
|
|
69
|
+
"prettier": "^3.6.2",
|
|
64
70
|
"react": "^18.2.0",
|
|
65
71
|
"react-dom": "^18.2.0",
|
|
72
|
+
"release-it": "^19.0.4",
|
|
66
73
|
"rimraf": "^3.0.2",
|
|
67
|
-
"storybook": "^
|
|
74
|
+
"storybook": "^10.0.0",
|
|
68
75
|
"ts-node": "^10.9.2",
|
|
76
|
+
"tsup": "^8.0.0",
|
|
69
77
|
"typescript": "^5.9.2",
|
|
70
|
-
"
|
|
78
|
+
"typescript-eslint": "^8.40.0",
|
|
79
|
+
"vite": "^7.1.11"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"storybook": "^10.0.0"
|
|
71
83
|
},
|
|
72
|
-
"
|
|
73
|
-
"
|
|
84
|
+
"bundler": {
|
|
85
|
+
"managerEntries": [
|
|
86
|
+
"src/preset/manager.tsx"
|
|
87
|
+
],
|
|
88
|
+
"previewEntries": [
|
|
89
|
+
"src/index.tsx"
|
|
90
|
+
],
|
|
91
|
+
"nodeEntries": [
|
|
92
|
+
"src/preset.ts"
|
|
93
|
+
]
|
|
74
94
|
},
|
|
75
95
|
"prettier": {
|
|
76
96
|
"singleQuote": true
|
|
@@ -79,7 +99,7 @@
|
|
|
79
99
|
"node": ">=20"
|
|
80
100
|
},
|
|
81
101
|
"keywords": [
|
|
82
|
-
"storybook-
|
|
102
|
+
"storybook-addon",
|
|
83
103
|
"appearance"
|
|
84
104
|
]
|
|
85
105
|
}
|
package/preset.ts
ADDED
package/src/Tool.tsx
CHANGED
|
@@ -3,18 +3,14 @@ import { global } from '@storybook/global';
|
|
|
3
3
|
import { themes, ThemeVars } from 'storybook/theming';
|
|
4
4
|
import { IconButton } from 'storybook/internal/components';
|
|
5
5
|
import { MoonIcon, SunIcon } from '@storybook/icons';
|
|
6
|
-
import {
|
|
7
|
-
STORY_CHANGED,
|
|
8
|
-
SET_STORIES,
|
|
9
|
-
DOCS_RENDERED,
|
|
10
|
-
} from 'storybook/internal/core-events';
|
|
6
|
+
import { STORY_CHANGED, SET_STORIES, DOCS_RENDERED } from 'storybook/internal/core-events';
|
|
11
7
|
import { API, useParameter } from 'storybook/manager-api';
|
|
12
8
|
import equal from 'fast-deep-equal';
|
|
13
9
|
import { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME } from './constants';
|
|
14
10
|
|
|
15
11
|
const { document, window } = global as { document: Document; window: Window };
|
|
16
|
-
|
|
17
|
-
type Mode =
|
|
12
|
+
|
|
13
|
+
type Mode = 'light' | 'dark';
|
|
18
14
|
|
|
19
15
|
interface DarkModeStore {
|
|
20
16
|
/** The class target in the preview iframe */
|
|
@@ -36,6 +32,7 @@ interface DarkModeStore {
|
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
const STORAGE_KEY = 'sb-addon-themes-3';
|
|
35
|
+
|
|
39
36
|
export const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
40
37
|
|
|
41
38
|
const defaultParams: Required<Omit<DarkModeStore, 'current'>> = {
|
|
@@ -60,7 +57,7 @@ const toggleDarkClass = (
|
|
|
60
57
|
current,
|
|
61
58
|
darkClass = defaultParams.darkClass,
|
|
62
59
|
lightClass = defaultParams.lightClass,
|
|
63
|
-
}: DarkModeStore
|
|
60
|
+
}: DarkModeStore,
|
|
64
61
|
) => {
|
|
65
62
|
if (current === 'dark') {
|
|
66
63
|
el.classList.remove(...arrayify(lightClass));
|
|
@@ -74,21 +71,19 @@ const toggleDarkClass = (
|
|
|
74
71
|
/** Coerce a string to a single item array, or return an array as-is */
|
|
75
72
|
const arrayify = (classes: string | string[]): string[] => {
|
|
76
73
|
const arr: string[] = [];
|
|
74
|
+
|
|
77
75
|
return arr.concat(classes).map((item) => item);
|
|
78
76
|
};
|
|
79
77
|
|
|
80
78
|
/** Update the preview iframe class */
|
|
81
79
|
const updatePreview = (store: DarkModeStore) => {
|
|
82
|
-
const iframe = document.getElementById(
|
|
83
|
-
'storybook-preview-iframe'
|
|
84
|
-
) as HTMLIFrameElement;
|
|
80
|
+
const iframe = document.getElementById('storybook-preview-iframe') as HTMLIFrameElement;
|
|
85
81
|
|
|
86
82
|
if (!iframe) {
|
|
87
83
|
return;
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
const iframeDocument =
|
|
91
|
-
iframe.contentDocument || iframe.contentWindow?.document;
|
|
86
|
+
const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
|
|
92
87
|
const target = iframeDocument?.querySelector<HTMLElement>(store.classTarget);
|
|
93
88
|
|
|
94
89
|
if (!target) {
|
|
@@ -110,9 +105,7 @@ const updateManager = (store: DarkModeStore) => {
|
|
|
110
105
|
};
|
|
111
106
|
|
|
112
107
|
/** Update changed dark mode settings and persist to localStorage */
|
|
113
|
-
export const store = (
|
|
114
|
-
userTheme: Partial<DarkModeStore> = {}
|
|
115
|
-
): DarkModeStore => {
|
|
108
|
+
export const store = (userTheme: Partial<DarkModeStore> = {}): DarkModeStore => {
|
|
116
109
|
const storedItem = window.localStorage.getItem(STORAGE_KEY);
|
|
117
110
|
|
|
118
111
|
if (typeof storedItem === 'string') {
|
|
@@ -155,33 +148,35 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
155
148
|
// Save custom themes on init
|
|
156
149
|
const userHasExplicitlySetTheTheme = React.useMemo(
|
|
157
150
|
() => store(params).userHasExplicitlySetTheTheme,
|
|
158
|
-
[params]
|
|
151
|
+
[params],
|
|
159
152
|
);
|
|
160
153
|
/** Set the theme in storybook, update the local state, and emit an event */
|
|
161
154
|
const setMode = React.useCallback(
|
|
162
155
|
(mode: Mode) => {
|
|
163
156
|
const currentStore = store();
|
|
157
|
+
|
|
164
158
|
api.setOptions({ theme: currentStore[mode] });
|
|
165
159
|
setDark(mode === 'dark');
|
|
166
160
|
api.getChannel().emit(DARK_MODE_EVENT_NAME, mode === 'dark');
|
|
167
161
|
updateManager(currentStore);
|
|
162
|
+
|
|
168
163
|
if (stylePreview) {
|
|
169
164
|
updatePreview(currentStore);
|
|
170
165
|
}
|
|
171
166
|
},
|
|
172
|
-
[api, stylePreview]
|
|
167
|
+
[api, stylePreview],
|
|
173
168
|
);
|
|
174
169
|
|
|
175
170
|
/** Update the theme settings in localStorage, react, and storybook */
|
|
176
171
|
const updateMode = React.useCallback(
|
|
177
172
|
(mode?: Mode) => {
|
|
178
173
|
const currentStore = store();
|
|
179
|
-
const current =
|
|
180
|
-
|
|
174
|
+
const current = mode || (currentStore.current === 'dark' ? 'light' : 'dark');
|
|
175
|
+
|
|
181
176
|
updateStore({ ...currentStore, current });
|
|
182
177
|
setMode(current);
|
|
183
178
|
},
|
|
184
|
-
[setMode]
|
|
179
|
+
[setMode],
|
|
185
180
|
);
|
|
186
181
|
|
|
187
182
|
/** Update the theme based on the color preference */
|
|
@@ -196,6 +191,7 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
196
191
|
/** Render the current theme */
|
|
197
192
|
const renderTheme = React.useCallback(() => {
|
|
198
193
|
const { current = 'light' } = store();
|
|
194
|
+
|
|
199
195
|
setMode(current);
|
|
200
196
|
}, [setMode]);
|
|
201
197
|
|
|
@@ -203,12 +199,14 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
203
199
|
const handleIconClick = () => {
|
|
204
200
|
updateMode();
|
|
205
201
|
const currentStore = store();
|
|
202
|
+
|
|
206
203
|
updateStore({ ...currentStore, userHasExplicitlySetTheTheme: true });
|
|
207
204
|
};
|
|
208
205
|
|
|
209
206
|
/** When storybook params change update the stored themes */
|
|
210
207
|
React.useEffect(() => {
|
|
211
208
|
const currentStore = store();
|
|
209
|
+
|
|
212
210
|
// Ensure we use the stores `current` value first to persist
|
|
213
211
|
// themeing between page loads and story changes.
|
|
214
212
|
updateStore({
|
|
@@ -223,6 +221,7 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
223
221
|
channel.on(SET_STORIES, renderTheme);
|
|
224
222
|
channel.on(DOCS_RENDERED, renderTheme);
|
|
225
223
|
prefersDark.addListener(prefersDarkUpdate);
|
|
224
|
+
|
|
226
225
|
return () => {
|
|
227
226
|
channel.removeListener(STORY_CHANGED, renderTheme);
|
|
228
227
|
channel.removeListener(SET_STORIES, renderTheme);
|
|
@@ -232,6 +231,7 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
232
231
|
});
|
|
233
232
|
React.useEffect(() => {
|
|
234
233
|
channel.on(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
|
|
234
|
+
|
|
235
235
|
return () => {
|
|
236
236
|
channel.removeListener(UPDATE_DARK_MODE_EVENT_NAME, updateMode);
|
|
237
237
|
};
|
|
@@ -250,12 +250,11 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
250
250
|
updateMode('dark');
|
|
251
251
|
}
|
|
252
252
|
}, [defaultMode, updateMode, userHasExplicitlySetTheTheme]);
|
|
253
|
+
|
|
253
254
|
return (
|
|
254
255
|
<IconButton
|
|
255
256
|
key="dark-mode"
|
|
256
|
-
title={
|
|
257
|
-
isDark ? 'Change theme to light mode' : 'Change theme to dark mode'
|
|
258
|
-
}
|
|
257
|
+
title={isDark ? 'Change theme to light mode' : 'Change theme to dark mode'}
|
|
259
258
|
onClick={handleIconClick}
|
|
260
259
|
>
|
|
261
260
|
{isDark ? <SunIcon aria-hidden="true" /> : <MoonIcon aria-hidden="true" />}
|
package/src/index.tsx
CHANGED
package/src/preset/manager.tsx
CHANGED
|
@@ -6,8 +6,7 @@ import * as React from 'react';
|
|
|
6
6
|
import Tool, { prefersDark, store } from '../Tool';
|
|
7
7
|
|
|
8
8
|
const currentStore = store();
|
|
9
|
-
const currentTheme =
|
|
10
|
-
currentStore.current || (prefersDark.matches && 'dark') || 'light';
|
|
9
|
+
const currentTheme = currentStore.current || (prefersDark.matches && 'dark') || 'light';
|
|
11
10
|
|
|
12
11
|
addons.setConfig({
|
|
13
12
|
theme: {
|
package/src/preset.ts
ADDED