Make theme switch a native select/option menu
This commit is contained in:
parent
06428daa53
commit
d87cc00fc8
8 changed files with 2278 additions and 901 deletions
91
package.json
91
package.json
|
@ -27,84 +27,85 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.3",
|
"@babel/runtime": "7.17.2",
|
||||||
"@fontsource/open-sans": "4.5.2",
|
"@fontsource/open-sans": "4.5.5",
|
||||||
"@fontsource/roboto-mono": "4.5.0",
|
"@fontsource/roboto-mono": "4.5.3",
|
||||||
"@hsjs/react-cache": "0.0.0-alpha.aa94237",
|
"@hsjs/react-cache": "0.0.0-alpha.aa94237",
|
||||||
"@reach/tooltip": "0.16.2",
|
"@reach/tooltip": "0.16.2",
|
||||||
|
"@reach/visually-hidden": "0.16.0",
|
||||||
"chart.js": "2.9.4",
|
"chart.js": "2.9.4",
|
||||||
"clsx": "^1.1.0",
|
"clsx": "^1.1.0",
|
||||||
"core-js": "3.19.1",
|
"core-js": "3.21.1",
|
||||||
"date-fns": "2.25.0",
|
"date-fns": "2.28.0",
|
||||||
"framer-motion": "5.3.0",
|
"framer-motion": "6.2.8",
|
||||||
"history": "5.1.0",
|
"history": "5.3.0",
|
||||||
"i18next": "21.4.2",
|
"i18next": "21.6.13",
|
||||||
"i18next-browser-languagedetector": "6.1.2",
|
"i18next-browser-languagedetector": "6.1.3",
|
||||||
"i18next-http-backend": "1.3.1",
|
"i18next-http-backend": "1.3.2",
|
||||||
"immer": "9.0.6",
|
"immer": "9.0.12",
|
||||||
"invariant": "^2.2.4",
|
"invariant": "^2.2.4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"modern-normalize": "1.1.0",
|
"modern-normalize": "1.1.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-feather": "^2.0.9",
|
"react-feather": "^2.0.9",
|
||||||
"react-i18next": "11.14.2",
|
"react-i18next": "11.15.5",
|
||||||
"react-icons": "4.3.1",
|
"react-icons": "4.3.1",
|
||||||
"react-modal": "3.14.4",
|
"react-modal": "3.14.4",
|
||||||
"react-query": "3.32.1",
|
"react-query": "3.34.16",
|
||||||
"react-router": "6.0.2",
|
"react-router": "6.2.2",
|
||||||
"react-router-dom": "6.0.2",
|
"react-router-dom": "6.2.2",
|
||||||
"react-switch": "^6.0.0",
|
"react-switch": "^6.0.0",
|
||||||
"react-table": "7.7.0",
|
"react-table": "7.7.0",
|
||||||
"react-tabs": "3.2.3",
|
"react-tabs": "4.0.1",
|
||||||
"react-tiny-fab": "4.0.4",
|
"react-tiny-fab": "4.0.4",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"recoil": "0.5.2",
|
"recoil": "0.6.1",
|
||||||
"regenerator-runtime": "0.13.9",
|
"regenerator-runtime": "0.13.9",
|
||||||
"reselect": "4.1.2",
|
"reselect": "4.1.5",
|
||||||
"tslib": "2.3.1",
|
"tslib": "2.3.1",
|
||||||
"workbox-core": "6.3.0",
|
"workbox-core": "6.5.1",
|
||||||
"workbox-expiration": "6.3.0",
|
"workbox-expiration": "6.5.1",
|
||||||
"workbox-precaching": "6.3.0",
|
"workbox-precaching": "6.5.1",
|
||||||
"workbox-routing": "6.3.0",
|
"workbox-routing": "6.5.1",
|
||||||
"workbox-strategies": "6.3.0"
|
"workbox-strategies": "6.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/invariant": "2.2.35",
|
"@types/invariant": "2.2.35",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.4.1",
|
||||||
"@types/lodash-es": "4.17.5",
|
"@types/lodash-es": "4.17.6",
|
||||||
"@types/react": "17.0.34",
|
"@types/react": "17.0.39",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.13",
|
||||||
"@types/react-modal": "3.13.1",
|
"@types/react-modal": "3.13.1",
|
||||||
"@types/react-tabs": "2.3.3",
|
"@types/react-tabs": "2.3.4",
|
||||||
"@types/react-window": "1.8.5",
|
"@types/react-window": "1.8.5",
|
||||||
"@typescript-eslint/eslint-plugin": "5.3.1",
|
"@typescript-eslint/eslint-plugin": "5.13.0",
|
||||||
"@typescript-eslint/parser": "5.3.1",
|
"@typescript-eslint/parser": "5.13.0",
|
||||||
"@vitejs/plugin-react-refresh": "1.3.6",
|
"@vitejs/plugin-react-refresh": "1.3.6",
|
||||||
"autoprefixer": "10.4.0",
|
"autoprefixer": "10.4.2",
|
||||||
"cssnano": "5.0.10",
|
"cssnano": "5.1.0",
|
||||||
"eslint": "8.2.0",
|
"eslint": "8.10.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-config-react-app": "^6.0.0",
|
"eslint-config-react-app": "7.0.0",
|
||||||
"eslint-plugin-flowtype": "8.0.3",
|
"eslint-plugin-flowtype": "8.0.3",
|
||||||
"eslint-plugin-import": "2.25.3",
|
"eslint-plugin-import": "2.25.4",
|
||||||
"eslint-plugin-jest": "25.2.4",
|
"eslint-plugin-jest": "26.1.1",
|
||||||
"eslint-plugin-jsx-a11y": "6.5.1",
|
"eslint-plugin-jsx-a11y": "6.5.1",
|
||||||
"eslint-plugin-react": "7.27.0",
|
"eslint-plugin-react": "7.29.3",
|
||||||
"eslint-plugin-react-hooks": "4.3.0",
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
"postcss": "8.3.11",
|
"postcss": "8.4.7",
|
||||||
"postcss-custom-media": "^8.0.0",
|
"postcss-custom-media": "^8.0.0",
|
||||||
"postcss-import": "14.0.2",
|
"postcss-import": "14.0.2",
|
||||||
"postcss-simple-vars": "^6.0.3",
|
"postcss-simple-vars": "^6.0.3",
|
||||||
"prettier": "2.4.1",
|
"prettier": "2.5.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "1.43.4",
|
"sass": "1.49.9",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.6.2",
|
||||||
"vite": "2.6.14",
|
"vite": "2.8.6",
|
||||||
"vite-plugin-pwa": "0.11.3"
|
"vite-plugin-pwa": "0.11.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
.iconWrapper {
|
|
||||||
--sz: 40px;
|
|
||||||
|
|
||||||
width: var(--sz);
|
|
||||||
height: var(--sz);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
padding: 5px;
|
|
||||||
color: var(--color-text);
|
|
||||||
border-radius: 100%;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
.iconWrapper:hover {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.iconWrapper:focus {
|
|
||||||
border-color: var(--color-focus-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeSwitchContainer {
|
|
||||||
appearance: none;
|
|
||||||
user-select: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
58
src/components/shared/ThemeSwitcher.module.scss
Normal file
58
src/components/shared/ThemeSwitcher.module.scss
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
.iconWrapper {
|
||||||
|
--sz: 40px;
|
||||||
|
|
||||||
|
width: var(--sz);
|
||||||
|
height: var(--sz);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
padding: 5px;
|
||||||
|
color: var(--color-text);
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.iconWrapper:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.iconWrapper:focus {
|
||||||
|
border-color: var(--color-focus-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeSwitchContainer {
|
||||||
|
--sz: 40px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--sz);
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
padding-left: var(--sz);
|
||||||
|
width: var(--sz);
|
||||||
|
height: var(--sz);
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: var(--color-bg-sidebar);
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--color-focus-blue);
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
// this has effect in Firefox
|
||||||
|
// Chrome and Safari use the native menu
|
||||||
|
background: var(--color-bg-sidebar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.iconWrapper {
|
||||||
|
pointer-events: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import Tooltip from '@reach/tooltip';
|
import Tooltip from '@reach/tooltip';
|
||||||
import cx from 'clsx';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'src/components/StateProvider';
|
import { connect } from 'src/components/StateProvider';
|
||||||
|
@ -7,36 +6,18 @@ import { framerMotionResouce } from 'src/misc/motion';
|
||||||
import { getTheme, switchTheme } from 'src/store/app';
|
import { getTheme, switchTheme } from 'src/store/app';
|
||||||
import { State } from 'src/store/types';
|
import { State } from 'src/store/types';
|
||||||
|
|
||||||
import s from './ThemeSwitcher.module.css';
|
import s from './ThemeSwitcher.module.scss';
|
||||||
|
|
||||||
export function ThemeSwitcherImpl({ theme, dispatch }) {
|
export function ThemeSwitcherImpl({ theme, dispatch }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const switchThemeHooked = React.useCallback(() => {
|
|
||||||
dispatch(switchTheme());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const nextThemeName = React.useMemo(() => {
|
|
||||||
switch (theme) {
|
|
||||||
case 'light':
|
|
||||||
return 'dark';
|
|
||||||
case 'dark':
|
|
||||||
return 'auto';
|
|
||||||
case 'auto':
|
|
||||||
return 'light';
|
|
||||||
default:
|
|
||||||
console.assert(false, 'Unknown theme');
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
const themeIcon = React.useMemo(() => {
|
const themeIcon = React.useMemo(() => {
|
||||||
switch (theme) {
|
switch (theme) {
|
||||||
case 'light':
|
|
||||||
return <MoonA />;
|
|
||||||
case 'dark':
|
case 'dark':
|
||||||
return <Auto />;
|
return <MoonA />;
|
||||||
case 'auto':
|
case 'auto':
|
||||||
|
return <Auto />;
|
||||||
|
case 'light':
|
||||||
return <Sun />;
|
return <Sun />;
|
||||||
default:
|
default:
|
||||||
console.assert(false, 'Unknown theme');
|
console.assert(false, 'Unknown theme');
|
||||||
|
@ -44,11 +25,21 @@ export function ThemeSwitcherImpl({ theme, dispatch }) {
|
||||||
}
|
}
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
|
const onChange = React.useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLSelectElement>) => dispatch(switchTheme(e.target.value)),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={t('theme')} aria-label={'switch to ' + nextThemeName + ' theme'}>
|
<Tooltip label={t('switch_theme')} aria-label={'switch theme'}>
|
||||||
<button className={cx(s.iconWrapper, s.themeSwitchContainer)} onClick={switchThemeHooked}>
|
<div className={s.themeSwitchContainer}>
|
||||||
{themeIcon}
|
<span className={s.iconWrapper}>{themeIcon}</span>
|
||||||
</button>
|
<select onChange={onChange}>
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +86,7 @@ function Sun() {
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
<circle cx="12" cy="12" r="5"></circle>
|
||||||
<motion.g initial={{ scale: 0.8 }} animate={{ scale: 1 }} transition={{ duration: 0.7 }}>
|
<motion.g initial={{ scale: 0.7 }} animate={{ scale: 1 }} transition={{ duration: 0.5 }}>
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||||
|
@ -137,7 +128,7 @@ function Auto() {
|
||||||
transition={{ duration: 0.7 }}
|
transition={{ duration: 0.7 }}
|
||||||
/>
|
/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<circle cx="12" cy="12" r="6" clip-path="url(#cut-off-bottom)" fill="currentColor" />
|
<circle cx="12" cy="12" r="6" clipPath="url(#cut-off-bottom)" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const data = {
|
||||||
Connections: 'Connections',
|
Connections: 'Connections',
|
||||||
Active: 'Active',
|
Active: 'Active',
|
||||||
Closed: 'Closed',
|
Closed: 'Closed',
|
||||||
|
switch_theme: 'Switch theme',
|
||||||
theme: 'theme',
|
theme: 'theme',
|
||||||
about: 'about',
|
about: 'about',
|
||||||
no_logs: 'No logs yet, hang tight...',
|
no_logs: 'No logs yet, hang tight...',
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const data = {
|
||||||
Connections: '连接',
|
Connections: '连接',
|
||||||
Active: '活动',
|
Active: '活动',
|
||||||
Closed: '已断开',
|
Closed: '已断开',
|
||||||
|
switch_theme: '切换主题',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
about: '关于',
|
about: '关于',
|
||||||
no_logs: '暂无日志...',
|
no_logs: '暂无日志...',
|
||||||
|
|
|
@ -105,24 +105,12 @@ function setTheme(theme: ThemeType = 'dark') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchTheme() {
|
export function switchTheme(nextTheme = 'auto') {
|
||||||
return (dispatch: DispatchFn, getState: GetStateFn) => {
|
return (dispatch: DispatchFn, getState: GetStateFn) => {
|
||||||
const currentTheme = getTheme(getState());
|
const currentTheme = getTheme(getState());
|
||||||
let nextTheme: ThemeType = 'auto';
|
if (currentTheme === nextTheme) return;
|
||||||
switch (currentTheme) {
|
|
||||||
case 'light':
|
|
||||||
nextTheme = 'dark';
|
|
||||||
break;
|
|
||||||
case 'dark':
|
|
||||||
nextTheme = 'auto';
|
|
||||||
break;
|
|
||||||
case 'auto':
|
|
||||||
nextTheme = 'light';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// side effect
|
// side effect
|
||||||
setTheme(nextTheme);
|
setTheme(nextTheme as ThemeType);
|
||||||
dispatch('storeSwitchTheme', (s) => {
|
dispatch('storeSwitchTheme', (s) => {
|
||||||
s.app.theme = nextTheme;
|
s.app.theme = nextTheme;
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue