@anmiles/theme-switcher 1.0.2 → 2.0.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.
Files changed (42) hide show
  1. package/.nycrc.json +27 -0
  2. package/CHANGELOG.md +10 -1
  3. package/README.md +43 -43
  4. package/cspell.json +18 -0
  5. package/dist/theme-switcher-2.0.0.js +307 -0
  6. package/dist/theme-switcher-2.0.0.min.js +22 -0
  7. package/eslint.config.mts +16 -0
  8. package/index.html +38 -0
  9. package/jest.config.js +13 -15
  10. package/package.json +56 -36
  11. package/server.mjs +11 -0
  12. package/src/__tests__/index.test.tsx +10 -8
  13. package/src/__tests__/theme.test.ts +1 -1
  14. package/src/components/App.tsx +13 -11
  15. package/src/components/Icon.tsx +3 -1
  16. package/src/components/ThemeSelector.tsx +3 -2
  17. package/src/components/__tests__/App.test.tsx +47 -48
  18. package/src/components/__tests__/__snapshots__/App.test.tsx.snap +60 -20
  19. package/src/components/icons/Dark.tsx +0 -1
  20. package/src/index.tsx +4 -3
  21. package/src/lib/__tests__/eventEmitter.test.ts +6 -6
  22. package/src/lib/eventEmitter.ts +5 -7
  23. package/src/lib/theme.ts +7 -9
  24. package/src/providers/__tests__/systemProvider.test.ts +16 -16
  25. package/src/providers/__tests__/userProvider.test.ts +4 -4
  26. package/src/providers/systemProvider.ts +1 -3
  27. package/src/providers/userProvider.ts +1 -3
  28. package/static/development/index.html +39 -0
  29. package/static/production/index.html +39 -0
  30. package/tsconfig.json +7 -11
  31. package/tsconfig.test.json +1 -1
  32. package/vite.config.mts +54 -0
  33. package/.eslintignore +0 -2
  34. package/.eslintrc.js +0 -10
  35. package/.vscode/settings.json +0 -6
  36. package/coverage.config.js +0 -8
  37. package/dev/index.html +0 -35
  38. package/dist/theme-switcher-1.0.2.js +0 -2194
  39. package/dist/theme-switcher-1.0.2.min.js +0 -2
  40. package/dist/theme-switcher-1.0.2.min.js.LICENSE.txt +0 -9
  41. package/tsconfig.build.json +0 -7
  42. package/webpack.config.js +0 -67
package/.nycrc.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "all": true,
3
+ "check-coverage" : true,
4
+
5
+ "statements" : 100,
6
+ "branches" : 100,
7
+ "lines" : 100,
8
+ "functions" : 100,
9
+
10
+ "report-dir" : "./coverage",
11
+ "temp-dir" : "./coverage",
12
+
13
+ "reporter" : [
14
+ "text",
15
+ "html"
16
+ ],
17
+ "extension": [
18
+ ".js",
19
+ ".mjs",
20
+ ".cjs",
21
+ ".jsx",
22
+ ".ts",
23
+ ".cts",
24
+ ".mts",
25
+ ".tsx"
26
+ ]
27
+ }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.0](../../tags/v2.0.0) - 2025-05-18
9
+ __(BREAKING) Dropped support for NodeJS 18 (EOL). Minimum required version is now NodeJS 20.__
10
+
11
+ ### Changed
12
+ - Migrated to NodeJS 20.19
13
+ - Migrated to ESLint V9 flat configs
14
+ - Updated dependencies
15
+ - Optimized size of static bundle (5.7Kb vs. 12.3Kb)
16
+
8
17
  ## [1.0.2](../../tags/v1.0.2) - 2024-12-15
9
18
  ### Changed
10
19
  - Update dist files
@@ -13,6 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
22
  ### Changed
14
23
  - First official release
15
24
 
16
- ## [0.1.0] - 2024-11-30
25
+ ## 0.1.0 - 2024-11-30
17
26
  ### Added
18
27
  - First version (not covered by unit tests; not released on NPM for now)
package/README.md CHANGED
@@ -9,80 +9,80 @@ Theme switcher for websites
9
9
  ### For React+TS project
