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
@@ -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>
);
};
@@ -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;
@@ -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',
};
});
@@ -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>;
@@ -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>
@@ -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>
@@ -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;
@@ -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(
@@ -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',
}}
@@ -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'),