@vueless/storybook-dark-mode 10.0.1-beta.0 → 10.0.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/README.md +5 -1
- package/dist/index.js +6 -10
- package/dist/manager.js +9 -9
- package/dist/preset.js +0 -5
- package/package.json +32 -28
- package/preset.js +7 -0
- package/src/Tool.tsx +23 -143
- package/src/constants.ts +2 -2
- package/src/index.tsx +6 -5
- package/src/preset/manager.tsx +11 -10
- package/src/preset.ts +1 -3
- package/src/store.ts +115 -0
- package/preset.ts +0 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Storybook Dark Mode
|
|
2
2
|
|
|
3
|
-
A Storybook v10
|
|
3
|
+
A Storybook v9 and 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
|
|
|
@@ -11,7 +11,11 @@ The project is supported and maintained by the [Vueless UI](https://github.com/v
|
|
|
11
11
|
Install the following npm module:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
+
# for Storybook v10
|
|
14
15
|
npm i -D @vueless/storybook-dark-mode
|
|
16
|
+
|
|
17
|
+
# for Storybook v9
|
|
18
|
+
npm i -D @vueless/storybook-dark-mode@^9
|
|
15
19
|
```
|
|
16
20
|
|
|
17
21
|
Then, add following content to `.storybook/main.js`
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
2
|
-
import '
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { addons } from 'storybook/preview-api';
|
|
3
3
|
import { global } from '@storybook/global';
|
|
4
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
5
|
import equal from 'fast-deep-equal';
|
|
10
6
|
|
|
11
7
|
// src/index.tsx
|
|
@@ -28,6 +24,10 @@ var defaultParams = {
|
|
|
28
24
|
var updateStore = (newStore) => {
|
|
29
25
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
|
|
30
26
|
};
|
|
27
|
+
var arrayify = (classes) => {
|
|
28
|
+
const arr = [];
|
|
29
|
+
return arr.concat(classes).map((item) => item);
|
|
30
|
+
};
|
|
31
31
|
var toggleDarkClass = (el, {
|
|
32
32
|
current,
|
|
33
33
|
darkClass = defaultParams.darkClass,
|
|
@@ -41,10 +41,6 @@ var toggleDarkClass = (el, {
|
|
|
41
41
|
el.classList.add(...arrayify(lightClass));
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
-
var arrayify = (classes) => {
|
|
45
|
-
const arr = [];
|
|
46
|
-
return arr.concat(classes).map((item) => item);
|
|
47
|
-
};
|
|
48
44
|
var updateManager = (store2) => {
|
|
49
45
|
const manager = document.querySelector(store2.classTarget);
|
|
50
46
|
if (!manager) {
|
package/dist/manager.js
CHANGED
|
@@ -2,10 +2,10 @@ import { addons, useParameter } from 'storybook/manager-api';
|
|
|
2
2
|
import { Addon_TypesEnum } from 'storybook/internal/types';
|
|
3
3
|
import { themes } from 'storybook/theming';
|
|
4
4
|
import * as React from 'react';
|
|
5
|
-
import { global } from '@storybook/global';
|
|
6
5
|
import { IconButton } from 'storybook/internal/components';
|
|
7
6
|
import { SunIcon, MoonIcon } from '@storybook/icons';
|
|
8
7
|
import { STORY_CHANGED, SET_STORIES, DOCS_RENDERED } from 'storybook/internal/core-events';
|
|
8
|
+
import { global } from '@storybook/global';
|
|
9
9
|
import equal from 'fast-deep-equal';
|
|
10
10
|
|
|
11
11
|
// src/preset/manager.tsx
|
|
@@ -13,8 +13,6 @@ import equal from 'fast-deep-equal';
|
|
|
13
13
|
// src/constants.ts
|
|
14
14
|
var DARK_MODE_EVENT_NAME = "DARK_MODE";
|
|
15
15
|
var UPDATE_DARK_MODE_EVENT_NAME = "UPDATE_DARK_MODE";
|
|
16
|
-
|
|
17
|
-
// src/Tool.tsx
|
|
18
16
|
var { document, window } = global;
|
|
19
17
|
var STORAGE_KEY = "sb-addon-themes-3";
|
|
20
18
|
var prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
@@ -30,6 +28,10 @@ var defaultParams = {
|
|
|
30
28
|
var updateStore = (newStore) => {
|
|
31
29
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
|
|
32
30
|
};
|
|
31
|
+
var arrayify = (classes) => {
|
|
32
|
+
const arr = [];
|
|
33
|
+
return arr.concat(classes).map((item) => item);
|
|
34
|
+
};
|
|
33
35
|
var toggleDarkClass = (el, {
|
|
34
36
|
current,
|
|
35
37
|
darkClass = defaultParams.darkClass,
|
|
@@ -43,10 +45,6 @@ var toggleDarkClass = (el, {
|
|
|
43
45
|
el.classList.add(...arrayify(lightClass));
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
|
-
var arrayify = (classes) => {
|
|
47
|
-
const arr = [];
|
|
48
|
-
return arr.concat(classes).map((item) => item);
|
|
49
|
-
};
|
|
50
48
|
var updatePreview = (store2) => {
|
|
51
49
|
const iframe = document.getElementById("storybook-preview-iframe");
|
|
52
50
|
if (!iframe) {
|
|
@@ -85,6 +83,8 @@ var store = (userTheme = {}) => {
|
|
|
85
83
|
return { ...defaultParams, ...userTheme };
|
|
86
84
|
};
|
|
87
85
|
updateManager(store());
|
|
86
|
+
|
|
87
|
+
// src/Tool.tsx
|
|
88
88
|
function DarkMode({ api }) {
|
|
89
89
|
const [isDark, setDark] = React.useState(prefersDark.matches);
|
|
90
90
|
const darkModeParams = useParameter("darkMode", {});
|
|
@@ -189,8 +189,8 @@ addons.setConfig({
|
|
|
189
189
|
...currentStore[currentTheme]
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
|
-
addons.register("storybook
|
|
193
|
-
addons.add("storybook
|
|
192
|
+
addons.register("@vueless/storybook-dark-mode", (api) => {
|
|
193
|
+
addons.add("@vueless/storybook-dark-mode", {
|
|
194
194
|
title: "dark mode",
|
|
195
195
|
type: Addon_TypesEnum.TOOL,
|
|
196
196
|
match: ({ viewMode }) => viewMode === "story" || viewMode === "docs",
|
package/dist/preset.js
CHANGED
package/package.json
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vueless/storybook-dark-mode",
|
|
3
|
-
"version": "10.0.1
|
|
3
|
+
"version": "10.0.1",
|
|
4
4
|
"description": "Toggle between light and dark mode in Storybook",
|
|
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",
|
|
21
|
-
"files": [
|
|
22
|
-
"src",
|
|
23
|
-
"dist",
|
|
24
|
-
"preset.ts"
|
|
25
|
-
],
|
|
26
5
|
"author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
|
|
27
6
|
"repository": {
|
|
28
7
|
"type": "git",
|
|
@@ -36,8 +15,8 @@
|
|
|
36
15
|
"prebuild": "npm run clean",
|
|
37
16
|
"build": "tsup",
|
|
38
17
|
"build:watch": "tsup --watch",
|
|
39
|
-
"lint": "eslint --no-fix
|
|
40
|
-
"lint:fix": "eslint --fix
|
|
18
|
+
"lint": "eslint --no-fix .",
|
|
19
|
+
"lint:fix": "eslint --fix .",
|
|
41
20
|
"lint:ci": "eslint --no-fix src/ --max-warnings=0",
|
|
42
21
|
"release:beta": "release-it --increment=prerelease --preRelease=beta --ci --no-git.tag --no-github.release",
|
|
43
22
|
"release:patch": "release-it patch --ci",
|
|
@@ -81,15 +60,36 @@
|
|
|
81
60
|
"peerDependencies": {
|
|
82
61
|
"storybook": "^10.0.0"
|
|
83
62
|
},
|
|
63
|
+
"type": "module",
|
|
64
|
+
"main": "dist/index.js",
|
|
65
|
+
"types": "dist/index.d.ts",
|
|
66
|
+
"exports": {
|
|
67
|
+
".": {
|
|
68
|
+
"types": "./dist/index.d.ts",
|
|
69
|
+
"default": "./dist/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./preview": {
|
|
72
|
+
"types": "./dist/index.d.ts",
|
|
73
|
+
"default": "./dist/index.js"
|
|
74
|
+
},
|
|
75
|
+
"./preset": "./dist/preset.js",
|
|
76
|
+
"./manager": "./dist/manager.js",
|
|
77
|
+
"./package.json": "./package.json"
|
|
78
|
+
},
|
|
79
|
+
"files": [
|
|
80
|
+
"src",
|
|
81
|
+
"dist",
|
|
82
|
+
"preset.js"
|
|
83
|
+
],
|
|
84
84
|
"bundler": {
|
|
85
85
|
"managerEntries": [
|
|
86
|
-
"src/preset/manager.tsx"
|
|
86
|
+
"./src/preset/manager.tsx"
|
|
87
87
|
],
|
|
88
88
|
"previewEntries": [
|
|
89
|
-
"src/index.tsx"
|
|
89
|
+
"./src/index.tsx"
|
|
90
90
|
],
|
|
91
91
|
"nodeEntries": [
|
|
92
|
-
"src/preset.ts"
|
|
92
|
+
"./src/preset.ts"
|
|
93
93
|
]
|
|
94
94
|
},
|
|
95
95
|
"prettier": {
|
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
},
|
|
101
101
|
"keywords": [
|
|
102
102
|
"storybook-addon",
|
|
103
|
-
"appearance"
|
|
103
|
+
"appearance",
|
|
104
|
+
"storybook",
|
|
105
|
+
"vueless",
|
|
106
|
+
"dark mode",
|
|
107
|
+
"color mode"
|
|
104
108
|
]
|
|
105
109
|
}
|
package/preset.js
ADDED
package/src/Tool.tsx
CHANGED
|
@@ -1,138 +1,18 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/** The class target in the preview iframe */
|
|
17
|
-
classTarget: string;
|
|
18
|
-
/** The current mode the storybook is set to */
|
|
19
|
-
current: Mode;
|
|
20
|
-
/** The dark theme for storybook */
|
|
21
|
-
dark: ThemeVars;
|
|
22
|
-
/** The dark class name for the preview iframe */
|
|
23
|
-
darkClass: string | string[];
|
|
24
|
-
/** The light theme for storybook */
|
|
25
|
-
light: ThemeVars;
|
|
26
|
-
/** The light class name for the preview iframe */
|
|
27
|
-
lightClass: string | string[];
|
|
28
|
-
/** Apply mode to iframe */
|
|
29
|
-
stylePreview: boolean;
|
|
30
|
-
/** Persist if the user has set the theme */
|
|
31
|
-
userHasExplicitlySetTheTheme: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const STORAGE_KEY = 'sb-addon-themes-3';
|
|
35
|
-
|
|
36
|
-
export const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
37
|
-
|
|
38
|
-
const defaultParams: Required<Omit<DarkModeStore, 'current'>> = {
|
|
39
|
-
classTarget: 'body',
|
|
40
|
-
dark: themes.dark,
|
|
41
|
-
darkClass: ['dark'],
|
|
42
|
-
light: themes.light,
|
|
43
|
-
lightClass: ['light'],
|
|
44
|
-
stylePreview: false,
|
|
45
|
-
userHasExplicitlySetTheTheme: false,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** Persist the dark mode settings in localStorage */
|
|
49
|
-
export const updateStore = (newStore: DarkModeStore) => {
|
|
50
|
-
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** Add the light/dark class to an element */
|
|
54
|
-
const toggleDarkClass = (
|
|
55
|
-
el: Element,
|
|
56
|
-
{
|
|
57
|
-
current,
|
|
58
|
-
darkClass = defaultParams.darkClass,
|
|
59
|
-
lightClass = defaultParams.lightClass,
|
|
60
|
-
}: DarkModeStore,
|
|
61
|
-
) => {
|
|
62
|
-
if (current === 'dark') {
|
|
63
|
-
el.classList.remove(...arrayify(lightClass));
|
|
64
|
-
el.classList.add(...arrayify(darkClass));
|
|
65
|
-
} else {
|
|
66
|
-
el.classList.remove(...arrayify(darkClass));
|
|
67
|
-
el.classList.add(...arrayify(lightClass));
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/** Coerce a string to a single item array, or return an array as-is */
|
|
72
|
-
const arrayify = (classes: string | string[]): string[] => {
|
|
73
|
-
const arr: string[] = [];
|
|
74
|
-
|
|
75
|
-
return arr.concat(classes).map((item) => item);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/** Update the preview iframe class */
|
|
79
|
-
const updatePreview = (store: DarkModeStore) => {
|
|
80
|
-
const iframe = document.getElementById('storybook-preview-iframe') as HTMLIFrameElement;
|
|
81
|
-
|
|
82
|
-
if (!iframe) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
|
|
87
|
-
const target = iframeDocument?.querySelector<HTMLElement>(store.classTarget);
|
|
88
|
-
|
|
89
|
-
if (!target) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
toggleDarkClass(target, store);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/** Update the manager iframe class */
|
|
97
|
-
const updateManager = (store: DarkModeStore) => {
|
|
98
|
-
const manager = document.querySelector(store.classTarget);
|
|
99
|
-
|
|
100
|
-
if (!manager) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
toggleDarkClass(manager, store);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/** Update changed dark mode settings and persist to localStorage */
|
|
108
|
-
export const store = (userTheme: Partial<DarkModeStore> = {}): DarkModeStore => {
|
|
109
|
-
const storedItem = window.localStorage.getItem(STORAGE_KEY);
|
|
110
|
-
|
|
111
|
-
if (typeof storedItem === 'string') {
|
|
112
|
-
const stored = JSON.parse(storedItem) as DarkModeStore;
|
|
113
|
-
|
|
114
|
-
if (userTheme) {
|
|
115
|
-
if (userTheme.dark && !equal(stored.dark, userTheme.dark)) {
|
|
116
|
-
stored.dark = userTheme.dark;
|
|
117
|
-
updateStore(stored);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (userTheme.light && !equal(stored.light, userTheme.light)) {
|
|
121
|
-
stored.light = userTheme.light;
|
|
122
|
-
updateStore(stored);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return stored;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { ...defaultParams, ...userTheme } as DarkModeStore;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// On initial load, set the dark mode class on the manager
|
|
133
|
-
// This is needed if you're using mostly CSS overrides to styles the storybook
|
|
134
|
-
// Otherwise the default theme is set in src/preset/manager.tsx
|
|
135
|
-
updateManager(store());
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { IconButton } from "storybook/internal/components";
|
|
3
|
+
import { MoonIcon, SunIcon } from "@storybook/icons";
|
|
4
|
+
import { API, useParameter } from "storybook/manager-api";
|
|
5
|
+
import { SET_STORIES, DOCS_RENDERED, STORY_CHANGED } from "storybook/internal/core-events";
|
|
6
|
+
import { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME } from "./constants";
|
|
7
|
+
import {
|
|
8
|
+
store,
|
|
9
|
+
prefersDark,
|
|
10
|
+
updateStore,
|
|
11
|
+
updateManager,
|
|
12
|
+
updatePreview,
|
|
13
|
+
type DarkModeStore,
|
|
14
|
+
type Mode,
|
|
15
|
+
} from "./store";
|
|
136
16
|
|
|
137
17
|
interface DarkModeProps {
|
|
138
18
|
/** The storybook API */
|
|
@@ -142,7 +22,7 @@ interface DarkModeProps {
|
|
|
142
22
|
/** A toolbar icon to toggle between dark and light themes in storybook */
|
|
143
23
|
export function DarkMode({ api }: DarkModeProps) {
|
|
144
24
|
const [isDark, setDark] = React.useState(prefersDark.matches);
|
|
145
|
-
const darkModeParams = useParameter<Partial<DarkModeStore>>(
|
|
25
|
+
const darkModeParams = useParameter<Partial<DarkModeStore>>("darkMode", {});
|
|
146
26
|
const { current: defaultMode, stylePreview, ...params } = darkModeParams;
|
|
147
27
|
const channel = api.getChannel();
|
|
148
28
|
// Save custom themes on init
|
|
@@ -156,8 +36,8 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
156
36
|
const currentStore = store();
|
|
157
37
|
|
|
158
38
|
api.setOptions({ theme: currentStore[mode] });
|
|
159
|
-
setDark(mode ===
|
|
160
|
-
api.getChannel().emit(DARK_MODE_EVENT_NAME, mode ===
|
|
39
|
+
setDark(mode === "dark");
|
|
40
|
+
api.getChannel().emit(DARK_MODE_EVENT_NAME, mode === "dark");
|
|
161
41
|
updateManager(currentStore);
|
|
162
42
|
|
|
163
43
|
if (stylePreview) {
|
|
@@ -171,7 +51,7 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
171
51
|
const updateMode = React.useCallback(
|
|
172
52
|
(mode?: Mode) => {
|
|
173
53
|
const currentStore = store();
|
|
174
|
-
const current = mode || (currentStore.current ===
|
|
54
|
+
const current = mode || (currentStore.current === "dark" ? "light" : "dark");
|
|
175
55
|
|
|
176
56
|
updateStore({ ...currentStore, current });
|
|
177
57
|
setMode(current);
|
|
@@ -185,12 +65,12 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
185
65
|
return;
|
|
186
66
|
}
|
|
187
67
|
|
|
188
|
-
updateMode(event.matches ?
|
|
68
|
+
updateMode(event.matches ? "dark" : "light");
|
|
189
69
|
}
|
|
190
70
|
|
|
191
71
|
/** Render the current theme */
|
|
192
72
|
const renderTheme = React.useCallback(() => {
|
|
193
|
-
const { current =
|
|
73
|
+
const { current = "light" } = store();
|
|
194
74
|
|
|
195
75
|
setMode(current);
|
|
196
76
|
}, [setMode]);
|
|
@@ -247,14 +127,14 @@ export function DarkMode({ api }: DarkModeProps) {
|
|
|
247
127
|
if (defaultMode) {
|
|
248
128
|
updateMode(defaultMode);
|
|
249
129
|
} else if (prefersDark.matches) {
|
|
250
|
-
updateMode(
|
|
130
|
+
updateMode("dark");
|
|
251
131
|
}
|
|
252
132
|
}, [defaultMode, updateMode, userHasExplicitlySetTheTheme]);
|
|
253
133
|
|
|
254
134
|
return (
|
|
255
135
|
<IconButton
|
|
256
136
|
key="dark-mode"
|
|
257
|
-
title={isDark ?
|
|
137
|
+
title={isDark ? "Change theme to light mode" : "Change theme to dark mode"}
|
|
258
138
|
onClick={handleIconClick}
|
|
259
139
|
>
|
|
260
140
|
{isDark ? <SunIcon aria-hidden="true" /> : <MoonIcon aria-hidden="true" />}
|
package/src/constants.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DARK_MODE_EVENT_NAME =
|
|
2
|
-
export const UPDATE_DARK_MODE_EVENT_NAME =
|
|
1
|
+
export const DARK_MODE_EVENT_NAME = "DARK_MODE";
|
|
2
|
+
export const UPDATE_DARK_MODE_EVENT_NAME = "UPDATE_DARK_MODE";
|
package/src/index.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { addons } from "storybook/preview-api";
|
|
3
|
+
import { DARK_MODE_EVENT_NAME } from "./constants";
|
|
4
|
+
import { store } from "./store";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Returns the current state of storybook's dark-mode
|
|
7
8
|
*/
|
|
8
9
|
export function useDarkMode(): boolean {
|
|
9
|
-
const [isDark, setIsDark] = useState(() => store().current ===
|
|
10
|
+
const [isDark, setIsDark] = useState(() => store().current === "dark");
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
12
13
|
const chan = addons.getChannel();
|
|
@@ -19,4 +20,4 @@ export function useDarkMode(): boolean {
|
|
|
19
20
|
return isDark;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export * from
|
|
23
|
+
export * from "./constants";
|
package/src/preset/manager.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { addons } from
|
|
2
|
-
import { Addon_TypesEnum } from
|
|
3
|
-
import { themes } from
|
|
4
|
-
import * as React from
|
|
1
|
+
import { addons } 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
5
|
|
|
6
|
-
import Tool
|
|
6
|
+
import Tool from "../Tool";
|
|
7
|
+
import { prefersDark, store } from "../store";
|
|
7
8
|
|
|
8
9
|
const currentStore = store();
|
|
9
|
-
const currentTheme = currentStore.current || (prefersDark.matches &&
|
|
10
|
+
const currentTheme = currentStore.current || (prefersDark.matches && "dark") || "light";
|
|
10
11
|
|
|
11
12
|
addons.setConfig({
|
|
12
13
|
theme: {
|
|
@@ -15,11 +16,11 @@ addons.setConfig({
|
|
|
15
16
|
},
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
addons.register(
|
|
19
|
-
addons.add(
|
|
20
|
-
title:
|
|
19
|
+
addons.register("@vueless/storybook-dark-mode", (api) => {
|
|
20
|
+
addons.add("@vueless/storybook-dark-mode", {
|
|
21
|
+
title: "dark mode",
|
|
21
22
|
type: Addon_TypesEnum.TOOL,
|
|
22
|
-
match: ({ viewMode }) => viewMode ===
|
|
23
|
+
match: ({ viewMode }) => viewMode === "story" || viewMode === "docs",
|
|
23
24
|
render: () => <Tool api={api} />,
|
|
24
25
|
});
|
|
25
26
|
});
|
package/src/preset.ts
CHANGED
package/src/store.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { global } from "@storybook/global";
|
|
2
|
+
import { themes, ThemeVars } from "storybook/theming";
|
|
3
|
+
import equal from "fast-deep-equal";
|
|
4
|
+
|
|
5
|
+
const { document, window } = global as { document: Document; window: Window };
|
|
6
|
+
|
|
7
|
+
type Mode = "light" | "dark";
|
|
8
|
+
|
|
9
|
+
interface DarkModeStore {
|
|
10
|
+
classTarget: string;
|
|
11
|
+
current: Mode;
|
|
12
|
+
dark: ThemeVars;
|
|
13
|
+
darkClass: string | string[];
|
|
14
|
+
light: ThemeVars;
|
|
15
|
+
lightClass: string | string[];
|
|
16
|
+
stylePreview: boolean;
|
|
17
|
+
userHasExplicitlySetTheTheme: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STORAGE_KEY = "sb-addon-themes-3";
|
|
21
|
+
|
|
22
|
+
export const prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
23
|
+
|
|
24
|
+
const defaultParams: Required<Omit<DarkModeStore, "current">> = {
|
|
25
|
+
classTarget: "body",
|
|
26
|
+
dark: themes.dark,
|
|
27
|
+
darkClass: ["dark"],
|
|
28
|
+
light: themes.light,
|
|
29
|
+
lightClass: ["light"],
|
|
30
|
+
stylePreview: false,
|
|
31
|
+
userHasExplicitlySetTheTheme: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const updateStore = (newStore: DarkModeStore) => {
|
|
35
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const arrayify = (classes: string | string[]): string[] => {
|
|
39
|
+
const arr: string[] = [];
|
|
40
|
+
|
|
41
|
+
return arr.concat(classes).map((item) => item);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const toggleDarkClass = (
|
|
45
|
+
el: Element,
|
|
46
|
+
{
|
|
47
|
+
current,
|
|
48
|
+
darkClass = defaultParams.darkClass,
|
|
49
|
+
lightClass = defaultParams.lightClass,
|
|
50
|
+
}: DarkModeStore,
|
|
51
|
+
) => {
|
|
52
|
+
if (current === "dark") {
|
|
53
|
+
el.classList.remove(...arrayify(lightClass));
|
|
54
|
+
el.classList.add(...arrayify(darkClass));
|
|
55
|
+
} else {
|
|
56
|
+
el.classList.remove(...arrayify(darkClass));
|
|
57
|
+
el.classList.add(...arrayify(lightClass));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const updatePreview = (store: DarkModeStore) => {
|
|
62
|
+
const iframe = document.getElementById("storybook-preview-iframe") as HTMLIFrameElement;
|
|
63
|
+
|
|
64
|
+
if (!iframe) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
|
|
69
|
+
const target = iframeDocument?.querySelector<HTMLElement>(store.classTarget);
|
|
70
|
+
|
|
71
|
+
if (!target) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
toggleDarkClass(target, store);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const updateManager = (store: DarkModeStore) => {
|
|
79
|
+
const manager = document.querySelector(store.classTarget);
|
|
80
|
+
|
|
81
|
+
if (!manager) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
toggleDarkClass(manager, store);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const store = (userTheme: Partial<DarkModeStore> = {}): DarkModeStore => {
|
|
89
|
+
const storedItem = window.localStorage.getItem(STORAGE_KEY);
|
|
90
|
+
|
|
91
|
+
if (typeof storedItem === "string") {
|
|
92
|
+
const stored = JSON.parse(storedItem) as DarkModeStore;
|
|
93
|
+
|
|
94
|
+
if (userTheme) {
|
|
95
|
+
if (userTheme.dark && !equal(stored.dark, userTheme.dark)) {
|
|
96
|
+
stored.dark = userTheme.dark;
|
|
97
|
+
updateStore(stored);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (userTheme.light && !equal(stored.light, userTheme.light)) {
|
|
101
|
+
stored.light = userTheme.light;
|
|
102
|
+
updateStore(stored);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return stored;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { ...defaultParams, ...userTheme } as DarkModeStore;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
updateManager(store());
|
|
113
|
+
|
|
114
|
+
export { updatePreview };
|
|
115
|
+
export type { DarkModeStore, Mode };
|
package/preset.ts
DELETED