10
10
 
11
11
  1. Install package:
12
- ```bash
13
- npm install @anmiles/theme-switcher
14
- ```
12
+ ```bash
13
+ npm install @anmiles/theme-switcher
14
+ ```
15
15
 
16
16
  2. Import component:
17
- ```ts
18
- import { ThemeSwitcher } from '@anmiles/theme-switcher';
19
- ```
17
+ ```ts
18
+ import { ThemeSwitcher } from '@anmiles/theme-switcher';
19
+ ```
20
20
 
21
21
  3. Use component:
22
- ```ts
23
- <ThemeSwitcher float="right" />
24
- ```
25
- where
26
- - `float` _(optional)_ - position of icon and dropdown box
22
+ ```ts
23
+ <ThemeSwitcher float="right" />
24
+ ```
25
+ where
26
+ - `float` _(optional)_ - position of icon and dropdown box
27
27
 
28
28
  ### For static HTML website
29
29
 
30
30
  1. Install package:
31
- ```bash
32
- npm install @anmiles/theme-switcher
33
- ```
31
+ ```bash
32
+ npm install @anmiles/theme-switcher
33
+ ```
34
34
 
35
35
  2. Copy all files from `dist` into the target website.
36
36
 
37
37
  4. Create HTML container for theme switcher:
38
38
 
39
- ```html
40
- <div class="my-selector"></div>
41
- ```
39
+ ```html
40
+ <div class="my-selector"></div>
41
+ ```
42
42
 
43
43
  5. Include React library and theme switcher:
44
44
 
45
- ### Development
45
+ ### Development
46
46
 
47
- ```html
48
- <script type="text/javascript" src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
49
- <script type="text/javascript" src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
50
- <script type="text/javascript" src="./theme-switcher-1.0.2.js"></script>
51
- ```
47
+ ```html
48
+ <script type="text/javascript" src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
49
+ <script type="text/javascript" src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
50
+ <script type="text/javascript" src="./theme-switcher-1.0.2.js"></script>
51
+ ```
52
52
 
53
- ### Production
53
+ ### Production
54
54
 
55
- ```html
56
- <script type="text/javascript" src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
57
- <script type="text/javascript" src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
58
- <script type="text/javascript" src="./theme-switcher-1.0.2.min.js"></script>
59
- ```
55
+ ```html
56
+ <script type="text/javascript" src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
57
+ <script type="text/javascript" src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
58
+ <script type="text/javascript" src="./theme-switcher-1.0.2.min.js"></script>
59
+ ```
60
60
 
61
61
  6. Place theme switcher into container:
62
62
 
63
- ```html
64
- <script type="text/javascript">
65
- new ThemeSwitcher({ float: 'right' })
66
- .render(document.querySelector('.my-selector'));
67
- </script>
68
- ```
69
- where
70
- - `float` _(optional)_ - position of icon and dropdown box
63
+ ```html
64
+ <script type="text/javascript">
65
+ new ThemeSwitcher({ float: 'right' })
66
+ .render(document.querySelector('.my-selector'));
67
+ </script>
68
+ ```
69
+ where
70
+ - `float` _(optional)_ - position of icon and dropdown box
71
71
 
72
72
  ## Usage
73
73
 
74
74
  Use selectors to write theme-specific styles:
75
75
 
76
76
  ```css
77
- body[data-theme="light"] .selector {
78
- /* css rules */
79
- }
77
+ body[data-theme="light"] .selector {
78
+ /* css rules */
79
+ }
80
80
  ```
81
81
 
82
82
  ```css
83
- body[data-theme="dark"] .selector {
84
- /* css rules */
85
- }
83
+ body[data-theme="dark"] .selector {
84
+ /* css rules */
85
+ }
86
86
  ```
87
87
 
88
88
  Or you can just write default styles for light theme and override them for dark theme using `body[data-theme="dark"]`.
