Feat/sidebar&top bar (#1454)

This commit is contained in:
Qi
2023-03-09 17:08:23 +08:00
committed by GitHub
parent 31d2e522eb
commit 921061eeb6
22 changed files with 259 additions and 541 deletions

View File

@@ -0,0 +1,26 @@
export const SidebarSwitchIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 5.00009V19.0001M6 7.62509H8M6 10.2501H8M6 12.8751H8M6.2 19.0001H17.8C18.9201 19.0001 19.4802 19.0001 19.908 18.8094C20.2843 18.6416 20.5903 18.3739 20.782 18.0446C21 17.6702 21 17.1802 21 16.2001V7.80009C21 6.82 21 6.32995 20.782 5.95561C20.5903 5.62632 20.2843 5.35861 19.908 5.19083C19.4802 5.00009 18.9201 5.00009 17.8 5.00009H6.2C5.0799 5.00009 4.51984 5.00009 4.09202 5.19083C3.71569 5.35861 3.40973 5.62632 3.21799 5.95561C3 6.32995 3 6.82 3 7.80009V16.2001C3 17.1802 3 17.6702 3.21799 18.0446C3.40973 18.3739 3.71569 18.6416 4.09202 18.8094C4.51984 19.0001 5.07989 19.0001 6.2 19.0001Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11 5.00009V19.0001M6 7.62509H8M6 10.2501H8M6 12.8751H8M6.2 19.0001H17.8C18.9201 19.0001 19.4802 19.0001 19.908 18.8094C20.2843 18.6416 20.5903 18.3739 20.782 18.0446C21 17.6702 21 17.1802 21 16.2001V7.80009C21 6.82 21 6.32995 20.782 5.95561C20.5903 5.62632 20.2843 5.35861 19.908 5.19083C19.4802 5.00009 18.9201 5.00009 17.8 5.00009H6.2C5.0799 5.00009 4.51984 5.00009 4.09202 5.19083C3.71569 5.35861 3.40973 5.62632 3.21799 5.95561C3 6.32995 3 6.82 3 7.80009V16.2001C3 17.1802 3 17.6702 3.21799 18.0446C3.40973 18.3739 3.71569 18.6416 4.09202 18.8094C4.51984 19.0001 5.07989 19.0001 6.2 19.0001Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

View File

@@ -0,0 +1,49 @@
import { Tooltip } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import React, { useCallback, useState } from 'react';
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
import { SidebarSwitchIcon } from './icons';
import { StyledSidebarSwitch } from './style';
type SidebarSwitchProps = {
visible?: boolean;
tooltipContent?: string;
testid?: string;
};
export const SidebarSwitch = ({
visible = true,
tooltipContent,
testid = '',
}: SidebarSwitchProps) => {
const [open, setOpen] = useSidebarStatus();
const [tooltipVisible, setTooltipVisible] = useState(false);
const { t } = useTranslation();
tooltipContent =
tooltipContent || (open ? t('Collapse sidebar') : t('Expand sidebar'));
return (
<Tooltip
content={tooltipContent}
placement="right"
zIndex={1000}
visible={tooltipVisible}
>
<StyledSidebarSwitch
visible={visible}
data-testid={testid}
onClick={useCallback(() => {
setOpen(!open);
setTooltipVisible(false);
}, [open, setOpen])}
onMouseEnter={useCallback(() => {
setTooltipVisible(true);
}, [])}
onMouseLeave={useCallback(() => {
setTooltipVisible(false);
}, [])}
>
<SidebarSwitchIcon />
</StyledSidebarSwitch>
</Tooltip>
);
};

View File

@@ -0,0 +1,23 @@
import { styled } from '@affine/component';
export const StyledSidebarSwitch = styled('button')<{ visible: boolean }>(
({ theme, visible }) => {
return {
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
color: theme.colors.innerHoverBackground,
width: '32px',
height: '32px',
borderRadius: '8px',
opacity: visible ? 1 : 0,
transition: 'all 0.2s ease-in-out',
...(visible ? {} : { cursor: 'not-allowed', pointerEvents: 'none' }),
':hover': {
background: '#F1F1F4',
color: theme.colors.iconColor,
},
};
}
);

View File

@@ -1,85 +0,0 @@
import { CSSProperties, DOMAttributes } from 'react';
type IconProps = {
style?: CSSProperties;
} & DOMAttributes<SVGElement>;
export const ArrowIcon = ({
style: propsStyle = {},
direction = 'right',
...props
}: IconProps & { direction?: 'left' | 'right' | 'middle' }) => {
const style = {
transform: `rotate(${direction === 'left' ? '0' : '180deg'})`,
opacity: direction === 'middle' ? 0 : 1,
...propsStyle,
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="6"
height="16"
viewBox="0 0 6 16"
fill="currentColor"
{...props}
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.602933 0.305738C0.986547 0.0865297 1.47523 0.219807 1.69444 0.603421L5.41093 7.10728C5.72715 7.66066 5.72715 8.34 5.41093 8.89338L1.69444 15.3972C1.47523 15.7809 0.986547 15.9141 0.602933 15.6949C0.219319 15.4757 0.0860414 14.987 0.305249 14.6034L4.02174 8.09956C4.05688 8.03807 4.05688 7.96259 4.02174 7.9011L0.305249 1.39724C0.0860414 1.01363 0.219319 0.524946 0.602933 0.305738Z"
/>
</svg>
);
};
export const PaperIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
d="M17 9.8H7V8.2h10v1.6ZM12 12.8H7v-1.6h5v1.6Z"
clipRule="evenodd"
/>
<path d="m14 19 7-7h-5a2 2 0 0 0-2 2v5Z" />
<path
fillRule="evenodd"
d="M5 6.6h14c.22 0 .4.18.4.4v6.6L21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h9l1.6-1.6H5a.4.4 0 0 1-.4-.4V7c0-.22.18-.4.4-.4Z"
clipRule="evenodd"
/>
</svg>
);
};
export const EdgelessIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
d="M12 17.4a5.4 5.4 0 1 0 0-10.8 5.4 5.4 0 0 0 0 10.8Zm7-5.4a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"
clipRule="evenodd"
/>
<path
fillRule="evenodd"
d="M18.565 8a.8.8 0 0 1 .8-.8c.797 0 1.511.07 2.07.24.5.15 1.172.477 1.334 1.202v.004c.089.405-.026.776-.186 1.065a3.165 3.165 0 0 1-.652.782c-.52.471-1.265.947-2.15 1.407-1.783.927-4.28 1.869-7.077 2.62-2.796.752-5.409 1.184-7.381 1.266-.98.04-1.848-.003-2.516-.162-.333-.079-.662-.196-.937-.38-.282-.19-.547-.48-.639-.892v-.002c-.138-.63.202-1.173.518-1.532.343-.39.836-.768 1.413-1.129a.8.8 0 0 1 .848 1.357c-.515.322-.862.605-1.06.83a1.524 1.524 0 0 0-.078.096c.07.03.169.064.304.095.461.11 1.163.158 2.08.12 1.822-.075 4.314-.481 7.033-1.212 2.718-.73 5.1-1.635 6.753-2.494.832-.433 1.441-.835 1.814-1.173.127-.115.213-.21.268-.284a1.67 1.67 0 0 0-.153-.053c-.342-.104-.878-.171-1.606-.171a.8.8 0 0 1-.8-.8Zm2.692 1.097-.004-.004a.026.026 0 0 1 .004.004Zm-18.46 5 .001-.002v.002Z"
clipRule="evenodd"
/>
</svg>
);
};

