├── .eslintrc.js ├── .github └── workflows │ └── pre-release.yml ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── globals.css └── variable.css ├── screenshots └── pathfinder.png ├── scripts └── gitInfo.js ├── src ├── components │ ├── Header │ │ ├── icons.tsx │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── utils.tsx │ ├── contact-modal │ │ ├── affine-text-logo.png │ │ ├── bg.png │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── edgeless-toolbar │ │ ├── icons.tsx │ │ ├── index.tsx │ │ ├── reply.svg │ │ └── style.ts │ ├── editor-mode-switch │ │ ├── icons.tsx │ │ ├── index.tsx │ │ ├── style.ts │ │ └── type.ts │ ├── editor-provider.tsx │ ├── editor.tsx │ ├── example-markdown.ts │ ├── faq │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── global-modal-provider.tsx │ ├── loading │ │ ├── index.tsx │ │ └── styled.ts │ ├── mobile-modal │ │ ├── bg.png │ │ ├── index.tsx │ │ └── styles.ts │ ├── shortcuts-modal │ │ ├── config.ts │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── simple-counter │ │ └── index.ts │ └── theme-mode-switch │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── style.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── affine.tsx │ ├── index.tsx │ └── temporary.css ├── styles │ ├── helper.ts │ ├── hooks.ts │ ├── index.ts │ ├── styled.ts │ ├── theme.ts │ ├── themeProvider.tsx │ ├── types.ts │ └── utils │ │ ├── index.ts │ │ ├── localStorageThemeHelper.ts │ │ └── systemThemeHelper.ts ├── types.ts ├── ui │ ├── menu │ │ ├── index.ts │ │ ├── menu.tsx │ │ └── styles.ts │ ├── modal │ │ ├── index.tsx │ │ ├── modal.tsx │ │ └── style.ts │ ├── popper │ │ ├── PopoverArrow.tsx │ │ ├── Popper.tsx │ │ ├── index.ts │ │ └── interface.ts │ ├── shared │ │ └── Container.tsx │ └── tooltip │ │ ├── Tooltip.tsx │ │ └── index.tsx └── utils │ ├── __tests__ │ └── get-is-mobile.spec.ts │ ├── get-is-mobile.ts │ └── print-build-info.ts ├── tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── playstore.icns ├── src │ └── main.rs └── tauri.conf.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/latest/user-guide/configuring 2 | // "off" or 0 - turn the rule off 3 | // "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code) 4 | // "error" or 2 - turn the rule on as an error (exit code will be 1) 5 | 6 | /** @type { import('eslint').Linter.Config } */ 7 | module.exports = { 8 | extends: [ 9 | 'next/core-web-vitals', 10 | 'plugin:@next/next/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | rules: { 14 | 'prettier/prettier': 'warn', 15 | }, 16 | 17 | reportUnusedDisableDirectives: true, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: 'release' 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: App Vesion 7 | required: false 8 | default: 0.1.0 9 | 10 | jobs: 11 | create-release: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | release_id: ${{ steps.create-release.outputs.result }} 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 16 22 | - name: create release 23 | id: create-release 24 | uses: actions/github-script@v6 25 | with: 26 | script: | 27 | const { data } = await github.rest.repos.createRelease({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | tag_name: `affine-client-v${{ github.event.inputs.version }}`, 31 | name: `Affine Client v${{ github.event.inputs.version }}`, 32 | body: 'Take a look at the assets to download and install this app.', 33 | draft: true, 34 | prerelease: false 35 | }) 36 | 37 | return data.id 38 | build-tauri: 39 | needs: create-release 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | platform: [macos-latest, ubuntu-latest, windows-latest] 44 | 45 | runs-on: ${{ matrix.platform }} 46 | steps: 47 | - uses: actions/checkout@v2 48 | - name: setup node 49 | uses: actions/setup-node@v1 50 | with: 51 | node-version: 16 52 | - name: install Rust stable 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: stable 56 | - name: install dependencies (ubuntu only) 57 | if: matrix.platform == 'ubuntu-latest' 58 | run: | 59 | sudo apt-get update 60 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf 61 | - name: install node tools 62 | run: | 63 | npm install -g pnpm yarn 64 | - name: install app dependencies & build AFFiNE 65 | run: | 66 | pnpm i 67 | pnpm build 68 | - uses: tauri-apps/tauri-action@v0 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | with: 72 | releaseId: ${{ needs.create-release.outputs.release_id }} 73 | 74 | publish-release: 75 | runs-on: ubuntu-latest 76 | needs: [create-release, build-tauri] 77 | 78 | steps: 79 | - name: publish pre release 80 | id: publish-pre-release 81 | uses: actions/github-script@v6 82 | env: 83 | release_id: ${{ needs.create-release.outputs.release_id }} 84 | with: 85 | script: | 86 | github.rest.repos.updateRelease({ 87 | owner: context.repo.owner, 88 | repo: context.repo.repo, 89 | release_id: process.env.release_id, 90 | draft: false, 91 | prerelease: true 92 | }) 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .next 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | src-tauri/target 28 | out/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Affine Client 2 | 3 | > Update: AFFiNE official client is availble to be downloaded, the latest release link is https://github.com/toeverything/AFFiNE/releases 4 | 5 | affine-client is a client for [AFFINE](https://github.com/toeverything/AFFiNE) based on [Tauri](https://tauri.app/) 6 | 7 | ## Supported Platforms 8 | 9 | - Windows 10 | - Linux 11 | - MacOS 12 | 13 | ## Download 14 | 15 | `https://github.com/m1911star/affine-client/releases` 16 | 17 | ## Build 18 | 19 | ### System Requirements 20 | 21 | - [Rust stable & Cargo](https://www.rust-lang.org/) 22 | - [pnpm](https://pnpm.io/) 23 | 24 | ### How to build 25 | 26 | 1. install system requirements 27 | 2. git clone this repo (including submodule): git clone --recurse-submodules git@github.com:m1911star/affine-client.git 28 | 3. cd affine-client 29 | 4. `sh scripts/build.sh` 30 | 31 | Navigate to `affine-client/tauri/target/release/bundle/` for target file 32 | 33 | > It may fail when bundling, just retry 34 | 35 | ### Screenshot 36 | 37 | ![home](./screenshots/pathfinder.png) 38 | 39 | ## Limitations 40 | 41 | This client is only a wrapper without any native api intergration for now. 42 | 43 | ## TODO 44 | 45 | - [x] add build pipeline 46 | - [x] speed up build 47 | 48 | ## Q&A 49 | 50 | - if you come across an error `dlopen(): error loading libfuse.so.2 AppImages require FUSE to run.`, please install `fuse2` first 51 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { getGitVersion, getCommitHash } = require('./scripts/gitInfo'); 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | productionBrowserSourceMaps: true, 7 | reactStrictMode: true, 8 | swcMinify: false, 9 | publicRuntimeConfig: { 10 | NODE_ENV: process.env.NODE_ENV, 11 | PROJECT_NAME: process.env.npm_package_name, 12 | BUILD_DATE: new Date().toISOString(), 13 | CI: process.env.CI || null, 14 | VERSION: getGitVersion(), 15 | COMMIT_HASH: getCommitHash(), 16 | }, 17 | basePath: process.env.BASE_PATH, 18 | }; 19 | 20 | module.exports = nextConfig; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pathfinder/app", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev": "next dev -p 1420", 6 | "build": "next build", 7 | "export": "next export -o dist", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "tauri": "tauri", 11 | "build:client": "tauri build", 12 | "build:prod": "next build & next export -o dist" 13 | }, 14 | "dependencies": { 15 | "@tauri-apps/api": "^1.1.0", 16 | "@blocksuite/blocks": "0.3.0-alpha.4", 17 | "@blocksuite/editor": "0.3.0-alpha.4", 18 | "@blocksuite/store": "0.3.0-alpha.4", 19 | "@emotion/css": "^11.10.0", 20 | "@emotion/react": "^11.10.4", 21 | "@emotion/server": "^11.10.0", 22 | "@emotion/styled": "^11.10.4", 23 | "@fontsource/poppins": "^4.5.10", 24 | "@fontsource/space-mono": "^4.5.10", 25 | "@mui/base": "5.0.0-alpha.101", 26 | "@mui/icons-material": "^5.10.9", 27 | "@mui/material": "^5.8.6", 28 | "css-spring": "^4.1.0", 29 | "lit": "^2.3.1", 30 | "next": "13.0.1", 31 | "prettier": "^2.7.1", 32 | "quill": "^1.3.7", 33 | "quill-cursors": "^4.0.0", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0" 36 | }, 37 | "devDependencies": { 38 | "@tauri-apps/cli": "^1.1.0", 39 | "@types/node": "18.7.18", 40 | "@types/react": "18.0.20", 41 | "@types/react-dom": "18.0.6", 42 | "eslint": "8.22.0", 43 | "eslint-config-next": "12.3.1", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-plugin-prettier": "^4.2.1", 46 | "typescript": "4.8.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1911star/affine-client/37a541dc1f098e555f3e619c309c0365eb086415/public/favicon.ico -------------------------------------------------------------------------------- /public/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-overflow-scrolling: touch; 3 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 4 | box-sizing: border-box; 5 | /*transition: all 0.1s;*/ 6 | } 7 | html, 8 | body, 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6, 15 | div, 16 | dl, 17 | dt, 18 | dd, 19 | ul, 20 | ol, 21 | li, 22 | p, 23 | blockquote, 24 | pre, 25 | hr, 26 | figure, 27 | table, 28 | caption, 29 | th, 30 | td, 31 | form, 32 | fieldset, 33 | legend, 34 | input, 35 | button, 36 | textarea, 37 | menu { 38 | margin: 0; 39 | padding: 0; 40 | } 41 | header, 42 | footer, 43 | section, 44 | article, 45 | aside, 46 | nav, 47 | hgroup, 48 | address, 49 | figure, 50 | figcaption, 51 | menu, 52 | details { 53 | display: block; 54 | } 55 | table { 56 | border-collapse: collapse; 57 | border-spacing: 0; 58 | } 59 | caption, 60 | th { 61 | text-align: left; 62 | font-weight: normal; 63 | } 64 | html, 65 | body, 66 | fieldset, 67 | img, 68 | iframe, 69 | abbr { 70 | border: 0; 71 | } 72 | i, 73 | cite, 74 | em, 75 | var, 76 | address, 77 | dfn { 78 | font-style: normal; 79 | } 80 | [hidefocus], 81 | summary { 82 | outline: 0; 83 | } 84 | li { 85 | list-style: none; 86 | } 87 | h1, 88 | h2, 89 | h3, 90 | h4, 91 | h5, 92 | h6, 93 | small { 94 | font-size: 100%; 95 | } 96 | sup, 97 | sub { 98 | font-size: 83%; 99 | } 100 | pre, 101 | code, 102 | kbd, 103 | samp { 104 | font-family: inherit; 105 | } 106 | q:before, 107 | q:after { 108 | content: none; 109 | } 110 | textarea { 111 | overflow: auto; 112 | resize: none; 113 | } 114 | label, 115 | summary { 116 | cursor: default; 117 | } 118 | a, 119 | button { 120 | cursor: pointer; 121 | } 122 | h1, 123 | h2, 124 | h3, 125 | h4, 126 | h5, 127 | h6, 128 | strong, 129 | b { 130 | font-weight: bold; 131 | } 132 | del, 133 | ins, 134 | u, 135 | s, 136 | a, 137 | a:hover { 138 | text-decoration: none; 139 | } 140 | body, 141 | textarea, 142 | input, 143 | button, 144 | select, 145 | keygen, 146 | legend { 147 | color: var(--affine-text-color); 148 | outline: 0; 149 | font-size: 18px; 150 | line-height: 1.5; 151 | font-family: var(--affine-font-family); 152 | } 153 | body { 154 | background: #fff; 155 | } 156 | a, 157 | a:hover { 158 | color: var(--affine-link-color); 159 | } 160 | a:visited { 161 | color: var(--affine-link-visited-color); 162 | } 163 | 164 | input { 165 | border: none; 166 | -moz-appearance: none; 167 | -webkit-appearance: none; /*解决ios上按钮的圆角问题*/ 168 | border-radius: 0; /*解决ios上输入框圆角问题*/ 169 | outline: medium; /*去掉鼠标点击的默认黄色边框*/ 170 | background-color: transparent; 171 | } 172 | 173 | input:-webkit-autofill { 174 | -webkit-box-shadow: 0 0 0px 1000px white inset; 175 | } 176 | 177 | input[type='number'] { 178 | -moz-appearance: textfield; 179 | } 180 | 181 | input[type='number']::-webkit-inner-spin-button, 182 | input[type='number']::-webkit-outer-spin-button { 183 | -webkit-appearance: none; 184 | margin: 0; 185 | } 186 | 187 | * { 188 | scrollbar-width: none; /* Firefox */ 189 | -ms-overflow-style: none; /* IE 10+ */ 190 | } 191 | ::-webkit-scrollbar { 192 | display: none; /* Chrome Safari */ 193 | } 194 | -------------------------------------------------------------------------------- /public/variable.css: -------------------------------------------------------------------------------- 1 | /*:root {*/ 2 | /* --affine-primary-color: #3a4c5c;*/ 3 | /* --affine-muted-color: #a6abb7;*/ 4 | /* --affine-highlight-color: #6880ff;*/ 5 | /* --affine-placeholder-color: #c7c7c7;*/ 6 | /* --affine-selected-color: rgba(104, 128, 255, 0.1);*/ 7 | 8 | /* --affine-font-family: Avenir Next, apple-system, BlinkMacSystemFont,*/ 9 | /* Helvetica Neue, Tahoma, PingFang SC, Microsoft Yahei, Arial,*/ 10 | /* Hiragino Sans GB, sans-serif, Apple Color Emoji, Segoe UI Emoji,*/ 11 | /* Segoe UI Symbol, Noto Color Emoji;*/ 12 | 13 | /* --affine-font-family2: Roboto Mono, apple-system, BlinkMacSystemFont,*/ 14 | /* Helvetica Neue, Tahoma, PingFang SC, Microsoft Yahei, Arial,*/ 15 | /* Hiragino Sans GB, sans-serif, Apple Color Emoji, Segoe UI Emoji,*/ 16 | /* Segoe UI Symbol, Noto Color Emoji;*/ 17 | /*}*/ 18 | 19 | /*:root {*/ 20 | /* --page-background-color: #fff;*/ 21 | /* --page-text-color: #3a4c5c;*/ 22 | /*}*/ 23 | -------------------------------------------------------------------------------- /screenshots/pathfinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1911star/affine-client/37a541dc1f098e555f3e619c309c0365eb086415/screenshots/pathfinder.png -------------------------------------------------------------------------------- /scripts/gitInfo.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // import { execSync } from 'child_process' 4 | const { execSync } = require('child_process'); 5 | 6 | const hasGit = () => { 7 | try { 8 | execSync('git --version'); 9 | } catch { 10 | return false; 11 | } 12 | return true; 13 | }; 14 | 15 | const getTopLevel = () => execSync('git rev-parse --show-toplevel'); 16 | const isRepository = () => { 17 | try { 18 | getTopLevel(); 19 | } catch { 20 | return false; 21 | } 22 | return true; 23 | }; 24 | 25 | const getGitVersion = () => { 26 | if (!hasGit() || !isRepository()) { 27 | console.error( 28 | "You haven't installed git or it does not exist in your PATH." 29 | ); 30 | return null; 31 | } 32 | const VERSION = execSync('git describe --always --dirty') 33 | .toString() 34 | // remove empty line 35 | .replace(/[\s\r\n]+$/, ''); 36 | 37 | return VERSION; 38 | }; 39 | 40 | const getCommitHash = (rev = 'HEAD') => 41 | execSync(`git rev-parse --short ${rev}`).toString(); 42 | 43 | module.exports = { 44 | getGitVersion, 45 | getCommitHash, 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Header/icons.tsx: -------------------------------------------------------------------------------- 1 | import type { DOMAttributes, CSSProperties } from 'react'; 2 | type IconProps = { 3 | style?: CSSProperties; 4 | } & DOMAttributes; 5 | 6 | export const RightArrow = ({ style = {}, ...props }: IconProps) => { 7 | return ( 8 | 17 | 22 | 23 | ); 24 | }; 25 | export const Export2Markdown = ({ style = {}, ...props }: IconProps) => { 26 | return ( 27 | 36 | 41 | 42 | 43 | ); 44 | }; 45 | export const Export2HTML = ({ style = {}, ...props }: IconProps) => { 46 | return ( 47 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | ); 67 | }; 68 | export const LogoIcon = ({ style = {}, ...props }: IconProps) => { 69 | return ( 70 | 79 | 84 | 85 | ); 86 | }; 87 | 88 | export const MoreIcon = ({ style = {}, ...props }: IconProps) => { 89 | return ( 90 | 97 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | export const ExportIcon = ({ style = {}, ...props }: IconProps) => { 104 | return ( 105 | 112 | 117 | 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | LogoIcon, 4 | MoreIcon, 5 | ExportIcon, 6 | Export2Markdown, 7 | Export2HTML, 8 | RightArrow, 9 | } from './icons'; 10 | import { 11 | StyledHeader, 12 | StyledTitle, 13 | StyledTitleWrapper, 14 | StyledLogo, 15 | StyledHeaderRightSide, 16 | IconButton, 17 | StyledHeaderContainer, 18 | StyledBrowserWarning, 19 | StyledCloseButton, 20 | StyledMenuItemWrapper, 21 | } from './styles'; 22 | import { useEditor } from '@/components/editor-provider'; 23 | import EditorModeSwitch from '@/components/editor-mode-switch'; 24 | import { EdgelessIcon, PaperIcon } from '../editor-mode-switch/icons'; 25 | import ThemeModeSwitch from '@/components/theme-mode-switch'; 26 | import { useModal } from '@/components/global-modal-provider'; 27 | import CloseIcon from '@mui/icons-material/Close'; 28 | import { getWarningMessage, shouldShowWarning } from './utils'; 29 | import { Menu, MenuItem } from '@/ui/menu'; 30 | 31 | const PopoverContent = () => { 32 | const { editor, mode, setMode } = useEditor(); 33 | return ( 34 | <> 35 | { 37 | setMode(mode === 'page' ? 'edgeless' : 'page'); 38 | }} 39 | > 40 | 41 | {mode === 'page' ? : } 42 | Convert to {mode === 'page' ? 'Edgeless' : 'Page'} 43 | 44 | 45 | 49 | { 51 | editor && editor.contentParser.onExportHtml(); 52 | }} 53 | > 54 | 55 | 56 | Export to HTML 57 | 58 | 59 | { 61 | editor && editor.contentParser.onExportMarkdown(); 62 | }} 63 | > 64 | 65 | 66 | Export to Markdown 67 | 68 | 69 | 70 | } 71 | > 72 | 73 | 74 | 75 | Export 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | }; 83 | 84 | const BrowserWarning = ({ onClose }: { onClose: () => void }) => { 85 | return ( 86 | 87 | {getWarningMessage()} 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | 95 | export const Header = () => { 96 | const [title, setTitle] = useState(''); 97 | const [isHover, setIsHover] = useState(false); 98 | const [showWarning, setShowWarning] = useState(shouldShowWarning()); 99 | 100 | const { contactModalHandler } = useModal(); 101 | const { editor } = useEditor(); 102 | 103 | useEffect(() => { 104 | if (editor) { 105 | setTitle(editor.model.title || ''); 106 | editor.model.propsUpdated.on(() => { 107 | setTitle(editor.model.title); 108 | }); 109 | } 110 | }, [editor]); 111 | return ( 112 | 113 | { 115 | setShowWarning(false); 116 | }} 117 | /> 118 | 119 | { 122 | contactModalHandler(true); 123 | }} 124 | > 125 | 126 | 127 | {title ? ( 128 | { 131 | setIsHover(true); 132 | }} 133 | onMouseLeave={() => { 134 | setIsHover(false); 135 | }} 136 | > 137 | 143 | {title} 144 | 145 | ) : null} 146 | 147 | 148 | 149 | } placement="bottom-end"> 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | ); 158 | }; 159 | -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import { displayFlex, styled } from '@/styles'; 2 | import { MenuItem } from '@/ui/menu'; 3 | 4 | export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>( 5 | ({ hasWarning }) => { 6 | return { 7 | position: 'relative', 8 | height: hasWarning ? '96px' : '60px', 9 | }; 10 | } 11 | ); 12 | export const StyledHeader = styled.div<{ hasWarning: boolean }>( 13 | ({ hasWarning, theme }) => { 14 | return { 15 | height: '60px', 16 | width: '100vw', 17 | ...displayFlex('space-between', 'center'), 18 | background: 'var(--affine-page-background)', 19 | transition: 'background-color 0.5s', 20 | position: 'fixed', 21 | left: '0', 22 | top: hasWarning ? '36px' : '0', 23 | padding: '0 22px', 24 | zIndex: 99, 25 | }; 26 | } 27 | ); 28 | 29 | export const StyledTitle = styled('div')(({ theme }) => ({ 30 | width: '720px', 31 | height: '100%', 32 | position: 'absolute', 33 | left: 0, 34 | right: 0, 35 | top: 0, 36 | margin: 'auto', 37 | 38 | ...displayFlex('center', 'center'), 39 | fontSize: theme.font.base, 40 | })); 41 | 42 | export const StyledTitleWrapper = styled('div')({ 43 | maxWidth: '720px', 44 | overflow: 'hidden', 45 | textOverflow: 'ellipsis', 46 | whiteSpace: 'nowrap', 47 | position: 'relative', 48 | }); 49 | 50 | export const StyledLogo = styled('div')(({ theme }) => ({ 51 | color: theme.colors.primaryColor, 52 | width: '60px', 53 | height: '60px', 54 | cursor: 'pointer', 55 | marginLeft: '-22px', 56 | ...displayFlex('center', 'center'), 57 | })); 58 | 59 | export const StyledHeaderRightSide = styled('div')({ 60 | height: '100%', 61 | display: 'flex', 62 | alignItems: 'center', 63 | }); 64 | 65 | export const StyledMenuItemWrapper = styled.div(({ theme }) => { 66 | return { 67 | height: '32px', 68 | position: 'relative', 69 | cursor: 'pointer', 70 | ...displayFlex('flex-start', 'center'), 71 | svg: { 72 | width: '16px', 73 | height: '16px', 74 | marginRight: '14px', 75 | }, 76 | 'svg:nth-child(2)': { 77 | position: 'absolute', 78 | right: 0, 79 | top: 0, 80 | bottom: 0, 81 | margin: 'auto', 82 | }, 83 | }; 84 | }); 85 | 86 | export const IconButton = styled('div')(({ theme }) => { 87 | return { 88 | width: '32px', 89 | height: '32px', 90 | ...displayFlex('center', 'center'), 91 | color: theme.colors.iconColor, 92 | borderRadius: '5px', 93 | ':hover': { 94 | color: theme.colors.primaryColor, 95 | background: theme.colors.hoverBackground, 96 | }, 97 | }; 98 | }); 99 | 100 | export const StyledBrowserWarning = styled.div(({ theme }) => { 101 | return { 102 | backgroundColor: theme.colors.warningBackground, 103 | color: theme.colors.warningColor, 104 | height: '36px', 105 | width: '100vw', 106 | fontSize: theme.font.sm, 107 | position: 'fixed', 108 | left: '0', 109 | top: '0', 110 | ...displayFlex('center', 'center'), 111 | }; 112 | }); 113 | 114 | export const StyledCloseButton = styled.div(({ theme }) => { 115 | return { 116 | width: '36px', 117 | height: '36px', 118 | color: theme.colors.iconColor, 119 | cursor: 'pointer', 120 | ...displayFlex('center', 'center'), 121 | position: 'absolute', 122 | right: '15px', 123 | top: '0', 124 | 125 | svg: { 126 | width: '15px', 127 | height: '15px', 128 | position: 'relative', 129 | zIndex: 1, 130 | }, 131 | }; 132 | }); 133 | -------------------------------------------------------------------------------- /src/components/Header/utils.tsx: -------------------------------------------------------------------------------- 1 | import getIsMobile from '@/utils/get-is-mobile'; 2 | // Inspire by https://stackoverflow.com/a/4900484/8415727 3 | const getChromeVersion = () => { 4 | const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); 5 | return raw ? parseInt(raw[2], 10) : false; 6 | }; 7 | const getIsChrome = () => { 8 | return ( 9 | /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor) 10 | ); 11 | }; 12 | const minimumChromeVersion = 102; 13 | 14 | export const shouldShowWarning = () => { 15 | return ( 16 | !window.CLIENT_APP && 17 | !getIsMobile() && 18 | (!getIsChrome() || getChromeVersion() < minimumChromeVersion) 19 | ); 20 | }; 21 | 22 | export const getWarningMessage = () => { 23 | if (!getIsChrome()) { 24 | return ( 25 | 26 | We recommend the Chrome browser for optimal experience. 27 | 28 | ); 29 | } 30 | if (getChromeVersion() < minimumChromeVersion) { 31 | return ( 32 | 33 | Please upgrade to the latest version of Chrome for the best experience. 34 | 35 | ); 36 | } 37 | return ''; 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/contact-modal/affine-text-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1911star/affine-client/37a541dc1f098e555f3e619c309c0365eb086415/src/components/contact-modal/affine-text-logo.png -------------------------------------------------------------------------------- /src/components/contact-modal/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1911star/affine-client/37a541dc1f098e555f3e619c309c0365eb086415/src/components/contact-modal/bg.png -------------------------------------------------------------------------------- /src/components/contact-modal/icons.tsx: -------------------------------------------------------------------------------- 1 | export const LogoIcon = () => { 2 | return ( 3 | 10 | 15 | 16 | ); 17 | }; 18 | export const DocIcon = () => { 19 | return ( 20 | 27 | 32 | 33 | ); 34 | }; 35 | 36 | export const TwitterIcon = () => { 37 | return ( 38 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export const GithubIcon = () => { 51 | return ( 52 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | export const DiscordIcon = (props: any) => { 75 | return ( 76 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | 95 | export const TelegramIcon = () => { 96 | return ( 97 | 104 | 105 | 106 | ); 107 | }; 108 | 109 | export const RedditIcon = () => { 110 | return ( 111 | 118 | 119 | 120 | ); 121 | }; 122 | 123 | export const LinkIcon = () => { 124 | return ( 125 | 132 | 137 | 138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /src/components/contact-modal/index.tsx: -------------------------------------------------------------------------------- 1 | import Modal from '@/ui/modal'; 2 | import CloseIcon from '@mui/icons-material/Close'; 3 | import { 4 | LogoIcon, 5 | DocIcon, 6 | TwitterIcon, 7 | GithubIcon, 8 | DiscordIcon, 9 | TelegramIcon, 10 | RedditIcon, 11 | LinkIcon, 12 | } from './icons'; 13 | import logo from './affine-text-logo.png'; 14 | import { 15 | StyledModalWrapper, 16 | StyledBigLink, 17 | StyledSmallLink, 18 | StyledSubTitle, 19 | StyledLeftContainer, 20 | StyledRightContainer, 21 | StyledContent, 22 | StyledLogo, 23 | StyledModalHeader, 24 | StyledModalHeaderLeft, 25 | StyledCloseButton, 26 | StyledModalFooter, 27 | } from './style'; 28 | 29 | const linkList = [ 30 | { 31 | icon: , 32 | title: 'GitHub', 33 | link: 'https://github.com/toeverything/AFFiNE', 34 | }, 35 | { 36 | icon: , 37 | title: 'Reddit', 38 | link: 'https://www.reddit.com/r/Affine/', 39 | }, 40 | { 41 | icon: , 42 | title: 'Twitter', 43 | link: 'https://twitter.com/AffineOfficial', 44 | }, 45 | { 46 | icon: , 47 | title: 'Telegram', 48 | link: 'https://t.me/affineworkos', 49 | }, 50 | { 51 | icon: , 52 | title: 'Discord', 53 | link: 'https://discord.gg/Arn7TqJBvG', 54 | }, 55 | ]; 56 | const rightLinkList = [ 57 | { 58 | icon: , 59 | title: 'Official Website ', 60 | subTitle: 'AFFiNE.pro', 61 | link: 'https://affine.pro', 62 | }, 63 | { 64 | icon: , 65 | title: 'Check Our Docs', 66 | subTitle: 'docs.AFFiNE.pro', 67 | link: 'https://docs.affine.pro', 68 | }, 69 | ]; 70 | 71 | type TransitionsModalProps = { 72 | open: boolean; 73 | onClose: () => void; 74 | }; 75 | 76 | export const ContactModal = ({ open, onClose }: TransitionsModalProps) => { 77 | return ( 78 | 79 | 80 | 81 | 82 | 83 | Alpha 84 | 85 | { 87 | onClose(); 88 | }} 89 | > 90 | 91 | 92 | 93 | 94 | 95 | 96 | {rightLinkList.map(({ icon, title, subTitle, link }) => { 97 | return ( 98 | 99 | {icon} 100 |

