feat(component): init app sidebar (#2135)

This commit is contained in:
Himself65
2023-04-27 16:46:08 -05:00
committed by GitHub
parent f3cbe54625
commit c1a65b6b76
23 changed files with 668 additions and 451 deletions

View File

@@ -33,9 +33,11 @@
"@mui/base": "5.0.0-alpha.127",
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.12.2",
"@popperjs/core": "^2.11.7",
"@radix-ui/react-avatar": "^1.0.2",
"@toeverything/hooks": "workspace:*",
"@toeverything/theme": "workspace:*",
"@vanilla-extract/dynamic": "^2.0.3",
"clsx": "^1.2.1",
"jotai": "^2.0.4",
"lit": "^2.7.2",

View File

@@ -0,0 +1,96 @@
import { baseTheme } from '@toeverything/theme';
import { createVar, style } from '@vanilla-extract/css';
export const floatingMaxWidth = 768;
export const navWidthVar = createVar('nav-width');
export const navStyle = style({
position: 'relative',
backgroundColor: 'var(--affine-background-secondary-color)',
width: navWidthVar,
minWidth: navWidthVar,
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'margin-left .3s',
zIndex: parseInt(baseTheme.zIndexModal),
borderRight: '1px solid var(--affine-border-color)',
'@media': {
[`(max-width: ${floatingMaxWidth}px)`]: {
position: 'absolute',
width: `calc(10vw + ${navWidthVar})`,
selectors: {
'&[data-open="false"]': {
marginLeft: `calc((10vw + ${navWidthVar}) * -1)`,
},
'&[data-is-macos-electron="true"]': {
backgroundColor: 'var(--affine-background-secondary-color)',
},
},
},
},
selectors: {
'&[data-open="false"]': {
marginLeft: `calc(${navWidthVar} * -1)`,
},
'&[data-is-macos-electron="true"]': {
backgroundColor: 'transparent',
},
},
vars: {
[navWidthVar]: '256px',
},
});
export const navHeaderStyle = style({
flex: '0 0 auto',
height: '52px',
padding: '0px 16px 0px 10px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
selectors: {
'&[data-is-macos-electron="true"]': {
justifyContent: 'flex-end',
},
},
});
export const navBodyStyle = style({
flex: '1 1 auto',
});
export const navFooterStyle = style({
flex: '0 0 auto',
borderTop: '1px solid var(--affine-border-color)',
});
export const sidebarButtonStyle = style({
width: '32px',
height: '32px',
color: 'var(--affine-icon-color)',
});
export const sidebarFloatMaskStyle = style({
transition: 'opacity .15s',
opacity: 0,
pointerEvents: 'none',
position: 'fixed',
top: 0,
left: 0,
right: '100%',
bottom: 0,
zIndex: parseInt(baseTheme.zIndexModal) - 1,
background: 'var(--affine-background-modal-color)',
'@media': {
[`(max-width: ${floatingMaxWidth}px)`]: {
selectors: {
'&[data-open="true"]': {
opacity: 1,
pointerEvents: 'auto',
right: '0',
},
},
},
},
});

View File

@@ -0,0 +1,7 @@
import { atomWithStorage } from 'jotai/utils';
export const appSidebarOpenAtom = atomWithStorage('app-sidebar-open', true);
export const appSidebarWidthAtom = atomWithStorage(
'app-sidebar-width',
256 /* px */
);

View File

@@ -0,0 +1,53 @@
import { IconButton } from '@affine/component';
import { SidebarIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { useAtom } from 'jotai';
import { useRef } from 'react';
import { AppSidebar, appSidebarOpenAtom, ResizeIndicator } from '.';
import { navHeaderStyle, sidebarButtonStyle } from './index.css';
export default {
title: 'Components/AppSidebar',
component: AppSidebar,
} satisfies Meta;
const Footer = () => <div>Add Page</div>;
export const Default: StoryFn = props => {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const ref = useRef<HTMLElement>(null);
return (
<>
<main
style={{
position: 'relative',
width: '100vw',
height: '600px',
overflow: 'hidden',
display: 'flex',
flexDirection: 'row',
}}
>
<AppSidebar footer={<Footer />} ref={ref}>
Test
</AppSidebar>
<ResizeIndicator targetElement={ref} />
<div>
<div className={navHeaderStyle}>
{!open && (
<IconButton
className={sidebarButtonStyle}
onClick={() => {
setOpen(true);
}}
>
<SidebarIcon width={24} height={24} />
</IconButton>
)}
</div>
</div>
</main>
</>
);
};

View File

@@ -0,0 +1,108 @@
import { getEnvironment } from '@affine/env';
import {
ArrowLeftSmallIcon,
ArrowRightSmallIcon,
SidebarIcon,
} from '@blocksuite/icons';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useAtom, useAtomValue } from 'jotai';
import type { PropsWithChildren, ReactElement } from 'react';
import type { ReactNode } from 'react';
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
import { IconButton } from '../../ui/button/IconButton';
import {
navBodyStyle,
navFooterStyle,
navHeaderStyle,
navStyle,
navWidthVar,
sidebarButtonStyle,
sidebarFloatMaskStyle,
} from './index.css';
import { appSidebarOpenAtom, appSidebarWidthAtom } from './index.jotai';
export { appSidebarOpenAtom };
export type AppSidebarProps = PropsWithChildren<{
footer?: ReactNode | undefined;
}>;
export const AppSidebar = forwardRef<HTMLElement, AppSidebarProps>(
function AppSidebar(props, forwardedRef): ReactElement {
const ref = useRef<HTMLElement>(null);
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
const handleSidebarOpen = useCallback(() => {
setOpen(open => !open);
}, [setOpen]);
useImperativeHandle(forwardedRef, () => ref.current as HTMLElement);
const environment = getEnvironment();
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
return (
<>
<nav
className={navStyle}
ref={ref}
style={assignInlineVars({
[navWidthVar]: `${appSidebarWidth}px`,
})}
data-testid="app-sidebar"
data-open={open}
data-is-macos-electron={isMacosDesktop}
>
<div
className={navHeaderStyle}
data-is-macos-electron={isMacosDesktop}
>
{isMacosDesktop && (
<>
<IconButton
size="middle"
onClick={() => {
window.history.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
size="middle"
onClick={() => {
window.history.forward();
}}
style={{ marginLeft: '32px' }}
>
<ArrowRightSmallIcon />
</IconButton>
</>
)}
<IconButton
data-testid="app-sidebar-arrow-button-collapse"
className={sidebarButtonStyle}
onClick={handleSidebarOpen}
>
<SidebarIcon width={24} height={24} />
</IconButton>
</div>
<div className={navBodyStyle}>{props.children}</div>
<div className={navFooterStyle}>{props.footer}</div>
</nav>
<div
data-testid="app-sidebar-float-mask"
data-open={open}
className={sidebarFloatMaskStyle}
onClick={useCallback(() => {
setOpen(false);
}, [setOpen])}
/>
</>
);
}
);
export type { ResizeIndicatorProps } from './resize-indicator';
export { ResizeIndicator } from './resize-indicator';

View File

@@ -0,0 +1,37 @@
import { style } from '@vanilla-extract/css';
import { navWidthVar } from '../index.css';
export const spacerStyle = style({
position: 'absolute',
width: '1px',
left: navWidthVar,
top: 0,
bottom: 0,
height: '100%',
zIndex: 'calc(var(--affine-z-index-modal) - 1)',
backgroundColor: 'var(--affine-border-color)',
opacity: 0,
cursor: 'col-resize',
'@media': {
'(max-width: 600px)': {
// do not allow resizing on mobile
display: 'none',
},
},
transition: 'opacity 0.15s ease 0.1s',
selectors: {
'&:hover': {
opacity: 1,
},
'&[data-resizing="true"]': {
transition: 'width .3s, min-width .3s, max-width .3s',
},
'&[data-open="false"]': {
display: 'none',
},
'&[data-open="open"]': {
display: 'block',
},
},
});

View File

@@ -0,0 +1,86 @@
import type { Instance } from '@popperjs/core';
import { createPopper } from '@popperjs/core';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { ReactElement, RefObject } from 'react';
import {
useCallback,
useDeferredValue,
useEffect,
useRef,
useState,
} from 'react';
import { appSidebarOpenAtom, appSidebarWidthAtom } from '../index.jotai';
import { spacerStyle } from './index.css';
export type ResizeIndicatorProps = {
targetElement: RefObject<HTMLElement>;
};
export const ResizeIndicator = (props: ResizeIndicatorProps): ReactElement => {
const ref = useRef<HTMLDivElement>(null);
const popperRef = useRef<Instance | null>(null);
const setWidth = useSetAtom(appSidebarWidthAtom);
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
const [isResizing, setIsResizing] = useState(false);
useEffect(() => {
if (ref.current) {
if (props.targetElement.current) {
popperRef.current = createPopper(
props.targetElement.current,
ref.current,
{
placement: 'right',
}
);
}
}
}, [props.targetElement]);
const sidebarWidth = useDeferredValue(useAtomValue(appSidebarWidthAtom));
useEffect(() => {
if (popperRef.current) {
popperRef.current.update();
}
}, [sidebarWidth]);
const onResizeStart = useCallback(() => {
let resized = false;
function onMouseMove(e: MouseEvent) {
e.preventDefault();
const newWidth = Math.min(480, Math.max(e.clientX, 256));
setWidth(newWidth);
setIsResizing(true);
resized = true;
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener(
'mouseup',
() => {
// if not resized, toggle sidebar
if (!resized) {
setSidebarOpen(o => !o);
}
if (popperRef.current) {
popperRef.current.update();
}
setIsResizing(false);
document.removeEventListener('mousemove', onMouseMove);
},
{ once: true }
);
}, [setSidebarOpen, setWidth]);
return (
<div
ref={ref}
className={spacerStyle}
data-testid="app-sidebar-resizer"
data-resizing={isResizing}
data-open={sidebarOpen}
onMouseDown={onResizeStart}
/>
);
};

View File

@@ -66,16 +66,18 @@ export const changeLogWrapperSlideOutStyle = style({
animation: `${slideOut} .3s ease-in-out forwards`,
});
export const changeLogSlideInStyle = style({
width: '110%',
// fixme: if width is 100% and marginLeft is 0,
// the UI will overflow on app sidebar
width: '99%',
marginLeft: '2px',
height: '32px',
display: 'flex',
justifyContent: 'flex-start',
justifyContent: 'space-between',
alignItems: 'center',
color: 'var(--affine-primary-color)',
backgroundColor: 'var(--affine-tertiary-color)',
border: '1px solid var(--affine-primary-color)',
borderRight: 'none',
marginLeft: '8px',
paddingLeft: '8px',
borderRadius: '16px 0 0 16px',
cursor: 'pointer',
@@ -89,7 +91,6 @@ export const changeLogSlideOutStyle = style({
animation: `${slideOut2} .3s ease-in-out forwards`,
});
export const linkStyle = style({
flexGrow: 1,
textAlign: 'left',
color: 'var(--affine-text-emphasis-color)',
display: 'flex',
@@ -103,6 +104,6 @@ export const iconStyle = style({
});
export const iconButtonStyle = style({
fontSize: '20px',
marginRight: '12%',
marginRight: '0',
color: 'var(--affine-icon-color)',
});

View File

@@ -30,7 +30,6 @@ export const StyledIconButton = styled('button', {
fontSize?: CSSProperties['fontSize'];
}>(
({
theme,
width,
height,
borderRadius,