mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
chore: change to monorepo
This commit is contained in:
18
packages/app/.eslintrc.js
Normal file
18
packages/app/.eslintrc.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// https://eslint.org/docs/latest/user-guide/configuring
|
||||
// "off" or 0 - turn the rule off
|
||||
// "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code)
|
||||
// "error" or 2 - turn the rule on as an error (exit code will be 1)
|
||||
|
||||
/** @type { import('eslint').Linter.Config } */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'next/core-web-vitals',
|
||||
'plugin:@next/next/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
|
||||
reportUnusedDisableDirectives: true,
|
||||
};
|
||||
34
packages/app/README.md
Normal file
34
packages/app/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
5
packages/app/next-env.d.ts
vendored
Normal file
5
packages/app/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
7
packages/app/next.config.js
Normal file
7
packages/app/next.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
swcMinify: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
38
packages/app/package.json
Normal file
38
packages/app/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@pathfinder/app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"export": "next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "^0.2.4",
|
||||
"@blocksuite/editor": "^0.2.4",
|
||||
"@blocksuite/store": "^0.2.4",
|
||||
"@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",
|
||||
"prettier": "^2.7.1",
|
||||
"quill": "^1.3.7",
|
||||
"quill-cursors": "^4.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.7.18",
|
||||
"@types/react": "18.0.20",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"typescript": "4.8.3"
|
||||
}
|
||||
}
|
||||
BIN
packages/app/public/favicon.ico
Normal file
BIN
packages/app/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
183
packages/app/public/globals.css
Normal file
183
packages/app/public/globals.css
Normal file
@@ -0,0 +1,183 @@
|
||||
* {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
hr,
|
||||
figure,
|
||||
table,
|
||||
caption,
|
||||
th,
|
||||
td,
|
||||
form,
|
||||
fieldset,
|
||||
legend,
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
header,
|
||||
footer,
|
||||
section,
|
||||
article,
|
||||
aside,
|
||||
nav,
|
||||
hgroup,
|
||||
address,
|
||||
figure,
|
||||
figcaption,
|
||||
menu,
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
caption,
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
fieldset,
|
||||
img,
|
||||
iframe,
|
||||
abbr {
|
||||
border: 0;
|
||||
}
|
||||
i,
|
||||
cite,
|
||||
em,
|
||||
var,
|
||||
address,
|
||||
dfn {
|
||||
font-style: normal;
|
||||
}
|
||||
[hidefocus],
|
||||
summary {
|
||||
outline: 0;
|
||||
}
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
small {
|
||||
font-size: 100%;
|
||||
}
|
||||
sup,
|
||||
sub {
|
||||
font-size: 83%;
|
||||
}
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: inherit;
|
||||
}
|
||||
q:before,
|
||||
q:after {
|
||||
content: none;
|
||||
}
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
}
|
||||
label,
|
||||
summary {
|
||||
cursor: default;
|
||||
}
|
||||
a,
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
em,
|
||||
strong,
|
||||
b {
|
||||
font-weight: bold;
|
||||
}
|
||||
del,
|
||||
ins,
|
||||
u,
|
||||
s,
|
||||
a,
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
body,
|
||||
textarea,
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
keygen,
|
||||
legend {
|
||||
color: var(--page-text-color);
|
||||
outline: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
font-family: var(--affine-font-family);
|
||||
}
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
a,
|
||||
a:hover {
|
||||
color: var(--page-text-color);
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none; /*解决ios上按钮的圆角问题*/
|
||||
border-radius: 0; /*解决ios上输入框圆角问题*/
|
||||
outline: medium; /*去掉鼠标点击的默认黄色边框*/
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0px 1000px white inset;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
22
packages/app/public/variable.css
Normal file
22
packages/app/public/variable.css
Normal file
@@ -0,0 +1,22 @@
|
||||
/*:root {*/
|
||||
/* --affine-primary-color: #3a4c5c;*/
|
||||
/* --affine-muted-color: #a6abb7;*/
|
||||
/* --affine-highlight-color: #6880ff;*/
|
||||
/* --affine-placeholder-color: #c7c7c7;*/
|
||||
/* --affine-selected-color: rgba(104, 128, 255, 0.1);*/
|
||||
|
||||
/* --affine-font-family: Avenir Next, 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;*/
|
||||
|
||||
/* --affine-font-family2: Roboto Mono, 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;*/
|
||||
/*}*/
|
||||
|
||||
/*:root {*/
|
||||
/* --page-background-color: #fff;*/
|
||||
/* --page-text-color: #3a4c5c;*/
|
||||
/*}*/
|
||||
188
packages/app/src/components/Header/icons.tsx
Normal file
188
packages/app/src/components/Header/icons.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { DOMAttributes, CSSProperties } from 'react';
|
||||
|
||||
type IconProps = {
|
||||
color?: string;
|
||||
style?: CSSProperties;
|
||||
} & DOMAttributes<SVGElement>;
|
||||
|
||||
export const LogoIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle };
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10.552 2 4 21h3.838l4.168-13.14L16.176 21H20L13.447 2h-2.895Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const EdgelessIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle };
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 17.4a5.4 5.4 0 1 0 0-10.8 5.4 5.4 0 0 0 0 10.8Zm7-5.4a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18.565 8a.8.8 0 0 1 .8-.8c.797 0 1.511.07 2.07.24.5.15 1.172.477 1.334 1.202v.004c.089.405-.026.776-.186 1.065a3.165 3.165 0 0 1-.652.782c-.52.471-1.265.947-2.15 1.407-1.783.927-4.28 1.869-7.077 2.62-2.796.752-5.409 1.184-7.381 1.266-.98.04-1.848-.003-2.516-.162-.333-.079-.662-.196-.937-.38-.282-.19-.547-.48-.639-.892v-.002c-.138-.63.202-1.173.518-1.532.343-.39.836-.768 1.413-1.129a.8.8 0 0 1 .848 1.357c-.515.322-.862.605-1.06.83a1.524 1.524 0 0 0-.078.096c.07.03.169.064.304.095.461.11 1.163.158 2.08.12 1.822-.075 4.314-.481 7.033-1.212 2.718-.73 5.1-1.635 6.753-2.494.832-.433 1.441-.835 1.814-1.173.127-.115.213-.21.268-.284a1.67 1.67 0 0 0-.153-.053c-.342-.104-.878-.171-1.606-.171a.8.8 0 0 1-.8-.8Zm2.692 1.097-.004-.004a.026.026 0 0 1 .004.004Zm-18.46 5 .001-.002v.002Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoonIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle };
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.549 3.314a.775.775 0 0 1-.136-.855.801.801 0 0 1 .746-.46c3.287.078 6.352 2.081 7.577 5.292 1.608 4.215-.569 8.911-4.862 10.49a8.407 8.407 0 0 1-9.044-2.138.775.775 0 0 1-.137-.855.802.802 0 0 1 .747-.46c.832.02 1.684-.11 2.51-.414 3.465-1.275 5.222-5.066 3.924-8.469a6.6 6.6 0 0 0-1.325-2.13Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PaperIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle };
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M17 9.8H7V8.2h10v1.6ZM12 12.8H7v-1.6h5v1.6Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path d="m14 19 7-7h-5a2 2 0 0 0-2 2v5Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5 6.6h14c.22 0 .4.18.4.4v6.6L21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h9l1.6-1.6H5a.4.4 0 0 1-.4-.4V7c0-.22.18-.4.4-.4Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const SunIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle };
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10.8 2.5a.8.8 0 0 0-1.6 0v.834a.8.8 0 1 0 1.6 0V2.5ZM5.15 4.018a.8.8 0 1 0-1.132 1.131l.678.679a.8.8 0 1 0 1.132-1.132l-.679-.678Zm10.832 1.131a.8.8 0 0 0-1.13-1.131l-.68.678a.8.8 0 1 0 1.132 1.132l.678-.679ZM10 5.867a4.133 4.133 0 1 0 0 8.267 4.133 4.133 0 0 0 0-8.267ZM2.5 9.2a.8.8 0 1 0 0 1.6h.834a.8.8 0 0 0 0-1.6H2.5Zm14.167 0a.8.8 0 1 0 0 1.6h.833a.8.8 0 0 0 0-1.6h-.833ZM5.827 15.31a.8.8 0 0 0-1.13-1.134l-.678.675a.8.8 0 0 0 1.129 1.134l.678-.675Zm9.476-1.134a.8.8 0 1 0-1.129 1.134l.679.675a.8.8 0 1 0 1.128-1.134l-.678-.675ZM10.8 16.667a.8.8 0 1 0-1.6 0v.833a.8.8 0 0 0 1.6 0v-.833Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoreIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle, transform: 'rotate(90deg)' };
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<circle cx="12" cy="5.5" r="1.5" />
|
||||
<circle cx="12" cy="12" r="1.5" />
|
||||
<circle cx="12" cy="18.5" r="1.5" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const ExportIcon = ({
|
||||
color,
|
||||
style: propsStyle = {},
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const style = { fill: color, ...propsStyle };
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
{...props}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 3.19995C12.2121 3.19995 12.4156 3.28424 12.5656 3.43427L16.5656 7.43427L15.4343 8.56564L12.8 5.93132V14H11.2V5.93132L8.56564 8.56564L7.43427 7.43427L11.4343 3.43427C11.5843 3.28424 11.7878 3.19995 12 3.19995ZM3.79995 12V16.7992C3.79995 17.3724 3.80057 17.7543 3.82454 18.0476C3.84775 18.3317 3.88879 18.4616 3.93074 18.544C4.04579 18.7698 4.22937 18.9533 4.45516 19.0684C4.5375 19.1103 4.66747 19.1514 4.9515 19.1746C5.24487 19.1985 5.6267 19.1992 6.19995 19.1992H17.8C18.3732 19.1992 18.755 19.1985 19.0484 19.1746C19.3324 19.1514 19.4624 19.1103 19.5447 19.0684C19.7705 18.9533 19.9541 18.7698 20.0692 18.544C20.1111 18.4616 20.1522 18.3317 20.1754 18.0476C20.1993 17.7543 20.2 17.3724 20.2 16.7992V12H21.8V16.8314C21.8 17.364 21.8 17.8116 21.77 18.1779C21.7388 18.5609 21.6708 18.9249 21.4948 19.2703C21.2263 19.7972 20.798 20.2255 20.2711 20.494C19.9256 20.67 19.5617 20.738 19.1787 20.7693C18.8124 20.7992 18.3648 20.7992 17.8322 20.7992H6.16775C5.63509 20.7992 5.18749 20.7992 4.82121 20.7693C4.43823 20.738 4.07426 20.67 3.72878 20.494C3.20193 20.2255 2.77358 19.7972 2.50513 19.2703C2.3291 18.9249 2.26115 18.5609 2.22986 18.1779C2.19993 17.8116 2.19994 17.364 2.19995 16.8313L2.19995 12H3.79995Z"
|
||||
fill="#9096A5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
134
packages/app/src/components/Header/index.tsx
Normal file
134
packages/app/src/components/Header/index.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
LogoIcon,
|
||||
PaperIcon,
|
||||
EdgelessIcon,
|
||||
SunIcon,
|
||||
MoonIcon,
|
||||
MoreIcon,
|
||||
ExportIcon,
|
||||
} from './icons';
|
||||
import {
|
||||
StyledHeader,
|
||||
StyledTitle,
|
||||
StyledTitleWrapper,
|
||||
StyledLogo,
|
||||
StyledModeSwitch,
|
||||
StyledHeaderRightSide,
|
||||
StyledMoreMenuItem,
|
||||
} from './styles';
|
||||
import { Popover } from '@/components/popover';
|
||||
import { useTheme } from '@/styles';
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
|
||||
const EditorModeSwitch = () => {
|
||||
const [mode, setMode] = useState<'page' | 'edgeless'>('page');
|
||||
|
||||
const handleModeSwitch = (mode: 'page' | 'edgeless') => {
|
||||
const event = new CustomEvent('affine.switch-mode', { detail: mode });
|
||||
window.dispatchEvent(event);
|
||||
|
||||
setMode(mode);
|
||||
};
|
||||
return (
|
||||
<StyledModeSwitch>
|
||||
<PaperIcon
|
||||
color={mode === 'page' ? '#6880FF' : '#a6abb7'}
|
||||
onClick={() => {
|
||||
handleModeSwitch('page');
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
></PaperIcon>
|
||||
<EdgelessIcon
|
||||
color={mode === 'edgeless' ? '#6880FF' : '#a6abb7'}
|
||||
onClick={() => {
|
||||
handleModeSwitch('edgeless');
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
></EdgelessIcon>
|
||||
</StyledModeSwitch>
|
||||
);
|
||||
};
|
||||
|
||||
const DarkModeSwitch = () => {
|
||||
const { changeMode, mode } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{mode === 'dark' ? (
|
||||
<SunIcon
|
||||
color="#9096A5"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
changeMode('light');
|
||||
}}
|
||||
></SunIcon>
|
||||
) : (
|
||||
<MoonIcon
|
||||
color="#9096A5"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
changeMode('dark');
|
||||
}}
|
||||
></MoonIcon>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PopoverContent = () => {
|
||||
const { editor } = useEditor();
|
||||
return (
|
||||
<div>
|
||||
<StyledMoreMenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportHtml();
|
||||
}}
|
||||
>
|
||||
<ExportIcon />
|
||||
Export to HTML
|
||||
</StyledMoreMenuItem>
|
||||
<StyledMoreMenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportMarkdown();
|
||||
}}
|
||||
>
|
||||
<ExportIcon />
|
||||
Export to markdown
|
||||
</StyledMoreMenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
const { editor } = useEditor();
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
setTitle(editor.model.title || '');
|
||||
editor.model.propsUpdated.on(() => {
|
||||
setTitle(editor.model.title);
|
||||
});
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<StyledHeader>
|
||||
<StyledLogo>
|
||||
<LogoIcon color={'#6880FF'} onClick={() => {}} />
|
||||
</StyledLogo>
|
||||
<StyledTitle>
|
||||
<EditorModeSwitch />
|
||||
<StyledTitleWrapper>{title}</StyledTitleWrapper>
|
||||
</StyledTitle>
|
||||
|
||||
<StyledHeaderRightSide>
|
||||
<DarkModeSwitch />
|
||||
<Popover popoverContent={<PopoverContent />}>
|
||||
<MoreIcon color="#9096A5" style={{ marginLeft: '20px' }} />
|
||||
</Popover>
|
||||
</StyledHeaderRightSide>
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
73
packages/app/src/components/Header/styles.ts
Normal file
73
packages/app/src/components/Header/styles.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledHeader = styled('div')({
|
||||
height: '60px',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
padding: '0 22px',
|
||||
});
|
||||
|
||||
export const StyledTitle = styled('div')({
|
||||
width: '720px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
margin: 'auto',
|
||||
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px',
|
||||
});
|
||||
|
||||
export const StyledTitleWrapper = styled('div')({
|
||||
maxWidth: '720px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const StyledLogo = styled('div')({});
|
||||
|
||||
export const StyledModeSwitch = styled('div')({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: '12px',
|
||||
});
|
||||
|
||||
export const StyledHeaderRightSide = styled('div')({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const StyledMoreMenuItem = styled('div')({
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
fontSize: '14px',
|
||||
color: '#4C6275',
|
||||
padding: '0 14px',
|
||||
svg: {
|
||||
fill: '#4C6275',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
marginRight: '14px',
|
||||
},
|
||||
':hover': {
|
||||
color: 'var(--affine-highlight-color)',
|
||||
background: '#F1F3FF',
|
||||
svg: {
|
||||
fill: 'var(--affine-highlight-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
31
packages/app/src/components/editor-provider.tsx
Normal file
31
packages/app/src/components/editor-provider.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
type EditorContextValue = {
|
||||
editor: EditorContainer | null;
|
||||
setEditor: (editor: EditorContainer) => void;
|
||||
};
|
||||
type EditorContextProps = PropsWithChildren<{}>;
|
||||
|
||||
export const EditorContext = createContext<EditorContextValue>({
|
||||
editor: null,
|
||||
setEditor: () => {},
|
||||
});
|
||||
|
||||
export const useEditor = () => useContext(EditorContext);
|
||||
|
||||
export const EditorProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<EditorContextProps>) => {
|
||||
const [editor, setEditor] = useState<EditorContainer | null>(null);
|
||||
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, setEditor }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorProvider;
|
||||
48
packages/app/src/components/editor.tsx
Normal file
48
packages/app/src/components/editor.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Suspense, useEffect, useRef } from 'react';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import '@blocksuite/blocks';
|
||||
import '@blocksuite/editor';
|
||||
import '@blocksuite/blocks/style';
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
|
||||
export const Editor = () => {
|
||||
const editorRef = useRef<EditorContainer>();
|
||||
const { setEditor } = useEditor();
|
||||
useEffect(() => {
|
||||
setEditor(editorRef.current!);
|
||||
const { store } = editorRef.current as EditorContainer;
|
||||
|
||||
const pageId = store.addBlock({
|
||||
flavour: 'page',
|
||||
title: 'Blocksuite live demo',
|
||||
});
|
||||
const groupId = store.addBlock({ flavour: 'group' }, pageId);
|
||||
|
||||
const text = new Text('Legend from here ...');
|
||||
store.addBlock({ flavour: 'paragraph', text }, groupId);
|
||||
|
||||
// store._history.clear();
|
||||
}, [setEditor]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Error!</div>}>
|
||||
<editor-container ref={editorRef} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'editor-container': EditorContainer;
|
||||
}
|
||||
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
// TODO fix type error
|
||||
'editor-container': any; // EditorContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Editor;
|
||||
57
packages/app/src/components/popover/index.tsx
Normal file
57
packages/app/src/components/popover/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useState } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
type PopoverProps = {
|
||||
popoverContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledPopoverContainer = styled('div')({
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const StyledPopoverWrapper = styled('div')({
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
paddingTop: '46px',
|
||||
});
|
||||
const StyledPopover = styled('div')<{ show: boolean }>(({ show }) => {
|
||||
return {
|
||||
width: '248px',
|
||||
background: '#fff',
|
||||
boxShadow:
|
||||
'0px 1px 10px -6px rgba(24, 39, 75, 0.5), 0px 3px 16px -6px rgba(24, 39, 75, 0.04)',
|
||||
borderRadius: '10px 0px 10px 10px',
|
||||
padding: '8px 4px',
|
||||
display: show ? 'block' : 'none',
|
||||
position: 'absolute',
|
||||
top: '46px',
|
||||
right: '0',
|
||||
};
|
||||
});
|
||||
export const Popover = ({
|
||||
children,
|
||||
popoverContent,
|
||||
}: PropsWithChildren<PopoverProps>) => {
|
||||
const [show, setShow] = useState(false);
|
||||
return (
|
||||
<StyledPopoverContainer
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setShow(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<StyledPopoverWrapper>
|
||||
<StyledPopover show={show}>{popoverContent}</StyledPopover>
|
||||
</StyledPopoverWrapper>
|
||||
</StyledPopoverContainer>
|
||||
);
|
||||
};
|
||||
58
packages/app/src/components/simple-counter/index.ts
Normal file
58
packages/app/src/components/simple-counter/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import * as React from 'react';
|
||||
|
||||
export const tagName = 'simple-counter';
|
||||
|
||||
// Adapt React in order to be able to use custom tags properly
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
[tagName]: PersonInfoProps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PersonInfoProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLElement>,
|
||||
HTMLElement
|
||||
> {
|
||||
name?: string;
|
||||
}
|
||||
// ===================== Adapt end ====================
|
||||
|
||||
@customElement(tagName)
|
||||
export class Counter extends LitElement {
|
||||
static styles = css`
|
||||
.counter-container {
|
||||
display: flex;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
@property()
|
||||
name?: string = '';
|
||||
|
||||
@state()
|
||||
count = 0;
|
||||
// Render the UI as a function of component state
|
||||
render() {
|
||||
return html`<div class="counter-container">
|
||||
<div class="name">${this.name}</div>
|
||||
<button @click=${this._subtract}>-</button>
|
||||
<div>${this.count}</div>
|
||||
<button @click="${this._increment}">+</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _increment(e: Event) {
|
||||
this.count++;
|
||||
}
|
||||
private _subtract(e: Event) {
|
||||
this.count--;
|
||||
}
|
||||
}
|
||||
22
packages/app/src/pages/_app.tsx
Normal file
22
packages/app/src/pages/_app.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { AppProps } from 'next/app';
|
||||
import dynamic from 'next/dynamic';
|
||||
import '../../public/globals.css';
|
||||
import '../../public/variable.css';
|
||||
import './temporary.css';
|
||||
import { EditorProvider } from '@/components/editor-provider';
|
||||
|
||||
const ThemeProvider = dynamic(() => import('@/styles/themeProvider'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<EditorProvider>
|
||||
<Component {...pageProps} />
|
||||
</EditorProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
53
packages/app/src/pages/_document.tsx
Normal file
53
packages/app/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>
|
||||
);
|
||||
}
|
||||
}
|
||||
7
packages/app/src/pages/affine.tsx
Normal file
7
packages/app/src/pages/affine.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const Affine = () => {
|
||||
return <div className="">Affine Page</div>;
|
||||
};
|
||||
|
||||
export default Affine;
|
||||
63
packages/app/src/pages/index.tsx
Normal file
63
packages/app/src/pages/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { styled, useTheme } from '@/styles';
|
||||
import { Header } from '@/components/Header';
|
||||
|
||||
import '@/components/simple-counter';
|
||||
|
||||
const StyledEditorContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
paddingTop: '78px',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledPage = styled('div')({
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'var(--page-background-color)',
|
||||
transition: 'background-color .5s',
|
||||
});
|
||||
|
||||
const DynamicEditor = dynamic(() => import('../components/editor'), {
|
||||
loading: () => <div>Loading...</div>,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<StyledPage>
|
||||
<Header />
|
||||
<StyledEditorContainer>
|
||||
<DynamicEditor />
|
||||
</StyledEditorContainer>
|
||||
{/*<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>*/}
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
44
packages/app/src/pages/temporary.css
Normal file
44
packages/app/src/pages/temporary.css
Normal file
@@ -0,0 +1,44 @@
|
||||
debug-menu {
|
||||
display: none !important;
|
||||
}
|
||||
.affine-editor-container {
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.affine-default-page-block-container {
|
||||
width: 720px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #4c6275 !important;
|
||||
}
|
||||
u::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.affine-paragraph-block-container.text {
|
||||
margin-top: 18px !important;
|
||||
}
|
||||
|
||||
.affine-default-page-block-title {
|
||||
width: 100%;
|
||||
}
|
||||
s {
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
.affine-edgeless-page-block-container {
|
||||
height: 100% !important;
|
||||
}
|
||||
.affine-block-children-container.edgeless {
|
||||
height: 100% !important;
|
||||
}
|
||||
.affine-list-rich-text-wrapper > div {
|
||||
box-sizing: content-box;
|
||||
color: var(--affine-highlight-color);
|
||||
min-width: unset !important;
|
||||
max-width: 26px;
|
||||
}
|
||||
4
packages/app/src/styles/hooks.ts
Normal file
4
packages/app/src/styles/hooks.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from './themeProvider';
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext);
|
||||
6
packages/app/src/styles/index.ts
Normal file
6
packages/app/src/styles/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
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
packages/app/src/styles/styled.ts
Normal file
3
packages/app/src/styles/styled.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import emotionStyled from '@emotion/styled';
|
||||
|
||||
export const styled = emotionStyled;
|
||||
55
packages/app/src/styles/theme.ts
Normal file
55
packages/app/src/styles/theme.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import '@emotion/react';
|
||||
import { AffineTheme, ThemeMode } from './types';
|
||||
|
||||
export const lightTheme: AffineTheme = {
|
||||
colors: {
|
||||
primary: '#0070f3',
|
||||
},
|
||||
};
|
||||
|
||||
export const darkTheme: AffineTheme = {
|
||||
colors: {
|
||||
primary: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
export const globalThemeConstant = (mode: ThemeMode, theme: AffineTheme) => {
|
||||
const isDark = mode === 'dark';
|
||||
return {
|
||||
'--color-primary': theme.colors.primary,
|
||||
'--page-background-color': isDark ? '#3d3c3f' : '#fff',
|
||||
'--page-text-color': isDark ? '#fff' : '#3a4c5c',
|
||||
|
||||
// editor style variables
|
||||
'--affine-primary-color': isDark ? '#fff' : '#3a4c5c',
|
||||
'--affine-muted-color': '#a6abb7',
|
||||
'--affine-highlight-color': '#6880ff',
|
||||
'--affine-placeholder-color': '#c7c7c7',
|
||||
'--affine-selected-color': 'rgba(104, 128, 255, 0.1)',
|
||||
|
||||
'--affine-font-family':
|
||||
'Avenir Next, 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',
|
||||
|
||||
'--affine-font-family2':
|
||||
'Roboto Mono, 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',
|
||||
};
|
||||
};
|
||||
|
||||
const editorStyleVariable = {
|
||||
'--affine-primary-color': '#3a4c5c',
|
||||
'--affine-muted-color': '#a6abb7',
|
||||
'--affine-highlight-color': '#6880ff',
|
||||
'--affine-placeholder-color': '#c7c7c7',
|
||||
'--affine-selected-color': 'rgba(104, 128, 255, 0.1)',
|
||||
|
||||
'--affine-font-family':
|
||||
'Avenir Next, 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',
|
||||
|
||||
'--affine-font-family2':
|
||||
'Roboto Mono, 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',
|
||||
};
|
||||
|
||||
const pageStyleVariable = {
|
||||
'--page-background-color': '#fff',
|
||||
'--page-text-color': '#3a4c5c',
|
||||
};
|
||||
79
packages/app/src/styles/themeProvider.tsx
Normal file
79
packages/app/src/styles/themeProvider.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
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(mode, themeStyle)}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<EmotionThemeProvider theme={themeStyle}>{children}</EmotionThemeProvider>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeProvider;
|
||||
22
packages/app/src/styles/types.ts
Normal file
22
packages/app/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
packages/app/src/styles/utils/index.ts
Normal file
2
packages/app/src/styles/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './systemThemeHelper';
|
||||
export * from './localStorageThemeHelper';
|
||||
13
packages/app/src/styles/utils/localStorageThemeHelper.ts
Normal file
13
packages/app/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
packages/app/src/styles/utils/systemThemeHelper.ts
Normal file
29
packages/app/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);
|
||||
};
|
||||
}
|
||||
25
packages/app/tsconfig.json
Normal file
25
packages/app/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user