{title}

101 |

102 | {subTitle} 103 | 104 |

105 |
106 | ); 107 | })} 108 |
109 | 110 | 111 | Get in touch!
112 | Join our community. 113 |
114 | {linkList.map(({ icon, title, link }) => { 115 | return ( 116 | 117 | {icon} 118 | {title} 119 | 120 | ); 121 | })} 122 |
123 |
124 | 125 | 126 |

127 | 132 | How is AFFiNE Alpha different? 133 | 134 |

135 |

Copyright © 2022 Toeverything

136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default ContactModal; 143 | -------------------------------------------------------------------------------- /src/components/contact-modal/style.ts: -------------------------------------------------------------------------------- 1 | import { absoluteCenter, displayFlex, styled } from '@/styles'; 2 | import bg from './bg.png'; 3 | 4 | export const StyledModalWrapper = styled('div')(({ theme }) => { 5 | return { 6 | width: '860px', 7 | height: '540px', 8 | backgroundColor: theme.colors.popoverBackground, 9 | backgroundImage: `url(${bg.src})`, 10 | borderRadius: '20px', 11 | position: 'absolute', 12 | left: 0, 13 | right: 0, 14 | top: 0, 15 | bottom: 0, 16 | margin: 'auto', 17 | }; 18 | }); 19 | 20 | export const StyledBigLink = styled('a')(({ theme }) => { 21 | return { 22 | width: '320px', 23 | height: '100px', 24 | marginBottom: '48px', 25 | paddingLeft: '114px', 26 | fontSize: '24px', 27 | lineHeight: '36px', 28 | fontWeight: '600', 29 | color: theme.colors.textColor, 30 | borderRadius: '10px', 31 | flexDirection: 'column', 32 | ...displayFlex('center'), 33 | position: 'relative', 34 | transition: 'background .15s', 35 | ':visited': { 36 | color: theme.colors.textColor, 37 | }, 38 | ':hover': { 39 | background: 'rgba(68, 97, 242, 0.1)', 40 | }, 41 | ':last-of-type': { 42 | marginBottom: 0, 43 | }, 44 | svg: { 45 | width: '50px', 46 | height: '50px', 47 | marginRight: '40px', 48 | color: theme.colors.primaryColor, 49 | ...absoluteCenter({ vertical: true, position: { left: '32px' } }), 50 | }, 51 | p: { 52 | width: '100%', 53 | height: '30px', 54 | lineHeight: '30px', 55 | ...displayFlex('flex-start', 'center'), 56 | ':not(:last-of-type)': { 57 | marginBottom: '4px', 58 | }, 59 | ':first-of-type': { 60 | fontSize: '22px', 61 | }, 62 | ':last-of-type': { 63 | fontSize: '20px', 64 | color: theme.colors.primaryColor, 65 | }, 66 | svg: { 67 | width: '15px', 68 | height: '15px', 69 | position: 'static', 70 | transform: 'translate(0,0)', 71 | marginLeft: '5px', 72 | }, 73 | }, 74 | }; 75 | }); 76 | export const StyledSmallLink = styled('a')(({ theme }) => { 77 | return { 78 | width: '214px', 79 | height: '37px', 80 | display: 'flex', 81 | alignItems: 'center', 82 | fontSize: '18px', 83 | fontWeight: '500', 84 | paddingLeft: '24px', 85 | borderRadius: '5px', 86 | color: theme.colors.textColor, 87 | transition: 'background .15s, color .15s', 88 | 89 | ':visited': { 90 | color: theme.colors.textColor, 91 | }, 92 | ':hover': { 93 | color: theme.colors.primaryColor, 94 | background: theme.colors.hoverBackground, 95 | }, 96 | svg: { 97 | width: '22px', 98 | marginRight: '30px', 99 | color: theme.colors.primaryColor, 100 | }, 101 | }; 102 | }); 103 | export const StyledSubTitle = styled('div')(({ theme }) => { 104 | return { 105 | fontSize: '18px', 106 | fontWeight: '600', 107 | color: theme.colors.textColor, 108 | marginBottom: '24px', 109 | }; 110 | }); 111 | 112 | export const StyledLeftContainer = styled('div')({ 113 | width: '320px', 114 | flexDirection: 'column', 115 | ...displayFlex('space-between', 'center'), 116 | }); 117 | export const StyledRightContainer = styled('div')({ 118 | width: '214px', 119 | flexShrink: '0', 120 | flexDirection: 'column', 121 | ...displayFlex('center', 'flex-end'), 122 | }); 123 | 124 | export const StyledContent = styled('div')({ 125 | height: '276px', 126 | width: '100%', 127 | padding: '0 140px', 128 | ...displayFlex('space-between', 'center'), 129 | color: '#3A4C5C', 130 | marginTop: '58px', 131 | letterSpacing: '0.06em', 132 | }); 133 | 134 | export const StyledLogo = styled('img')({ 135 | height: '18px', 136 | width: 'auto', 137 | }); 138 | 139 | export const StyledModalHeader = styled('div')(({ theme }) => { 140 | return { 141 | height: '20px', 142 | marginTop: '36px', 143 | padding: '0 48px', 144 | ...displayFlex('space-between', 'center'), 145 | }; 146 | }); 147 | 148 | export const StyledModalHeaderLeft = styled('div')(({ theme }) => { 149 | return { 150 | color: theme.colors.primaryColor, 151 | ...displayFlex('flex-end', 'flex-end'), 152 | span: { 153 | height: '20px', 154 | border: `1px solid ${theme.colors.primaryColor}`, 155 | borderRadius: '10px', 156 | padding: '0 8px', 157 | lineHeight: '26px', 158 | fontSize: '14px', 159 | marginLeft: '12px', 160 | ...displayFlex('center', 'center'), 161 | }, 162 | }; 163 | }); 164 | 165 | export const StyledCloseButton = styled('div')(({ theme }) => { 166 | return { 167 | width: '60px', 168 | height: '60px', 169 | color: theme.colors.iconColor, 170 | cursor: 'pointer', 171 | ...displayFlex('center', 'center'), 172 | position: 'absolute', 173 | right: '0', 174 | top: '0', 175 | 176 | // TODO: we need to add @emotion/babel-plugin 177 | '::after': { 178 | content: '""', 179 | width: '30px', 180 | height: '30px', 181 | borderRadius: '6px', 182 | ...absoluteCenter({ horizontal: true, vertical: true }), 183 | }, 184 | ':hover': { 185 | color: theme.colors.primaryColor, 186 | '::after': { 187 | background: theme.colors.hoverBackground, 188 | }, 189 | }, 190 | svg: { 191 | width: '20px', 192 | height: '20px', 193 | position: 'relative', 194 | zIndex: 1, 195 | }, 196 | }; 197 | }); 198 | 199 | export const StyledModalFooter = styled('div')(({ theme }) => { 200 | return { 201 | fontSize: '14px', 202 | lineHeight: '20px', 203 | textAlign: 'center', 204 | color: theme.colors.textColor, 205 | 206 | marginTop: '40px', 207 | 'p:first-of-type': { 208 | color: theme.colors.primaryColor, 209 | letterSpacing: '0.06em', 210 | marginBottom: '25px', 211 | a: { 212 | ':visited': { 213 | color: theme.colors.linkColor, 214 | }, 215 | }, 216 | }, 217 | }; 218 | }); 219 | -------------------------------------------------------------------------------- /src/components/edgeless-toolbar/icons.tsx: -------------------------------------------------------------------------------- 1 | export const SelectIcon = () => { 2 | return ( 3 | 10 | 11 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export const TextIcon = () => { 32 | return ( 33 | 40 | 45 | 46 | ); 47 | }; 48 | 49 | export const ShapeIcon = () => { 50 | return ( 51 | 58 | 63 | 64 | ); 65 | }; 66 | 67 | export const PenIcon = () => { 68 | return ( 69 | 76 | 81 | 82 | ); 83 | }; 84 | 85 | export const StickerIcon = () => { 86 | return ( 87 | 94 | 95 | 96 | ); 97 | }; 98 | 99 | export const ConnectorIcon = () => { 100 | return ( 101 | 108 | 113 | 114 | ); 115 | }; 116 | 117 | export const UndoIcon = () => { 118 | return ( 119 | 126 | 131 | 132 | ); 133 | }; 134 | 135 | export const RedoIcon = () => { 136 | return ( 137 | 144 | 149 | 150 | ); 151 | }; 152 | -------------------------------------------------------------------------------- /src/components/edgeless-toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { 3 | StyledEdgelessToolbar, 4 | StyledToolbarWrapper, 5 | StyledToolbarItem, 6 | } from './style'; 7 | import { 8 | SelectIcon, 9 | TextIcon, 10 | ShapeIcon, 11 | PenIcon, 12 | StickerIcon, 13 | ConnectorIcon, 14 | UndoIcon, 15 | RedoIcon, 16 | } from './icons'; 17 | import { Tooltip } from '@/ui/tooltip'; 18 | import Slide from '@mui/material/Slide'; 19 | import { useEditor } from '@/components/editor-provider'; 20 | 21 | const toolbarList1 = [ 22 | { 23 | flavor: 'select', 24 | icon: , 25 | toolTip: 'Select', 26 | disable: false, 27 | }, 28 | { 29 | flavor: 'text', 30 | icon: , 31 | toolTip: 'Text (coming soon)', 32 | disable: true, 33 | }, 34 | { 35 | flavor: 'shape', 36 | icon: , 37 | toolTip: 'Shape (coming soon)', 38 | disable: true, 39 | }, 40 | { 41 | flavor: 'sticky', 42 | icon: , 43 | toolTip: 'Sticky (coming soon)', 44 | disable: true, 45 | }, 46 | { 47 | flavor: 'pen', 48 | icon: , 49 | toolTip: 'Pen (coming soon)', 50 | disable: true, 51 | }, 52 | 53 | { 54 | flavor: 'connector', 55 | icon: , 56 | toolTip: 'Connector (coming soon)', 57 | disable: true, 58 | }, 59 | ]; 60 | const toolbarList2 = [ 61 | { 62 | flavor: 'undo', 63 | icon: , 64 | toolTip: 'Undo', 65 | disable: false, 66 | }, 67 | { 68 | flavor: 'redo', 69 | icon: , 70 | toolTip: 'Redo', 71 | disable: false, 72 | }, 73 | ]; 74 | 75 | const UndoRedo = () => { 76 | const [canUndo, setCanUndo] = useState(false); 77 | const [canRedo, setCanRedo] = useState(false); 78 | const { editor } = useEditor(); 79 | useEffect(() => { 80 | if (!editor) return; 81 | const { page } = editor; 82 | 83 | page.signals.historyUpdated.on(() => { 84 | setCanUndo(page.canUndo); 85 | setCanRedo(page.canRedo); 86 | }); 87 | }, [editor]); 88 | 89 | return ( 90 | 91 | 92 | { 95 | editor?.page?.undo(); 96 | }} 97 | > 98 | 99 | 100 | 101 | 102 | { 105 | editor?.page?.redo(); 106 | }} 107 | > 108 | 109 | 110 | 111 | 112 | ); 113 | }; 114 | 115 | export const EdgelessToolbar = () => { 116 | const { mode } = useEditor(); 117 | 118 | return ( 119 | 125 | 126 | 127 | {toolbarList1.map(({ icon, toolTip, flavor, disable }, index) => { 128 | return ( 129 | 130 | { 133 | console.log('flavor', flavor); 134 | }} 135 | > 136 | {icon} 137 | 138 | 139 | ); 140 | })} 141 | 142 | 143 | 144 | 145 | ); 146 | }; 147 | 148 | export default EdgelessToolbar; 149 | -------------------------------------------------------------------------------- /src/components/edgeless-toolbar/reply.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/edgeless-toolbar/style.ts: -------------------------------------------------------------------------------- 1 | import { styled, displayFlex } from '@/styles'; 2 | 3 | export const StyledEdgelessToolbar = styled.div(({ theme }) => ({ 4 | height: '320px', 5 | position: 'fixed', 6 | left: '12px', 7 | top: 0, 8 | bottom: 0, 9 | margin: 'auto', 10 | zIndex: theme.zIndex.modal, 11 | })); 12 | 13 | export const StyledToolbarWrapper = styled.div(({ theme }) => ({ 14 | width: '44px', 15 | borderRadius: '10px', 16 | boxShadow: theme.shadow.modal, 17 | padding: '4px', 18 | background: theme.colors.popoverBackground, 19 | transition: 'background .5s', 20 | marginBottom: '12px', 21 | })); 22 | 23 | export const StyledToolbarItem = styled.div<{ 24 | disable?: boolean; 25 | }>(({ theme, disable = false }) => ({ 26 | width: '36px', 27 | height: '36px', 28 | ...displayFlex('center', 'center'), 29 | color: disable ? theme.colors.disableColor : theme.colors.iconColor, 30 | cursor: disable ? 'not-allowed' : 'pointer', 31 | svg: { 32 | width: '36px', 33 | height: '36px', 34 | }, 35 | ':hover': disable 36 | ? {} 37 | : { 38 | color: theme.colors.primaryColor, 39 | background: theme.colors.hoverBackground, 40 | }, 41 | })); 42 | -------------------------------------------------------------------------------- /src/components/editor-mode-switch/icons.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, DOMAttributes } from 'react'; 2 | 3 | type IconProps = { 4 | style?: CSSProperties; 5 | } & DOMAttributes; 6 | 7 | export const ArrowIcon = ({ 8 | style: propsStyle = {}, 9 | direction = 'right', 10 | ...props 11 | }: IconProps & { direction?: 'left' | 'right' | 'middle' }) => { 12 | const style = { 13 | transform: `rotate(${direction === 'left' ? '0' : '180deg'})`, 14 | opacity: direction === 'middle' ? 0 : 1, 15 | ...propsStyle, 16 | }; 17 | return ( 18 | 27 | 32 | 33 | ); 34 | }; 35 | 36 | export const PaperIcon = ({ style = {}, ...props }: IconProps) => { 37 | return ( 38 | 47 | 52 | 53 | 58 | 59 | ); 60 | }; 61 | 62 | export const EdgelessIcon = ({ style = {}, ...props }: IconProps) => { 63 | return ( 64 | 73 | 78 | 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/editor-mode-switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, cloneElement } from 'react'; 2 | import { 3 | StyledAnimateRadioContainer, 4 | StyledMiddleLine, 5 | StyledRadioItem, 6 | StyledLabel, 7 | StyledIcon, 8 | } from './style'; 9 | import type { 10 | RadioItemStatus, 11 | AnimateRadioProps, 12 | AnimateRadioItemProps, 13 | } from './type'; 14 | import { useTheme } from '@/styles'; 15 | import { EdgelessIcon, PaperIcon } from './icons'; 16 | import { useEditor } from '@/components/editor-provider'; 17 | 18 | const PaperItem = ({ active }: { active?: boolean }) => { 19 | const { 20 | theme: { 21 | colors: { iconColor, primaryColor }, 22 | }, 23 | } = useTheme(); 24 | 25 | return ; 26 | }; 27 | 28 | const EdgelessItem = ({ active }: { active?: boolean }) => { 29 | const { 30 | theme: { 31 | colors: { iconColor, primaryColor }, 32 | }, 33 | } = useTheme(); 34 | 35 | return ; 36 | }; 37 | 38 | const AnimateRadioItem = ({ 39 | active, 40 | status, 41 | icon: propsIcon, 42 | label, 43 | isLeft, 44 | ...props 45 | }: AnimateRadioItemProps) => { 46 | const icon = ( 47 | 48 | {cloneElement(propsIcon, { 49 | active, 50 | })} 51 | 52 | ); 53 | return ( 54 | 55 | {isLeft ? icon : null} 56 | 57 | {label} 58 | 59 | {isLeft ? null : icon} 60 | 61 | ); 62 | }; 63 | 64 | export const EditorModeSwitch = ({ 65 | isHover, 66 | style = {}, 67 | }: AnimateRadioProps) => { 68 | const { mode: themeMode } = useTheme(); 69 | const { mode, setMode } = useEditor(); 70 | const modifyRadioItemStatus = (): RadioItemStatus => { 71 | return { 72 | left: isHover 73 | ? mode === 'page' 74 | ? 'stretch' 75 | : 'normal' 76 | : mode === 'page' 77 | ? 'shrink' 78 | : 'hidden', 79 | right: isHover 80 | ? mode === 'edgeless' 81 | ? 'stretch' 82 | : 'normal' 83 | : mode === 'edgeless' 84 | ? 'shrink' 85 | : 'hidden', 86 | }; 87 | }; 88 | const [radioItemStatus, setRadioItemStatus] = useState( 89 | modifyRadioItemStatus 90 | ); 91 | 92 | useEffect(() => { 93 | setRadioItemStatus(modifyRadioItemStatus()); 94 | // eslint-disable-next-line react-hooks/exhaustive-deps 95 | }, [isHover, mode]); 96 | 97 | return ( 98 | 103 | } 107 | active={mode === 'page'} 108 | status={radioItemStatus.left} 109 | onClick={() => { 110 | setMode('page'); 111 | }} 112 | onMouseEnter={() => { 113 | setRadioItemStatus({ 114 | right: 'normal', 115 | left: 'stretch', 116 | }); 117 | }} 118 | onMouseLeave={() => { 119 | setRadioItemStatus(modifyRadioItemStatus()); 120 | }} 121 | /> 122 | 143 | ); 144 | }; 145 | 146 | export default EditorModeSwitch; 147 | -------------------------------------------------------------------------------- /src/components/editor-mode-switch/style.ts: -------------------------------------------------------------------------------- 1 | import { displayFlex, keyframes, styled } from '@/styles'; 2 | // @ts-ignore 3 | import spring, { toString } from 'css-spring'; 4 | import type { ItemStatus } from './type'; 5 | 6 | const ANIMATE_DURATION = 500; 7 | 8 | export const StyledAnimateRadioContainer = styled('div')<{ shrink: boolean }>( 9 | ({ shrink, theme }) => { 10 | const animateScaleStretch = keyframes`${toString( 11 | spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' }) 12 | )}`; 13 | const animateScaleShrink = keyframes( 14 | `${toString( 15 | spring({ width: '160px' }, { width: '36px' }, { preset: 'gentle' }) 16 | )}` 17 | ); 18 | const shrinkStyle = shrink 19 | ? { 20 | animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`, 21 | background: 'transparent', 22 | } 23 | : { 24 | animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`, 25 | }; 26 | 27 | return { 28 | height: '36px', 29 | borderRadius: '18px', 30 | background: theme.colors.hoverBackground, 31 | position: 'relative', 32 | display: 'flex', 33 | transition: `background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms`, 34 | border: '1px solid transparent', 35 | 36 | ...shrinkStyle, 37 | ':hover': { 38 | border: `1px solid ${theme.colors.primaryColor}`, 39 | }, 40 | }; 41 | } 42 | ); 43 | 44 | export const StyledMiddleLine = styled('div')<{ 45 | hidden: boolean; 46 | dark: boolean; 47 | }>(({ hidden, dark }) => { 48 | return { 49 | width: '1px', 50 | height: '16px', 51 | background: dark ? '#4d4c53' : '#D0D7E3', 52 | top: '0', 53 | bottom: '0', 54 | margin: 'auto', 55 | opacity: hidden ? '0' : '1', 56 | }; 57 | }); 58 | 59 | export const StyledRadioItem = styled('div')<{ 60 | status: ItemStatus; 61 | active: boolean; 62 | }>(({ status, active, theme }) => { 63 | const animateScaleStretch = keyframes`${toString( 64 | spring({ width: '44px' }, { width: '112px' }) 65 | )}`; 66 | const animateScaleOrigin = keyframes( 67 | `${toString(spring({ width: '112px' }, { width: '44px' }))}` 68 | ); 69 | const animateScaleShrink = keyframes( 70 | `${toString(spring({ width: '0px' }, { width: '36px' }))}` 71 | ); 72 | const dynamicStyle = 73 | status === 'stretch' 74 | ? { 75 | animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`, 76 | flexShrink: '0', 77 | } 78 | : status === 'shrink' 79 | ? { 80 | animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`, 81 | } 82 | : status === 'normal' 83 | ? { animation: `${animateScaleOrigin} ${ANIMATE_DURATION}ms forwards` } 84 | : {}; 85 | 86 | const { 87 | colors: { iconColor, primaryColor }, 88 | } = theme; 89 | return { 90 | width: '0', 91 | height: '100%', 92 | display: 'flex', 93 | cursor: 'pointer', 94 | overflow: 'hidden', 95 | color: active ? primaryColor : iconColor, 96 | ...dynamicStyle, 97 | }; 98 | }); 99 | 100 | export const StyledLabel = styled('div')<{ 101 | shrink: boolean; 102 | isLeft: boolean; 103 | }>(({ shrink, isLeft }) => { 104 | const animateScaleStretch = keyframes`${toString( 105 | spring( 106 | { width: '0px' }, 107 | { width: isLeft ? '65px' : '75px' }, 108 | { preset: 'gentle' } 109 | ) 110 | )}`; 111 | const animateScaleShrink = keyframes( 112 | `${toString( 113 | spring( 114 | { width: isLeft ? '65px' : '75px' }, 115 | { width: '0px' }, 116 | { preset: 'gentle' } 117 | ) 118 | )}` 119 | ); 120 | const shrinkStyle = shrink 121 | ? { 122 | animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`, 123 | } 124 | : { 125 | animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`, 126 | }; 127 | 128 | return { 129 | display: 'flex', 130 | alignItems: 'center', 131 | justifyContent: isLeft ? 'flex-start' : 'flex-end', 132 | fontSize: '16px', 133 | flexShrink: '0', 134 | transition: `transform ${ANIMATE_DURATION}ms`, 135 | fontWeight: 'normal', 136 | overflow: 'hidden', 137 | ...shrinkStyle, 138 | }; 139 | }); 140 | 141 | export const StyledIcon = styled('div')<{ 142 | shrink: boolean; 143 | isLeft: boolean; 144 | }>(({ shrink, isLeft }) => { 145 | const dynamicStyle = shrink 146 | ? { width: '36px' } 147 | : { width: isLeft ? '44px' : '34px' }; 148 | return { 149 | ...displayFlex('center', 'center'), 150 | flexShrink: '0', 151 | ...dynamicStyle, 152 | }; 153 | }); 154 | -------------------------------------------------------------------------------- /src/components/editor-mode-switch/type.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, DOMAttributes, ReactElement } from 'react'; 2 | 3 | export type ItemStatus = 'normal' | 'stretch' | 'shrink' | 'hidden'; 4 | 5 | export type RadioItemStatus = { 6 | left: ItemStatus; 7 | right: ItemStatus; 8 | }; 9 | export type AnimateRadioProps = { 10 | isHover: boolean; 11 | style: CSSProperties; 12 | }; 13 | export type AnimateRadioItemProps = { 14 | active: boolean; 15 | status: ItemStatus; 16 | label: string; 17 | icon: ReactElement; 18 | isLeft: boolean; 19 | } & DOMAttributes; 20 | -------------------------------------------------------------------------------- /src/components/editor-provider.tsx: -------------------------------------------------------------------------------- 1 | import type { EditorContainer } from '@blocksuite/editor'; 2 | 3 | import { createContext, useContext, useEffect, useState } from 'react'; 4 | import type { PropsWithChildren } from 'react'; 5 | 6 | type EditorContextValue = { 7 | editor: EditorContainer | null; 8 | mode: EditorContainer['mode']; 9 | setEditor: (editor: EditorContainer) => void; 10 | setMode: (mode: EditorContainer['mode']) => void; 11 | }; 12 | 13 | type EditorContextProps = PropsWithChildren<{}>; 14 | 15 | export const EditorContext = createContext({ 16 | editor: null, 17 | mode: 'page', 18 | setEditor: () => {}, 19 | setMode: () => {}, 20 | }); 21 | 22 | export const useEditor = () => useContext(EditorContext); 23 | 24 | export const EditorProvider = ({ 25 | children, 26 | }: PropsWithChildren) => { 27 | const [editor, setEditor] = useState(null); 28 | const [mode, setMode] = useState('page'); 29 | 30 | useEffect(() => { 31 | const event = new CustomEvent('affine.switch-mode', { detail: mode }); 32 | window.dispatchEvent(event); 33 | }, [mode]); 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | export default EditorProvider; 43 | -------------------------------------------------------------------------------- /src/components/editor.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from '@/components/editor-provider'; 2 | import '@blocksuite/blocks'; 3 | import '@blocksuite/blocks/style'; 4 | import type { EditorContainer } from '@blocksuite/editor'; 5 | import { createEditor, BlockSchema } from '@blocksuite/editor'; 6 | import { Workspace } from '@blocksuite/store'; 7 | import { forwardRef, Suspense, useEffect, useRef } from 'react'; 8 | import pkg from '../../package.json'; 9 | import exampleMarkdown from './example-markdown'; 10 | 11 | // eslint-disable-next-line react/display-name 12 | const BlockSuiteEditor = forwardRef(({}, ref) => { 13 | const containerElement = useRef(null); 14 | useEffect(() => { 15 | if (!containerElement.current) { 16 | return; 17 | } 18 | const workspace = new Workspace({}); 19 | const page = workspace.createPage('page0').register(BlockSchema); 20 | const editor = createEditor(page); 21 | containerElement.current.appendChild(editor); 22 | if (ref) { 23 | if ('current' in ref) { 24 | ref.current = editor; 25 | } else { 26 | ref(editor); 27 | } 28 | } 29 | return () => { 30 | editor.remove(); 31 | }; 32 | }, [ref]); 33 | return
; 34 | }); 35 | 36 | export const Editor = () => { 37 | const editorRef = useRef(null); 38 | const { setEditor } = useEditor(); 39 | useEffect(() => { 40 | if (!editorRef.current) { 41 | return; 42 | } 43 | setEditor(editorRef.current); 44 | const { page } = editorRef.current as EditorContainer; 45 | const pageId = page.addBlock({ 46 | flavour: 'affine:page', 47 | title: 'Welcome to the AFFiNE Alpha', 48 | }); 49 | const groupId = page.addBlock({ flavour: 'affine:group' }, pageId); 50 | editorRef.current.clipboard.importMarkdown(exampleMarkdown, `${groupId}`); 51 | page.resetHistory(); 52 | }, [setEditor]); 53 | 54 | useEffect(() => { 55 | const version = pkg.dependencies['@blocksuite/editor'].substring(1); 56 | console.log(`BlockSuite live demo ${version}`); 57 | }, []); 58 | 59 | return ( 60 | Error!
}> 61 | 62 | 63 | ); 64 | }; 65 | 66 | declare global { 67 | interface HTMLElementTagNameMap { 68 | 'editor-container': EditorContainer; 69 | } 70 | 71 | namespace JSX { 72 | interface IntrinsicElements { 73 | // TODO fix types on react 74 | 'editor-container': EditorContainer; 75 | } 76 | } 77 | } 78 | 79 | export default Editor; 80 | -------------------------------------------------------------------------------- /src/components/example-markdown.ts: -------------------------------------------------------------------------------- 1 | const exampleMarkdown = `The AFFiNE Alpha is here! You can also view our [Official Website](https://affine.pro/)! 2 | 3 | **What's different in AFFiNE Alpha?** 4 | 5 | 1. A much ~smoother editing~ experience, with much ~greater stability~; 6 | 2. More complete ~Markdown~ support and improved ~keyboard shortcuts~; 7 | 3. New features such as ~dark mode~; 8 | * **Switch between view styles using the** ☀ **and** 🌙. 9 | 4. ~Clean and modern UI/UX~ design. 10 | 11 | **Looking for Markdown syntax or keyboard shortcuts?** 12 | 13 | * Find the (?) in the bottom right, then the ️⌨️, to view a full list of \`Keyboard Shortcuts\` 14 | 15 | **Have an enjoyable editing experience !!!** 😃 16 | 17 | Have some feedback or just want to get in touch? Use the (?), then 🎧 to get in touch and join our communities. 18 | 19 | **Still in development** 20 | 21 | There are also some things you should consider about this AFFiNE Alpha including some ~limitations~: 22 | 23 | * Single page editing - currently editing multiple docs/pages is not supported; 24 | * Changes are not automatically stored, to save changes you should export your data. Options can be found by going to the top right and finding the \`⋮\` icon; 25 | * Without an import/open feature you are still able to access your data by copying it back in. 26 | 27 | **Keyboard Shortcuts:** 28 | 29 | 1. Undo: \`⌘+Z\` or \`Ctrl+Z\` 30 | 2. Redo: \`⌘+⇧+Z\` or \`Ctrl+Shift+Z\` 31 | 3. Bold: \`⌘+B\` or \`Ctrl+B\` 32 | 4. Italic: \`⌘+I\` or \`Ctrl+I\` 33 | 5. Underline: \`⌘+U\` or \`Ctrl+U\` 34 | 6. Strikethrough: \`⌘+⇧+S\` or \`Ctrl+Shift+S\` 35 | 7. Inline code: \`⌘+E\` or \`Ctrl+E\` 36 | 8. Link: \`⌘+K\` or \`Ctrl+K\` 37 | 9. Body text: \`⌘+⌥+0\` or \`Ctrl+Shift+0\` 38 | 10. Heading 1: \`⌘+⌥+1\` or \`Ctrl+Shift+1\` 39 | 11. Heading 2: \`⌘+⌥+2\` or \`Ctrl+Shift+2\` 40 | 12. Heading 3: \`⌘+⌥+3\` or \`Ctrl+Shift+3\` 41 | 13. Heading 4: \`⌘+⌥+4\` or \`Ctrl+Shift+4\` 42 | 14. Heading 5: \`⌘+⌥+5\` or \`Ctrl+Shift+5\` 43 | 15. Heading 6: \`⌘+⌥+6\` or \`Ctrl+Shift+6\` 44 | 16. Increase indent: \`Tab\` 45 | 17. Reduce indent: \`⇧+Tab\` or \`Shift+Tab\` 46 | 47 | **Markdown Syntax:** 48 | 49 | 1. Bold: \`**text**\` 50 | 2. Italic: \`*text*\` 51 | 3. Underline: \`~text~\` 52 | 4. Strikethrough: \`~~text~~\` 53 | 5. Inline code: \`\` \`text\` \`\` 54 | 6. Heading 1: \`# text\` 55 | 7. Heading 2: \`## text\` 56 | 8. Heading 3: \`### text\` 57 | 9. Heading 4: \`#### text\` 58 | 10. Heading 5: \`##### text\` 59 | 11. Heading 6: \`###### text\` 60 | `; 61 | 62 | export default exampleMarkdown; 63 | -------------------------------------------------------------------------------- /src/components/faq/icons.tsx: -------------------------------------------------------------------------------- 1 | export const HelpIcon = () => { 2 | return ( 3 | 10 | 15 | 16 | ); 17 | }; 18 | 19 | export const ContactIcon = () => { 20 | return ( 21 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export const KeyboardIcon = () => { 34 | return ( 35 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export const CloseIcon = () => { 48 | return ( 49 | 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/faq/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | StyledFAQ, 4 | StyledIconWrapper, 5 | StyledFAQWrapper, 6 | StyledTransformIcon, 7 | } from './style'; 8 | import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons'; 9 | import Grow from '@mui/material/Grow'; 10 | import { Tooltip } from '@/ui/tooltip'; 11 | import { useEditor } from '@/components/editor-provider'; 12 | import { useModal } from '@/components/global-modal-provider'; 13 | import { useTheme } from '@/styles'; 14 | 15 | export const FAQ = () => { 16 | const [showContent, setShowContent] = useState(false); 17 | const { mode } = useTheme(); 18 | const { mode: editorMode } = useEditor(); 19 | const { shortcutsModalHandler, contactModalHandler } = useModal(); 20 | const isEdgelessDark = mode === 'dark' && editorMode === 'edgeless'; 21 | 22 | return ( 23 | <> 24 | { 27 | setShowContent(true); 28 | }} 29 | onMouseLeave={() => { 30 | setShowContent(false); 31 | }} 32 | > 33 | 34 | 35 | 36 | { 40 | setShowContent(false); 41 | contactModalHandler(true); 42 | }} 43 | > 44 | 45 | 46 | 47 | 48 | { 52 | setShowContent(false); 53 | shortcutsModalHandler(true); 54 | }} 55 | > 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | 75 | ); 76 | }; 77 | 78 | const routesLIst: any = [ 79 | { 80 | path: '/', 81 | children: [ 82 | { 83 | element: , 84 | }, 85 | ], 86 | }, 87 | ]; 88 | -------------------------------------------------------------------------------- /src/components/faq/style.ts: -------------------------------------------------------------------------------- 1 | import { displayFlex, styled } from '@/styles'; 2 | 3 | export const StyledFAQ = styled('div')(({ theme }) => { 4 | return { 5 | width: '32px', 6 | height: '32px', 7 | color: theme.colors.iconColor, 8 | position: 'fixed', 9 | right: '30px', 10 | bottom: '30px', 11 | borderRadius: '50%', 12 | zIndex: theme.zIndex.popover, 13 | }; 14 | }); 15 | export const StyledTransformIcon = styled.div<{ in: boolean }>( 16 | ({ in: isIn, theme }) => ({ 17 | height: '32px', 18 | width: '32px', 19 | borderRadius: '50%', 20 | position: 'absolute', 21 | left: '0', 22 | right: '0', 23 | bottom: '0', 24 | top: '0', 25 | margin: 'auto', 26 | ...displayFlex('center', 'center'), 27 | opacity: isIn ? 1 : 0, 28 | backgroundColor: isIn 29 | ? theme.colors.hoverBackground 30 | : theme.colors.pageBackground, 31 | }) 32 | ); 33 | export const StyledIconWrapper = styled('div')<{ isEdgelessDark: boolean }>( 34 | ({ theme, isEdgelessDark }) => { 35 | return { 36 | color: isEdgelessDark 37 | ? theme.colors.popoverBackground 38 | : theme.colors.iconColor, 39 | marginBottom: '24px', 40 | ...displayFlex('center', 'center'), 41 | cursor: 'pointer', 42 | backgroundColor: isEdgelessDark 43 | ? 'transparent' 44 | : theme.colors.pageBackground, 45 | borderRadius: '50%', 46 | width: '32px', 47 | height: '32px', 48 | transition: 'background-color 0.3s', 49 | position: 'relative', 50 | ':hover': { 51 | color: isEdgelessDark 52 | ? theme.colors.iconColor 53 | : theme.colors.primaryColor, 54 | backgroundColor: theme.colors.hoverBackground, 55 | }, 56 | }; 57 | } 58 | ); 59 | 60 | export const StyledFAQWrapper = styled('div')(({ theme }) => { 61 | return { 62 | position: 'absolute', 63 | bottom: '100%', 64 | left: '0', 65 | width: '100%', 66 | color: theme.colors.iconColor, 67 | ':hover': { 68 | color: theme.colors.popoverColor, 69 | }, 70 | }; 71 | }); 72 | -------------------------------------------------------------------------------- /src/components/global-modal-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react'; 2 | import type { PropsWithChildren } from 'react'; 3 | import ShortcutsModal from './shortcuts-modal'; 4 | import ContactModal from '@/components/contact-modal'; 5 | 6 | type ModalContextValue = { 7 | shortcutsModalHandler: (visible: boolean) => void; 8 | contactModalHandler: (visible: boolean) => void; 9 | }; 10 | type ModalContextProps = PropsWithChildren<{}>; 11 | 12 | export const ModalContext = createContext({ 13 | shortcutsModalHandler: () => {}, 14 | contactModalHandler: () => {}, 15 | }); 16 | 17 | export const useModal = () => useContext(ModalContext); 18 | 19 | export const ModalProvider = ({ 20 | children, 21 | }: PropsWithChildren) => { 22 | const [openContactModal, setOpenContactModal] = useState(false); 23 | const [openShortcutsModal, setOpenShortcutsModal] = useState(false); 24 | 25 | return ( 26 | { 29 | setOpenShortcutsModal(visible); 30 | }, 31 | contactModalHandler: visible => { 32 | setOpenContactModal(visible); 33 | }, 34 | }} 35 | > 36 | { 39 | setOpenContactModal(false); 40 | }} 41 | > 42 | { 45 | setOpenShortcutsModal(false); 46 | }} 47 | > 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | export default ModalProvider; 54 | -------------------------------------------------------------------------------- /src/components/loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyledLoading, StyledLoadingItem } from './styled'; 2 | 3 | export const Loading = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /src/components/loading/styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@/styles'; 2 | 3 | // Inspired by https://codepen.io/graphilla/pen/rNvBMYY 4 | const loadingItemSize = '40px'; 5 | export const StyledLoading = styled.div` 6 | position: relative; 7 | left: 50%; 8 | transform: rotateX(55deg) rotateZ(-45deg) 9 | translate(calc(${loadingItemSize} * -2), 0); 10 | margin-bottom: calc(3 * ${loadingItemSize}); 11 | 12 | @keyframes slide { 13 | 0% { 14 | transform: translate(var(--sx), var(--sy)); 15 | } 16 | 65% { 17 | transform: translate(var(--ex), var(--sy)); 18 | } 19 | 95%, 20 | 100% { 21 | transform: translate(var(--ex), var(--ey)); 22 | } 23 | } 24 | @keyframes load { 25 | 20% { 26 | content: '.'; 27 | } 28 | 40% { 29 | content: '..'; 30 | } 31 | 80%, 32 | 100% { 33 | content: '...'; 34 | } 35 | } 36 | `; 37 | 38 | export const StyledLoadingItem = styled.div` 39 | position: absolute; 40 | width: ${loadingItemSize}; 41 | height: ${loadingItemSize}; 42 | background: #9dacf9; 43 | animation: slide 0.9s cubic-bezier(0.65, 0.53, 0.59, 0.93) infinite; 44 | 45 | &::before, 46 | &::after { 47 | content: ''; 48 | position: absolute; 49 | width: 100%; 50 | height: 100%; 51 | } 52 | 53 | &::before { 54 | background: #5260b9; 55 | transform: skew(0deg, -45deg); 56 | right: 100%; 57 | top: 50%; 58 | } 59 | 60 | &::after { 61 | background: #6880ff; 62 | transform: skew(-45deg, 0deg); 63 | top: 100%; 64 | right: 50%; 65 | } 66 | 67 | &:nth-of-type(1) { 68 | --sx: 50%; 69 | --sy: -50%; 70 | --ex: 150%; 71 | --ey: 50%; 72 | } 73 | 74 | &:nth-of-type(2) { 75 | --sx: -50%; 76 | --sy: -50%; 77 | --ex: 50%; 78 | --ey: -50%; 79 | } 80 | 81 | &:nth-of-type(3) { 82 | --sx: 150%; 83 | --sy: 50%; 84 | --ex: 50%; 85 | --ey: 50%; 86 | } 87 | 88 | &:nth-of-type(4) { 89 | --sx: 50%; 90 | --sy: 50%; 91 | --ex: -50%; 92 | --ey: -50%; 93 | } 94 | `; 95 | -------------------------------------------------------------------------------- /src/components/mobile-modal/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1911star/affine-client/37a541dc1f098e555f3e619c309c0365eb086415/src/components/mobile-modal/bg.png -------------------------------------------------------------------------------- /src/components/mobile-modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal from '@/ui/modal'; 3 | import getIsMobile from '@/utils/get-is-mobile'; 4 | import { 5 | ModalWrapper, 6 | StyledButton, 7 | StyledCloseButton, 8 | StyledContent, 9 | StyledTitle, 10 | } from './styles'; 11 | import CloseIcon from '@mui/icons-material/Close'; 12 | export const MobileModal = () => { 13 | const [showModal, setShowModal] = useState(getIsMobile()); 14 | return ( 15 | { 18 | setShowModal(false); 19 | }} 20 | > 21 | 22 | { 24 | setShowModal(false); 25 | }} 26 | > 27 | 28 | 29 | 30 | Ooops! 31 | 32 |

Looks like you are browsing on a mobile device.

33 |

34 | We are still working on mobile support and recommend you use a 35 | desktop device. 36 |

37 |
38 | { 40 | setShowModal(false); 41 | }} 42 | > 43 | Got it 44 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default MobileModal; 51 | -------------------------------------------------------------------------------- /src/components/mobile-modal/styles.ts: -------------------------------------------------------------------------------- 1 | import { displayFlex, styled } from '@/styles'; 2 | import bg from './bg.png'; 3 | 4 | export const ModalWrapper = styled.div(({ theme }) => { 5 | return { 6 | width: '348px', 7 | height: '388px', 8 | background: theme.colors.popoverBackground, 9 | borderRadius: '28px', 10 | position: 'relative', 11 | backgroundImage: `url(${bg.src})`, 12 | }; 13 | }); 14 | 15 | export const StyledCloseButton = styled.div(({ theme }) => { 16 | return { 17 | width: '66px', 18 | height: '66px', 19 | color: theme.colors.iconColor, 20 | cursor: 'pointer', 21 | ...displayFlex('center', 'center'), 22 | position: 'absolute', 23 | right: '0', 24 | top: '0', 25 | 26 | svg: { 27 | width: '15px', 28 | height: '15px', 29 | position: 'relative', 30 | zIndex: 1, 31 | }, 32 | }; 33 | }); 34 | 35 | export const StyledTitle = styled.div(({ theme }) => { 36 | return { 37 | ...displayFlex('center', 'center'), 38 | fontSize: '20px', 39 | fontWeight: 500, 40 | marginTop: '60px', 41 | lineHeight: 1, 42 | }; 43 | }); 44 | 45 | export const StyledContent = styled.div(({ theme }) => { 46 | return { 47 | padding: '0 40px', 48 | marginTop: '32px', 49 | fontSize: '18px', 50 | lineHeight: '25px', 51 | 'p:not(last-of-type)': { 52 | marginBottom: '10px', 53 | }, 54 | }; 55 | }); 56 | 57 | export const StyledButton = styled.div(({ theme }) => { 58 | return { 59 | width: '146px', 60 | height: '42px', 61 | background: theme.colors.primaryColor, 62 | color: '#FFFFFF', 63 | fontSize: '18px', 64 | fontWeight: 500, 65 | borderRadius: '21px', 66 | margin: '52px auto 0', 67 | cursor: 'pointer', 68 | ...displayFlex('center', 'center'), 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /src/components/shortcuts-modal/config.ts: -------------------------------------------------------------------------------- 1 | export const macKeyboardShortcuts = { 2 | Undo: '⌘+Z', 3 | Redo: '⌘+⇧+Z', 4 | Bold: '⌘+B', 5 | Italic: '⌘+I', 6 | Underline: '⌘+U', 7 | Strikethrough: '⌘+⇧+S', 8 | 'Inline code': ' ⌘+E', 9 | Link: '⌘+K', 10 | 'Body text': '⌘+⌥+0', 11 | 'Heading 1': '⌘+⌥+1', 12 | 'Heading 2': '⌘+⌥+2', 13 | 'Heading 3': '⌘+⌥+3', 14 | 'Heading 4': '⌘+⌥+4', 15 | 'Heading 5': '⌘+⌥+5', 16 | 'Heading 6': '⌘+⌥+6', 17 | 'Increase indent': 'Tab', 18 | 'Reduce indent': '⇧+Tab', 19 | }; 20 | 21 | export const macMarkdownShortcuts = { 22 | Bold: '**Text** ', 23 | Italic: '*Text* ', 24 | Underline: '~Text~ ', 25 | Strikethrough: '~~Text~~ ', 26 | 'Inline code': '`Text` ', 27 | 'Heading 1': '# Text', 28 | 'Heading 2': '## Text', 29 | 'Heading 3': '### Text', 30 | 'Heading 4': '#### Text', 31 | 'Heading 5': '##### Text', 32 | 'Heading 6': '###### Text', 33 | }; 34 | 35 | export const windowsKeyboardShortcuts = { 36 | Undo: 'Ctrl+Z', 37 | Redo: 'Ctrl+Y', 38 | Bold: 'Ctrl+B', 39 | Italic: 'Ctrl+I', 40 | Underline: 'Ctrl+U', 41 | Strikethrough: 'Ctrl+Shift+S', 42 | 'Inline code': ' Ctrl+E', 43 | Link: 'Ctrl+K', 44 | 'Body text': 'Ctrl+Shift+0', 45 | 'Heading 1': 'Ctrl+Shift+1', 46 | 'Heading 2': 'Ctrl+Shift+2', 47 | 'Heading 3': 'Ctrl+Shift+3', 48 | 'Heading 4': 'Ctrl+Shift+4', 49 | 'Heading 5': 'Ctrl+Shift+5', 50 | 'Heading 6': 'Ctrl+Shift+6', 51 | 'Increase indent': 'Tab', 52 | 'Reduce indent': 'Shift+Tab', 53 | }; 54 | export const winMarkdownShortcuts = { 55 | Bold: '**Text** ', 56 | Italic: '*Text* ', 57 | Underline: '~Text~ ', 58 | Strikethrough: '~~Text~~ ', 59 | 'Inline code': '`Text` ', 60 | 'Heading 1': '# Text', 61 | 'Heading 2': '## Text', 62 | 'Heading 3': '### Text', 63 | 'Heading 4': '#### Text', 64 | 'Heading 5': '##### Text', 65 | 'Heading 6': '###### Text', 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/shortcuts-modal/icons.tsx: -------------------------------------------------------------------------------- 1 | export const CloseIcon = () => { 2 | return ( 3 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export const KeyboardIcon = () => { 16 | return ( 17 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/shortcuts-modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { createPortal } from 'react-dom'; 2 | import { KeyboardIcon } from './icons'; 3 | import { 4 | StyledListItem, 5 | StyledModalHeader, 6 | StyledShortcutsModal, 7 | StyledSubTitle, 8 | StyledTitle, 9 | CloseButton, 10 | } from './style'; 11 | import { 12 | macKeyboardShortcuts, 13 | macMarkdownShortcuts, 14 | windowsKeyboardShortcuts, 15 | winMarkdownShortcuts, 16 | } from '@/components/shortcuts-modal/config'; 17 | import CloseIcon from '@mui/icons-material/Close'; 18 | import Slide from '@mui/material/Slide'; 19 | type ModalProps = { 20 | open: boolean; 21 | onClose: () => void; 22 | }; 23 | 24 | const isMac = () => { 25 | return /macintosh|mac os x/i.test(navigator.userAgent); 26 | }; 27 | 28 | export const ShortcutsModal = ({ open, onClose }: ModalProps) => { 29 | const markdownShortcuts = isMac() 30 | ? macMarkdownShortcuts 31 | : winMarkdownShortcuts; 32 | const keyboardShortcuts = isMac() 33 | ? macKeyboardShortcuts 34 | : windowsKeyboardShortcuts; 35 | return createPortal( 36 | 37 | 38 | <> 39 | 40 | 41 | 42 | Shortcuts 43 | 44 | 45 | { 47 | onClose(); 48 | }} 49 | > 50 | 51 | 52 | 53 | 54 | Keyboard Shortcuts 55 | 56 | {Object.entries(keyboardShortcuts).map(([title, shortcuts]) => { 57 | return ( 58 | 59 | {title} 60 | {shortcuts} 61 | 62 | ); 63 | })} 64 | Markdown Syntax 65 | {Object.entries(markdownShortcuts).map(([title, shortcuts]) => { 66 | return ( 67 | 68 | {title} 69 | {shortcuts} 70 | 71 | ); 72 | })} 73 | 74 | 75 | , 76 | document.body 77 | ); 78 | }; 79 | 80 | export default ShortcutsModal; 81 | -------------------------------------------------------------------------------- /src/components/shortcuts-modal/style.ts: -------------------------------------------------------------------------------- 1 | import { displayFlex, styled } from '@/styles'; 2 | 3 | export const StyledShortcutsModal = styled.div(({ theme }) => ({ 4 | width: '288px', 5 | height: '74vh', 6 | paddingBottom: '28px', 7 | backgroundColor: theme.colors.popoverBackground, 8 | boxShadow: theme.shadow.popover, 9 | borderRadius: `${theme.radius.popover} 0 ${theme.radius.popover} ${theme.radius.popover}`, 10 | color: theme.colors.popoverColor, 11 | overflow: 'auto', 12 | boxRadius: '10px', 13 | position: 'fixed', 14 | right: '12px', 15 | top: '0', 16 | bottom: '0', 17 | margin: 'auto', 18 | zIndex: theme.zIndex.modal, 19 | })); 20 | export const StyledTitle = styled.div(({ theme }) => ({ 21 | color: theme.colors.textColor, 22 | fontWeight: '500', 23 | fontSize: theme.font.sm, 24 | height: '44px', 25 | ...displayFlex('center', 'center'), 26 | svg: { 27 | width: '20px', 28 | marginRight: '14px', 29 | color: theme.colors.primaryColor, 30 | }, 31 | })); 32 | export const StyledSubTitle = styled.div(({ theme }) => ({ 33 | color: theme.colors.popoverColor, 34 | fontWeight: '500', 35 | fontSize: theme.font.sm, 36 | height: '34px', 37 | lineHeight: '36px', 38 | marginTop: '28px', 39 | padding: '0 16px', 40 | })); 41 | export const StyledModalHeader = styled.div(({ theme }) => ({ 42 | ...displayFlex('space-between', 'center'), 43 | paddingTop: '8px 4px 0 4px', 44 | width: '100%', 45 | padding: '8px 16px 0 16px', 46 | position: 'sticky', 47 | left: '0', 48 | top: '0', 49 | background: 'var(--affine-popover-background)', 50 | 51 | transition: 'background-color 0.5s', 52 | })); 53 | 54 | export const StyledListItem = styled.div(({ theme }) => ({ 55 | height: '34px', 56 | ...displayFlex('space-between', 'center'), 57 | fontSize: theme.font.sm, 58 | padding: '0 16px', 59 | })); 60 | 61 | export const CloseButton = styled('div')(({ theme }) => { 62 | return { 63 | width: '24px', 64 | height: '24px', 65 | borderRadius: '5px', 66 | color: theme.colors.iconColor, 67 | cursor: 'pointer', 68 | ...displayFlex('center', 'center'), 69 | svg: { 70 | width: '15px', 71 | height: '15px', 72 | }, 73 | ':hover': { 74 | background: theme.colors.hoverBackground, 75 | color: theme.colors.primaryColor, 76 | }, 77 | }; 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/simple-counter/index.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from 'lit'; 2 | import { customElement, property, state } from 'lit/decorators.js'; 3 | import * as React from 'react'; 4 | 5 | export const tagName = 'simple-counter'; 6 | 7 | // Adapt React in order to be able to use custom tags properly 8 | declare global { 9 | namespace JSX { 10 | interface IntrinsicElements { 11 | [tagName]: PersonInfoProps; 12 | } 13 | } 14 | } 15 | 16 | interface PersonInfoProps 17 | extends React.DetailedHTMLProps< 18 | React.HTMLAttributes, 19 | HTMLElement 20 | > { 21 | name?: string; 22 | } 23 | // ===================== Adapt end ==================== 24 | 25 | @customElement(tagName) 26 | export class Counter extends LitElement { 27 | static styles = css` 28 | .counter-container { 29 | display: flex; 30 | color: var(--affine-text-color); 31 | } 32 | button { 33 | margin: 0 5px; 34 | } 35 | `; 36 | 37 | @property() 38 | name?: string = ''; 39 | 40 | @state() 41 | count = 0; 42 | // Render the UI as a function of component state 43 | render() { 44 | return html`
45 |
${this.name}
46 | 47 |
${this.count}
48 | 49 |
`; 50 | } 51 | 52 | private _increment(e: Event) { 53 | this.count++; 54 | } 55 | private _subtract(e: Event) { 56 | this.count--; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/theme-mode-switch/icons.tsx: -------------------------------------------------------------------------------- 1 | import type { DOMAttributes, CSSProperties } from 'react'; 2 | type IconProps = { 3 | style?: CSSProperties; 4 | } & DOMAttributes; 5 | 6 | export const MoonIcon = ({ style = {}, ...props }: IconProps) => { 7 | return ( 8 | 17 | 22 | 23 | ); 24 | }; 25 | 26 | export const SunIcon = ({ style = {}, ...props }: IconProps) => { 27 | return ( 28 | 37 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/theme-mode-switch/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useTheme } from '@/styles'; 3 | import { MoonIcon, SunIcon } from './icons'; 4 | import { StyledThemeModeSwitch, StyledSwitchItem } from './style'; 5 | 6 | export const ThemeModeSwitch = () => { 7 | const { mode, changeMode } = useTheme(); 8 | const [isHover, setIsHover] = useState(false); 9 | const [firstTrigger, setFirstTrigger] = useState(false); 10 | return ( 11 | { 14 | setIsHover(true); 15 | if (!firstTrigger) { 16 | setFirstTrigger(true); 17 | } 18 | }} 19 | onMouseLeave={() => { 20 | setIsHover(false); 21 | }} 22 | > 23 | { 29 | changeMode('light'); 30 | }} 31 | > 32 | 33 | 34 | { 40 | changeMode('dark'); 41 | }} 42 | > 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default ThemeModeSwitch; 50 | -------------------------------------------------------------------------------- /src/components/theme-mode-switch/style.ts: -------------------------------------------------------------------------------- 1 | import { displayFlex, keyframes, styled } from '@/styles'; 2 | import { CSSProperties } from 'react'; 3 | // @ts-ignore 4 | import spring, { toString } from 'css-spring'; 5 | 6 | const ANIMATE_DURATION = 400; 7 | 8 | export const StyledThemeModeSwitch = styled('div')({ 9 | width: '32px', 10 | height: '32px', 11 | borderRadius: '5px', 12 | overflow: 'hidden', 13 | backgroundColor: 'transparent', 14 | position: 'relative', 15 | }); 16 | export const StyledSwitchItem = styled('div')<{ 17 | active: boolean; 18 | isHover: boolean; 19 | firstTrigger: boolean; 20 | }>(({ active, isHover, firstTrigger, theme }) => { 21 | const activeRaiseAnimate = keyframes`${toString( 22 | spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' }) 23 | )}`; 24 | const raiseAnimate = keyframes`${toString( 25 | spring({ top: '100%' }, { top: '0' }, { preset: 'gentle' }) 26 | )}`; 27 | const activeDeclineAnimate = keyframes`${toString( 28 | spring({ top: '-100%' }, { top: '0' }, { preset: 'gentle' }) 29 | )}`; 30 | const declineAnimate = keyframes`${toString( 31 | spring({ top: '0' }, { top: '100%' }, { preset: 'gentle' }) 32 | )}`; 33 | const activeStyle = active 34 | ? { 35 | color: theme.colors.iconColor, 36 | top: '0', 37 | animation: firstTrigger 38 | ? `${ 39 | isHover ? activeRaiseAnimate : activeDeclineAnimate 40 | } ${ANIMATE_DURATION}ms forwards` 41 | : 'unset', 42 | animationDirection: isHover ? 'normal' : 'alternate', 43 | } 44 | : ({ 45 | top: '100%', 46 | color: theme.colors.primaryColor, 47 | backgroundColor: theme.colors.hoverBackground, 48 | animation: firstTrigger 49 | ? `${ 50 | isHover ? raiseAnimate : declineAnimate 51 | } ${ANIMATE_DURATION}ms forwards` 52 | : 'unset', 53 | animationDirection: isHover ? 'normal' : 'alternate', 54 | } as CSSProperties); 55 | 56 | return { 57 | width: '32px', 58 | height: '32px', 59 | position: 'absolute', 60 | left: '0', 61 | ...displayFlex('center', 'center'), 62 | cursor: 'pointer', 63 | ...activeStyle, 64 | }; 65 | }); 66 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import dynamic from 'next/dynamic'; 3 | import '../../public/globals.css'; 4 | import '../../public/variable.css'; 5 | import './temporary.css'; 6 | import { EditorProvider } from '@/components/editor-provider'; 7 | import { ModalProvider } from '@/components/global-modal-provider'; 8 | 9 | import '@fontsource/space-mono'; 10 | import '@fontsource/poppins'; 11 | import '../utils/print-build-info'; 12 | 13 | const ThemeProvider = dynamic(() => import('@/styles/themeProvider'), { 14 | ssr: false, 15 | }); 16 | 17 | function MyApp({ Component, pageProps }: AppProps) { 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default MyApp; 32 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import createEmotionServer from '@emotion/server/create-instance'; 2 | import { cache } from '@emotion/css'; 3 | 4 | import Document, { 5 | Html, 6 | Head, 7 | Main, 8 | NextScript, 9 | DocumentContext, 10 | } from 'next/document'; 11 | import * as React from 'react'; 12 | 13 | export const renderStatic = async (html: string) => { 14 | if (html === undefined) { 15 | throw new Error('did you forget to return html from renderToString?'); 16 | } 17 | const { extractCritical } = createEmotionServer(cache); 18 | const { ids, css } = extractCritical(html); 19 | 20 | return { html, ids, css }; 21 | }; 22 | 23 | export default class AppDocument extends Document { 24 | static async getInitialProps(ctx: DocumentContext) { 25 | const page = await ctx.renderPage(); 26 | const { css, ids } = await renderStatic(page.html); 27 | const initialProps = await Document.getInitialProps(ctx); 28 | return { 29 | ...initialProps, 30 | styles: ( 31 | 32 | {initialProps.styles} 33 |