From c59971596393d95f49bccfe2dbce61c675a014bf Mon Sep 17 00:00:00 2001 From: EYHN Date: Mon, 4 Mar 2024 06:42:12 +0000 Subject: [PATCH] feat(core): split right sidebar (#5971) https://github.com/toeverything/AFFiNE/assets/13579374/c846c069-aa32-445d-b59b-b773a9b05ced Now each view has a general container, the yellow area is the general container part, and the green part is the routing specific part. ![CleanShot 2024-03-01 at 11.47.35@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/g3jz87HxbjOJpXV3FPT7/9a9f6ad6-2207-42e5-ae66-f7426bc9f3fc.png) --- .../sidebar-header/sidebar-switch.tsx | 11 +- .../components/not-found-page/styles.css.ts | 4 +- .../resize-panel/resize-panel.css.ts | 2 +- .../src/components/workspace/index.css.ts | 6 +- .../frontend/component/src/theme/global.css | 1 - .../component/src/ui/scrollbar/index.css.ts | 2 +- .../blocksuite-editor-container.tsx | 14 +- .../block-suite-header/title/style.css.ts | 6 + .../src/components/page-detail-editor.tsx | 6 +- .../page-list/docs/page-list-item.tsx | 6 +- .../src/components/page-list/page-group.tsx | 18 +- .../core/src/components/pure/header/index.tsx | 66 +--- .../src/components/pure/header/style.css.tsx | 37 +- .../collections/collections-list.tsx | 2 +- .../core/src/hooks/use-is-tiny-screen.ts | 73 ---- .../multi-tab-sidebar/entities/sidebar-tab.ts | 13 + .../entities/sidebar-tabs.ts | 14 + .../entities/tabs}/copilot.css.ts | 0 .../entities/tabs}/copilot.tsx | 8 +- .../entities/tabs}/frame.css.ts | 0 .../entities/tabs}/frame.tsx | 9 +- .../entities/tabs}/journal.css.ts | 0 .../entities/tabs}/journal.tsx | 142 ++++---- .../entities/tabs}/outline.css.ts | 0 .../entities/tabs}/outline.tsx | 8 +- .../src/modules/multi-tab-sidebar/index.ts | 4 + .../multi-tab-sidebar/view/body.css.ts | 13 + .../modules/multi-tab-sidebar/view/body.tsx | 17 + .../view/header-switcher.css.ts} | 5 + .../view/header-switcher.tsx | 77 +++++ .../src/modules/page-list/page-list-view.ts | 3 - .../entities/right-sidebar-view.ts | 6 + .../right-sidebar/entities/right-sidebar.ts | 47 +++ .../core/src/modules/right-sidebar/index.ts | 3 + .../right-sidebar/view/container.css.ts | 40 +++ .../modules/right-sidebar/view/container.tsx | 68 ++++ .../modules/right-sidebar/view/header.css.ts | 43 +++ .../src/modules/right-sidebar/view/header.tsx | 69 ++++ .../right-sidebar/view/view-island.tsx | 42 +++ .../frontend/core/src/modules/services.ts | 2 + .../workbench/{view => entities}/view.ts | 5 + .../workbench/{ => entities}/workbench.ts | 0 .../core/src/modules/workbench/index.ts | 9 +- .../workbench/{ => view}/browser-adapter.ts | 2 +- .../workbench/{ => view}/desktop-adapter.ts | 2 +- .../core/src/modules/workbench/view/index.ts | 1 - .../workbench/view/route-container.css.ts | 57 +++ .../workbench/view/route-container.tsx | 76 ++++ .../workbench/view/use-is-active-view.tsx | 12 + .../workbench/view/use-view-position.tsx | 35 ++ .../src/modules/workbench/view/use-view.tsx | 15 + .../workbench/view/view-body-island.tsx | 6 + .../workbench/view/view-header-island.tsx | 6 + .../src/modules/workbench/view/view-root.tsx | 50 ++- .../workbench/{ => view}/workbench-link.tsx | 16 +- .../{ => view}/workbench-root.css.ts | 9 + .../workbench/{ => view}/workbench-root.tsx | 41 ++- .../workspace/all-collection/header.css.ts | 14 + .../pages/workspace/all-collection/header.tsx | 37 +- .../pages/workspace/all-collection/index.tsx | 57 +-- .../workspace/all-page/all-page-header.tsx | 61 ++-- .../pages/workspace/all-page/all-page.css.ts | 22 +- .../src/pages/workspace/all-page/all-page.tsx | 45 ++- .../all-tag.css.ts} | 6 +- .../src/pages/workspace/all-tag/header.tsx | 19 +- .../src/pages/workspace/all-tag/index.tsx | 35 +- .../workspace/collection/collection.css.ts | 14 + .../src/pages/workspace/collection/header.tsx | 38 +- .../src/pages/workspace/collection/index.tsx | 326 +++++++++--------- .../detail-page/detail-page-header.css.ts | 47 +-- .../detail-page/detail-page-header.tsx | 130 +------ .../workspace/detail-page/detail-page.css.ts | 46 +-- .../workspace/detail-page/detail-page.tsx | 168 +++++---- .../detail-page/editor-sidebar/atoms.ts | 99 ------ .../editor-sidebar/editor-sidebar.tsx | 16 - .../editor-sidebar/extensions/extensions.tsx | 69 ---- .../editor-sidebar/extensions/index.tsx | 1 - .../detail-page/editor-sidebar/index.ts | 4 - .../detail-page/editor-sidebar/types.ts | 15 - .../core/src/pages/workspace/index.tsx | 6 +- .../src/pages/workspace/page-list-empty.tsx | 1 + .../core/src/pages/workspace/tag/header.tsx | 19 +- .../core/src/pages/workspace/tag/index.tsx | 7 +- .../src/pages/workspace/trash-page.css.ts | 8 +- .../core/src/pages/workspace/trash-page.tsx | 47 +-- packages/frontend/core/src/utils/island.tsx | 41 +++ .../meta.json | 4 - .../storage.db | Bin 24576 -> 0 bytes 88 files changed, 1393 insertions(+), 1238 deletions(-) delete mode 100644 packages/frontend/core/src/hooks/use-is-tiny-screen.ts create mode 100644 packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tab.ts create mode 100644 packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tabs.ts rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/copilot.css.ts (100%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/copilot.tsx (79%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/frame.css.ts (100%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/frame.tsx (78%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/journal.css.ts (100%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/journal.tsx (72%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/outline.css.ts (100%) rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions => modules/multi-tab-sidebar/entities/tabs}/outline.tsx (79%) create mode 100644 packages/frontend/core/src/modules/multi-tab-sidebar/index.ts create mode 100644 packages/frontend/core/src/modules/multi-tab-sidebar/view/body.css.ts create mode 100644 packages/frontend/core/src/modules/multi-tab-sidebar/view/body.tsx rename packages/frontend/core/src/{pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts => modules/multi-tab-sidebar/view/header-switcher.css.ts} (90%) create mode 100644 packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.tsx delete mode 100644 packages/frontend/core/src/modules/page-list/page-list-view.ts create mode 100644 packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar-view.ts create mode 100644 packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar.ts create mode 100644 packages/frontend/core/src/modules/right-sidebar/index.ts create mode 100644 packages/frontend/core/src/modules/right-sidebar/view/container.css.ts create mode 100644 packages/frontend/core/src/modules/right-sidebar/view/container.tsx create mode 100644 packages/frontend/core/src/modules/right-sidebar/view/header.css.ts create mode 100644 packages/frontend/core/src/modules/right-sidebar/view/header.tsx create mode 100644 packages/frontend/core/src/modules/right-sidebar/view/view-island.tsx rename packages/frontend/core/src/modules/workbench/{view => entities}/view.ts (86%) rename packages/frontend/core/src/modules/workbench/{ => entities}/workbench.ts (100%) rename packages/frontend/core/src/modules/workbench/{ => view}/browser-adapter.ts (98%) rename packages/frontend/core/src/modules/workbench/{ => view}/desktop-adapter.ts (95%) delete mode 100644 packages/frontend/core/src/modules/workbench/view/index.ts create mode 100644 packages/frontend/core/src/modules/workbench/view/route-container.css.ts create mode 100644 packages/frontend/core/src/modules/workbench/view/route-container.tsx create mode 100644 packages/frontend/core/src/modules/workbench/view/use-is-active-view.tsx create mode 100644 packages/frontend/core/src/modules/workbench/view/use-view-position.tsx create mode 100644 packages/frontend/core/src/modules/workbench/view/use-view.tsx create mode 100644 packages/frontend/core/src/modules/workbench/view/view-body-island.tsx create mode 100644 packages/frontend/core/src/modules/workbench/view/view-header-island.tsx rename packages/frontend/core/src/modules/workbench/{ => view}/workbench-link.tsx (70%) rename packages/frontend/core/src/modules/workbench/{ => view}/workbench-root.css.ts (55%) rename packages/frontend/core/src/modules/workbench/{ => view}/workbench-root.tsx (54%) create mode 100644 packages/frontend/core/src/pages/workspace/all-collection/header.css.ts rename packages/frontend/core/src/pages/workspace/{detail-page/editor-sidebar/editor-sidebar.css.ts => all-tag/all-tag.css.ts} (64%) delete mode 100644 packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/atoms.ts delete mode 100644 packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx delete mode 100644 packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx delete mode 100644 packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx delete mode 100644 packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts delete mode 100644 packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts create mode 100644 packages/frontend/core/src/utils/island.tsx delete mode 100644 packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/meta.json delete mode 100644 packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/storage.db diff --git a/packages/frontend/component/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx b/packages/frontend/component/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx index d8fe62a8df..61b54640e4 100644 --- a/packages/frontend/component/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx +++ b/packages/frontend/component/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx @@ -1,5 +1,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { SidebarIcon } from '@blocksuite/icons'; +import clsx from 'clsx'; import { useAtom } from 'jotai'; import { IconButton } from '../../../ui/button'; @@ -7,7 +8,13 @@ import { Tooltip } from '../../../ui/tooltip'; import { appSidebarOpenAtom } from '../index.jotai'; import * as styles from './sidebar-switch.css'; -export const SidebarSwitch = ({ show }: { show: boolean }) => { +export const SidebarSwitch = ({ + show = true, + className, +}: { + show?: boolean; + className?: string; +}) => { const [open, setOpen] = useAtom(appSidebarOpenAtom); const t = useAFFiNEI18N(); const tooltipContent = open @@ -22,7 +29,7 @@ export const SidebarSwitch = ({ show }: { show: boolean }) => { side={open ? 'bottom' : 'right'} > , blockId: string | undefined, timeout = 1000 ) => { @@ -74,10 +75,10 @@ const useBlockElementById = ( let canceled = false; const start = Date.now(); function run() { - if (canceled || !container || !blockId) { + if (canceled || !containerRef.current || !blockId) { return; } - const element = findBlockElementById(container, blockId); + const element = findBlockElementById(containerRef.current, blockId); if (element) { setBlockElement(element); } else if (Date.now() - start < timeout) { @@ -88,7 +89,7 @@ const useBlockElementById = ( return () => { canceled = true; }; - }, [container, blockId, timeout]); + }, [blockId, containerRef, timeout]); return blockElement; }; @@ -200,10 +201,7 @@ export const BlocksuiteEditorContainer = forwardRef< } }, [affineEditorContainerProxy, ref]); - const blockElement = useBlockElementById( - rootRef.current, - defaultSelectedBlockId - ); + const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId); useEffect(() => { if (blockElement) { diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts index e49ef52f31..a58d338971 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts @@ -3,4 +3,10 @@ import { style } from '@vanilla-extract/css'; export const title = style({ fontWeight: 500, color: cssVar('textPrimaryColor'), + selectors: { + '&[data-editing="true"]': { + ['WebkitAppRegion' as string]: 'no-drag', + flexGrow: 1, + }, + }, }); diff --git a/packages/frontend/core/src/components/page-detail-editor.tsx b/packages/frontend/core/src/components/page-detail-editor.tsx index 8149285008..6fb824b7c8 100644 --- a/packages/frontend/core/src/components/page-detail-editor.tsx +++ b/packages/frontend/core/src/components/page-detail-editor.tsx @@ -1,6 +1,5 @@ import './page-detail-editor.css'; -import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor'; import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page'; import { assertExists, DisposableGroup } from '@blocksuite/global/utils'; import type { AffineEditorContainer } from '@blocksuite/presets'; @@ -70,14 +69,12 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({ return fontStyle.value; }, [appSettings.fontStyle]); - const [, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor(); const blockId = useRouterHash(); const onLoadEditor = useCallback( (editor: AffineEditorContainer) => { // debug current detail editor globalThis.currentEditor = editor; - setActiveBlocksuiteEditor(editor); const disposableGroup = new DisposableGroup(); disposableGroup.add( page.slots.blockUpdated.once(() => { @@ -99,10 +96,9 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({ return () => { disposableGroup.dispose(); - setActiveBlocksuiteEditor(null); }; }, - [onLoad, page, setActiveBlocksuiteEditor] + [onLoad, page] ); return ( diff --git a/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx b/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx index d2801b57a1..3e8ed841d4 100644 --- a/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx +++ b/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx @@ -1,9 +1,8 @@ import { Checkbox } from '@affine/component'; -import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useDraggable } from '@dnd-kit/core'; import { type PropsWithChildren, useCallback, useMemo } from 'react'; -import { WorkbenchLink } from '../../../modules/workbench/workbench-link'; +import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link'; import type { DraggableTitleCellData, PageListItemProps } from '../types'; import { ColWrapper, formatDate, stopPropagation } from '../utils'; import * as styles from './page-list-item.css'; @@ -13,14 +12,13 @@ const ListTitleCell = ({ title, preview, }: Pick) => { - const t = useAFFiNEI18N(); return (
- {title || t['Untitled']()} + {title}
{preview ? (
{ - const title = useBlockSuiteWorkspacePageTitle(workspace, id); - return title; +const PageTitle = ({ id }: { id: string }) => { + const page = useLiveData( + useService(PageRecordList).records.map(record => { + return record.find(p => p.id === id); + }) + ); + const title = useLiveData(page?.title); + const t = useAFFiNEI18N(); + return title || t['Untitled'](); }; const UnifiedPageIcon = ({ @@ -315,7 +323,7 @@ function pageMetaToListItemProp( : undefined; const itemProps: PageListItemProps = { pageId: item.id, - title: , + title: , preview: ( ), diff --git a/packages/frontend/core/src/components/pure/header/index.tsx b/packages/frontend/core/src/components/pure/header/index.tsx index b3478a9905..5c9f5b3c27 100644 --- a/packages/frontend/core/src/components/pure/header/index.tsx +++ b/packages/frontend/core/src/components/pure/header/index.tsx @@ -1,13 +1,10 @@ import { appSidebarFloatingAtom, appSidebarOpenAtom, - SidebarSwitch, } from '@affine/component/app-sidebar'; -import { useIsTinyScreen } from '@affine/core/hooks/use-is-tiny-screen'; import clsx from 'clsx'; import { useAtomValue } from 'jotai'; import type { ReactNode } from 'react'; -import { useCallback, useRef, useState } from 'react'; import * as style from './style.css'; @@ -16,86 +13,35 @@ interface HeaderPros { right?: ReactNode; center?: ReactNode; bottomBorder?: boolean; - isFloat?: boolean; } // The Header component is used to solve the following problems // 1. Manage layout issues independently of page or business logic // 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed -export const Header = ({ - left, - center, - right, - bottomBorder, - isFloat, -}: HeaderPros) => { - const sidebarSwitchRef = useRef(null); - const leftSlotRef = useRef(null); - const centerSlotRef = useRef(null); - const rightSlotRef = useRef(null); - - const [headerRoot, setHeaderRoot] = useState(null); - - const onSetHeaderRoot = useCallback((node: HTMLDivElement | null) => { - setHeaderRoot(node); - }, []); - - const isTinyScreen = useIsTinyScreen({ - container: headerRoot, - leftStatic: sidebarSwitchRef, - leftSlot: [leftSlotRef], - centerDom: centerSlotRef, - rightSlot: [rightSlotRef], - }); - +export const Header = ({ left, center, right }: HeaderPros) => { const open = useAtomValue(appSidebarOpenAtom); const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); return (
-
-
-
- -
-
+
-
{left}
+
{left}
{center}
-
-
- {right} -
+
+
{right}
); diff --git a/packages/frontend/core/src/components/pure/header/style.css.tsx b/packages/frontend/core/src/components/pure/header/style.css.tsx index d989bc8418..5b634bdacc 100644 --- a/packages/frontend/core/src/components/pure/header/style.css.tsx +++ b/packages/frontend/core/src/components/pure/header/style.css.tsx @@ -3,48 +3,23 @@ import { style } from '@vanilla-extract/css'; export const header = style({ display: 'flex', + flex: 1, justifyContent: 'space-between', position: 'relative', - padding: '0 16px', - minHeight: '52px', - background: 'var(--affine-background-primary-color)', zIndex: 2, - selectors: { - '&[data-sidebar-floating="false"]': { - WebkitAppRegion: 'drag', - }, - }, '@media': { print: { display: 'none', }, }, - ':has([data-popper-placement])': { - WebkitAppRegion: 'no-drag', - }, } as ComplexStyleRule); -export const headerFloat = style({ - position: 'absolute', - width: '100%', -}); - -export const bottomBorder = style({ - borderBottom: '1px solid var(--affine-border-color)', -}); - export const headerItem = style({ minHeight: '32px', display: 'flex', alignItems: 'center', flexShrink: 0, selectors: { - '&.top-item': { - height: '52px', - }, - '&.top-item-visible': { - marginRight: '20px', - }, '&.left': { justifyContent: 'left', }, @@ -66,12 +41,6 @@ export const headerCenter = style({ transform: 'translateX(-50%)', left: '50%', zIndex: 1, - selectors: { - '&.shadow': { - position: 'static', - visibility: 'hidden', - }, - }, }); export const headerSideContainer = style({ @@ -82,10 +51,6 @@ export const headerSideContainer = style({ '&.right': { flexDirection: 'row-reverse', }, - '&.block': { - display: 'block', - paddingBottom: '10px', - }, }, }); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx index 242ce86d00..a326564c70 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx @@ -22,7 +22,7 @@ import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag'; import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta'; import { Workbench } from '../../../../modules/workbench'; -import { WorkbenchLink } from '../../../../modules/workbench/workbench-link'; +import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link'; import type { CollectionsListProps } from '../index'; import { Page } from './page'; import * as styles from './styles.css'; diff --git a/packages/frontend/core/src/hooks/use-is-tiny-screen.ts b/packages/frontend/core/src/hooks/use-is-tiny-screen.ts deleted file mode 100644 index 4b0880991b..0000000000 --- a/packages/frontend/core/src/hooks/use-is-tiny-screen.ts +++ /dev/null @@ -1,73 +0,0 @@ -import 'foxact/use-debounced-state'; - -import { debounce } from 'lodash-es'; -import { type RefObject, useEffect, useState } from 'react'; - -export function useIsTinyScreen({ - container, - leftStatic, - leftSlot, - centerDom, - rightSlot, -}: { - container: HTMLElement | null; - leftStatic: RefObject; - leftSlot: RefObject[]; - centerDom: RefObject; - rightSlot: RefObject[]; -}) { - const [isTinyScreen, setIsTinyScreen] = useState(false); - - useEffect(() => { - if (!container) { - return; - } - const handleResize = debounce(() => { - if (!centerDom.current) { - return; - } - const leftStaticWidth = leftStatic.current?.clientWidth || 0; - const leftSlotWidth = leftSlot.reduce((accWidth, dom) => { - return accWidth + (dom.current?.clientWidth || 0); - }, 0); - - const rightSlotWidth = rightSlot.reduce((accWidth, dom) => { - return accWidth + (dom.current?.clientWidth || 0); - }, 0); - - if (!leftSlotWidth && !rightSlotWidth) { - if (isTinyScreen) { - setIsTinyScreen(false); - } - return; - } - - const containerRect = container.getBoundingClientRect(); - const centerRect = centerDom.current.getBoundingClientRect(); - - if ( - leftStaticWidth + leftSlotWidth + containerRect.left >= - centerRect.left || - containerRect.right - centerRect.right <= rightSlotWidth - ) { - setIsTinyScreen(true); - } else { - setIsTinyScreen(false); - } - }, 100); - - handleResize(); - - const resizeObserver = new ResizeObserver(() => { - handleResize(); - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.unobserve(container); - }; - }, [centerDom, isTinyScreen, leftSlot, leftStatic, container, rightSlot]); - - return isTinyScreen; -} diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tab.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tab.ts new file mode 100644 index 0000000000..d5da220ea3 --- /dev/null +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tab.ts @@ -0,0 +1,13 @@ +import type { AffineEditorContainer } from '@blocksuite/presets'; + +export type SidebarTabName = 'outline' | 'frame' | 'copilot' | 'journal'; + +export interface SidebarTabProps { + editor: AffineEditorContainer | null; +} + +export interface SidebarTab { + name: SidebarTabName; + icon: React.ReactNode; + Component: React.ComponentType; +} diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tabs.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tabs.ts new file mode 100644 index 0000000000..7695e16f68 --- /dev/null +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/sidebar-tabs.ts @@ -0,0 +1,14 @@ +import type { SidebarTab } from './sidebar-tab'; +import { copilotTab } from './tabs/copilot'; +import { framePanelTab } from './tabs/frame'; +import { journalTab } from './tabs/journal'; +import { outlineTab } from './tabs/outline'; + +// the list of all possible tabs in affine. +// order matters (determines the order of the tabs) +export const sidebarTabs: SidebarTab[] = [ + journalTab, + outlineTab, + framePanelTab, + copilotTab, +]; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/copilot.css.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/copilot.css.ts similarity index 100% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/copilot.css.ts rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/copilot.css.ts diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/copilot.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/copilot.tsx similarity index 79% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/copilot.tsx rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/copilot.tsx index 18a3e99363..d0bfa1ef17 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/copilot.tsx +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/copilot.tsx @@ -1,16 +1,14 @@ -import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor'; import { assertExists } from '@blocksuite/global/utils'; import { AiIcon } from '@blocksuite/icons'; import { CopilotPanel } from '@blocksuite/presets'; import { useCallback, useRef } from 'react'; -import type { EditorExtension } from '../types'; +import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './outline.css'; // A wrapper for CopilotPanel -const EditorCopilotPanel = () => { +const EditorCopilotPanel = ({ editor }: SidebarTabProps) => { const copilotPanelRef = useRef(null); - const [editor] = useActiveBlocksuiteEditor(); const onRefChange = useCallback((container: HTMLDivElement | null) => { if (container) { @@ -38,7 +36,7 @@ const EditorCopilotPanel = () => { return
; }; -export const copilotExtension: EditorExtension = { +export const copilotTab: SidebarTab = { name: 'copilot', icon: , Component: EditorCopilotPanel, diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.css.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/frame.css.ts similarity index 100% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.css.ts rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/frame.css.ts diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/frame.tsx similarity index 78% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.tsx rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/frame.tsx index c3a258c8f5..e886a5a1e5 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.tsx +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/frame.tsx @@ -1,18 +1,15 @@ -import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor'; import { assertExists } from '@blocksuite/global/utils'; import { FrameIcon } from '@blocksuite/icons'; import { FramePanel } from '@blocksuite/presets'; import { useCallback, useRef } from 'react'; -import type { EditorExtension } from '../types'; +import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './frame.css'; // A wrapper for FramePanel -const EditorFramePanel = () => { +const EditorFramePanel = ({ editor }: SidebarTabProps) => { const framePanelRef = useRef(null); - const [editor] = useActiveBlocksuiteEditor(); - const onRefChange = useCallback((container: HTMLDivElement | null) => { if (container) { assertExists(framePanelRef.current, 'frame panel should be initialized'); @@ -36,7 +33,7 @@ const EditorFramePanel = () => { return
; }; -export const framePanelExtension: EditorExtension = { +export const framePanelTab: SidebarTab = { name: 'frame', icon: , Component: EditorFramePanel, diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.css.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.css.ts similarity index 100% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.css.ts rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.css.ts diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx similarity index 72% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx index b2503ffcc0..371fd3a2c4 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/journal.tsx @@ -7,15 +7,12 @@ import { } from '@affine/component'; import { MoveToTrash } from '@affine/core/components/page-list'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; -import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; -import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useJournalHelper, useJournalInfoHelper, useJournalRouteHelper, } from '@affine/core/hooks/use-journal'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; -import type { BlockSuiteWorkspace } from '@affine/core/shared'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { EdgelessIcon, @@ -23,14 +20,21 @@ import { PageIcon, TodayIcon, } from '@blocksuite/icons'; -import type { Doc, DocMeta } from '@blocksuite/store'; +import type { PageRecord } from '@toeverything/infra'; +import { + Doc, + PageRecordList, + useLiveData, + Workspace, +} from '@toeverything/infra'; +import { useService } from '@toeverything/infra/di'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; import dayjs from 'dayjs'; import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { EditorExtension, EditorExtensionProps } from '..'; +import type { SidebarTab } from '../sidebar-tab'; import * as styles from './journal.css'; /** @@ -44,28 +48,31 @@ const CountDisplay = ({ return {count > max ? `${max}+` : count}; }; interface PageItemProps extends HTMLAttributes { - pageMeta: DocMeta; - workspace: BlockSuiteWorkspace; + pageRecord: PageRecord; right?: ReactNode; } const PageItem = ({ - pageMeta, - workspace, + pageRecord, right, className, ...attrs }: PageItemProps) => { - const { isJournal } = useJournalInfoHelper(workspace, pageMeta.id); - const title = useBlockSuiteWorkspacePageTitle(workspace, pageMeta.id); + const title = useLiveData(pageRecord.title); + const mode = useLiveData(pageRecord.mode); + const workspace = useService(Workspace); + const { isJournal } = useJournalInfoHelper( + workspace.blockSuiteWorkspace, + pageRecord.id + ); const Icon = isJournal ? TodayIcon - : pageMeta.mode === 'edgeless' + : mode === 'edgeless' ? EdgelessIcon : PageIcon; return (
@@ -84,18 +91,19 @@ interface NavItem { label: string; count: number; } -interface JournalBlockProps extends EditorExtensionProps { +interface JournalBlockProps { date: dayjs.Dayjs; } -const EditorJournalPanel = (props: EditorExtensionProps) => { - const { workspace, page } = props; +const EditorJournalPanel = () => { const t = useAFFiNEI18N(); + const doc = useService(Doc); + const workspace = useService(Workspace); const { journalDate, isJournal } = useJournalInfoHelper( - page.workspace, - page.id + workspace.blockSuiteWorkspace, + doc.id ); - const { openJournal } = useJournalRouteHelper(workspace); + const { openJournal } = useJournalRouteHelper(workspace.blockSuiteWorkspace); const [date, setDate] = useState(dayjs().format('YYYY-MM-DD')); useEffect(() => { @@ -149,19 +157,22 @@ const EditorJournalPanel = (props: EditorExtensionProps) => { onChange={onDateSelect} />
- - + +
); }; const sortPagesByDate = ( - pages: DocMeta[], + pages: PageRecord[], field: 'updatedDate' | 'createDate', order: 'asc' | 'desc' = 'desc' ) => { return [...pages].sort((a, b) => { - return (order === 'asc' ? 1 : -1) * dayjs(b[field]).diff(dayjs(a[field])); + return ( + (order === 'asc' ? 1 : -1) * + dayjs(b.meta.value[field]).diff(dayjs(a.meta.value[field])) + ); }); }; @@ -176,25 +187,30 @@ const DailyCountEmptyFallback = ({ name }: { name: NavItemName }) => {
); }; -const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => { +const JournalDailyCountBlock = ({ date }: JournalBlockProps) => { + const workspace = useService(Workspace); const nodeRef = useRef(null); const t = useAFFiNEI18N(); const [activeItem, setActiveItem] = useState('createdToday'); - const pageMetas = useBlockSuiteDocMeta(workspace); + const pageRecordList = useService(PageRecordList); + const pageRecords = useLiveData(pageRecordList.records); const navigateHelper = useNavigateHelper(); const getTodaysPages = useCallback( (field: 'createDate' | 'updatedDate') => { return sortPagesByDate( - pageMetas.filter(pageMeta => { - if (pageMeta.trash) return false; - return pageMeta[field] && dayjs(pageMeta[field]).isSame(date, 'day'); + pageRecords.filter(pageRecord => { + if (pageRecord.meta.value.trash) return false; + return ( + pageRecord.meta.value[field] && + dayjs(pageRecord.meta.value[field]).isSame(date, 'day') + ); }), field ); }, - [date, pageMetas] + [date, pageRecords] ); const createdToday = useMemo( @@ -263,15 +279,14 @@ const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => {
- {renderList.map((pageMeta, index) => ( + {renderList.map((pageRecord, index) => ( - navigateHelper.openPage(workspace.id, pageMeta.id) + navigateHelper.openPage(workspace.id, pageRecord.id) } tabIndex={name === activeItem ? 0 : -1} key={index} - pageMeta={pageMeta} - workspace={workspace} + pageRecord={pageRecord} /> ))}
@@ -286,29 +301,27 @@ const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => { const MAX_CONFLICT_COUNT = 5; interface ConflictListProps - extends JournalBlockProps, - PropsWithChildren, + extends PropsWithChildren, HTMLAttributes { - pages: Doc[]; + pageRecords: PageRecord[]; } const ConflictList = ({ - page: currentPage, - pages, - workspace, + pageRecords, children, className, ...attrs }: ConflictListProps) => { const navigateHelper = useNavigateHelper(); - const { setTrashModal } = useTrashModalHelper(workspace); + const workspace = useService(Workspace); + const currentDoc = useService(Doc); + const { setTrashModal } = useTrashModalHelper(workspace.blockSuiteWorkspace); const handleOpenTrashModal = useCallback( - (page: Doc) => { - if (!page.meta) return; + (pageRecord: PageRecord) => { setTrashModal({ open: true, - pageIds: [page.id], - pageTitles: [page.meta.title], + pageIds: [pageRecord.id], + pageTitles: [pageRecord.meta.value.title], }); }, [setTrashModal] @@ -316,19 +329,19 @@ const ConflictList = ({ return (
- {pages.map(page => { - const isCurrent = page.id === currentPage.id; + {pageRecords.map(pageRecord => { + const isCurrent = pageRecord.id === currentDoc.id; return ( handleOpenTrashModal(page)} /> + handleOpenTrashModal(pageRecord)} + /> } > @@ -336,7 +349,7 @@ const ConflictList = ({ } - onClick={() => navigateHelper.openPage(workspace.id, page.id)} + onClick={() => navigateHelper.openPage(workspace.id, pageRecord.id)} /> ); })} @@ -344,29 +357,32 @@ const ConflictList = ({
); }; -const JournalConflictBlock = (props: JournalBlockProps) => { - const { workspace, date } = props; +const JournalConflictBlock = ({ date }: JournalBlockProps) => { const t = useAFFiNEI18N(); - const journalHelper = useJournalHelper(workspace); - const pages = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD')); + const workspace = useService(Workspace); + const pageRecordList = useService(PageRecordList); + const journalHelper = useJournalHelper(workspace.blockSuiteWorkspace); + const docs = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD')); + const pageRecords = useLiveData(pageRecordList.records).filter(v => { + return docs.some(doc => doc.id === v.id); + }); - if (pages.length <= 1) return null; + if (docs.length <= 1) return null; return ( - {pages.length > MAX_CONFLICT_COUNT ? ( + {docs.length > MAX_CONFLICT_COUNT ? ( + } >
{t['com.affine.journal.conflict-show-more']({ - count: (pages.length - MAX_CONFLICT_COUNT).toFixed(0), + count: (pageRecords.length - MAX_CONFLICT_COUNT).toFixed(0), })}
@@ -375,7 +391,7 @@ const JournalConflictBlock = (props: JournalBlockProps) => { ); }; -export const journalExtension: EditorExtension = { +export const journalTab: SidebarTab = { name: 'journal', icon: , Component: EditorJournalPanel, diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.css.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/outline.css.ts similarity index 100% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.css.ts rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/outline.css.ts diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/outline.tsx similarity index 79% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.tsx rename to packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/outline.tsx index 511a4c4e86..0c5586c629 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.tsx +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/entities/tabs/outline.tsx @@ -1,16 +1,14 @@ -import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor'; import { assertExists } from '@blocksuite/global/utils'; import { TocIcon } from '@blocksuite/icons'; import { OutlinePanel } from '@blocksuite/presets'; import { useCallback, useRef } from 'react'; -import type { EditorExtension } from '../types'; +import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './outline.css'; // A wrapper for TOCNotesPanel -const EditorOutline = () => { +const EditorOutline = ({ editor }: SidebarTabProps) => { const outlinePanelRef = useRef(null); - const [editor] = useActiveBlocksuiteEditor(); const onRefChange = useCallback((container: HTMLDivElement | null) => { if (container) { @@ -35,7 +33,7 @@ const EditorOutline = () => { return
; }; -export const outlineExtension: EditorExtension = { +export const outlineTab: SidebarTab = { name: 'outline', icon: , Component: EditorOutline, diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/index.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/index.ts new file mode 100644 index 0000000000..fcdc4c8355 --- /dev/null +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/index.ts @@ -0,0 +1,4 @@ +export type { SidebarTabName } from './entities/sidebar-tab'; +export { sidebarTabs } from './entities/sidebar-tabs'; +export { MultiTabSidebarBody } from './view/body'; +export { MultiTabSidebarHeaderSwitcher } from './view/header-switcher'; diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.css.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.css.ts new file mode 100644 index 0000000000..280a9c363c --- /dev/null +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.css.ts @@ -0,0 +1,13 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; +export const root = style({ + display: 'flex', + flexDirection: 'column', + flex: 1, + width: '100%', + height: '100%', + minWidth: '320px', + overflow: 'hidden', + alignItems: 'center', + borderTop: `1px solid ${cssVar('borderColor')}`, +}); diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.tsx new file mode 100644 index 0000000000..48cc43513f --- /dev/null +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.tsx @@ -0,0 +1,17 @@ +import type { PropsWithChildren } from 'react'; + +import type { SidebarTab, SidebarTabProps } from '../entities/sidebar-tab'; +import * as styles from './body.css'; + +export const MultiTabSidebarBody = ( + props: PropsWithChildren +) => { + const Component = props.tab?.Component; + + return ( +
+ {props.children} + {Component ? : null} +
+ ); +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.css.ts similarity index 90% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts rename to packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.css.ts index a8f9bf927c..a3c5f90929 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.css.ts @@ -1,6 +1,11 @@ import { cssVar } from '@toeverything/theme'; import { createVar, style } from '@vanilla-extract/css'; export const activeIdx = createVar(); +export const switchRootWrapper = style({ + height: '52px', + display: 'flex', + alignItems: 'center', +}); export const switchRoot = style({ vars: { [activeIdx]: '0', diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.tsx new file mode 100644 index 0000000000..cb6a4eed6c --- /dev/null +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.tsx @@ -0,0 +1,77 @@ +import { IconButton } from '@affine/component'; +import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; +import { useWorkspaceEnabledFeatures } from '@affine/core/hooks/use-workspace-features'; +import { FeatureType } from '@affine/graphql'; +import { Doc, useService, Workspace } from '@toeverything/infra'; +import { assignInlineVars } from '@vanilla-extract/dynamic'; +import { useEffect, useMemo } from 'react'; + +import type { SidebarTab, SidebarTabName } from '../entities/sidebar-tab'; +import * as styles from './header-switcher.css'; + +export interface MultiTabSidebarHeaderSwitcherProps { + tabs: SidebarTab[]; + activeTabName: SidebarTabName | null; + setActiveTabName: (ext: SidebarTabName) => void; +} + +// provide a switcher for active extensions +// will be used in global top header (MacOS) or sidebar (Windows) +export const MultiTabSidebarHeaderSwitcher = ({ + tabs, + activeTabName, + setActiveTabName, +}: MultiTabSidebarHeaderSwitcherProps) => { + const workspace = useService(Workspace); + const doc = useService(Doc); + const copilotEnabled = useWorkspaceEnabledFeatures(workspace.meta).includes( + FeatureType.Copilot + ); + + const { isJournal } = useJournalInfoHelper( + workspace.blockSuiteWorkspace, + doc.id + ); + + const exts = useMemo( + () => + tabs.filter(ext => { + if (ext.name === 'copilot' && !copilotEnabled) return false; + return true; + }), + [copilotEnabled, tabs] + ); + + const activeExtension = exts.find(ext => ext.name === activeTabName); + + // if journal is active, set selected to journal + useEffect(() => { + const journalExtension = tabs.find(ext => ext.name === 'journal'); + isJournal && journalExtension && setActiveTabName('journal'); + }, [tabs, isJournal, setActiveTabName]); + + const vars = assignInlineVars({ + [styles.activeIdx]: String( + exts.findIndex(ext => ext.name === activeExtension?.name) ?? 0 + ), + }); + + return ( +
+
+ {exts.map(extension => { + return ( + setActiveTabName(extension.name)} + key={extension.name} + data-active={activeExtension === extension} + className={styles.button} + > + {extension.icon} + + ); + })} +
+
+ ); +}; diff --git a/packages/frontend/core/src/modules/page-list/page-list-view.ts b/packages/frontend/core/src/modules/page-list/page-list-view.ts deleted file mode 100644 index 2638cd3750..0000000000 --- a/packages/frontend/core/src/modules/page-list/page-list-view.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class PageListView { - constructor() {} -} diff --git a/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar-view.ts b/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar-view.ts new file mode 100644 index 0000000000..322d727be3 --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar-view.ts @@ -0,0 +1,6 @@ +import { createIsland } from '../../../utils/island'; + +export class RightSidebarView { + readonly body = createIsland(); + readonly header = createIsland(); +} diff --git a/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar.ts b/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar.ts new file mode 100644 index 0000000000..d670c31d01 --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar.ts @@ -0,0 +1,47 @@ +import { LiveData } from '@toeverything/infra/livedata'; + +import type { RightSidebarView } from './right-sidebar-view'; + +export class RightSidebar { + readonly isOpen = new LiveData(false); + readonly views = new LiveData([]); + readonly front = this.views.map( + stack => stack[0] as RightSidebarView | undefined + ); + readonly hasViews = this.views.map(stack => stack.length > 0); + + open() { + this.isOpen.next(true); + } + + toggle() { + this.isOpen.next(!this.isOpen.value); + } + + close() { + this.isOpen.next(false); + } + + /** + * @private use `RightSidebarViewIsland` instead + */ + _append(view: RightSidebarView) { + this.views.next([...this.views.value, view]); + } + + /** + * @private use `RightSidebarViewIsland` instead + */ + _moveToFront(view: RightSidebarView) { + if (this.views.value.includes(view)) { + this.views.next([view, ...this.views.value.filter(v => v !== view)]); + } + } + + /** + * @private use `RightSidebarViewIsland` instead + */ + _remove(view: RightSidebarView) { + this.views.next(this.views.value.filter(v => v !== view)); + } +} diff --git a/packages/frontend/core/src/modules/right-sidebar/index.ts b/packages/frontend/core/src/modules/right-sidebar/index.ts new file mode 100644 index 0000000000..11dd63935b --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/index.ts @@ -0,0 +1,3 @@ +export { RightSidebar } from './entities/right-sidebar'; +export { RightSidebarContainer } from './view/container'; +export { RightSidebarViewIsland } from './view/view-island'; diff --git a/packages/frontend/core/src/modules/right-sidebar/view/container.css.ts b/packages/frontend/core/src/modules/right-sidebar/view/container.css.ts new file mode 100644 index 0000000000..72918888ac --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/view/container.css.ts @@ -0,0 +1,40 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const sidebarContainerInner = style({ + display: 'flex', + background: cssVar('backgroundPrimaryColor'), + flexDirection: 'column', + overflow: 'hidden', + height: '100%', + width: '100%', +}); + +export const sidebarContainer = style({ + display: 'flex', + flexShrink: 0, + height: '100%', + selectors: { + [`&[data-client-border=true]`]: { + paddingLeft: 9, + }, + [`&[data-client-border=false]`]: { + borderLeft: `1px solid ${cssVar('borderColor')}`, + }, + }, +}); + +export const sidebarBodyTarget = style({ + flex: 1, + width: '100%', + minWidth: '320px', + overflow: 'hidden', + selectors: { + [`&[data-client-border=true]`]: { + paddingLeft: 9, + }, + [`&[data-client-border=false]`]: { + borderLeft: `1px solid ${cssVar('borderColor')}`, + }, + }, +}); diff --git a/packages/frontend/core/src/modules/right-sidebar/view/container.tsx b/packages/frontend/core/src/modules/right-sidebar/view/container.tsx new file mode 100644 index 0000000000..1946a7fb4e --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/view/container.tsx @@ -0,0 +1,68 @@ +import { ResizePanel } from '@affine/component/resize-panel'; +import { appSettingAtom } from '@toeverything/infra/atom'; +import { useService } from '@toeverything/infra/di'; +import { useLiveData } from '@toeverything/infra/livedata'; +import { useAtomValue } from 'jotai'; +import { useCallback, useState } from 'react'; + +import { RightSidebar } from '../entities/right-sidebar'; +import * as styles from './container.css'; +import { Header } from './header'; + +const MIN_SIDEBAR_WIDTH = 320; +const MAX_SIDEBAR_WIDTH = 800; + +export const RightSidebarContainer = () => { + const { clientBorder } = useAtomValue(appSettingAtom); + const [width, setWidth] = useState(300); + const [resizing, setResizing] = useState(false); + const rightSidebar = useService(RightSidebar); + + const frontView = useLiveData(rightSidebar.front); + const open = useLiveData(rightSidebar.isOpen) && frontView !== undefined; + + const handleOpenChange = useCallback( + (open: boolean) => { + if (open) { + rightSidebar.open(); + } else { + rightSidebar.close(); + } + }, + [rightSidebar] + ); + + const handleToggleOpen = useCallback(() => { + rightSidebar.toggle(); + }, [rightSidebar]); + + return ( + + {frontView && ( +
+
+ +
+ )} +
+ ); +}; diff --git a/packages/frontend/core/src/modules/right-sidebar/view/header.css.ts b/packages/frontend/core/src/modules/right-sidebar/view/header.css.ts new file mode 100644 index 0000000000..fa9ccd50e2 --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/view/header.css.ts @@ -0,0 +1,43 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const header = style({ + display: 'flex', + height: '52px', + width: '100%', + alignItems: 'center', + flexShrink: 0, + padding: '0 16px', + gap: '12px', + background: cssVar('backgroundPrimaryColor'), + selectors: { + '&[data-sidebar-floating="false"]': { + ['WebkitAppRegion' as string]: 'drag', + }, + }, + '@media': { + print: { + display: 'none', + }, + }, +}); + +export const spacer = style({ + flexGrow: 1, + minWidth: 12, +}); + +export const standaloneExtensionSwitcherWrapper = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + height: '52px', + position: 'relative', +}); + +export const windowsAppControlsContainer = style({ + display: 'flex', + height: '100%', + marginRight: '-16px', +}); diff --git a/packages/frontend/core/src/modules/right-sidebar/view/header.tsx b/packages/frontend/core/src/modules/right-sidebar/view/header.tsx new file mode 100644 index 0000000000..520980ea98 --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/view/header.tsx @@ -0,0 +1,69 @@ +import { IconButton } from '@affine/component'; +import { RightSidebarIcon } from '@blocksuite/icons'; + +import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls'; +import type { RightSidebarView } from '../entities/right-sidebar-view'; +import * as styles from './header.css'; + +export type HeaderProps = { + floating: boolean; + onToggle?: () => void; + view: RightSidebarView; +}; + +function Container({ + children, + style, + className, + floating, +}: { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; + floating?: boolean; +}) { + return ( +
+ {children} +
+ ); +} + +const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => { + return ( + + + + ); +}; + +const Windows = ({ floating, onToggle, view }: HeaderProps) => { + return ( + + +
+ +
+ +
+ + ); +}; + +const NonWindows = ({ floating, view, onToggle }: HeaderProps) => { + return ( + + +
+ + + ); +}; + +export const Header = + environment.isDesktop && environment.isWindows ? Windows : NonWindows; diff --git a/packages/frontend/core/src/modules/right-sidebar/view/view-island.tsx b/packages/frontend/core/src/modules/right-sidebar/view/view-island.tsx new file mode 100644 index 0000000000..dfd067de53 --- /dev/null +++ b/packages/frontend/core/src/modules/right-sidebar/view/view-island.tsx @@ -0,0 +1,42 @@ +import { useService } from '@toeverything/infra'; +import { useEffect, useMemo } from 'react'; + +import { RightSidebar } from '../entities/right-sidebar'; +import { RightSidebarView } from '../entities/right-sidebar-view'; + +export interface RightSidebarViewProps { + body: JSX.Element; + header?: JSX.Element | null; + name?: string; + active?: boolean; +} + +export const RightSidebarViewIsland = ({ + body, + header, + active, +}: RightSidebarViewProps) => { + const rightSidebar = useService(RightSidebar); + + const view = useMemo(() => new RightSidebarView(), []); + + useEffect(() => { + rightSidebar._append(view); + return () => { + rightSidebar._remove(view); + }; + }, [rightSidebar, view]); + + useEffect(() => { + if (active) { + rightSidebar._moveToFront(view); + } + }, [active, rightSidebar, view]); + + return ( + <> + {header} + {body} + + ); +}; diff --git a/packages/frontend/core/src/modules/services.ts b/packages/frontend/core/src/modules/services.ts index 0920801e5c..cc235aa051 100644 --- a/packages/frontend/core/src/modules/services.ts +++ b/packages/frontend/core/src/modules/services.ts @@ -11,6 +11,7 @@ import { LocalStorageGlobalCache, LocalStorageGlobalState, } from './infra-web/storage'; +import { RightSidebar } from './right-sidebar/entities/right-sidebar'; import { Workbench } from './workbench'; import { CurrentWorkspaceService, @@ -23,6 +24,7 @@ export function configureBusinessServices(services: ServiceCollection) { services .scope(WorkspaceScope) .add(Workbench) + .add(RightSidebar) .add(WorkspacePropertiesAdapter, [Workspace]) .add(CollectionService, [Workspace]) .add(WorkspaceLegacyProperties, [Workspace]); diff --git a/packages/frontend/core/src/modules/workbench/view/view.ts b/packages/frontend/core/src/modules/workbench/entities/view.ts similarity index 86% rename from packages/frontend/core/src/modules/workbench/view/view.ts rename to packages/frontend/core/src/modules/workbench/entities/view.ts index 199d85d1e9..8899e56dba 100644 --- a/packages/frontend/core/src/modules/workbench/view/view.ts +++ b/packages/frontend/core/src/modules/workbench/entities/view.ts @@ -4,6 +4,8 @@ import { createMemoryHistory } from 'history'; import { nanoid } from 'nanoid'; import { Observable } from 'rxjs'; +import { createIsland } from '../../../utils/island'; + export class View { id = nanoid(); @@ -19,6 +21,9 @@ export class View { this.history.location ); + header = createIsland(); + body = createIsland(); + push(path: To) { this.history.push(path); } diff --git a/packages/frontend/core/src/modules/workbench/workbench.ts b/packages/frontend/core/src/modules/workbench/entities/workbench.ts similarity index 100% rename from packages/frontend/core/src/modules/workbench/workbench.ts rename to packages/frontend/core/src/modules/workbench/entities/workbench.ts diff --git a/packages/frontend/core/src/modules/workbench/index.ts b/packages/frontend/core/src/modules/workbench/index.ts index ac6d78a1d2..9cefeba9f7 100644 --- a/packages/frontend/core/src/modules/workbench/index.ts +++ b/packages/frontend/core/src/modules/workbench/index.ts @@ -1,2 +1,7 @@ -export * from './view'; -export * from './workbench'; +export { View } from './entities/view'; +export { Workbench } from './entities/workbench'; +export { useIsActiveView } from './view/use-is-active-view'; +export { ViewBodyIsland } from './view/view-body-island'; +export { ViewHeaderIsland } from './view/view-header-island'; +export { WorkbenchLink } from './view/workbench-link'; +export { WorkbenchRoot } from './view/workbench-root'; diff --git a/packages/frontend/core/src/modules/workbench/browser-adapter.ts b/packages/frontend/core/src/modules/workbench/view/browser-adapter.ts similarity index 98% rename from packages/frontend/core/src/modules/workbench/browser-adapter.ts rename to packages/frontend/core/src/modules/workbench/view/browser-adapter.ts index a24ab34e7a..aea568c5a1 100644 --- a/packages/frontend/core/src/modules/workbench/browser-adapter.ts +++ b/packages/frontend/core/src/modules/workbench/view/browser-adapter.ts @@ -4,7 +4,7 @@ import { useEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useLocation, useNavigate } from 'react-router-dom'; -import type { Workbench } from './workbench'; +import type { Workbench } from '../entities/workbench'; /** * This hook binds the workbench to the browser router. diff --git a/packages/frontend/core/src/modules/workbench/desktop-adapter.ts b/packages/frontend/core/src/modules/workbench/view/desktop-adapter.ts similarity index 95% rename from packages/frontend/core/src/modules/workbench/desktop-adapter.ts rename to packages/frontend/core/src/modules/workbench/view/desktop-adapter.ts index c8d4aace2c..0d5bd25556 100644 --- a/packages/frontend/core/src/modules/workbench/desktop-adapter.ts +++ b/packages/frontend/core/src/modules/workbench/view/desktop-adapter.ts @@ -3,7 +3,7 @@ import { useEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useLocation } from 'react-router-dom'; -import type { Workbench } from './workbench'; +import type { Workbench } from '../entities/workbench'; /** * This hook binds the workbench to the browser router. diff --git a/packages/frontend/core/src/modules/workbench/view/index.ts b/packages/frontend/core/src/modules/workbench/view/index.ts deleted file mode 100644 index 5638cb2c6d..0000000000 --- a/packages/frontend/core/src/modules/workbench/view/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './view'; diff --git a/packages/frontend/core/src/modules/workbench/view/route-container.css.ts b/packages/frontend/core/src/modules/workbench/view/route-container.css.ts new file mode 100644 index 0000000000..2f3e423992 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/route-container.css.ts @@ -0,0 +1,57 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + height: '100%', + overflow: 'hidden', + width: '100%', + position: 'relative', + flexDirection: 'column', + minWidth: 0, + background: cssVar('backgroundPrimaryColor'), +}); + +export const header = style({ + display: 'flex', + height: '52px', + width: '100%', + alignItems: 'center', + flexShrink: 0, + background: cssVar('backgroundPrimaryColor'), + padding: '0 16px', + ['WebkitAppRegion' as string]: 'drag', + '@media': { + print: { + display: 'none', + }, + }, +}); + +export const viewBodyContainer = style({ + display: 'flex', + flex: 1, + overflow: 'hidden', +}); + +export const leftSidebarButton = style({ + margin: '0 16px 0 0', +}); + +export const rightSidebarButton = style({ + margin: '0 0 0 16px', +}); + +export const viewHeaderContainer = style({ + display: 'flex', + height: '100%', + flexGrow: 1, + minWidth: 12, +}); + +export const windowsAppControlsContainer = style({ + display: 'flex', + height: '100%', + marginRight: '-16px', + paddingLeft: '16px', +}); diff --git a/packages/frontend/core/src/modules/workbench/view/route-container.tsx b/packages/frontend/core/src/modules/workbench/view/route-container.tsx new file mode 100644 index 0000000000..e5d158e081 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/route-container.tsx @@ -0,0 +1,76 @@ +import { IconButton } from '@affine/component'; +import { + appSidebarOpenAtom, + SidebarSwitch, +} from '@affine/component/app-sidebar'; +import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; +import { RightSidebarIcon } from '@blocksuite/icons'; +import { useLiveData } from '@toeverything/infra'; +import { useService } from '@toeverything/infra/di'; +import { useAtomValue } from 'jotai'; +import { Suspense, useCallback } from 'react'; + +import { RightSidebar } from '../../right-sidebar'; +import * as styles from './route-container.css'; +import { useView } from './use-view'; +import { useViewPosition } from './use-view-position'; + +export interface Props { + route: { + Component: React.ComponentType; + }; +} + +const ToggleButton = ({ + onToggle, + className, +}: { + onToggle?: () => void; + className: string; +}) => { + return ( + + + + ); +}; + +export const RouteContainer = ({ route }: Props) => { + const view = useView(); + const viewPosition = useViewPosition(); + const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); + const rightSidebar = useService(RightSidebar); + const rightSidebarOpen = useLiveData(rightSidebar.isOpen); + const rightSidebarHasViews = useLiveData(rightSidebar.hasViews); + const handleToggleRightSidebar = useCallback(() => { + rightSidebar.toggle(); + }, [rightSidebar]); + const isWindowsDesktop = environment.isDesktop && environment.isWindows; + return ( +
+
+ {viewPosition.isFirst && !leftSidebarOpen && ( + + )} + + {viewPosition.isLast && !rightSidebarOpen && rightSidebarHasViews && ( + <> + + {isWindowsDesktop && ( +
+ +
+ )} + + )} +
+ + loading}> + + +
+ ); +}; diff --git a/packages/frontend/core/src/modules/workbench/view/use-is-active-view.tsx b/packages/frontend/core/src/modules/workbench/view/use-is-active-view.tsx new file mode 100644 index 0000000000..9d94ceac1b --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/use-is-active-view.tsx @@ -0,0 +1,12 @@ +import { useService } from '@toeverything/infra'; +import { useLiveData } from '@toeverything/infra'; + +import { Workbench } from '../entities/workbench'; +import { useView } from './use-view'; + +export function useIsActiveView() { + const workbench = useService(Workbench); + const currentView = useView(); + const activeView = useLiveData(workbench.activeView); + return currentView === activeView; +} diff --git a/packages/frontend/core/src/modules/workbench/view/use-view-position.tsx b/packages/frontend/core/src/modules/workbench/view/use-view-position.tsx new file mode 100644 index 0000000000..ccc329c1d8 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/use-view-position.tsx @@ -0,0 +1,35 @@ +import { useService } from '@toeverything/infra/di'; +import { useEffect, useState } from 'react'; + +import type { View } from '../entities/view'; +import { Workbench } from '../entities/workbench'; +import { useView } from './use-view'; + +export const useViewPosition = () => { + const workbench = useService(Workbench); + const view = useView(); + + const [position, setPosition] = useState(() => + calcPosition(view, workbench.views.value) + ); + + useEffect(() => { + const subscription = workbench.views.subscribe(views => { + setPosition(calcPosition(view, views)); + }); + return () => { + subscription.unsubscribe(); + }; + }, [view, workbench]); + + return position; +}; + +function calcPosition(view: View, viewList: View[]) { + const index = viewList.indexOf(view); + return { + index: index, + isFirst: index === 0, + isLast: index === viewList.length - 1, + }; +} diff --git a/packages/frontend/core/src/modules/workbench/view/use-view.tsx b/packages/frontend/core/src/modules/workbench/view/use-view.tsx new file mode 100644 index 0000000000..588d26e01a --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/use-view.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +import type { View } from '../entities/view'; + +export const ViewContext = createContext(null); + +export const useView = () => { + const view = useContext(ViewContext); + if (!view) { + throw new Error( + 'No view found in context. Make sure you are rendering inside a ViewRoot.' + ); + } + return view; +}; diff --git a/packages/frontend/core/src/modules/workbench/view/view-body-island.tsx b/packages/frontend/core/src/modules/workbench/view/view-body-island.tsx new file mode 100644 index 0000000000..6ca691433a --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/view-body-island.tsx @@ -0,0 +1,6 @@ +import { useView } from './use-view'; + +export const ViewBodyIsland = ({ children }: React.PropsWithChildren) => { + const view = useView(); + return {children}; +}; diff --git a/packages/frontend/core/src/modules/workbench/view/view-header-island.tsx b/packages/frontend/core/src/modules/workbench/view/view-header-island.tsx new file mode 100644 index 0000000000..ca875eb88d --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/view-header-island.tsx @@ -0,0 +1,6 @@ +import { useView } from './use-view'; + +export const ViewHeaderIsland = ({ children }: React.PropsWithChildren) => { + const view = useView(); + return {children}; +}; diff --git a/packages/frontend/core/src/modules/workbench/view/view-root.tsx b/packages/frontend/core/src/modules/workbench/view/view-root.tsx index 74548946bf..6c9a181162 100644 --- a/packages/frontend/core/src/modules/workbench/view/view-root.tsx +++ b/packages/frontend/core/src/modules/workbench/view/view-root.tsx @@ -1,5 +1,5 @@ import { useLiveData } from '@toeverything/infra/livedata'; -import { useEffect, useMemo } from 'react'; +import { lazy as reactLazy, useEffect, useMemo } from 'react'; import { createMemoryRouter, RouterProvider, @@ -8,10 +8,30 @@ import { } from 'react-router-dom'; import { viewRoutes } from '../../../router'; -import type { View } from './view'; +import type { View } from '../entities/view'; +import { RouteContainer } from './route-container'; +import { ViewContext } from './use-view'; + +const warpedRoutes = viewRoutes.map(({ path, lazy }) => { + const Component = reactLazy(() => + lazy().then(m => ({ + default: m.Component as React.ComponentType, + })) + ); + const route = { + Component, + }; + + return { + path, + Component: () => { + return ; + }, + }; +}); export const ViewRoot = ({ view }: { view: View }) => { - const viewRouter = useMemo(() => createMemoryRouter(viewRoutes), []); + const viewRouter = useMemo(() => createMemoryRouter(warpedRoutes), []); const location = useLiveData(view.location); @@ -23,16 +43,18 @@ export const ViewRoot = ({ view }: { view: View }) => { // https://github.com/remix-run/react-router/issues/7375#issuecomment-975431736 return ( - - - - - + + + + + + + ); }; diff --git a/packages/frontend/core/src/modules/workbench/workbench-link.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx similarity index 70% rename from packages/frontend/core/src/modules/workbench/workbench-link.tsx rename to packages/frontend/core/src/modules/workbench/view/workbench-link.tsx index ecc17f03ee..9ad49879ce 100644 --- a/packages/frontend/core/src/modules/workbench/workbench-link.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx @@ -2,7 +2,7 @@ import { useService } from '@toeverything/infra/di'; import type { To } from 'history'; import { useCallback } from 'react'; -import { Workbench } from './workbench'; +import { Workbench } from '../entities/workbench'; export const WorkbenchLink = ({ to, @@ -17,11 +17,15 @@ export const WorkbenchLink = ({ (event: React.MouseEvent) => { event.preventDefault(); // TODO: open this when multi view control is implemented - // if (environment.isDesktop && (event.ctrlKey || event.metaKey)) { - // workbench.open(to, { at: 'beside' }); - // } else { - workbench.open(to); - // } + if ( + (window as any).enableMultiView && + environment.isDesktop && + (event.ctrlKey || event.metaKey) + ) { + workbench.open(to, { at: 'beside' }); + } else { + workbench.open(to); + } onClick?.(event); }, diff --git a/packages/frontend/core/src/modules/workbench/workbench-root.css.ts b/packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts similarity index 55% rename from packages/frontend/core/src/modules/workbench/workbench-root.css.ts rename to packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts index 5def4df04f..4b4f1305b1 100644 --- a/packages/frontend/core/src/modules/workbench/workbench-root.css.ts +++ b/packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts @@ -3,8 +3,17 @@ import { style } from '@vanilla-extract/css'; export const workbenchRootContainer = style({ display: 'flex', height: '100%', + flex: 1, + overflow: 'hidden', + selectors: { + [`&[data-client-border="true"]`]: { + gap: '8px', + }, + }, }); export const workbenchViewContainer = style({ flex: 1, + overflow: 'hidden', + height: '100%', }); diff --git a/packages/frontend/core/src/modules/workbench/workbench-root.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx similarity index 54% rename from packages/frontend/core/src/modules/workbench/workbench-root.tsx rename to packages/frontend/core/src/modules/workbench/view/workbench-root.tsx index 4477162988..0b997aae3e 100644 --- a/packages/frontend/core/src/modules/workbench/workbench-root.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx @@ -1,17 +1,16 @@ +import { appSettingAtom } from '@toeverything/infra/atom'; import { useService } from '@toeverything/infra/di'; import { useLiveData } from '@toeverything/infra/livedata'; -import { useCallback } from 'react'; +import { useAtomValue } from 'jotai'; +import { useCallback, useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; +import type { View } from '../entities/view'; +import { Workbench } from '../entities/workbench'; import { useBindWorkbenchToBrowserRouter } from './browser-adapter'; import { useBindWorkbenchToDesktopRouter } from './desktop-adapter'; -import type { View } from './view'; -import { ViewRoot } from './view/view-root'; -import { Workbench } from './workbench'; -import { - workbenchRootContainer, - workbenchViewContainer, -} from './workbench-root.css'; +import { ViewRoot } from './view-root'; +import * as styles from './workbench-root.css'; const useAdapter = environment.isDesktop ? useBindWorkbenchToDesktopRouter @@ -30,8 +29,13 @@ export const WorkbenchRoot = () => { useAdapter(workbench, basename); + const { clientBorder } = useAtomValue(appSettingAtom); + return ( -
+
{views.map((view, index) => ( ))} @@ -46,8 +50,25 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => { workbench.active(index); }, [workbench, index]); + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + const element = containerRef.current; + element.addEventListener('mousedown', handleOnFocus, { + capture: true, + }); + return () => { + element.removeEventListener('mousedown', handleOnFocus, { + capture: true, + }); + }; + } + return; + }, [handleOnFocus]); + return ( -
+
); diff --git a/packages/frontend/core/src/pages/workspace/all-collection/header.css.ts b/packages/frontend/core/src/pages/workspace/all-collection/header.css.ts new file mode 100644 index 0000000000..9b1b795a32 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/all-collection/header.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; + +export const headerCreateNewCollectionIconButton = style({ + padding: '4px 8px', + fontSize: '16px', + width: '32px', + height: '28px', + borderRadius: '8px', + transition: 'opacity 0.1s ease-in-out', +}); +export const headerCreateNewButtonHidden = style({ + opacity: 0, + pointerEvents: 'none', +}); diff --git a/packages/frontend/core/src/pages/workspace/all-collection/header.tsx b/packages/frontend/core/src/pages/workspace/all-collection/header.tsx index 76d54670e4..34be6e3ff4 100644 --- a/packages/frontend/core/src/pages/workspace/all-collection/header.tsx +++ b/packages/frontend/core/src/pages/workspace/all-collection/header.tsx @@ -1,12 +1,10 @@ import { IconButton } from '@affine/component'; import { Header } from '@affine/core/components/pure/header'; -import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { PlusIcon } from '@blocksuite/icons'; import clsx from 'clsx'; -import { useMemo } from 'react'; -import * as styles from '../all-page/all-page.css'; +import * as styles from './header.css'; export const AllCollectionHeader = ({ showCreateNew, @@ -15,33 +13,18 @@ export const AllCollectionHeader = ({ showCreateNew: boolean; onCreateCollection?: () => void; }) => { - const isWindowsDesktop = environment.isDesktop && environment.isWindows; - - const renderRightItem = useMemo(() => { - return ( - } - onClick={onCreateCollection} - className={clsx( - styles.headerCreateNewButton, - styles.headerCreateNewCollectionIconButton, - !showCreateNew && styles.headerCreateNewButtonHidden - )} - /> - ); - }, [onCreateCollection, showCreateNew]); - return (
- {renderRightItem} - {isWindowsDesktop ? : null} -
+ } + onClick={onCreateCollection} + className={clsx( + styles.headerCreateNewCollectionIconButton, + !showCreateNew && styles.headerCreateNewButtonHidden + )} + /> } center={} /> diff --git a/packages/frontend/core/src/pages/workspace/all-collection/index.tsx b/packages/frontend/core/src/pages/workspace/all-collection/index.tsx index e95bb6b80a..f3bda3806d 100644 --- a/packages/frontend/core/src/pages/workspace/all-collection/index.tsx +++ b/packages/frontend/core/src/pages/workspace/all-collection/index.tsx @@ -1,4 +1,3 @@ -import { HubIsland } from '@affine/core/components/affine/hub-island'; import { CollectionListHeader, type CollectionMeta, @@ -16,7 +15,7 @@ import { nanoid } from 'nanoid'; import { useCallback, useMemo, useState } from 'react'; import { CollectionService } from '../../../modules/collection'; -import * as styles from '../all-page/all-page.css'; +import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; import { EmptyCollectionList } from '../page-list-empty'; import { AllCollectionHeader } from './header'; @@ -58,33 +57,35 @@ export const AllCollection = () => { }, [collectionService, currentWorkspace, navigateHelper, open]); return ( -
- - {collectionMetas.length > 0 ? ( - + + - ) : ( - - } - /> - )} - - -
+ + + {collectionMetas.length > 0 ? ( + + ) : ( + + } + /> + )} + + ); }; diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx index 55f6aa19f7..634ce35218 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx @@ -3,17 +3,14 @@ import { PageListNewPageButton, } from '@affine/core/components/page-list'; import { Header } from '@affine/core/components/pure/header'; -import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import type { Filter } from '@affine/env/filter'; import { PlusIcon } from '@blocksuite/icons'; import { useService } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra'; import clsx from 'clsx'; -import { useMemo } from 'react'; import * as styles from './all-page.css'; -import { FilterContainer } from './all-page-filter'; export const AllPageHeader = ({ showCreateNew, @@ -25,44 +22,28 @@ export const AllPageHeader = ({ onChangeFilters: (filters: Filter[]) => void; }) => { const workspace = useService(Workspace); - const isWindowsDesktop = environment.isDesktop && environment.isWindows; - - const renderRightItem = useMemo(() => { - return ( - - - - ); - }, [showCreateNew]); return ( - <> -
- } - right={ -
- {renderRightItem} - {isWindowsDesktop ? : null} -
- } - center={} - /> - - +
+ } + right={ + + + + } + center={} + /> ); }; diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts b/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts index 3b8ae4fbc4..1220d9c30e 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts @@ -1,12 +1,4 @@ -import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; -export const root = style({ - height: '100%', - width: '100%', - display: 'flex', - flexFlow: 'column', - background: cssVar('backgroundPrimaryColor'), -}); export const scrollContainer = style({ flex: 1, width: '100%', @@ -26,13 +18,11 @@ export const headerCreateNewButtonHidden = style({ opacity: 0, pointerEvents: 'none', }); -export const headerRightWindows = style({ + +export const body = style({ display: 'flex', - alignItems: 'center', - gap: 8, - selectors: { - '&[data-is-windows-desktop="true"]': { - transform: 'translateX(16px)', - }, - }, + flexDirection: 'column', + flex: 1, + height: '100%', + width: '100%', }); diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx index 74500c7b69..bac7c2a415 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx @@ -1,4 +1,3 @@ -import { HubIsland } from '@affine/core/components/affine/hub-island'; import { PageListHeader, useFilteredPageMetas, @@ -12,8 +11,10 @@ import { useService } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra'; import { useEffect, useState } from 'react'; +import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; import { EmptyPageList } from '../page-list-empty'; import * as styles from './all-page.css'; +import { FilterContainer } from './all-page-filter'; import { AllPageHeader } from './all-page-header'; export const AllPage = () => { @@ -27,26 +28,32 @@ export const AllPage = () => { }); return ( -
- - {filteredPageMetas.length > 0 ? ( - + + - ) : ( - } - blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} - /> - )} - -
+ + +
+ + {filteredPageMetas.length > 0 ? ( + + ) : ( + } + blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} + /> + )} +
+
+ ); }; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.css.ts b/packages/frontend/core/src/pages/workspace/all-tag/all-tag.css.ts similarity index 64% rename from packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.css.ts rename to packages/frontend/core/src/pages/workspace/all-tag/all-tag.css.ts index 034cbde9de..a5f5542709 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.css.ts +++ b/packages/frontend/core/src/pages/workspace/all-tag/all-tag.css.ts @@ -1,9 +1,9 @@ import { style } from '@vanilla-extract/css'; -export const root = style({ + +export const body = style({ display: 'flex', flexDirection: 'column', flex: 1, - overflow: 'auto', + height: '100%', width: '100%', - minWidth: '320px', }); diff --git a/packages/frontend/core/src/pages/workspace/all-tag/header.tsx b/packages/frontend/core/src/pages/workspace/all-tag/header.tsx index 48fdc95302..33b6f92fff 100644 --- a/packages/frontend/core/src/pages/workspace/all-tag/header.tsx +++ b/packages/frontend/core/src/pages/workspace/all-tag/header.tsx @@ -1,23 +1,6 @@ import { Header } from '@affine/core/components/pure/header'; -import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; -import * as styles from '../all-page/all-page.css'; - export const AllTagHeader = () => { - const isWindowsDesktop = environment.isDesktop && environment.isWindows; - - return ( -
- {isWindowsDesktop ? : null} -
- } - center={} - /> - ); + return
} />; }; diff --git a/packages/frontend/core/src/pages/workspace/all-tag/index.tsx b/packages/frontend/core/src/pages/workspace/all-tag/index.tsx index 296fa874ba..4c65fe7fca 100644 --- a/packages/frontend/core/src/pages/workspace/all-tag/index.tsx +++ b/packages/frontend/core/src/pages/workspace/all-tag/index.tsx @@ -1,4 +1,3 @@ -import { HubIsland } from '@affine/core/components/affine/hub-island'; import { useTagMetas } from '@affine/core/components/page-list'; import { TagListHeader, @@ -8,8 +7,9 @@ import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-me import { useService } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra'; -import * as styles from '../all-page/all-page.css'; +import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; import { EmptyTagList } from '../page-list-empty'; +import * as styles from './all-tag.css'; import { AllTagHeader } from './header'; export const AllTag = () => { @@ -22,19 +22,24 @@ export const AllTag = () => { ); return ( -
- - {tags.length > 0 ? ( - - ) : ( - } /> - )} - -
+ <> + + + + +
+ {tags.length > 0 ? ( + + ) : ( + } /> + )} +
+
+ ); }; diff --git a/packages/frontend/core/src/pages/workspace/collection/collection.css.ts b/packages/frontend/core/src/pages/workspace/collection/collection.css.ts index 6cc54d8925..8daf9aa86a 100644 --- a/packages/frontend/core/src/pages/workspace/collection/collection.css.ts +++ b/packages/frontend/core/src/pages/workspace/collection/collection.css.ts @@ -23,3 +23,17 @@ export const button = style({ backgroundColor: cssVar('hoverColor'), }, }); +export const headerCreateNewButton = style({ + transition: 'opacity 0.1s ease-in-out', +}); +export const headerCreateNewCollectionIconButton = style({ + padding: '4px 8px', + fontSize: '16px', + width: '32px', + height: '28px', + borderRadius: '8px', +}); +export const headerCreateNewButtonHidden = style({ + opacity: 0, + pointerEvents: 'none', +}); diff --git a/packages/frontend/core/src/pages/workspace/collection/header.tsx b/packages/frontend/core/src/pages/workspace/collection/header.tsx index 41b1254f95..f462f9a056 100644 --- a/packages/frontend/core/src/pages/workspace/collection/header.tsx +++ b/packages/frontend/core/src/pages/workspace/collection/header.tsx @@ -1,12 +1,10 @@ import { IconButton } from '@affine/component'; import { Header } from '@affine/core/components/pure/header'; -import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { PlusIcon } from '@blocksuite/icons'; import clsx from 'clsx'; -import { useMemo } from 'react'; -import * as styles from '../all-page/all-page.css'; +import * as styles from './collection.css'; export const CollectionDetailHeader = ({ showCreateNew, @@ -15,33 +13,19 @@ export const CollectionDetailHeader = ({ showCreateNew: boolean; onCreate: () => void; }) => { - const isWindowsDesktop = environment.isDesktop && environment.isWindows; - - const renderRightItem = useMemo(() => { - return ( - } - onClick={onCreate} - className={clsx( - styles.headerCreateNewButton, - styles.headerCreateNewCollectionIconButton, - !showCreateNew && styles.headerCreateNewButtonHidden - )} - /> - ); - }, [onCreate, showCreateNew]); - return (
- {renderRightItem} - {isWindowsDesktop ? : null} -
+ } + onClick={onCreate} + className={clsx( + styles.headerCreateNewButton, + styles.headerCreateNewCollectionIconButton, + !showCreateNew && styles.headerCreateNewButtonHidden + )} + /> } center={} /> diff --git a/packages/frontend/core/src/pages/workspace/collection/index.tsx b/packages/frontend/core/src/pages/workspace/collection/index.tsx index 379bdda2b5..3d74cc28fd 100644 --- a/packages/frontend/core/src/pages/workspace/collection/index.tsx +++ b/packages/frontend/core/src/pages/workspace/collection/index.tsx @@ -1,15 +1,9 @@ -import { - appSidebarOpenAtom, - SidebarSwitch, -} from '@affine/component/app-sidebar'; import { pushNotificationAtom } from '@affine/component/notification-center'; -import { HubIsland } from '@affine/core/components/affine/hub-island'; import { AffineShapeIcon, useEditCollection, VirtualizedPageList, } from '@affine/core/components/page-list'; -import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { CollectionService } from '@affine/core/modules/collection'; @@ -25,14 +19,13 @@ import { import { Workspace } from '@toeverything/infra'; import { useService } from '@toeverything/infra/di'; import { useLiveData } from '@toeverything/infra/livedata'; -import { useAtomValue } from 'jotai'; import { useSetAtom } from 'jotai'; import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; +import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; import { WorkspaceSubPath } from '../../../shared'; -import * as allPageStyles from '../all-page/all-page.css'; import * as styles from './collection.css'; import { CollectionDetailHeader } from './header'; @@ -52,19 +45,22 @@ export const CollectionDetail = ({ }, [collection, collectionService, open]); return ( -
- - - + <> + + + + + + {node} -
+ ); }; @@ -115,8 +111,6 @@ export const Component = function CollectionPage() { ); }; -const isWindowsDesktop = environment.isDesktop && environment.isWindows; - const Placeholder = ({ collection }: { collection: Collection }) => { const workspace = useService(Workspace); const collectionService = useService(CollectionService); @@ -139,199 +133,191 @@ const Placeholder = ({ collection }: { collection: Collection }) => { localStorage.setItem('hide-empty-collection-help-info', 'true'); }, []); const t = useAFFiNEI18N(); - const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); const handleJumpToCollections = useCallback(() => { jumpToCollections(workspace.id); }, [jumpToCollections, workspace]); return ( -
-
- + <> +
- - {t['com.affine.collection.allCollections']()} -
/
-
-
- {collection.name} -
-
- {isWindowsDesktop && } -
-
-
- -
- {t['com.affine.collection.emptyCollection']()} -
-
- {t['com.affine.collection.emptyCollectionDescription']()} -
+ + {t['com.affine.collection.allCollections']()} +
/
+
+
-
- - - {t['com.affine.collection.addPages']()} - -
-
- - - {t['com.affine.collection.addRules']()} - -
+ {collection.name}
+
- {showTips ? ( + + +
+
+ {t['com.affine.collection.emptyCollection']()} +
+
-
{t['com.affine.collection.helpInfo']()}
- + {t['com.affine.collection.emptyCollectionDescription']()}
-
- - Add pages: You can - freely select pages and add them to the collection. - +
+ + + {t['com.affine.collection.addPages']()} +
-
- - Add rules: Rules are - based on filtering. After adding rules, pages that meet the - requirements will be automatically added to the current - collection. - +
+ + + {t['com.affine.collection.addRules']()} +
- ) : null} -
+ {showTips ? ( +
+
+
{t['com.affine.collection.helpInfo']()}
+ +
+
+
+ + Add pages: You can + freely select pages and add them to the collection. + +
+
+ + Add rules: Rules + are based on filtering. After adding rules, pages that meet + the requirements will be automatically added to the current + collection. + +
+
+
+ ) : null} +
+ {node} -
+ ); }; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts index 7f19b7ac28..98974cc607 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts @@ -1,59 +1,16 @@ -import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; export const header = style({ display: 'flex', - height: '52px', + height: '100%', width: '100%', alignItems: 'center', - flexShrink: 0, - background: cssVar('backgroundPrimaryColor'), - borderBottom: `1px solid ${cssVar('borderColor')}`, - selectors: { - '&[data-sidebar-floating="false"]': { - ['WebkitAppRegion' as string]: 'drag', - }, - '&:has([data-popper-placement])': { - ['WebkitAppRegion' as string]: 'no-drag', - }, - }, - '@media': { - print: { - display: 'none', - }, - }, -}); -export const mainHeader = style([ - header, - { - padding: '0 16px', - gap: 12, - }, -]); -export const sidebarHeader = style([ - header, - { - padding: '0 16px', - gap: '12px', - }, -]); -export const mainHeaderRight = style({ - display: 'flex', - alignItems: 'center', - gap: '8px', + gap: 12, }); export const spacer = style({ flexGrow: 1, minWidth: 12, }); -export const standaloneExtensionSwitcherWrapper = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexShrink: 0, - height: '52px', - position: 'relative', -}); export const journalWeekPicker = style({ minWidth: 100, flexGrow: 1, diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx index 1e4b543a53..2d5fb3b517 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx @@ -1,85 +1,20 @@ import type { InlineEditHandle } from '@affine/component'; -import { IconButton } from '@affine/component'; -import { - appSidebarFloatingAtom, - appSidebarOpenAtom, - SidebarSwitch, -} from '@affine/component/app-sidebar'; +import { appSidebarFloatingAtom } from '@affine/component/app-sidebar'; import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite'; import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker'; import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button'; import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; -import { RightSidebarIcon } from '@blocksuite/icons'; import type { Doc } from '@blocksuite/store'; import type { Workspace } from '@toeverything/infra'; -import { useAtomValue, useSetAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; import { useCallback, useRef } from 'react'; import { SharePageButton } from '../../../components/affine/share-page-modal'; import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index'; import { HeaderDivider } from '../../../components/pure/header'; -import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls'; import * as styles from './detail-page-header.css'; -import { ExtensionTabs } from './editor-sidebar'; -import { - editorSidebarOpenAtom, - editorSidebarToggleAtom, -} from './editor-sidebar/atoms'; - -interface PageHeaderRightProps { - showSidebarSwitch?: boolean; -} - -const ToggleSidebarButton = () => { - const toggle = useSetAtom(editorSidebarToggleAtom); - return ( - - - - ); -}; -const isWindowsDesktop = environment.isDesktop && environment.isWindows; - -const WindowsMainPageHeaderRight = ({ - showSidebarSwitch, -}: PageHeaderRightProps) => { - const editorSidebarOpen = useAtomValue(editorSidebarOpenAtom); - - if (editorSidebarOpen) { - return null; - } - - return ( - <> - -
- {showSidebarSwitch ? : null} - -
- - ); -}; - -const NonWindowsMainPageHeaderRight = ({ - showSidebarSwitch, -}: PageHeaderRightProps) => { - const editorSidebarOpen = useAtomValue(editorSidebarOpenAtom); - - if (editorSidebarOpen || !showSidebarSwitch) { - return null; - } - - return ( - <> - -
- -
- - ); -}; function Header({ children, @@ -106,21 +41,10 @@ function Header({ interface PageHeaderProps { page: Doc; workspace: Workspace; - showSidebarSwitch?: boolean; } -const RightHeader = isWindowsDesktop - ? WindowsMainPageHeaderRight - : NonWindowsMainPageHeaderRight; -export function JournalPageHeader({ - page, - workspace, - showSidebarSwitch = true, -}: PageHeaderProps) { - const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); +export function JournalPageHeader({ page, workspace }: PageHeaderProps) { return ( -
- - {!leftSidebarOpen ? : null} +
) : null} -
); } -export function NormalPageHeader({ - page, - workspace, - showSidebarSwitch = true, -}: PageHeaderProps) { +export function NormalPageHeader({ page, workspace }: PageHeaderProps) { const titleInputHandleRef = useRef(null); - const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); const onRename = useCallback(() => { setTimeout(() => titleInputHandleRef.current?.triggerEdit()); }, []); return ( -
- - {!leftSidebarOpen ? : null} +
{page ? : null} -
); } @@ -186,36 +101,3 @@ export function DetailPageHeader(props: PageHeaderProps) { ); } - -interface SidebarHeaderProps { - workspace: Workspace; - page: Doc; -} -function WindowsSidebarHeader(props: SidebarHeaderProps) { - return ( - <> -
-
- - -
-
- -
- - ); -} - -function NonWindowsSidebarHeader(props: SidebarHeaderProps) { - return ( -
- -
- -
- ); -} - -export const RightSidebarHeader = isWindowsDesktop - ? WindowsSidebarHeader - : NonWindowsSidebarHeader; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts index 1d085e0df3..e6b996897c 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts @@ -1,26 +1,14 @@ import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; -export const root = style({ - display: 'flex', - height: '100%', - overflow: 'hidden', - width: '100%', -}); + export const mainContainer = style({ display: 'flex', - flex: 1, - height: '100%', - position: 'relative', flexDirection: 'column', - minWidth: 0, + flex: 1, overflow: 'hidden', - background: cssVar('backgroundPrimaryColor'), - selectors: { - [`${root}[data-client-border=true] &`]: { - borderRadius: '4px', - }, - }, + borderTop: `1px solid ${cssVar('borderColor')}`, }); + export const editorContainer = style({ position: 'relative', display: 'flex', @@ -28,29 +16,3 @@ export const editorContainer = style({ flex: 1, zIndex: 0, }); -export const sidebarContainer = style({ - display: 'flex', - flexShrink: 0, - height: '100%', - selectors: { - [`${root}[data-client-border=true] &`]: { - paddingLeft: 9, - }, - [`${root}[data-client-border=false] &`]: { - borderLeft: `1px solid ${cssVar('borderColor')}`, - }, - }, -}); -export const sidebarContainerInner = style({ - display: 'flex', - background: cssVar('backgroundPrimaryColor'), - flexDirection: 'column', - overflow: 'hidden', - height: '100%', - width: '100%', - selectors: { - [`${root}[data-client-border=true] &`]: { - borderRadius: '4px', - }, - }, -}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 97acb4aed2..523c9b7c2b 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -1,6 +1,5 @@ import { Scrollable } from '@affine/component'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; -import { ResizePanel } from '@affine/component/resize-panel'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { BookmarkService, @@ -20,13 +19,12 @@ import { ServiceProviderContext, useLiveData, } from '@toeverything/infra'; -import { appSettingAtom, Workspace } from '@toeverything/infra'; +import { Workspace } from '@toeverything/infra'; import { useService } from '@toeverything/infra'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useSetAtom } from 'jotai'; import { memo, type ReactElement, - type ReactNode, useCallback, useEffect, useMemo, @@ -37,84 +35,55 @@ import type { Map as YMap } from 'yjs'; import { recentPageIdsBaseAtom } from '../../../atoms'; import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; -import { HubIsland } from '../../../components/affine/hub-island'; import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; import { ImagePreviewModal } from '../../../components/image-preview'; import { PageDetailEditor } from '../../../components/page-detail-editor'; import { TrashPageFooter } from '../../../components/pure/trash-page-footer'; import { TopTip } from '../../../components/top-tip'; import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; +import { useActiveBlocksuiteEditor } from '../../../hooks/use-block-suite-editor'; import { usePageDocumentTitle } from '../../../hooks/use-global-state'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; +import { + MultiTabSidebarBody, + MultiTabSidebarHeaderSwitcher, + type SidebarTabName, +} from '../../../modules/multi-tab-sidebar'; +import { sidebarTabs } from '../../../modules/multi-tab-sidebar'; +import { RightSidebarViewIsland } from '../../../modules/right-sidebar'; +import { + useIsActiveView, + ViewBodyIsland, + ViewHeaderIsland, +} from '../../../modules/workbench'; import { performanceRenderLogger } from '../../../shared'; import { PageNotFound } from '../../404'; import * as styles from './detail-page.css'; -import { DetailPageHeader, RightSidebarHeader } from './detail-page-header'; -import { - EditorSidebar, - editorSidebarOpenAtom, - editorSidebarResizingAtom, - editorSidebarWidthAtom, -} from './editor-sidebar'; - -interface DetailPageLayoutProps { - main: ReactNode; - header: ReactNode; - footer: ReactNode; - sidebar: ReactNode; -} - -const MIN_SIDEBAR_WIDTH = 320; -const MAX_SIDEBAR_WIDTH = 800; - -// todo: consider move to a shared place if we also want to reuse the layout for other routes -const DetailPageLayout = ({ - main, - header, - footer, - sidebar, -}: DetailPageLayoutProps): ReactElement => { - const [width, setWidth] = useAtom(editorSidebarWidthAtom); - const { clientBorder } = useAtomValue(appSettingAtom); - const [resizing, setResizing] = useAtom(editorSidebarResizingAtom); - const [open, setOpen] = useAtom(editorSidebarOpenAtom); - - return ( -
-
- {header} - {main} - {footer} -
- {sidebar ? ( - - {sidebar} - - ) : null} -
- ); -}; +import { DetailPageHeader } from './detail-page-header'; const DetailPageImpl = memo(function DetailPageImpl() { const page = useService(Doc); const pageRecordList = useService(PageRecordList); const currentPageId = page.id; const { openPage, jumpToTag } = useNavigateHelper(); + const [editor, setEditor] = useState(null); const currentWorkspace = useService(Workspace); const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; + const isActiveView = useIsActiveView(); + // TODO: remove jotai here + const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor(); + + useEffect(() => { + if (isActiveView) { + setActiveBlockSuiteEditor(editor); + } + }, [editor, isActiveView, setActiveBlockSuiteEditor]); + + const [activeTabName, setActiveTabName] = useState( + null + ); + const pageMeta = useBlockSuiteDocMeta(blockSuiteWorkspace).find( meta => meta.id === page.id ); @@ -184,6 +153,9 @@ const DetailPageImpl = memo(function DetailPageImpl() { const disposeTagClick = editor.slots.tagClicked.on(({ tagId }) => { jumpToTag(currentWorkspace.id, tagId); }); + + setEditor(editor); + return () => { dispose.dispose(); disposeTagClick.dispose(); @@ -200,24 +172,23 @@ const DetailPageImpl = memo(function DetailPageImpl() { ] ); + const isWindowsDesktop = environment.isDesktop && environment.isWindows; + return ( <> - - - - - } - main={ - // Add a key to force rerender when page changed, to avoid error boundary persisting. + + + + +
+ {/* Add a key to force rerender when page changed, to avoid error boundary persisting. */} + - - } - footer={isInTrash ? : null} - sidebar={ - !isInTrash ? ( -
- - -
+ {isInTrash ? : null} +
+
+ + ) : null } + body={ + ext.name === activeTabName) ?? + sidebarTabs[0] + } + > + {/* Show switcher in body for windows desktop */} + {isWindowsDesktop && ( + + )} + + } /> + ({ - isOpen: false, - resizing: false, - width: 300, // todo: should be resizable - activeExtension: extensions[0], - extensions: extensions, // todo: maybe should be dynamic (by feature flag?) -}); - -const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen); -const resizingAtom = selectAtom(baseStateAtom, state => state.resizing); -const activeExtensionAtom = selectAtom( - baseStateAtom, - state => state.activeExtension -); -const widthAtom = selectAtom(baseStateAtom, state => state.width); - -export const editorExtensionsAtom = selectAtom( - baseStateAtom, - state => state.extensions, - isEqual -); - -// get/set sidebar open state -export const editorSidebarOpenAtom = atom( - get => get(isOpenAtom), - (_, set, isOpen: boolean) => { - set(baseStateAtom, prev => { - return { ...prev, isOpen }; - }); - } -); - -// get/set sidebar resizing state -export const editorSidebarResizingAtom = atom( - get => get(resizingAtom), - (_, set, resizing: boolean) => { - set(baseStateAtom, prev => { - return { ...prev, resizing }; - }); - } -); - -// get/set active extension -export const editorSidebarActiveExtensionAtom = atom( - get => get(activeExtensionAtom), - (_, set, extension: EditorExtensionName) => { - set(baseStateAtom, prev => { - const extensions = prev.extensions; - const newExtension = extensions.find(e => e.name === extension); - assertExists(newExtension, `extension ${extension} not found`); - return { ...prev, activeExtension: newExtension }; - }); - } -); - -// toggle sidebar (write only) -export const editorSidebarToggleAtom = atom(null, (_, set) => { - set(baseStateAtom, prev => { - return { ...prev, isOpen: !prev.isOpen }; - }); -}); - -// get/set sidebar width -export const editorSidebarWidthAtom = atom( - get => get(widthAtom), - (_, set, width: number) => { - set(baseStateAtom, prev => { - return { ...prev, width }; - }); - } -); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx deleted file mode 100644 index e14da26d7c..0000000000 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useAtomValue } from 'jotai'; - -import type { EditorExtensionProps } from '.'; -import { editorSidebarActiveExtensionAtom } from './atoms'; -import * as styles from './editor-sidebar.css'; - -export const EditorSidebar = (props: EditorExtensionProps) => { - const activeExtension = useAtomValue(editorSidebarActiveExtensionAtom); - const Component = activeExtension?.Component; - - return ( -
- {Component ? : null} -
- ); -}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx deleted file mode 100644 index 90334838cf..0000000000 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { IconButton } from '@affine/component'; -import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; -import { useWorkspaceEnabledFeatures } from '@affine/core/hooks/use-workspace-features'; -import { FeatureType } from '@affine/graphql'; -import type { Doc } from '@blocksuite/store'; -import type { Workspace } from '@toeverything/infra'; -import { assignInlineVars } from '@vanilla-extract/dynamic'; -import { useAtom, useAtomValue } from 'jotai'; -import { useEffect } from 'react'; - -import { - editorExtensionsAtom, - editorSidebarActiveExtensionAtom, -} from '../atoms'; -import * as styles from './extensions.css'; - -export interface ExtensionTabsProps { - workspace: Workspace; - page: Doc; -} - -// provide a switcher for active extensions -// will be used in global top header (MacOS) or sidebar (Windows) -export const ExtensionTabs = ({ page, workspace }: ExtensionTabsProps) => { - // todo: filter in editorExtensionsAtom instead? - const copilotEnabled = useWorkspaceEnabledFeatures(workspace.meta).includes( - FeatureType.Copilot - ); - - const { isJournal } = useJournalInfoHelper(page.workspace, page.id); - - const exts = useAtomValue(editorExtensionsAtom).filter(ext => { - if (ext.name === 'copilot' && !copilotEnabled) return false; - return true; - }); - const [selected, setSelected] = useAtom(editorSidebarActiveExtensionAtom); - - // if journal is active, set selected to journal - useEffect(() => { - isJournal && setSelected('journal'); - }, [isJournal, setSelected]); - - const vars = assignInlineVars({ - [styles.activeIdx]: String( - exts.findIndex(ext => ext.name === selected?.name) ?? 0 - ), - }); - useEffect(() => { - if (!selected || !exts.some(e => selected.name === e.name)) { - setSelected(exts[0].name); - } - }, [exts, selected, setSelected]); - return ( -
- {exts.map(extension => { - return ( - setSelected(extension.name)} - key={extension.name} - data-active={selected?.name === extension.name} - className={styles.button} - > - {extension.icon} - - ); - })} -
- ); -}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx deleted file mode 100644 index 4509f5ccb0..0000000000 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './extensions'; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts deleted file mode 100644 index 3ee1e3b1b2..0000000000 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './atoms'; -export * from './editor-sidebar'; -export * from './extensions'; -export * from './types'; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts deleted file mode 100644 index 004040f30e..0000000000 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { BlockSuiteWorkspace } from '@affine/core/shared'; -import type { Doc } from '@blocksuite/store'; - -export type EditorExtensionName = 'outline' | 'frame' | 'copilot' | 'journal'; - -export interface EditorExtensionProps { - workspace: BlockSuiteWorkspace; - page: Doc; -} - -export interface EditorExtension { - name: EditorExtensionName; - icon: React.ReactNode; - Component: React.ComponentType; -} diff --git a/packages/frontend/core/src/pages/workspace/index.tsx b/packages/frontend/core/src/pages/workspace/index.tsx index b596aa3a6d..29a72357f0 100644 --- a/packages/frontend/core/src/pages/workspace/index.tsx +++ b/packages/frontend/core/src/pages/workspace/index.tsx @@ -21,8 +21,10 @@ import { import { useParams } from 'react-router-dom'; import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary'; +import { HubIsland } from '../../components/affine/hub-island'; import { WorkspaceLayout } from '../../layouts/workspace-layout'; -import { WorkbenchRoot } from '../../modules/workbench/workbench-root'; +import { RightSidebarContainer } from '../../modules/right-sidebar'; +import { WorkbenchRoot } from '../../modules/workbench'; import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace'; import { performanceRenderLogger } from '../../shared'; import { PageNotFound } from '../404'; @@ -115,6 +117,8 @@ export const Component = (): ReactElement => { + + diff --git a/packages/frontend/core/src/pages/workspace/page-list-empty.tsx b/packages/frontend/core/src/pages/workspace/page-list-empty.tsx index 4aa24cb3e9..8dcb1a8b7e 100644 --- a/packages/frontend/core/src/pages/workspace/page-list-empty.tsx +++ b/packages/frontend/core/src/pages/workspace/page-list-empty.tsx @@ -76,6 +76,7 @@ export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => {
); }; + export const EmptyTagList = ({ heading }: { heading: ReactNode }) => { const t = useAFFiNEI18N(); return ( diff --git a/packages/frontend/core/src/pages/workspace/tag/header.tsx b/packages/frontend/core/src/pages/workspace/tag/header.tsx index 608001612d..d46818a636 100644 --- a/packages/frontend/core/src/pages/workspace/tag/header.tsx +++ b/packages/frontend/core/src/pages/workspace/tag/header.tsx @@ -1,23 +1,6 @@ import { Header } from '@affine/core/components/pure/header'; -import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; -import * as styles from '../all-page/all-page.css'; - export const TagDetailHeader = () => { - const isWindowsDesktop = environment.isDesktop && environment.isWindows; - - return ( -
- {isWindowsDesktop ? : null} -
- } - center={} - /> - ); + return
} />; }; diff --git a/packages/frontend/core/src/pages/workspace/tag/index.tsx b/packages/frontend/core/src/pages/workspace/tag/index.tsx index 563f398689..82e1eab740 100644 --- a/packages/frontend/core/src/pages/workspace/tag/index.tsx +++ b/packages/frontend/core/src/pages/workspace/tag/index.tsx @@ -1,4 +1,3 @@ -import { HubIsland } from '@affine/core/components/affine/hub-island'; import { PageListHeader, useTagMetas, @@ -11,7 +10,6 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { PageNotFound } from '../../404'; -import * as styles from '../all-page/all-page.css'; import { EmptyPageList } from '../page-list-empty'; import { TagDetailHeader } from './header'; @@ -40,7 +38,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => { } return ( -
+ <> {tagPageMetas.length > 0 ? ( @@ -51,8 +49,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => { blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} /> )} - -
+ ); }; diff --git a/packages/frontend/core/src/pages/workspace/trash-page.css.ts b/packages/frontend/core/src/pages/workspace/trash-page.css.ts index 0b6e1bcb77..c3e448d09e 100644 --- a/packages/frontend/core/src/pages/workspace/trash-page.css.ts +++ b/packages/frontend/core/src/pages/workspace/trash-page.css.ts @@ -1,6 +1,5 @@ import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; -export { root } from './all-page/all-page.css'; export const trashTitle = style({ display: 'flex', alignItems: 'center', @@ -8,6 +7,13 @@ export const trashTitle = style({ padding: '0 8px', fontWeight: 600, }); +export const body = style({ + display: 'flex', + flexDirection: 'column', + flex: 1, + height: '100%', + width: '100%', +}); export const trashIcon = style({ color: cssVar('iconColor'), fontSize: cssVar('fontH5'), diff --git a/packages/frontend/core/src/pages/workspace/trash-page.tsx b/packages/frontend/core/src/pages/workspace/trash-page.tsx index 0f3e0b141a..36797baf76 100644 --- a/packages/frontend/core/src/pages/workspace/trash-page.tsx +++ b/packages/frontend/core/src/pages/workspace/trash-page.tsx @@ -21,6 +21,7 @@ import { Workspace } from '@toeverything/infra'; import { useService } from '@toeverything/infra/di'; import { useCallback } from 'react'; +import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench'; import { EmptyPageList } from './page-list-empty'; import * as styles from './trash-page.css'; @@ -94,26 +95,32 @@ export const TrashPage = () => { return ; }, []); return ( -
- - {filteredPageMetas.length > 0 ? ( - - ) : ( - - )} -
+ <> + + + + +
+ {filteredPageMetas.length > 0 ? ( + + ) : ( + + )} +
+
+ ); }; diff --git a/packages/frontend/core/src/utils/island.tsx b/packages/frontend/core/src/utils/island.tsx new file mode 100644 index 0000000000..e66052b1b1 --- /dev/null +++ b/packages/frontend/core/src/utils/island.tsx @@ -0,0 +1,41 @@ +import { LiveData, useLiveData } from '@toeverything/infra/livedata'; +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; + +export const createIsland = () => { + const targetLiveData = new LiveData(null); + let mounted = false; + let provided = false; + return { + Target: ({ ...other }: React.HTMLProps) => { + const target = useRef(null); + useEffect(() => { + if (mounted === true) { + throw new Error('Island should not be mounted more than once'); + } + mounted = true; + targetLiveData.next(target.current); + return () => { + mounted = false; + targetLiveData.next(null); + }; + }, []); + return
; + }, + Provider: ({ children }: React.PropsWithChildren) => { + const target = useLiveData(targetLiveData); + useEffect(() => { + if (provided === true && process.env.NODE_ENV !== 'production') { + throw new Error('Island should not be provided more than once'); + } + provided = true; + return () => { + provided = false; + }; + }, []); + return target ? createPortal(children, target) : null; + }, + }; +}; + +export type Island = ReturnType; diff --git a/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/meta.json b/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/meta.json deleted file mode 100644 index 3b79ffde9e..0000000000 --- a/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": "709a97a0-aa01-4d0b-b29a-f3ed2f63033c", - "mainDBPath": "/Users/eyhn/AFFiNE/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/storage.db" -} diff --git a/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/storage.db b/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/storage.db deleted file mode 100644 index 0c76fa0aca8bf9d3b93aa0e45971334808c41c36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI&Z)?*)90%~b^`8dX?peel9)-cmHtD(zaUz(^D8(eL*%j=gkgit0hk@${lW?_NK5-0<8;onZX|U$%P?K#djAcE{9$32J zFuTWP(=>PGQ0&iyTzz)Vc-Jycxn}j*3*)6WFgZIJIF4a+_t(hJnM!N%uuxG|m0mqa zLmN%QQIuYa`C!t%WfbqkN5+k}ob{cqr8z_P+8E|y8GEtEbhD@DdjB@UROOyx<|_2zMy-n(gB?tWU8=`=#`bOIX{;bujF>Ea%ltJ>)LXjB{wG7 zzkg|#t{0xU^0$+HQd(7MLhE0M{GdPp0uX=z1Rwwb2tWV=5P$##AaIKX?$Z+4Jdg== z+MUC__WoY`h_w%oj~*X)o-EJ**Q9=ZixWiX5P$##AOHafKmY;|fB*y_009X6CZNzl z<8JmULUR6>Fa4uH00Izz00bZa0SG_<0uX=z1R$`+0!oq_&i`w?!$=JR5P$##AOHaf nKmY;|fB*y_KnVW-$5Q|S2tWV=5P$##AOHafKmY;|Sbu>Z5y94z