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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { useTheme } from '@mui/material';
import React, { cloneElement, CSSProperties, useEffect, useState } from 'react';
import { CSSProperties } from 'react';
import {
usePageMeta,
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
// todo(himself65): remove `useTheme` hook
import { BlockSuiteWorkspace } from '../../../../shared';
import { EdgelessIcon, PaperIcon } from './Icons';
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>
);
};
import { StyledEditorModeSwitch, StyledSwitchItem } from './style';
export type EditorModeSwitchProps = {
// todo(himself65): combine these two properties
blockSuiteWorkspace: BlockSuiteWorkspace;
pageId: string;
isHover: boolean;
style: CSSProperties;
style?: CSSProperties;
};
export const EditorModeSwitch: React.FC<EditorModeSwitchProps> = ({
isHover,
style = {},
export const EditorModeSwitch = ({
style,
blockSuiteWorkspace,
pageId,
}) => {
const theme = useTheme();
}: EditorModeSwitchProps) => {
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
@ -82,85 +28,32 @@ export const EditorModeSwitch: React.FC<EditorModeSwitchProps> = ({
assertExists(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 (
<StyledAnimateRadioContainer
data-testid="editor-mode-switcher"
shrink={!isHover}
<StyledEditorModeSwitch
style={style}
disabled={!!trash}
switchLeft={mode === 'page'}
showAlone={trash}
>
<AnimateRadioItem
isLeft={true}
label={t('Page')}
icon={<PaperItem />}
<StyledSwitchItem
data-testid="switch-page-mode-button"
active={mode === 'page'}
status={radioItemStatus.left}
hide={trash && mode !== 'page'}
onClick={() => {
setPageMeta(pageId, { mode: 'page' });
}}
onMouseEnter={() => {
setRadioItemStatus({
right: 'normal',
left: 'stretch',
});
}}
onMouseLeave={() => {
setRadioItemStatus(modifyRadioItemStatus());
}}
/>
<StyledMiddleLine
hidden={!isHover}
dark={theme.palette.mode === 'dark'}
/>
<AnimateRadioItem
isLeft={false}
label={t('Edgeless')}
data-testid="switch-edgeless-item"
icon={<EdgelessItem />}
>
<PaperIcon />
</StyledSwitchItem>
<StyledSwitchItem
data-testid="switch-edgeless-mode-button"
active={mode === 'edgeless'}
status={radioItemStatus.right}
hide={trash && mode !== 'edgeless'}
onClick={() => {
setPageMeta(pageId, { mode: 'edgeless' });
}}
onMouseEnter={() => {
setRadioItemStatus({
left: 'normal',
right: 'stretch',
});
}}
onMouseLeave={() => {
setRadioItemStatus(modifyRadioItemStatus());
}}
/>
</StyledAnimateRadioContainer>
>
<EdgelessIcon />
</StyledSwitchItem>
</StyledEditorModeSwitch>
);
};
export default EditorModeSwitch;

View File

@ -1,179 +1,58 @@
import { css, displayFlex, keyframes, 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
`,
}
: {};
import { displayFlex, styled } from '@affine/component';
export const StyledEditorModeSwitch = styled('div')<{
switchLeft: boolean;
showAlone?: boolean;
}>(({ theme, switchLeft, showAlone }) => {
const {
colors: { iconColor, primaryColor },
palette: { mode },
} = 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 {
...displayFlex('center', 'center'),
flexShrink: '0',
...dynamicStyle,
width: showAlone ? '40px' : '78px',
height: '32px',
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,
MoreVerticalIcon,
} from '@blocksuite/icons';
import { EdgelessIcon, PaperIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useTheme } from '@mui/material';
import { useState } from 'react';
@ -22,7 +23,6 @@ import {
usePageMeta,
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
import { EdgelessIcon, PaperIcon } from '../editor-mode-switch/Icons';
export const EditorOptionMenu = () => {
const { t } = useTranslation();
@ -52,6 +52,7 @@ export const EditorOptionMenu = () => {
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
iconSize={[20, 20]}
icon={
favorite ? (
<FavoritedIcon style={{ color: theme.colors.primaryColor }} />
@ -64,6 +65,7 @@ export const EditorOptionMenu = () => {
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
iconSize={[20, 20]}
data-testid="editor-option-menu-edgeless"
onClick={() => {
setPageMeta(pageId, {
@ -84,6 +86,7 @@ export const EditorOptionMenu = () => {
globalThis.editor.contentParser.onExportHtml();
}}
icon={<ExportToHtmlIcon />}
iconSize={[20, 20]}
>
{t('Export to HTML')}
</MenuItem>
@ -93,13 +96,14 @@ export const EditorOptionMenu = () => {
globalThis.editor.contentParser.onExportMarkdown();
}}
icon={<ExportToMarkdownIcon />}
iconSize={[20, 20]}
>
{t('Export to Markdown')}
</MenuItem>
</>
}
>
<MenuItem icon={<ExportIcon />} isDir={true}>
<MenuItem icon={<ExportIcon />} iconSize={[20, 20]} isDir={true}>
{t('Export')}
</MenuItem>
</Menu>
@ -109,6 +113,7 @@ export const EditorOptionMenu = () => {
setOpen(true);
}}
icon={<DeleteTemporarilyIcon />}
iconSize={[20, 20]}
>
{t('Delete')}
</MenuItem>
@ -124,7 +129,7 @@ export const EditorOptionMenu = () => {
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[20, 20]}>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<MoreVerticalIcon />
</IconButton>
</Menu>

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ export const StyledHeaderContainer = styled('div')<{ hasWarning: boolean }>(
export const StyledHeader = styled('div')<{ hasWarning: boolean }>(
({ theme }) => {
return {
height: '60px',
height: '64px',
width: '100%',
padding: '0 28px',
...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',
background: stringToColour(name || 'AFFiNE'),
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
display: 'inline-flex',
lineHeight: '1',
justifyContent: 'center',
alignItems: 'center',
}}
>
{(name || 'AFFiNE').substring(0, 1)}

View File

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

View File

@ -1,5 +1,4 @@
import { MuiCollapse } from '@affine/component';
import { Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
@ -16,14 +15,15 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useMemo, useState } from 'react';
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { RemWorkspace } from '../../../shared';
import { Arrow } from './icons';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import {
StyledArrowButton,
StyledLink,
StyledListItem,
StyledNewPageButton,
StyledSidebarWrapper,
StyledSliderBar,
StyledSliderBarWrapper,
StyledSubListItem,
@ -83,8 +83,6 @@ export type WorkSpaceSliderBarProps = {
currentPageId: string | null;
openPage: (pageId: string) => void;
createPage: () => Promise<string>;
show: boolean;
setShow: (show: boolean) => void;
currentPath: string;
paths: {
all: (workspaceId: string) => string;
@ -100,17 +98,17 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
currentPageId,
openPage,
createPage,
show,
setShow,
currentPath,
paths,
onOpenQuickSearchModal,
onOpenWorkspaceListModal,
}) => {
const currentWorkspaceId = currentWorkspace?.id || null;
const [showSubFavorite, setShowSubFavorite] = useState(true);
const [showTip, setShowTip] = useState(false);
const [showSubFavorite, setOpenSubFavorite] = useState(true);
const { t } = useTranslation();
const [open] = useSidebarStatus();
const [sidebarOpen] = useSidebarStatus();
const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null);
const onClickNewPage = useCallback(async () => {
const pageId = await createPage();
@ -120,33 +118,14 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
}, [createPage, openPage]);
return (
<>
<StyledSliderBar show={isPublicWorkspace ? false : show}>
<Tooltip
content={show ? t('Collapse sidebar') : t('Expand sidebar')}
placement="right"
visible={showTip}
>
<StyledArrowButton
data-testid="sliderBar-arrowButton"
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>
<StyledSliderBar show={isPublicWorkspace ? false : sidebarOpen}>
<StyledSidebarWrapper>
<SidebarSwitch
visible={open}
tooltipContent={t('Collapse sidebar')}
testid="sliderBar-arrowButton-collapse"
/>
</StyledSidebarWrapper>
<StyledSliderBarWrapper data-testid="sliderBar">
<WorkspaceSelector
currentWorkspace={currentWorkspace}
@ -196,7 +175,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
<IconButton
darker={true}
onClick={useCallback(() => {
setShowSubFavorite(!showSubFavorite);
setOpenSubFavorite(!showSubFavorite);
}, [showSubFavorite])}
>
<ArrowDownSmallIcon
@ -233,7 +212,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
{/* <WorkspaceSetting
isShow={showWorkspaceSetting}
onClose={() => {
setShowWorkspaceSetting(false);
setOpenWorkspaceSetting(false);
}}
/> */}
{/* 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',
position: 'relative',
zIndex: theme.zIndex.modal,
padding: show ? '24px 12px' : '24px 0',
padding: show ? '0 12px' : '0',
flexShrink: 0,
};
}
);
export const StyledSidebarWrapper = styled('div')(() => {
return {
position: 'absolute',
right: '12px',
top: '16px',
zIndex: 1,
};
});
export const StyledSliderBarWrapper = styled('div')(() => {
return {
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')<{
active?: 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 { NoSsr } from '@mui/material';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import { useRouter } from 'next/router';
@ -35,8 +34,6 @@ const QuickSearchModal = dynamic(
() => import('../components/pure/quick-search-modal')
);
const sideBarOpenAtom = atomWithStorage('sideBarOpen', true);
const logger = new DebugLogger('workspace-layout');
export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
function WorkspacesSuspense({ children }) {
@ -91,7 +88,6 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [show, setShow] = useAtom(sideBarOpenAtom);
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId();
const workspaces = useWorkspaces();
@ -183,8 +179,6 @@ export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={handleOpenPage}
createPage={handleCreatePage}
show={show}
setShow={setShow}
currentPath={router.asPath}
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
/>

View File

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

View File

@ -7,31 +7,8 @@ loadPage();
test.describe('Change page mode(Page or Edgeless)', () => {
test('Switch to edgeless by switch edgeless item', async ({ page }) => {
const switcher = page.locator('[data-testid=editor-mode-switcher]');
const box = await switcher.boundingBox();
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 btn = await page.getByTestId('switch-edgeless-mode-button');
await btn.click();
const edgeless = page.locator('affine-edgeless-page');
expect(await edgeless.isVisible()).toBe(true);

View File

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