diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts
index a2c591691c..329588f103 100644
--- a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts
+++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts
@@ -46,6 +46,4 @@ export const rowCell = style({
export const container = style({
display: 'flex',
flexDirection: 'column',
- marginTop: 20,
- marginBottom: 4,
});
diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx
index c099d0ad28..88a1282182 100644
--- a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx
+++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx
@@ -1,6 +1,7 @@
import { i18nTime, useI18n } from '@affine/i18n';
import { DateTimeIcon, HistoryIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
+import clsx from 'clsx';
import type { ConfigType } from 'dayjs';
import { useDebouncedValue } from 'foxact/use-debounced-value';
import { type ReactNode, useContext, useMemo } from 'react';
@@ -28,7 +29,13 @@ const RowComponent = ({
);
};
-export const TimeRow = ({ docId }: { docId: string }) => {
+export const TimeRow = ({
+ docId,
+ className,
+}: {
+ docId: string;
+ className?: string;
+}) => {
const t = useI18n();
const manager = useContext(managerContext);
const workspaceService = useService(WorkspaceService);
@@ -88,5 +95,7 @@ export const TimeRow = ({ docId }: { docId: string }) => {
const dTimestampElement = useDebouncedValue(timestampElement, 500);
- return
{dTimestampElement}
;
+ return (
+
{dTimestampElement}
+ );
};
diff --git a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts
index fcccab3ebf..d34aed52aa 100644
--- a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts
+++ b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts
@@ -2,6 +2,8 @@ import { cssVar } from '@toeverything/theme';
import { createVar, globalStyle, style } from '@vanilla-extract/css';
const propertyNameCellWidth = createVar();
+export const rowHPadding = createVar();
+export const fontSize = createVar();
export const root = style({
display: 'flex',
@@ -10,6 +12,16 @@ export const root = style({
fontFamily: cssVar('fontSansFamily'),
vars: {
[propertyNameCellWidth]: '160px',
+ [rowHPadding]: '6px',
+ [fontSize]: cssVar('fontSm'),
+ },
+ '@container': {
+ [`viewport (width <= 640px)`]: {
+ vars: {
+ [rowHPadding]: '0px',
+ [fontSize]: cssVar('fontXs'),
+ },
+ },
},
});
@@ -22,7 +34,7 @@ export const rootCentered = style({
padding: `0 ${cssVar('editorSidePadding', '24px')}`,
'@container': {
[`viewport (width <= 640px)`]: {
- padding: '0 24px',
+ padding: '0 16px',
},
},
});
@@ -39,7 +51,7 @@ export const tableHeaderInfoRow = style({
justifyContent: 'space-between',
alignItems: 'center',
color: cssVar('textSecondaryColor'),
- fontSize: cssVar('fontSm'),
+ fontSize: fontSize,
fontWeight: 500,
minHeight: 34,
'@media': {
@@ -54,9 +66,9 @@ export const tableHeaderSecondaryRow = style({
flexDirection: 'row',
alignItems: 'center',
color: cssVar('textPrimaryColor'),
- fontSize: cssVar('fontSm'),
+ fontSize: fontSize,
fontWeight: 500,
- padding: '0 6px',
+ padding: `0 ${rowHPadding}`,
gap: '8px',
height: 24,
'@media': {
@@ -82,7 +94,7 @@ export const spacer = style({
});
export const tableHeaderBacklinksHint = style({
- padding: '6px',
+ padding: `0 ${rowHPadding}`,
cursor: 'pointer',
borderRadius: '4px',
':hover': {
@@ -103,7 +115,7 @@ export const tableHeaderTimestamp = style({
alignItems: 'start',
gap: '8px',
cursor: 'default',
- padding: '0 6px',
+ padding: `0 ${rowHPadding}`,
});
export const tableHeaderDivider = style({
@@ -273,7 +285,7 @@ export const editablePropertyRowCell = style([
export const propertyRowNameCell = style([
propertyRowCell,
{
- padding: 6,
+ padding: `6px ${rowHPadding}`,
flexShrink: 0,
color: cssVar('textSecondaryColor'),
width: propertyNameCellWidth,
@@ -316,7 +328,7 @@ export const propertyRowValueCell = style([
propertyRowCell,
editablePropertyRowCell,
{
- padding: '6px 8px',
+ padding: `6px ${rowHPadding} 6px 6px`,
border: `1px solid transparent`,
color: cssVar('textPrimaryColor'),
':focus': {
@@ -353,7 +365,7 @@ export const propertyRowValueTextarea = style([
propertyRowValueCell,
{
border: 'none',
- padding: '6px 8px',
+ padding: `6px ${rowHPadding} 6px 8px`,
height: '100%',
position: 'absolute',
top: 0,
@@ -368,7 +380,7 @@ export const propertyRowValueTextareaInvisible = style([
propertyRowValueCell,
{
border: 'none',
- padding: '6px 8px',
+ padding: `6px ${rowHPadding} 6px 8px`,
visibility: 'hidden',
whiteSpace: 'break-spaces',
wordBreak: 'break-all',
@@ -379,7 +391,7 @@ export const propertyRowValueTextareaInvisible = style([
export const propertyRowValueNumberCell = style([
propertyRowValueTextCell,
{
- padding: '6px 8px',
+ padding: `6px ${rowHPadding} 6px 8px`,
},
]);
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx
index 428fdcdbcc..076dbcdd3d 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx
@@ -191,7 +191,7 @@ export const BlocksuiteDocEditor = forwardRef<
) : (
)}
-
+ {!shared ?
: null}
{
+ const currentWorkspace = useService(WorkspaceService).workspace;
+ const docsService = useService(DocsService);
+ const docRecordList = docsService.list;
+ const docListReady = useLiveData(docRecordList.isReady$);
+ const docRecord = useLiveData(docRecordList.doc$(pageId));
+ const viewService = useService(ViewService);
+
+ const queryString = useLiveData(
+ viewService.view.queryString$<{
+ mode?: string;
+ }>()
+ );
+
+ const queryStringMode =
+ queryString.mode && ['edgeless', 'page'].includes(queryString.mode)
+ ? (queryString.mode as DocMode)
+ : null;
+
+ // We only read the querystring mode when entering, so use useState here.
+ const [initialQueryStringMode] = useState(() => queryStringMode);
+
+ const [doc, setDoc] = useState(null);
+ const [editor, setEditor] = useState(null);
+ const editorMode = useLiveData(editor?.mode$);
+
+ useLayoutEffect(() => {
+ if (!docRecord) {
+ return;
+ }
+ const { doc: opened, release } = docsService.open(pageId);
+ setDoc(opened);
+ return () => {
+ release();
+ };
+ }, [docRecord, docsService, pageId]);
+
+ useLayoutEffect(() => {
+ if (!doc) {
+ return;
+ }
+ const editor = doc.scope
+ .get(EditorsService)
+ .createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page');
+ setEditor(editor);
+ return () => {
+ editor.dispose();
+ };
+ }, [doc, initialQueryStringMode]);
+
+ // update editor mode to queryString
+ useEffect(() => {
+ if (editorMode) {
+ viewService.view.updateQueryString(
+ {
+ mode: editorMode,
+ },
+ {
+ replace: true,
+ }
+ );
+ }
+ }, [editorMode, viewService.view]);
+
+ // set sync engine priority target
+ useEffect(() => {
+ currentWorkspace.engine.doc.setPriority(pageId, 10);
+ return () => {
+ currentWorkspace.engine.doc.setPriority(pageId, 5);
+ };
+ }, [currentWorkspace, pageId]);
+
+ const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));
+
+ useEffect(() => {
+ if (doc && isInTrash) {
+ currentWorkspace.docCollection.awarenessStore.setReadonly(
+ doc.blockSuiteDoc.blockCollection,
+ true
+ );
+ }
+ }, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]);
+
+ return {
+ doc,
+ editor,
+ docListReady,
+ };
+};
+
+/**
+ * A common wrapper for detail page for both mobile and desktop page.
+ * It only contains the logic for page loading, context setup, but not the page content.
+ */
+export const DetailPageWrapper = ({
+ pageId,
+ children,
+}: PropsWithChildren<{ pageId: string }>) => {
+ const { doc, editor, docListReady } = useLoadDoc(pageId);
+ // if sync engine has been synced and the page is null, show 404 page.
+ if (docListReady && !doc) {
+ return ;
+ }
+
+ if (!doc || !editor) {
+ return ;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
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 db02c609bd..a35326e778 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,16 +1,14 @@
import { Scrollable, useHasScrollTop } 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 { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
-import type { Editor } from '@affine/core/modules/editor';
-import { EditorService, EditorsService } from '@affine/core/modules/editor';
+import { EditorService } from '@affine/core/modules/editor';
import { RecentDocsService } from '@affine/core/modules/quicksearch';
import { ViewService } from '@affine/core/modules/workbench/services/view';
-import type { DocMode, PageRootService } from '@blocksuite/blocks';
+import type { PageRootService } from '@blocksuite/blocks';
import {
BookmarkBlockService,
customImageProxyMiddleware,
@@ -23,10 +21,8 @@ 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';
import {
DocService,
- DocsService,
FrameworkScope,
globalBlockSuiteSchema,
GlobalContextService,
@@ -36,15 +32,7 @@ import {
WorkspaceService,
} from '@toeverything/infra';
import clsx from 'clsx';
-import type { ReactElement } from 'react';
-import {
- memo,
- useCallback,
- useEffect,
- useLayoutEffect,
- useRef,
- useState,
-} from 'react';
+import { memo, useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import type { Map as YMap } from 'yjs';
@@ -65,9 +53,9 @@ import {
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 { DetailPageWrapper } from './detail-page-wrapper';
import { EditorChatPanel } from './tabs/chat';
import { EditorFramePanel } from './tabs/frame';
import { EditorJournalPanel } from './tabs/journal';
@@ -330,107 +318,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
);
});
-export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
- const currentWorkspace = useService(WorkspaceService).workspace;
- const docsService = useService(DocsService);
- const docRecordList = docsService.list;
- const docListReady = useLiveData(docRecordList.isReady$);
- const docRecord = useLiveData(docRecordList.doc$(pageId));
- const viewService = useService(ViewService);
-
- const queryString = useLiveData(
- viewService.view.queryString$<{
- mode?: string;
- }>()
- );
-
- const queryStringMode =
- queryString.mode && ['edgeless', 'page'].includes(queryString.mode)
- ? (queryString.mode as DocMode)
- : null;
-
- // We only read the querystring mode when entering, so use useState here.
- const [initialQueryStringMode] = useState(() => queryStringMode);
-
- const [doc, setDoc] = useState(null);
- const [editor, setEditor] = useState(null);
- const editorMode = useLiveData(editor?.mode$);
-
- useLayoutEffect(() => {
- if (!docRecord) {
- return;
- }
- const { doc: opened, release } = docsService.open(pageId);
- setDoc(opened);
- return () => {
- release();
- };
- }, [docRecord, docsService, pageId]);
-
- useLayoutEffect(() => {
- if (!doc) {
- return;
- }
- const editor = doc.scope
- .get(EditorsService)
- .createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page');
- setEditor(editor);
- return () => {
- editor.dispose();
- };
- }, [doc, initialQueryStringMode]);
-
- // update editor mode to queryString
- useEffect(() => {
- if (editorMode) {
- viewService.view.updateQueryString(
- {
- mode: editorMode,
- },
- {
- replace: true,
- }
- );
- }
- }, [editorMode, viewService.view]);
-
- // set sync engine priority target
- useEffect(() => {
- currentWorkspace.engine.doc.setPriority(pageId, 10);
- return () => {
- currentWorkspace.engine.doc.setPriority(pageId, 5);
- };
- }, [currentWorkspace, pageId]);
-
- const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));
-
- useEffect(() => {
- if (doc && isInTrash) {
- currentWorkspace.docCollection.awarenessStore.setReadonly(
- doc.blockSuiteDoc.blockCollection,
- true
- );
- }
- }, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]);
-
- // if sync engine has been synced and the page is null, show 404 page.
- if (docListReady && !doc) {
- return ;
- }
-
- if (!doc || !editor) {
- return ;
- }
-
- return (
-
-
-
-
-
- );
-};
-
export const Component = () => {
performanceRenderLogger.debug('DetailPage');
@@ -448,5 +335,9 @@ export const Component = () => {
const pageId = params.pageId;
- return pageId ? : null;
+ return pageId ? (
+
+
+
+ ) : null;
};
diff --git a/packages/frontend/core/src/utils/event.ts b/packages/frontend/core/src/utils/event.ts
index 71786d1698..4d0fd45750 100644
--- a/packages/frontend/core/src/utils/event.ts
+++ b/packages/frontend/core/src/utils/event.ts
@@ -4,6 +4,10 @@ export function stopPropagation(event: BaseSyntheticEvent) {
event.stopPropagation();
}
+export function preventDefault(event: BaseSyntheticEvent) {
+ event.preventDefault();
+}
+
export function stopEvent(event: BaseSyntheticEvent) {
event.stopPropagation();
event.preventDefault();
diff --git a/packages/frontend/mobile/src/components/page-header/styles.css.ts b/packages/frontend/mobile/src/components/page-header/styles.css.ts
index 50b998cb7c..2ea30885ab 100644
--- a/packages/frontend/mobile/src/components/page-header/styles.css.ts
+++ b/packages/frontend/mobile/src/components/page-header/styles.css.ts
@@ -45,5 +45,5 @@ export const prefix = style({
export const suffix = style({
display: 'flex',
alignItems: 'center',
- gap: 18,
+ gap: 6,
});
diff --git a/packages/frontend/mobile/src/pages/workspace/detail.tsx b/packages/frontend/mobile/src/pages/workspace/detail.tsx
deleted file mode 100644
index 937c081665..0000000000
--- a/packages/frontend/mobile/src/pages/workspace/detail.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { PageHeader } from '../../components/page-header';
-
-export const Component = () => {
- return (
- <>
-
- /workspace/:workspaceId/:pageId
;
- >
- );
-};
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/journal-icon-button.tsx b/packages/frontend/mobile/src/pages/workspace/detail/journal-icon-button.tsx
new file mode 100644
index 0000000000..f111fa9128
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/journal-icon-button.tsx
@@ -0,0 +1,43 @@
+import { IconButton, MobileMenu } from '@affine/component';
+import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
+import { EditorJournalPanel } from '@affine/core/pages/workspace/detail-page/tabs/journal';
+import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc';
+import { useService, WorkspaceService } from '@toeverything/infra';
+
+export const JournalIconButton = ({
+ docId,
+ className,
+}: {
+ docId: string;
+ className?: string;
+}) => {
+ const workspace = useService(WorkspaceService).workspace;
+ const { journalDate, isJournal } = useJournalInfoHelper(
+ workspace.docCollection,
+ docId
+ );
+ const Icon = journalDate
+ ? journalDate.isBefore(new Date(), 'day')
+ ? YesterdayIcon
+ : journalDate.isAfter(new Date(), 'day')
+ ? TomorrowIcon
+ : TodayIcon
+ : TodayIcon;
+
+ if (!isJournal) {
+ return null;
+ }
+
+ return (
+ }
+ contentOptions={{
+ align: 'center',
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.css.ts b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.css.ts
new file mode 100644
index 0000000000..ad20d552c0
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.css.ts
@@ -0,0 +1,91 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { globalStyle, style } from '@vanilla-extract/css';
+
+export const root = style({
+ background: cssVarV2('layer/background/primary'),
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+export const header = style({
+ background: cssVarV2('layer/background/primary'),
+ position: 'sticky',
+ top: 0,
+ zIndex: 1,
+});
+
+export const mainContainer = style({
+ containerType: 'inline-size',
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ overflow: 'hidden',
+ borderTop: `0.5px solid transparent`,
+ transition: 'border-color 0.2s',
+ selectors: {
+ '&[data-dynamic-top-border="false"]': {
+ borderColor: cssVar('borderColor'),
+ },
+ '&[data-has-scroll-top="true"]': {
+ borderColor: cssVar('borderColor'),
+ },
+ },
+});
+
+export const editorContainer = style({
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ zIndex: 0,
+});
+// brings styles of .affine-page-viewport from blocksuite
+export const affineDocViewport = style({
+ display: 'flex',
+ flexDirection: 'column',
+ containerName: 'viewport',
+ containerType: 'inline-size',
+ background: cssVarV2('layer/background/primary'),
+ selectors: {
+ '&[data-mode="edgeless"]': {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+ },
+});
+
+export const scrollbar = style({
+ marginRight: '4px',
+});
+
+globalStyle('.doc-title-container', {
+ fontSize: cssVar('fontH1'),
+ '@container': {
+ [`viewport (width <= 640px)`]: {
+ padding: '10px 16px',
+ lineHeight: '38px',
+ },
+ },
+});
+
+globalStyle('.affine-page-root-block-container', {
+ '@container': {
+ [`viewport (width <= 640px)`]: {
+ paddingLeft: 16,
+ paddingRight: 16,
+ },
+ },
+});
+
+export const journalIconButton = style({
+ position: 'absolute',
+ zIndex: 1,
+ top: 16,
+ right: 12,
+ display: 'flex',
+});
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx
new file mode 100644
index 0000000000..de23e21331
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx
@@ -0,0 +1,225 @@
+import { IconButton } from '@affine/component';
+import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
+import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
+import { useRegisterBlocksuiteEditorCommands } from '@affine/core/hooks/affine/use-register-blocksuite-editor-commands';
+import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
+import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
+import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
+import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
+import { EditorService } from '@affine/core/modules/editor';
+import { WorkbenchService } from '@affine/core/modules/workbench';
+import { ViewService } from '@affine/core/modules/workbench/services/view';
+import { DetailPageWrapper } from '@affine/core/pages/workspace/detail-page/detail-page-wrapper';
+import type { PageRootService } from '@blocksuite/blocks';
+import {
+ BookmarkBlockService,
+ customImageProxyMiddleware,
+ EmbedGithubBlockService,
+ EmbedLoomBlockService,
+ EmbedYoutubeBlockService,
+ ImageBlockService,
+} from '@blocksuite/blocks';
+import { DisposableGroup } from '@blocksuite/global/utils';
+import { ShareIcon } from '@blocksuite/icons/rc';
+import { type AffineEditorContainer } from '@blocksuite/presets';
+import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
+import {
+ DocService,
+ FrameworkScope,
+ GlobalContextService,
+ useLiveData,
+ useServices,
+ WorkspaceService,
+} from '@toeverything/infra';
+import clsx from 'clsx';
+import { useCallback, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+
+import { PageHeader } from '../../../components';
+import { JournalIconButton } from './journal-icon-button';
+import * as styles from './mobile-detail-page.css';
+import { PageHeaderMenuButton } from './page-header-more-button';
+
+const DetailPageImpl = () => {
+ const { editorService, docService, workspaceService, globalContextService } =
+ useServices({
+ WorkbenchService,
+ ViewService,
+ EditorService,
+ DocService,
+ WorkspaceService,
+ GlobalContextService,
+ });
+ const editor = editorService.editor;
+ const workspace = workspaceService.workspace;
+ const docCollection = workspace.docCollection;
+ const globalContext = globalContextService.globalContext;
+ const doc = docService.doc;
+
+ const mode = useLiveData(editor.mode$);
+
+ const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
+ const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
+ const editorContainer = useLiveData(editor.editorContainer$);
+
+ const { setDocReadonly } = useDocMetaHelper(workspace.docCollection);
+
+ // TODO(@eyhn): remove jotai here
+ const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
+
+ useEffect(() => {
+ setActiveBlockSuiteEditor(editorContainer);
+ }, [editorContainer, setActiveBlockSuiteEditor]);
+
+ useEffect(() => {
+ globalContext.docId.set(doc.id);
+ globalContext.isDoc.set(true);
+
+ return () => {
+ globalContext.docId.set(null);
+ globalContext.isDoc.set(false);
+ };
+ }, [doc, globalContext]);
+
+ useEffect(() => {
+ globalContext.docMode.set(mode);
+
+ return () => {
+ globalContext.docMode.set(null);
+ };
+ }, [doc, globalContext, mode]);
+
+ useEffect(() => {
+ setDocReadonly(doc.id, true);
+ }, [doc.id, setDocReadonly]);
+
+ useEffect(() => {
+ globalContext.isTrashDoc.set(!!isInTrash);
+
+ return () => {
+ globalContext.isTrashDoc.set(null);
+ };
+ }, [globalContext, isInTrash]);
+
+ useRegisterBlocksuiteEditorCommands(editor);
+ const title = useLiveData(doc.title$);
+ usePageDocumentTitle(title);
+
+ const onLoad = useCallback(
+ (_bsPage: BlockSuiteDoc, editorContainer: AffineEditorContainer) => {
+ // blocksuite editor host
+ const editorHost = editorContainer.host;
+
+ // provide image proxy endpoint to blocksuite
+ editorHost?.std.clipboard.use(
+ customImageProxyMiddleware(runtimeConfig.imageProxyUrl)
+ );
+ ImageBlockService.setImageProxyURL(runtimeConfig.imageProxyUrl);
+
+ // provide link preview endpoint to blocksuite
+ BookmarkBlockService.setLinkPreviewEndpoint(runtimeConfig.linkPreviewUrl);
+ EmbedGithubBlockService.setLinkPreviewEndpoint(
+ runtimeConfig.linkPreviewUrl
+ );
+ EmbedYoutubeBlockService.setLinkPreviewEndpoint(
+ runtimeConfig.linkPreviewUrl
+ );
+ EmbedLoomBlockService.setLinkPreviewEndpoint(
+ runtimeConfig.linkPreviewUrl
+ );
+
+ // provide page mode and updated date to blocksuite
+ const pageService =
+ editorHost?.std.spec.getService('affine:page');
+ const disposable = new DisposableGroup();
+ if (pageService) {
+ disposable.add(
+ pageService.slots.docLinkClicked.on(({ docId, blockId }) => {
+ return blockId
+ ? jumpToPageBlock(docCollection.id, docId, blockId)
+ : openPage(docCollection.id, docId);
+ })
+ );
+ disposable.add(
+ pageService.slots.tagClicked.on(({ tagId }) => {
+ jumpToTag(workspace.id, tagId);
+ })
+ );
+ }
+
+ editor.setEditorContainer(editorContainer);
+
+ return () => {
+ disposable.dispose();
+ };
+ },
+ [
+ editor,
+ jumpToPageBlock,
+ docCollection.id,
+ openPage,
+ jumpToTag,
+ workspace.id,
+ ]
+ );
+
+ return (
+
+
+
+ {/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
+
+
+
+
+
+
+
+ );
+};
+
+export const Component = () => {
+ const params = useParams();
+ const pageId = params.pageId;
+
+ if (!pageId) {
+ return null;
+ }
+
+ return (
+
+
+
+ }
+ />
+
+ >
+ }
+ />
+
+
+
+ );
+};
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.css.ts b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.css.ts
new file mode 100644
index 0000000000..a1ae246ec4
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.css.ts
@@ -0,0 +1,16 @@
+import { cssVar } from '@toeverything/theme';
+import { style } from '@vanilla-extract/css';
+
+export const iconButton = style({
+ selectors: {
+ '&[data-state=open]': {
+ backgroundColor: cssVar('hoverColor'),
+ },
+ },
+ padding: '10px',
+});
+
+export const outlinePanel = style({
+ maxHeight: '60vh',
+ overflow: 'auto',
+});
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.tsx b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.tsx
new file mode 100644
index 0000000000..cedb3eff8b
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.tsx
@@ -0,0 +1,132 @@
+import { IconButton, toast } from '@affine/component';
+import {
+ MenuSeparator,
+ MobileMenu,
+ MobileMenuItem,
+} from '@affine/component/ui/menu';
+import { useFavorite } from '@affine/core/components/blocksuite/block-suite-header/favorite';
+import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
+import { track } from '@affine/core/mixpanel';
+import { EditorService } from '@affine/core/modules/editor';
+import { EditorOutlinePanel } from '@affine/core/pages/workspace/detail-page/tabs/outline';
+import { preventDefault } from '@affine/core/utils';
+import { useI18n } from '@affine/i18n';
+import {
+ EdgelessIcon,
+ InformationIcon,
+ MoreHorizontalIcon,
+ PageIcon,
+ TocIcon,
+} from '@blocksuite/icons/rc';
+import { useLiveData, useService } from '@toeverything/infra';
+import { useCallback } from 'react';
+
+import * as styles from './page-header-more-button.css';
+import { DocInfoSheet } from './sheets/doc-info';
+
+type PageMenuProps = {
+ docId: string;
+};
+
+export const PageHeaderMenuButton = ({ docId }: PageMenuProps) => {
+ const t = useI18n();
+
+ const editorService = useService(EditorService);
+ const editorContainer = useLiveData(editorService.editor.editorContainer$);
+
+ const isInTrash = useLiveData(
+ editorService.editor.doc.meta$.map(meta => meta.trash)
+ );
+ const currentMode = useLiveData(editorService.editor.mode$);
+
+ const { favorite, toggleFavorite } = useFavorite(docId);
+
+ const handleSwitchMode = useCallback(() => {
+ editorService.editor.toggleMode();
+ track.$.header.docOptions.switchPageMode({
+ mode: currentMode === 'page' ? 'edgeless' : 'page',
+ });
+ toast(
+ currentMode === 'page'
+ ? t['com.affine.toastMessage.edgelessMode']()
+ : t['com.affine.toastMessage.pageMode']()
+ );
+ }, [currentMode, editorService, t]);
+
+ const handleMenuOpenChange = useCallback((open: boolean) => {
+ if (open) {
+ track.$.header.docOptions.open();
+ }
+ }, []);
+
+ const handleToggleFavorite = useCallback(() => {
+ track.$.header.docOptions.toggleFavorite();
+ toggleFavorite();
+ }, [toggleFavorite]);
+
+ const EditMenu = (
+ <>
+ : }
+ data-testid="editor-option-menu-edgeless"
+ onSelect={handleSwitchMode}
+ >
+ {t['Convert to ']()}
+ {currentMode === 'page'
+ ? t['com.affine.pageMode.edgeless']()
+ : t['com.affine.pageMode.page']()}
+
+ }
+ >
+ {favorite
+ ? t['com.affine.favoritePageOperation.remove']()
+ : t['com.affine.favoritePageOperation.add']()}
+
+
+ }>
+ }
+ onClick={preventDefault}
+ >
+ {t['com.affine.page-properties.page-info.view']()}
+
+
+
+
+
+
+
+ >
+ );
+ if (isInTrash) {
+ return null;
+ }
+ return (
+
+ );
+};
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.css.ts b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.css.ts
new file mode 100644
index 0000000000..fe23253ea6
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.css.ts
@@ -0,0 +1,51 @@
+import { rowHPadding } from '@affine/core/components/affine/page-properties/styles.css';
+import { style } from '@vanilla-extract/css';
+
+export const viewport = style({
+ vars: {
+ [rowHPadding]: '0px',
+ },
+});
+
+export const item = style({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ gap: 4,
+ height: 34,
+ padding: '0 20px',
+
+ fontSize: 17,
+ lineHeight: '22px',
+ fontWeight: 400,
+ letterSpacing: -0.43,
+});
+
+export const linksRow = style({
+ padding: '0 16px',
+});
+
+export const timeRow = style({
+ padding: '0 16px',
+});
+
+export const tagsRow = style({
+ padding: '0 16px',
+});
+
+export const properties = style({
+ padding: '0 16px',
+});
+
+export const scrollBar = style({
+ width: 6,
+ transform: 'translateX(-4px)',
+});
+
+export const rowNameContainer = style({
+ display: 'flex',
+ flexDirection: 'row',
+ gap: 6,
+ padding: 6,
+ width: '160px',
+});
diff --git a/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.tsx b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.tsx
new file mode 100644
index 0000000000..e5e75d21b3
--- /dev/null
+++ b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.tsx
@@ -0,0 +1,100 @@
+import { Divider, Scrollable } from '@affine/component';
+import {
+ PagePropertyRow,
+ SortableProperties,
+ usePagePropertiesManager,
+} from '@affine/core/components/affine/page-properties';
+import { managerContext } from '@affine/core/components/affine/page-properties/common';
+import { LinksRow } from '@affine/core/components/affine/page-properties/info-modal/links-row';
+import { TagsRow } from '@affine/core/components/affine/page-properties/info-modal/tags-row';
+import { TimeRow } from '@affine/core/components/affine/page-properties/info-modal/time-row';
+import { DocsSearchService } from '@affine/core/modules/docs-search';
+import { useI18n } from '@affine/i18n';
+import { LiveData, useLiveData, useService } from '@toeverything/infra';
+import { Suspense, useMemo } from 'react';
+
+import * as styles from './doc-info.css';
+
+export const DocInfoSheet = ({ docId }: { docId: string }) => {
+ const manager = usePagePropertiesManager(docId);
+ const docsSearchService = useService(DocsSearchService);
+ const t = useI18n();
+
+ const links = useLiveData(
+ useMemo(
+ () => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
+ [docId, docsSearchService]
+ )
+ );
+ const backlinks = useLiveData(
+ useMemo(
+ () => LiveData.from(docsSearchService.watchRefsTo(docId), null),
+ [docId, docsSearchService]
+ )
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/frontend/mobile/src/router.tsx b/packages/frontend/mobile/src/router.tsx
index fd48bc8a83..3e35606927 100644
--- a/packages/frontend/mobile/src/router.tsx
+++ b/packages/frontend/mobile/src/router.tsx
@@ -95,7 +95,7 @@ export const viewRoutes = [
},
{
path: '/:pageId',
- lazy: () => import('./pages/workspace/detail'),
+ lazy: () => import('./pages/workspace/detail/mobile-detail-page'),
},
{
path: '*',