Merge pull request #4 from toeverything/feat/layout

feat: add theme change handler
This commit is contained in:
Qi 2022-09-29 10:57:15 +08:00 committed by GitHub
commit 9245ecd627
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 374 additions and 23 deletions

View File

@ -12,6 +12,7 @@
"dependencies": {
"@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4",
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.4",
"lit": "^2.3.1",
"next": "12.3.1",

View File

@ -3,6 +3,7 @@ lockfileVersion: 5.4
specifiers:
'@emotion/css': ^11.10.0
'@emotion/react': ^11.10.4
'@emotion/server': ^11.10.0
'@emotion/styled': ^11.10.4
'@types/node': 18.7.18
'@types/react': 18.0.20
@ -21,6 +22,7 @@ specifiers:
dependencies:
'@emotion/css': 11.10.0
'@emotion/react': 11.10.4_w5j4k42lgipnm43s3brx6h3c34
'@emotion/server': 11.10.0_@emotion+css@11.10.0
'@emotion/styled': 11.10.4_yiaqs725o7pcd7rteavrnhgj4y
lit: 2.3.1
next: 12.3.1_biqbaboplfbrettd7655fr4n2y
@ -202,6 +204,21 @@ packages:
csstype: 3.1.1
dev: false
/@emotion/server/11.10.0_@emotion+css@11.10.0:
resolution: {integrity: sha512-MTvJ21JPo9aS02GdjFW4nhdwOi2tNNpMmAM/YED0pkxzjDNi5WbiTwXqaCnvLc2Lr8NFtjhT0az1vTJyLIHYcw==}
peerDependencies:
'@emotion/css': ^11.0.0-rc.0
peerDependenciesMeta:
'@emotion/css':
optional: true
dependencies:
'@emotion/css': 11.10.0
'@emotion/utils': 1.2.0
html-tokenize: 2.0.1
multipipe: 1.0.2
through: 2.3.8
dev: false
/@emotion/sheet/1.2.0:
resolution: {integrity: sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==}
dev: false
@ -675,6 +692,10 @@ packages:
fill-range: 7.0.1
dev: true
/buffer-from/0.1.2:
resolution: {integrity: sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==}
dev: false
/call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
@ -743,6 +764,10 @@ packages:
requiresBuild: true
dev: true
/core-util-is/1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: false
/cosmiconfig/7.0.1:
resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==}
engines: {node: '>=10'}
@ -837,6 +862,12 @@ packages:
esutils: 2.0.3
dev: true
/duplexer2/0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
dependencies:
readable-stream: 2.3.7
dev: false
/emoji-regex/9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
dev: true
@ -1417,6 +1448,17 @@ packages:
react-is: 16.13.1
dev: false
/html-tokenize/2.0.1:
resolution: {integrity: sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==}
hasBin: true
dependencies:
buffer-from: 0.1.2
inherits: 2.0.4
minimist: 1.2.6
readable-stream: 1.0.34
through2: 0.4.2
dev: false
/ignore/5.2.0:
resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
engines: {node: '>= 4'}
@ -1443,7 +1485,6 @@ packages:
/inherits/2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/internal-slot/1.0.3:
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
@ -1552,6 +1593,14 @@ packages:
call-bind: 1.0.2
dev: true
/isarray/0.0.1:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
dev: false
/isarray/1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: false
/isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
@ -1681,7 +1730,6 @@ packages:
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: true
/ms/2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -1695,6 +1743,13 @@ packages:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/multipipe/1.0.2:
resolution: {integrity: sha512-6uiC9OvY71vzSGX8lZvSqscE7ft9nPupJ8fMjrCNRAUy2LREUW42UL+V/NTrogr6rFgRydUrCX4ZitfpSNkSCQ==}
dependencies:
duplexer2: 0.1.4
object-assign: 4.1.1
dev: false
/nanoid/3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -1753,12 +1808,15 @@ packages:
/object-assign/4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: true
/object-inspect/1.12.2:
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
dev: true
/object-keys/0.4.0:
resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==}
dev: false
/object-keys/1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@ -1913,6 +1971,10 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
/process-nextick-args/2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: false
/prop-types/15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
@ -1950,6 +2012,27 @@ packages:
loose-envify: 1.4.0
dev: false
/readable-stream/1.0.34:
resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 0.0.1
string_decoder: 0.10.31
dev: false
/readable-stream/2.3.7:
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
dev: false
/regenerator-runtime/0.13.9:
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
@ -2093,6 +2176,16 @@ packages:
es-abstract: 1.20.2
dev: true
/string_decoder/0.10.31:
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
dev: false
/string_decoder/1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
dev: false
/strip-ansi/6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -2152,6 +2245,17 @@ packages:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
/through/2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: false
/through2/0.4.2:
resolution: {integrity: sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==}
dependencies:
readable-stream: 1.0.34
xtend: 2.1.2
dev: false
/to-fast-properties/2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@ -2232,6 +2336,10 @@ packages:
react: 18.2.0
dev: false
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
/v8-compile-cache/2.3.0:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
dev: true
@ -2263,6 +2371,13 @@ packages:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
/xtend/2.1.2:
resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==}
engines: {node: '>=0.4'}
dependencies:
object-keys: 0.4.0
dev: false
/yallist/4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true

