mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): responsive detail page header (#7263)
This commit is contained in:
@@ -7,19 +7,13 @@ import { ShareMenu } from './share-menu';
|
||||
type SharePageModalProps = {
|
||||
workspace: Workspace;
|
||||
page: Doc;
|
||||
isJournal?: boolean;
|
||||
};
|
||||
|
||||
export const SharePageButton = ({
|
||||
workspace,
|
||||
page,
|
||||
isJournal,
|
||||
}: SharePageModalProps) => {
|
||||
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
return (
|
||||
<ShareMenu
|
||||
isJournal={isJournal}
|
||||
workspaceMetadata={workspace.meta}
|
||||
currentPage={page}
|
||||
onEnableAffineCloud={() =>
|
||||
|
||||
@@ -138,7 +138,7 @@ globalStyle(`${shareLinkStyle} > span`, {
|
||||
globalStyle(`${shareLinkStyle} > div > svg`, {
|
||||
color: cssVar('linkColor'),
|
||||
});
|
||||
export const journalShareButton = style({
|
||||
export const shareButton = style({
|
||||
height: 32,
|
||||
padding: '0px 8px',
|
||||
});
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
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 { useI18n } from '@affine/i18n';
|
||||
import { WebIcon } from '@blocksuite/icons/rc';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
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 { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
|
||||
export interface ShareMenuProps {
|
||||
export interface ShareMenuProps extends PropsWithChildren {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
currentPage: Doc;
|
||||
isJournal?: boolean;
|
||||
onEnableAffineCloud: () => void;
|
||||
}
|
||||
|
||||
const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
export const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<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();
|
||||
|
||||
return (
|
||||
<Button ref={ref} className={styles.shareButton} type="primary">
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
return (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
@@ -53,27 +62,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
||||
data-testid="local-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
<div data-testid="local-share-menu-button">
|
||||
{props.children || <DefaultShareButton />}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
@@ -85,13 +81,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
||||
data-testid="cloud-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
<div data-testid="cloud-share-menu-button">
|
||||
{props.children || <DefaultShareButton />}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,18 @@ import {
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
MenuSub,
|
||||
} from '@affine/component/ui/menu';
|
||||
import { openHistoryTipsModalAtom } from '@affine/core/atoms';
|
||||
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 { 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 { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -24,7 +28,9 @@ import {
|
||||
HistoryIcon,
|
||||
ImportIcon,
|
||||
PageIcon,
|
||||
ShareIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
@@ -40,16 +46,19 @@ import { useFavorite } from '../favorite';
|
||||
|
||||
type PageMenuProps = {
|
||||
rename?: () => void;
|
||||
pageId: string;
|
||||
page: Doc;
|
||||
isJournal?: boolean;
|
||||
};
|
||||
// fixme: refactor this file
|
||||
export const PageHeaderMenuButton = ({
|
||||
rename,
|
||||
pageId,
|
||||
page,
|
||||
isJournal,
|
||||
}: PageMenuProps) => {
|
||||
const pageId = page?.id;
|
||||
const t = useI18n();
|
||||
const { hideShare } = useDetailPageHeaderResponsive();
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
@@ -127,8 +136,46 @@ export const PageHeaderMenuButton = ({
|
||||
}
|
||||
}, [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 = (
|
||||
<>
|
||||
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
preFix={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export const StyledEditorModeSwitch = styled('div')<{
|
||||
background: showAlone
|
||||
? 'transparent'
|
||||
: 'var(--affine-background-secondary-color)',
|
||||
borderRadius: '12px',
|
||||
borderRadius: '8px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
padding: '4px 4px',
|
||||
position: 'relative',
|
||||
@@ -23,7 +23,7 @@ export const StyledEditorModeSwitch = styled('div')<{
|
||||
height: '24px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
borderRadius: '8px',
|
||||
borderRadius: '4px',
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
transform: `translateX(${switchLeft ? '0' : '32px'})`,
|
||||
|
||||
@@ -1,60 +1,19 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { EdgelessRootService } from '@blocksuite/blocks';
|
||||
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';
|
||||
|
||||
export const PresentButton = () => {
|
||||
const t = useI18n();
|
||||
const [isPresent, setIsPresent] = useState(false);
|
||||
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]);
|
||||
const { isPresent, handlePresent } = usePresent();
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<PresentationIcon />}
|
||||
className={styles.presentButton}
|
||||
onClick={handlePresent}
|
||||
onClick={() => handlePresent()}
|
||||
disabled={isPresent}
|
||||
withoutHoverStyle
|
||||
>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { registerAffineCommand } from '@affine/core/commands';
|
||||
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';
|
||||
|
||||
export function useRegisterCopyLinkCommands({
|
||||
workspaceId,
|
||||
workspaceMeta,
|
||||
docId,
|
||||
isActiveView,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceMeta: WorkspaceMetadata;
|
||||
docId: string;
|
||||
isActiveView: boolean;
|
||||
}) {
|
||||
const isActiveView = useIsActiveView();
|
||||
const workspaceId = workspaceMeta.id;
|
||||
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId: docId,
|
||||
@@ -31,12 +35,12 @@ export function useRegisterCopyLinkCommands({
|
||||
label: '',
|
||||
icon: null,
|
||||
run() {
|
||||
isActiveView && onClickCopyLink();
|
||||
isActiveView && isCloud && onClickCopyLink();
|
||||
},
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [docId, isActiveView, onClickCopyLink]);
|
||||
}, [docId, isActiveView, isCloud, onClickCopyLink]);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ 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();
|
||||
|
||||
@@ -58,6 +58,7 @@ export const rightSidebarButton = style({
|
||||
export const viewHeaderContainer = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: 0,
|
||||
flexGrow: 1,
|
||||
minWidth: 12,
|
||||
});
|
||||
|
||||
@@ -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 { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
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 { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
||||
@@ -41,6 +41,7 @@ const ToggleButton = ({
|
||||
};
|
||||
|
||||
export const RouteContainer = ({ route }: Props) => {
|
||||
const viewHeaderContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const view = useService(ViewService).view;
|
||||
const viewPosition = useViewPosition();
|
||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
@@ -51,6 +52,14 @@ export const RouteContainer = ({ route }: Props) => {
|
||||
rightSidebar.toggle();
|
||||
}, [rightSidebar]);
|
||||
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 (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
@@ -60,7 +69,10 @@ export const RouteContainer = ({ route }: Props) => {
|
||||
className={styles.leftSidebarButton}
|
||||
/>
|
||||
)}
|
||||
<view.header.Target className={styles.viewHeaderContainer} />
|
||||
<view.header.Target
|
||||
ref={viewHeaderContainerRef}
|
||||
className={styles.viewHeaderContainer}
|
||||
/>
|
||||
{viewPosition.isLast && (
|
||||
<>
|
||||
{rightSidebarHasViews && (
|
||||
|
||||
@@ -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 { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
||||
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
||||
import { 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 { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
|
||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { type Workspace } from '@toeverything/infra';
|
||||
import { useAtomValue } from 'jotai';
|
||||
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 { HeaderDivider } from '../../../components/pure/header';
|
||||
import * as styles from './detail-page-header.css';
|
||||
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
||||
|
||||
function Header({
|
||||
children,
|
||||
@@ -43,6 +46,7 @@ interface PageHeaderProps {
|
||||
workspace: Workspace;
|
||||
}
|
||||
export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
const { hideShare, hideToday } = useDetailPageHeaderResponsive();
|
||||
return (
|
||||
<Header className={styles.header}>
|
||||
<EditorModeSwitch
|
||||
@@ -55,11 +59,13 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
page={page}
|
||||
/>
|
||||
</div>
|
||||
<JournalTodayButton docCollection={workspace.docCollection} />
|
||||
{hideToday ? null : (
|
||||
<JournalTodayButton docCollection={workspace.docCollection} />
|
||||
)}
|
||||
<HeaderDivider />
|
||||
<PageHeaderMenuButton isJournal pageId={page?.id} />
|
||||
{page ? (
|
||||
<SharePageButton isJournal workspace={workspace} page={page} />
|
||||
<PageHeaderMenuButton isJournal page={page} />
|
||||
{page && !hideShare ? (
|
||||
<SharePageButton workspace={workspace} page={page} />
|
||||
) : null}
|
||||
</Header>
|
||||
);
|
||||
@@ -67,6 +73,8 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
|
||||
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||
const { hideCollect, hideShare, hidePresent, showDivider } =
|
||||
useDetailPageHeaderResponsive();
|
||||
|
||||
const onRename = useCallback(() => {
|
||||
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
||||
@@ -82,19 +90,33 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
pageId={page?.id}
|
||||
docCollection={workspace.docCollection}
|
||||
/>
|
||||
<PageHeaderMenuButton rename={onRename} pageId={page?.id} />
|
||||
<FavoriteButton pageId={page?.id} />
|
||||
{hideCollect ? null : <FavoriteButton pageId={page?.id} />}
|
||||
<PageHeaderMenuButton rename={onRename} page={page} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailPageHeader(props: PageHeaderProps) {
|
||||
const { page } = props;
|
||||
const { page, workspace } = props;
|
||||
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
||||
const isInTrash = page.meta?.trash;
|
||||
|
||||
useRegisterCopyLinkCommands({
|
||||
workspaceMeta: workspace.meta,
|
||||
docId: page.id,
|
||||
});
|
||||
|
||||
return isJournal && !isInTrash ? (
|
||||
<JournalPageHeader {...props} />
|
||||
) : (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,11 @@
|
||||
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';
|
||||
|
||||
export const createIsland = () => {
|
||||
@@ -7,8 +13,13 @@ export const createIsland = () => {
|
||||
let mounted = false;
|
||||
let provided = false;
|
||||
return {
|
||||
Target: ({ ...other }: React.HTMLProps<HTMLDivElement>) => {
|
||||
Target: forwardRef(function IslandTarget(
|
||||
{ ...other }: React.HTMLProps<HTMLDivElement>,
|
||||
ref: Ref<HTMLDivElement>
|
||||
) {
|
||||
const target = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => target.current as HTMLDivElement, []);
|
||||
useEffect(() => {
|
||||
if (mounted === true) {
|
||||
throw new Error('Island should not be mounted more than once');
|
||||
@@ -21,7 +32,7 @@ export const createIsland = () => {
|
||||
};
|
||||
}, []);
|
||||
return <div {...other} ref={target}></div>;
|
||||
},
|
||||
}),
|
||||
Provider: ({ children }: React.PropsWithChildren) => {
|
||||
const target = useLiveData(targetLiveData$);
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user