From 168c54f0b53fbf13bbbe646fe96d6cf8a0af9107 Mon Sep 17 00:00:00 2001 From: m1911star Date: Mon, 10 Jul 2023 23:07:41 +0800 Subject: [PATCH 1/2] init outline plugin --- apps/web/next.config.mjs | 1 + apps/web/package.json | 2 + apps/web/preset.config.mjs | 4 +- apps/web/src/bootstrap/index.ts | 4 + .../web/src/components/page-detail-editor.tsx | 1 + apps/web/tsconfig.json | 3 + plugins/outline/package.json | 25 ++++++ plugins/outline/src/blocksuite/index.css.ts | 31 ++++++++ plugins/outline/src/blocksuite/index.tsx | 27 +++++++ plugins/outline/src/blocksuite/ui.tsx | 77 +++++++++++++++++++ plugins/outline/src/index.ts | 35 +++++++++ plugins/outline/tsconfig.json | 19 +++++ tsconfig.json | 5 ++ yarn.lock | 44 +++++++++++ 14 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 plugins/outline/package.json create mode 100644 plugins/outline/src/blocksuite/index.css.ts create mode 100644 plugins/outline/src/blocksuite/index.tsx create mode 100644 plugins/outline/src/blocksuite/ui.tsx create mode 100644 plugins/outline/src/index.ts create mode 100644 plugins/outline/tsconfig.json diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 74184d5200ef8..24b4ab9cf72f9 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -94,6 +94,7 @@ const nextConfig = { '@affine/workspace', '@affine/jotai', '@affine/copilot', + '@affine/outline', '@toeverything/hooks', '@toeverything/y-indexeddb', '@toeverything/infra', diff --git a/apps/web/package.json b/apps/web/package.json index 08258c0a9d2f0..64167edbf5d47 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ "@affine/graphql": "workspace:*", "@affine/i18n": "workspace:*", "@affine/jotai": "workspace:*", + "@affine/outline": "workspace:*", "@affine/templates": "workspace:*", "@affine/workspace": "workspace:*", "@blocksuite/block-std": "0.0.0-20230705162600-2cb608e4-nightly", @@ -52,6 +53,7 @@ "react-is": "^18.2.0", "react-resizable-panels": "^0.0.53", "rxjs": "^7.8.1", + "smooth-scroll-into-view-if-needed": "^2.0.0", "swr": "^2.1.5", "y-protocols": "^1.0.5", "yjs": "^13.6.6", diff --git a/apps/web/preset.config.mjs b/apps/web/preset.config.mjs index f7af4cc4bf749..eafb03d5ce37d 100644 --- a/apps/web/preset.config.mjs +++ b/apps/web/preset.config.mjs @@ -53,7 +53,8 @@ const buildPreset = { buildPreset.beta = buildPreset.stable; buildPreset.internal = buildPreset.stable; -const currentBuild = process.env.BUILD_TYPE || 'stable'; +console.log(process.env.BUILD_TYPE, 'current build type'); +const currentBuild = process.env.BUILD_TYPE || 'canary'; if (process.env.CI && !process.env.BUILD_TYPE) { throw new Error('BUILD_ENV is required in CI'); @@ -101,6 +102,7 @@ const buildFlags = { // this environment variable is for debug proposes only // do not put them into CI ...(process.env.CI ? {} : environmentPreset), + ...currentBuildPreset, }; export { buildFlags }; diff --git a/apps/web/src/bootstrap/index.ts b/apps/web/src/bootstrap/index.ts index ee4084e91f876..518cdf9b4be86 100644 --- a/apps/web/src/bootstrap/index.ts +++ b/apps/web/src/bootstrap/index.ts @@ -39,6 +39,10 @@ if (!environment.isServer) { import('@affine/bookmark-block'); } +if (!environment.isServer) { + import('@affine/outline'); +} + // platform check { if (globalThis.platform) { diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx index 79a7ac3b17e83..88743eb4a2bda 100644 --- a/apps/web/src/components/page-detail-editor.tsx +++ b/apps/web/src/components/page-detail-editor.tsx @@ -136,6 +136,7 @@ const LayoutPanel = memo(function LayoutPanel( props: LayoutPanelProps ): ReactElement { const node = props.node; + console.log('node', node); if (typeof node === 'string') { if (node === 'editor') { return ; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 6705fa73d13c8..9e503eaea1728 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -61,6 +61,9 @@ { "path": "../../plugins/copilot" }, + { + "path": "../../plugins/outline" + }, // Static Server { diff --git a/plugins/outline/package.json b/plugins/outline/package.json new file mode 100644 index 0000000000000..f1b692ae3911f --- /dev/null +++ b/plugins/outline/package.json @@ -0,0 +1,25 @@ +{ + "name": "@affine/outline", + "private": true, + "main": "./src/index.ts", + "module": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@affine/component": "workspace:*", + "@toeverything/plugin-infra": "workspace:*" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", + "idb": "^7.1.1", + "jotai": "^2.2.2", + "react": "18.3.0-canary-1fdacbefd-20230630" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + }, + "version": "0.7.0-canary.36" +} diff --git a/plugins/outline/src/blocksuite/index.css.ts b/plugins/outline/src/blocksuite/index.css.ts new file mode 100644 index 0000000000000..fad58fddce8d8 --- /dev/null +++ b/plugins/outline/src/blocksuite/index.css.ts @@ -0,0 +1,31 @@ +import { style } from '@vanilla-extract/css'; + +// a sider menu style +export const outlineContainerStyle = style({ + display: 'flex', + flexDirection: 'column', + height: '100%', + top: '20px', + position: 'absolute', + width: '200px', + backgroundColor: 'var(--affine-background-color)', + color: 'var(--affine-text-primary-color)', +}); + +export const outlineHeaderStyle = style({}); + +export const outlineContentStyle = style({}); + +export const outlineMenuItemStyle = style({ + cursor: 'pointer', + padding: '4px 8px', + ':hover': { + backgroundColor: 'var(--affine-background-color-hover)', + }, + ':active': { + backgroundColor: 'var(--affine-background-color-active)', + }, + ':focus': { + backgroundColor: 'var(--affine-background-color-active)', + }, +}); diff --git a/plugins/outline/src/blocksuite/index.tsx b/plugins/outline/src/blocksuite/index.tsx new file mode 100644 index 0000000000000..a65bc68dc522f --- /dev/null +++ b/plugins/outline/src/blocksuite/index.tsx @@ -0,0 +1,27 @@ +import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type'; +import { noop } from 'foxact/noop'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { OutlineUI } from './ui'; + +export default { + uiDecorator: editor => { + if (editor.parentElement) { + const div = document.createElement('div'); + editor.parentElement.appendChild(div); + const root = createRoot(div); + root.render( + + + + ); + return () => { + root.unmount(); + div.remove(); + }; + } else { + return noop; + } + }, +} satisfies Partial; diff --git a/plugins/outline/src/blocksuite/ui.tsx b/plugins/outline/src/blocksuite/ui.tsx new file mode 100644 index 0000000000000..e01e8f38f729a --- /dev/null +++ b/plugins/outline/src/blocksuite/ui.tsx @@ -0,0 +1,77 @@ +import type { PageBlockModel } from '@blocksuite/blocks'; +import type { Page } from '@blocksuite/store'; +import type { FC } from 'react'; +import { useCallback } from 'react'; +import scrollIntoView from 'smooth-scroll-into-view-if-needed'; + +import { + outlineContainerStyle, + outlineContentStyle, + outlineHeaderStyle, + outlineMenuItemStyle, +} from './index.css'; + +export type OutlineProps = { + page: Page; +}; +function isHeading(str?: string) { + return /^h[1-6]$/.test(str ?? ''); +} +const getOutlineStructure = (root: PageBlockModel) => { + const children = root.children; + const directoryTree: { + level: number; + title: string; + id: string; + }[] = []; + for (const child of children) { + for (const heading of child.children) { + if (heading.type && isHeading(heading.type)) { + console.log(heading + '1'); + const headingLevel = parseInt(heading.type.charAt(1)); + directoryTree.push({ + level: headingLevel, + id: heading.id, + title: heading.text?.toString() ?? '', + }); + } + } + } + return directoryTree; +}; + +export const OutlineUI: FC = ({ page }) => { + const title = (page.root as PageBlockModel).title.toString(); + const root = page.root as PageBlockModel; + const tree = getOutlineStructure(root); + const handleNav = useCallback((id: string) => { + const target = document.querySelector(`[data-block-id="${id}"]`); + if (!target) return; + scrollIntoView(target, { + behavior: 'smooth', + scrollMode: 'always', + block: 'center', + inline: 'center', + }); + }, []); + return ( +
+
Outline
+
{title}
+
+ {tree.map((item, index) => { + return ( +
handleNav(item.id)} + key={index} + className={outlineMenuItemStyle} + style={{ marginLeft: (item.level - 1) * 10 }} + > + {item.title} +
+ ); + })} +
+
+ ); +}; diff --git a/plugins/outline/src/index.ts b/plugins/outline/src/index.ts new file mode 100644 index 0000000000000..e64460eb81a74 --- /dev/null +++ b/plugins/outline/src/index.ts @@ -0,0 +1,35 @@ +import { definePlugin } from '@toeverything/plugin-infra/manager'; +import { ReleaseStage } from '@toeverything/plugin-infra/type'; + +definePlugin( + { + id: 'com.affine.outline', + name: { + fallback: 'AFFiNE Outline', + i18nKey: 'com.affine.outline.name', + }, + description: { + fallback: + 'AFFiNE Outline will help you with best writing experience on the World.', + }, + publisher: { + name: { + fallback: 'AFFiNE', + i18nKey: 'com.affine.org', + }, + link: 'https://affine.pro', + }, + stage: ReleaseStage.NIGHTLY, + commands: [], + version: '0.0.1', + }, + undefined, + { + load: () => import('./blocksuite/index'), + hotModuleReload: onHot => + import.meta.webpackHot && + import.meta.webpackHot.accept('./blocksuite', () => + onHot(import('./blocksuite/index')) + ), + } +); diff --git a/plugins/outline/tsconfig.json b/plugins/outline/tsconfig.json new file mode 100644 index 0000000000000..85a28c467d463 --- /dev/null +++ b/plugins/outline/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "noEmit": false, + "outDir": "lib" + }, + "references": [ + { + "path": "../../packages/component" + }, + { + "path": "../../packages/plugin-infra" + }, + { + "path": "../../packages/env" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 7d3f997c3dcbe..86a7e27146d7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -69,6 +69,8 @@ "@affine/graphql": ["./packages/graphql/src"], "@affine/copilot": ["./plugins/copilot/src"], "@affine/copilot/*": ["./plugins/copilot/src/*"], + "@affine/outline": ["./plugins/outline/src"], + "@affine/outline/*": ["./plugins/outline/src/*"], "@affine/electron/scripts/*": ["./apps/electron/scripts/*"], "@affine-test/kit/*": ["./tests/kit/*"], "@affine-test/fixtures/*": ["./tests/fixtures/*"], @@ -120,6 +122,9 @@ { "path": "./plugins/copilot" }, + { + "path": "./plugins/outline" + }, // Tests { diff --git a/yarn.lock b/yarn.lock index 2ea0b56970bd1..5c8514e6a08a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -395,6 +395,23 @@ __metadata: languageName: unknown linkType: soft +"@affine/outline@workspace:*, @affine/outline@workspace:plugins/outline": + version: 0.0.0-use.local + resolution: "@affine/outline@workspace:plugins/outline" + dependencies: + "@affine/component": "workspace:*" + "@toeverything/plugin-infra": "workspace:*" + "@types/react": ^18.2.14 + "@types/react-dom": ^18.2.6 + idb: ^7.1.1 + jotai: ^2.2.2 + react: 18.3.0-canary-1fdacbefd-20230630 + peerDependencies: + react: "*" + react-dom: "*" + languageName: unknown + linkType: soft + "@affine/server@workspace:apps/server": version: 0.0.0-use.local resolution: "@affine/server@workspace:apps/server" @@ -515,6 +532,7 @@ __metadata: "@affine/graphql": "workspace:*" "@affine/i18n": "workspace:*" "@affine/jotai": "workspace:*" + "@affine/outline": "workspace:*" "@affine/templates": "workspace:*" "@affine/workspace": "workspace:*" "@blocksuite/block-std": 0.0.0-20230705162600-2cb608e4-nightly @@ -571,6 +589,7 @@ __metadata: react-resizable-panels: ^0.0.53 redux: ^4.2.1 rxjs: ^7.8.1 + smooth-scroll-into-view-if-needed: ^2.0.0 swc-plugin-coverage-instrument: ^0.0.18 swr: ^2.1.5 ts-node: ^10.9.1 @@ -15323,6 +15342,13 @@ __metadata: languageName: node linkType: hard +"compute-scroll-into-view@npm:^3.0.2": + version: 3.0.3 + resolution: "compute-scroll-into-view@npm:3.0.3" + checksum: 7143869648d4de8ff2cb60eb8e96a21b47948c3210d15d1bfaa7e88de722c7f83f06676b97ebff94831dde0c03e42458ecfbde466747945187ee5c7667c68395 + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -27651,6 +27677,15 @@ __metadata: languageName: node linkType: hard +"scroll-into-view-if-needed@npm:^3.0.6": + version: 3.0.10 + resolution: "scroll-into-view-if-needed@npm:3.0.10" + dependencies: + compute-scroll-into-view: ^3.0.2 + checksum: eab326e527620883040e1937329bce28396ac67199098202fc785853b1576646ff1c987594f5630f78bfd84fda8486a793845c0f5c0b1ad70638c6d015578ebb + languageName: node + linkType: hard + "scuid@npm:^1.1.0": version: 1.1.0 resolution: "scuid@npm:1.1.0" @@ -28103,6 +28138,15 @@ __metadata: languageName: node linkType: hard +"smooth-scroll-into-view-if-needed@npm:^2.0.0": + version: 2.0.0 + resolution: "smooth-scroll-into-view-if-needed@npm:2.0.0" + dependencies: + scroll-into-view-if-needed: ^3.0.6 + checksum: 2a96df1d7e477eca30d7649e30cec85217084ad83a2cdf08559fac58a03121e07607e6977e480468d18cebe244cea6b7b4fd86de9bb2341a10b4be4a413cbef8 + languageName: node + linkType: hard + "snake-case@npm:^3.0.4": version: 3.0.4 resolution: "snake-case@npm:3.0.4" From 4f94b9cce281e944e4ce28d88a06cbab0dc873bd Mon Sep 17 00:00:00 2001 From: m1911star Date: Mon, 10 Jul 2023 23:39:45 +0800 Subject: [PATCH 2/2] add basic styles --- apps/web/package.json | 1 - apps/web/preset.config.mjs | 4 +-- .../web/src/components/page-detail-editor.tsx | 1 - plugins/outline/package.json | 3 +- plugins/outline/src/blocksuite/index.css.ts | 32 +++++++++++++++---- plugins/outline/src/blocksuite/ui.tsx | 14 +++++--- yarn.lock | 2 +- 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index abe7a83ef296d..938f641ac8e03 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -53,7 +53,6 @@ "react-is": "^18.2.0", "react-resizable-panels": "^0.0.53", "rxjs": "^7.8.1", - "smooth-scroll-into-view-if-needed": "^2.0.0", "swr": "^2.1.5", "y-protocols": "^1.0.5", "yjs": "^13.6.6", diff --git a/apps/web/preset.config.mjs b/apps/web/preset.config.mjs index 87026278a6254..8964a26511044 100644 --- a/apps/web/preset.config.mjs +++ b/apps/web/preset.config.mjs @@ -55,8 +55,7 @@ const buildPreset = { buildPreset.beta = buildPreset.stable; buildPreset.internal = buildPreset.stable; -console.log(process.env.BUILD_TYPE, 'current build type'); -const currentBuild = process.env.BUILD_TYPE || 'canary'; +const currentBuild = process.env.BUILD_TYPE || 'stable'; if (process.env.CI && !process.env.BUILD_TYPE) { throw new Error('BUILD_ENV is required in CI'); @@ -107,7 +106,6 @@ const buildFlags = { // this environment variable is for debug proposes only // do not put them into CI ...(process.env.CI ? {} : environmentPreset), - ...currentBuildPreset, }; export { buildFlags }; diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx index 6c513cfebb830..773b39fc96093 100644 --- a/apps/web/src/components/page-detail-editor.tsx +++ b/apps/web/src/components/page-detail-editor.tsx @@ -146,7 +146,6 @@ const LayoutPanel = memo(function LayoutPanel( props: LayoutPanelProps ): ReactElement { const node = props.node; - console.log('node', node); if (typeof node === 'string') { if (node === 'editor') { return ; diff --git a/plugins/outline/package.json b/plugins/outline/package.json index f1b692ae3911f..a6662474a0ba2 100644 --- a/plugins/outline/package.json +++ b/plugins/outline/package.json @@ -8,7 +8,8 @@ }, "dependencies": { "@affine/component": "workspace:*", - "@toeverything/plugin-infra": "workspace:*" + "@toeverything/plugin-infra": "workspace:*", + "smooth-scroll-into-view-if-needed": "^2.0.0" }, "devDependencies": { "@types/react": "^18.2.14", diff --git a/plugins/outline/src/blocksuite/index.css.ts b/plugins/outline/src/blocksuite/index.css.ts index fad58fddce8d8..fcb0d2c58643a 100644 --- a/plugins/outline/src/blocksuite/index.css.ts +++ b/plugins/outline/src/blocksuite/index.css.ts @@ -5,27 +5,47 @@ export const outlineContainerStyle = style({ display: 'flex', flexDirection: 'column', height: '100%', - top: '20px', + padding: '0px 4px', + top: '200px', position: 'absolute', width: '200px', backgroundColor: 'var(--affine-background-color)', color: 'var(--affine-text-primary-color)', }); -export const outlineHeaderStyle = style({}); +export const outlineHeaderStyle = style({ + fontWeight: 'bold', + marginBottom: '20px', +}); export const outlineContentStyle = style({}); export const outlineMenuItemStyle = style({ cursor: 'pointer', - padding: '4px 8px', + padding: '4px', + display: 'flex', + alignItems: 'center', + whiteSpace: 'nowrap', + overflow: 'hidden', + margin: '2px 0', + textOverflow: 'ellipsis', + color: 'var(--affine-text-primary-color)', ':hover': { - backgroundColor: 'var(--affine-background-color-hover)', + backgroundColor: 'var(--affine-hover-color)', }, ':active': { - backgroundColor: 'var(--affine-background-color-active)', + backgroundColor: 'var(--affine-hover-color)', }, ':focus': { - backgroundColor: 'var(--affine-background-color-active)', + backgroundColor: 'var(--affine-hover-color)', }, }); + +export const outlineItemContentStyle = style({ + display: 'inline-block', + opacity: 0.6, + maxWidth: '100%', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); diff --git a/plugins/outline/src/blocksuite/ui.tsx b/plugins/outline/src/blocksuite/ui.tsx index e01e8f38f729a..b12ed21e42f6d 100644 --- a/plugins/outline/src/blocksuite/ui.tsx +++ b/plugins/outline/src/blocksuite/ui.tsx @@ -2,12 +2,13 @@ import type { PageBlockModel } from '@blocksuite/blocks'; import type { Page } from '@blocksuite/store'; import type { FC } from 'react'; import { useCallback } from 'react'; -import scrollIntoView from 'smooth-scroll-into-view-if-needed'; +import scrollIntoView from 'scroll-into-view-if-needed'; import { outlineContainerStyle, outlineContentStyle, outlineHeaderStyle, + outlineItemContentStyle, outlineMenuItemStyle, } from './index.css'; @@ -41,7 +42,6 @@ const getOutlineStructure = (root: PageBlockModel) => { }; export const OutlineUI: FC = ({ page }) => { - const title = (page.root as PageBlockModel).title.toString(); const root = page.root as PageBlockModel; const tree = getOutlineStructure(root); const handleNav = useCallback((id: string) => { @@ -57,17 +57,21 @@ export const OutlineUI: FC = ({ page }) => { return (
Outline
-
{title}
{tree.map((item, index) => { + if (!item.title) return null; return (
handleNav(item.id)} key={index} className={outlineMenuItemStyle} - style={{ marginLeft: (item.level - 1) * 10 }} > - {item.title} + + {item.title} +
); })} diff --git a/yarn.lock b/yarn.lock index 97eaa447eb2b5..c397e98b934b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -408,6 +408,7 @@ __metadata: idb: ^7.1.1 jotai: ^2.2.2 react: 18.3.0-canary-1fdacbefd-20230630 + smooth-scroll-into-view-if-needed: ^2.0.0 peerDependencies: react: "*" react-dom: "*" @@ -592,7 +593,6 @@ __metadata: react-resizable-panels: ^0.0.53 redux: ^4.2.1 rxjs: ^7.8.1 - smooth-scroll-into-view-if-needed: ^2.0.0 swc-plugin-coverage-instrument: ^0.0.18 swr: ^2.1.5 ts-node: ^10.9.1