mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(component): init app sidebar (#2135)
This commit is contained in:
@@ -1,19 +1,14 @@
|
|||||||
import { atomWithSyncStorage } from '@affine/jotai';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
export type Visibility = Record<string, boolean>;
|
export type Visibility = Record<string, boolean>;
|
||||||
|
|
||||||
const DEFAULT_VALUE = '0.0.0';
|
const DEFAULT_VALUE = '0.0.0';
|
||||||
|
|
||||||
export const lastVersionAtom = atomWithSyncStorage(
|
export const lastVersionAtom = atomWithStorage('lastVersion', DEFAULT_VALUE);
|
||||||
'lastVersion',
|
|
||||||
DEFAULT_VALUE
|
|
||||||
);
|
|
||||||
export const guideHiddenAtom = atomWithSyncStorage<Visibility>(
|
|
||||||
'guideHidden',
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const guideHiddenUntilNextUpdateAtom = atomWithSyncStorage<Visibility>(
|
export const guideHiddenAtom = atomWithStorage<Visibility>('guideHidden', {});
|
||||||
|
|
||||||
|
export const guideHiddenUntilNextUpdateAtom = atomWithStorage<Visibility>(
|
||||||
'guideHiddenUntilNextUpdate',
|
'guideHiddenUntilNextUpdate',
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Tooltip } from '@affine/component';
|
import { Tooltip } from '@affine/component';
|
||||||
|
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
||||||
import { getEnvironment } from '@affine/env';
|
import { getEnvironment } from '@affine/env';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +10,6 @@ import {
|
|||||||
useGuideHiddenUntilNextUpdate,
|
useGuideHiddenUntilNextUpdate,
|
||||||
useUpdateTipsOnVersionChange,
|
useUpdateTipsOnVersionChange,
|
||||||
} from '../../../hooks/use-is-first-load';
|
} from '../../../hooks/use-is-first-load';
|
||||||
import { useSidebarStatus } from '../../../hooks/use-sidebar-status';
|
|
||||||
import { SidebarSwitchIcon } from './icons';
|
import { SidebarSwitchIcon } from './icons';
|
||||||
import { StyledSidebarSwitch } from './style';
|
import { StyledSidebarSwitch } from './style';
|
||||||
type SidebarSwitchProps = {
|
type SidebarSwitchProps = {
|
||||||
@@ -24,7 +25,7 @@ export const SidebarSwitch = ({
|
|||||||
...props
|
...props
|
||||||
}: SidebarSwitchProps) => {
|
}: SidebarSwitchProps) => {
|
||||||
useUpdateTipsOnVersionChange();
|
useUpdateTipsOnVersionChange();
|
||||||
const [open, setOpen] = useSidebarStatus();
|
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||||
const [guideHidden, setGuideHidden] = useGuideHidden();
|
const [guideHidden, setGuideHidden] = useGuideHidden();
|
||||||
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
|
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
|
||||||
@@ -56,9 +57,9 @@ export const SidebarSwitch = ({
|
|||||||
visible={visible}
|
visible={visible}
|
||||||
disabled={!visible}
|
disabled={!visible}
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setOpen(!open);
|
setOpen(open => !open);
|
||||||
setTooltipVisible(false);
|
setTooltipVisible(false);
|
||||||
if (guideHiddenUntilNextUpdate['quickSearchTips'] === false) {
|
if (!guideHiddenUntilNextUpdate['quickSearchTips']) {
|
||||||
setGuideHiddenUntilNextUpdate({
|
setGuideHiddenUntilNextUpdate({
|
||||||
...guideHiddenUntilNextUpdate,
|
...guideHiddenUntilNextUpdate,
|
||||||
quickSearchTips: true,
|
quickSearchTips: true,
|
||||||
@@ -70,7 +71,6 @@ export const SidebarSwitch = ({
|
|||||||
}, [
|
}, [
|
||||||
guideHidden,
|
guideHidden,
|
||||||
guideHiddenUntilNextUpdate,
|
guideHiddenUntilNextUpdate,
|
||||||
open,
|
|
||||||
setGuideHidden,
|
setGuideHidden,
|
||||||
setGuideHiddenUntilNextUpdate,
|
setGuideHiddenUntilNextUpdate,
|
||||||
setOpen,
|
setOpen,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { CloseIcon } from '@blocksuite/icons';
|
import { CloseIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@@ -12,10 +14,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import {
|
|
||||||
useSidebarFloating,
|
|
||||||
useSidebarStatus,
|
|
||||||
} from '../../../hooks/use-sidebar-status';
|
|
||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||||
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
|
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
|
||||||
import EditPage from './header-right-items/EditPage';
|
import EditPage from './header-right-items/EditPage';
|
||||||
@@ -142,17 +140,11 @@ export const Header = forwardRef<
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowWarning(shouldShowWarning());
|
setShowWarning(shouldShowWarning());
|
||||||
}, []);
|
}, []);
|
||||||
const [open] = useSidebarStatus();
|
const [open] = useAtom(appSidebarOpenAtom);
|
||||||
const sidebarFloating = useSidebarFloating();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledHeaderContainer
|
<StyledHeaderContainer ref={ref} hasWarning={showWarning} {...props}>
|
||||||
sidebarFloating={sidebarFloating && open}
|
|
||||||
ref={ref}
|
|
||||||
hasWarning={showWarning}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<BrowserWarning
|
<BrowserWarning
|
||||||
show={showWarning}
|
show={showWarning}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
|
|
||||||
export const StyledHeaderContainer = styled('div')<{
|
export const StyledHeaderContainer = styled('div')<{
|
||||||
hasWarning: boolean;
|
hasWarning: boolean;
|
||||||
sidebarFloating: boolean;
|
}>(({ hasWarning }) => {
|
||||||
}>(({ theme, hasWarning, sidebarFloating }) => {
|
|
||||||
return {
|
return {
|
||||||
height: hasWarning ? '96px' : '52px',
|
height: hasWarning ? '96px' : '52px',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
@@ -16,10 +15,6 @@ export const StyledHeaderContainer = styled('div')<{
|
|||||||
top: 0,
|
top: 0,
|
||||||
background: 'var(--affine-background-primary-color)',
|
background: 'var(--affine-background-primary-color)',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
WebkitAppRegion: sidebarFloating ? '' : 'drag',
|
|
||||||
button: {
|
|
||||||
WebkitAppRegion: 'no-drag',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
export const StyledHeader = styled('div')<{ hasWarning: boolean }>(
|
export const StyledHeader = styled('div')<{ hasWarning: boolean }>(
|
||||||
|
|||||||
@@ -1,47 +1,6 @@
|
|||||||
import { config } from '@affine/env';
|
|
||||||
import { useTranslation } from '@affine/i18n';
|
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
|
||||||
import {
|
|
||||||
DeleteTemporarilyIcon,
|
|
||||||
FolderIcon,
|
|
||||||
PlusIcon,
|
|
||||||
SearchIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
ShareIcon,
|
|
||||||
} from '@blocksuite/icons';
|
|
||||||
import type { Page, PageMeta } from '@blocksuite/store';
|
import type { Page, PageMeta } from '@blocksuite/store';
|
||||||
import type React from 'react';
|
|
||||||
import type { UIEvent } from 'react';
|
|
||||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
useSidebarFloating,
|
|
||||||
useSidebarResizing,
|
|
||||||
useSidebarStatus,
|
|
||||||
useSidebarWidth,
|
|
||||||
} from '../../../hooks/use-sidebar-status';
|
|
||||||
import type { AllWorkspace } from '../../../shared';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
import { ChangeLog } from './changeLog';
|
|
||||||
import Favorite from './favorite';
|
|
||||||
import { RouteNavigation } from './RouteNavigation';
|
|
||||||
import { StyledListItem } from './shared-styles';
|
|
||||||
import {
|
|
||||||
StyledLink,
|
|
||||||
StyledNewPageButton,
|
|
||||||
StyledScrollWrapper,
|
|
||||||
StyledSidebarHeader,
|
|
||||||
StyledSliderBar,
|
|
||||||
StyledSliderBarInnerWrapper,
|
|
||||||
StyledSliderBarWrapper,
|
|
||||||
StyledSliderModalBackground,
|
|
||||||
} from './style';
|
|
||||||
import { WorkspaceSelector } from './WorkspaceSelector';
|
|
||||||
|
|
||||||
const SidebarSwitch = lazy(() =>
|
|
||||||
import('../../affine/sidebar-switch').then(module => ({
|
|
||||||
default: module.SidebarSwitch,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export type FavoriteListProps = {
|
export type FavoriteListProps = {
|
||||||
currentPageId: string | null;
|
currentPageId: string | null;
|
||||||
@@ -67,204 +26,3 @@ export type WorkSpaceSliderBarProps = {
|
|||||||
shared: (workspaceId: string) => string;
|
shared: (workspaceId: string) => string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
|
||||||
isPublicWorkspace,
|
|
||||||
currentWorkspace,
|
|
||||||
currentPageId,
|
|
||||||
openPage,
|
|
||||||
createPage,
|
|
||||||
currentPath,
|
|
||||||
paths,
|
|
||||||
onOpenQuickSearchModal,
|
|
||||||
onOpenWorkspaceListModal,
|
|
||||||
}) => {
|
|
||||||
const currentWorkspaceId = currentWorkspace?.id || null;
|
|
||||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useSidebarStatus();
|
|
||||||
const onClickNewPage = useCallback(async () => {
|
|
||||||
const page = await createPage();
|
|
||||||
openPage(page.id);
|
|
||||||
}, [createPage, openPage]);
|
|
||||||
const floatingSlider = useSidebarFloating();
|
|
||||||
const [sliderWidth] = useSidebarWidth();
|
|
||||||
const [isResizing] = useSidebarResizing();
|
|
||||||
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
|
|
||||||
const show = isPublicWorkspace ? false : sidebarOpen;
|
|
||||||
const actualWidth = floatingSlider ? 'calc(10vw + 400px)' : sliderWidth;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (environment.isDesktop) {
|
|
||||||
window.apis?.onSidebarVisibilityChange(sidebarOpen);
|
|
||||||
}
|
|
||||||
}, [sidebarOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const keydown = (e: KeyboardEvent) => {
|
|
||||||
if ((e.key === '/' && e.metaKey) || (e.key === '/' && e.ctrlKey)) {
|
|
||||||
setSidebarOpen(!sidebarOpen);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', keydown, { capture: true });
|
|
||||||
return () =>
|
|
||||||
document.removeEventListener('keydown', keydown, { capture: true });
|
|
||||||
}, [sidebarOpen, setSidebarOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledSliderBarWrapper
|
|
||||||
resizing={isResizing}
|
|
||||||
floating={floatingSlider}
|
|
||||||
show={show}
|
|
||||||
style={{ width: actualWidth }}
|
|
||||||
data-testid="sliderBar-root"
|
|
||||||
>
|
|
||||||
<StyledSliderBar>
|
|
||||||
<StyledSidebarHeader>
|
|
||||||
<RouteNavigation />
|
|
||||||
<Suspense>
|
|
||||||
<SidebarSwitch
|
|
||||||
visible={sidebarOpen}
|
|
||||||
tooltipContent={t('Collapse sidebar')}
|
|
||||||
data-testid="sliderBar-arrowButton-collapse"
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</StyledSidebarHeader>
|
|
||||||
|
|
||||||
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
|
|
||||||
<WorkspaceSelector
|
|
||||||
currentWorkspace={currentWorkspace}
|
|
||||||
onClick={onOpenWorkspaceListModal}
|
|
||||||
/>
|
|
||||||
<ChangeLog />
|
|
||||||
<StyledListItem
|
|
||||||
data-testid="slider-bar-quick-search-button"
|
|
||||||
onClick={useCallback(() => {
|
|
||||||
onOpenQuickSearchModal();
|
|
||||||
}, [onOpenQuickSearchModal])}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
{t('Quick search')}
|
|
||||||
</StyledListItem>
|
|
||||||
<StyledListItem
|
|
||||||
active={
|
|
||||||
currentPath ===
|
|
||||||
(currentWorkspaceId && paths.setting(currentWorkspaceId))
|
|
||||||
}
|
|
||||||
data-testid="slider-bar-workspace-setting-button"
|
|
||||||
style={{
|
|
||||||
marginBottom: '16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
{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))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledLink
|
|
||||||
href={{
|
|
||||||
pathname:
|
|
||||||
currentWorkspaceId && paths.shared(currentWorkspaceId),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShareIcon />
|
|
||||||
<span data-testid="shared-pages">{t('Shared Pages')}</span>
|
|
||||||
</StyledLink>
|
|
||||||
</StyledListItem>
|
|
||||||
))}
|
|
||||||
<StyledListItem
|
|
||||||
active={
|
|
||||||
currentPath ===
|
|
||||||
(currentWorkspaceId && paths.trash(currentWorkspaceId))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledLink
|
|
||||||
href={{
|
|
||||||
pathname:
|
|
||||||
currentWorkspaceId && paths.trash(currentWorkspaceId),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteTemporarilyIcon /> {t('Trash')}
|
|
||||||
</StyledLink>
|
|
||||||
</StyledListItem>
|
|
||||||
</StyledSliderBarInnerWrapper>
|
|
||||||
|
|
||||||
<StyledNewPageButton
|
|
||||||
data-testid="new-page-button"
|
|
||||||
onClick={onClickNewPage}
|
|
||||||
>
|
|
||||||
<PlusIcon /> {t('New Page')}
|
|
||||||
</StyledNewPageButton>
|
|
||||||
</StyledSliderBar>
|
|
||||||
</StyledSliderBarWrapper>
|
|
||||||
<StyledSliderModalBackground
|
|
||||||
data-testid="sliderBar-modalBackground"
|
|
||||||
active={floatingSlider && sidebarOpen}
|
|
||||||
onClick={() => setSidebarOpen(false)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WorkSpaceSliderBar;
|
|
||||||
|
|||||||
@@ -2,60 +2,10 @@ import { displayFlex, styled, textEllipsis } from '@affine/component';
|
|||||||
import { baseTheme } from '@toeverything/theme';
|
import { baseTheme } from '@toeverything/theme';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const macosElectron = environment.isDesktop && environment.isMacOs;
|
|
||||||
|
|
||||||
export const StyledSliderBarWrapper = styled('div')<{
|
|
||||||
show: boolean;
|
|
||||||
floating: boolean;
|
|
||||||
resizing: boolean;
|
|
||||||
}>(({ theme, show, floating, resizing }) => {
|
|
||||||
return {
|
|
||||||
height: '100%',
|
|
||||||
position: 'absolute',
|
|
||||||
'button, a': {
|
|
||||||
userSelect: 'none',
|
|
||||||
},
|
|
||||||
zIndex: 'var(--affine-z-index-modal)',
|
|
||||||
transition: resizing ? '' : 'transform .3s, width .3s, max-width .3s',
|
|
||||||
transform: show ? 'translateX(0)' : 'translateX(-100%)',
|
|
||||||
maxWidth: floating ? 'calc(10vw + 400px)' : 'calc(100vw - 698px)',
|
|
||||||
background:
|
|
||||||
!floating && macosElectron
|
|
||||||
? 'transparent'
|
|
||||||
: 'var(--affine-background-secondary-color)',
|
|
||||||
borderRight: macosElectron ? '' : '1px solid',
|
|
||||||
borderColor: 'var(--affine-border-color)',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const StyledSliderBar = styled('div')(() => {
|
|
||||||
return {
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
padding: '0 4px',
|
|
||||||
flexShrink: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
export const StyledSidebarHeader = styled('div')(() => {
|
|
||||||
return {
|
|
||||||
height: '52px',
|
|
||||||
flexShrink: 0,
|
|
||||||
padding: '0 16px 0 10px',
|
|
||||||
WebkitAppRegion: 'drag',
|
|
||||||
button: {
|
|
||||||
WebkitAppRegion: 'no-drag',
|
|
||||||
},
|
|
||||||
...displayFlex(macosElectron ? 'flex-end' : 'space-between', 'center'),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
export const StyledSliderBarInnerWrapper = styled('div')(() => {
|
export const StyledSliderBarInnerWrapper = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
// overflowX: 'hidden',
|
margin: '0 2px',
|
||||||
// overflowY: 'auto',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
height: 'calc(100% - 52px * 2)',
|
height: 'calc(100% - 52px * 2)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -91,8 +41,6 @@ export const StyledNewPageButton = styled('button')(({ theme }) => {
|
|||||||
return {
|
return {
|
||||||
height: '52px',
|
height: '52px',
|
||||||
...displayFlex('flex-start', 'center'),
|
...displayFlex('flex-start', 'center'),
|
||||||
borderTop: '1px solid',
|
|
||||||
borderColor: 'var(--affine-border-color)',
|
|
||||||
padding: '0 8px 0 16px',
|
padding: '0 8px 0 16px',
|
||||||
svg: {
|
svg: {
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
|
|||||||
216
apps/web/src/components/root-app-sidebar/index.tsx
Normal file
216
apps/web/src/components/root-app-sidebar/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
AppSidebar,
|
||||||
|
appSidebarOpenAtom,
|
||||||
|
ResizeIndicator,
|
||||||
|
} from '@affine/component/app-sidebar';
|
||||||
|
import { config } from '@affine/env';
|
||||||
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
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 React, { useCallback, useEffect, useRef, useState } 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 { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
||||||
|
|
||||||
|
export type RootAppSidebarProps = {
|
||||||
|
isPublicWorkspace: boolean;
|
||||||
|
onOpenQuickSearchModal: () => void;
|
||||||
|
onOpenWorkspaceListModal: () => void;
|
||||||
|
currentWorkspace: AllWorkspace | null;
|
||||||
|
currentPageId: string | null;
|
||||||
|
openPage: (pageId: string) => void;
|
||||||
|
createPage: () => Page;
|
||||||
|
currentPath: string;
|
||||||
|
paths: {
|
||||||
|
all: (workspaceId: string) => string;
|
||||||
|
favorite: (workspaceId: string) => string;
|
||||||
|
trash: (workspaceId: string) => string;
|
||||||
|
setting: (workspaceId: string) => string;
|
||||||
|
shared: (workspaceId: string) => string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is for the whole affine app sidebar.
|
||||||
|
* This component wraps the app sidebar in `@affine/component` with logic and data.
|
||||||
|
*
|
||||||
|
* @todo(himself65): rewrite all styled component into @vanilla-extract/css
|
||||||
|
*/
|
||||||
|
export const RootAppSidebar = ({
|
||||||
|
currentWorkspace,
|
||||||
|
currentPageId,
|
||||||
|
openPage,
|
||||||
|
createPage,
|
||||||
|
currentPath,
|
||||||
|
paths,
|
||||||
|
onOpenQuickSearchModal,
|
||||||
|
onOpenWorkspaceListModal,
|
||||||
|
}: RootAppSidebarProps): ReactElement => {
|
||||||
|
const currentWorkspaceId = currentWorkspace?.id || null;
|
||||||
|
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
|
||||||
|
const onClickNewPage = useCallback(async () => {
|
||||||
|
const page = await createPage();
|
||||||
|
openPage(page.id);
|
||||||
|
}, [createPage, openPage]);
|
||||||
|
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||||
|
useEffect(() => {
|
||||||
|
if (environment.isDesktop) {
|
||||||
|
window.apis?.onSidebarVisibilityChange(sidebarOpen);
|
||||||
|
}
|
||||||
|
}, [sidebarOpen]);
|
||||||
|
const ref = useRef<HTMLElement>(null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppSidebar
|
||||||
|
ref={ref}
|
||||||
|
footer={
|
||||||
|
<StyledNewPageButton
|
||||||
|
data-testid="new-page-button"
|
||||||
|
onClick={onClickNewPage}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t('New Page')}
|
||||||
|
</StyledNewPageButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
|
||||||
|
<WorkspaceSelector
|
||||||
|
currentWorkspace={currentWorkspace}
|
||||||
|
onClick={onOpenWorkspaceListModal}
|
||||||
|
/>
|
||||||
|
<ChangeLog />
|
||||||
|
<StyledListItem
|
||||||
|
data-testid="slider-bar-quick-search-button"
|
||||||
|
onClick={useCallback(() => {
|
||||||
|
onOpenQuickSearchModal();
|
||||||
|
}, [onOpenQuickSearchModal])}
|
||||||
|
>
|
||||||
|
<SearchIcon />
|
||||||
|
{t('Quick search')}
|
||||||
|
</StyledListItem>
|
||||||
|
<StyledListItem
|
||||||
|
active={
|
||||||
|
currentPath ===
|
||||||
|
(currentWorkspaceId && paths.setting(currentWorkspaceId))
|
||||||
|
}
|
||||||
|
data-testid="slider-bar-workspace-setting-button"
|
||||||
|
style={{
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{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))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledLink
|
||||||
|
href={{
|
||||||
|
pathname:
|
||||||
|
currentWorkspaceId && paths.shared(currentWorkspaceId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShareIcon />
|
||||||
|
<span data-testid="shared-pages">{t('Shared Pages')}</span>
|
||||||
|
</StyledLink>
|
||||||
|
</StyledListItem>
|
||||||
|
))}
|
||||||
|
<StyledListItem
|
||||||
|
active={
|
||||||
|
currentPath ===
|
||||||
|
(currentWorkspaceId && paths.trash(currentWorkspaceId))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledLink
|
||||||
|
href={{
|
||||||
|
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteTemporarilyIcon /> {t('Trash')}
|
||||||
|
</StyledLink>
|
||||||
|
</StyledListItem>
|
||||||
|
</StyledSliderBarInnerWrapper>
|
||||||
|
</AppSidebar>
|
||||||
|
<ResizeIndicator targetElement={ref} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useTheme } from '@mui/material';
|
|
||||||
import { useMediaQuery } from '@react-hookz/web';
|
|
||||||
import { atom, useAtom } from 'jotai';
|
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
|
||||||
|
|
||||||
const sideBarOpenAtom = atomWithStorage('sidebarOpen', true);
|
|
||||||
const sideBarWidthAtom = atomWithStorage('sidebarWidth', 256);
|
|
||||||
const sidebarResizingAtom = atom(false);
|
|
||||||
|
|
||||||
export function useSidebarStatus() {
|
|
||||||
return useAtom(sideBarOpenAtom);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSidebarWidth() {
|
|
||||||
return useAtom(sideBarWidthAtom);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSidebarFloating() {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
useMediaQuery(theme.breakpoints.down('md').replace(/^@media( ?)/m, '')) ??
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSidebarResizing() {
|
|
||||||
return useAtom(sidebarResizingAtom);
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ export const StyledPage = styled('div')<{ resizing?: boolean }>(
|
|||||||
transition: 'background-color .5s',
|
transition: 'background-color .5s',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
|
flexDirection: 'row',
|
||||||
'--affine-editor-width': '686px',
|
'--affine-editor-width': '686px',
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
'--affine-editor-width': '550px',
|
'--affine-editor-width': '550px',
|
||||||
@@ -40,17 +41,15 @@ export const StyledWrapper = styled('div')(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MainContainerWrapper = styled('div')<{ resizing: boolean }>(
|
export const MainContainerWrapper = styled('div')(() => {
|
||||||
({ resizing }) => {
|
return {
|
||||||
return {
|
display: 'flex',
|
||||||
display: 'flex',
|
flexGrow: 1,
|
||||||
flexGrow: 1,
|
position: 'relative',
|
||||||
position: 'relative',
|
maxWidth: '100vw',
|
||||||
maxWidth: '100vw',
|
overflow: 'auto',
|
||||||
overflow: 'auto',
|
};
|
||||||
};
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const MainContainer = styled('div')(({ theme }) => {
|
export const MainContainer = styled('div')(({ theme }) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -33,17 +33,11 @@ import {
|
|||||||
} from '../atoms/public-workspace';
|
} from '../atoms/public-workspace';
|
||||||
import { HelpIsland } from '../components/pure/help-island';
|
import { HelpIsland } from '../components/pure/help-island';
|
||||||
import { PageLoading } from '../components/pure/loading';
|
import { PageLoading } from '../components/pure/loading';
|
||||||
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
|
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||||
import { useRouterTitle } from '../hooks/use-router-title';
|
import { useRouterTitle } from '../hooks/use-router-title';
|
||||||
import { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
|
import { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
|
||||||
import {
|
|
||||||
useSidebarFloating,
|
|
||||||
useSidebarResizing,
|
|
||||||
useSidebarStatus,
|
|
||||||
useSidebarWidth,
|
|
||||||
} from '../hooks/use-sidebar-status';
|
|
||||||
import { useSyncRouterWithCurrentPageId } from '../hooks/use-sync-router-with-current-page-id';
|
import { useSyncRouterWithCurrentPageId } from '../hooks/use-sync-router-with-current-page-id';
|
||||||
import { useSyncRouterWithCurrentWorkspaceId } from '../hooks/use-sync-router-with-current-workspace-id';
|
import { useSyncRouterWithCurrentWorkspaceId } from '../hooks/use-sync-router-with-current-workspace-id';
|
||||||
import { useWorkspaces } from '../hooks/use-workspaces';
|
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||||
@@ -55,9 +49,6 @@ import {
|
|||||||
MainContainer,
|
MainContainer,
|
||||||
MainContainerWrapper,
|
MainContainerWrapper,
|
||||||
StyledPage,
|
StyledPage,
|
||||||
StyledSliderResizer,
|
|
||||||
StyledSliderResizerInner,
|
|
||||||
StyledSpacer,
|
|
||||||
StyledToolWrapper,
|
StyledToolWrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
@@ -377,49 +368,14 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
const handleOpenQuickSearchModal = useCallback(() => {
|
const handleOpenQuickSearchModal = useCallback(() => {
|
||||||
setOpenQuickSearchModalAtom(true);
|
setOpenQuickSearchModalAtom(true);
|
||||||
}, [setOpenQuickSearchModalAtom]);
|
}, [setOpenQuickSearchModalAtom]);
|
||||||
const [resizingSidebar, setIsResizing] = useSidebarResizing();
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useSidebarStatus();
|
|
||||||
const sidebarFloating = useSidebarFloating();
|
|
||||||
const [sidebarWidth, setSliderWidth] = useSidebarWidth();
|
|
||||||
const actualSidebarWidth = !sidebarOpen
|
|
||||||
? 0
|
|
||||||
: sidebarFloating
|
|
||||||
? 'calc(10vw + 400px)'
|
|
||||||
: sidebarWidth;
|
|
||||||
const mainWidth =
|
|
||||||
sidebarOpen && !sidebarFloating ? `calc(100% - ${sidebarWidth}px)` : '100%';
|
|
||||||
const [resizing] = useSidebarResizing();
|
|
||||||
|
|
||||||
const onResizeStart = useCallback(() => {
|
|
||||||
let resized = false;
|
|
||||||
function onMouseMove(e: MouseEvent) {
|
|
||||||
const newWidth = Math.min(480, Math.max(e.clientX, 256));
|
|
||||||
setSliderWidth(newWidth);
|
|
||||||
setIsResizing(true);
|
|
||||||
resized = true;
|
|
||||||
}
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener(
|
|
||||||
'mouseup',
|
|
||||||
() => {
|
|
||||||
// if not resized, toggle sidebar
|
|
||||||
if (!resized) {
|
|
||||||
setSidebarOpen(o => !o);
|
|
||||||
}
|
|
||||||
setIsResizing(false);
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}, [setIsResizing, setSidebarOpen, setSliderWidth]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<StyledPage resizing={resizingSidebar}>
|
<StyledPage>
|
||||||
<WorkSpaceSliderBar
|
<RootAppSidebar
|
||||||
isPublicWorkspace={isPublicWorkspace}
|
isPublicWorkspace={isPublicWorkspace}
|
||||||
onOpenQuickSearchModal={handleOpenQuickSearchModal}
|
onOpenQuickSearchModal={handleOpenQuickSearchModal}
|
||||||
currentWorkspace={currentWorkspace}
|
currentWorkspace={currentWorkspace}
|
||||||
@@ -436,23 +392,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
currentPath={router.asPath.split('?')[0]}
|
currentPath={router.asPath.split('?')[0]}
|
||||||
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
|
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
|
||||||
/>
|
/>
|
||||||
<StyledSpacer
|
<MainContainerWrapper>
|
||||||
floating={sidebarFloating}
|
|
||||||
resizing={resizing}
|
|
||||||
sidebarOpen={sidebarOpen}
|
|
||||||
style={{ width: actualSidebarWidth }}
|
|
||||||
>
|
|
||||||
{!sidebarFloating && sidebarOpen && (
|
|
||||||
<StyledSliderResizer
|
|
||||||
data-testid="sliderBar-resizer"
|
|
||||||
isResizing={resizing}
|
|
||||||
onMouseDown={onResizeStart}
|
|
||||||
>
|
|
||||||
<StyledSliderResizerInner isResizing={resizing} />
|
|
||||||
</StyledSliderResizer>
|
|
||||||
)}
|
|
||||||
</StyledSpacer>
|
|
||||||
<MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}>
|
|
||||||
<MainContainer className="main-container">
|
<MainContainer className="main-container">
|
||||||
<Suspense fallback={<PageLoading text={t('Page is Loading')} />}>
|
<Suspense fallback={<PageLoading text={t('Page is Loading')} />}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -33,9 +33,11 @@
|
|||||||
"@mui/base": "5.0.0-alpha.127",
|
"@mui/base": "5.0.0-alpha.127",
|
||||||
"@mui/icons-material": "^5.11.16",
|
"@mui/icons-material": "^5.11.16",
|
||||||
"@mui/material": "^5.12.2",
|
"@mui/material": "^5.12.2",
|
||||||
|
"@popperjs/core": "^2.11.7",
|
||||||
"@radix-ui/react-avatar": "^1.0.2",
|
"@radix-ui/react-avatar": "^1.0.2",
|
||||||
"@toeverything/hooks": "workspace:*",
|
"@toeverything/hooks": "workspace:*",
|
||||||
"@toeverything/theme": "workspace:*",
|
"@toeverything/theme": "workspace:*",
|
||||||
|
"@vanilla-extract/dynamic": "^2.0.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"jotai": "^2.0.4",
|
"jotai": "^2.0.4",
|
||||||
"lit": "^2.7.2",
|
"lit": "^2.7.2",
|
||||||
|
|||||||
96
packages/component/src/components/app-sidebar/index.css.ts
Normal file
96
packages/component/src/components/app-sidebar/index.css.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { baseTheme } from '@toeverything/theme';
|
||||||
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const floatingMaxWidth = 768;
|
||||||
|
export const navWidthVar = createVar('nav-width');
|
||||||
|
|
||||||
|
export const navStyle = style({
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||||
|
width: navWidthVar,
|
||||||
|
minWidth: navWidthVar,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
transition: 'margin-left .3s',
|
||||||
|
zIndex: parseInt(baseTheme.zIndexModal),
|
||||||
|
borderRight: '1px solid var(--affine-border-color)',
|
||||||
|
'@media': {
|
||||||
|
[`(max-width: ${floatingMaxWidth}px)`]: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: `calc(10vw + ${navWidthVar})`,
|
||||||
|
selectors: {
|
||||||
|
'&[data-open="false"]': {
|
||||||
|
marginLeft: `calc((10vw + ${navWidthVar}) * -1)`,
|
||||||
|
},
|
||||||
|
'&[data-is-macos-electron="true"]': {
|
||||||
|
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
'&[data-open="false"]': {
|
||||||
|
marginLeft: `calc(${navWidthVar} * -1)`,
|
||||||
|
},
|
||||||
|
'&[data-is-macos-electron="true"]': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vars: {
|
||||||
|
[navWidthVar]: '256px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const navHeaderStyle = style({
|
||||||
|
flex: '0 0 auto',
|
||||||
|
height: '52px',
|
||||||
|
padding: '0px 16px 0px 10px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
selectors: {
|
||||||
|
'&[data-is-macos-electron="true"]': {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const navBodyStyle = style({
|
||||||
|
flex: '1 1 auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const navFooterStyle = style({
|
||||||
|
flex: '0 0 auto',
|
||||||
|
borderTop: '1px solid var(--affine-border-color)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sidebarButtonStyle = style({
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
color: 'var(--affine-icon-color)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sidebarFloatMaskStyle = style({
|
||||||
|
transition: 'opacity .15s',
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: '100%',
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: parseInt(baseTheme.zIndexModal) - 1,
|
||||||
|
background: 'var(--affine-background-modal-color)',
|
||||||
|
'@media': {
|
||||||
|
[`(max-width: ${floatingMaxWidth}px)`]: {
|
||||||
|
selectors: {
|
||||||
|
'&[data-open="true"]': {
|
||||||
|
opacity: 1,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
right: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
|
export const appSidebarOpenAtom = atomWithStorage('app-sidebar-open', true);
|
||||||
|
export const appSidebarWidthAtom = atomWithStorage(
|
||||||
|
'app-sidebar-width',
|
||||||
|
256 /* px */
|
||||||
|
);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { IconButton } from '@affine/component';
|
||||||
|
import { SidebarIcon } from '@blocksuite/icons';
|
||||||
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
import { AppSidebar, appSidebarOpenAtom, ResizeIndicator } from '.';
|
||||||
|
import { navHeaderStyle, sidebarButtonStyle } from './index.css';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/AppSidebar',
|
||||||
|
component: AppSidebar,
|
||||||
|
} satisfies Meta;
|
||||||
|
|
||||||
|
const Footer = () => <div>Add Page</div>;
|
||||||
|
|
||||||
|
export const Default: StoryFn = props => {
|
||||||
|
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||||
|
const ref = useRef<HTMLElement>(null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100vw',
|
||||||
|
height: '600px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppSidebar footer={<Footer />} ref={ref}>
|
||||||
|
Test
|
||||||
|
</AppSidebar>
|
||||||
|
<ResizeIndicator targetElement={ref} />
|
||||||
|
<div>
|
||||||
|
<div className={navHeaderStyle}>
|
||||||
|
{!open && (
|
||||||
|
<IconButton
|
||||||
|
className={sidebarButtonStyle}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SidebarIcon width={24} height={24} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
108
packages/component/src/components/app-sidebar/index.tsx
Normal file
108
packages/component/src/components/app-sidebar/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { getEnvironment } from '@affine/env';
|
||||||
|
import {
|
||||||
|
ArrowLeftSmallIcon,
|
||||||
|
ArrowRightSmallIcon,
|
||||||
|
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, useImperativeHandle, useRef } from 'react';
|
||||||
|
|
||||||
|
import { IconButton } from '../../ui/button/IconButton';
|
||||||
|
import {
|
||||||
|
navBodyStyle,
|
||||||
|
navFooterStyle,
|
||||||
|
navHeaderStyle,
|
||||||
|
navStyle,
|
||||||
|
navWidthVar,
|
||||||
|
sidebarButtonStyle,
|
||||||
|
sidebarFloatMaskStyle,
|
||||||
|
} from './index.css';
|
||||||
|
import { appSidebarOpenAtom, appSidebarWidthAtom } from './index.jotai';
|
||||||
|
|
||||||
|
export { appSidebarOpenAtom };
|
||||||
|
|
||||||
|
export type AppSidebarProps = PropsWithChildren<{
|
||||||
|
footer?: ReactNode | undefined;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const AppSidebar = forwardRef<HTMLElement, AppSidebarProps>(
|
||||||
|
function AppSidebar(props, forwardedRef): ReactElement {
|
||||||
|
const ref = useRef<HTMLElement>(null);
|
||||||
|
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||||
|
|
||||||
|
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
|
||||||
|
|
||||||
|
const handleSidebarOpen = useCallback(() => {
|
||||||
|
setOpen(open => !open);
|
||||||
|
}, [setOpen]);
|
||||||
|
|
||||||
|
useImperativeHandle(forwardedRef, () => ref.current as HTMLElement);
|
||||||
|
|
||||||
|
const environment = getEnvironment();
|
||||||
|
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav
|
||||||
|
className={navStyle}
|
||||||
|
ref={ref}
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<div className={navFooterStyle}>{props.footer}</div>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
data-testid="app-sidebar-float-mask"
|
||||||
|
data-open={open}
|
||||||
|
className={sidebarFloatMaskStyle}
|
||||||
|
onClick={useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, [setOpen])}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type { ResizeIndicatorProps } from './resize-indicator';
|
||||||
|
export { ResizeIndicator } from './resize-indicator';
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
import { navWidthVar } from '../index.css';
|
||||||
|
|
||||||
|
export const spacerStyle = style({
|
||||||
|
position: 'absolute',
|
||||||
|
width: '1px',
|
||||||
|
left: navWidthVar,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 'calc(var(--affine-z-index-modal) - 1)',
|
||||||
|
backgroundColor: 'var(--affine-border-color)',
|
||||||
|
opacity: 0,
|
||||||
|
cursor: 'col-resize',
|
||||||
|
'@media': {
|
||||||
|
'(max-width: 600px)': {
|
||||||
|
// do not allow resizing on mobile
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transition: 'opacity 0.15s ease 0.1s',
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
'&[data-resizing="true"]': {
|
||||||
|
transition: 'width .3s, min-width .3s, max-width .3s',
|
||||||
|
},
|
||||||
|
'&[data-open="false"]': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'&[data-open="open"]': {
|
||||||
|
display: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { Instance } from '@popperjs/core';
|
||||||
|
import { createPopper } from '@popperjs/core';
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import type { ReactElement, RefObject } from 'react';
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useDeferredValue,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { appSidebarOpenAtom, appSidebarWidthAtom } from '../index.jotai';
|
||||||
|
import { spacerStyle } from './index.css';
|
||||||
|
|
||||||
|
export type ResizeIndicatorProps = {
|
||||||
|
targetElement: RefObject<HTMLElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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.current) {
|
||||||
|
popperRef.current = createPopper(
|
||||||
|
props.targetElement.current,
|
||||||
|
ref.current,
|
||||||
|
{
|
||||||
|
placement: 'right',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.targetElement]);
|
||||||
|
|
||||||
|
const sidebarWidth = useDeferredValue(useAtomValue(appSidebarWidthAtom));
|
||||||
|
useEffect(() => {
|
||||||
|
if (popperRef.current) {
|
||||||
|
popperRef.current.update();
|
||||||
|
}
|
||||||
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
|
const onResizeStart = useCallback(() => {
|
||||||
|
let resized = false;
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newWidth = Math.min(480, Math.max(e.clientX, 256));
|
||||||
|
setWidth(newWidth);
|
||||||
|
setIsResizing(true);
|
||||||
|
resized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener(
|
||||||
|
'mouseup',
|
||||||
|
() => {
|
||||||
|
// if not resized, toggle sidebar
|
||||||
|
if (!resized) {
|
||||||
|
setSidebarOpen(o => !o);
|
||||||
|
}
|
||||||
|
if (popperRef.current) {
|
||||||
|
popperRef.current.update();
|
||||||
|
}
|
||||||
|
setIsResizing(false);
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}, [setSidebarOpen, setWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={spacerStyle}
|
||||||
|
data-testid="app-sidebar-resizer"
|
||||||
|
data-resizing={isResizing}
|
||||||
|
data-open={sidebarOpen}
|
||||||
|
onMouseDown={onResizeStart}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -66,16 +66,18 @@ export const changeLogWrapperSlideOutStyle = style({
|
|||||||
animation: `${slideOut} .3s ease-in-out forwards`,
|
animation: `${slideOut} .3s ease-in-out forwards`,
|
||||||
});
|
});
|
||||||
export const changeLogSlideInStyle = style({
|
export const changeLogSlideInStyle = style({
|
||||||
width: '110%',
|
// fixme: if width is 100% and marginLeft is 0,
|
||||||
|
// the UI will overflow on app sidebar
|
||||||
|
width: '99%',
|
||||||
|
marginLeft: '2px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
color: 'var(--affine-primary-color)',
|
color: 'var(--affine-primary-color)',
|
||||||
backgroundColor: 'var(--affine-tertiary-color)',
|
backgroundColor: 'var(--affine-tertiary-color)',
|
||||||
border: '1px solid var(--affine-primary-color)',
|
border: '1px solid var(--affine-primary-color)',
|
||||||
borderRight: 'none',
|
borderRight: 'none',
|
||||||
marginLeft: '8px',
|
|
||||||
paddingLeft: '8px',
|
paddingLeft: '8px',
|
||||||
borderRadius: '16px 0 0 16px',
|
borderRadius: '16px 0 0 16px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -89,7 +91,6 @@ export const changeLogSlideOutStyle = style({
|
|||||||
animation: `${slideOut2} .3s ease-in-out forwards`,
|
animation: `${slideOut2} .3s ease-in-out forwards`,
|
||||||
});
|
});
|
||||||
export const linkStyle = style({
|
export const linkStyle = style({
|
||||||
flexGrow: 1,
|
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
color: 'var(--affine-text-emphasis-color)',
|
color: 'var(--affine-text-emphasis-color)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -103,6 +104,6 @@ export const iconStyle = style({
|
|||||||
});
|
});
|
||||||
export const iconButtonStyle = style({
|
export const iconButtonStyle = style({
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
marginRight: '12%',
|
marginRight: '0',
|
||||||
color: 'var(--affine-icon-color)',
|
color: 'var(--affine-icon-color)',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const StyledIconButton = styled('button', {
|
|||||||
fontSize?: CSSProperties['fontSize'];
|
fontSize?: CSSProperties['fontSize'];
|
||||||
}>(
|
}>(
|
||||||
({
|
({
|
||||||
theme,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import { waitMarkdownImported } from '../libs/page-logic';
|
|||||||
test('Collapse Sidebar', async ({ page }) => {
|
test('Collapse Sidebar', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-root');
|
const sliderBarArea = page.getByTestId('app-sidebar');
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Expand Sidebar', async ({ page }) => {
|
test('Expand Sidebar', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ test('Click resizer can close sidebar', async ({ page }) => {
|
|||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).toBeVisible();
|
await expect(sliderBarArea).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId('sliderBar-resizer').click();
|
await page.getByTestId('app-sidebar-resizer').click();
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,14 +39,14 @@ test('Drag resizer can resize sidebar', async ({ page }) => {
|
|||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).toBeVisible();
|
await expect(sliderBarArea).toBeVisible();
|
||||||
|
|
||||||
const sliderResizer = page.getByTestId('sliderBar-resizer');
|
const sliderResizer = page.getByTestId('app-sidebar-resizer');
|
||||||
await sliderResizer.hover();
|
await sliderResizer.hover();
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(400, 300, {
|
await page.mouse.move(400, 300, {
|
||||||
steps: 10,
|
steps: 10,
|
||||||
});
|
});
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
const boundingBox = await page.getByTestId('sliderBar-root').boundingBox();
|
const boundingBox = await page.getByTestId('app-sidebar').boundingBox();
|
||||||
expect(boundingBox?.width).toBe(400);
|
expect(boundingBox?.width).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,9 +54,7 @@ test('Sidebar in between sm & md breakpoint', async ({ page }) => {
|
|||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
const sliderBarModalBackground = page.getByTestId(
|
const sliderBarModalBackground = page.getByTestId('app-sidebar-float-mask');
|
||||||
'sliderBar-modalBackground'
|
|
||||||
);
|
|
||||||
await expect(sliderBarArea).toBeInViewport();
|
await expect(sliderBarArea).toBeInViewport();
|
||||||
await expect(sliderBarModalBackground).not.toBeVisible();
|
await expect(sliderBarModalBackground).not.toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ test('When opening the website for the first time, the first folding sidebar wil
|
|||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
|
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
|
||||||
await expect(quickSearchTips).not.toBeVisible();
|
await expect(quickSearchTips).not.toBeVisible();
|
||||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.getByTestId('sliderBar-arrowButton-expand').click();
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
await expect(quickSearchTips).toBeVisible();
|
await expect(quickSearchTips).toBeVisible();
|
||||||
@@ -192,15 +194,17 @@ test('After appearing once, it will not appear a second time', async ({
|
|||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
|
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
|
||||||
await expect(quickSearchTips).not.toBeVisible();
|
await expect(quickSearchTips).not.toBeVisible();
|
||||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.getByTestId('sliderBar-arrowButton-expand').click();
|
||||||
const sliderBarArea = page.getByTestId('sliderBar');
|
const sliderBarArea = page.getByTestId('sliderBar');
|
||||||
await expect(sliderBarArea).not.toBeVisible();
|
await expect(sliderBarArea).not.toBeVisible();
|
||||||
await expect(quickSearchTips).toBeVisible();
|
await expect(quickSearchTips).toBeVisible();
|
||||||
await page.locator('[data-testid=quick-search-got-it]').click();
|
await page.locator('[data-testid=quick-search-got-it]').click();
|
||||||
await expect(quickSearchTips).not.toBeVisible();
|
await expect(quickSearchTips).not.toBeVisible();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.locator('[data-testid=sliderBar-arrowButton-expand]').click();
|
await page.waitForSelector('v-line');
|
||||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
|
||||||
await expect(quickSearchTips).not.toBeVisible();
|
await expect(quickSearchTips).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { waitMarkdownImported } from '../libs/page-logic';
|
|||||||
test('Create subpage', async ({ page }) => {
|
test('Create subpage', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|||||||
11
yarn.lock
11
yarn.lock
@@ -64,6 +64,7 @@ __metadata:
|
|||||||
"@mui/base": 5.0.0-alpha.127
|
"@mui/base": 5.0.0-alpha.127
|
||||||
"@mui/icons-material": ^5.11.16
|
"@mui/icons-material": ^5.11.16
|
||||||
"@mui/material": ^5.12.2
|
"@mui/material": ^5.12.2
|
||||||
|
"@popperjs/core": ^2.11.7
|
||||||
"@radix-ui/react-avatar": ^1.0.2
|
"@radix-ui/react-avatar": ^1.0.2
|
||||||
"@storybook/addon-actions": ^7.0.7
|
"@storybook/addon-actions": ^7.0.7
|
||||||
"@storybook/addon-coverage": ^0.0.8
|
"@storybook/addon-coverage": ^0.0.8
|
||||||
@@ -84,6 +85,7 @@ __metadata:
|
|||||||
"@types/react-dnd": ^3.0.2
|
"@types/react-dnd": ^3.0.2
|
||||||
"@types/react-dom": 18.0.11
|
"@types/react-dom": 18.0.11
|
||||||
"@vanilla-extract/css": ^1.11.0
|
"@vanilla-extract/css": ^1.11.0
|
||||||
|
"@vanilla-extract/dynamic": ^2.0.3
|
||||||
"@vitejs/plugin-react": ^4.0.0
|
"@vitejs/plugin-react": ^4.0.0
|
||||||
clsx: ^1.2.1
|
clsx: ^1.2.1
|
||||||
concurrently: ^8.0.1
|
concurrently: ^8.0.1
|
||||||
@@ -9259,6 +9261,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@vanilla-extract/dynamic@npm:^2.0.3":
|
||||||
|
version: 2.0.3
|
||||||
|
resolution: "@vanilla-extract/dynamic@npm:2.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@vanilla-extract/private": ^1.0.3
|
||||||
|
checksum: 9ad4068d7e28361a7aca46b5f14094e74613428fb600e54227d8ba7a35926c0d7339de1876eb2b3563f6d97c0f08fa09d5ff597f76776f8edca37510165682b0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@vanilla-extract/integration@npm:^6.0.0, @vanilla-extract/integration@npm:^6.0.2":
|
"@vanilla-extract/integration@npm:^6.0.0, @vanilla-extract/integration@npm:^6.0.2":
|
||||||
version: 6.2.1
|
version: 6.2.1
|
||||||
resolution: "@vanilla-extract/integration@npm:6.2.1"
|
resolution: "@vanilla-extract/integration@npm:6.2.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user