mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 20:41:53 +03:00
Merge pull request #4 from toeverything/feat/layout
feat: add theme change handler
This commit is contained in:
commit
9245ecd627
@ -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",
|
||||
|
121
pnpm-lock.yaml
121
pnpm-lock.yaml
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
53
src/pages/_document.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
4
src/styles/hooks.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from './themeProvider';
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext);
|
@ -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
3
src/styles/styled.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import emotionStyled from '@emotion/styled';
|
||||
|
||||
export const styled = emotionStyled;
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
77
src/styles/themeProvider.tsx
Normal file
77
src/styles/themeProvider.tsx
Normal 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
22
src/styles/types.ts
Normal 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 {}
|
||||
}
|
2
src/styles/utils/index.ts
Normal file
2
src/styles/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './systemThemeHelper';
|
||||
export * from './localStorageThemeHelper';
|
13
src/styles/utils/localStorageThemeHelper.ts
Normal file
13
src/styles/utils/localStorageThemeHelper.ts
Normal 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();
|
29
src/styles/utils/systemThemeHelper.ts
Normal file
29
src/styles/utils/systemThemeHelper.ts
Normal 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);
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user