refactor(core): use custom scrollbar for editor and adjust shared page style (#5752)

Close TOV-481
- Use a custom scrollbar component for editor
- Modified the header of the share page and added a new footer
This commit is contained in:
JimmFly
2024-02-26 08:54:52 +00:00
parent e9f9eea80c
commit 0be62d892d
26 changed files with 504 additions and 209 deletions

View File

@@ -4,7 +4,7 @@ export const modalHeaderWrapper = style({});
globalStyle(`${modalHeaderWrapper} .logo`, { globalStyle(`${modalHeaderWrapper} .logo`, {
fontSize: cssVar('fontH3'), fontSize: cssVar('fontH3'),
fontWeight: 600, fontWeight: 600,
color: cssVar('blue'), color: cssVar('black'),
marginRight: '6px', marginRight: '6px',
verticalAlign: 'middle', verticalAlign: 'middle',
}); });
@@ -32,7 +32,7 @@ export const authInputWrapper = style({
}); });
globalStyle(`${authInputWrapper} label`, { globalStyle(`${authInputWrapper} label`, {
display: 'block', display: 'block',
color: 'var(--light-text-color-text-secondary-color, #8E8D91)', color: cssVar('textSecondaryColor'),
marginBottom: '4px', marginBottom: '4px',
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
fontWeight: 600, fontWeight: 600,

View File

@@ -222,30 +222,6 @@ input[type='number']::-webkit-outer-spin-button {
height: 0; height: 0;
} }
.affine-doc-viewport::-webkit-scrollbar {
width: 20px;
/* Chrome Safari */
height: 20px;
}
.affine-doc-viewport::-webkit-scrollbar-thumb {
border-radius: 12px;
background-clip: padding-box;
border-style: solid;
border-width: 7px;
border-color: transparent;
transition: all 0.2s;
}
.affine-doc-viewport:hover::-webkit-scrollbar-thumb {
background-color: var(--affine-divider-color);
}
.affine-doc-viewport:hover::-webkit-scrollbar-thumb:hover {
background-color: var(--affine-icon-color);
border-width: 5px;
}
.editor-wrapper { .editor-wrapper {
position: relative; position: relative;
padding-right: 0; padding-right: 0;
@@ -282,7 +258,6 @@ affine-block-hub {
.editor-wrapper { .editor-wrapper {
width: 100%; width: 100%;
height: 100%;
} }
/* issue: https://github.com/toeverything/AFFiNE/issues/2004 */ /* issue: https://github.com/toeverything/AFFiNE/issues/2004 */

View File

@@ -139,11 +139,16 @@ const HistoryEditorPreview = ({
{snapshotPage ? ( {snapshotPage ? (
<AffineErrorBoundary> <AffineErrorBoundary>
<BlockSuiteEditor <Scrollable.Root>
className={styles.editor} <Scrollable.Viewport>
mode={mode} <BlockSuiteEditor
page={snapshotPage} className={styles.editor}
/> mode={mode}
page={snapshotPage}
/>
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</AffineErrorBoundary> </AffineErrorBoundary>
) : ( ) : (
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>

View File

@@ -95,7 +95,6 @@ export const previewHeaderTimestamp = style({
export const editor = style({ export const editor = style({
height: '100%', height: '100%',
flexGrow: 1, flexGrow: 1,
overflow: 'hidden',
}); });
export const rowWrapper = style({ export const rowWrapper = style({
display: 'flex', display: 'flex',

View File

@@ -104,7 +104,10 @@ export const BlocksuiteDocEditor = forwardRef<
return ( return (
<div className={styles.docEditorRoot}> <div className={styles.docEditorRoot}>
<div className={clsx('affine-doc-viewport', styles.affineDocViewport)}> <div
className={clsx('affine-doc-viewport', styles.affineDocViewport)}
data-doc-viewport={true}
>
{!isJournal ? ( {!isJournal ? (
<adapted.DocTitle page={page} ref={titleRef} /> <adapted.DocTitle page={page} ref={titleRef} />
) : ( ) : (

View File

@@ -2,8 +2,6 @@ import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const docEditorRoot = style({ export const docEditorRoot = style({
display: 'block', display: 'block',
height: '100%',
overflow: 'hidden',
background: cssVar('backgroundPrimaryColor'), background: cssVar('backgroundPrimaryColor'),
}); });
@@ -11,9 +9,6 @@ export const docEditorRoot = style({
export const affineDocViewport = style({ export const affineDocViewport = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%',
overflowX: 'hidden',
overflowY: 'auto',
userSelect: 'none', userSelect: 'none',
containerName: 'viewport', containerName: 'viewport',
// todo: find out what this does in bs // todo: find out what this does in bs
@@ -25,6 +20,13 @@ export const affineDocViewport = style({
zIndex: -1, zIndex: -1,
}, },
}, },
selectors: {
'&[data-doc-viewport="true"]': {
overflowX: 'visible',
overflowY: 'visible',
height: 'auto',
},
},
}); });
export const docContainer = style({ export const docContainer = style({

View File

@@ -1,26 +0,0 @@
import { Logo1Icon } from '@blocksuite/icons';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import * as styles from './styles.css';
import { PublishPageUserAvatar } from './user-avatar';
const ShareHeaderLeftItem = () => {
const loginStatus = useCurrentLoginStatus();
if (loginStatus === 'authenticated') {
return <PublishPageUserAvatar />;
}
return (
<a
href="https://affine.pro/"
target="_blank"
rel="noreferrer"
className={styles.iconWrapper}
data-testid="share-page-logo"
>
<Logo1Icon />
</a>
);
};
export default ShareHeaderLeftItem;

View File

@@ -1,15 +0,0 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const iconWrapper = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '24px',
cursor: 'pointer',
color: cssVar('textPrimaryColor'),
selectors: {
'&:visited': {
color: cssVar('textPrimaryColor'),
},
},
});

View File

@@ -1,45 +0,0 @@
import { Avatar } from '@affine/component/ui/avatar';
import { Menu, MenuIcon, MenuItem } from '@affine/component/ui/menu';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SignOutIcon } from '@blocksuite/icons';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { signOutCloud } from '../../../utils/cloud-utils';
import * as styles from './styles.css';
export const PublishPageUserAvatar = () => {
const user = useCurrentUser();
const t = useAFFiNEI18N();
const location = useLocation();
const handleSignOut = useAsyncCallback(async () => {
await signOutCloud({ callbackUrl: location.pathname });
}, [location.pathname]);
const menuItem = useMemo(() => {
return (
<MenuItem
preFix={
<MenuIcon>
<SignOutIcon />
</MenuIcon>
}
data-testid="share-page-sign-out-option"
onClick={handleSignOut}
>
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
);
}, [handleSignOut, t]);
return (
<Menu items={menuItem}>
<div className={styles.iconWrapper} data-testid="share-page-user-avatar">
<Avatar size={24} url={user.image} name={user.name} />
</div>
</Menu>
);
};

View File

@@ -1,12 +1,17 @@
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { useCurrentUser } from '@affine/core/hooks/affine/use-current-user';
import { useMembers } from '@affine/core/hooks/affine/use-members';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback, useEffect } from 'react';
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { useMembers } from '../../../hooks/affine/use-members';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import type { ShareHeaderRightItemProps } from './index'; import type { ShareHeaderRightItemProps } from './index';
import * as styles from './styles.css';
export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => { export const AuthenticatedItem = ({
setIsMember,
...props
}: { setIsMember: (value: boolean) => void } & ShareHeaderRightItemProps) => {
const { workspaceId, pageId } = props; const { workspaceId, pageId } = props;
const user = useCurrentUser(); const user = useCurrentUser();
const members = useMembers(workspaceId, 0); const members = useMembers(workspaceId, 0);
@@ -14,11 +19,21 @@ export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const { jumpToPage } = useNavigateHelper(); const { jumpToPage } = useNavigateHelper();
const handleEdit = useCallback(() => {
jumpToPage(workspaceId, pageId);
}, [workspaceId, pageId, jumpToPage]);
useEffect(() => {
if (isMember) {
setIsMember(true);
}
}, [isMember, setIsMember]);
if (isMember) { if (isMember) {
return ( return (
<Button <Button
type="plain" className={styles.editButton}
onClick={() => jumpToPage(workspaceId, pageId)} onClick={handleEdit}
data-testid="share-page-edit-button" data-testid="share-page-edit-button"
> >
{t['Edit']()} {t['Edit']()}

View File

@@ -1,8 +1,11 @@
import type { PageMode } from '../../../atoms'; import type { PageMode } from '@affine/core/atoms';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
import { useState } from 'react';
import { AuthenticatedItem } from './authenticated-item'; import { AuthenticatedItem } from './authenticated-item';
import { PresentButton } from './present'; import { PresentButton } from './present';
import * as styles from './styles.css'; import * as styles from './styles.css';
import { PublishPageUserAvatar } from './user-avatar';
export type ShareHeaderRightItemProps = { export type ShareHeaderRightItemProps = {
workspaceId: string; workspaceId: string;
@@ -13,14 +16,25 @@ export type ShareHeaderRightItemProps = {
const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => { const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => {
const loginStatus = useCurrentLoginStatus(); const loginStatus = useCurrentLoginStatus();
const { publishMode } = props; const { publishMode } = props;
const [isMember, setIsMember] = useState(false);
// TODO: Add TOC // TODO: Add TOC
return ( return (
<div className={styles.rightItemContainer}> <div className={styles.rightItemContainer}>
{loginStatus === 'authenticated' ? ( {loginStatus === 'authenticated' ? (
<AuthenticatedItem {...props} /> <AuthenticatedItem setIsMember={setIsMember} {...props} />
) : null} ) : null}
{publishMode === 'edgeless' ? <PresentButton /> : null} {publishMode === 'edgeless' ? <PresentButton /> : null}
{loginStatus === 'authenticated' ? (
<>
<div
className={styles.headerDivider}
data-is-member={isMember}
data-is-edgeless={publishMode === 'edgeless'}
/>
<PublishPageUserAvatar />
</>
) : null}
</div> </div>
); );
}; };

View File

@@ -1,4 +1,5 @@
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 { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { EdgelessPageService } from '@blocksuite/blocks'; import type { EdgelessPageService } from '@blocksuite/blocks';
import { PresentationIcon } from '@blocksuite/icons'; import { PresentationIcon } from '@blocksuite/icons';
@@ -9,15 +10,15 @@ import * as styles from './styles.css';
export const PresentButton = () => { export const PresentButton = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [isPresent, setIsPresent] = useState(false); const [isPresent, setIsPresent] = useState(false);
const [editor] = useActiveBlocksuiteEditor();
const handlePresent = useCallback(() => { const handlePresent = useCallback(() => {
// TODO: use editor Atom const editorHost = editor?.host;
const editorRoot = document.querySelector('editor-host'); if (!editorHost || isPresent) return;
if (!editorRoot || isPresent) return;
// TODO: use surfaceService subAtom // TODO: use surfaceService subAtom
const enterPresentationMode = () => { const enterPresentationMode = () => {
const edgelessPageService = editorRoot?.spec.getService( const edgelessPageService = editorHost.spec.getService(
'affine:page' 'affine:page'
) as EdgelessPageService; ) as EdgelessPageService;
@@ -33,16 +34,15 @@ export const PresentButton = () => {
enterPresentationMode(); enterPresentationMode();
setIsPresent(true); setIsPresent(true);
}, [isPresent]); }, [editor?.host, isPresent]);
useEffect(() => { useEffect(() => {
if (!isPresent) return; if (!isPresent) return;
// TODO: use editor Atom const editorHost = editor?.host;
const editorRoot = document.querySelector('editor-host'); if (!editorHost) return;
if (!editorRoot) return;
const edgelessPage = editorRoot?.querySelector('affine-edgeless-page'); const edgelessPage = editorHost?.querySelector('affine-edgeless-page');
if (!edgelessPage) return; if (!edgelessPage) return;
edgelessPage.slots.edgelessToolUpdated.on(() => { edgelessPage.slots.edgelessToolUpdated.on(() => {
@@ -52,15 +52,15 @@ export const PresentButton = () => {
return () => { return () => {
edgelessPage.slots.edgelessToolUpdated.dispose(); edgelessPage.slots.edgelessToolUpdated.dispose();
}; };
}, [isPresent]); }, [editor?.host, isPresent]);
return ( return (
<Button <Button
type="primary"
icon={<PresentationIcon />} icon={<PresentationIcon />}
className={styles.presentButton} className={styles.presentButton}
onClick={handlePresent} onClick={handlePresent}
disabled={isPresent} disabled={isPresent}
withoutHoverStyle
> >
{t['com.affine.share-page.header.present']()} {t['com.affine.share-page.header.present']()}
</Button> </Button>

View File

@@ -1,5 +1,6 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const iconWrapper = style({ export const iconWrapper = style({
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
@@ -16,9 +17,117 @@ export const iconWrapper = style({
export const rightItemContainer = style({ export const rightItemContainer = style({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: '16px',
padding: '0 8px', padding: '0 8px',
}); });
export const headerDivider = style({
width: '1px',
height: '20px',
background: cssVar('borderColor'),
display: 'none',
selectors: {
'&[data-is-member="true"]': {
display: 'block',
},
'&[data-is-edgeless="true"]': {
display: 'block',
},
},
'@media': {
'screen and (max-width: 640px)': {
selectors: {
'&[data-is-member="false"][data-is-edgeless="true"]': {
display: 'none',
},
},
},
},
});
export const presentButton = style({ export const presentButton = style({
gap: '4px', gap: '4px',
background: cssVar('black'),
color: cssVar('white'),
borderColor: cssVar('pureBlack10'),
boxShadow: cssVar('buttonInnerShadow'),
'@media': {
'screen and (max-width: 640px)': {
display: 'none',
},
},
});
globalStyle(`${presentButton} svg`, {
color: cssVar('white'),
});
export const editButton = style({
padding: '4px 8px',
});
export const userPlanButton = style({
display: 'flex',
fontSize: cssVar('fontXs'),
height: 20,
fontWeight: 500,
color: cssVar('pureWhite'),
backgroundColor: cssVar('brandColor'),
padding: '0 4px',
borderRadius: 4,
justifyContent: 'center',
alignItems: 'center',
});
export const accountCard = style({
padding: '4px 8px',
borderRadius: '8px',
userSelect: 'none',
display: 'flex',
columnGap: '10px',
justifyContent: 'space-between',
alignItems: 'center',
});
export const avatar = style({
width: '28px',
height: '28px',
borderRadius: '50%',
fontSize: '20px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0,
});
export const content = style({
flexGrow: '1',
minWidth: 0,
});
export const nameContainer = style({
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
width: '100%',
gap: '4px',
height: '22px',
});
export const userName = style({
fontSize: cssVar('fontSm'),
fontWeight: 600,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
height: '22px',
});
export const userEmail = style({
fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'),
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexGrow: 1,
height: '20px',
}); });

View File

@@ -0,0 +1,91 @@
import { Avatar } from '@affine/component/ui/avatar';
import {
Menu,
MenuIcon,
MenuItem,
MenuSeparator,
} from '@affine/component/ui/menu';
import { useCurrentUser } from '@affine/core/hooks/affine/use-current-user';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
import { signOutCloud } from '@affine/core/utils/cloud-utils';
import { SubscriptionPlan } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SignOutIcon } from '@blocksuite/icons';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import * as styles from './styles.css';
const UserInfo = () => {
const user = useCurrentUser();
const [subscription] = useUserSubscription();
const plan = subscription?.plan ?? SubscriptionPlan.Free;
return (
<div className={styles.accountCard}>
<Avatar
size={28}
name={user.name}
url={user.image}
className={styles.avatar}
/>
<div className={styles.content}>
<div className={styles.nameContainer}>
<div className={styles.userName} title={user.name}>
{user.name}
</div>
<div className={styles.userPlanButton}>{plan}</div>
</div>
<div className={styles.userEmail} title={user.email}>
{user.email}
</div>
</div>
</div>
);
};
export const PublishPageUserAvatar = () => {
const user = useCurrentUser();
const t = useAFFiNEI18N();
const location = useLocation();
const handleSignOut = useAsyncCallback(async () => {
await signOutCloud({ callbackUrl: location.pathname });
}, [location.pathname]);
const menuItem = useMemo(() => {
return (
<>
<UserInfo />
<MenuSeparator />
<MenuItem
preFix={
<MenuIcon>
<SignOutIcon />
</MenuIcon>
}
data-testid="share-page-sign-out-option"
onClick={handleSignOut}
>
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
</>
);
}, [handleSignOut, t]);
return (
<Menu
items={menuItem}
contentOptions={{
style: {
transform: 'translateX(-16px)',
},
}}
>
<div className={styles.iconWrapper} data-testid="share-page-user-avatar">
<Avatar size={24} url={user.image} name={user.name} />
</div>
</Menu>
);
};

View File

@@ -1,7 +1,6 @@
import { globalStyle, style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const editor = style({ export const editor = style({
flex: 1, flex: 1,
overflow: 'auto',
selectors: { selectors: {
'&.full-screen': { '&.full-screen': {
vars: { vars: {
@@ -9,16 +8,11 @@ export const editor = style({
'--affine-editor-side-padding': '15px', '--affine-editor-side-padding': '15px',
}, },
}, },
'&.is-public-page': {
height: '100%',
},
}, },
}); });
globalStyle( globalStyle(
`${editor} .affine-doc-viewport:not(.affine-embed-synced-doc-editor)`, `${editor} .affine-doc-viewport:not(.affine-embed-synced-doc-editor)`,
{ {
paddingBottom: '150px', paddingBottom: '150px',
paddingLeft: '20px',
scrollbarGutter: 'stable',
} }
); );

View File

@@ -108,7 +108,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
<Editor <Editor
className={clsx(styles.editor, { className={clsx(styles.editor, {
'full-screen': appSettings.fullWidthLayout, 'full-screen': appSettings.fullWidthLayout,
'is-public-page': isPublic,
})} })}
style={ style={
{ {

View File

@@ -0,0 +1,58 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
height: '100%',
overflow: 'hidden',
width: '100%',
});
export const mainContainer = style({
display: 'flex',
flex: 1,
height: '100%',
position: 'relative',
flexDirection: 'column',
minWidth: 0,
overflow: 'hidden',
background: cssVar('backgroundPrimaryColor'),
});
export const editorContainer = style({
position: 'relative',
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
zIndex: 0,
});
export const link = style({
position: 'absolute',
right: '50%',
transform: 'translateX(50%)',
bottom: '20px',
zIndex: cssVar('zIndexPopover'),
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
background: cssVar('black'),
borderRadius: '8px',
border: `1px solid ${cssVar('pureBlack10')}`,
boxShadow: cssVar('--affine-button-inner-shadow'),
color: cssVar('white'),
padding: '8px 18px',
gap: '4px',
'@media': {
print: {
display: 'none',
},
},
});
export const linkText = style({
padding: '0px 4px',
fontSize: cssVar('fontBase'),
fontWeight: 700,
whiteSpace: 'nowrap',
});

View File

@@ -1,11 +1,15 @@
import { Scrollable } from '@affine/component';
import { MainContainer } from '@affine/component/workspace'; import { MainContainer } from '@affine/component/workspace';
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state'; import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { fetchWithTraceReport } from '@affine/graphql'; import { fetchWithTraceReport } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
AffineCloudBlobStorage, AffineCloudBlobStorage,
StaticBlobStorage, StaticBlobStorage,
} from '@affine/workspace-impl'; } from '@affine/workspace-impl';
import { Logo1Icon } from '@blocksuite/icons';
import { import {
EmptyBlobStorage, EmptyBlobStorage,
LocalBlobStorage, LocalBlobStorage,
@@ -36,6 +40,8 @@ import { PageDetailEditor } from '../../components/page-detail-editor';
import { SharePageNotFoundError } from '../../components/share-page-not-found-error'; import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
import { CurrentPageService } from '../../modules/page'; import { CurrentPageService } from '../../modules/page';
import { CurrentWorkspaceService } from '../../modules/workspace'; import { CurrentWorkspaceService } from '../../modules/workspace';
import * as styles from './share-detail-page.css';
import { ShareFooter } from './share-footer';
import { ShareHeader } from './share-header'; import { ShareHeader } from './share-header';
type DocPublishMode = 'edgeless' | 'page'; type DocPublishMode = 'edgeless' | 'page';
@@ -123,6 +129,7 @@ export const Component = () => {
const workspaceManager = useService(WorkspaceManager); const workspaceManager = useService(WorkspaceManager);
const currentWorkspace = useService(CurrentWorkspaceService); const currentWorkspace = useService(CurrentWorkspaceService);
const t = useAFFiNEI18N();
useEffect(() => { useEffect(() => {
// create a workspace for share page // create a workspace for share page
@@ -181,7 +188,7 @@ export const Component = () => {
const page = useServiceOptional(Page); const page = useServiceOptional(Page);
usePageDocumentTitle(page?.meta); usePageDocumentTitle(page?.meta);
const loginStatus = useCurrentLoginStatus();
if (!page) { if (!page) {
return; return;
} }
@@ -189,18 +196,41 @@ export const Component = () => {
return ( return (
<AppContainer> <AppContainer>
<MainContainer> <MainContainer>
<ShareHeader <div className={styles.root}>
pageId={page.id} <div className={styles.mainContainer}>
publishMode={publishMode} <ShareHeader
blockSuiteWorkspace={page.blockSuitePage.workspace} pageId={page.id}
/> publishMode={publishMode}
<PageDetailEditor blockSuiteWorkspace={page.blockSuitePage.workspace}
isPublic />
publishMode={publishMode} <Scrollable.Root>
workspace={page.blockSuitePage.workspace} <Scrollable.Viewport className={styles.editorContainer}>
pageId={page.id} <PageDetailEditor
onLoad={() => noop} isPublic
/> publishMode={publishMode}
workspace={page.blockSuitePage.workspace}
pageId={page.id}
onLoad={() => noop}
/>
{publishMode === 'page' ? <ShareFooter /> : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
{loginStatus !== 'authenticated' ? (
<a
href="https://affine.pro"
target="_blank"
className={styles.link}
rel="noreferrer"
>
<span className={styles.linkText}>
{t['com.affine.share-page.footer.built-with']()}
</span>
<Logo1Icon fontSize={20} />
</a>
) : null}
</div>
</div>
</MainContainer> </MainContainer>
</AppContainer> </AppContainer>
); );

View File

@@ -0,0 +1,47 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const footerContainer = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
width: '100%',
maxWidth: cssVar('editorWidth'),
marginLeft: 'auto',
marginRight: 'auto',
paddingLeft: cssVar('editorSidePadding'),
paddingRight: cssVar('editorSidePadding'),
marginBottom: '200px',
});
export const footer = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
width: '100%',
padding: '12px',
background: cssVar('backgroundOverlayPanelColor'),
});
export const description = style({
fontSize: cssVar('fontSm'),
color: cssVar('textSecondaryColor'),
textAlign: 'center',
});
export const getStartLink = style({
display: 'flex',
padding: '0px 4px',
justifyContent: 'center',
alignItems: 'center',
gap: '4px',
color: cssVar('black'),
selectors: {
'&:visited': {
color: cssVar('black'),
},
},
});

View File

@@ -0,0 +1,26 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
import * as styles from './share-footer.css';
export const ShareFooter = () => {
const t = useAFFiNEI18N();
return (
<div className={styles.footerContainer}>
<div className={styles.footer}>
<div className={styles.description}>
{t['com.affine.share-page.footer.description']()}
</div>
<a
className={styles.getStartLink}
href="https://affine.pro/"
target="_blank"
rel="noreferrer"
>
{t['com.affine.share-page.footer.get-started']()}
<ArrowRightBigIcon fontSize={16} />
</a>
</div>
</div>
);
};

View File

@@ -0,0 +1,18 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const header = style({
display: 'flex',
height: '52px',
width: '100%',
alignItems: 'center',
flexShrink: 0,
background: cssVar('backgroundPrimaryColor'),
borderBottom: `1px solid ${cssVar('borderColor')}`,
padding: '0 16px',
});
export const spacer = style({
flexGrow: 1,
minWidth: 12,
});

View File

@@ -1,11 +1,10 @@
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { PageMode } from '../../atoms'; import type { PageMode } from '../../atoms';
import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index'; import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index';
import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item'; import * as styles from './share-header.css';
import ShareHeaderRightItem from '../../components/cloud/share-header-right-item';
import { Header } from '../../components/pure/header';
export function ShareHeader({ export function ShareHeader({
pageId, pageId,
@@ -17,32 +16,24 @@ export function ShareHeader({
blockSuiteWorkspace: BlockSuiteWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
}) { }) {
return ( return (
<Header <div className={styles.header}>
isFloat={publishMode === 'edgeless'} <EditorModeSwitch
left={<ShareHeaderLeftItem />} isPublic
center={ blockSuiteWorkspace={blockSuiteWorkspace}
<> pageId={pageId}
<EditorModeSwitch publicMode={publishMode}
isPublic />
blockSuiteWorkspace={blockSuiteWorkspace} <BlocksuiteHeaderTitle
pageId={pageId} blockSuiteWorkspace={blockSuiteWorkspace}
publicMode={publishMode} pageId={pageId}
/> isPublic={true}
<BlocksuiteHeaderTitle />
blockSuiteWorkspace={blockSuiteWorkspace} <div className={styles.spacer} />
pageId={pageId} <ShareHeaderRightItem
isPublic={true} workspaceId={blockSuiteWorkspace.id}
/> pageId={pageId}
</> publishMode={publishMode}
} />
right={ </div>
<ShareHeaderRightItem
workspaceId={blockSuiteWorkspace.id}
pageId={pageId}
publishMode={publishMode}
/>
}
bottomBorder
/>
); );
} }

View File

@@ -26,7 +26,6 @@ export const editorContainer = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: 1, flex: 1,
overflow: 'hidden',
zIndex: 0, zIndex: 0,
}); });
export const sidebarContainer = style({ export const sidebarContainer = style({

View File

@@ -1,3 +1,4 @@
import { Scrollable } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { ResizePanel } from '@affine/component/resize-panel'; import { ResizePanel } from '@affine/component/resize-panel';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
@@ -230,14 +231,17 @@ const DetailPageImpl = memo(function DetailPageImpl() {
main={ main={
// Add a key to force rerender when page changed, to avoid error boundary persisting. // Add a key to force rerender when page changed, to avoid error boundary persisting.
<AffineErrorBoundary key={currentPageId}> <AffineErrorBoundary key={currentPageId}>
<div className={styles.editorContainer}> <Scrollable.Root>
<PageDetailEditor <Scrollable.Viewport className={styles.editorContainer}>
pageId={currentPageId} <PageDetailEditor
onLoad={onLoad} pageId={currentPageId}
workspace={blockSuiteWorkspace} onLoad={onLoad}
/> workspace={blockSuiteWorkspace}
<HubIsland /> />
</div> </Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
<HubIsland />
</AffineErrorBoundary> </AffineErrorBoundary>
} }
footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null} footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null}

View File

@@ -1138,5 +1138,9 @@
"unnamed": "unnamed", "unnamed": "unnamed",
"upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.", "upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.",
"will be moved to Trash": "{{title}} will be moved to Trash", "will be moved to Trash": "{{title}} will be moved to Trash",
"will delete member": "will delete member" "will delete member": "will delete member",
"com.affine.share-page.footer.create-with": "Create with",
"com.affine.share-page.footer.built-with": "Built with",
"com.affine.share-page.footer.description": "Empower your sharing with AffiNE Cloud: One-click doc sharing",
"com.affine.share-page.footer.get-started": "Get started for free"
} }

View File

@@ -112,10 +112,8 @@ test('share page with default edgeless', async ({ page, browser }) => {
expect(await page2.textContent('affine-paragraph')).toContain( expect(await page2.textContent('affine-paragraph')).toContain(
'TEST CONTENT' 'TEST CONTENT'
); );
const logo = page2.getByTestId('share-page-logo');
const editButton = page2.getByTestId('share-page-edit-button'); const editButton = page2.getByTestId('share-page-edit-button');
await expect(editButton).not.toBeVisible(); await expect(editButton).not.toBeVisible();
await expect(logo).toBeVisible();
} }
}); });