feat(core): responsive detail page header (#7263)

This commit is contained in:
CatsJuice
2024-06-20 05:47:43 +00:00
parent 78429166da
commit 1fdfa834a0
15 changed files with 264 additions and 108 deletions

View File

@@ -7,19 +7,13 @@ import { ShareMenu } from './share-menu';
type SharePageModalProps = { type SharePageModalProps = {
workspace: Workspace; workspace: Workspace;
page: Doc; page: Doc;
isJournal?: boolean;
}; };
export const SharePageButton = ({ export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
workspace,
page,
isJournal,
}: SharePageModalProps) => {
const confirmEnableCloud = useEnableCloud(); const confirmEnableCloud = useEnableCloud();
return ( return (
<ShareMenu <ShareMenu
isJournal={isJournal}
workspaceMetadata={workspace.meta} workspaceMetadata={workspace.meta}
currentPage={page} currentPage={page}
onEnableAffineCloud={() => onEnableAffineCloud={() =>

View File

@@ -138,7 +138,7 @@ globalStyle(`${shareLinkStyle} > span`, {
globalStyle(`${shareLinkStyle} > div > svg`, { globalStyle(`${shareLinkStyle} > div > svg`, {
color: cssVar('linkColor'), color: cssVar('linkColor'),
}); });
export const journalShareButton = style({ export const shareButton = style({
height: 32, height: 32,
padding: '0px 8px', padding: '0px 8px',
}); });

View File

@@ -1,27 +1,24 @@
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { Divider } from '@affine/component/ui/divider'; import { Divider } from '@affine/component/ui/divider';
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
import { useIsActiveView } from '@affine/core/modules/workbench';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { WebIcon } from '@blocksuite/icons/rc'; import { WebIcon } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import type { WorkspaceMetadata } from '@toeverything/infra'; import type { WorkspaceMetadata } from '@toeverything/infra';
import clsx from 'clsx'; import { forwardRef, type PropsWithChildren, type Ref } from 'react';
import * as styles from './index.css'; import * as styles from './index.css';
import { ShareExport } from './share-export'; import { ShareExport } from './share-export';
import { SharePage } from './share-page'; import { SharePage } from './share-page';
export interface ShareMenuProps { export interface ShareMenuProps extends PropsWithChildren {
workspaceMetadata: WorkspaceMetadata; workspaceMetadata: WorkspaceMetadata;
currentPage: Doc; currentPage: Doc;
isJournal?: boolean;
onEnableAffineCloud: () => void; onEnableAffineCloud: () => void;
} }
const ShareMenuContent = (props: ShareMenuProps) => { export const ShareMenuContent = (props: ShareMenuProps) => {
const t = useI18n(); const t = useI18n();
return ( return (
<div className={styles.containerStyle}> <div className={styles.containerStyle}>
@@ -40,8 +37,20 @@ const ShareMenuContent = (props: ShareMenuProps) => {
); );
}; };
const LocalShareMenu = (props: ShareMenuProps) => { const DefaultShareButton = forwardRef(function DefaultShareButton(
_,
ref: Ref<HTMLButtonElement>
) {
const t = useI18n(); const t = useI18n();
return (
<Button ref={ref} className={styles.shareButton} type="primary">
{t['com.affine.share-menu.shareButton']()}
</Button>
);
});
const LocalShareMenu = (props: ShareMenuProps) => {
return ( return (
<Menu <Menu
items={<ShareMenuContent {...props} />} items={<ShareMenuContent {...props} />}
@@ -53,27 +62,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
modal: false, modal: false,
}} }}
> >
<Button <div data-testid="local-share-menu-button">
className={clsx({ [styles.journalShareButton]: props.isJournal })} {props.children || <DefaultShareButton />}
data-testid="local-share-menu-button" </div>
type="primary"
>
{t['com.affine.share-menu.shareButton']()}
</Button>
</Menu> </Menu>
); );
}; };
const CloudShareMenu = (props: ShareMenuProps) => { const CloudShareMenu = (props: ShareMenuProps) => {
const t = useI18n();
// only enable copy link commands when the view is active and the workspace is cloud
const isActiveView = useIsActiveView();
useRegisterCopyLinkCommands({
workspaceId: props.workspaceMetadata.id,
docId: props.currentPage.id,
isActiveView,
});
return ( return (
<Menu <Menu
items={<ShareMenuContent {...props} />} items={<ShareMenuContent {...props} />}
@@ -85,13 +81,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
modal: false, modal: false,
}} }}
> >
<Button <div data-testid="cloud-share-menu-button">
className={clsx({ [styles.journalShareButton]: props.isJournal })} {props.children || <DefaultShareButton />}
data-testid="cloud-share-menu-button" </div>
type="primary"
>
{t['com.affine.share-menu.shareButton']()}
</Button>
</Menu> </Menu>
); );
}; };

View File

@@ -4,14 +4,18 @@ import {
MenuIcon, MenuIcon,
MenuItem, MenuItem,
MenuSeparator, MenuSeparator,
MenuSub,
} from '@affine/component/ui/menu'; } from '@affine/component/ui/menu';
import { openHistoryTipsModalAtom } from '@affine/core/atoms'; import { openHistoryTipsModalAtom } from '@affine/core/atoms';
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal'; import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
import { Export, MoveToTrash } from '@affine/core/components/page-list'; import { Export, MoveToTrash } from '@affine/core/components/page-list';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive';
import { mixpanel } from '@affine/core/utils'; import { mixpanel } from '@affine/core/utils';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -24,7 +28,9 @@ import {
HistoryIcon, HistoryIcon,
ImportIcon, ImportIcon,
PageIcon, PageIcon,
ShareIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store';
import { import {
DocService, DocService,
useLiveData, useLiveData,
@@ -40,16 +46,19 @@ import { useFavorite } from '../favorite';
type PageMenuProps = { type PageMenuProps = {
rename?: () => void; rename?: () => void;
pageId: string; page: Doc;
isJournal?: boolean; isJournal?: boolean;
}; };
// fixme: refactor this file // fixme: refactor this file
export const PageHeaderMenuButton = ({ export const PageHeaderMenuButton = ({
rename, rename,
pageId, page,
isJournal, isJournal,
}: PageMenuProps) => { }: PageMenuProps) => {
const pageId = page?.id;
const t = useI18n(); const t = useI18n();
const { hideShare } = useDetailPageHeaderResponsive();
const confirmEnableCloud = useEnableCloud();
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;
const docCollection = workspace.docCollection; const docCollection = workspace.docCollection;
@@ -127,8 +136,46 @@ export const PageHeaderMenuButton = ({
} }
}, [importFile]); }, [importFile]);
const showResponsiveMenu = hideShare;
const ResponsiveMenuItems = (
<>
{hideShare ? (
<MenuSub
subContentOptions={{
sideOffset: 12,
alignOffset: -8,
}}
items={
<div style={{ padding: 4 }}>
<ShareMenuContent
workspaceMetadata={workspace.meta}
currentPage={page}
onEnableAffineCloud={() =>
confirmEnableCloud(workspace, {
openPageId: page.id,
})
}
/>
</div>
}
triggerOptions={{
preFix: (
<MenuIcon>
<ShareIcon />
</MenuIcon>
),
}}
>
{t['com.affine.share-menu.shareButton']()}
</MenuSub>
) : null}
<MenuSeparator />
</>
);
const EditMenu = ( const EditMenu = (
<> <>
{showResponsiveMenu ? ResponsiveMenuItems : null}
{!isJournal && ( {!isJournal && (
<MenuItem <MenuItem
preFix={ preFix={

View File

@@ -0,0 +1,17 @@
import { IconButton } from '@affine/component';
import { PresentationIcon } from '@blocksuite/icons/rc';
import { usePresent } from './use-present';
export const DetailPageHeaderPresentButton = () => {
const { isPresent, handlePresent } = usePresent();
return (
<IconButton
style={{ flexShrink: 0 }}
size={'large'}
icon={<PresentationIcon />}
onClick={() => handlePresent(!isPresent)}
></IconButton>
);
};

View File

@@ -0,0 +1,59 @@
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import type { EdgelessRootService } from '@blocksuite/blocks';
import { useCallback, useEffect, useState } from 'react';
export const usePresent = () => {
const [isPresent, setIsPresent] = useState(false);
const [editor] = useActiveBlocksuiteEditor();
const handlePresent = useCallback(
(enable = true) => {
isPresent;
const editorHost = editor?.host;
if (!editorHost) return;
// TODO: use surfaceService subAtom
const enterOrLeavePresentationMode = () => {
const edgelessRootService = editorHost.spec.getService(
'affine:page'
) as EdgelessRootService;
if (!edgelessRootService) {
return;
}
const activeTool = edgelessRootService.tool.edgelessTool.type;
const isFrameNavigator = activeTool === 'frameNavigator';
if ((enable && isFrameNavigator) || (!enable && !isFrameNavigator))
return;
edgelessRootService.tool.setEdgelessTool({
type: enable ? 'frameNavigator' : 'default',
});
};
enterOrLeavePresentationMode();
setIsPresent(enable);
},
[editor?.host, isPresent]
);
useEffect(() => {
if (!isPresent) return;
const editorHost = editor?.host;
if (!editorHost) return;
const edgelessPage = editorHost?.querySelector('affine-edgeless-root');
if (!edgelessPage) return;
return edgelessPage.slots.edgelessToolUpdated.on(() => {
setIsPresent(edgelessPage.edgelessTool.type === 'frameNavigator');
}).dispose;
}, [editor?.host, isPresent]);
return {
isPresent,
handlePresent,
};
};

View File

@@ -11,7 +11,7 @@ export const StyledEditorModeSwitch = styled('div')<{
background: showAlone background: showAlone
? 'transparent' ? 'transparent'
: 'var(--affine-background-secondary-color)', : 'var(--affine-background-secondary-color)',
borderRadius: '12px', borderRadius: '8px',
...displayFlex('space-between', 'center'), ...displayFlex('space-between', 'center'),
padding: '4px 4px', padding: '4px 4px',
position: 'relative', position: 'relative',
@@ -23,7 +23,7 @@ export const StyledEditorModeSwitch = styled('div')<{
height: '24px', height: '24px',
background: 'var(--affine-background-primary-color)', background: 'var(--affine-background-primary-color)',
boxShadow: 'var(--affine-shadow-1)', boxShadow: 'var(--affine-shadow-1)',
borderRadius: '8px', borderRadius: '4px',
zIndex: 1, zIndex: 1,
position: 'absolute', position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '32px'})`, transform: `translateX(${switchLeft ? '0' : '32px'})`,

View File

@@ -1,60 +1,19 @@
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { EdgelessRootService } from '@blocksuite/blocks';
import { PresentationIcon } from '@blocksuite/icons/rc'; import { PresentationIcon } from '@blocksuite/icons/rc';
import { useCallback, useEffect, useState } from 'react';
import { usePresent } from '../../blocksuite/block-suite-header/present/use-present';
import * as styles from './styles.css'; import * as styles from './styles.css';
export const PresentButton = () => { export const PresentButton = () => {
const t = useI18n(); const t = useI18n();
const [isPresent, setIsPresent] = useState(false); const { isPresent, handlePresent } = usePresent();
const [editor] = useActiveBlocksuiteEditor();
const handlePresent = useCallback(() => {
const editorHost = editor?.host;
if (!editorHost || isPresent) return;
// TODO: use surfaceService subAtom
const enterPresentationMode = () => {
const edgelessRootService = editorHost.spec.getService(
'affine:page'
) as EdgelessRootService;
if (
!edgelessRootService ||
edgelessRootService.tool.edgelessTool.type === 'frameNavigator'
) {
return;
}
edgelessRootService.tool.setEdgelessTool({ type: 'frameNavigator' });
};
enterPresentationMode();
setIsPresent(true);
}, [editor?.host, isPresent]);
useEffect(() => {
if (!isPresent) return;
const editorHost = editor?.host;
if (!editorHost) return;
const edgelessPage = editorHost?.querySelector('affine-edgeless-root');
if (!edgelessPage) return;
return edgelessPage.slots.edgelessToolUpdated.on(() => {
setIsPresent(edgelessPage.edgelessTool.type === 'frameNavigator');
}).dispose;
}, [editor?.host, isPresent]);
return ( return (
<Button <Button
icon={<PresentationIcon />} icon={<PresentationIcon />}
className={styles.presentButton} className={styles.presentButton}
onClick={handlePresent} onClick={() => handlePresent()}
disabled={isPresent} disabled={isPresent}
withoutHoverStyle withoutHoverStyle
> >

View File

@@ -1,16 +1,20 @@
import { registerAffineCommand } from '@affine/core/commands'; import { registerAffineCommand } from '@affine/core/commands';
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url'; import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
import { useIsActiveView } from '@affine/core/modules/workbench';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
export function useRegisterCopyLinkCommands({ export function useRegisterCopyLinkCommands({
workspaceId, workspaceMeta,
docId, docId,
isActiveView,
}: { }: {
workspaceId: string; workspaceMeta: WorkspaceMetadata;
docId: string; docId: string;
isActiveView: boolean;
}) { }) {
const isActiveView = useIsActiveView();
const workspaceId = workspaceMeta.id;
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
const { onClickCopyLink } = useSharingUrl({ const { onClickCopyLink } = useSharingUrl({
workspaceId, workspaceId,
pageId: docId, pageId: docId,
@@ -31,12 +35,12 @@ export function useRegisterCopyLinkCommands({
label: '', label: '',
icon: null, icon: null,
run() { run() {
isActiveView && onClickCopyLink(); isActiveView && isCloud && onClickCopyLink();
}, },
}) })
); );
return () => { return () => {
unsubs.forEach(unsub => unsub()); unsubs.forEach(unsub => unsub());
}; };
}, [docId, isActiveView, onClickCopyLink]); }, [docId, isActiveView, isCloud, onClickCopyLink]);
} }

View File

@@ -45,6 +45,8 @@ export class View extends Entity {
); );
size$ = new LiveData(100); size$ = new LiveData(100);
/** Width of header content in px (excludes sidebar-toggle/windows button/...) */
headerContentWidth$ = new LiveData(1920);
header = createIsland(); header = createIsland();
body = createIsland(); body = createIsland();

View File

@@ -58,6 +58,7 @@ export const rightSidebarButton = style({
export const viewHeaderContainer = style({ export const viewHeaderContainer = style({
display: 'flex', display: 'flex',
height: '100%', height: '100%',
width: 0,
flexGrow: 1, flexGrow: 1,
minWidth: 12, minWidth: 12,
}); });

View File

@@ -1,9 +1,9 @@
import { IconButton } from '@affine/component'; import { IconButton, observeResize } from '@affine/component';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { RightSidebarIcon } from '@blocksuite/icons/rc'; import { RightSidebarIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { Suspense, useCallback } from 'react'; import { Suspense, useCallback, useEffect, useRef } from 'react';
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai'; import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
@@ -41,6 +41,7 @@ const ToggleButton = ({
}; };
export const RouteContainer = ({ route }: Props) => { export const RouteContainer = ({ route }: Props) => {
const viewHeaderContainerRef = useRef<HTMLDivElement | null>(null);
const view = useService(ViewService).view; const view = useService(ViewService).view;
const viewPosition = useViewPosition(); const viewPosition = useViewPosition();
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
@@ -51,6 +52,14 @@ export const RouteContainer = ({ route }: Props) => {
rightSidebar.toggle(); rightSidebar.toggle();
}, [rightSidebar]); }, [rightSidebar]);
const isWindowsDesktop = environment.isDesktop && environment.isWindows; 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 ( return (
<div className={styles.root}> <div className={styles.root}>
<div className={styles.header}> <div className={styles.header}>
@@ -60,7 +69,10 @@ export const RouteContainer = ({ route }: Props) => {
className={styles.leftSidebarButton} className={styles.leftSidebarButton}
/> />
)} )}
<view.header.Target className={styles.viewHeaderContainer} /> <view.header.Target
ref={viewHeaderContainerRef}
className={styles.viewHeaderContainer}
/>
{viewPosition.isLast && ( {viewPosition.isLast && (
<> <>
{rightSidebarHasViews && ( {rightSidebarHasViews && (

View File

@@ -1,12 +1,14 @@
import type { InlineEditHandle } from '@affine/component'; import { Divider, type InlineEditHandle } from '@affine/component';
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite'; 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 { 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 { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu'; import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuite/block-suite-header/present/detail-header-present-button';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra'; import { type Workspace } from '@toeverything/infra';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
@@ -15,6 +17,7 @@ import { appSidebarFloatingAtom } from '../../../components/app-sidebar';
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index'; import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
import { HeaderDivider } from '../../../components/pure/header'; import { HeaderDivider } from '../../../components/pure/header';
import * as styles from './detail-page-header.css'; import * as styles from './detail-page-header.css';
import { useDetailPageHeaderResponsive } from './use-header-responsive';
function Header({ function Header({
children, children,
@@ -43,6 +46,7 @@ interface PageHeaderProps {
workspace: Workspace; workspace: Workspace;
} }
export function JournalPageHeader({ page, workspace }: PageHeaderProps) { export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
const { hideShare, hideToday } = useDetailPageHeaderResponsive();
return ( return (
<Header className={styles.header}> <Header className={styles.header}>
<EditorModeSwitch <EditorModeSwitch
@@ -55,11 +59,13 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
page={page} page={page}
/> />
</div> </div>
<JournalTodayButton docCollection={workspace.docCollection} /> {hideToday ? null : (
<JournalTodayButton docCollection={workspace.docCollection} />
)}
<HeaderDivider /> <HeaderDivider />
<PageHeaderMenuButton isJournal pageId={page?.id} /> <PageHeaderMenuButton isJournal page={page} />
{page ? ( {page && !hideShare ? (
<SharePageButton isJournal workspace={workspace} page={page} /> <SharePageButton workspace={workspace} page={page} />
) : null} ) : null}
</Header> </Header>
); );
@@ -67,6 +73,8 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
export function NormalPageHeader({ page, workspace }: PageHeaderProps) { export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
const titleInputHandleRef = useRef<InlineEditHandle>(null); const titleInputHandleRef = useRef<InlineEditHandle>(null);
const { hideCollect, hideShare, hidePresent, showDivider } =
useDetailPageHeaderResponsive();
const onRename = useCallback(() => { const onRename = useCallback(() => {
setTimeout(() => titleInputHandleRef.current?.triggerEdit()); setTimeout(() => titleInputHandleRef.current?.triggerEdit());
@@ -82,19 +90,33 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
pageId={page?.id} pageId={page?.id}
docCollection={workspace.docCollection} docCollection={workspace.docCollection}
/> />
<PageHeaderMenuButton rename={onRename} pageId={page?.id} /> {hideCollect ? null : <FavoriteButton pageId={page?.id} />}
<FavoriteButton pageId={page?.id} /> <PageHeaderMenuButton rename={onRename} page={page} />
<div className={styles.spacer} /> <div className={styles.spacer} />
{page ? <SharePageButton workspace={workspace} page={page} /> : null}
{!hidePresent ? <DetailPageHeaderPresentButton /> : null}
{page && !hideShare ? (
<SharePageButton workspace={workspace} page={page} />
) : null}
{showDivider ? (
<Divider orientation="vertical" style={{ height: 20, marginLeft: 4 }} />
) : null}
</Header> </Header>
); );
} }
export function DetailPageHeader(props: PageHeaderProps) { export function DetailPageHeader(props: PageHeaderProps) {
const { page } = props; const { page, workspace } = props;
const { isJournal } = useJournalInfoHelper(page.collection, page.id); const { isJournal } = useJournalInfoHelper(page.collection, page.id);
const isInTrash = page.meta?.trash; const isInTrash = page.meta?.trash;
useRegisterCopyLinkCommands({
workspaceMeta: workspace.meta,
docId: page.id,
});
return isJournal && !isInTrash ? ( return isJournal && !isInTrash ? (
<JournalPageHeader {...props} /> <JournalPageHeader {...props} />
) : ( ) : (

View File

@@ -0,0 +1,36 @@
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 = () => {
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$);
// share button should be hidden once split-view is enabled
const hideShare = availableWidth < 500 || workbenchViewsCount > 1;
const hidePresent = availableWidth < 400 || mode !== 'edgeless';
const hideCollect = availableWidth < 300;
const hideToday = availableWidth < 300;
const showDivider =
viewPosition.isLast && !rightSidebarOpen && !(hidePresent && hideShare);
return {
hideShare,
hidePresent,
hideCollect,
hideToday,
showDivider,
};
};

View File

@@ -1,5 +1,11 @@
import { LiveData, useLiveData } from '@toeverything/infra'; import { LiveData, useLiveData } from '@toeverything/infra';
import { useEffect, useRef } from 'react'; import {
forwardRef,
type Ref,
useEffect,
useImperativeHandle,
useRef,
} from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
export const createIsland = () => { export const createIsland = () => {
@@ -7,8 +13,13 @@ export const createIsland = () => {
let mounted = false; let mounted = false;
let provided = false; let provided = false;
return { return {
Target: ({ ...other }: React.HTMLProps<HTMLDivElement>) => { Target: forwardRef(function IslandTarget(
{ ...other }: React.HTMLProps<HTMLDivElement>,
ref: Ref<HTMLDivElement>
) {
const target = useRef<HTMLDivElement | null>(null); const target = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => target.current as HTMLDivElement, []);
useEffect(() => { useEffect(() => {
if (mounted === true) { if (mounted === true) {
throw new Error('Island should not be mounted more than once'); throw new Error('Island should not be mounted more than once');
@@ -21,7 +32,7 @@ export const createIsland = () => {
}; };
}, []); }, []);
return <div {...other} ref={target}></div>; return <div {...other} ref={target}></div>;
}, }),
Provider: ({ children }: React.PropsWithChildren) => { Provider: ({ children }: React.PropsWithChildren) => {
const target = useLiveData(targetLiveData$); const target = useLiveData(targetLiveData$);
useEffect(() => { useEffect(() => {