@anmiles/theme-switcher 1.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 (38) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.js +10 -0
  3. package/.vscode/settings.json +6 -0
  4. package/CHANGELOG.md +14 -0
  5. package/LICENSE.md +21 -0
  6. package/README.md +88 -0
  7. package/coverage.config.js +8 -0
  8. package/dev/index.html +35 -0
  9. package/dist/theme-switcher-0.1.0.js +2194 -0
  10. package/dist/theme-switcher-0.1.0.min.js +2 -0
  11. package/dist/theme-switcher-0.1.0.min.js.LICENSE.txt +9 -0
  12. package/jest.config.js +26 -0
  13. package/package.json +71 -0
  14. package/src/__mocks__/css.ts +1 -0
  15. package/src/__tests__/index.test.tsx +25 -0
  16. package/src/__tests__/theme.test.ts +23 -0
  17. package/src/components/App.tsx +55 -0
  18. package/src/components/Icon.tsx +19 -0
  19. package/src/components/ThemeSelector.tsx +34 -0
  20. package/src/components/__tests__/App.test.tsx +350 -0
  21. package/src/components/__tests__/__snapshots__/App.test.tsx.snap +153 -0
  22. package/src/components/icons/Checked.tsx +24 -0
  23. package/src/components/icons/Dark.tsx +13 -0
  24. package/src/components/icons/Light.tsx +21 -0
  25. package/src/components/icons/System.tsx +18 -0
  26. package/src/index.tsx +20 -0
  27. package/src/lib/__tests__/eventEmitter.test.ts +109 -0
  28. package/src/lib/eventEmitter.ts +32 -0
  29. package/src/lib/theme.ts +21 -0
  30. package/src/providers/__tests__/systemProvider.test.ts +102 -0
  31. package/src/providers/__tests__/userProvider.test.ts +60 -0
  32. package/src/providers/systemProvider.ts +40 -0
  33. package/src/providers/userProvider.ts +24 -0
  34. package/src/styles/style.css +52 -0
  35. package/tsconfig.build.json +7 -0
  36. package/tsconfig.json +16 -0
  37. package/tsconfig.test.json +7 -0
  38. package/webpack.config.js +67 -0
