未验证 提交 ec6e76e4 编写于 作者: P Peter Pan 提交者: GitHub

feat: add theme toggle (#830)

上级 e50c15b0
<svg height="14" viewBox="0 0 14 14" width="14" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g fill="#fff">
<circle cx="4" cy="8" r="1" />
<circle cx="5" cy="4" r="1" />
<circle cx="9" cy="4" r="1" />
</g>
<path d="m3.27235335 1.6761119c1.47031684-1.02952694 3.21873715-1.365625 4.85695999-1.07676211s3.16624816 1.20268672 4.19577516 2.67300356c.7388055 1.05512367 1.1206639 2.2534373 1.1704891 3.45153172.0436106 1.04865781-.1668543 2.09710997-.6145934 3.04909362-.494968-.31183307-1.05803-.49207378-1.6336058-.53065417-.7684739-.05151019-1.55912562.14907212-2.23926069.62530782-.68005955.47618286-1.1389421 1.15045256-1.35350247 1.89019066-.16074735.5542075-.18410258 1.1451771-.06062147 1.7172293-1.04751749.0948995-2.1046345-.0660591-3.07509828-.4658128-1.1086379-.4566704-2.10401594-1.2253229-2.84278359-2.2803925-1.02952694-1.47031682-1.365625-3.21873713-1.07676211-4.85695997s1.20268672-3.1662482 2.67300356-4.19577513z" stroke="#fff" />
</g>
</svg>
......@@ -31,6 +31,11 @@
"stop": "Stop",
"stop-realtime-refresh": "Stop realtime refresh",
"stopped": "Stopped",
"theme": {
"auto": "Auto",
"dark": "Dark",
"light": "Light"
},
"time-mode": {
"relative": "Relative",
"step": "Step",
......
......@@ -31,6 +31,11 @@
"stop": "停止",
"stop-realtime-refresh": "停止实时数据刷新",
"stopped": "已停止",
"theme": {
"auto": "自动",
"dark": "深色",
"light": "浅色"
},
"time-mode": {
"relative": "Relative",
"step": "Step",
......
......@@ -5,6 +5,7 @@ import {border, borderRadius, rem, size, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import Language from '~/components/Language';
import type {Route} from '~/routes';
import ThemeToggle from '~/components/ThemeToggle';
import Tippy from '@tippyjs/react';
import ee from '~/utils/event';
import {getApiToken} from '~/utils/fetch';
......@@ -264,6 +265,24 @@ const Navbar: FunctionComponent = () => {
})}
</div>
<div className="right">
<Tippy
placement="bottom-end"
animation="shift-away-subtle"
interactive
arrow={false}
offset={[18, 0]}
hideOnClick={false}
role="menu"
content={
<SubNav>
<ThemeToggle />
</SubNav>
}
>
<NavItem className="nav-item">
<Icon type="theme" />
</NavItem>
</Tippy>
<NavItem className="nav-item" onClick={changeLanguage}>
<Language />
</NavItem>
......
import React, {FunctionComponent} from 'react';
import {actions, selectors} from '~/store';
import {colors, themes} from '~/utils/theme';
import {rem, transitionProps} from '~/utils/style';
import {useDispatch, useSelector} from 'react-redux';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const Wrapper = styled.dl`
display: flex;
align-items: center;
background-color: var(--background-color);
${transitionProps('background-color')}
margin: ${rem(10)};
`;
const Item = styled.div<{color: string; border: string; active?: boolean}>`
margin: 0 ${rem(10)};
padding: ${rem(6)} ${rem(10)};
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
dd {
display: block;
width: ${rem(18)};
height: ${rem(18)};
margin: 0;
position: relative;
border-radius: 50%;
${props => props.color};
&::before {
content: ' ';
display: block;
position: absolute;
top: 0;
left: 0;
width: calc(100% - 2px);
height: calc(100% - 2px);
border: 1px solid ${props => props.border};
border-radius: 50%;
}
&::after {
content: ' ';
display: ${props => (props.active ? 'block' : 'none')};
position: absolute;
top: 50%;
left: 50%;
background-color: #fff;
width: ${rem(8)};
height: ${rem(8)};
transform: translate(-50%, -50%);
border-radius: 50%;
}
}
dt {
display: block;
margin-top: ${rem(12)};
}
`;
const items = [
{
color: `background-color: ${colors.primary.default}`,
border: 'rgba(255, 255, 255, 0.59)',
label: 'light'
},
{
color: `background-color: ${themes.dark.backgroundColor}`,
border: 'rgba(255, 255, 255, 0.3)',
label: 'dark'
},
{
color: 'background-image: linear-gradient(133deg, #e9e9e9 5%, #a3a3a3 97%);',
border: 'rgba(255, 255, 255, 0.3)',
label: 'auto'
}
] as const;
const ThemeToggle: FunctionComponent = () => {
const {t} = useTranslation('common');
const dispatch = useDispatch();
const selected = useSelector(selectors.theme.selected);
return (
<Wrapper>
{items.map(item => (
<Item
color={item.color}
border={item.border}
active={selected === item.label}
key={item.label}
onClick={() => dispatch(actions.theme.selectTheme(item.label))}
>
<dd></dd>
<dt>{t(`common:theme.${item.label}`)}</dt>
</Item>
))}
</Wrapper>
);
};
export default ThemeToggle;
......@@ -7,3 +7,10 @@ export function setTheme(theme: Theme) {
theme
};
}
export function selectTheme(theme: Theme | 'auto') {
return {
type: ActionTypes.SELECT_THEME,
theme
};
}
import {THEME, autoTheme} from '~/utils/theme';
import type {ThemeActionTypes, ThemeState} from './types';
import {ActionTypes} from './types';
import {theme} from '~/utils/theme';
import type {Theme} from '~/utils/theme';
const STORAGE_KEY = 'theme';
const theme = THEME || (window.localStorage.getItem(STORAGE_KEY) as Theme | undefined) || 'auto';
const initState: ThemeState = {
theme
theme: theme === 'auto' ? autoTheme : theme,
selected: theme
};
window.document.body.classList.remove('light', 'dark', 'auto');
window.document.body.classList.add(initState.selected);
function themeReducer(state = initState, action: ThemeActionTypes): ThemeState {
switch (action.type) {
case ActionTypes.SET_THEME:
......@@ -14,6 +23,15 @@ function themeReducer(state = initState, action: ThemeActionTypes): ThemeState {
...state,
theme: action.theme
};
case ActionTypes.SELECT_THEME:
window.localStorage.setItem(STORAGE_KEY, action.theme);
window.document.body.classList.remove('light', 'dark', 'auto');
window.document.body.classList.add(action.theme);
return {
...state,
theme: action.theme === 'auto' ? autoTheme : action.theme,
selected: action.theme
};
default:
return state;
}
......
import type {RootState} from '../index';
export const theme = (state: RootState) => state.theme.theme;
export const selected = (state: RootState) => state.theme.selected;
......@@ -3,11 +3,13 @@ import type {Theme} from '~/utils/theme';
export type {Theme} from '~/utils/theme';
export enum ActionTypes {
SET_THEME = 'SET_THEME'
SET_THEME = 'SET_THEME',
SELECT_THEME = 'SELECT_THEME'
}
export interface ThemeState {
theme: Theme;
selected: Theme | 'auto';
}
interface SetThemeAction {
......@@ -15,4 +17,9 @@ interface SetThemeAction {
theme: Theme;
}
export type ThemeActionTypes = SetThemeAction;
interface SelectThemeAction {
type: ActionTypes.SELECT_THEME;
theme: Theme | 'auto';
}
export type ThemeActionTypes = SetThemeAction | SelectThemeAction;
......@@ -9,7 +9,9 @@ export const THEME: Theme | undefined = import.meta.env.SNOWPACK_PUBLIC_THEME;
export const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
export const theme = THEME || (matchMedia.matches ? 'dark' : 'light');
export const autoTheme: Theme = matchMedia.matches ? 'dark' : 'light';
export const theme = THEME || autoTheme;
export const colors = {
primary: {
......@@ -134,18 +136,23 @@ function generateThemeVariables(theme: Record<string, string>) {
.join('\n');
}
const mediaQuery = css`
@media (prefers-color-scheme: dark) {
${generateThemeVariables(themes.dark)}
}
`;
export const variables = css`
:root {
${generateColorVariables(colors)}
${generateThemeVariables(themes[THEME || 'light'])}
${(!THEME && mediaQuery) || ''}
body.auto {
${generateThemeVariables(themes.light)}
@media (prefers-color-scheme: dark) {
${generateThemeVariables(themes.dark)}
}
}
body.light {
${generateThemeVariables(themes.light)}
}
body.dark {
${generateThemeVariables(themes.dark)}
}
}
`;
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册