diff --git a/packages/frontend/component/src/components/resize-panel/resize-panel.tsx b/packages/frontend/component/src/components/resize-panel/resize-panel.tsx index a24cd16700..5c9d38b487 100644 --- a/packages/frontend/component/src/components/resize-panel/resize-panel.tsx +++ b/packages/frontend/component/src/components/resize-panel/resize-panel.tsx @@ -39,6 +39,7 @@ export interface ResizePanelProps resizeHandleVerticalPadding?: number; enableAnimation?: boolean; width: number; + unmountOnExit?: boolean; onOpen: (open: boolean) => void; onResizing: (resizing: boolean) => void; onWidthChange: (width: number) => void; @@ -149,6 +150,7 @@ export const ResizePanel = forwardRef( floating, enableAnimation: _enableAnimation = true, open, + unmountOnExit, onOpen, onResizing, onWidthChange, @@ -182,7 +184,7 @@ export const ResizePanel = forwardRef( data-handle-position={resizeHandlePos} data-enable-animation={enableAnimation && !resizing} > - {status !== 'exited' && children} + {!(status === 'exited' && unmountOnExit !== false) && children} { - const chatCards: ChatCards | null = await new Promise(resolve => - requestAnimationFrame(() => - resolve(chatPanel.querySelector('chat-cards')) - ) - ); - if (!chatCards) return; - if (chatCards.temporaryParams) return; - chatCards.temporaryParams = params; - }; - } - private static readonly instance = new AIProvider(); static LAST_ACTION_SESSIONID = ''; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 1f2f4aafbd..b7b633b938 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -52,16 +52,18 @@ type PageMenuProps = { rename?: () => void; page: Doc; isJournal?: boolean; + containerWidth: number; }; // fixme: refactor this file export const PageHeaderMenuButton = ({ rename, page, isJournal, + containerWidth, }: PageMenuProps) => { const pageId = page?.id; const t = useI18n(); - const { hideShare } = useDetailPageHeaderResponsive(); + const { hideShare } = useDetailPageHeaderResponsive(containerWidth); const confirmEnableCloud = useEnableCloud(); const workspace = useService(WorkspaceService).workspace; diff --git a/packages/frontend/core/src/components/pure/ai-island/index.tsx b/packages/frontend/core/src/components/pure/ai-island/index.tsx index daf449d0df..bd5092e859 100644 --- a/packages/frontend/core/src/components/pure/ai-island/index.tsx +++ b/packages/frontend/core/src/components/pure/ai-island/index.tsx @@ -1,5 +1,10 @@ -import { RightSidebarService } from '@affine/core/modules/right-sidebar'; -import { useLiveData, useService } from '@toeverything/infra'; +import { WorkbenchService } from '@affine/core/modules/workbench'; +import { + GlobalStateService, + LiveData, + useLiveData, + useService, +} from '@toeverything/infra'; import { useEffect, useState } from 'react'; import { ToolContainer } from '../../workspace'; @@ -11,18 +16,37 @@ import { gradient, } from './styles.css'; +const RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY = + 'app:settings:rightsidebar:ai:has-ever-opened'; + export const AIIsland = () => { // to make sure ai island is hidden first and animate in const [hide, setHide] = useState(true); - const rightSidebar = useService(RightSidebarService).rightSidebar; - const activeTabName = useLiveData(rightSidebar.activeTabName$); - const rightSidebarOpen = useLiveData(rightSidebar.isOpen$); - const aiChatHasEverOpened = useLiveData(rightSidebar.aiChatHasEverOpened$); + const workbench = useService(WorkbenchService).workbench; + const activeView = useLiveData(workbench.activeView$); + const haveChatTab = useLiveData( + activeView.sidebarTabs$.map(tabs => tabs.some(t => t.id === 'chat')) + ); + const activeTab = useLiveData(activeView.activeSidebarTab$); + const sidebarOpen = useLiveData(workbench.sidebarOpen$); + const globalState = useService(GlobalStateService).globalState; + const aiChatHasEverOpened = useLiveData( + LiveData.from( + globalState.watch(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY), + false + ) + ); useEffect(() => { - setHide(rightSidebarOpen && activeTabName === 'chat'); - }, [activeTabName, rightSidebarOpen]); + if (sidebarOpen && activeTab?.id === 'chat') { + globalState.set(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY, true); + } + }, [activeTab, globalState, sidebarOpen]); + + useEffect(() => { + setHide((sidebarOpen && activeTab?.id === 'chat') || !haveChatTab); + }, [activeTab, haveChatTab, sidebarOpen]); return ( @@ -43,8 +67,8 @@ export const AIIsland = () => { data-testid="ai-island" onClick={() => { if (hide) return; - rightSidebar.open(); - rightSidebar.setActiveTabName('chat'); + workbench.openSidebar(); + activeView.activeSidebarTab('chat'); }} > diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index a589be9093..e9ffd59fa8 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -47,6 +47,7 @@ import { SyncAwareness } from '../components/affine/awareness'; import { appSidebarResizingAtom } from '../components/app-sidebar'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; import type { DraggableTitleCellData } from '../components/page-list'; +import { AIIsland } from '../components/pure/ai-island'; import { RootAppSidebar } from '../components/root-app-sidebar'; import { MainContainer } from '../components/workspace'; import { WorkspaceUpgrade } from '../components/workspace-upgrade'; @@ -82,6 +83,7 @@ export const WorkspaceLayout = function WorkspaceLayout({ {children} {/* should show after workspace loaded */} + ); }; diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index d4533ab0e2..2bdf0651a4 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -11,7 +11,6 @@ import { configurePeekViewModule } from './peek-view'; import { configurePermissionsModule } from './permissions'; import { configureWorkspacePropertiesModule } from './properties'; import { configureQuickSearchModule } from './quicksearch'; -import { configureRightSidebarModule } from './right-sidebar'; import { configureShareDocsModule } from './share-doc'; import { configureStorageImpls } from './storage'; import { configureTagModule } from './tag'; @@ -22,7 +21,6 @@ export function configureCommonModules(framework: Framework) { configureInfraModules(framework); configureCollectionModule(framework); configureNavigationModule(framework); - configureRightSidebarModule(framework); configureTagModule(framework); configureWorkbenchModule(framework); configureWorkspacePropertiesModule(framework); diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/index.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/index.ts deleted file mode 100644 index f5f66a1421..0000000000 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type { SidebarTabName } from './multi-tabs/sidebar-tab'; -export { sidebarTabs, type TabOnLoadFn } from './multi-tabs/sidebar-tabs'; -export { MultiTabSidebarBody } from './view/body'; -export { MultiTabSidebarHeaderSwitcher } from './view/header-switcher'; diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts deleted file mode 100644 index 284ba74598..0000000000 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AffineEditorContainer } from '@blocksuite/presets'; - -export type SidebarTabName = 'outline' | 'frame' | 'chat' | 'journal'; - -export interface SidebarTabProps { - editor: AffineEditorContainer | null; - onLoad: ((component: HTMLElement) => void) | null; -} - -export interface SidebarTab { - name: SidebarTabName; - icon: React.ReactNode; - Component: React.ComponentType; -} diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tabs.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tabs.ts deleted file mode 100644 index 1bf84cdc7a..0000000000 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tabs.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { SidebarTab } from './sidebar-tab'; -import { chatTab } from './tabs/chat'; -import { framePanelTab } from './tabs/frame'; -import { journalTab } from './tabs/journal'; -import { outlineTab } from './tabs/outline'; - -export type TabOnLoadFn = (component: HTMLElement) => void; - -// the list of all possible tabs in affine. -// order matters (determines the order of the tabs) -export const sidebarTabs: SidebarTab[] = [ - chatTab, - journalTab, - outlineTab, - framePanelTab, -]; 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 deleted file mode 100644 index 25a55d7792..0000000000 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.css.ts +++ /dev/null @@ -1,12 +0,0 @@ -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%', - 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 deleted file mode 100644 index d1f7072b10..0000000000 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/view/body.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { PropsWithChildren } from 'react'; - -import type { SidebarTab, SidebarTabProps } from '../multi-tabs/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/modules/multi-tab-sidebar/view/header-switcher.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.tsx deleted file mode 100644 index 4a55d127fe..0000000000 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/view/header-switcher.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { RadioItem } from '@affine/component'; -import { RadioGroup } from '@affine/component'; -import { cssVar } from '@toeverything/theme'; -import { useMemo } from 'react'; - -import type { SidebarTab, SidebarTabName } from '../multi-tabs/sidebar-tab'; - -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 tabItems = useMemo(() => { - return tabs.map(extension => { - return { - value: extension.name, - label: extension.icon, - style: { padding: 0, fontSize: 20, width: 24 }, - } satisfies RadioItem; - }); - }, [tabs]); - - return ( - - ); -}; 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 deleted file mode 100644 index 507fd62f97..0000000000 --- a/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar-view.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Entity } from '@toeverything/infra'; - -import { createIsland } from '../../../utils/island'; - -export class RightSidebarView extends Entity { - 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 deleted file mode 100644 index 62ffcf63ba..0000000000 --- a/packages/frontend/core/src/modules/right-sidebar/entities/right-sidebar.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { GlobalState } from '@toeverything/infra'; -import { Entity, LiveData } from '@toeverything/infra'; -import { combineLatest } from 'rxjs'; - -import type { SidebarTabName } from '../../multi-tab-sidebar'; -import { RightSidebarView } from './right-sidebar-view'; - -const RIGHT_SIDEBAR_KEY = 'app:settings:rightsidebar'; -const RIGHT_SIDEBAR_TABS_ACTIVE_KEY = 'app:settings:rightsidebar:tabs:active'; -const RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY = - 'app:settings:rightsidebar:ai:has-ever-opened'; - -export class RightSidebar extends Entity { - _disposables: Array<() => void> = []; - constructor(private readonly globalState: GlobalState) { - super(); - - const sub = combineLatest([this.activeTabName$, this.isOpen$]).subscribe( - ([name, open]) => { - if (name === 'chat' && open) { - this.globalState.set(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY, true); - } - } - ); - this._disposables.push(() => sub.unsubscribe()); - } - - readonly isOpen$ = LiveData.from( - this.globalState.watch(RIGHT_SIDEBAR_KEY), - false - ).map(Boolean); - readonly views$ = new LiveData([]); - readonly front$ = this.views$.map( - stack => stack[0] as RightSidebarView | undefined - ); - readonly hasViews$ = this.views$.map(stack => stack.length > 0); - readonly activeTabName$ = LiveData.from( - this.globalState.watch(RIGHT_SIDEBAR_TABS_ACTIVE_KEY), - null - ); - - /** To determine if AI chat has ever been opened, used to show the animation for the first time */ - readonly aiChatHasEverOpened$ = LiveData.from( - this.globalState.watch(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY), - false - ); - - override dispose() { - super.dispose(); - this._disposables.forEach(dispose => dispose()); - } - - setActiveTabName(name: SidebarTabName) { - this.globalState.set(RIGHT_SIDEBAR_TABS_ACTIVE_KEY, name); - } - - open() { - this._set(true); - } - - toggle() { - this._set(!this.isOpen$.value); - } - - close() { - this._set(false); - } - - _set(value: boolean) { - this.globalState.set(RIGHT_SIDEBAR_KEY, value); - } - - /** - * @private use `RightSidebarViewIsland` instead - */ - _append() { - const view = this.framework.createEntity(RightSidebarView); - this.views$.next([...this.views$.value, view]); - return 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 deleted file mode 100644 index dbd8e120c7..0000000000 --- a/packages/frontend/core/src/modules/right-sidebar/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -export { RightSidebar } from './entities/right-sidebar'; -export { RightSidebarService } from './services/right-sidebar'; -export { RightSidebarContainer } from './view/container'; -export { RightSidebarViewIsland } from './view/view-island'; - -import { - type Framework, - GlobalState, - WorkspaceScope, -} from '@toeverything/infra'; - -import { RightSidebar } from './entities/right-sidebar'; -import { RightSidebarView } from './entities/right-sidebar-view'; -import { RightSidebarService } from './services/right-sidebar'; - -export function configureRightSidebarModule(services: Framework) { - services - .scope(WorkspaceScope) - .service(RightSidebarService) - .entity(RightSidebar, [GlobalState]) - .entity(RightSidebarView); -} diff --git a/packages/frontend/core/src/modules/right-sidebar/services/right-sidebar.ts b/packages/frontend/core/src/modules/right-sidebar/services/right-sidebar.ts deleted file mode 100644 index 454aaba612..0000000000 --- a/packages/frontend/core/src/modules/right-sidebar/services/right-sidebar.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Service } from '@toeverything/infra'; - -import { RightSidebar } from '../entities/right-sidebar'; - -export class RightSidebarService extends Service { - rightSidebar = this.framework.createEntity(RightSidebar); -} diff --git a/packages/frontend/core/src/modules/right-sidebar/view/container.tsx b/packages/frontend/core/src/modules/right-sidebar/view/container.tsx deleted file mode 100644 index 2900596d5b..0000000000 --- a/packages/frontend/core/src/modules/right-sidebar/view/container.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ResizePanel } from '@affine/component/resize-panel'; -import { rightSidebarWidthAtom } from '@affine/core/atoms'; -import { appSettingAtom, useLiveData, useService } from '@toeverything/infra'; -import { useAtom, useAtomValue } from 'jotai'; -import { useCallback, useEffect, useState } from 'react'; - -import { RightSidebarService } from '../services/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] = useAtom(rightSidebarWidthAtom); - const [resizing, setResizing] = useState(false); - const rightSidebar = useService(RightSidebarService).rightSidebar; - - const frontView = useLiveData(rightSidebar.front$); - const open = useLiveData(rightSidebar.isOpen$) && frontView !== undefined; - const [floating, setFloating] = useState(false); - - useEffect(() => { - const onResize = () => setFloating(!!(window.innerWidth < 768)); - onResize(); - window.addEventListener('resize', onResize); - return () => { - window.removeEventListener('resize', onResize); - }; - }, []); - - 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/view-island.tsx b/packages/frontend/core/src/modules/right-sidebar/view/view-island.tsx deleted file mode 100644 index 9e8ab527b5..0000000000 --- a/packages/frontend/core/src/modules/right-sidebar/view/view-island.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useService } from '@toeverything/infra'; -import { useEffect, useState } from 'react'; - -import type { RightSidebarView } from '../entities/right-sidebar-view'; -import { RightSidebarService } from '../services/right-sidebar'; - -export interface RightSidebarViewProps { - body: JSX.Element; - header?: JSX.Element | null; - name?: string; - active?: boolean; -} - -export const RightSidebarViewIsland = ({ - body, - header, - active, -}: RightSidebarViewProps) => { - const rightSidebar = useService(RightSidebarService).rightSidebar; - - const [view, setView] = useState(null); - - useEffect(() => { - const view = rightSidebar._append(); - setView(view); - return () => { - rightSidebar._remove(view); - setView(null); - }; - }, [rightSidebar]); - - useEffect(() => { - if (active && view) { - rightSidebar._moveToFront(view); - } - }, [active, rightSidebar, view]); - - if (!view) { - return null; - } - - return ( - <> - {header} - {body} - - ); -}; diff --git a/packages/frontend/core/src/modules/workbench/README.md b/packages/frontend/core/src/modules/workbench/README.md new file mode 100644 index 0000000000..5d3abca190 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/README.md @@ -0,0 +1,43 @@ +# Workbench + +``` + ┌─────────────Workbench─────-----──────┐ + | Tab1 | Tab2 | Tab3 - □ x | + │ ┌───────┐ ┌───────┐ ┌───────┐ ┌──────┤ + │ │header │ │header │ │header │ │ │ + │ │ │ │ │ │ │ │ side │ + │ │ │ │ │ │ │ │ bar │ + │ │ view │ │ view │ │ view │ │ │ + │ │ │ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ │ + │ └───────┘ └───────┘ └───────┘ │ │ + └───────────────────────────────┴──────┘ +``` + +`Workbench` is the window manager in affine, including the main area and the right sidebar area. + +`View` is a managed window under the workbench. Each view has its own history(Support go back and forward) and currently URL. +The view renders the content as defined by the router ([here](../../router.tsx)). +Each route can render its own `Header`, `Body`, and several `Sidebar`s by [ViewIsland](./view/view-islands.tsx). + +The `Workbench` manages all Views and decides when to display and close them. +There is always one **active View**, and the URL of the active View is considered the URL of the entire application. + +## Sidebar + +Each `View` can define its `Sidebar`, which will be displayed in the right area of ​​the screen. +If the same view has multiple sidebars, a switcher will be displayed so that users can switch between multiple sidebars. + +> only the sidebar of the currently active view will be displayed. + +## Tab + +WIP + +## Persistence + +When close the application and reopen, the entire workbench should be restored to its previous state. +WIP + +> If running in a browser, the workbench will passing the browser's back and forward navigation to the active view. diff --git a/packages/frontend/core/src/modules/workbench/entities/sidebar-tab.ts b/packages/frontend/core/src/modules/workbench/entities/sidebar-tab.ts new file mode 100644 index 0000000000..90276e422e --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/entities/sidebar-tab.ts @@ -0,0 +1,5 @@ +import { Entity } from '@toeverything/infra'; + +export class SidebarTab extends Entity<{ id: string }> { + readonly id = this.props.id; +} diff --git a/packages/frontend/core/src/modules/workbench/entities/view.ts b/packages/frontend/core/src/modules/workbench/entities/view.ts index 667bc49753..86d10918ac 100644 --- a/packages/frontend/core/src/modules/workbench/entities/view.ts +++ b/packages/frontend/core/src/modules/workbench/entities/view.ts @@ -2,19 +2,36 @@ import { Entity, LiveData } from '@toeverything/infra'; import type { Location, To } from 'history'; import { Observable } from 'rxjs'; -import { createIsland } from '../../../utils/island'; import { createNavigableHistory } from '../../../utils/navigable-history'; -import type { ViewScope } from '../scopes/view'; +import { ViewScope } from '../scopes/view'; +import { SidebarTab } from './sidebar-tab'; -export class View extends Entity { - id = this.scope.props.id; +export class View extends Entity<{ + id: string; + defaultLocation?: To | undefined; +}> { + scope = this.framework.createScope(ViewScope, { + view: this as View, + }); + id = this.props.id; - constructor(public readonly scope: ViewScope) { + sidebarTabs$ = new LiveData([]); + + // _activeTabId may point to a non-existent tab. + // In this case, we still retain the activeTabId data and wait for the non-existent tab to be mounted. + _activeSidebarTabId$ = new LiveData(null); + activeSidebarTab$ = LiveData.computed(get => { + const activeTabId = get(this._activeSidebarTabId$); + const tabs = get(this.sidebarTabs$); + return tabs.length > 0 + ? tabs.find(tab => tab.id === activeTabId) ?? tabs[0] + : null; + }); + + constructor() { super(); this.history = createNavigableHistory({ - initialEntries: [ - this.scope.props.defaultLocation ?? { pathname: '/all' }, - ], + initialEntries: [this.props.defaultLocation ?? { pathname: '/all' }], initialIndex: 0, }); } @@ -45,11 +62,6 @@ export class View extends Entity { ); size$ = new LiveData(100); - /** Width of header content in px (excludes sidebar-toggle/windows button/...) */ - headerContentWidth$ = new LiveData(1920); - - header = createIsland(); - body = createIsland(); push(path: To) { this.history.push(path); @@ -66,4 +78,24 @@ export class View extends Entity { setSize(size?: number) { this.size$.next(size ?? 100); } + + addSidebarTab(id: string) { + this.sidebarTabs$.next([ + ...this.sidebarTabs$.value, + this.scope.createEntity(SidebarTab, { + id, + }), + ]); + return id; + } + + removeSidebarTab(id: string) { + this.sidebarTabs$.next( + this.sidebarTabs$.value.filter(tab => tab.id !== id) + ); + } + + activeSidebarTab(id: string | null) { + this._activeSidebarTabId$.next(id); + } } diff --git a/packages/frontend/core/src/modules/workbench/entities/workbench.ts b/packages/frontend/core/src/modules/workbench/entities/workbench.ts index f03ef4ae64..4593d85506 100644 --- a/packages/frontend/core/src/modules/workbench/entities/workbench.ts +++ b/packages/frontend/core/src/modules/workbench/entities/workbench.ts @@ -2,11 +2,8 @@ import { Unreachable } from '@affine/env/constant'; import { Entity, LiveData } from '@toeverything/infra'; import type { To } from 'history'; import { nanoid } from 'nanoid'; -import { combineLatest, map, switchMap } from 'rxjs'; -import { ViewScope } from '../scopes/view'; -import { ViewService } from '../services/view'; -import type { View } from './view'; +import { View } from './view'; export type WorkbenchPosition = 'beside' | 'active' | 'head' | 'tail' | number; @@ -17,24 +14,20 @@ interface WorkbenchOpenOptions { export class Workbench extends Entity { readonly views$ = new LiveData([ - this.framework.createScope(ViewScope, { id: nanoid() }).get(ViewService) - .view, + this.framework.createEntity(View, { id: nanoid() }), ]); activeViewIndex$ = new LiveData(0); - activeView$ = LiveData.from( - combineLatest([this.views$, this.activeViewIndex$]).pipe( - map(([views, index]) => views[index]) - ), - this.views$.value[this.activeViewIndex$.value] - ); - + activeView$ = LiveData.computed(get => { + const activeIndex = get(this.activeViewIndex$); + const views = get(this.views$); + return views[activeIndex]; + }); basename$ = new LiveData('/'); - - location$ = LiveData.from( - this.activeView$.pipe(switchMap(view => view.location$)), - this.views$.value[this.activeViewIndex$.value].history.location - ); + location$ = LiveData.computed(get => { + return get(get(this.activeView$).location$); + }); + sidebarOpen$ = new LiveData(false); active(index: number) { index = Math.max(0, Math.min(index, this.views$.value.length - 1)); @@ -42,13 +35,28 @@ export class Workbench extends Entity { } createView(at: WorkbenchPosition = 'beside', defaultLocation: To) { - const view = this.framework - .createScope(ViewScope, { id: nanoid(), defaultLocation }) - .get(ViewService).view; + const view = this.framework.createEntity(View, { + id: nanoid(), + defaultLocation, + }); const newViews = [...this.views$.value]; newViews.splice(this.indexAt(at), 0, view); this.views$.next(newViews); - return newViews.indexOf(view); + const index = newViews.indexOf(view); + this.active(index); + return index; + } + + openSidebar() { + this.sidebarOpen$.next(true); + } + + closeSidebar() { + this.sidebarOpen$.next(false); + } + + toggleSidebar() { + this.sidebarOpen$.next(!this.sidebarOpen$.value); } open( diff --git a/packages/frontend/core/src/modules/workbench/index.ts b/packages/frontend/core/src/modules/workbench/index.ts index 52555be4cf..fe76b315d7 100644 --- a/packages/frontend/core/src/modules/workbench/index.ts +++ b/packages/frontend/core/src/modules/workbench/index.ts @@ -1,14 +1,14 @@ export { Workbench } from './entities/workbench'; -export { ViewScope as View } from './scopes/view'; +export { ViewScope } from './scopes/view'; export { WorkbenchService } from './services/workbench'; export { useIsActiveView } from './view/use-is-active-view'; -export { ViewBodyIsland } from './view/view-body-island'; -export { ViewHeaderIsland } from './view/view-header-island'; +export { ViewBody, ViewHeader, ViewSidebarTab } from './view/view-islands'; export { WorkbenchLink } from './view/workbench-link'; export { WorkbenchRoot } from './view/workbench-root'; import { type Framework, WorkspaceScope } from '@toeverything/infra'; +import { SidebarTab } from './entities/sidebar-tab'; import { View } from './entities/view'; import { Workbench } from './entities/workbench'; import { ViewScope } from './scopes/view'; @@ -20,7 +20,8 @@ export function configureWorkbenchModule(services: Framework) { .scope(WorkspaceScope) .service(WorkbenchService) .entity(Workbench) + .entity(View) .scope(ViewScope) - .entity(View, [ViewScope]) - .service(ViewService); + .service(ViewService, [ViewScope]) + .entity(SidebarTab); } diff --git a/packages/frontend/core/src/modules/workbench/scopes/view.ts b/packages/frontend/core/src/modules/workbench/scopes/view.ts index 8a286577e5..920273a055 100644 --- a/packages/frontend/core/src/modules/workbench/scopes/view.ts +++ b/packages/frontend/core/src/modules/workbench/scopes/view.ts @@ -1,7 +1,7 @@ import { Scope } from '@toeverything/infra'; -import type { To } from 'history'; + +import type { View } from '../entities/view'; export class ViewScope extends Scope<{ - id: string; - defaultLocation?: To | undefined; + view: View; }> {} diff --git a/packages/frontend/core/src/modules/workbench/services/view.ts b/packages/frontend/core/src/modules/workbench/services/view.ts index 028e57d41a..a1c6cf8482 100644 --- a/packages/frontend/core/src/modules/workbench/services/view.ts +++ b/packages/frontend/core/src/modules/workbench/services/view.ts @@ -1,7 +1,11 @@ import { Service } from '@toeverything/infra'; -import { View } from '../entities/view'; +import type { ViewScope } from '../scopes/view'; export class ViewService extends Service { - view = this.framework.createEntity(View); + view = this.scope.props.view; + + constructor(private readonly scope: ViewScope) { + super(); + } } diff --git a/packages/frontend/core/src/modules/workbench/view/route-container.tsx b/packages/frontend/core/src/modules/workbench/view/route-container.tsx index 8a44d5a675..02654e2945 100644 --- a/packages/frontend/core/src/modules/workbench/view/route-container.tsx +++ b/packages/frontend/core/src/modules/workbench/view/route-container.tsx @@ -1,17 +1,18 @@ -import { IconButton, observeResize } from '@affine/component'; +import { IconButton } from '@affine/component'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { RightSidebarIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; import { useAtomValue } from 'jotai'; -import { Suspense, useCallback, useEffect, useRef } from 'react'; +import { Suspense, useCallback } from 'react'; import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai'; import { SidebarSwitch } from '../../../components/app-sidebar/sidebar-header/sidebar-switch'; -import { RightSidebarService } from '../../right-sidebar'; import { ViewService } from '../services/view'; +import { WorkbenchService } from '../services/workbench'; import * as styles from './route-container.css'; import { useViewPosition } from './use-view-position'; +import { ViewBodyTarget, ViewHeaderTarget } from './view-islands'; export interface Props { route: { @@ -41,25 +42,16 @@ const ToggleButton = ({ }; export const RouteContainer = ({ route }: Props) => { - const viewHeaderContainerRef = useRef(null); - const view = useService(ViewService).view; const viewPosition = useViewPosition(); const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); - const rightSidebar = useService(RightSidebarService).rightSidebar; - const rightSidebarOpen = useLiveData(rightSidebar.isOpen$); - const rightSidebarHasViews = useLiveData(rightSidebar.hasViews$); - const handleToggleRightSidebar = useCallback(() => { - rightSidebar.toggle(); - }, [rightSidebar]); + const workbench = useService(WorkbenchService).workbench; + const view = useService(ViewService).view; + const sidebarOpen = useLiveData(workbench.sidebarOpen$); + const handleToggleSidebar = useCallback(() => { + workbench.toggleSidebar(); + }, [workbench]); const isWindowsDesktop = environment.isDesktop && environment.isWindows; - useEffect(() => { - const container = viewHeaderContainerRef.current; - if (!container) return; - return observeResize(container, entry => { - view.headerContentWidth$.next(entry.contentRect.width); - }); - }, [view.headerContentWidth$]); return (
@@ -69,34 +61,32 @@ export const RouteContainer = ({ route }: Props) => { className={styles.leftSidebarButton} /> )} - {viewPosition.isLast && ( <> - {rightSidebarHasViews && ( - + + {isWindowsDesktop && !sidebarOpen && ( +
+ +
)} - {isWindowsDesktop && - !(rightSidebarOpen && rightSidebarHasViews) && ( -
- -
- )} )}
+ - +
); }; diff --git a/packages/frontend/core/src/modules/right-sidebar/view/container.css.ts b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-container.css.ts similarity index 62% rename from packages/frontend/core/src/modules/right-sidebar/view/container.css.ts rename to packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-container.css.ts index aa08c5f2d9..f06770b140 100644 --- a/packages/frontend/core/src/modules/right-sidebar/view/container.css.ts +++ b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-container.css.ts @@ -17,24 +17,26 @@ export const sidebarContainerInner = style({ }, }); -export const sidebarContainer = style({ - display: 'flex', - flexShrink: 0, - height: '100%', - right: 0, - selectors: { - [`&[data-client-border=true]`]: { - paddingLeft: 8, - borderRadius: 6, - }, - [`&[data-client-border=false]`]: { - borderLeft: `1px solid ${cssVar('borderColor')}`, - }, - }, -}); - export const sidebarBodyTarget = style({ + display: 'flex', + flexDirection: 'column', flex: 1, width: '100%', + height: '100%', overflow: 'hidden', + alignItems: 'center', + borderTop: `1px solid ${cssVar('borderColor')}`, +}); + +export const sidebarBodyNoSelection = style({ + display: 'flex', + flexDirection: 'column', + flex: 1, + width: '100%', + height: '100%', + overflow: 'hidden', + justifyContent: 'center', + userSelect: 'none', + color: cssVar('--affine-text-secondary-color'), + alignItems: 'center', }); diff --git a/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-container.tsx b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-container.tsx new file mode 100644 index 0000000000..f71ea737c3 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-container.tsx @@ -0,0 +1,52 @@ +import { useLiveData, useService } from '@toeverything/infra'; +import clsx from 'clsx'; +import { useCallback } from 'react'; + +import { ViewService } from '../../services/view'; +import { WorkbenchService } from '../../services/workbench'; +import { ViewSidebarTabBodyTarget } from '../view-islands'; +import * as styles from './sidebar-container.css'; +import { Header } from './sidebar-header'; +import { SidebarHeaderSwitcher } from './sidebar-header-switcher'; + +export const SidebarContainer = ({ + className, + ...props +}: React.HtmlHTMLAttributes) => { + const workbenchService = useService(WorkbenchService); + const workbench = workbenchService.workbench; + const viewService = useService(ViewService); + const view = viewService.view; + const sidebarTabs = useLiveData(view.sidebarTabs$); + const activeSidebarTab = useLiveData(view.activeSidebarTab$); + + const handleToggleOpen = useCallback(() => { + workbench.toggleSidebar(); + }, [workbench]); + + const isWindowsDesktop = environment.isDesktop && environment.isWindows; + + return ( +
+
+ {!isWindowsDesktop && sidebarTabs.length > 0 && ( + + )} +
+ {isWindowsDesktop && sidebarTabs.length > 0 && } + {sidebarTabs.length > 0 ? ( + sidebarTabs.map(sidebar => ( + + )) + ) : ( +
No Selection
+ )} +
+ ); +}; diff --git a/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header-switcher.css.ts b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header-switcher.css.ts new file mode 100644 index 0000000000..a35bed6c84 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header-switcher.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const iconContainer = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + pointerEvents: 'none', +}); diff --git a/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header-switcher.tsx b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header-switcher.tsx new file mode 100644 index 0000000000..c94b9a4b64 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header-switcher.tsx @@ -0,0 +1,48 @@ +import { RadioGroup } from '@affine/component'; +import { useLiveData, useService } from '@toeverything/infra'; +import { cssVar } from '@toeverything/theme'; +import { useCallback } from 'react'; + +import { ViewService } from '../../services/view'; +import { ViewSidebarTabIconTarget } from '../view-islands'; +import * as styles from './sidebar-header-switcher.css'; + +// provide a switcher for active extensions +// will be used in global top header (MacOS) or sidebar (Windows) +export const SidebarHeaderSwitcher = () => { + const view = useService(ViewService).view; + const tabs = useLiveData(view.sidebarTabs$); + const activeTab = useLiveData(view.activeSidebarTab$); + + const tabItems = tabs.map(tab => ({ + value: tab.id, + label: ( + + ), + style: { padding: 0, fontSize: 20, width: 24 }, + })); + + const handleActiveTabChange = useCallback( + (tabId: string) => { + view.activeSidebarTab(tabId); + }, + [view] + ); + + return ( + + ); +}; diff --git a/packages/frontend/core/src/modules/right-sidebar/view/header.css.ts b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.css.ts similarity index 100% rename from packages/frontend/core/src/modules/right-sidebar/view/header.css.ts rename to packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.css.ts diff --git a/packages/frontend/core/src/modules/right-sidebar/view/header.tsx b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx similarity index 73% rename from packages/frontend/core/src/modules/right-sidebar/view/header.tsx rename to packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx index 94511df564..4cb2bcb5a2 100644 --- a/packages/frontend/core/src/modules/right-sidebar/view/header.tsx +++ b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx @@ -1,14 +1,13 @@ import { IconButton } from '@affine/component'; +import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { RightSidebarIcon } from '@blocksuite/icons/rc'; -import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls'; -import type { RightSidebarView } from '../entities/right-sidebar-view'; -import * as styles from './header.css'; +import * as styles from './sidebar-header.css'; export type HeaderProps = { floating: boolean; onToggle?: () => void; - view: RightSidebarView; + children?: React.ReactNode; }; function Container({ @@ -42,10 +41,10 @@ const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => { ); }; -const Windows = ({ floating, onToggle, view }: HeaderProps) => { +const Windows = ({ floating, onToggle, children }: HeaderProps) => { return ( - + {children}
@@ -55,10 +54,10 @@ const Windows = ({ floating, onToggle, view }: HeaderProps) => { ); }; -const NonWindows = ({ floating, view, onToggle }: HeaderProps) => { +const NonWindows = ({ floating, children, onToggle }: HeaderProps) => { return ( - + {children}
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 deleted file mode 100644 index 642da4b943..0000000000 --- a/packages/frontend/core/src/modules/workbench/view/view-body-island.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useService } from '@toeverything/infra'; - -import { ViewService } from '../services/view'; - -export const ViewBodyIsland = ({ children }: React.PropsWithChildren) => { - const view = useService(ViewService).view; - 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 deleted file mode 100644 index 4cd6a4d626..0000000000 --- a/packages/frontend/core/src/modules/workbench/view/view-header-island.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useService } from '@toeverything/infra'; - -import { ViewService } from '../services/view'; - -export const ViewHeaderIsland = ({ children }: React.PropsWithChildren) => { - const view = useService(ViewService).view; - return {children}; -}; diff --git a/packages/frontend/core/src/modules/workbench/view/view-islands.tsx b/packages/frontend/core/src/modules/workbench/view/view-islands.tsx new file mode 100644 index 0000000000..a667937bd5 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/view/view-islands.tsx @@ -0,0 +1,219 @@ +/** + * # View Islands + * + * This file defines some components that allow each UI area to be defined inside each View route as shown below, + * and the Workbench is responsible for rendering these areas into their containers. + * + * ```tsx + * const MyView = () => { + * return <> + * + * ... + * + * + * ... + * + * }> + * ... + * + * + * } + * + * const viewRoute = [ + * { + * path: '/my-view', + * component: MyView, + * } + * ] + * ``` + * + * Each Island is divided into `Target` and `Provider`. + * The `Provider` wraps the content to be rendered, while the `Target` is placed where it needs to be rendered. + * Then you get a view portal. + */ + +import { createIsland, type Island } from '@affine/core/utils/island'; +import { useLiveData, useService } from '@toeverything/infra'; +import type React from 'react'; +import { + createContext, + forwardRef, + type Ref, + useContext, + useEffect, + useState, +} from 'react'; + +import { ViewService } from '../services/view'; + +interface ViewIslandRegistry { + [key: string]: Island | undefined; +} + +/** + * A registry context will be placed at the top level of the workbench. + * + * The `View` will create islands and place them in the registry, + * while `Workbench` can use the KEY to retrieve and display the islands. + */ +const ViewIslandRegistryContext = createContext({}); +const ViewIslandSetContext = createContext +> | null>(null); + +const ViewIsland = ({ + id, + children, +}: React.PropsWithChildren<{ id: string }>) => { + const setter = useContext(ViewIslandSetContext); + + if (!setter) { + throw new Error( + 'ViewIslandProvider must be used inside ViewIslandRegistryProvider' + ); + } + + const [island] = useState(createIsland()); + + useEffect(() => { + setter(prev => ({ ...prev, [id]: island })); + + return () => { + setter(prev => { + const next = { ...prev }; + delete next[id]; + return next; + }); + }; + }, [id, island, setter]); + + return {children}; +}; + +const ViewIslandTarget = forwardRef(function ViewIslandTarget( + { + id, + children, + ...otherProps + }: { id: string } & React.HTMLProps, + ref: Ref +) { + const island = useContext(ViewIslandRegistryContext)[id]; + if (!island) { + return
; + } + + return ( + + {children} + + ); +}); + +export const ViewIslandRegistryProvider = ({ + children, +}: React.PropsWithChildren) => { + const [contextValue, setContextValue] = useState({}); + + return ( + + + {children} + + + ); +}; + +export const ViewBody = ({ children }: React.PropsWithChildren) => { + const view = useService(ViewService).view; + + return {children}; +}; + +export const ViewBodyTarget = forwardRef(function ViewBodyTarget( + { + viewId, + ...otherProps + }: React.HTMLProps & { viewId: string }, + ref: React.ForwardedRef +) { + return ; +}); + +export const ViewHeader = ({ children }: React.PropsWithChildren) => { + const view = useService(ViewService).view; + + return {children}; +}; + +export const ViewHeaderTarget = forwardRef(function ViewHeaderTarget( + { + viewId, + ...otherProps + }: React.HTMLProps & { viewId: string }, + ref: React.ForwardedRef +) { + return ; +}); + +export const ViewSidebarTab = ({ + children, + tabId, + icon, + unmountOnInactive = true, +}: React.PropsWithChildren<{ + tabId: string; + icon: React.ReactNode; + unmountOnInactive?: boolean; +}>) => { + const view = useService(ViewService).view; + const activeTab = useLiveData(view.activeSidebarTab$); + useEffect(() => { + view.addSidebarTab(tabId); + return () => { + view.removeSidebarTab(tabId); + }; + }, [tabId, view]); + + return ( + <> + {icon} + + {unmountOnInactive && activeTab?.id !== tabId ? null : children} + + + ); +}; + +export const ViewSidebarTabIconTarget = forwardRef( + function ViewSidebarTabIconTarget({ + viewId, + tabId, + ...otherProps + }: React.HTMLProps & { tabId: string; viewId: string }) { + return ( + + ); + } +); + +export const ViewSidebarTabBodyTarget = forwardRef( + function ViewSidebarTabBodyTarget({ + viewId, + tabId, + ...otherProps + }: React.HTMLProps & { + tabId: string; + viewId: string; + }) { + return ( + + ); + } +); diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts b/packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts index 4193b0c83d..8fbd1db5ba 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts +++ b/packages/frontend/core/src/modules/workbench/view/workbench-root.css.ts @@ -1,3 +1,4 @@ +import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; export const workbenchRootContainer = style({ @@ -12,3 +13,19 @@ export const workbenchViewContainer = style({ overflow: 'hidden', height: '100%', }); + +export const workbenchSidebar = style({ + display: 'flex', + flexShrink: 0, + height: '100%', + right: 0, + selectors: { + [`&[data-client-border=true]`]: { + paddingLeft: 8, + borderRadius: 6, + }, + [`&[data-client-border=false]`]: { + borderLeft: `1px solid ${cssVar('borderColor')}`, + }, + }, +}); diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx index 883c5fcd7a..d91e51718a 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx @@ -1,12 +1,22 @@ -import { useLiveData, useService } from '@toeverything/infra'; -import { memo, useCallback, useEffect, useRef } from 'react'; +import { ResizePanel } from '@affine/component/resize-panel'; +import { rightSidebarWidthAtom } from '@affine/core/atoms'; +import { + appSettingAtom, + FrameworkScope, + useLiveData, + useService, +} from '@toeverything/infra'; +import { useAtom, useAtomValue } from 'jotai'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import type { View } from '../entities/view'; import { WorkbenchService } from '../services/workbench'; import { useBindWorkbenchToBrowserRouter } from './browser-adapter'; import { useBindWorkbenchToDesktopRouter } from './desktop-adapter'; +import { SidebarContainer } from './sidebar/sidebar-container'; import { SplitView } from './split-view/split-view'; +import { ViewIslandRegistryProvider } from './view-islands'; import { ViewRoot } from './view-root'; import * as styles from './workbench-root.css'; @@ -43,12 +53,15 @@ export const WorkbenchRoot = memo(() => { }, [basename, workbench.basename$]); return ( - + + + + ); }); @@ -84,3 +97,67 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
); }; + +const MIN_SIDEBAR_WIDTH = 320; +const MAX_SIDEBAR_WIDTH = 800; + +const WorkbenchSidebar = () => { + const { clientBorder } = useAtomValue(appSettingAtom); + + const [width, setWidth] = useAtom(rightSidebarWidthAtom); + const [resizing, setResizing] = useState(false); + + const workbench = useService(WorkbenchService).workbench; + + const views = useLiveData(workbench.views$); + const activeView = useLiveData(workbench.activeView$); + const sidebarOpen = useLiveData(workbench.sidebarOpen$); + const [floating, setFloating] = useState(false); + + const handleOpenChange = useCallback( + (open: boolean) => { + if (open) { + workbench.openSidebar(); + } else { + workbench.closeSidebar(); + } + }, + [workbench] + ); + + useEffect(() => { + const onResize = () => setFloating(!!(window.innerWidth < 768)); + onResize(); + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + }; + }, []); + + return ( + + {views.map(view => ( + + + + ))} + + ); +}; 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 d32c183e02..c7ee17fedf 100644 --- a/packages/frontend/core/src/pages/workspace/all-collection/index.tsx +++ b/packages/frontend/core/src/pages/workspace/all-collection/index.tsx @@ -12,7 +12,7 @@ import { nanoid } from 'nanoid'; import { useCallback, useMemo, useState } from 'react'; import { CollectionService } from '../../../modules/collection'; -import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; +import { ViewBody, ViewHeader } from '../../../modules/workbench'; import { EmptyCollectionList } from '../page-list-empty'; import { AllCollectionHeader } from './header'; import * as styles from './index.css'; @@ -55,13 +55,13 @@ export const AllCollection = () => { return ( <> - + - - + +
{collectionMetas.length > 0 ? ( { /> )}
-
+ ); }; 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 aa5632e66b..3e34179741 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 @@ -9,7 +9,7 @@ import type { Filter } from '@affine/env/filter'; import { useService, WorkspaceService } from '@toeverything/infra'; import { useState } from 'react'; -import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; +import { ViewBody, ViewHeader } from '../../../modules/workbench'; import { EmptyPageList } from '../page-list-empty'; import * as styles from './all-page.css'; import { FilterContainer } from './all-page-filter'; @@ -27,14 +27,14 @@ export const AllPage = () => { return ( <> - + - - + +
{filteredPageMetas.length > 0 ? ( @@ -50,7 +50,7 @@ export const AllPage = () => { /> )}
-
+ ); }; 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 be7268843a..8d891d78c9 100644 --- a/packages/frontend/core/src/pages/workspace/all-tag/index.tsx +++ b/packages/frontend/core/src/pages/workspace/all-tag/index.tsx @@ -8,7 +8,7 @@ import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag'; import { useLiveData, useService } from '@toeverything/infra'; import { useCallback, useState } from 'react'; -import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; +import { ViewBody, ViewHeader } from '../../../modules/workbench'; import { EmptyTagList } from '../page-list-empty'; import * as styles from './all-tag.css'; import { AllTagHeader } from './header'; @@ -56,10 +56,10 @@ export const AllTag = () => { return ( <> - + - - + +
{tags.length > 0 ? ( { } /> )}
-
+ - + - - + + - + {node} ); @@ -127,7 +127,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => { return ( <> - +
{
-
- + +
{
) : null}
- + {node} ); 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 beffb8f5c5..bb81f80185 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,4 +1,8 @@ -import { Divider, type InlineEditHandle } from '@affine/component'; +import { + Divider, + type InlineEditHandle, + observeResize, +} from '@affine/component'; import { openInfoModalAtom } from '@affine/core/atoms'; import { InfoModal } from '@affine/core/components/affine/page-properties'; import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite'; @@ -13,7 +17,7 @@ import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import type { Doc } from '@blocksuite/store'; import { type Workspace } from '@toeverything/infra'; import { useAtom, useAtomValue } from 'jotai'; -import { useCallback, useRef } from 'react'; +import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; import { SharePageButton } from '../../../components/affine/share-page-modal'; import { appSidebarFloatingAtom } from '../../../components/app-sidebar'; @@ -22,36 +26,50 @@ import { HeaderDivider } from '../../../components/pure/header'; import * as styles from './detail-page-header.css'; import { useDetailPageHeaderResponsive } from './use-header-responsive'; -function Header({ - children, - style, - className, -}: { - children: React.ReactNode; - className?: string; - style?: React.CSSProperties; -}) { +const Header = forwardRef< + HTMLDivElement, + { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; + } +>(({ children, style, className }, ref) => { const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); return (
{children}
); -} +}); + +Header.displayName = 'forwardRef(Header)'; interface PageHeaderProps { page: Doc; workspace: Workspace; } export function JournalPageHeader({ page, workspace }: PageHeaderProps) { - const { hideShare, hideToday } = useDetailPageHeaderResponsive(); + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + return observeResize(container, entry => { + setContainerWidth(entry.contentRect.width); + }); + }, []); + + const { hideShare, hideToday } = + useDetailPageHeaderResponsive(containerWidth); return ( -
+
)} - + {page && !hideShare ? ( ) : null} @@ -76,14 +98,25 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) { export function NormalPageHeader({ page, workspace }: PageHeaderProps) { const titleInputHandleRef = useRef(null); + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + return observeResize(container, entry => { + setContainerWidth(entry.contentRect.width); + }); + }, []); + const { hideCollect, hideShare, hidePresent, showDivider } = - useDetailPageHeaderResponsive(); + useDetailPageHeaderResponsive(containerWidth); const onRename = useCallback(() => { setTimeout(() => titleInputHandleRef.current?.triggerEdit()); }, []); return ( -
+
: null} )} - +
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 369494ab4d..2e922c2002 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,10 +1,11 @@ import { Scrollable } from '@affine/component'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; +import type { ChatPanel } from '@affine/core/blocksuite/presets/ai'; import { AIProvider } from '@affine/core/blocksuite/presets/ai'; import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding'; -import { AIIsland } from '@affine/core/components/pure/ai-island'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { RecentDocsService } from '@affine/core/modules/quicksearch'; +import { ViewService } from '@affine/core/modules/workbench/services/view'; import type { PageRootService } from '@blocksuite/blocks'; import { BookmarkBlockService, @@ -15,6 +16,7 @@ import { ImageBlockService, } from '@blocksuite/blocks'; import { DisposableGroup } from '@blocksuite/global/utils'; +import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc'; import { type AffineEditorContainer } from '@blocksuite/presets'; import type { Doc as BlockSuiteDoc } from '@blocksuite/store'; import type { Doc } from '@toeverything/infra'; @@ -30,7 +32,14 @@ import { } from '@toeverything/infra'; import clsx from 'clsx'; import type { ReactElement } from 'react'; -import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; +import { + memo, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; import { useParams } from 'react-router-dom'; import type { Map as YMap } from 'yjs'; @@ -43,29 +52,26 @@ import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-r 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, - sidebarTabs, - type TabOnLoadFn, -} from '../../../modules/multi-tab-sidebar'; -import { - RightSidebarService, - RightSidebarViewIsland, -} from '../../../modules/right-sidebar'; import { useIsActiveView, - ViewBodyIsland, - ViewHeaderIsland, + ViewBody, + ViewHeader, + ViewSidebarTab, + WorkbenchService, } from '../../../modules/workbench'; import { performanceRenderLogger } from '../../../shared'; import { PageNotFound } from '../../404'; import * as styles from './detail-page.css'; import { DetailPageHeader } from './detail-page-header'; +import { EditorChatPanel } from './tabs/chat'; +import { EditorFramePanel } from './tabs/frame'; +import { EditorJournalPanel } from './tabs/journal'; +import { EditorOutline } from './tabs/outline'; const DetailPageImpl = memo(function DetailPageImpl() { - const rightSidebar = useService(RightSidebarService).rightSidebar; - const activeTabName = useLiveData(rightSidebar.activeTabName$); + const workbench = useService(WorkbenchService).workbench; + const view = useService(ViewService).view; + const activeSidebarTab = useLiveData(view.activeSidebarTab$); const doc = useService(DocService).doc; const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); @@ -75,18 +81,12 @@ const DetailPageImpl = memo(function DetailPageImpl() { const docCollection = workspace.docCollection; const mode = useLiveData(doc.mode$); const { appSettings } = useAppSettingHelper(); - const [tabOnLoad, setTabOnLoad] = useState(null); + const chatPanelRef = useRef(null); const isActiveView = useIsActiveView(); // TODO(@eyhn): remove jotai here const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor(); - const setActiveTabName = useCallback( - (...args: Parameters) => - rightSidebar.setActiveTabName(...args), - [rightSidebar] - ); - useEffect(() => { if (isActiveView) { setActiveBlockSuiteEditor(editor); @@ -95,28 +95,17 @@ const DetailPageImpl = memo(function DetailPageImpl() { useEffect(() => { const disposable = AIProvider.slots.requestOpenWithChat.on(params => { - const opened = rightSidebar.isOpen$.value; - const actived = activeTabName === 'chat'; + console.log(params); + workbench.openSidebar(); + view.activeSidebarTab('chat'); - if (!opened) { - rightSidebar.open(); - } - if (!actived) { - setActiveTabName('chat'); - } - - // Save chat parameters: - // * The right sidebar is not open - // * Chat panel is not activated - if (!opened || !actived) { - const callback = AIProvider.genRequestChatCardsFn(params); - setTabOnLoad(() => callback); - } else { - setTabOnLoad(null); + if (chatPanelRef.current) { + const chatCards = chatPanelRef.current.querySelector('chat-cards'); + if (chatCards) chatCards.temporaryParams = params; } }); return () => disposable.dispose(); - }, [activeTabName, rightSidebar, setActiveTabName]); + }, [activeSidebarTab, view, workbench]); useEffect(() => { if (isActiveView) { @@ -224,16 +213,13 @@ const DetailPageImpl = memo(function DetailPageImpl() { ] ); - const isWindowsDesktop = environment.isDesktop && environment.isWindows; - return ( <> - + - - + +
- {/* Add a key to force rerender when page changed, to avoid error boundary persisting. */} @@ -260,39 +246,24 @@ const DetailPageImpl = memo(function DetailPageImpl() { {isInTrash ? : null}
-
+ + + } unmountOnInactive={false}> + + + + }> + + + + }> + + + + }> + + - - ) : null - } - body={ - ext.name === activeTabName) ?? - sidebarTabs[0] - } - onLoad={tabOnLoad} - > - {/* Show switcher in body for windows desktop */} - {isWindowsDesktop && ( - - )} - - } - /> diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/tabs/chat.css.ts similarity index 100% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.css.ts rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/chat.css.ts diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx b/packages/frontend/core/src/pages/workspace/detail-page/tabs/chat.tsx similarity index 70% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/chat.tsx index 9e8f38fc6c..d2851e9608 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/tabs/chat.tsx @@ -1,13 +1,20 @@ import { ChatPanel } from '@affine/core/blocksuite/presets/ai'; import { assertExists } from '@blocksuite/global/utils'; -import { AiIcon } from '@blocksuite/icons/rc'; -import { useCallback, useEffect, useRef } from 'react'; +import type { AffineEditorContainer } from '@blocksuite/presets'; +import { forwardRef, useCallback, useEffect, useRef } from 'react'; -import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './chat.css'; +export interface SidebarTabProps { + editor: AffineEditorContainer | null; + onLoad?: ((component: HTMLElement) => void) | null; +} + // A wrapper for CopilotPanel -const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => { +export const EditorChatPanel = forwardRef(function EditorChatPanel( + { editor, onLoad }: SidebarTabProps, + ref: React.ForwardedRef +) { const chatPanelRef = useRef(null); const onRefChange = useCallback((container: HTMLDivElement | null) => { @@ -21,11 +28,17 @@ const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => { if (onLoad && chatPanelRef.current) { (chatPanelRef.current as ChatPanel).updateComplete .then(() => { - onLoad(chatPanelRef.current as HTMLElement); + if (ref) { + if (typeof ref === 'function') { + ref(chatPanelRef.current); + } else { + ref.current = chatPanelRef.current; + } + } }) .catch(console.error); } - }, [onLoad]); + }, [onLoad, ref]); useEffect(() => { if (!editor) return; @@ -57,10 +70,4 @@ const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => { // (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20]; return
; -}; - -export const chatTab: SidebarTab = { - name: 'chat', - icon: , - Component: EditorChatPanel, -}; +}); diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/frame.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/tabs/frame.css.ts similarity index 100% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/frame.css.ts rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/frame.css.ts diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/frame.tsx b/packages/frontend/core/src/pages/workspace/detail-page/tabs/frame.tsx similarity index 75% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/frame.tsx rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/frame.tsx index 70e6b0a420..76a2c4fc2e 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/frame.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/tabs/frame.tsx @@ -1,13 +1,16 @@ import { assertExists } from '@blocksuite/global/utils'; -import { FrameIcon } from '@blocksuite/icons/rc'; +import type { AffineEditorContainer } from '@blocksuite/presets'; import { FramePanel } from '@blocksuite/presets'; import { useCallback, useRef } from 'react'; -import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './frame.css'; // A wrapper for FramePanel -const EditorFramePanel = ({ editor }: SidebarTabProps) => { +export const EditorFramePanel = ({ + editor, +}: { + editor: AffineEditorContainer | null; +}) => { const framePanelRef = useRef(null); const onRefChange = useCallback((container: HTMLDivElement | null) => { @@ -32,9 +35,3 @@ const EditorFramePanel = ({ editor }: SidebarTabProps) => { return
; }; - -export const framePanelTab: SidebarTab = { - name: 'frame', - icon: , - Component: EditorFramePanel, -}; diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/journal.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/tabs/journal.css.ts similarity index 100% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/journal.css.ts rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/journal.css.ts diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/journal.tsx b/packages/frontend/core/src/pages/workspace/detail-page/tabs/journal.tsx similarity index 98% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/journal.tsx rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/journal.tsx index 0f18f97d7c..b4664d1e01 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/journal.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/tabs/journal.tsx @@ -29,7 +29,6 @@ import dayjs from 'dayjs'; import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { SidebarTab } from '../sidebar-tab'; import * as styles from './journal.css'; /** @@ -85,7 +84,7 @@ interface JournalBlockProps { date: dayjs.Dayjs; } -const EditorJournalPanel = () => { +export const EditorJournalPanel = () => { const t = useI18n(); const doc = useService(DocService).doc; const workspace = useService(WorkspaceService).workspace; @@ -381,9 +380,3 @@ const JournalConflictBlock = ({ date }: JournalBlockProps) => { ); }; - -export const journalTab: SidebarTab = { - name: 'journal', - icon: , - Component: EditorJournalPanel, -}; diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/outline.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.css.ts similarity index 100% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/outline.css.ts rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.css.ts diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/outline.tsx b/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx similarity index 76% rename from packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/outline.tsx rename to packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx index 69c153ed52..2ed0d7f768 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/outline.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx @@ -1,13 +1,16 @@ import { assertExists } from '@blocksuite/global/utils'; -import { TocIcon } from '@blocksuite/icons/rc'; +import type { AffineEditorContainer } from '@blocksuite/presets'; import { OutlinePanel } from '@blocksuite/presets'; import { useCallback, useRef } from 'react'; -import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './outline.css'; // A wrapper for TOCNotesPanel -const EditorOutline = ({ editor }: SidebarTabProps) => { +export const EditorOutline = ({ + editor, +}: { + editor: AffineEditorContainer | null; +}) => { const outlinePanelRef = useRef(null); const onRefChange = useCallback((container: HTMLDivElement | null) => { @@ -32,9 +35,3 @@ const EditorOutline = ({ editor }: SidebarTabProps) => { return
; }; - -export const outlineTab: SidebarTab = { - name: 'outline', - icon: , - Component: EditorOutline, -}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/use-header-responsive.ts b/packages/frontend/core/src/pages/workspace/detail-page/use-header-responsive.ts index cfe71c464a..e578e79927 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/use-header-responsive.ts +++ b/packages/frontend/core/src/pages/workspace/detail-page/use-header-responsive.ts @@ -1,21 +1,16 @@ -import { RightSidebarService } from '@affine/core/modules/right-sidebar'; import { WorkbenchService } from '@affine/core/modules/workbench'; -import { ViewService } from '@affine/core/modules/workbench/services/view'; import { useViewPosition } from '@affine/core/modules/workbench/view/use-view-position'; import { DocService, useLiveData, useService } from '@toeverything/infra'; -export const useDetailPageHeaderResponsive = () => { +export const useDetailPageHeaderResponsive = (availableWidth: number) => { const mode = useLiveData(useService(DocService).doc.mode$); - const view = useService(ViewService).view; const workbench = useService(WorkbenchService).workbench; - const availableWidth = useLiveData(view.headerContentWidth$); const viewPosition = useViewPosition(); const workbenchViewsCount = useLiveData( workbench.views$.map(views => views.length) ); - const rightSidebar = useService(RightSidebarService).rightSidebar; - const rightSidebarOpen = useLiveData(rightSidebar.isOpen$); + const rightSidebarOpen = useLiveData(workbench.sidebarOpen$); // share button should be hidden once split-view is enabled const hideShare = availableWidth < 500 || workbenchViewsCount > 1; diff --git a/packages/frontend/core/src/pages/workspace/index.tsx b/packages/frontend/core/src/pages/workspace/index.tsx index 8b25b88119..f02b51960e 100644 --- a/packages/frontend/core/src/pages/workspace/index.tsx +++ b/packages/frontend/core/src/pages/workspace/index.tsx @@ -15,7 +15,6 @@ import { useParams } from 'react-router-dom'; import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary'; import { WorkspaceLayout } from '../../layouts/workspace-layout'; -import { RightSidebarContainer } from '../../modules/right-sidebar'; import { WorkbenchRoot } from '../../modules/workbench'; import { AllWorkspaceModals } from '../../providers/modal-provider'; import { performanceRenderLogger } from '../../shared'; @@ -163,7 +162,6 @@ export const Component = (): ReactElement => { - diff --git a/packages/frontend/core/src/pages/workspace/tag/index.tsx b/packages/frontend/core/src/pages/workspace/tag/index.tsx index 910da8c69f..295f36a87f 100644 --- a/packages/frontend/core/src/pages/workspace/tag/index.tsx +++ b/packages/frontend/core/src/pages/workspace/tag/index.tsx @@ -4,10 +4,7 @@ import { } from '@affine/core/components/page-list'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { TagService } from '@affine/core/modules/tag'; -import { - ViewBodyIsland, - ViewHeaderIsland, -} from '@affine/core/modules/workbench'; +import { ViewBody, ViewHeader } from '@affine/core/modules/workbench'; import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; @@ -37,10 +34,10 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => { return ( <> - + - - + +
{filteredPageMetas.length > 0 ? ( { /> )}
-
+ ); }; diff --git a/packages/frontend/core/src/pages/workspace/trash-page.tsx b/packages/frontend/core/src/pages/workspace/trash-page.tsx index d1eb9bb3f2..7fdfe5e046 100644 --- a/packages/frontend/core/src/pages/workspace/trash-page.tsx +++ b/packages/frontend/core/src/pages/workspace/trash-page.tsx @@ -9,7 +9,7 @@ import { assertExists } from '@blocksuite/global/utils'; import { DeleteIcon } from '@blocksuite/icons/rc'; import { useService, WorkspaceService } from '@toeverything/infra'; -import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench'; +import { ViewBody, ViewHeader } from '../../modules/workbench'; import { EmptyPageList } from './page-list-empty'; import * as styles from './trash-page.css'; @@ -39,10 +39,10 @@ export const TrashPage = () => { return ( <> - + - - + +
{filteredPageMetas.length > 0 ? ( @@ -53,7 +53,7 @@ export const TrashPage = () => { /> )}
-
+ ); };