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
@@ -139,11 +139,16 @@ const HistoryEditorPreview = ({
{snapshotPage ? (
<AffineErrorBoundary>
<BlockSuiteEditor
className={styles.editor}
mode={mode}
page={snapshotPage}
/>
<Scrollable.Root>
<Scrollable.Viewport>
<BlockSuiteEditor
className={styles.editor}
mode={mode}
page={snapshotPage}
/>
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</AffineErrorBoundary>
) : (
<div className={styles.loadingContainer}>
@@ -95,7 +95,6 @@ export const previewHeaderTimestamp = style({
export const editor = style({
height: '100%',
flexGrow: 1,
overflow: 'hidden',
});
export const rowWrapper = style({
display: 'flex',
@@ -104,7 +104,10 @@ export const BlocksuiteDocEditor = forwardRef<
return (
<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 ? (
<adapted.DocTitle page={page} ref={titleRef} />
) : (
@@ -2,8 +2,6 @@ import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const docEditorRoot = style({
display: 'block',
height: '100%',
overflow: 'hidden',
background: cssVar('backgroundPrimaryColor'),
});
@@ -11,9 +9,6 @@ export const docEditorRoot = style({
export const affineDocViewport = style({
display: 'flex',
flexDirection: 'column',
height: '100%',
overflowX: 'hidden',
overflowY: 'auto',
userSelect: 'none',
containerName: 'viewport',
// todo: find out what this does in bs
@@ -25,6 +20,13 @@ export const affineDocViewport = style({
zIndex: -1,
},
},
selectors: {
'&[data-doc-viewport="true"]': {
overflowX: 'visible',
overflowY: 'visible',
height: 'auto',
},
},
});
export const docContainer = style({
@@ -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;
@@ -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'),
},
},
});
@@ -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>
);
};
@@ -1,12 +1,17 @@
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 { 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 * 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 user = useCurrentUser();
const members = useMembers(workspaceId, 0);
@@ -14,11 +19,21 @@ export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => {
const t = useAFFiNEI18N();
const { jumpToPage } = useNavigateHelper();
const handleEdit = useCallback(() => {
jumpToPage(workspaceId, pageId);
}, [workspaceId, pageId, jumpToPage]);
useEffect(() => {
if (isMember) {
setIsMember(true);
}
}, [isMember, setIsMember]);
if (isMember) {
return (
<Button
type="plain"
onClick={() => jumpToPage(workspaceId, pageId)}
className={styles.editButton}
onClick={handleEdit}
data-testid="share-page-edit-button"
>
{t['Edit']()}
@@ -1,8 +1,11 @@
import type { PageMode } from '../../../atoms';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { PageMode } from '@affine/core/atoms';
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
import { useState } from 'react';
import { AuthenticatedItem } from './authenticated-item';
import { PresentButton } from './present';
import * as styles from './styles.css';
import { PublishPageUserAvatar } from './user-avatar';
export type ShareHeaderRightItemProps = {
workspaceId: string;
@@ -13,14 +16,25 @@ export type ShareHeaderRightItemProps = {
const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => {
const loginStatus = useCurrentLoginStatus();
const { publishMode } = props;
const [isMember, setIsMember] = useState(false);
// TODO: Add TOC
return (
<div className={styles.rightItemContainer}>
{loginStatus === 'authenticated' ? (
<AuthenticatedItem {...props} />
<AuthenticatedItem setIsMember={setIsMember} {...props} />
) : null}
{publishMode === 'edgeless' ? <PresentButton /> : null}
{loginStatus === 'authenticated' ? (
<>
<div
className={styles.headerDivider}
data-is-member={isMember}
data-is-edgeless={publishMode === 'edgeless'}
/>
<PublishPageUserAvatar />
</>
) : null}
</div>
);
};
@@ -1,4 +1,5 @@
import { Button } from '@affine/component/ui/button';
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { EdgelessPageService } from '@blocksuite/blocks';
import { PresentationIcon } from '@blocksuite/icons';
@@ -9,15 +10,15 @@ import * as styles from './styles.css';
export const PresentButton = () => {
const t = useAFFiNEI18N();
const [isPresent, setIsPresent] = useState(false);
const [editor] = useActiveBlocksuiteEditor();
const handlePresent = useCallback(() => {
// TODO: use editor Atom
const editorRoot = document.querySelector('editor-host');
if (!editorRoot || isPresent) return;
const editorHost = editor?.host;
if (!editorHost || isPresent) return;
// TODO: use surfaceService subAtom
const enterPresentationMode = () => {
const edgelessPageService = editorRoot?.spec.getService(
const edgelessPageService = editorHost.spec.getService(
'affine:page'
) as EdgelessPageService;
@@ -33,16 +34,15 @@ export const PresentButton = () => {
enterPresentationMode();
setIsPresent(true);
}, [isPresent]);
}, [editor?.host, isPresent]);
useEffect(() => {
if (!isPresent) return;
// TODO: use editor Atom
const editorRoot = document.querySelector('editor-host');
if (!editorRoot) return;
const editorHost = editor?.host;
if (!editorHost) return;
const edgelessPage = editorRoot?.querySelector('affine-edgeless-page');
const edgelessPage = editorHost?.querySelector('affine-edgeless-page');
if (!edgelessPage) return;
edgelessPage.slots.edgelessToolUpdated.on(() => {
@@ -52,15 +52,15 @@ export const PresentButton = () => {
return () => {
edgelessPage.slots.edgelessToolUpdated.dispose();
};
}, [isPresent]);
}, [editor?.host, isPresent]);
return (
<Button
type="primary"
icon={<PresentationIcon />}
className={styles.presentButton}
onClick={handlePresent}
disabled={isPresent}
withoutHoverStyle
>
{t['com.affine.share-page.header.present']()}
</Button>
@@ -1,5 +1,6 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
import { globalStyle, style } from '@vanilla-extract/css';
export const iconWrapper = style({
display: 'flex',
justifyContent: 'center',
@@ -16,9 +17,117 @@ export const iconWrapper = style({
export const rightItemContainer = style({
display: 'flex',
alignItems: 'center',
gap: '8px',
gap: '16px',
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({
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',
});
@@ -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>
);
};
@@ -1,7 +1,6 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const editor = style({
flex: 1,
overflow: 'auto',
selectors: {
'&.full-screen': {
vars: {
@@ -9,16 +8,11 @@ export const editor = style({
'--affine-editor-side-padding': '15px',
},
},
'&.is-public-page': {
height: '100%',
},
},
});
globalStyle(
`${editor} .affine-doc-viewport:not(.affine-embed-synced-doc-editor)`,
{
paddingBottom: '150px',
paddingLeft: '20px',
scrollbarGutter: 'stable',
}
);
@@ -108,7 +108,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
<Editor
className={clsx(styles.editor, {
'full-screen': appSettings.fullWidthLayout,
'is-public-page': isPublic,
})}
style={
{