feat: new sidebar (app shell) styles (#2303)

This commit is contained in:
Peng Xiao
2023-05-12 11:13:51 +08:00
committed by LongYinan
parent add5deae0f
commit 683343ad82
54 changed files with 1166 additions and 642 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ async function createWindow() {
y: mainWindowState.y,
width: mainWindowState.width,
minWidth: 640,
transparent: isMacOS(),
minHeight: 480,
visualEffectState: 'active',
vibrancy: 'under-window',
height: mainWindowState.height,
@@ -7,7 +7,7 @@ export const StyledSidebarSwitch = styled(IconButton, {
})<{ visible: boolean }>(({ visible }) => {
return {
opacity: visible ? 1 : 0,
WebkitAppRegion: 'no-drag',
WebkitAppRegion: visible ? 'no-drag' : 'drag',
transition: 'all 0.2s ease-in-out',
};
});
@@ -4,7 +4,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import {
forwardRef,
@@ -161,7 +161,7 @@ export const Header = forwardRef<
setShowWarning(shouldShowWarning());
setShowGuideDownloadClientTip(shouldShowGuideDownloadClientTip);
}, [shouldShowGuideDownloadClientTip]);
const [open] = useAtom(appSidebarOpenAtom);
const open = useAtomValue(appSidebarOpenAtom);
const t = useAFFiNEI18N();
const mode = useCurrentMode();
@@ -189,7 +189,6 @@ export const Header = forwardRef<
className={styles.header}
data-has-warning={showWarning}
data-testid="editor-header-items"
data-tauri-drag-region
data-is-edgeless={mode === 'edgeless'}
>
<Suspense>
@@ -6,18 +6,9 @@ export const headerContainer = style({
position: 'sticky',
top: 0,
background: 'var(--affine-background-primary-color)',
// @ts-ignore
WebkitAppRegion: 'drag',
zIndex: 'var(--affine-z-index-popover)',
'@media': {
'(max-width: 768px)': {
selectors: {
'&[data-open="true"]': {
// @ts-ignore
WebkitAppRegion: 'no-drag',
},
},
},
},
selectors: {
'&[data-has-warning="true"]': {
height: '96px',
@@ -6,7 +6,7 @@ export const StyledSelectorContainer = styled('div')(() => {
display: 'flex',
alignItems: 'center',
padding: '0 6px',
marginBottom: '16px',
margin: '0 -6px',
borderRadius: '8px',
color: 'var(--affine-text-primary-color)',
':hover': {
@@ -1,13 +1,10 @@
import { MenuItem } from '@affine/component/app-sidebar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { StyledCollapseItem } from '../shared-styles';
export const EmptyItem = () => {
const t = useAFFiNEI18N();
return (
<StyledCollapseItem disable={true} textWrap={true}>
{t['Favorite pages for easy access']()}
</StyledCollapseItem>
<MenuItem disabled={true}>{t['Favorite pages for easy access']()}</MenuItem>
);
};
@@ -1,20 +1,18 @@
import { MuiCollapse } from '@affine/component';
import { MenuLinkItem } from '@affine/component/app-sidebar';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import type { FavoriteListProps } from '../index';
import { StyledCollapseItem } from '../shared-styles';
import EmptyItem from './empty-item';
export const FavoriteList = ({
pageMeta,
openPage,
showList,
}: FavoriteListProps) => {
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
const router = useRouter();
const record = useAtomValue(workspacePreferredModeAtom);
const pageMeta = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const workspaceId = currentWorkspace.id;
const favoriteList = useMemo(
() => pageMeta.filter(p => p.favorite && !p.trash),
@@ -22,45 +20,25 @@ export const FavoriteList = ({
);
return (
<MuiCollapse
in={showList}
style={{
maxHeight: 300,
overflowY: 'auto',
marginLeft: '16px',
}}
>
<>
{favoriteList.map((pageMeta, index) => {
const active = router.query.pageId === pageMeta.id;
const icon =
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
return (
<div key={`${pageMeta}-${index}`}>
<StyledCollapseItem
data-testid={`favorite-list-item-${pageMeta.id}`}
active={active}
ref={ref => {
if (ref && active) {
ref.scrollIntoView({ behavior: 'smooth' });
}
}}
onClick={() => {
if (active) {
return;
}
openPage(pageMeta.id);
}}
>
{record[pageMeta.id] === 'edgeless' ? (
<EdgelessIcon />
) : (
<PageIcon />
)}
<span>{pageMeta.title || 'Untitled'}</span>
</StyledCollapseItem>
</div>
<MenuLinkItem
key={`${pageMeta}-${index}`}
data-testid={`favorite-list-item-${pageMeta.id}`}
active={active}
href={`/workspace/${workspaceId}/${pageMeta.id}`}
icon={icon}
>
<span>{pageMeta.title || 'Untitled'}</span>
</MenuLinkItem>
);
})}
{favoriteList.length === 0 && <EmptyItem />}
</MuiCollapse>
</>
);
};
@@ -1,66 +1 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownSmallIcon, FavoriteIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useCallback, useState } from 'react';
import type { AllWorkspace } from '../../../../shared';
import type { WorkSpaceSliderBarProps } from '../index';
import { StyledCollapseButton, StyledListItem } from '../shared-styles';
import { StyledLink } from '../style';
import FavoriteList from './favorite-list';
export const Favorite = ({
currentPath,
paths,
currentPageId,
openPage,
currentWorkspace,
}: Pick<
WorkSpaceSliderBarProps,
'currentPath' | 'paths' | 'currentPageId' | 'openPage'
> & {
currentWorkspace: AllWorkspace;
}) => {
const currentWorkspaceId = currentWorkspace.id;
const pageMeta = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const [showSubFavorite, setOpenSubFavorite] = useState(true);
const t = useAFFiNEI18N();
return (
<>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.favorite(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.favorite(currentWorkspaceId),
}}
>
<FavoriteIcon />
{t['Favorites']()}
</StyledLink>
<StyledCollapseButton
onClick={useCallback(() => {
setOpenSubFavorite(!showSubFavorite);
}, [showSubFavorite])}
collapse={showSubFavorite}
>
<ArrowDownSmallIcon />
</StyledCollapseButton>
</StyledListItem>
<FavoriteList
currentPageId={currentPageId}
showList={showSubFavorite}
openPage={openPage}
pageMeta={pageMeta}
/>
</>
);
};
export default Favorite;
export * from './favorite-list';
@@ -1,13 +0,0 @@
export const Arrow = () => {
return (
<svg
width="6"
height="10"
viewBox="0 0 6 10"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0.354 9.22997C0.201333 9.0773 0.125 8.91764 0.125 8.75097C0.125 8.5843 0.201333 8.42464 0.354 8.27197L3.625 5.00097L0.354 1.72997C0.201333 1.5773 0.125 1.41764 0.125 1.25097C0.125 1.0843 0.201333 0.924636 0.354 0.771969C0.506667 0.619302 0.666333 0.542969 0.833 0.542969C0.999667 0.542969 1.15933 0.619302 1.312 0.771969L4.979 4.43897C5.06233 4.52164 5.125 4.61164 5.167 4.70897C5.20833 4.8063 5.229 4.90364 5.229 5.00097C5.229 5.0983 5.20833 5.19564 5.167 5.29297C5.125 5.3903 5.06233 5.4803 4.979 5.56297L1.312 9.22997C1.15933 9.38264 0.999667 9.45897 0.833 9.45897C0.666333 9.45897 0.506667 9.38264 0.354 9.22997Z" />
</svg>
);
};
@@ -1,12 +1,9 @@
import type { Page, PageMeta } from '@blocksuite/store';
import type { Page } from '@blocksuite/store';
import type { AllWorkspace } from '../../../shared';
export type FavoriteListProps = {
currentPageId: string | null;
openPage: (pageId: string) => void;
showList: boolean;
pageMeta: PageMeta[];
currentWorkspace: AllWorkspace;
};
export type WorkSpaceSliderBarProps = {
@@ -1,31 +0,0 @@
import { IconButton } from '@affine/component';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
import { StyledRouteNavigationWrapper } from './shared-styles';
export const RouteNavigation = () => {
if (!environment.isDesktop) {
return <></>;
}
return (
<StyledRouteNavigationWrapper>
<IconButton
size="middle"
onClick={() => {
window.history.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
size="middle"
onClick={() => {
window.history.forward();
}}
style={{ marginLeft: '32px' }}
>
<ArrowRightSmallIcon />
</IconButton>
</StyledRouteNavigationWrapper>
);
};
@@ -1,7 +1,14 @@
import {
AddPageButton,
AppSidebar,
appSidebarOpenAtom,
ResizeIndicator,
AppUpdaterButton,
CategoryDivider,
MenuLinkItem,
QuickSearchInput,
SidebarContainer,
SidebarScrollableContainer,
updateAvailableAtom,
} from '@affine/component/app-sidebar';
import { config } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -9,27 +16,18 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
FolderIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
ShareIcon,
} from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import type { ReactElement, UIEvent } from 'react';
import type { ReactElement } from 'react';
import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect } from 'react';
import type { AllWorkspace } from '../../shared';
import ChangeLog from '../pure/workspace-slider-bar/changeLog';
import Favorite from '../pure/workspace-slider-bar/favorite';
import { StyledListItem } from '../pure/workspace-slider-bar/shared-styles';
import {
StyledLink,
StyledNewPageButton,
StyledScrollWrapper,
StyledSliderBarInnerWrapper,
} from '../pure/workspace-slider-bar/style';
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
export type RootAppSidebarProps = {
@@ -37,7 +35,6 @@ export type RootAppSidebarProps = {
onOpenQuickSearchModal: () => void;
onOpenWorkspaceListModal: () => void;
currentWorkspace: AllWorkspace | null;
currentPageId: string | null;
openPage: (pageId: string) => void;
createPage: () => Page;
currentPath: string;
@@ -50,6 +47,26 @@ export type RootAppSidebarProps = {
};
};
const RouteMenuLinkItem = ({
currentPath,
path,
icon,
children,
...props
}: {
currentPath: string; // todo: pass through useRouter?
path?: string | null;
icon: ReactElement;
children?: ReactElement;
} & React.HTMLAttributes<HTMLDivElement>) => {
const active = currentPath === path;
return (
<MenuLinkItem {...props} active={active} href={path ?? ''} icon={icon}>
{children}
</MenuLinkItem>
);
};
/**
* This is for the whole affine app sidebar.
* This component wraps the app sidebar in `@affine/component` with logic and data.
@@ -58,7 +75,6 @@ export type RootAppSidebarProps = {
*/
export const RootAppSidebar = ({
currentWorkspace,
currentPageId,
openPage,
createPage,
currentPath,
@@ -69,7 +85,6 @@ export const RootAppSidebar = ({
const currentWorkspaceId = currentWorkspace?.id || null;
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
const onClickNewPage = useCallback(async () => {
const page = await createPage();
openPage(page.id);
@@ -80,160 +95,78 @@ export const RootAppSidebar = ({
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen);
}
}, [sidebarOpen]);
const [ref, setRef] = useState<HTMLElement | null>(null);
const handleQuickSearchButtonKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onOpenQuickSearchModal();
}
},
[onOpenQuickSearchModal]
);
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
return (
<>
<AppSidebar
ref={setRef}
footer={
<StyledNewPageButton
data-testid="new-page-button"
onClick={onClickNewPage}
>
<PlusIcon /> {t['New Page']()}
</StyledNewPageButton>
}
>
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
<AppSidebar>
<SidebarContainer>
<WorkspaceSelector
currentWorkspace={currentWorkspace}
onClick={onOpenWorkspaceListModal}
/>
<ChangeLog />
<StyledListItem
<QuickSearchInput
data-testid="slider-bar-quick-search-button"
onClick={useCallback(() => {
onOpenQuickSearchModal();
}, [onOpenQuickSearchModal])}
onKeyDown={handleQuickSearchButtonKeyDown}
onClick={onOpenQuickSearchModal}
/>
<RouteMenuLinkItem
icon={<FolderIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.all(currentWorkspaceId)}
>
<div
role="button"
tabIndex={0}
style={{
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<SearchIcon />
{t['Quick search']()}
</div>
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.setting(currentWorkspaceId))
}
<span data-testid="all-pages">{t['All pages']()}</span>
</RouteMenuLinkItem>
<RouteMenuLinkItem
data-testid="slider-bar-workspace-setting-button"
style={{
marginBottom: '16px',
}}
icon={<SettingsIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<SettingsIcon />
<div>{t['Workspace Settings']()}</div>
</StyledLink>
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.all(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.all(currentWorkspaceId),
}}
>
<FolderIcon />
<span data-testid="all-pages">{t['All pages']()}</span>
</StyledLink>
</StyledListItem>
<StyledScrollWrapper
showTopBorder={!isScrollAtTop}
onScroll={(e: UIEvent<HTMLDivElement>) => {
(e.target as HTMLDivElement).scrollTop === 0
? setIsScrollAtTop(true)
: setIsScrollAtTop(false);
}}
>
{blockSuiteWorkspace && (
<Favorite
currentPath={currentPath}
paths={paths}
currentPageId={currentPageId}
openPage={openPage}
currentWorkspace={currentWorkspace}
/>
)}
</StyledScrollWrapper>
<div style={{ height: 16 }}></div>
<span data-testid="settings">{t['Settings']()}</span>
</RouteMenuLinkItem>
</SidebarContainer>
<SidebarScrollableContainer>
<CategoryDivider label={t['Favorites']()} />
{blockSuiteWorkspace && (
<FavoriteList currentWorkspace={currentWorkspace} />
)}
{config.enableLegacyCloud &&
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<StyledListItem>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="Published-to-web">Published to web</span>
</StyledLink>
</StyledListItem>
) : (
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.shared(currentWorkspaceId))
}
<RouteMenuLinkItem
icon={<ShareIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.shared(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</StyledLink>
</StyledListItem>
<span data-testid="Published-to-web">Published to web</span>
</RouteMenuLinkItem>
) : (
<RouteMenuLinkItem
icon={<ShareIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.shared(currentWorkspaceId)}
>
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</RouteMenuLinkItem>
))}
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.trash(currentWorkspaceId))
}
<CategoryDivider label={t['others']()} />
<RouteMenuLinkItem
icon={<DeleteTemporarilyIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.trash(currentWorkspaceId)}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
}}
>
<DeleteTemporarilyIcon /> {t['Trash']()}
</StyledLink>
</StyledListItem>
</StyledSliderBarInnerWrapper>
<span data-testid="trash-page">{t['Trash']()}</span>
</RouteMenuLinkItem>
</SidebarScrollableContainer>
<SidebarContainer>
{clientUpdateAvailable && <AppUpdaterButton />}
<AddPageButton onClick={onClickNewPage} />
</SidebarContainer>
</AppSidebar>
<ResizeIndicator targetElement={ref} />
</>
);
};
+9 -3
View File
@@ -1,3 +1,7 @@
import {
appSidebarOpenAtom,
appSidebarResizingAtom,
} from '@affine/component/app-sidebar';
import {
AppContainer,
MainContainer,
@@ -330,17 +334,19 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
setOpenQuickSearchModalAtom(true);
}, [setOpenQuickSearchModalAtom]);
const resizing = useAtomValue(appSidebarResizingAtom);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
return (
<>
<Head>
<title>{title}</title>
</Head>
<AppContainer>
<AppContainer resizing={resizing}>
<RootAppSidebar
isPublicWorkspace={isPublicWorkspace}
onOpenQuickSearchModal={handleOpenQuickSearchModal}
currentWorkspace={currentWorkspace}
currentPageId={currentPageId}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback(
(pageId: string) => {
@@ -353,7 +359,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
currentPath={router.asPath.split('?')[0]}
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
/>
<MainContainer>
<MainContainer sidebarOpen={sidebarOpen}>
<Suspense fallback={<WorkspaceFallback />}>{children}</Suspense>
<ToolContainer>
{/* fixme(himself65): remove this */}
@@ -0,0 +1,30 @@
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'inline-flex',
background: 'var(--affine-white-30)',
alignItems: 'center',
borderRadius: '8px',
border: '1px solid var(--affine-black-10)',
fontSize: 'var(--affine-font-sm)',
width: '100%',
height: '52px',
userSelect: 'none',
cursor: 'pointer',
padding: '0 24px',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
},
},
});
export const icon = style({
marginRight: '18px',
color: 'var(--affine-icon-color)',
fontSize: '24px',
});
export const spacer = style({
flex: 1,
});
@@ -0,0 +1,16 @@
import type { Meta, StoryFn } from '@storybook/react';
import { AddPageButton } from '.';
export default {
title: 'Components/AppSidebar/AddPageButton',
component: AddPageButton,
} satisfies Meta;
export const Default: StoryFn = () => {
return (
<main style={{ width: '240px' }}>
<AddPageButton onClick={() => alert('opened')} />
</main>
);
};
@@ -0,0 +1,31 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PlusIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import * as styles from './index.css';
interface AddPageButtonProps {
onClick?: () => void;
className?: string;
style?: React.CSSProperties;
}
// Although it is called an input, it is actually a button.
export function AddPageButton({
onClick,
className,
style,
}: AddPageButtonProps) {
const t = useAFFiNEI18N();
return (
<button
data-testid="new-page-button"
style={style}
className={clsx([styles.root, className])}
onClick={onClick}
>
<PlusIcon className={styles.icon} /> {t['New Page']()}
</button>
);
}
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 122 116">
<path id="b" stroke="#fff" stroke-linecap="round" stroke-width="0" d="M17.9256 115C17.434 111.774 13.1701 104.086 13.4282 95.6465C13.6862 87.207 18.6628 76.0721 17.9256 64.3628C17.1883 52.6535 8.7772 35.9512 9.00452 25.3907C9.23185 14.8302 16.2114 5.06512 17.9256 1"/>
<path id="d" stroke="#fff" stroke-linecap="round" stroke-width="0" d="M84.1628 115C85.2376 112.055 94.5618 98.8394 93.9975 91.1338C93.4332 83.4281 82.5505 73.2615 84.1628 62.5704C85.775 51.8793 96.4803 35.4248 95.9832 25.7826C95.4861 16.1404 87.9113 4.71163 84.1628 1"/>
<path id="f" stroke="#fff" stroke-linecap="round" stroke-width="0" d="M37.0913 115C37.9604 111.921 44.4347 99.4545 45.3816 92.9773C48.9305 68.7011 35.7877 73.9552 37.0913 62.7781C38.3949 51.6011 47.3889 36.9895 46.9869 26.9091C46.585 16.8286 40.1222 4.88034 37.0913 1"/>
<path id="h" stroke="#fff" stroke-linecap="round" stroke-width="0" d="M112.443 115C111.698 112.235 108.25 106.542 107.715 93.7582C107.241 82.4286 107.229 83.9543 112.443 66.1429C116.085 44.0408 100.661 42.5908 101.006 33.539C101.35 24.4871 109.843 4.48439 112.443 1"/>
<g>
<circle r="1.5" fill="rgba(96, 70, 254, 0.3)">
<animateMotion dur="10s" repeatCount="indefinite">
<mpath href="#b" />
</animateMotion>
</circle>
</g>
<g>
<circle r="1" fill="rgba(96, 70, 254, 0.3)" fill-opacity="1" shape-rendering="crispEdges">
<animateMotion dur="8s" repeatCount="indefinite">
<mpath href="#d" />
</animateMotion>
</circle>
</g>
<g>
<circle r=".5" fill="rgba(96, 70, 254, 0.3)" fill-opacity="1" shape-rendering="crispEdges">
<animateMotion dur="4s" repeatCount="indefinite">
<mpath href="#f" />
</animateMotion>
</circle>
</g>
<g>
<circle r=".8" fill="rgba(96, 70, 254, 0.3)" fill-opacity="1" shape-rendering="crispEdges">
<animateMotion dur="6s" repeatCount="indefinite">
<mpath href="#h" />
</animateMotion>
</circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,132 @@
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'inline-flex',
background: 'var(--affine-white-30)',
alignItems: 'center',
borderRadius: '8px',
border: '1px solid var(--affine-black-10)',
fontSize: 'var(--affine-font-sm)',
width: '100%',
height: '52px',
userSelect: 'none',
cursor: 'pointer',
padding: '0 12px',
position: 'relative',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
},
'&:before': {
content: "''",
position: 'absolute',
top: '-3px',
right: '-3px',
width: '8px',
height: '8px',
backgroundColor: 'var(--affine-primary-color)',
borderRadius: '50%',
zIndex: 1,
opacity: 1,
transition: '0.3s ease',
},
},
vars: {
'--svg-dot-animation': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 122 116'%3E%3Cpath id='b' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M17.9256 115C17.434 111.774 13.1701 104.086 13.4282 95.6465C13.6862 87.207 18.6628 76.0721 17.9256 64.3628C17.1883 52.6535 8.7772 35.9512 9.00452 25.3907C9.23185 14.8302 16.2114 5.06512 17.9256 1'/%3E%3Cpath id='d' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M84.1628 115C85.2376 112.055 94.5618 98.8394 93.9975 91.1338C93.4332 83.4281 82.5505 73.2615 84.1628 62.5704C85.775 51.8793 96.4803 35.4248 95.9832 25.7826C95.4861 16.1404 87.9113 4.71163 84.1628 1'/%3E%3Cpath id='f' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M37.0913 115C37.9604 111.921 44.4347 99.4545 45.3816 92.9773C48.9305 68.7011 35.7877 73.9552 37.0913 62.7781C38.3949 51.6011 47.3889 36.9895 46.9869 26.9091C46.585 16.8286 40.1222 4.88034 37.0913 1'/%3E%3Cpath id='h' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M112.443 115C111.698 112.235 108.25 106.542 107.715 93.7582C107.241 82.4286 107.229 83.9543 112.443 66.1429C116.085 44.0408 100.661 42.5908 101.006 33.539C101.35 24.4871 109.843 4.48439 112.443 1'/%3E%3Cg%3E%3Ccircle r='1.5' fill='rgba(96, 70, 254, 0.3)'%3E%3CanimateMotion dur='10s' repeatCount='indefinite'%3E%3Cmpath href='%23b' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='1' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='8s' repeatCount='indefinite'%3E%3Cmpath href='%23d' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='.5' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='4s' repeatCount='indefinite'%3E%3Cmpath href='%23f' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='.8' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='6s' repeatCount='indefinite'%3E%3Cmpath href='%23h' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")`,
},
});
export const icon = style({
marginRight: '18px',
color: 'var(--affine-primary-color)',
fontSize: '24px',
});
export const particles = style({
background: `var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat',
backgroundPosition: 'center, center top 100%',
backgroundSize: '100%, 130%',
WebkitMaskImage:
'linear-gradient(to top, transparent, black, black, transparent)',
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
});
export const particlesBefore = style({
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat, repeat',
backgroundPosition: 'center, center top 100%, center center',
backgroundSize: '100% 120%, 150%, 120%',
filter: 'blur(1px)',
willChange: 'filter',
});
export const installLabel = style({
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
width: '100%',
height: '100%',
fontSize: 'var(--affine-font-sm)',
whiteSpace: 'nowrap',
});
export const installLabelNormal = style([
installLabel,
{
selectors: {
[`${root}:hover &`]: {
display: 'none',
},
},
},
]);
export const installLabelHover = style([
installLabel,
{
display: 'none',
selectors: {
[`${root}:hover &`]: {
display: 'flex',
},
},
},
]);
export const halo = style({
overflow: 'hidden',
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
':before': {
content: '""',
display: 'block',
width: '60%',
height: '40%',
position: 'absolute',
top: '80%',
left: '50%',
background:
'linear-gradient(180deg, rgba(50, 26, 206, 0.1) 10%, rgba(50, 26, 206, 0.35) 30%, rgba(84, 56, 255, 1) 50%)',
filter: 'blur(10px) saturate(1.2)',
transform: 'translateX(-50%) translateY(calc(0 * 1%)) scale(0)',
transition: '0.3s ease',
willChange: 'filter',
},
selectors: {
'&:hover:before': {
transform: 'translateX(-50%) translateY(calc(-70 * 1%)) scale(1)',
},
},
});
@@ -0,0 +1,16 @@
import type { Meta, StoryFn } from '@storybook/react';
import { AppUpdaterButton } from '.';
export default {
title: 'Components/AppSidebar/AppUpdaterButton',
component: AppUpdaterButton,
} satisfies Meta;
export const Default: StoryFn = () => {
return (
<main style={{ width: '240px' }}>
<AppUpdaterButton />
</main>
);
};
@@ -0,0 +1,36 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ResetIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import * as styles from './index.css';
interface AddPageButtonProps {
className?: string;
style?: React.CSSProperties;
}
// Although it is called an input, it is actually a button.
export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
const t = useAFFiNEI18N();
return (
<button
data-testid="new-page-button"
style={style}
className={clsx([styles.root, className])}
onClick={() => {
window.apis?.updater.updateClient();
}}
>
<div className={styles.particles} aria-hidden="true"></div>
<span className={styles.halo} aria-hidden="true"></span>
<div className={clsx([styles.installLabelNormal])}>
<span>{t['Update Available']()}</span>
</div>
<div className={clsx([styles.installLabelHover])}>
<ResetIcon className={styles.icon} />
<span>{t['Restart Install Client Update']()}</span>
</div>
</button>
);
}
@@ -0,0 +1,16 @@
import { style } from '@vanilla-extract/css';
export const root = style({
fontSize: 'var(--affine-font-xs)',
height: '16px',
userSelect: 'none',
selectors: {
'&:not(:first-of-type)': {
marginTop: '10px',
},
},
});
export const label = style({
color: 'var(--affine-black-30)',
});
@@ -0,0 +1,16 @@
import type { Meta, StoryFn } from '@storybook/react';
import { CategoryDivider } from '.';
export default {
title: 'Components/AppSidebar/CategoryDivider',
component: CategoryDivider,
} satisfies Meta;
export const Default: StoryFn = () => {
return (
<main style={{ width: '240px' }}>
<CategoryDivider label="Favorites" />
</main>
);
};
@@ -0,0 +1,17 @@
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import * as styles from './index.css';
interface CategoryDividerProps extends PropsWithChildren {
label: string;
}
export function CategoryDivider({ label, children }: CategoryDividerProps) {
return (
<div className={clsx([styles.root])}>
<div className={styles.label}>{label}</div>
{children}
</div>
);
}
@@ -6,7 +6,6 @@ export const fallbackStyle = style({
});
export const fallbackHeaderStyle = style({
padding: '0 6px',
height: '58px',
width: '100%',
display: 'flex',
@@ -5,27 +5,26 @@ import { createVar, style } from '@vanilla-extract/css';
export const floatingMaxWidth = 768;
export const navWidthVar = createVar('nav-width');
export const navStyle = style({
export const navWrapperStyle = style({
vars: {
[navWidthVar]: '256px',
},
position: 'relative',
backgroundColor: 'var(--affine-background-secondary-color)',
width: navWidthVar,
minWidth: navWidthVar,
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'margin-left .3s, width .3s',
zIndex: parseInt(baseTheme.zIndexModal),
borderRight: '1px solid var(--affine-border-color)',
backgroundColor: 'var(--affine-background-secondary-color)',
'@media': {
[`(max-width: ${floatingMaxWidth}px)`]: {
position: 'absolute',
width: `calc(10vw + ${navWidthVar})`,
width: `calc(${navWidthVar})`,
zIndex: 2,
selectors: {
'&[data-open="false"]': {
marginLeft: `calc((10vw + ${navWidthVar}) * -1)`,
},
'&[data-is-macos-electron="true"]': {
backgroundColor: 'var(--affine-background-secondary-color)',
backgroundColor: 'var(--affine-background-primary-color)',
},
},
},
@@ -37,19 +36,30 @@ export const navStyle = style({
'&[data-is-macos-electron="true"]': {
backgroundColor: 'transparent',
},
'&[data-enable-animation="true"]': {
transition: 'margin-left .3s, width .3s',
},
},
vars: {
[navWidthVar]: '256px',
},
});
export const navStyle = style({
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
zIndex: parseInt(baseTheme.zIndexModal),
borderRight: '1px solid transparent',
});
export const navHeaderStyle = style({
flex: '0 0 auto',
height: '52px',
padding: '0px 16px 0px 10px',
padding: '0px 16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '32px',
'@media': {
[`(max-width: ${floatingMaxWidth}px)`]: {
selectors: {
@@ -69,15 +79,13 @@ export const navHeaderStyle = style({
export const navBodyStyle = style({
flex: '1 1 auto',
});
export const navFooterStyle = style({
flex: '0 0 auto',
borderTop: '1px solid var(--affine-border-color)',
height: 'calc(100% - 52px)',
display: 'flex',
flexDirection: 'column',
});
export const sidebarButtonStyle = style({
width: '32px',
width: 'auto',
height: '32px',
color: 'var(--affine-icon-color)',
});
@@ -91,7 +99,7 @@ export const sidebarFloatMaskStyle = style({
left: 0,
right: '100%',
bottom: 0,
zIndex: parseInt(baseTheme.zIndexModal) - 1,
zIndex: 1,
background: 'var(--affine-background-modal-color)',
'@media': {
[`(max-width: ${floatingMaxWidth}px)`]: {
@@ -105,66 +113,3 @@ export const sidebarFloatMaskStyle = style({
},
},
});
export const haloStyle = style({
overflow: 'hidden',
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
':before': {
content: '""',
display: 'block',
width: '60%',
height: '40%',
position: 'absolute',
top: '80%',
left: '50%',
background:
'linear-gradient(180deg, rgba(50, 26, 206, 0.1) 10%, rgba(50, 26, 206, 0.35) 30%, rgba(84, 56, 255, 1) 50%)',
filter: 'blur(10px) saturate(1.2)',
transform: 'translateX(-50%) translateY(calc(0 * 1%)) scale(0)',
transition: '0.3s ease',
willChange: 'filter',
},
selectors: {
'&:hover:before': {
transform: 'translateX(-50%) translateY(calc(-70 * 1%)) scale(1)',
},
},
});
export const updaterButtonStyle = style({});
export const particlesStyle = style({
background: `var(--svg-animation), var(--svg-animation)`,
backgroundRepeat: 'no-repeat, repeat',
backgroundPosition: 'center, center top 100%',
backgroundSize: '100%, 130%',
WebkitMaskImage:
'linear-gradient(to top, transparent, black, black, transparent)',
width: '100%',
height: '100%',
position: 'absolute',
});
export const particlesBefore = style({
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
background: `var(--svg-animation), var(--svg-animation), var(--svg-animation)`,
backgroundRepeat: 'no-repeat, repeat, repeat',
backgroundPosition: 'center, center top 100%, center center',
backgroundSize: '100% 120%, 150%, 120%',
filter: 'blur(1px)',
willChange: 'filter',
});
export const installLabelStyle = style({
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
paddingLeft: '8px',
});
@@ -1,3 +1,4 @@
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { atomWithStorage } from 'jotai/utils';
import { Observable } from 'rxjs';
@@ -7,6 +8,7 @@ export const appSidebarOpenAtom = atomWithStorage(
APP_SIDEBAR_OPEN,
undefined as boolean | undefined
);
export const appSidebarResizingAtom = atom(false);
export const appSidebarWidthAtom = atomWithStorage(
'app-sidebar-width',
256 /* px */
@@ -1,17 +1,23 @@
import { IconButton } from '@affine/component';
import { SidebarIcon } from '@blocksuite/icons';
import {
DeleteTemporarilyIcon,
SettingsIcon,
SidebarIcon,
} from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { useAtom } from 'jotai';
import type { PropsWithChildren } from 'react';
import { useState } from 'react';
import {
AppSidebar,
AppSidebarFallback,
appSidebarOpenAtom,
ResizeIndicator,
} from '.';
import { AppSidebar, AppSidebarFallback, appSidebarOpenAtom } from '.';
import { AddPageButton } from './add-page-button';
import { CategoryDivider } from './category-divider';
import { navHeaderStyle, sidebarButtonStyle } from './index.css';
import { MenuLinkItem } from './menu-item';
import { QuickSearchInput } from './quick-search-input';
import {
SidebarContainer,
SidebarScrollableContainer,
} from './sidebar-containers';
export default {
title: 'Components/AppSidebar',
@@ -23,7 +29,7 @@ const Container = ({ children }: PropsWithChildren) => (
style={{
position: 'relative',
width: '100vw',
height: '600px',
height: 'calc(100vh - 40px)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'row',
@@ -51,17 +57,12 @@ const Main = () => {
</div>
);
};
const Footer = () => <div>Add Page</div>;
export const Default: StoryFn = () => {
const [ref, setRef] = useState<HTMLElement | null>(null);
return (
<>
<Container>
<AppSidebar footer={<Footer />} ref={setRef}>
Test
</AppSidebar>
<ResizeIndicator targetElement={ref} />
<AppSidebar />
<Main />
</Container>
</>
@@ -76,3 +77,68 @@ export const Fallback = () => {
</Container>
);
};
export const WithItems: StoryFn = () => {
return (
<Container>
<AppSidebar>
<SidebarContainer>
<QuickSearchInput />
<div style={{ height: '20px' }} />
<MenuLinkItem
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<MenuLinkItem
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<MenuLinkItem
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
</SidebarContainer>
<SidebarScrollableContainer>
<CategoryDivider label="Favorites" />
<MenuLinkItem
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<MenuLinkItem
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<CategoryDivider label="Others" />
<MenuLinkItem
icon={<DeleteTemporarilyIcon />}
href="/test"
onClick={() => alert('opened')}
>
Trash
</MenuLinkItem>
</SidebarScrollableContainer>
<SidebarContainer>
<AddPageButton />
</SidebarContainer>
</AppSidebar>
<Main />
</Container>
);
};
@@ -1,156 +1,102 @@
import { Button } from '@affine/component';
import { getEnvironment } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ArrowLeftSmallIcon,
ArrowRightSmallIcon,
ResetIcon,
SidebarIcon,
} from '@blocksuite/icons';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useAtom, useAtomValue } from 'jotai';
import type { PropsWithChildren, ReactElement } from 'react';
import type { ReactNode } from 'react';
import { forwardRef, useCallback, useEffect } from 'react';
import { useEffect, useRef, useState } from 'react';
import { IconButton } from '../../ui/button/icon-button';
import {
floatingMaxWidth,
haloStyle,
installLabelStyle,
navBodyStyle,
navFooterStyle,
navHeaderStyle,
navStyle,
navWidthVar,
particlesStyle,
sidebarButtonStyle,
navWrapperStyle,
sidebarFloatMaskStyle,
updaterButtonStyle,
} from './index.css';
import {
APP_SIDEBAR_OPEN,
appSidebarOpenAtom,
appSidebarResizingAtom,
appSidebarWidthAtom,
updateAvailableAtom,
} from './index.jotai';
import { ResizeIndicator } from './resize-indicator';
import { SidebarHeader } from './sidebar-header';
export { appSidebarOpenAtom };
export type AppSidebarProps = PropsWithChildren;
export type AppSidebarProps = PropsWithChildren<{
footer?: ReactNode | undefined;
}>;
function useEnableAnimation() {
const [enable, setEnable] = useState(false);
useEffect(() => {
setTimeout(() => {
setEnable(true);
}, 500);
}, []);
return enable;
}
export const AppSidebar = forwardRef<HTMLElement, AppSidebarProps>(
function AppSidebar(props, forwardedRef): ReactElement {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
const t = useAFFiNEI18N();
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
const initialRender = open === undefined;
export function AppSidebar(props: AppSidebarProps): ReactElement {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
const initialRender = open === undefined;
const handleSidebarOpen = useCallback(() => {
setOpen(open => !open);
}, [setOpen]);
const isResizing = useAtomValue(appSidebarResizingAtom);
const navRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (
open === undefined &&
localStorage.getItem(APP_SIDEBAR_OPEN) === null
) {
// give the initial value,
// so that the sidebar can be closed on mobile by default
const { matches } = window.matchMedia(
`(min-width: ${floatingMaxWidth}px)`
);
setOpen(matches);
}
}, [open, setOpen]);
const environment = getEnvironment();
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
if (initialRender) {
// avoid the UI flash
return <div />;
useEffect(() => {
if (open === undefined && localStorage.getItem(APP_SIDEBAR_OPEN) === null) {
// give the initial value,
// so that the sidebar can be closed on mobile by default
const { matches } = window.matchMedia(
`(min-width: ${floatingMaxWidth}px)`
);
setOpen(matches);
}
return (
<>
<nav
className={navStyle}
ref={forwardedRef}
style={assignInlineVars({
[navWidthVar]: `${appSidebarWidth}px`,
})}
data-testid="app-sidebar"
data-open={open}
data-is-macos-electron={isMacosDesktop}
>
<div
className={navHeaderStyle}
data-is-macos-electron={isMacosDesktop}
data-open={open}
>
{isMacosDesktop && (
<>
<IconButton
size="middle"
onClick={() => {
window.history.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
size="middle"
onClick={() => {
window.history.forward();
}}
style={{ marginLeft: '32px' }}
>
<ArrowRightSmallIcon />
</IconButton>
</>
)}
<IconButton
data-testid="app-sidebar-arrow-button-collapse"
className={sidebarButtonStyle}
onClick={handleSidebarOpen}
>
<SidebarIcon width={24} height={24} />
</IconButton>
</div>
<div className={navBodyStyle}>{props.children}</div>
{clientUpdateAvailable && (
<Button
onClick={() => {
window.apis?.updater.updateClient();
}}
noBorder
className={updaterButtonStyle}
type={'light'}
>
<div className={particlesStyle} aria-hidden="true"></div>
<span className={haloStyle} aria-hidden="true"></span>
<div className={installLabelStyle}>
<ResetIcon />
<span>{t['Restart Install Client Update']()}</span>
</div>
</Button>
)}
<div className={navFooterStyle}>{props.footer}</div>
</nav>
<div
data-testid="app-sidebar-float-mask"
data-open={open}
className={sidebarFloatMaskStyle}
onClick={() => setOpen(false)}
/>
</>
);
}
);
}, [open, setOpen]);
// disable animation to avoid UI flash
const enableAnimation = useEnableAnimation();
const environment = getEnvironment();
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
if (initialRender) {
// avoid the UI flash
return <div />;
}
return (
<>
<div
style={assignInlineVars({
[navWidthVar]: `${appSidebarWidth}px`,
})}
className={navWrapperStyle}
data-open={open}
data-is-macos-electron={isMacosDesktop}
data-enable-animation={enableAnimation && !isResizing}
>
<nav className={navStyle} ref={navRef} data-testid="app-sidebar">
<SidebarHeader />
<div className={navBodyStyle} data-testid="sliderBar-inner">
{props.children}
</div>
</nav>
<ResizeIndicator targetElement={navRef.current} />
</div>
<div
data-testid="app-sidebar-float-mask"
data-open={open}
className={sidebarFloatMaskStyle}
onClick={() => setOpen(false)}
/>
</>
);
}
export * from './add-page-button';
export * from './app-updater-button';
export * from './category-divider';
export { AppSidebarFallback } from './fallback';
export type { ResizeIndicatorProps } from './resize-indicator';
export { ResizeIndicator } from './resize-indicator';
export * from './menu-item';
export * from './quick-search-input';
export * from './sidebar-containers';
export { appSidebarOpenAtom, appSidebarResizingAtom, updateAvailableAtom };
@@ -0,0 +1,48 @@
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'inline-flex',
alignItems: 'center',
borderRadius: '4px',
width: '100%',
minHeight: '30px',
userSelect: 'none',
cursor: 'pointer',
padding: '0 12px',
fontSize: 'var(--affine-font-sm)',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
},
'&[data-active="true"]': {
color: 'var(--affine-primary-color)',
background: 'var(--affine-hover-color)',
},
'&[data-disabled="true"]': {
cursor: 'default',
color: 'var(--affine-text-secondary-color)',
pointerEvents: 'none',
},
},
});
export const content = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const icon = style({
marginRight: '14px',
color: 'var(--affine-icon-color)',
fontSize: '18px',
});
export const spacer = style({
flex: 1,
});
export const linkItemRoot = style({
color: 'inherit',
display: 'contents',
});
@@ -0,0 +1,34 @@
import { SettingsIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { MenuItem, MenuLinkItem } from '.';
export default {
title: 'Components/AppSidebar/MenuItem',
component: MenuItem,
} satisfies Meta;
export const Default: StoryFn = () => {
return (
<main style={{ width: '240px' }}>
<MenuItem icon={<SettingsIcon />} onClick={() => alert('opened')}>
Normal Item
</MenuItem>
<MenuLinkItem
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Normal Link Item
</MenuLinkItem>
<MenuLinkItem
active
icon={<SettingsIcon />}
href="/test"
onClick={() => alert('opened')}
>
Primary Item
</MenuLinkItem>
</main>
);
};
@@ -0,0 +1,47 @@
import clsx from 'clsx';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import React from 'react';
import * as styles from './index.css';
interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
icon?: React.ReactElement;
active?: boolean;
disabled?: boolean;
}
interface MenuLinkItemProps extends MenuItemProps, Pick<LinkProps, 'href'> {}
export function MenuItem({
onClick,
icon,
active,
children,
disabled,
...props
}: MenuItemProps) {
return (
<div
{...props}
className={clsx([styles.root, props.className])}
onClick={onClick}
data-active={active}
data-disabled={disabled}
>
{icon &&
React.cloneElement(icon, {
className: clsx([styles.icon, icon.props.className]),
})}
<div className={styles.content}>{children}</div>
</div>
);
}
export function MenuLinkItem({ href, ...props }: MenuLinkItemProps) {
return (
<Link href={href} className={styles.linkItemRoot}>
<MenuItem {...props}></MenuItem>
</Link>
);
}
@@ -0,0 +1,35 @@
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'inline-flex',
background: 'var(--affine-white-10)',
alignItems: 'center',
borderRadius: '8px',
border: '1px solid var(--affine-black-10)',
fontSize: 'var(--affine-font-sm)',
width: '100%',
height: '36px',
userSelect: 'none',
cursor: 'pointer',
padding: '0 12px',
margin: '12px 0',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
},
},
});
export const icon = style({
marginRight: '14px',
color: 'var(--affine-icon-color)',
});
export const spacer = style({
flex: 1,
});
export const shortcutHint = style({
color: 'var(--affine-black-30)',
fontSize: 'var(--affine-font-base)',
});
@@ -0,0 +1,16 @@
import type { Meta, StoryFn } from '@storybook/react';
import { QuickSearchInput } from '.';
export default {
title: 'Components/AppSidebar/QuickSearchInput',
component: QuickSearchInput,
} satisfies Meta;
export const Default: StoryFn = () => {
return (
<main style={{ width: '240px' }}>
<QuickSearchInput onClick={() => alert('opened')} />
</main>
);
};
@@ -0,0 +1,32 @@
import { getEnvironment } from '@affine/env/config';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SearchIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import * as styles from './index.css';
interface QuickSearchInputProps extends React.HTMLAttributes<HTMLDivElement> {
onClick?: () => void;
}
// Although it is called an input, it is actually a button.
export function QuickSearchInput({ onClick, ...props }: QuickSearchInputProps) {
const t = useAFFiNEI18N();
const environment = getEnvironment();
const isMac = environment.isBrowser && environment.isMacOs;
return (
<div
{...props}
className={clsx([props.className, styles.root])}
onClick={onClick}
>
<SearchIcon className={styles.icon} />
{t['Quick search']()}
<div className={styles.spacer} />
<div className={styles.shortcutHint}>
{isMac ? ' ⌘ + K' : ' Ctrl + K'}
</div>
</div>
);
}
@@ -1,16 +1,14 @@
import { style } from '@vanilla-extract/css';
import { navWidthVar } from '../index.css';
export const spacerStyle = style({
export const resizerContainer = style({
position: 'absolute',
left: navWidthVar,
right: 0,
top: 0,
bottom: 0,
width: '7px',
width: '10px',
height: '100%',
borderLeft: '1px solid var(--affine-border-color)',
zIndex: 'calc(var(--affine-z-index-modal) - 1)',
zIndex: 'calc(var(--affine-z-index-modal) + 1)',
transform: 'translateX(50%)',
backgroundColor: 'transparent',
opacity: 0,
cursor: 'col-resize',
@@ -36,3 +34,14 @@ export const spacerStyle = style({
},
},
});
export const resizerInner = style({
position: 'absolute',
height: '100%',
width: '2px',
left: '3px',
backgroundColor: 'var(--affine-border-color)',
selectors: {
[`${resizerContainer}:hover &`]: {},
},
});
@@ -1,56 +1,38 @@
import type { Instance } from '@popperjs/core';
import { createPopper } from '@popperjs/core';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { useCallback, useLayoutEffect, useState } from 'react';
import {
useCallback,
useDeferredValue,
useEffect,
useRef,
useState,
} from 'react';
appSidebarOpenAtom,
appSidebarResizingAtom,
appSidebarWidthAtom,
} from '../index.jotai';
import * as styles from './index.css';
import { appSidebarOpenAtom, appSidebarWidthAtom } from '../index.jotai';
import { spacerStyle } from './index.css';
export type ResizeIndicatorProps = {
type ResizeIndicatorProps = {
targetElement: HTMLElement | null;
};
export const ResizeIndicator = (props: ResizeIndicatorProps): ReactElement => {
const ref = useRef<HTMLDivElement>(null);
const popperRef = useRef<Instance | null>(null);
const setWidth = useSetAtom(appSidebarWidthAtom);
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
const [isResizing, setIsResizing] = useState(false);
useEffect(() => {
if (ref.current) {
if (props.targetElement) {
const popper = createPopper(props.targetElement, ref.current, {
placement: 'right',
});
popperRef.current = popper;
return () => {
popper.destroy();
popperRef.current = null;
};
}
}
}, [props.targetElement]);
const [isResizing, setIsResizing] = useAtom(appSidebarResizingAtom);
const sidebarWidth = useDeferredValue(useAtomValue(appSidebarWidthAtom));
useEffect(() => {
if (popperRef.current) {
popperRef.current.update();
}
}, [sidebarWidth]);
const [anchorLeft, setAnchorLeft] = useState(0);
useLayoutEffect(() => {
if (!props.targetElement) return;
const { left } = props.targetElement.getBoundingClientRect();
setAnchorLeft(left);
}, [props.targetElement]);
const onResizeStart = useCallback(() => {
let resized = false;
function onMouseMove(e: MouseEvent) {
e.preventDefault();
const newWidth = Math.min(480, Math.max(e.clientX, 256));
if (!props.targetElement) return;
const newWidth = Math.min(480, Math.max(e.clientX - anchorLeft, 256));
setWidth(newWidth);
setIsResizing(true);
resized = true;
@@ -64,24 +46,28 @@ export const ResizeIndicator = (props: ResizeIndicatorProps): ReactElement => {
if (!resized) {
setSidebarOpen(o => !o);
}
if (popperRef.current) {
popperRef.current.update();
}
setIsResizing(false);
document.removeEventListener('mousemove', onMouseMove);
},
{ once: true }
);
}, [setSidebarOpen, setWidth]);
}, [
anchorLeft,
props.targetElement,
setIsResizing,
setSidebarOpen,
setWidth,
]);
return (
<div
ref={ref}
className={spacerStyle}
className={styles.resizerContainer}
data-testid="app-sidebar-resizer"
data-resizing={isResizing}
data-open={sidebarOpen}
onMouseDown={onResizeStart}
/>
>
<div className={styles.resizerInner} />
</div>
);
};
@@ -0,0 +1,75 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const baseContainer = style({
padding: '12px 16px',
display: 'flex',
flexFlow: 'column nowrap',
rowGap: '8px',
});
export const scrollableContainerRoot = style({
flex: '1 1 auto',
overflowY: 'hidden',
vars: {
'--scrollbar-width': '10px',
},
transition: 'all .3s .2s',
borderTop: '1px solid transparent',
selectors: {
'&[data-has-scroll-top="true"]': {
boxShadow: 'inset 0 8px 8px -8px var(--affine-black-10)',
},
},
});
export const scrollableViewport = style({
height: '100%',
});
globalStyle(`${scrollableViewport} > div`, {
maxWidth: '100%',
display: 'block !important',
});
export const scrollableContainer = style([
baseContainer,
{
height: '100%',
},
]);
export const scrollbar = style({
display: 'flex',
flexDirection: 'column',
userSelect: 'none',
touchAction: 'none',
padding: '0 2px',
width: 'var(--scrollbar-width)',
height: '100%',
opacity: 1,
transition: 'opacity .15s',
selectors: {
'&[data-state="hidden"]': {
opacity: 0,
},
},
});
export const scrollbarThumb = style({
position: 'relative',
background: 'var(--affine-black-30)',
borderRadius: '4px',
selectors: {
'&::before': {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '100%',
height: '100%',
minWidth: '44px',
minHeight: '44px',
},
},
});
@@ -0,0 +1,61 @@
import * as ScrollArea from '@radix-ui/react-scroll-area';
import clsx from 'clsx';
import { type PropsWithChildren, useEffect, useRef, useState } from 'react';
import * as styles from './index.css';
export function SidebarContainer({ children }: PropsWithChildren) {
return <div className={clsx([styles.baseContainer])}>{children}</div>;
}
function useHasScrollTop() {
const ref = useRef<HTMLDivElement>(null);
const [hasScrollTop, setHasScrollTop] = useState(false);
useEffect(() => {
if (!ref.current) {
return;
}
const container = ref.current;
function updateScrollTop() {
if (container) {
const hasScrollTop = container.scrollTop > 0;
setHasScrollTop(hasScrollTop);
}
}
container.addEventListener('scroll', updateScrollTop);
updateScrollTop();
return () => {
container.removeEventListener('scroll', updateScrollTop);
};
}, []);
return [hasScrollTop, ref] as const;
}
export function SidebarScrollableContainer({ children }: PropsWithChildren) {
const [hasScrollTop, ref] = useHasScrollTop();
return (
<ScrollArea.Root
data-has-scroll-top={hasScrollTop}
className={styles.scrollableContainerRoot}
>
<ScrollArea.Viewport
className={clsx([styles.scrollableViewport])}
ref={ref}
>
<div className={clsx([styles.scrollableContainer])}>{children}</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
forceMount
orientation="vertical"
className={styles.scrollbar}
>
<ScrollArea.Thumb className={styles.scrollbarThumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
);
}
@@ -0,0 +1,52 @@
import { IconButton } from '@affine/component';
import { getEnvironment } from '@affine/env/config';
import {
ArrowLeftSmallIcon,
ArrowRightSmallIcon,
SidebarIcon,
} from '@blocksuite/icons';
import { useAtom } from 'jotai';
import { navHeaderStyle, sidebarButtonStyle } from '../index.css';
import { appSidebarOpenAtom } from '../index.jotai';
export const SidebarHeader = () => {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const environment = getEnvironment();
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
return (
<div
className={navHeaderStyle}
data-is-macos-electron={isMacosDesktop}
data-open={open}
>
{isMacosDesktop && (
<>
<IconButton
size="middle"
onClick={() => {
window.history.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
size="middle"
onClick={() => {
window.history.forward();
}}
>
<ArrowRightSmallIcon />
</IconButton>
</>
)}
<IconButton
data-testid="app-sidebar-arrow-button-collapse"
className={sidebarButtonStyle}
onClick={() => setOpen(open => !open)}
>
<SidebarIcon width={24} height={24} />
</IconButton>
</div>
);
};
@@ -51,7 +51,7 @@ const slideOut2 = keyframes({
export const changeLogWrapperSlideInStyle = style({
width: 'calc(100% + 4px)',
height: '0px',
flexShrink: 0,
animation: `${slideIn} 1s ease-in-out forwards`,
display: 'flex',
justifyContent: 'flex-start',
@@ -1,4 +1,4 @@
import { style } from '@vanilla-extract/css';
import { globalStyle, style } from '@vanilla-extract/css';
import { breakpoints } from '../../styles/mui-theme';
export const appStyle = style({
@@ -13,6 +13,14 @@ export const appStyle = style({
'&[data-is-resizing="true"]': {
cursor: 'col-resize',
},
'&[data-noise-background="true"]:before': {
content: '""',
position: 'absolute',
inset: 0,
opacity: 'var(--affine-noise-opacity)',
backgroundSize: '25%',
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.25' numOctaves='10' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
},
},
vars: {
'--affine-editor-width': '686px',
@@ -26,11 +34,35 @@ export const appStyle = style({
},
});
globalStyle(`html[data-theme="light"] ${appStyle}`, {
vars: {
'--affine-noise-opacity': '0.25',
},
});
globalStyle(`html[data-theme="dark"] ${appStyle}`, {
vars: {
'--affine-noise-opacity': '0.1',
},
});
export const mainContainerStyle = style({
position: 'relative',
flexGrow: 1,
maxWidth: '100%',
zIndex: 0,
backgroundColor: 'var(--affine-background-primary-color)',
selectors: {
'&[data-is-desktop="true"]': {
margin: '8px 8px 8px 8px',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: 'var(--affine-shadow-1)',
},
'&[data-is-desktop="true"][data-is-sidebar-open="true"]': {
marginLeft: '2px',
},
},
});
export const toolStyle = style({
@@ -9,8 +9,13 @@ export type WorkspaceRootProps = PropsWithChildren<{
}>;
export const AppContainer = (props: WorkspaceRootProps): ReactElement => {
const noisyBackground = environment.isDesktop && environment.isMacOs;
return (
<div className={appStyle} data-is-resizing={props.resizing}>
<div
className={appStyle}
data-noise-background={noisyBackground}
data-is-resizing={props.resizing}
>
{props.children}
</div>
);
@@ -18,12 +23,15 @@ export const AppContainer = (props: WorkspaceRootProps): ReactElement => {
export type MainContainerProps = PropsWithChildren<{
className?: string;
sidebarOpen?: boolean;
}>;
export const MainContainer = (props: MainContainerProps): ReactElement => {
return (
<div
className={clsx(mainContainerStyle, 'main-container', props.className)}
data-is-desktop={environment.isDesktop}
data-is-sidebar-open={props.sidebarOpen}
>
{props.children}
</div>
+1
View File
@@ -12,6 +12,7 @@
},
"exports": {
".": "./src/index.ts",
"./config": "./src/config.ts",
"./constant": "./src/constant.ts",
"./blocksuite": "./src/blocksuite.ts"
},
+2
View File
@@ -18,6 +18,7 @@
"No item": "No item",
"Import": "Import",
"Trash": "Trash",
"others": "Others",
"New Page": "New Page",
"New Keyword Page": "New '{{query}}' page",
"Find 0 result": "Find 0 result",
@@ -231,6 +232,7 @@
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
"Recent": "Recent",
"Successfully deleted": "Successfully deleted",
"Update Available": "Update available",
"Restart Install Client Update": "Restart to install update",
"Add Workspace": "Add Workspace",
"Add Workspace Hint": "Select where you already have",
@@ -18,7 +18,7 @@ test('New a page , then delete it in all pages, permanently delete it', async ({
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to restore',
});
@@ -36,7 +36,7 @@ test('New a page , then delete it in all pages, permanently delete it', async ({
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
await page.getByTestId('trash-page').click();
// permanently delete it
await page
.getByTestId('more-actions-' + newPageId)
@@ -18,7 +18,7 @@ test.skip('New a page ,then open it and export html', async ({ page }) => {
await page
.getByPlaceholder('Title')
.fill('this is a new page to export html content');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to export html content',
@@ -46,7 +46,7 @@ test.skip('New a page ,then open it and export markdown', async ({ page }) => {
await page
.getByPlaceholder('Title')
.fill('this is a new page to export markdown content');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to export markdown content',
});
@@ -16,7 +16,7 @@ test('New a page and open it ,then favorite it', async ({ page }) => {
await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to favorite',
});
@@ -55,7 +55,7 @@ test('Cancel favorite', async ({ page }) => {
await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to favorite',
});
@@ -68,14 +68,13 @@ test('Cancel favorite', async ({ page }) => {
await favoriteBtn.click();
// expect it in favorite list
await page.getByRole('link', { name: 'Favorites' }).click();
expect(
page.getByRole('cell', { name: 'this is a new page to favorite' })
).not.toBeUndefined();
// cancel favorite
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const box = await page
.getByRole('cell', { name: 'this is a new page to favorite' })
@@ -86,7 +85,6 @@ test('Cancel favorite', async ({ page }) => {
await page.getByTestId('favorited-icon').click();
// expect it not in favorite list
await page.getByRole('link', { name: 'Favorites' }).click();
expect(
page.getByText(
'Tips: Click Add to Favorites/Trash and the page will appear here.'
@@ -8,7 +8,6 @@ import {
newPage,
waitMarkdownImported,
} from '../libs/page-logic';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test('Show favorite items in sidebar', async ({ page }) => {
await openHomePage(page);
@@ -17,7 +16,7 @@ test('Show favorite items in sidebar', async ({ page }) => {
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to favorite',
});
@@ -41,7 +40,7 @@ test('Show favorite items in favorite list', async ({ page }) => {
await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to favorite',
});
@@ -52,16 +51,11 @@ test('Show favorite items in favorite list', async ({ page }) => {
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
await page.getByRole('link', { name: 'Favorites' }).click();
await page.getByTestId('all-pages').click();
expect(
page.getByRole('cell', { name: 'this is a new page to favorite' })
).not.toBeUndefined();
await page.getByRole('cell').getByRole('button').nth(0).click();
expect(
await page
.getByText('Click Add to Favorites and the page will appear here.')
.isVisible()
).toBe(true);
await assertCurrentWorkspaceFlavour('local', page);
});
+1 -1
View File
@@ -25,7 +25,7 @@ test('click btn bew page and find it in all pages', async ({ page }) => {
await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', { name: 'this is a new page' });
expect(cell).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
@@ -18,7 +18,7 @@ test('click btn bew page and open in tab', async ({ page }) => {
const newPageUrl = page.url();
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
await page
.getByTestId('more-actions-' + newPageId)
@@ -18,7 +18,7 @@ test('New a page , then delete it in all pages, restore it', async ({
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to restore',
});
@@ -36,7 +36,7 @@ test('New a page , then delete it in all pages, restore it', async ({
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
await page.getByTestId('trash-page').click();
await page.waitForTimeout(50);
const trashPage = page.url();
// restore it
@@ -48,7 +48,7 @@ test('New a page , then delete it in all pages, restore it', async ({
// stay in trash page
expect(page.url()).toBe(trashPage);
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const restoreCell = page.getByRole('cell', {
name: 'this is a new page to restore',
});
@@ -16,7 +16,7 @@ test('New a page ,then open it and show delete modal', async ({ page }) => {
await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to delete',
});
@@ -40,7 +40,7 @@ test('New a page ,then go to all pages and show delete modal', async ({
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to delete',
});
@@ -18,7 +18,7 @@ test('New a page , then delete it in all pages, finally find it in trash', async
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to delete',
});
@@ -36,7 +36,7 @@ test('New a page , then delete it in all pages, finally find it in trash', async
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
await page.getByTestId('trash-page').click();
expect(
page.getByRole('cell', { name: 'this is a new page to delete' })
).not.toBeUndefined();
+1 -1
View File
@@ -48,7 +48,7 @@ test('Click right-bottom corner change log icon', async ({ page }) => {
);
await page.waitForTimeout(50);
expect(await editorRightBottomChangeLog.isVisible()).toEqual(true);
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('all-pages').click();
const normalRightBottomChangeLog = page.locator(
'[data-testid=right-bottom-change-log-icon]'
);