View File

@@ -1,80 +1,26 @@
import { useTranslation } from '@affine/i18n'; import { EdgelessIcon, PaperIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { useTheme } from '@mui/material'; import { CSSProperties } from 'react';
import React, { cloneElement, CSSProperties, useEffect, useState } from 'react';
import { import {
usePageMeta, usePageMeta,
usePageMetaHelper, usePageMetaHelper,
} from '../../../../hooks/use-page-meta'; } from '../../../../hooks/use-page-meta';
// todo(himself65): remove `useTheme` hook
import { BlockSuiteWorkspace } from '../../../../shared'; import { BlockSuiteWorkspace } from '../../../../shared';
import { EdgelessIcon, PaperIcon } from './Icons'; import { StyledEditorModeSwitch, StyledSwitchItem } from './style';
import {
StyledAnimateRadioContainer,
StyledIcon,
StyledLabel,
StyledMiddleLine,
StyledRadioItem,
} from './style';
import type { AnimateRadioItemProps, RadioItemStatus } from './type';
const PaperItem = ({ active }: { active?: boolean }) => {
const {
colors: { iconColor, primaryColor },
} = useTheme();
return <PaperIcon style={{ color: active ? primaryColor : iconColor }} />;
};
const EdgelessItem = ({ active }: { active?: boolean }) => {
const {
colors: { iconColor, primaryColor },
} = useTheme();
return <EdgelessIcon style={{ color: active ? primaryColor : iconColor }} />;
};
const AnimateRadioItem = ({
active,
status,
icon: propsIcon,
label,
isLeft,
...props
}: AnimateRadioItemProps) => {
const icon = (
<StyledIcon shrink={status === 'shrink'} isLeft={isLeft}>
{cloneElement(propsIcon, {
active,
})}
</StyledIcon>
);
return (
<StyledRadioItem title={label} active={active} status={status} {...props}>
{isLeft ? icon : null}
<StyledLabel shrink={status !== 'stretch'} isLeft={isLeft}>
{label}
</StyledLabel>
{isLeft ? null : icon}
</StyledRadioItem>
);
};
export type EditorModeSwitchProps = { export type EditorModeSwitchProps = {
// todo(himself65): combine these two properties // todo(himself65): combine these two properties
blockSuiteWorkspace: BlockSuiteWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
pageId: string; pageId: string;
isHover: boolean; style?: CSSProperties;
style: CSSProperties;
}; };
export const EditorModeSwitch: React.FC<EditorModeSwitchProps> = ({ export const EditorModeSwitch = ({
isHover, style,
style = {},
blockSuiteWorkspace, blockSuiteWorkspace,
pageId, pageId,
}) => { }: EditorModeSwitchProps) => {
const theme = useTheme();
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const pageMeta = usePageMeta(blockSuiteWorkspace).find( const pageMeta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId meta => meta.id === pageId
@@ -82,85 +28,32 @@ export const EditorModeSwitch: React.FC<EditorModeSwitchProps> = ({
assertExists(pageMeta); assertExists(pageMeta);
const { trash, mode = 'page' } = pageMeta; const { trash, mode = 'page' } = pageMeta;
const modifyRadioItemStatus = (): RadioItemStatus => {
return {
left: isHover
? mode === 'page'
? 'stretch'
: 'normal'
: mode === 'page'
? 'shrink'
: 'hidden',
right: isHover
? mode === 'edgeless'
? 'stretch'
: 'normal'
: mode === 'edgeless'
? 'shrink'
: 'hidden',
};
};
const [radioItemStatus, setRadioItemStatus] = useState<RadioItemStatus>(
modifyRadioItemStatus
);
useEffect(() => {
setRadioItemStatus(modifyRadioItemStatus());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isHover, mode]);
const { t } = useTranslation();
return ( return (
<StyledAnimateRadioContainer <StyledEditorModeSwitch
data-testid="editor-mode-switcher"
shrink={!isHover}
style={style} style={style}
disabled={!!trash} switchLeft={mode === 'page'}
showAlone={trash}
> >
<AnimateRadioItem <StyledSwitchItem
isLeft={true} data-testid="switch-page-mode-button"
label={t('Page')}
icon={<PaperItem />}
active={mode === 'page'} active={mode === 'page'}
status={radioItemStatus.left} hide={trash && mode !== 'page'}
onClick={() => { onClick={() => {
setPageMeta(pageId, { mode: 'page' }); setPageMeta(pageId, { mode: 'page' });
}} }}
onMouseEnter={() => { >
setRadioItemStatus({ <PaperIcon />
right: 'normal', </StyledSwitchItem>
left: 'stretch', <StyledSwitchItem
}); data-testid="switch-edgeless-mode-button"
}}
onMouseLeave={() => {
setRadioItemStatus(modifyRadioItemStatus());
}}
/>
<StyledMiddleLine
hidden={!isHover}
dark={theme.palette.mode === 'dark'}
/>
<AnimateRadioItem
isLeft={false}
label={t('Edgeless')}
data-testid="switch-edgeless-item"
icon={<EdgelessItem />}
active={mode === 'edgeless'} active={mode === 'edgeless'}
status={radioItemStatus.right} hide={trash && mode !== 'edgeless'}
onClick={() => { onClick={() => {
setPageMeta(pageId, { mode: 'edgeless' }); setPageMeta(pageId, { mode: 'edgeless' });
}} }}
onMouseEnter={() => { >
setRadioItemStatus({ <EdgelessIcon />
left: 'normal', </StyledSwitchItem>
right: 'stretch', </StyledEditorModeSwitch>
});
}}
onMouseLeave={() => {
setRadioItemStatus(modifyRadioItemStatus());
}}
/>
</StyledAnimateRadioContainer>
); );
}; };
export default EditorModeSwitch;

View File

@@ -1,179 +1,58 @@
import { css, displayFlex, keyframes, styled } from '@affine/component'; import { displayFlex, styled } from '@affine/component';
// @ts-ignore
import spring, { toString } from 'css-spring';
// @ts-ignore
import type { ItemStatus } from './type';
const ANIMATE_DURATION = 500;
export const StyledAnimateRadioContainer = styled('div')<{
shrink: boolean;
disabled: boolean;
}>(({ shrink, theme, disabled }) => {
const animateScaleStretch = toString(
spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' })
);
const animateScaleShrink = toString(
spring({ width: '160px' }, { width: '36px' }, { preset: 'gentle' })
);
const shrinkStyle: any = shrink
? {
animation: css`
${keyframes`${animateScaleShrink}`} ${ANIMATE_DURATION}ms forwards
`,
background: 'transparent',
}
: {
animation: css`
${keyframes`${animateScaleStretch}`} ${ANIMATE_DURATION}ms forwards
`,
};
return css`
height: 36px;
border-radius: 18px;
background: ${disabled ? 'transparent' : theme.colors.hoverBackground}
position: relative;
display: flex;
transition: background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms;
border: 1px solid transparent;
${
disabled
? css`
pointer-events: none;
`
: css`
animation: ${shrinkStyle.animation};
background: ${shrinkStyle.background};
`
}
//...(disabled ? { pointerEvents: 'none' } : shrinkStyle),
:hover {
border: ${disabled ? '' : `1px solid ${theme.colors.primaryColor}`}
}
`;
});
export const StyledMiddleLine = styled('div')<{
hidden: boolean;
dark: boolean;
}>(({ hidden, dark }) => {
return {
width: '1px',
height: '16px',
background: dark ? '#4d4c53' : '#D0D7E3',
top: '0',
bottom: '0',
margin: 'auto',
opacity: hidden ? '0' : '1',
};
});
export const StyledRadioItem = styled('div')<{
status: ItemStatus;
active: boolean;
}>(({ status, active, theme }) => {
const animateScaleStretch = toString(
spring({ width: '44px' }, { width: '112px' })
);
const animateScaleOrigin = toString(
spring({ width: '112px' }, { width: '44px' })
);
const animateScaleShrink = toString(
spring({ width: '0px' }, { width: '36px' })
);
const dynamicStyle =
status === 'stretch'
? {
animation: css`
${keyframes`${animateScaleStretch}`} ${ANIMATE_DURATION}ms forwards
`,
flexShrink: '0',
}
: status === 'shrink'
? {
animation: css`
${keyframes`${animateScaleShrink}`} ${ANIMATE_DURATION}ms forwards
`,
}
: status === 'normal'
? {
animation: css`
${keyframes`${animateScaleOrigin}`} ${ANIMATE_DURATION}ms forwards
`,
}
: {};
export const StyledEditorModeSwitch = styled('div')<{
switchLeft: boolean;
showAlone?: boolean;
}>(({ theme, switchLeft, showAlone }) => {
const { const {
colors: { iconColor, primaryColor }, palette: { mode },
} = theme; } = theme;
return css`
width: 0;
height: 100%;
display: flex;
cursor: pointer;
overflow: hidden;
color: ${active ? primaryColor : iconColor};
animation: ${dynamicStyle.animation};
flex-shrink: ${dynamicStyle.flexShrink};
`;
});
export const StyledLabel = styled('div')<{
shrink: boolean;
isLeft: boolean;
}>(({ shrink, isLeft }) => {
const animateScaleStretch = toString(
spring(
{ width: '0px' },
{ width: isLeft ? '65px' : '75px' },
{ preset: 'gentle' }
)
);
const animateScaleShrink = toString(
spring(
{ width: isLeft ? '65px' : '75px' },
{ width: '0px' },
{ preset: 'gentle' }
)
);
const shrinkStyle = shrink
? {
animation: css`
${keyframes`${animateScaleShrink}`} ${ANIMATE_DURATION}ms forwards
`,
}
: {
animation: css`
${keyframes`${animateScaleStretch}`} ${ANIMATE_DURATION}ms forwards
`,
};
return css`
display: flex;
align-items: center;
justify-content: ${isLeft ? 'flex-start' : 'flex-end'};
font-size: 16px;
flex-shrink: 0;
transition: transform ${ANIMATE_DURATION}ms;
font-weight: normal;
overflow: hidden;
white-space: nowrap;
animation: ${shrinkStyle.animation};
`;
});
export const StyledIcon = styled('div')<{
shrink: boolean;
isLeft: boolean;
}>(({ shrink, isLeft }) => {
const dynamicStyle = shrink
? { width: '36px' }
: { width: isLeft ? '44px' : '34px' };
return { return {
...displayFlex('center', 'center'), width: showAlone ? '40px' : '78px',
flexShrink: '0', height: '32px',
...dynamicStyle, background: showAlone
? 'transparent'
: mode === 'dark'
? '#242424'
: '#F9F9FB',
borderRadius: '12px',
...displayFlex('space-between', 'center'),
padding: '0 8px',
position: 'relative',
'::after': {
content: '""',
display: showAlone ? 'none' : 'block',
width: '24px',
height: '24px',
background: theme.colors.pageBackground,
boxShadow:
mode === 'dark'
? '0px 0px 6px rgba(22, 22, 22, 0.6)'
: '0px 0px 6px #E2E2E2',
borderRadius: '8px',
zIndex: 1,
position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '38px'})`,
transition: 'all .15s',
},
};
});
export const StyledSwitchItem = styled('button')<{
active: boolean;
hide?: boolean;
}>(({ theme, active, hide = false }) => {
return {
width: '24px',
height: '24px',
borderRadius: '8px',
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
display: hide ? 'none' : 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
zIndex: 2,
fontSize: '20px',
}; };
}); });

View File

@@ -1,15 +0,0 @@
import { DOMAttributes, ReactElement } from 'react';
export type ItemStatus = 'normal' | 'stretch' | 'shrink' | 'hidden';
export type RadioItemStatus = {
left: ItemStatus;
right: ItemStatus;
};
export type AnimateRadioItemProps = {
active: boolean;
status: ItemStatus;
label: string;
icon: ReactElement;
isLeft: boolean;
} & DOMAttributes<HTMLDivElement>;

View File

@@ -12,6 +12,7 @@ import {
FavoriteIcon, FavoriteIcon,
MoreVerticalIcon, MoreVerticalIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { EdgelessIcon, PaperIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
@@ -22,7 +23,6 @@ import {
usePageMeta, usePageMeta,
usePageMetaHelper, usePageMetaHelper,
} from '../../../../hooks/use-page-meta'; } from '../../../../hooks/use-page-meta';
import { EdgelessIcon, PaperIcon } from '../editor-mode-switch/Icons';
export const EditorOptionMenu = () => { export const EditorOptionMenu = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -52,6 +52,7 @@ export const EditorOptionMenu = () => {
favorite ? t('Removed from Favorites') : t('Added to Favorites') favorite ? t('Removed from Favorites') : t('Added to Favorites')
); );
}} }}
iconSize={[20, 20]}
icon={ icon={
favorite ? ( favorite ? (
<FavoritedIcon style={{ color: theme.colors.primaryColor }} /> <FavoritedIcon style={{ color: theme.colors.primaryColor }} />
@@ -64,6 +65,7 @@ export const EditorOptionMenu = () => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />} icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
iconSize={[20, 20]}
data-testid="editor-option-menu-edgeless" data-testid="editor-option-menu-edgeless"
onClick={() => { onClick={() => {
setPageMeta(pageId, { setPageMeta(pageId, {
@@ -84,6 +86,7 @@ export const EditorOptionMenu = () => {
globalThis.editor.contentParser.onExportHtml(); globalThis.editor.contentParser.onExportHtml();
}} }}
icon={<ExportToHtmlIcon />} icon={<ExportToHtmlIcon />}
iconSize={[20, 20]}
> >
{t('Export to HTML')} {t('Export to HTML')}
</MenuItem> </MenuItem>
@@ -93,13 +96,14 @@ export const EditorOptionMenu = () => {
globalThis.editor.contentParser.onExportMarkdown(); globalThis.editor.contentParser.onExportMarkdown();
}} }}
icon={<ExportToMarkdownIcon />} icon={<ExportToMarkdownIcon />}
iconSize={[20, 20]}
> >
{t('Export to Markdown')} {t('Export to Markdown')}
</MenuItem> </MenuItem>
</> </>
} }
> >
<MenuItem icon={<ExportIcon />} isDir={true}> <MenuItem icon={<ExportIcon />} iconSize={[20, 20]} isDir={true}>
{t('Export')} {t('Export')}
</MenuItem> </MenuItem>
</Menu> </Menu>
@@ -109,6 +113,7 @@ export const EditorOptionMenu = () => {
setOpen(true); setOpen(true);
}} }}
icon={<DeleteTemporarilyIcon />} icon={<DeleteTemporarilyIcon />}
iconSize={[20, 20]}
> >
{t('Delete')} {t('Delete')}
</MenuItem> </MenuItem>
@@ -124,7 +129,7 @@ export const EditorOptionMenu = () => {
disablePortal={true} disablePortal={true}
trigger="click" trigger="click"
> >
<IconButton data-testid="editor-option-menu" iconSize={[20, 20]}> <IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<MoreVerticalIcon /> <MoreVerticalIcon />
</IconButton> </IconButton>
</Menu> </Menu>

View File

@@ -24,7 +24,7 @@ const IconWrapper = styled('div')(({ theme }) => {
width: '32px', width: '32px',
height: '32px', height: '32px',
marginRight: '12px', marginRight: '12px',
fontSize: '20px', fontSize: '24px',
color: theme.colors.iconColor, color: theme.colors.iconColor,
...displayFlex('center', 'center'), ...displayFlex('center', 'center'),
}; };
@@ -102,7 +102,6 @@ export const SyncUser = () => {
setOpen(true); setOpen(true);
}} }}
style={{ marginRight: '12px' }} style={{ marginRight: '12px' }}
iconSize={[20, 20]}
> >
<LocalWorkspaceIcon /> <LocalWorkspaceIcon />
</IconButton> </IconButton>

View File

@@ -4,13 +4,17 @@ import spring, { toString } from 'css-spring';
const ANIMATE_DURATION = 400; const ANIMATE_DURATION = 400;
export const StyledThemeModeSwitch = styled('div')({ export const StyledThemeModeSwitch = styled('div')(({ theme }) => {
width: '32px', return {
height: '32px', width: '32px',
borderRadius: '6px', height: '32px',
overflow: 'hidden', borderRadius: '6px',
backgroundColor: 'transparent', overflow: 'hidden',
position: 'relative', backgroundColor: 'transparent',
position: 'relative',
color: theme.colors.iconColor,
fontSize: '24px',
};
}); });
export const StyledSwitchItem = styled('div')<{ export const StyledSwitchItem = styled('div')<{
active: boolean; active: boolean;
@@ -63,7 +67,6 @@ export const StyledSwitchItem = styled('div')<{
background-color: ${activeStyle.backgroundColor}; background-color: ${activeStyle.backgroundColor};
animation: ${activeStyle.animation}; animation: ${activeStyle.animation};
animation-direction: ${activeStyle.animationDirection}; animation-direction: ${activeStyle.animationDirection};
font-size: 20px;
//svg { //svg {
// width: 24px; // width: 24px;
// height: 24px; // height: 24px;

View File

@@ -1,6 +1,9 @@
import { useTranslation } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons'; import { CloseIcon } from '@blocksuite/icons';
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu'; import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import SyncUser from './header-right-items/SyncUser'; import SyncUser from './header-right-items/SyncUser';
import ThemeModeSwitch from './header-right-items/theme-mode-switch'; import ThemeModeSwitch from './header-right-items/theme-mode-switch';
@@ -56,6 +59,9 @@ export const Header: React.FC<HeaderProps> = ({
useEffect(() => { useEffect(() => {
setShowWarning(shouldShowWarning()); setShowWarning(shouldShowWarning());
}, []); }, []);
const [open] = useSidebarStatus();
const { t } = useTranslation();
return ( return (
<StyledHeaderContainer hasWarning={showWarning}> <StyledHeaderContainer hasWarning={showWarning}>
<BrowserWarning <BrowserWarning
@@ -69,6 +75,12 @@ export const Header: React.FC<HeaderProps> = ({
data-testid="editor-header-items" data-testid="editor-header-items"
data-tauri-drag-region data-tauri-drag-region
> >
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
testid="sliderBar-arrowButton-expand"
/>
{children} {children}
<StyledHeaderRightSide> <StyledHeaderRightSide>
{useMemo( {useMemo(

View File

@@ -1,7 +1,7 @@
import { Content } from '@affine/component'; import { Content } from '@affine/component';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import React, { useState } from 'react'; import React from 'react';
import { openQuickSearchModalAtom } from '../../../atoms'; import { openQuickSearchModalAtom } from '../../../atoms';
import { usePageMeta } from '../../../hooks/use-page-meta'; import { usePageMeta } from '../../../hooks/use-page-meta';
@@ -42,7 +42,6 @@ export const BlockSuiteEditorHeader: React.FC<BlockSuiteEditorHeaderProps> = ({
); );
assertExists(pageMeta); assertExists(pageMeta);
const title = pageMeta.title; const title = pageMeta.title;
const [isHover, setIsHover] = useState(false);
const { trash: isTrash } = pageMeta; const { trash: isTrash } = pageMeta;
return ( return (
<Header <Header
@@ -57,25 +56,12 @@ export const BlockSuiteEditorHeader: React.FC<BlockSuiteEditorHeaderProps> = ({
> >
{children} {children}
{title && !isPublic && ( {title && !isPublic && (
<StyledTitle <StyledTitle data-tauri-drag-region>
data-tauri-drag-region
onMouseEnter={() => {
if (isTrash) return;
setIsHover(true);
}}
onMouseLeave={() => {
if (isTrash) return;
setIsHover(false);
}}
>
<StyledTitleWrapper> <StyledTitleWrapper>
<StyledSwitchWrapper> <StyledSwitchWrapper>
<EditorModeSwitch <EditorModeSwitch
blockSuiteWorkspace={blockSuiteWorkspace} blockSuiteWorkspace={blockSuiteWorkspace}
pageId={pageId} pageId={pageId}
isHover={isHover}
style={{ style={{
marginRight: '12px', marginRight: '12px',
}} }}

View File

@@ -10,7 +10,7 @@ export const StyledHeaderContainer = styled('div')<{ hasWarning: boolean }>(
export const StyledHeader = styled('div')<{ hasWarning: boolean }>( export const StyledHeader = styled('div')<{ hasWarning: boolean }>(
({ theme }) => { ({ theme }) => {
return { return {
height: '60px', height: '64px',
width: '100%', width: '100%',
padding: '0 28px', padding: '0 28px',
...displayFlex('flex-end', 'center'), ...displayFlex('flex-end', 'center'),

View File

@@ -54,10 +54,10 @@ export const Avatar: React.FC<AvatarProps> = React.memo<AvatarProps>(
fontSize: Math.ceil(0.5 * size) + 'px', fontSize: Math.ceil(0.5 * size) + 'px',
background: stringToColour(name || 'AFFiNE'), background: stringToColour(name || 'AFFiNE'),
borderRadius: '50%', borderRadius: '50%',
textAlign: 'center', display: 'inline-flex',
lineHeight: size + 'px', lineHeight: '1',
display: 'inline-block', justifyContent: 'center',
verticalAlign: 'middle', alignItems: 'center',
}} }}
> >
{(name || 'AFFiNE').substring(0, 1)} {(name || 'AFFiNE').substring(0, 1)}

View File

@@ -2,12 +2,13 @@ import { MuiAvatar, textEllipsis } from '@affine/component';
import { styled } from '@affine/component'; import { styled } from '@affine/component';
export const SelectorWrapper = styled('div')(({ theme }) => { export const SelectorWrapper = styled('div')(({ theme }) => {
return { return {
height: '52px', height: '64px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: '0 12px', padding: '0 44px 0 12px',
borderRadius: '5px', borderRadius: '5px',
color: theme.colors.textColor, color: theme.colors.textColor,
position: 'relative',
':hover': { ':hover': {
cursor: 'pointer', cursor: 'pointer',
background: theme.colors.hoverBackground, background: theme.colors.hoverBackground,
@@ -25,7 +26,6 @@ export const WorkspaceName = styled('span')(({ theme }) => {
marginLeft: '12px', marginLeft: '12px',
fontSize: theme.font.h6, fontSize: theme.font.h6,
fontWeight: 500, fontWeight: 500,
marginTop: '4px',
flexGrow: 1, flexGrow: 1,
...textEllipsis(1), ...textEllipsis(1),
}; };

View File

@@ -1,5 +1,4 @@
import { MuiCollapse } from '@affine/component'; import { MuiCollapse } from '@affine/component';
import { Tooltip } from '@affine/component';
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { import {
@@ -16,14 +15,15 @@ import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
import { usePageMeta } from '../../../hooks/use-page-meta'; import { usePageMeta } from '../../../hooks/use-page-meta';
import { RemWorkspace } from '../../../shared'; import { RemWorkspace } from '../../../shared';
import { Arrow } from './icons'; import { SidebarSwitch } from '../../affine/sidebar-switch';
import { import {
StyledArrowButton,
StyledLink, StyledLink,
StyledListItem, StyledListItem,
StyledNewPageButton, StyledNewPageButton,
StyledSidebarWrapper,
StyledSliderBar, StyledSliderBar,
StyledSliderBarWrapper, StyledSliderBarWrapper,
StyledSubListItem, StyledSubListItem,
@@ -83,8 +83,6 @@ export type WorkSpaceSliderBarProps = {
currentPageId: string | null; currentPageId: string | null;
openPage: (pageId: string) => void; openPage: (pageId: string) => void;
createPage: () => Promise<string>; createPage: () => Promise<string>;
show: boolean;
setShow: (show: boolean) => void;
currentPath: string; currentPath: string;
paths: { paths: {
all: (workspaceId: string) => string; all: (workspaceId: string) => string;
@@ -100,17 +98,17 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
currentPageId, currentPageId,
openPage, openPage,
createPage, createPage,
show,
setShow,
currentPath, currentPath,
paths, paths,
onOpenQuickSearchModal, onOpenQuickSearchModal,
onOpenWorkspaceListModal, onOpenWorkspaceListModal,
}) => { }) => {
const currentWorkspaceId = currentWorkspace?.id || null; const currentWorkspaceId = currentWorkspace?.id || null;
const [showSubFavorite, setShowSubFavorite] = useState(true); const [showSubFavorite, setOpenSubFavorite] = useState(true);
const [showTip, setShowTip] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [open] = useSidebarStatus();
const [sidebarOpen] = useSidebarStatus();
const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null); const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null);
const onClickNewPage = useCallback(async () => { const onClickNewPage = useCallback(async () => {
const pageId = await createPage(); const pageId = await createPage();
@@ -120,33 +118,14 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
}, [createPage, openPage]); }, [createPage, openPage]);
return ( return (
<> <>
<StyledSliderBar show={isPublicWorkspace ? false : show}> <StyledSliderBar show={isPublicWorkspace ? false : sidebarOpen}>
<Tooltip <StyledSidebarWrapper>
content={show ? t('Collapse sidebar') : t('Expand sidebar')} <SidebarSwitch
placement="right" visible={open}
visible={showTip} tooltipContent={t('Collapse sidebar')}
> testid="sliderBar-arrowButton-collapse"
<StyledArrowButton />
data-testid="sliderBar-arrowButton" </StyledSidebarWrapper>
isShow={show}
style={{
visibility: isPublicWorkspace ? 'hidden' : 'visible',
}}
onClick={useCallback(() => {
setShow(!show);
setShowTip(false);
}, [setShow, show])}
onMouseEnter={useCallback(() => {
setShowTip(true);
}, [])}
onMouseLeave={useCallback(() => {
setShowTip(false);
}, [])}
>
<Arrow />
</StyledArrowButton>
</Tooltip>
<StyledSliderBarWrapper data-testid="sliderBar"> <StyledSliderBarWrapper data-testid="sliderBar">
<WorkspaceSelector <WorkspaceSelector
currentWorkspace={currentWorkspace} currentWorkspace={currentWorkspace}
@@ -196,7 +175,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
<IconButton <IconButton
darker={true} darker={true}
onClick={useCallback(() => { onClick={useCallback(() => {
setShowSubFavorite(!showSubFavorite); setOpenSubFavorite(!showSubFavorite);
}, [showSubFavorite])} }, [showSubFavorite])}
> >
<ArrowDownSmallIcon <ArrowDownSmallIcon
@@ -233,7 +212,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
{/* <WorkspaceSetting {/* <WorkspaceSetting
isShow={showWorkspaceSetting} isShow={showWorkspaceSetting}
onClose={() => { onClose={() => {
setShowWorkspaceSetting(false); setOpenWorkspaceSetting(false);
}} }}
/> */} /> */}
{/* TODO: will finish the feature next version */} {/* TODO: will finish the feature next version */}

View File

@@ -12,11 +12,19 @@ export const StyledSliderBar = styled('div')<{ show: boolean }>(
transition: 'width .15s, padding .15s', transition: 'width .15s, padding .15s',
position: 'relative', position: 'relative',
zIndex: theme.zIndex.modal, zIndex: theme.zIndex.modal,
padding: show ? '24px 12px' : '24px 0', padding: show ? '0 12px' : '0',
flexShrink: 0, flexShrink: 0,
}; };
} }
); );
export const StyledSidebarWrapper = styled('div')(() => {
return {
position: 'absolute',
right: '12px',
top: '16px',
zIndex: 1,
};
});
export const StyledSliderBarWrapper = styled('div')(() => { export const StyledSliderBarWrapper = styled('div')(() => {
return { return {
height: '100%', height: '100%',
@@ -26,31 +34,6 @@ export const StyledSliderBarWrapper = styled('div')(() => {
}; };
}); });
export const StyledArrowButton = styled('button')<{ isShow: boolean }>(
({ theme, isShow }) => {
return {
width: '32px',
height: '32px',
...displayFlex('center', 'center'),
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
borderRadius: '50%',
transition: 'all .15s',
position: 'absolute',
top: '34px',
right: '-20px',
zIndex: theme.zIndex.modal,
svg: {
transform: isShow ? 'rotate(180deg)' : 'unset',
},
':hover': {
color: '#fff',
backgroundColor: theme.colors.primaryColor,
},
};
}
);
export const StyledListItem = styled('div')<{ export const StyledListItem = styled('div')<{
active?: boolean; active?: boolean;
disabled?: boolean; disabled?: boolean;

View File

@@ -0,0 +1,8 @@
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
const sideBarOpenAtom = atomWithStorage('sidebarOpen', true);
export function useSidebarStatus() {
return useAtom(sideBarOpenAtom);
}

View File

@@ -3,7 +3,6 @@ import { setUpLanguage, useTranslation } from '@affine/i18n';
import { assertExists, nanoid } from '@blocksuite/store'; import { assertExists, nanoid } from '@blocksuite/store';
import { NoSsr } from '@mui/material'; import { NoSsr } from '@mui/material';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -35,8 +34,6 @@ const QuickSearchModal = dynamic(
() => import('../components/pure/quick-search-modal') () => import('../components/pure/quick-search-modal')
); );
const sideBarOpenAtom = atomWithStorage('sideBarOpen', true);
const logger = new DebugLogger('workspace-layout'); const logger = new DebugLogger('workspace-layout');
export const WorkspaceLayout: React.FC<React.PropsWithChildren> = export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
function WorkspacesSuspense({ children }) { function WorkspacesSuspense({ children }) {
@@ -91,7 +88,6 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({ export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
children, children,
}) => { }) => {
const [show, setShow] = useAtom(sideBarOpenAtom);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId(); const [currentPageId] = useCurrentPageId();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
@@ -183,8 +179,6 @@ export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
onOpenWorkspaceListModal={handleOpenWorkspaceListModal} onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={handleOpenPage} openPage={handleOpenPage}
createPage={handleCreatePage} createPage={handleCreatePage}
show={show}
setShow={setShow}
currentPath={router.asPath} currentPath={router.asPath}
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator} paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
/> />

View File

@@ -10,17 +10,19 @@ import { StyledArrow, StyledMenuItem } from './styles';
export type IconMenuProps = PropsWithChildren<{ export type IconMenuProps = PropsWithChildren<{
isDir?: boolean; isDir?: boolean;
icon?: ReactElement; icon?: ReactElement;
iconSize?: [number, number];
}> & }> &
HTMLAttributes<HTMLButtonElement>; HTMLAttributes<HTMLButtonElement>;
export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>( export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
({ isDir = false, icon, children, ...props }, ref) => { ({ isDir = false, icon, iconSize, children, ...props }, ref) => {
const [iconWidth, iconHeight] = iconSize || [16, 16];
return ( return (
<StyledMenuItem ref={ref} {...props}> <StyledMenuItem ref={ref} {...props}>
{icon && {icon &&
cloneElement(icon, { cloneElement(icon, {
width: 16, width: iconWidth,
height: 16, height: iconHeight,
style: { style: {
marginRight: 14, marginRight: 14,
...icon.props?.style, ...icon.props?.style,

View File

@@ -7,31 +7,8 @@ loadPage();
test.describe('Change page mode(Page or Edgeless)', () => { test.describe('Change page mode(Page or Edgeless)', () => {
test('Switch to edgeless by switch edgeless item', async ({ page }) => { test('Switch to edgeless by switch edgeless item', async ({ page }) => {
const switcher = page.locator('[data-testid=editor-mode-switcher]'); const btn = await page.getByTestId('switch-edgeless-mode-button');
const box = await switcher.boundingBox(); await btn.click();
expect(box?.x).not.toBeUndefined();
// mouse hover trigger animation for showing full switcher
// await page.mouse.move((box?.x ?? 0) + 5, (box?.y ?? 0) + 5);
await page.mouse.move((box?.x ?? 0) + 10, (box?.y ?? 0) + 10);
// await page.waitForTimeout(1000);
const edgelessButton = page.getByTestId('switch-edgeless-item'); // page.getByText('Edgeless').click()
await edgelessButton.click();
// // mouse move to edgeless button
// await page.mouse.move(
// (box?.x ?? 0) + (box?.width ?? 0) - 5,
// (box?.y ?? 0) + 5
// );
// await page.waitForTimeout(1000);
// // click switcher
// await page.mouse.click(
// (box?.x ?? 0) + (box?.width ?? 0) - 5,
// (box?.y ?? 0) + 5
// );
const edgeless = page.locator('affine-edgeless-page'); const edgeless = page.locator('affine-edgeless-page');
expect(await edgeless.isVisible()).toBe(true); expect(await edgeless.isVisible()).toBe(true);

View File

@@ -7,17 +7,17 @@ loadPage();
test.describe('Layout ui', () => { test.describe('Layout ui', () => {
test('Collapse Sidebar', async ({ page }) => { test('Collapse Sidebar', async ({ page }) => {
await page.getByTestId('sliderBar-arrowButton').click(); await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar'); const sliderBarArea = page.getByTestId('sliderBar');
await expect(sliderBarArea).not.toBeVisible(); await expect(sliderBarArea).not.toBeVisible();
}); });
test('Expand Sidebar', async ({ page }) => { test('Expand Sidebar', async ({ page }) => {
await page.getByTestId('sliderBar-arrowButton').click(); await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar'); const sliderBarArea = page.getByTestId('sliderBar');
await expect(sliderBarArea).not.toBeVisible(); await expect(sliderBarArea).not.toBeVisible();
await page.getByTestId('sliderBar-arrowButton').click(); await page.getByTestId('sliderBar-arrowButton-expand').click();
await expect(sliderBarArea).toBeVisible(); await expect(sliderBarArea).toBeVisible();
}); });
}); });