diff options
| author | Haishan <[email protected]> | 2022-03-06 17:23:14 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2022-03-06 17:23:14 +0800 |
| commit | 8b2e4b6609deea56fdcafdf88172e577e5fadc38 (patch) | |
| tree | 9eae0c4359bbdced9afef25f67de799dcb50ac92 /src | |
| parent | 3e87d8b52e62c6f8da48176c81267bacd36e3965 (diff) | |
| parent | d87cc00fc83bb2bc5c1ffa69b4dad71d51e1cc66 (diff) | |
Merge pull request #675 from haishanh/automatic-color-scheme
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Root.scss | 14 | ||||
| -rw-r--r-- | src/components/shared/ThemeSwitcher.module.css | 28 | ||||
| -rw-r--r-- | src/components/shared/ThemeSwitcher.module.scss | 58 | ||||
| -rw-r--r-- | src/components/shared/ThemeSwitcher.tsx | 84 | ||||
| -rw-r--r-- | src/i18n/en.ts | 1 | ||||
| -rw-r--r-- | src/i18n/zh.ts | 1 | ||||
| -rw-r--r-- | src/store/app.ts | 18 |
7 files changed, 141 insertions, 63 deletions
diff --git a/src/components/Root.scss b/src/components/Root.scss index 0df5819..4ae7d5f 100644 --- a/src/components/Root.scss +++ b/src/components/Root.scss @@ -124,10 +124,16 @@ body { --select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20); } -// we don't have a "system" or "auto" mode now -// it's just not make sense to have these yet -// @media (prefers-color-scheme: dark) {} -// @media (prefers-color-scheme: light) {} +:root[data-theme='auto'] { + @media (prefers-color-scheme: dark) { + @include dark; + color-scheme: dark; + } + @media (prefers-color-scheme: light) { + @include light; + color-scheme: light; + } +} :root[data-theme='dark'] { @include dark; diff --git a/src/components/shared/ThemeSwitcher.module.css b/src/components/shared/ThemeSwitcher.module.css deleted file mode 100644 index 919c86c..0000000 --- a/src/components/shared/ThemeSwitcher.module.css +++ /dev/null @@ -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; -} diff --git a/src/components/shared/ThemeSwitcher.module.scss b/src/components/shared/ThemeSwitcher.module.scss new file mode 100644 index 0000000..c5de126 --- /dev/null +++ b/src/components/shared/ThemeSwitcher.module.scss @@ -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; + } +} diff --git a/src/components/shared/ThemeSwitcher.tsx b/src/components/shared/ThemeSwitcher.tsx index fba5b0b..45b60bc 100644 --- a/src/components/shared/ThemeSwitcher.tsx +++ b/src/components/shared/ThemeSwitcher.tsx @@ -1,5 +1,4 @@ import Tooltip from '@reach/tooltip'; -import cx from 'clsx'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { connect } from 'src/components/StateProvider'; @@ -7,28 +6,40 @@ import { framerMotionResouce } from 'src/misc/motion'; import { getTheme, switchTheme } from 'src/store/app'; import { State } from 'src/store/types'; -import s from './ThemeSwitcher.module.css'; +import s from './ThemeSwitcher.module.scss'; export function ThemeSwitcherImpl({ theme, dispatch }) { const { t } = useTranslation(); - const switchThemeHooked = React.useCallback(() => { - dispatch(switchTheme()); - }, [dispatch]); + const themeIcon = React.useMemo(() => { + switch (theme) { + case 'dark': + return <MoonA />; + case 'auto': + return <Auto />; + case 'light': + return <Sun />; + default: + console.assert(false, 'Unknown theme'); + return <MoonA />; + } + }, [theme]); + + const onChange = React.useCallback( + (e: React.ChangeEvent<HTMLSelectElement>) => dispatch(switchTheme(e.target.value)), + [dispatch] + ); return ( - <Tooltip - label={t('theme')} - aria-label={ - 'switch to ' + (theme === 'light' ? 'dark' : 'light') + ' theme' - } - > - <button - className={cx(s.iconWrapper, s.themeSwitchContainer)} - onClick={switchThemeHooked} - > - {theme === 'light' ? <MoonA /> : <Sun />} - </button> + <Tooltip label={t('switch_theme')} aria-label={'switch theme'}> + <div className={s.themeSwitchContainer}> + <span className={s.iconWrapper}>{themeIcon}</span> + <select onChange={onChange}> + <option value="auto">Auto</option> + <option value="dark">Dark</option> + <option value="light">Light</option> + </select> + </div> </Tooltip> ); } @@ -75,11 +86,7 @@ function Sun() { strokeLinejoin="round" > <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="21" x2="12" y2="23"></line> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> @@ -93,5 +100,38 @@ function Sun() { ); } +function Auto() { + const module = framerMotionResouce.read(); + const motion = module.motion; + + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <circle cx="12" cy="12" r="11" /> + <clipPath id="cut-off-bottom"> + <motion.rect + x="12" + y="0" + width="12" + height="24" + initial={{ rotate: -30 }} + animate={{ rotate: 0 }} + transition={{ duration: 0.7 }} + /> + </clipPath> + <circle cx="12" cy="12" r="6" clipPath="url(#cut-off-bottom)" fill="currentColor" /> + </svg> + ); +} + const mapState = (s: State) => ({ theme: getTheme(s) }); export const ThemeSwitcher = connect(mapState)(ThemeSwitcherImpl); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 1991284..fcdf253 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -27,6 +27,7 @@ export const data = { Connections: 'Connections', Active: 'Active', Closed: 'Closed', + switch_theme: 'Switch theme', theme: 'theme', about: 'about', no_logs: 'No logs yet, hang tight...', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index bd035ef..e92f9ff 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -27,6 +27,7 @@ export const data = { Connections: '连接', Active: '活动', Closed: '已断开', + switch_theme: '切换主题', theme: '主题', about: '关于', no_logs: '暂无日志...', diff --git a/src/store/app.ts b/src/store/app.ts index ea14e84..e5594a3 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -94,25 +94,25 @@ export function updateClashAPIConfig({ baseURL, secret }) { } const rootEl = document.querySelector('html'); -const themeColorMeta = document.querySelector('meta[name="theme-color"]'); -function setTheme(theme = 'dark') { - if (theme === 'dark') { +type ThemeType = 'dark' | 'light' | 'auto'; +function setTheme(theme: ThemeType = 'dark') { + if (theme === 'auto') { + rootEl.setAttribute('data-theme', 'auto'); + } else if (theme === 'dark') { rootEl.setAttribute('data-theme', 'dark'); - themeColorMeta.setAttribute('content', '#202020'); } else { rootEl.setAttribute('data-theme', 'light'); - themeColorMeta.setAttribute('content', '#f7f7f7'); } } -export function switchTheme() { +export function switchTheme(nextTheme = 'auto') { return (dispatch: DispatchFn, getState: GetStateFn) => { const currentTheme = getTheme(getState()); - const theme = currentTheme === 'light' ? 'dark' : 'light'; + if (currentTheme === nextTheme) return; // side effect - setTheme(theme); + setTheme(nextTheme as ThemeType); dispatch('storeSwitchTheme', (s) => { - s.app.theme = theme; + s.app.theme = nextTheme; }); // side effect saveState(getState().app); |