View File

@ -144,9 +144,13 @@ button,
select,
keygen,
legend {
font: 18px/1.14 arial, \5b8b\4f53;
color: #333;
outline: 0;
font-size: 18px;
line-height: 1.5;
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Tahoma,
PingFang SC, Microsoft Yahei, Arial, Hiragino Sans GB, sans-serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
body {
background: #fff;

View File

@ -1,7 +1,6 @@
import { LitElement, css, html, unsafeCSS } from 'lit';
import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import * as React from 'react';
import { theme } from '@/styles';
export const tagName = 'simple-counter';
@ -28,7 +27,7 @@ export class Counter extends LitElement {
static styles = css`
.counter-container {
display: flex;
color: ${unsafeCSS(theme.colors.primary)};
color: var(--color-primary);
}
button {
margin: 0 5px;

View File

@ -1,12 +1,11 @@
import type { AppProps } from 'next/app';
import { ThemeProvider } from '@emotion/react';
import { theme } from '../styles';
import { ThemeProvider } from '@/styles';
import '../../public/globals.css';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme={theme}>
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);

53
src/pages/_document.tsx Normal file
View File

@ -0,0 +1,53 @@
import createEmotionServer from '@emotion/server/create-instance';
import { cache } from '@emotion/css';
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document';
import * as React from 'react';
export const renderStatic = async (html: string) => {
if (html === undefined) {
throw new Error('did you forget to return html from renderToString?');
}
const { extractCritical } = createEmotionServer(cache);
const { ids, css } = extractCritical(html);
return { html, ids, css };
};
export default class AppDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const page = await ctx.renderPage();
const { css, ids } = await renderStatic(page.html);
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<React.Fragment>
{initialProps.styles}
<style
data-emotion={`css ${ids.join(' ')}`}
dangerouslySetInnerHTML={{ __html: css }}
/>
</React.Fragment>
),
};
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

View File

@ -1,6 +1,5 @@
import type { NextPage } from 'next';
import styled from '@emotion/styled';
import { styled, useTheme } from '@/styles';
import '@/components/simple-counter';
const Button = styled('div')(({ theme }) => {
@ -10,10 +9,33 @@ const Button = styled('div')(({ theme }) => {
});
const Home: NextPage = () => {
const { changeMode, mode } = useTheme();
return (
<div>
<Button>A button use the theme styles</Button>
<simple-counter name="A counter created by web component" />
<p>current mode {mode}</p>
<button
onClick={() => {
changeMode('light');
}}
>
light
</button>
<button
onClick={() => {
changeMode('dark');
}}
>
dark
</button>
<button
onClick={() => {
changeMode('auto');
}}
>
auto
</button>
</div>
);
};

4
src/styles/hooks.ts Normal file
View File

@ -0,0 +1,4 @@
import { useContext } from 'react';
import { ThemeContext } from './themeProvider';
export const useTheme = () => useContext(ThemeContext);

View File

@ -1 +1,6 @@
export * from './theme';
export type { ThemeMode, ThemeProviderProps, AffineTheme } from './types';
export { styled } from './styled';
export { ThemeProvider } from './themeProvider';
export { lightTheme, darkTheme } from './theme';
export { useTheme } from './hooks';

3
src/styles/styled.ts Normal file
View File

@ -0,0 +1,3 @@
import emotionStyled from '@emotion/styled';
export const styled = emotionStyled;

View File

@ -1,17 +1,20 @@
import '@emotion/react';
import { AffineTheme } from './types';
interface AffineTheme {
colors: {
primary: string;
};
}
export const theme: AffineTheme = {
export const lightTheme: AffineTheme = {
colors: {
primary: '#0070f3',
},
};
declare module '@emotion/react' {
export interface Theme extends AffineTheme {}
}
export const darkTheme: AffineTheme = {
colors: {
primary: '#000',
},
};
export const globalThemeConstant = (theme: AffineTheme) => {
return {
'--color-primary': theme.colors.primary,
};
};

View File

@ -0,0 +1,77 @@
import {
ThemeProvider as EmotionThemeProvider,
Global,
css,
} from '@emotion/react';
import { createContext, useEffect, useState } from 'react';
import type { PropsWithChildren } from 'react';
import {
Theme,
ThemeMode,
ThemeProviderProps,
ThemeProviderValue,
} from './types';
import { lightTheme, darkTheme, globalThemeConstant } from './theme';
import { SystemThemeHelper, localStorageThemeHelper } from './utils';
export const ThemeContext = createContext<ThemeProviderValue>({
mode: 'light',
changeMode: () => {},
theme: lightTheme,
});
export const ThemeProvider = ({
defaultTheme = 'light',
children,
}: PropsWithChildren<ThemeProviderProps>) => {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [mode, setMode] = useState<ThemeMode>('auto');
const themeStyle = theme === 'light' ? lightTheme : darkTheme;
const changeMode = (themeMode: ThemeMode) => {
themeMode !== mode && setMode(themeMode);
// Remember the theme mode which user selected for next time
localStorageThemeHelper.set(themeMode);
};
useEffect(() => {
setMode(localStorageThemeHelper.get() || 'auto');
}, []);
useEffect(() => {
const systemThemeHelper = new SystemThemeHelper();
const selectedThemeMode = localStorageThemeHelper.get();
const themeMode = selectedThemeMode || mode;
if (themeMode === 'auto') {
setTheme(systemThemeHelper.get());
} else {
setTheme(themeMode);
}
// When system theme changed, change the theme mode
systemThemeHelper.onChange(() => {
// TODO: There may be should be provided a way to let user choose whether to
if (mode === 'auto') {
setTheme(systemThemeHelper.get());
}
});
return () => {
systemThemeHelper.dispose();
};
}, [mode]);
return (
<ThemeContext.Provider value={{ mode, changeMode, theme: themeStyle }}>
<Global
styles={css`
:root {
${globalThemeConstant(themeStyle)}
}
`}
/>
<EmotionThemeProvider theme={themeStyle}>{children}</EmotionThemeProvider>
</ThemeContext.Provider>
);
};

22
src/styles/types.ts Normal file
View File

@ -0,0 +1,22 @@
export type Theme = 'light' | 'dark';
export type ThemeMode = Theme | 'auto';
export type ThemeProviderProps = {
defaultTheme?: Theme;
};
export type ThemeProviderValue = {
theme: AffineTheme;
mode: ThemeMode;
changeMode: (newMode: ThemeMode) => void;
};
export interface AffineTheme {
colors: {
primary: string;
};
}
declare module '@emotion/react' {
export interface Theme extends AffineTheme {}
}

View File

@ -0,0 +1,2 @@
export * from './systemThemeHelper';
export * from './localStorageThemeHelper';

View File

@ -0,0 +1,13 @@
import { ThemeMode } from '../types';
export class LocalStorageThemeHelper {
name = 'Affine-theme-mode';
get = (): ThemeMode | null => {
return localStorage.getItem(this.name) as ThemeMode | null;
};
set = (mode: ThemeMode) => {
localStorage.setItem(this.name, mode);
};
}
export const localStorageThemeHelper = new LocalStorageThemeHelper();

View File

@ -0,0 +1,29 @@
import { Theme } from '../types';
export class SystemThemeHelper {
media: MediaQueryList = window.matchMedia('(prefers-color-scheme: light)');
eventList: Array<(e: Event) => void> = [];
eventHandler = (e: Event) => {
this.eventList.forEach(fn => fn(e));
};
constructor() {
this.media.addEventListener('change', this.eventHandler);
}
get = (): Theme => {
if (typeof window === 'undefined') {
return 'light';
}
return this.media.matches ? 'light' : 'dark';
};
onChange = (callback: () => void) => {
this.eventList.push(callback);
};
dispose = () => {
this.eventList = [];
this.media.removeEventListener('change', this.eventHandler);
};
}