@@ -0,0 +1,153 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`src/App switcher html should match snapshot on dark system theme 1`] = `
4
+ <div
5
+ class="themeSwitcher"
6
+ data-testid="theme-switcher"
7
+ >
8
+ {light}
9
+ <ul
10
+ data-testid="theme-selector"
11
+ >
12
+ <li
13
+ data-testid="theme-item-light"
14
+ >
15
+ {light}
16
+ <span>
17
+ Light
18
+ </span>
19
+ </li>
20
+ <li
21
+ data-testid="theme-item-dark"
22
+ >
23
+ {dark}
24
+ <span>
25
+ Dark
26
+ </span>
27
+ </li>
28
+ <li
29
+ data-testid="theme-item-system"
30
+ >
31
+ {system}
32
+ <span>
33
+ System
34
+ </span>
35
+ {checked}
36
+ </li>
37
+ </ul>
38
+ </div>
39
+ `;
40
+
41
+ exports[`src/App switcher html should match snapshot on dark user theme 1`] = `
42
+ <div
43
+ class="themeSwitcher"
44
+ data-testid="theme-switcher"
45
+ >
46
+ {dark}
47
+ <ul
48
+ data-testid="theme-selector"
49
+ >
50
+ <li
51
+ data-testid="theme-item-light"
52
+ >
53
+ {light}
54
+ <span>
55
+ Light
56
+ </span>
57
+ </li>
58
+ <li
59
+ data-testid="theme-item-dark"
60
+ >
61
+ {dark}
62
+ <span>
63
+ Dark
64
+ </span>
65
+ {checked}
66
+ </li>
67
+ <li
68
+ data-testid="theme-item-system"
69
+ >
70
+ {system}
71
+ <span>
72
+ System
73
+ </span>
74
+ </li>
75
+ </ul>
76
+ </div>
77
+ `;
78
+
79
+ exports[`src/App switcher html should match snapshot on light system theme 1`] = `
80
+ <div
81
+ class="themeSwitcher"
82
+ data-testid="theme-switcher"
83
+ >
84
+ {light}
85
+ <ul
86
+ data-testid="theme-selector"
87
+ >
88
+ <li
89
+ data-testid="theme-item-light"
90
+ >
91
+ {light}
92
+ <span>
93
+ Light
94
+ </span>
95
+ </li>
96
+ <li
97
+ data-testid="theme-item-dark"
98
+ >
99
+ {dark}
100
+ <span>
101
+ Dark
102
+ </span>
103
+ </li>
104
+ <li
105
+ data-testid="theme-item-system"
106
+ >
107
+ {system}
108
+ <span>
109
+ System
110
+ </span>
111
+ {checked}
112
+ </li>
113
+ </ul>
114
+ </div>
115
+ `;
116
+
117
+ exports[`src/App switcher html should match snapshot on light user theme 1`] = `
118
+ <div
119
+ class="themeSwitcher"
120
+ data-testid="theme-switcher"
121
+ >
122
+ {light}
123
+ <ul
124
+ data-testid="theme-selector"
125
+ >
126
+ <li
127
+ data-testid="theme-item-light"
128
+ >
129
+ {light}
130
+ <span>
131
+ Light
132
+ </span>
133
+ {checked}
134
+ </li>
135
+ <li
136
+ data-testid="theme-item-dark"
137
+ >
138
+ {dark}
139
+ <span>
140
+ Dark
141
+ </span>
142
+ </li>
143
+ <li
144
+ data-testid="theme-item-system"
145
+ >
146
+ {system}
147
+ <span>
148
+ System
149
+ </span>
150
+ </li>
151
+ </ul>
152
+ </div>
153
+ `;
@@ -0,0 +1,24 @@
1
+ /* istanbul ignore file */
2
+ export default function Checked() {
3
+ return (
4
+ <svg
5
+ viewBox="0 0 640 540" xmlns="http://www.w3.org/2000/svg"
6
+ className="checked"
7
+ >
8
+ <path
9
+ fill="currentColor"
10
+ d="
11
+ M 12,370
12
+ a 40,40,0,0,1,56.56,-56.56
13
+ l 130,130
14
+ l 370,-430
15
+ a 40,40,0,0,1,56.56,56.56
16
+ l -398.28,458.28
17
+ a 40,40,0,0,1,-56.56,0
18
+ l -140,-140
19
+ Z"
20
+ />
21
+ </svg>
22
+
23
+ );
24
+ }
@@ -0,0 +1,13 @@
1
+ /* istanbul ignore file */
2
+ export default function Dark() {
3
+ return (
4
+ <svg
5
+ viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
6
+ className="dark" strokeWidth="8" strokeLinecap="round" fill="none"
7
+ >
8
+ <circle cx="50" cy="50" r="46" strokeDasharray="180" transform="rotate(22.5 50 50)" />
9
+ <circle cx="75" cy="25" r="46" strokeDasharray="108 200" transform="rotate(67.5 75 25)" />
10
+ </svg>
11
+
12
+ );
13
+ }
@@ -0,0 +1,21 @@
1
+ /* istanbul ignore file */
2
+ export default function Light() {
3
+ return (
4
+ <svg
5
+ viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
6
+ className="light" strokeWidth="8" strokeLinecap="round" fill="none"
7
+ >
8
+ <circle cx="50" cy="50" r="20" />
9
+
10
+ <path d="M 50 86 v 10" transform="rotate(0 50 50)" />
11
+ <path d="M 50 86 v 10" transform="rotate(90 50 50)" />
12
+ <path d="M 50 86 v 10" transform="rotate(180 50 50)" />
13
+ <path d="M 50 86 v 10" transform="rotate(270 50 50)" />
14
+
15
+ <path d="M 50 86 v 15" transform="rotate(45 50 50)" />
16
+ <path d="M 50 86 v 15" transform="rotate(135 50 50)" />
17
+ <path d="M 50 86 v 15" transform="rotate(225 50 50)" />
18
+ <path d="M 50 86 v 15" transform="rotate(315 50 50)" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,18 @@
1
+ /* istanbul ignore file */
2
+ export default function System() {
3
+ return (
4
+ <svg
5
+ viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
6
+ className="system" strokeWidth="8" strokeLinecap="round" fill="none"
7
+ >
8
+ <circle cx="50" cy="50" r="46" />
9
+
10
+ <path
11
+ strokeWidth="0" fill="currentColor" d="
12
+ M 50,0
13
+ a 50,50,0,1,1,0,100
14
+ Z"
15
+ />
16
+ </svg>
17
+ );
18
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import type { AppProps } from './components/App';
4
+ import App from './components/App';
5
+
6
+ class ThemeSwitcherElement {
7
+ constructor(private readonly props: AppProps) {}
8
+
9
+ public render(parentNode: HTMLElement) {
10
+ const root = createRoot(parentNode);
11
+
12
+ root.render(
13
+ <StrictMode>
14
+ <App { ...this.props } />
15
+ </StrictMode>,
16
+ );
17
+ }
18
+ }
19
+
20
+ export { ThemeSwitcherElement, App as ThemeSwitcher };
@@ -0,0 +1,109 @@
1
+ import { EventEmitter } from '../eventEmitter';
2
+
3
+ describe('src/lib/eventEmitter', () => {
4
+ const data = { key : 'value' };
5
+
6
+ const changeListener1 = jest.fn();
7
+ const changeListener2 = jest.fn();
8
+
9
+ const deleteListener1 = jest.fn();
10
+ const deleteListener2 = jest.fn();
11
+
12
+ let eventEmitter: InstanceType<typeof EventEmitter>;
13
+
14
+ beforeEach(() => {
15
+ eventEmitter = new EventEmitter();
16
+
17
+ eventEmitter.on('change', changeListener1);
18
+ eventEmitter.on('change', changeListener2);
19
+
20
+ eventEmitter.on('delete', deleteListener1);
21
+ eventEmitter.on('delete', deleteListener2);
22
+ });
23
+
24
+ it('should register added listeners', () => {
25
+ const expectedListeners = {
26
+ change : [ changeListener1, changeListener2 ],
27
+ delete : [ deleteListener1, deleteListener2 ],
28
+ };
29
+
30
+ expect(eventEmitter['listeners']).toEqual(expectedListeners);
31
+ });
32
+
33
+ it('should call listeners when emitting event', () => {
34
+ eventEmitter['emit']('change', data);
35
+
36
+ expect(changeListener1).toHaveBeenCalledWith(data);
37
+ expect(changeListener2).toHaveBeenCalledWith(data);
38
+
39
+ expect(deleteListener1).not.toHaveBeenCalled();
40
+ expect(deleteListener2).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it('should not call listeners onw unknown event', () => {
44
+ eventEmitter['emit']('unknown', data);
45
+
46
+ expect(changeListener1).not.toHaveBeenCalled();
47
+ expect(changeListener2).not.toHaveBeenCalled();
48
+
49
+ expect(deleteListener1).not.toHaveBeenCalled();
50
+ expect(deleteListener2).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('should properly call listeners when emitting event without data', () => {
54
+ eventEmitter['emit']('delete');
55
+
56
+ expect(deleteListener1).toHaveBeenCalledWith();
57
+ expect(deleteListener2).toHaveBeenCalledWith();
58
+ });
59
+
60
+ it('should call listener the number of times that it was registered', () => {
61
+ eventEmitter['emit']('change', data);
62
+
63
+ expect(changeListener1).toHaveBeenCalledTimes(1);
64
+ changeListener1.mockClear();
65
+
66
+ eventEmitter.on('change', changeListener1);
67
+ eventEmitter.on('change', changeListener1);
68
+
69
+ eventEmitter['emit']('change', data);
70
+
71
+ expect(changeListener1).toHaveBeenCalledTimes(3);
72
+ });
73
+
74
+ it('should not call unsubscribed listener', () => {
75
+ eventEmitter.off('change', changeListener1);
76
+ eventEmitter['emit']('change', data);
77
+
78
+ expect(changeListener1).not.toHaveBeenCalled();
79
+ expect(changeListener2).toHaveBeenCalledWith(data);
80
+ });
81
+
82
+ it('should change nothing on unsubscribing from non-existing events', () => {
83
+ eventEmitter.off('unknown', changeListener1);
84
+ eventEmitter['emit']('change', data);
85
+
86
+ expect(changeListener1).toHaveBeenCalledWith(data);
87
+ expect(changeListener2).toHaveBeenCalledWith(data);
88
+
89
+ expect(deleteListener1).not.toHaveBeenCalled();
90
+ expect(deleteListener2).not.toHaveBeenCalled();
91
+ });
92
+
93
+ it('should work correctly in derived class', () => {
94
+ const spy = jest.fn();
95
+
96
+ class EventEmitterChild extends EventEmitter<{ myEvent : [{ id : number }, string] }> {
97
+ }
98
+
99
+ const instance = new EventEmitterChild();
100
+
101
+ instance.on('myEvent', (...data) => {
102
+ spy(...data);
103
+ });
104
+
105
+ instance['emit']('myEvent', { id : 1 }, 'something');
106
+
107
+ expect(spy).toHaveBeenCalledWith({ id : 1 }, 'something');
108
+ });
109
+ });
@@ -0,0 +1,32 @@
1
+ class EventEmitter<TEventMap extends Record<string, Array<unknown>>> {
2
+ private readonly listeners : {
3
+ [TEvent in keyof TEventMap]?: Array<(...data: TEventMap[TEvent]) => void>
4
+ } = {};
5
+
6
+ public on<TEvent extends keyof TEventMap>(
7
+ event: TEvent,
8
+ listener: (...data: TEventMap[TEvent]) => void,
9
+ ): void {
10
+ const listeners = this.listeners[event] ??= [];
11
+ listeners.push(listener);
12
+ }
13
+
14
+ public off<TEvent extends keyof TEventMap>(
15
+ event: TEvent,
16
+ listener: (...data: TEventMap[TEvent]) => void,
17
+ ): void {
18
+ const listeners = this.listeners[event] ??= [];
19
+ listeners.splice(listeners.indexOf(listener), 1);
20
+ }
21
+
22
+ protected emit<TEvent extends keyof TEventMap>(
23
+ event: TEvent,
24
+ ...data: TEventMap[TEvent]
25
+ ): void {
26
+ this.listeners[event]?.forEach((listener) => {
27
+ listener(...data);
28
+ });
29
+ }
30
+ }
31
+
32
+ export { EventEmitter };
@@ -0,0 +1,21 @@
1
+ const themes = [ 'light', 'dark' ] as const;
2
+ type Theme = typeof themes[number];
3
+ const defaultTheme: Theme = 'light';
4
+
5
+ function isTheme(arg: unknown): arg is Theme {
6
+ return typeof arg === 'string' && themes.includes(arg as Theme);
7
+ }
8
+
9
+ function getThemeName(theme: Theme | undefined): string {
10
+ switch (theme) {
11
+ case 'light':
12
+ return 'Light';
13
+ case 'dark':
14
+ return 'Dark';
15
+ default:
16
+ return 'System';
17
+ }
18
+ }
19
+
20
+ export type { Theme };
21
+ export { themes, defaultTheme, isTheme, getThemeName };
@@ -0,0 +1,102 @@
1
+ import { EventEmitter } from '../../lib/eventEmitter';
2
+ import type { Theme } from '../../lib/theme';
3
+ import { defaultTheme } from '../../lib/theme';
4
+ import { SystemProvider } from '../systemProvider';
5
+
6
+ let systemProvider: SystemProvider;
7
+ const changeSpy = jest.fn();
8
+
9
+ class MediaQueryListEvents extends EventEmitter<{ change : [Partial<MediaQueryListEvent>] }> {}
10
+
11
+ const mediaQueryListEvents: Record<Theme, InstanceType<typeof MediaQueryListEvents>> = {
12
+ light : new MediaQueryListEvents(),
13
+ dark : new MediaQueryListEvents(),
14
+ };
15
+
16
+ let systemPreference: Theme | undefined;
17
+
18
+ beforeEach(() => {
19
+ window.localStorage.clear();
20
+ systemProvider = new SystemProvider();
21
+ systemProvider.on('change', changeSpy);
22
+ systemPreference = undefined;
23
+
24
+ window.matchMedia = jest.fn().mockImplementation((query: string): Partial<MediaQueryList> => {
25
+ const parsedTheme = /\(prefers-color-scheme: (.*)\)/.exec(query)?.[1] as Theme;
26
+
27
+ return {
28
+ matches : parsedTheme === systemPreference,
29
+ addEventListener : (
30
+ event: keyof MediaQueryListEventMap,
31
+ listener: (ev: Partial<MediaQueryListEvent>) => void,
32
+ ) => {
33
+ mediaQueryListEvents[parsedTheme].on(event, listener);
34
+ },
35
+ };
36
+ });
37
+ });
38
+
39
+ describe('src/providers/systemProvider', () => {
40
+ describe('get', () => {
41
+ it('should return defaultTheme if window.matchMedia is not defined', () => {
42
+ // @ts-expect-error window always has matchMedia
43
+ delete window.matchMedia;
44
+ expect(systemProvider.get()).toEqual(defaultTheme);
45
+ });
46
+
47
+ it('should return defaultTheme if nothing preferred', () => {
48
+ expect(systemProvider.get()).toEqual(defaultTheme);
49
+ });
50
+
51
+ it('should return light theme if preferred', () => {
52
+ systemPreference = 'light';
53
+ expect(systemProvider.get()).toEqual('light');
54
+ });
55
+
56
+ it('should return dark theme if preferred', () => {
57
+ systemPreference = 'dark';
58
+ expect(systemProvider.get()).toEqual('dark');
59
+ });
60
+ });
61
+
62
+ describe('watch', () => {
63
+ it('should never emit change events if window.matchMedia is not defined', () => {
64
+ // @ts-expect-error window always has matchMedia
65
+ delete window.matchMedia;
66
+ systemProvider.watch();
67
+ mediaQueryListEvents.light['emit']('change', { matches : true });
68
+ mediaQueryListEvents.dark['emit']('change', { matches : true });
69
+
70
+ expect(changeSpy).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('should never emit change events if not called', () => {
74
+ mediaQueryListEvents.light['emit']('change', { matches : true });
75
+ mediaQueryListEvents.dark['emit']('change', { matches : true });
76
+
77
+ expect(changeSpy).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('should emit change event on mediaQueryList change for light theme with match', () => {
81
+ systemProvider.watch();
82
+ mediaQueryListEvents.light['emit']('change', { matches : true });
83
+
84
+ expect(changeSpy).toHaveBeenCalledWith('light');
85
+ });
86
+
87
+ it('should emit change event on mediaQueryList change for dark theme with match', () => {
88
+ systemProvider.watch();
89
+ mediaQueryListEvents.dark['emit']('change', { matches : true });
90
+
91
+ expect(changeSpy).toHaveBeenCalledWith('dark');
92
+ });
93
+
94
+ it('should not emit change event on mediaQueryList changes with no match', () => {
95
+ systemProvider.watch();
96
+ mediaQueryListEvents.light['emit']('change', { matches : false });
97
+ mediaQueryListEvents.dark['emit']('change', { matches : false });
98
+
99
+ expect(changeSpy).not.toHaveBeenCalled();
100
+ });
101
+ });
102
+ });
@@ -0,0 +1,60 @@
1
+ import { UserProvider } from '../userProvider';
2
+
3
+ const storageKey = 'theme';
4
+ const changeSpy = jest.fn();
5
+ let userProvider: UserProvider;
6
+
7
+ beforeEach(() => {
8
+ window.localStorage.clear();
9
+ userProvider = new UserProvider();
10
+ userProvider.on('change', changeSpy);
11
+ });
12
+
13
+ describe('src/providers/userProvider', () => {
14
+ describe('get', () => {
15
+ it('should return light theme if set in localStorage', () => {
16
+ window.localStorage.setItem(storageKey, 'light');
17
+
18
+ expect(userProvider.get()).toEqual('light');
19
+ });
20
+
21
+ it('should return dark theme if set in localStorage', () => {
22
+ window.localStorage.setItem(storageKey, 'dark');
23
+
24
+ expect(userProvider.get()).toEqual('dark');
25
+ });
26
+
27
+ it('should return undefined if unknown string set in localStorage', () => {
28
+ window.localStorage.setItem(storageKey, 'unknown');
29
+
30
+ expect(userProvider.get()).toEqual(undefined);
31
+ });
32
+
33
+ it('should return undefined if not set in localStorage', () => {
34
+ expect(userProvider.get()).toEqual(undefined);
35
+ });
36
+ });
37
+
38
+ describe('set', () => {
39
+ it('should remove theme from localStorage and emit event if undefined passed', () => {
40
+ userProvider.set(undefined);
41
+
42
+ expect(changeSpy).toHaveBeenCalledWith(undefined);
43
+ expect(localStorage.getItem(storageKey)).toEqual(null);
44
+ });
45
+
46
+ it('should save light theme to localStorage and emit event if light passed', () => {
47
+ userProvider.set('light');
48
+
49
+ expect(changeSpy).toHaveBeenCalledWith('light');
50
+ expect(localStorage.getItem(storageKey)).toEqual('light');
51
+ });
52
+
53
+ it('should save dark theme to localStorage and emit event if dark passed', () => {
54
+ userProvider.set('dark');
55
+
56
+ expect(changeSpy).toHaveBeenCalledWith('dark');
57
+ expect(localStorage.getItem(storageKey)).toEqual('dark');
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,40 @@
1
+ import { EventEmitter } from '../lib/eventEmitter';
2
+ import { defaultTheme, themes } from '../lib/theme';
3
+ import type { Theme } from '../lib/theme';
4
+
5
+ class SystemProvider extends EventEmitter<{ change : [Theme] }> {
6
+
7
+ public get(): Theme {
8
+ if (!('matchMedia' in window)) {
9
+ return defaultTheme;
10
+ }
11
+
12
+ for (const theme of themes) {
13
+ const mediaQueryList = window.matchMedia(`(prefers-color-scheme: ${theme})`);
14
+
15
+ if (mediaQueryList.matches) {
16
+ return theme;
17
+ }
18
+ }
19
+
20
+ return defaultTheme;
21
+ }
22
+
23
+ public watch(): void {
24
+ if (!('matchMedia' in window)) {
25
+ return;
26
+ }
27
+
28
+ for (const theme of themes) {
29
+ const mediaQueryList = window.matchMedia(`(prefers-color-scheme: ${theme})`);
30
+
31
+ mediaQueryList.addEventListener('change', (ev) => {
32
+ if (ev.matches) {
33
+ this.emit('change', theme);
34
+ }
35
+ });
36
+ }
37
+ }
38
+ }
39
+
40
+ export { SystemProvider };
@@ -0,0 +1,24 @@
1
+ import { EventEmitter } from '../lib/eventEmitter';
2
+ import { isTheme } from '../lib/theme';
3
+ import type { Theme } from '../lib/theme';
4
+
5
+ class UserProvider extends EventEmitter<{ change : [Theme | undefined] }> {
6
+ private readonly storageKey = 'theme';
7
+
8
+ public get(): Theme | undefined {
9
+ const theme = localStorage.getItem(this.storageKey);
10
+ return isTheme(theme) ? theme : undefined;
11
+ }
12
+
13
+ public set(theme: Theme | undefined): void {
14
+ if (theme) {
15
+ localStorage.setItem(this.storageKey, theme);
16
+ } else {
17
+ localStorage.removeItem(this.storageKey);
18
+ }
19
+
20
+ this.emit('change', theme);
21
+ }
22
+ }
23
+
24
+ export { UserProvider };
@@ -0,0 +1,52 @@
1
+ .themeSwitcher {
2
+ cursor: pointer;
3
+ position: relative;
4
+ }
5
+
6
+ .themeSwitcher > svg:hover,
7
+ .themeSwitcher li:hover {
8
+ filter: brightness(1.5);
9
+ }
10
+
11
+ .themeSwitcher svg {
12
+ width: 2em;
13
+ height: 2em;
14
+ stroke: currentColor;
15
+ display: block;
16
+ }
17
+
18
+ .themeSwitcher ul {
19
+ list-style-type: none;
20
+ position: absolute;
21
+ left: 0;
22
+ margin: 0.5em 0;
23
+ padding: 0;
24
+ gap: 0;
25
+ overflow-y: visible;
26
+ z-index: 1;
27
+ }
28
+
29
+ .themeSwitcher li {
30
+ padding: 0.5em 1em;
31
+ }
32
+
33
+ .themeSwitcher[data-float="right"] ul {
34
+ left: auto;
35
+ right: 0;
36
+ }
37
+
38
+ .themeSwitcher li {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 0.5em;
42
+ }
43
+
44
+ .themeSwitcher li svg {
45
+ width: 1.5em;
46
+ height: 1.5em;
47
+ }
48
+
49
+ .themeSwitcher svg.checked {
50
+ width: 16px;
51
+ height: 13.5px;
52
+ }