mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
Feat/sidebar&top bar (#1454)
This commit is contained in:
26
apps/web/src/components/affine/sidebar-switch/icons.tsx
Normal file
26
apps/web/src/components/affine/sidebar-switch/icons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
49
apps/web/src/components/affine/sidebar-switch/index.tsx
Normal file
49
apps/web/src/components/affine/sidebar-switch/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
23
apps/web/src/components/affine/sidebar-switch/style.ts
Normal file
23
apps/web/src/components/affine/sidebar-switch/style.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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 { 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;
|
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
8
apps/web/src/hooks/affine/use-sidebar-status.ts
Normal file
8
apps/web/src/hooks/affine/use-sidebar-status.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user