package/cspell.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": "0.2",
3
+ "enabled": true,
4
+ "language": "en",
5
+ "minWordLength": 4,
6
+ "enableGlobDot": true,
7
+ "useGitignore": true,
8
+ "ignorePaths": [
9
+ ".git",
10
+ "package-lock.json"
11
+ ],
12
+ "words": [
13
+ "anmiles",
14
+ "anatoliy",
15
+ "oblaukhov"
16
+ ],
17
+ "flagWords": []
18
+ }
@@ -0,0 +1,307 @@
1
+ var ThemeSwitcher = function(exports, require$$0, require$$0$1) {
2
+ "use strict";
3
+ var __vite_style__ = document.createElement("style");
4
+ __vite_style__.textContent = '.themeSwitcher {\n cursor: pointer;\n position: relative;\n}\n\n.themeSwitcher > svg:hover,\n.themeSwitcher li:hover {\n filter: brightness(1.5);\n}\n\n.themeSwitcher svg {\n width: 2em;\n height: 2em;\n stroke: currentColor;\n display: block;\n}\n\n.themeSwitcher ul {\n list-style-type: none;\n position: absolute;\n left: 0;\n margin: 0.5em 0;\n padding: 0;\n gap: 0;\n overflow-y: visible;\n z-index: 1;\n}\n\n.themeSwitcher li {\n padding: 0.5em 1em;\n}\n\n.themeSwitcher[data-float="right"] ul {\n left: auto;\n right: 0;\n}\n\n.themeSwitcher li {\n display: flex;\n align-items: center;\n gap: 0.5em;\n}\n\n.themeSwitcher li svg {\n width: 1.5em;\n height: 1.5em;\n}\n\n.themeSwitcher svg.checked {\n width: 16px;\n height: 13.5px;\n}\n/*$vite$:1*/';
5
+ document.head.appendChild(__vite_style__);
6
+ var jsxRuntime = { exports: {} };
7
+ var reactJsxRuntime_production_min = {};
8
+ /**
9
+ * @license React
10
+ * react-jsx-runtime.production.min.js
11
+ *
12
+ * Copyright (c) Facebook, Inc. and its affiliates.
13
+ *
14
+ * This source code is licensed under the MIT license found in the
15
+ * LICENSE file in the root directory of this source tree.
16
+ */
17
+ var hasRequiredReactJsxRuntime_production_min;
18
+ function requireReactJsxRuntime_production_min() {
19
+ if (hasRequiredReactJsxRuntime_production_min) return reactJsxRuntime_production_min;
20
+ hasRequiredReactJsxRuntime_production_min = 1;
21
+ var f = require$$0, k = Symbol.for("react.element"), l = Symbol.for("react.fragment"), m = Object.prototype.hasOwnProperty, n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, p = { key: true, ref: true, __self: true, __source: true };
22
+ function q(c, a, g) {
23
+ var b, d = {}, e = null, h = null;
24
+ void 0 !== g && (e = "" + g);
25
+ void 0 !== a.key && (e = "" + a.key);
26
+ void 0 !== a.ref && (h = a.ref);
27
+ for (b in a) m.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
28
+ if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]);
29
+ return { $$typeof: k, type: c, key: e, ref: h, props: d, _owner: n.current };
30
+ }
31
+ reactJsxRuntime_production_min.Fragment = l;
32
+ reactJsxRuntime_production_min.jsx = q;
33
+ reactJsxRuntime_production_min.jsxs = q;
34
+ return reactJsxRuntime_production_min;
35
+ }
36
+ var hasRequiredJsxRuntime;
37
+ function requireJsxRuntime() {
38
+ if (hasRequiredJsxRuntime) return jsxRuntime.exports;
39
+ hasRequiredJsxRuntime = 1;
40
+ {
41
+ jsxRuntime.exports = requireReactJsxRuntime_production_min();
42
+ }
43
+ return jsxRuntime.exports;
44
+ }
45
+ var jsxRuntimeExports = requireJsxRuntime();
46
+ var client = {};
47
+ var hasRequiredClient;
48
+ function requireClient() {
49
+ if (hasRequiredClient) return client;
50
+ hasRequiredClient = 1;
51
+ var m = require$$0$1;
52
+ {
53
+ client.createRoot = m.createRoot;
54
+ client.hydrateRoot = m.hydrateRoot;
55
+ }
56
+ return client;
57
+ }
58
+ var clientExports = requireClient();
59
+ class EventEmitter {
60
+ constructor() {
61
+ this.listeners = {};
62
+ }
63
+ on(event, listener) {
64
+ var _a;
65
+ const listeners = (_a = this.listeners)[event] ?? (_a[event] = []);
66
+ listeners.push(listener);
67
+ }
68
+ off(event, listener) {
69
+ var _a;
70
+ const listeners = (_a = this.listeners)[event] ?? (_a[event] = []);
71
+ listeners.splice(listeners.indexOf(listener), 1);
72
+ }
73
+ emit(event, ...data) {
74
+ var _a;
75
+ (_a = this.listeners[event]) == null ? void 0 : _a.forEach((listener) => {
76
+ listener(...data);
77
+ });
78
+ }
79
+ }
80
+ const themes = ["light", "dark"];
81
+ const defaultTheme = "light";
82
+ function isTheme(arg) {
83
+ return typeof arg === "string" && themes.map(String).includes(arg);
84
+ }
85
+ function getThemeName(theme) {
86
+ switch (theme) {
87
+ case "light":
88
+ return "Light";
89
+ case "dark":
90
+ return "Dark";
91
+ case void 0:
92
+ default:
93
+ return "System";
94
+ }
95
+ }
96
+ class SystemProvider extends EventEmitter {
97
+ get() {
98
+ if (!("matchMedia" in window)) {
99
+ return defaultTheme;
100
+ }
101
+ for (const theme of themes) {
102
+ const mediaQueryList = window.matchMedia(`(prefers-color-scheme: ${theme})`);
103
+ if (mediaQueryList.matches) {
104
+ return theme;
105
+ }
106
+ }
107
+ return defaultTheme;
108
+ }
109
+ watch() {
110
+ if (!("matchMedia" in window)) {
111
+ return;
112
+ }
113
+ for (const theme of themes) {
114
+ const mediaQueryList = window.matchMedia(`(prefers-color-scheme: ${theme})`);
115
+ mediaQueryList.addEventListener("change", (ev) => {
116
+ if (ev.matches) {
117
+ this.emit("change", theme);
118
+ }
119
+ });
120
+ }
121
+ }
122
+ }
123
+ class UserProvider extends EventEmitter {
124
+ constructor() {
125
+ super(...arguments);
126
+ this.storageKey = "theme";
127
+ }
128
+ get() {
129
+ const theme = localStorage.getItem(this.storageKey);
130
+ return isTheme(theme) ? theme : void 0;
131
+ }
132
+ set(theme) {
133
+ if (theme) {
134
+ localStorage.setItem(this.storageKey, theme);
135
+ } else {
136
+ localStorage.removeItem(this.storageKey);
137
+ }
138
+ this.emit("change", theme);
139
+ }
140
+ }
141
+ function Dark() {
142
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
143
+ "svg",
144
+ {
145
+ viewBox: "0 0 100 100",
146
+ xmlns: "http://www.w3.org/2000/svg",
147
+ className: "dark",
148
+ strokeWidth: "8",
149
+ strokeLinecap: "round",
150
+ fill: "none",
151
+ children: [
152
+ /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "50", cy: "50", r: "46", strokeDasharray: "180", transform: "rotate(22.5 50 50)" }),
153
+ /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "75", cy: "25", r: "46", strokeDasharray: "108 200", transform: "rotate(67.5 75 25)" })
154
+ ]
155
+ }
156
+ );
157
+ }
158
+ function Light() {
159
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
160
+ "svg",
161
+ {
162
+ viewBox: "0 0 100 100",
163
+ xmlns: "http://www.w3.org/2000/svg",
164
+ className: "light",
165
+ strokeWidth: "8",
166
+ strokeLinecap: "round",
167
+ fill: "none",
168
+ children: [
169
+ /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "50", cy: "50", r: "20" }),
170
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 10", transform: "rotate(0 50 50)" }),
171
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 10", transform: "rotate(90 50 50)" }),
172
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 10", transform: "rotate(180 50 50)" }),
173
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 10", transform: "rotate(270 50 50)" }),
174
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 15", transform: "rotate(45 50 50)" }),
175
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 15", transform: "rotate(135 50 50)" }),
176
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 15", transform: "rotate(225 50 50)" }),
177
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 86 v 15", transform: "rotate(315 50 50)" })
178
+ ]
179
+ }
180
+ );
181
+ }
182
+ function System() {
183
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
184
+ "svg",
185
+ {
186
+ viewBox: "0 0 100 100",
187
+ xmlns: "http://www.w3.org/2000/svg",
188
+ className: "system",
189
+ strokeWidth: "8",
190
+ strokeLinecap: "round",
191
+ fill: "none",
192
+ children: [
193
+ /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "50", cy: "50", r: "46" }),
194
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
195
+ "path",
196
+ {
197
+ strokeWidth: "0",
198
+ fill: "currentColor",
199
+ d: "\n M 50,0\n a 50,50,0,1,1,0,100\n Z"
200
+ }
201
+ )
202
+ ]
203
+ }
204
+ );
205
+ }
206
+ function Icon({ theme }) {
207
+ switch (theme) {
208
+ case "light":
209
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(Light, {});
210
+ case "dark":
211
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(Dark, {});
212
+ case void 0:
213
+ default:
214
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(System, {});
215
+ }
216
+ }
217
+ function Checked() {
218
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
219
+ "svg",
220
+ {
221
+ viewBox: "0 0 640 540",
222
+ xmlns: "http://www.w3.org/2000/svg",
223
+ className: "checked",
224
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
225
+ "path",
226
+ {
227
+ fill: "currentColor",
228
+ d: "\n M 12,370\n a 40,40,0,0,1,56.56,-56.56\n l 130,130\n l 370,-430\n a 40,40,0,0,1,56.56,56.56\n l -398.28,458.28\n a 40,40,0,0,1,-56.56,0\n l -140,-140\n Z"
229
+ }
230
+ )
231
+ }
232
+ );
233
+ }
234
+ function ThemeSelector({ currentUserTheme, onListItemClick }) {
235
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { "data-testid": "theme-selector", children: [...themes, void 0].map((theme) => {
236
+ const themeName = getThemeName(theme);
237
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
238
+ "li",
239
+ {
240
+ "data-testid": `theme-item-${themeName.toLowerCase()}`,
241
+ onClick: () => {
242
+ onListItemClick(theme);
243
+ },
244
+ children: [
245
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { theme }),
246
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: themeName }),
247
+ currentUserTheme === theme && /* @__PURE__ */ jsxRuntimeExports.jsx(Checked, {})
248
+ ]
249
+ },
250
+ themeName
251
+ );
252
+ }) });
253
+ }
254
+ function App({ float }) {
255
+ const userProvider = require$$0.useMemo(() => new UserProvider(), []);
256
+ const systemProvider = require$$0.useMemo(() => new SystemProvider(), []);
257
+ const [userTheme, setUserTheme] = require$$0.useState(userProvider.get());
258
+ const [systemTheme, setSystemTheme] = require$$0.useState(systemProvider.get());
259
+ const [showList, setShowList] = require$$0.useState(false);
260
+ const theme = userTheme ?? systemTheme;
261
+ require$$0.useEffect(() => {
262
+ document.body.setAttribute("data-theme", theme);
263
+ userProvider.on("change", setUserTheme);
264
+ systemProvider.on("change", setSystemTheme);
265
+ systemProvider.watch();
266
+ }, [theme, userProvider, systemProvider]);
267
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
268
+ "div",
269
+ {
270
+ className: "themeSwitcher",
271
+ "data-testid": "theme-switcher",
272
+ "data-float": float,
273
+ onClick: () => {
274
+ setShowList(!showList);
275
+ },
276
+ children: [
277
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { theme }),
278
+ !showList ? null : /* @__PURE__ */ jsxRuntimeExports.jsx(
279
+ ThemeSelector,
280
+ {
281
+ currentUserTheme: userTheme,
282
+ onListItemClick: (theme2) => {
283
+ userProvider.set(theme2);
284
+ setShowList(false);
285
+ }
286
+ }
287
+ )
288
+ ]
289
+ }
290
+ );
291
+ }
292
+ class Element {
293
+ constructor(props) {
294
+ this.props = props;
295
+ }
296
+ render(parentNode) {
297
+ const root = clientExports.createRoot(parentNode);
298
+ root.render(
299
+ /* @__PURE__ */ jsxRuntimeExports.jsx(require$$0.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(App, { ...this.props }) })
300
+ );
301
+ }
302
+ }
303
+ exports.Element = Element;
304
+ exports.ThemeSwitcher = App;
305
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
306
+ return exports;
307
+ }({}, React, ReactDOM);
@@ -0,0 +1,22 @@
1
+ var ThemeSwitcher=function(d,c,T){"use strict";var y=document.createElement("style");y.textContent=`.themeSwitcher{cursor:pointer;position:relative}.themeSwitcher>svg:hover,.themeSwitcher li:hover{filter:brightness(1.5)}.themeSwitcher svg{width:2em;height:2em;stroke:currentColor;display:block}.themeSwitcher ul{list-style-type:none;position:absolute;left:0;margin:.5em 0;padding:0;gap:0;overflow-y:visible;z-index:1}.themeSwitcher li{padding:.5em 1em}.themeSwitcher[data-float=right] ul{left:auto;right:0}.themeSwitcher li{display:flex;align-items:center;gap:.5em}.themeSwitcher li svg{width:1.5em;height:1.5em}.themeSwitcher svg.checked{width:16px;height:13.5px}
2
+ /*$vite$:1*/`,document.head.appendChild(y);var w={exports:{}},m={};/**
3
+ * @license React
4
+ * react-jsx-runtime.production.min.js
5
+ *
6
+ * Copyright (c) Facebook, Inc. and its affiliates.
7
+ *
8
+ * This source code is licensed under the MIT license found in the
9
+ * LICENSE file in the root directory of this source tree.
10
+ */var j;function C(){if(j)return m;j=1;var s=c,e=Symbol.for("react.element"),r=Symbol.for("react.fragment"),n=Object.prototype.hasOwnProperty,i=s.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,g={key:!0,ref:!0,__self:!0,__source:!0};function p(h,o,l){var a,x={},v=null,E=null;l!==void 0&&(v=""+l),o.key!==void 0&&(v=""+o.key),o.ref!==void 0&&(E=o.ref);for(a in o)n.call(o,a)&&!g.hasOwnProperty(a)&&(x[a]=o[a]);if(h&&h.defaultProps)for(a in o=h.defaultProps,o)x[a]===void 0&&(x[a]=o[a]);return{$$typeof:e,type:h,key:v,ref:E,props:x,_owner:i.current}}return m.Fragment=r,m.jsx=p,m.jsxs=p,m}var S;function b(){return S||(S=1,w.exports=C()),w.exports}var t=b(),u={},k;function O(){if(k)return u;k=1;var s=T;return u.createRoot=s.createRoot,u.hydrateRoot=s.hydrateRoot,u}var N=O();class _{constructor(){this.listeners={}}on(e,r){var i;((i=this.listeners)[e]??(i[e]=[])).push(r)}off(e,r){var i;const n=(i=this.listeners)[e]??(i[e]=[]);n.splice(n.indexOf(r),1)}emit(e,...r){var n;(n=this.listeners[e])==null||n.forEach(i=>{i(...r)})}}const f=["light","dark"],R="light";function P(s){return typeof s=="string"&&f.map(String).includes(s)}function I(s){switch(s){case"light":return"Light";case"dark":return"Dark";case void 0:default:return"System"}}class D extends _{get(){if(!("matchMedia"in window))return R;for(const e of f)if(window.matchMedia(`(prefers-color-scheme: ${e})`).matches)return e;return R}watch(){if("matchMedia"in window)for(const e of f)window.matchMedia(`(prefers-color-scheme: ${e})`).addEventListener("change",n=>{n.matches&&this.emit("change",e)})}}class B extends _{constructor(){super(...arguments),this.storageKey="theme"}get(){const e=localStorage.getItem(this.storageKey);return P(e)?e:void 0}set(e){e?localStorage.setItem(this.storageKey,e):localStorage.removeItem(this.storageKey),this.emit("change",e)}}function J(){return t.jsxs("svg",{viewBox:"0 0 100 100",xmlns:"http://www.w3.org/2000/svg",className:"dark",strokeWidth:"8",strokeLinecap:"round",fill:"none",children:[t.jsx("circle",{cx:"50",cy:"50",r:"46",strokeDasharray:"180",transform:"rotate(22.5 50 50)"}),t.jsx("circle",{cx:"75",cy:"25",r:"46",strokeDasharray:"108 200",transform:"rotate(67.5 75 25)"})]})}function U(){return t.jsxs("svg",{viewBox:"0 0 100 100",xmlns:"http://www.w3.org/2000/svg",className:"light",strokeWidth:"8",strokeLinecap:"round",fill:"none",children:[t.jsx("circle",{cx:"50",cy:"50",r:"20"}),t.jsx("path",{d:"M 50 86 v 10",transform:"rotate(0 50 50)"}),t.jsx("path",{d:"M 50 86 v 10",transform:"rotate(90 50 50)"}),t.jsx("path",{d:"M 50 86 v 10",transform:"rotate(180 50 50)"}),t.jsx("path",{d:"M 50 86 v 10",transform:"rotate(270 50 50)"}),t.jsx("path",{d:"M 50 86 v 15",transform:"rotate(45 50 50)"}),t.jsx("path",{d:"M 50 86 v 15",transform:"rotate(135 50 50)"}),t.jsx("path",{d:"M 50 86 v 15",transform:"rotate(225 50 50)"}),t.jsx("path",{d:"M 50 86 v 15",transform:"rotate(315 50 50)"})]})}function W(){return t.jsxs("svg",{viewBox:"0 0 100 100",xmlns:"http://www.w3.org/2000/svg",className:"system",strokeWidth:"8",strokeLinecap:"round",fill:"none",children:[t.jsx("circle",{cx:"50",cy:"50",r:"46"}),t.jsx("path",{strokeWidth:"0",fill:"currentColor",d:`
11
+ M 50,0
12
+ a 50,50,0,1,1,0,100
13
+ Z`})]})}function M({theme:s}){switch(s){case"light":return t.jsx(U,{});case"dark":return t.jsx(J,{});case void 0:default:return t.jsx(W,{})}}function K(){return t.jsx("svg",{viewBox:"0 0 640 540",xmlns:"http://www.w3.org/2000/svg",className:"checked",children:t.jsx("path",{fill:"currentColor",d:`
14
+ M 12,370
15
+ a 40,40,0,0,1,56.56,-56.56
16
+ l 130,130
17
+ l 370,-430
18
+ a 40,40,0,0,1,56.56,56.56
19
+ l -398.28,458.28
20
+ a 40,40,0,0,1,-56.56,0
21
+ l -140,-140
22
+ Z`})})}function Q({currentUserTheme:s,onListItemClick:e}){return t.jsx("ul",{"data-testid":"theme-selector",children:[...f,void 0].map(r=>{const n=I(r);return t.jsxs("li",{"data-testid":`theme-item-${n.toLowerCase()}`,onClick:()=>{e(r)},children:[t.jsx(M,{theme:r}),t.jsx("span",{children:n}),s===r&&t.jsx(K,{})]},n)})})}function L({float:s}){const e=c.useMemo(()=>new B,[]),r=c.useMemo(()=>new D,[]),[n,i]=c.useState(e.get()),[g,p]=c.useState(r.get()),[h,o]=c.useState(!1),l=n??g;return c.useEffect(()=>{document.body.setAttribute("data-theme",l),e.on("change",i),r.on("change",p),r.watch()},[l,e,r]),t.jsxs("div",{className:"themeSwitcher","data-testid":"theme-switcher","data-float":s,onClick:()=>{o(!h)},children:[t.jsx(M,{theme:l}),h?t.jsx(Q,{currentUserTheme:n,onListItemClick:a=>{e.set(a),o(!1)}}):null]})}class A{constructor(e){this.props=e}render(e){N.createRoot(e).render(t.jsx(c.StrictMode,{children:t.jsx(L,{...this.props})}))}}return d.Element=A,d.ThemeSwitcher=L,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"}),d}({},React,ReactDOM);
@@ -0,0 +1,16 @@
1
+ import { configs } from '@anmiles/eslint-config';
2
+ import type { Linter } from 'eslint';
3
+
4
+ export default [
5
+ ...configs.base,
6
+ ...configs.ts,
7
+ ...configs.jest,
8
+ ...configs.react,
9
+
10
+ {
11
+ ignores: [
12
+ 'coverage/*',
13
+ 'dist/*',
14
+ ],
15
+ },
16
+ ] as Linter.Config[];
package/index.html ADDED
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Theme switcher</title>
5
+ <meta charset="utf-8">
6
+ <link rel="stylesheet" href="https://dev.anmiles.net/index.css" type="text/css">
7
+ <link rel="icon" href="https://dev.anmiles.net/favicon.ico" type="image/x-icon">
8
+ <style type="text/css">
9
+ .theme {
10
+ cursor: pointer;
11
+ float: right;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body>
16
+
17
+ <div class="top">
18
+ <div class="theme"></div>
19
+ </div>
20
+
21
+ <div class="box">
22
+
23
+ <p>Merry alone do it burst me songs. Sorry equal charm joy her those folly ham. In they no is many both. Recommend new contented intention improving bed performed age. Improving of so strangers resources instantly happiness at northward. Danger nearer length oppose really add now either. But ask regret eat branch fat garden. Become am he except wishes. Past so at door we walk want such sang. Feeling colonel get her garrets own.</p>
24
+
25
+ <p>Boisterous he on understood attachment as entreaties ye devonshire. In mile an form snug were been sell. Hastened admitted joy nor absolute gay its. Extremely ham any his departure for contained curiosity defective. Way now instrument had eat diminution melancholy expression sentiments stimulated. One built fat you out manor books. Mrs interested now his affronting inquietude contrasted cultivated. Lasting showing expense greater on colonel no.</p>
26
+
27
+ <p>Man request adapted spirits set pressed. Up to denoting subjects sensible feelings it indulged directly. We dwelling elegance do shutters appetite yourself diverted. Our next drew much you with rank. Tore many held age hold rose than our. She literature sentiments any contrasted. Set aware joy sense young now tears china shy.</p>
28
+
29
+ </div>
30
+
31
+ <script type="module">
32
+ import * as ThemeSwitcher from '/src/index.tsx';
33
+
34
+ new ThemeSwitcher.Element({ float: 'right' }).render(document.querySelector('.theme'));
35
+ </script>
36
+
37
+ </body>
38
+ </html>
package/jest.config.js CHANGED
@@ -1,26 +1,24 @@
1
1
  module.exports = {
2
- preset : 'ts-jest',
3
- transform : {
4
- '^.+\\.tsx?$' : [
5
- 'ts-jest',
6
- {
7
- tsconfig : '<rootDir>/tsconfig.test.json',
8
- },
9
- ],
2
+ preset : 'ts-jest',
3
+ transform: {
4
+ '^.+\\.tsx?$': [ 'ts-jest', {
5
+ tsconfig: './tsconfig.test.json',
6
+ } ],
10
7
  },
11
8
 
12
- clearMocks : true,
13
- testEnvironment : 'jsdom',
9
+ clearMocks : true,
10
+ testEnvironment: 'jsdom',
14
11
 
15
- roots : [ '<rootDir>/src' ],
16
- testMatch : [ '<rootDir>/src/**/__tests__/*.test.{ts,tsx}' ],
12
+ roots : [ '<rootDir>/src' ],
13
+ testMatch: [ '<rootDir>/src/**/__tests__/*.test.{ts,tsx}' ],
17
14
 
18
- collectCoverageFrom : [
15
+ collectCoverageFrom: [
19
16
  '<rootDir>/src/**/*.{ts,tsx}',
20
17
  '!<rootDir>/src/**/__tests__/**',
18
+ '!<rootDir>/src/**/__mocks__/**',
21
19
  ],
22
20
 
23
- moduleNameMapper : {
24
- '\\.(css|less)$' : '<rootDir>/src/__mocks__/css.ts',
21
+ moduleNameMapper: {
22
+ '\\.(css|less)$': '<rootDir>/src/__mocks__/css.ts',
25
23
  },
26
24
  };