refactor(core): side bar resizing (#5280)
Rewrite sidebar panel using a customized react-resizable-panels version that supports sidebar pixel sizing (not using flex percentages). Now the left & right sidebar using the same `ResizePanel` impl. fix https://github.com/toeverything/AFFiNE/issues/5271 fix TOV-163 fix TOV-146 fix TOV-168 fix TOV-109 fix TOV-165
This commit is contained in:
@@ -1,21 +1,12 @@
|
|||||||
import { baseTheme } from '@toeverything/theme';
|
|
||||||
import type { ComplexStyleRule } from '@vanilla-extract/css';
|
import type { ComplexStyleRule } from '@vanilla-extract/css';
|
||||||
import { createVar, style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const floatingMaxWidth = 768;
|
export const floatingMaxWidth = 768;
|
||||||
export const navWidthVar = createVar('nav-width');
|
|
||||||
|
|
||||||
export const navWrapperStyle = style({
|
export const navWrapperStyle = style({
|
||||||
vars: {
|
|
||||||
[navWidthVar]: '256px',
|
|
||||||
},
|
|
||||||
position: 'relative',
|
|
||||||
width: navWidthVar,
|
|
||||||
minWidth: navWidthVar,
|
|
||||||
height: '100%',
|
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
paddingBottom: '8px',
|
paddingBottom: '8px',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'var(--affine-background-primary-color)',
|
||||||
'@media': {
|
'@media': {
|
||||||
print: {
|
print: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
@@ -23,23 +14,7 @@ export const navWrapperStyle = style({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-is-floating="true"]': {
|
'&[data-has-border=true]': {
|
||||||
position: 'absolute',
|
|
||||||
width: `calc(${navWidthVar})`,
|
|
||||||
zIndex: 4,
|
|
||||||
backgroundColor: 'var(--affine-background-primary-color)',
|
|
||||||
},
|
|
||||||
'&[data-open="false"]': {
|
|
||||||
marginLeft: `calc(${navWidthVar} * -1)`,
|
|
||||||
},
|
|
||||||
'&[data-enable-animation="true"]': {
|
|
||||||
transition: 'margin-left .3s .05s, width .3s .05s',
|
|
||||||
},
|
|
||||||
'&[data-is-floating="false"].has-background': {
|
|
||||||
backgroundColor: 'var(--affine-white-60)',
|
|
||||||
borderRight: '1px solid var(--affine-border-color)',
|
|
||||||
},
|
|
||||||
'&.has-border': {
|
|
||||||
borderRight: '1px solid var(--affine-border-color)',
|
borderRight: '1px solid var(--affine-border-color)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -63,7 +38,6 @@ export const navStyle = style({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
zIndex: parseInt(baseTheme.zIndexModal),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const navHeaderStyle = style({
|
export const navHeaderStyle = style({
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Skeleton } from '../../ui/skeleton';
|
import { Skeleton } from '../../ui/skeleton';
|
||||||
|
import { ResizePanel } from '../resize-panel';
|
||||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||||
import {
|
import {
|
||||||
floatingMaxWidth,
|
floatingMaxWidth,
|
||||||
navBodyStyle,
|
navBodyStyle,
|
||||||
navHeaderStyle,
|
navHeaderStyle,
|
||||||
navStyle,
|
navStyle,
|
||||||
navWidthVar,
|
|
||||||
navWrapperStyle,
|
navWrapperStyle,
|
||||||
sidebarFloatMaskStyle,
|
sidebarFloatMaskStyle,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
@@ -23,7 +21,6 @@ import {
|
|||||||
appSidebarResizingAtom,
|
appSidebarResizingAtom,
|
||||||
appSidebarWidthAtom,
|
appSidebarWidthAtom,
|
||||||
} from './index.jotai';
|
} from './index.jotai';
|
||||||
import { ResizeIndicator } from './resize-indicator';
|
|
||||||
import type { SidebarHeaderProps } from './sidebar-header';
|
import type { SidebarHeaderProps } from './sidebar-header';
|
||||||
import { SidebarHeader } from './sidebar-header';
|
import { SidebarHeader } from './sidebar-header';
|
||||||
|
|
||||||
@@ -33,30 +30,19 @@ export type AppSidebarProps = PropsWithChildren<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function useEnableAnimation() {
|
|
||||||
const [enable, setEnable] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
setEnable(true);
|
|
||||||
}, 500);
|
|
||||||
}, []);
|
|
||||||
return enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type History = {
|
export type History = {
|
||||||
stack: string[];
|
stack: string[];
|
||||||
current: number;
|
current: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_WIDTH = 480;
|
||||||
|
const MIN_WIDTH = 256;
|
||||||
|
|
||||||
export function AppSidebar(props: AppSidebarProps): ReactElement {
|
export function AppSidebar(props: AppSidebarProps): ReactElement {
|
||||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||||
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
|
const [width, setWidth] = useAtom(appSidebarWidthAtom);
|
||||||
const [appSidebarFloating, setAppSidebarFloating] = useAtom(
|
const [floating, setFloating] = useAtom(appSidebarFloatingAtom);
|
||||||
appSidebarFloatingAtom
|
const [resizing, setResizing] = useAtom(appSidebarResizingAtom);
|
||||||
);
|
|
||||||
|
|
||||||
const isResizing = useAtomValue(appSidebarResizingAtom);
|
|
||||||
const navRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onResize() {
|
function onResize() {
|
||||||
@@ -64,7 +50,7 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
`(max-width: ${floatingMaxWidth}px)`
|
`(max-width: ${floatingMaxWidth}px)`
|
||||||
).matches;
|
).matches;
|
||||||
const isOverflowWidth = window.matchMedia(
|
const isOverflowWidth = window.matchMedia(
|
||||||
`(max-width: ${appSidebarWidth / 0.4}px)`
|
`(max-width: ${width / 0.4}px)`
|
||||||
).matches;
|
).matches;
|
||||||
const isFloating = isFloatingMaxWidth || isOverflowWidth;
|
const isFloating = isFloatingMaxWidth || isOverflowWidth;
|
||||||
if (
|
if (
|
||||||
@@ -75,7 +61,7 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
// so that the sidebar can be closed on mobile by default
|
// so that the sidebar can be closed on mobile by default
|
||||||
setOpen(!isFloating);
|
setOpen(!isFloating);
|
||||||
}
|
}
|
||||||
setAppSidebarFloating(isFloating && !!open);
|
setFloating(isFloating && !!open);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dOnResize = debounce(onResize, 50);
|
const dOnResize = debounce(onResize, 50);
|
||||||
@@ -83,33 +69,34 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', dOnResize);
|
window.removeEventListener('resize', dOnResize);
|
||||||
};
|
};
|
||||||
}, [appSidebarWidth, open, setAppSidebarFloating, setOpen]);
|
}, [open, setFloating, setOpen, width]);
|
||||||
|
|
||||||
// disable animation to avoid UI flash
|
|
||||||
const enableAnimation = useEnableAnimation();
|
|
||||||
|
|
||||||
|
const transparent = environment.isDesktop && !props.hasBackground;
|
||||||
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
||||||
|
const hasRightBorder = !environment.isDesktop || !transparent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<ResizePanel
|
||||||
style={assignInlineVars({
|
floating={floating}
|
||||||
[navWidthVar]: `${appSidebarWidth}px`,
|
open={open}
|
||||||
})}
|
resizing={resizing}
|
||||||
className={clsx(navWrapperStyle, {
|
maxWidth={MAX_WIDTH}
|
||||||
'has-background': environment.isDesktop && props.hasBackground,
|
minWidth={MIN_WIDTH}
|
||||||
'has-border':
|
width={width}
|
||||||
!environment.isDesktop ||
|
resizeHandlePos="right"
|
||||||
(environment.isDesktop && props.hasBackground),
|
onOpen={setOpen}
|
||||||
})}
|
onResizing={setResizing}
|
||||||
data-open={open}
|
onWidthChange={setWidth}
|
||||||
|
className={navWrapperStyle}
|
||||||
|
resizeHandleVerticalPadding={transparent ? 16 : 0}
|
||||||
|
data-transparent={transparent}
|
||||||
|
data-has-border={hasRightBorder}
|
||||||
data-testid="app-sidebar-wrapper"
|
data-testid="app-sidebar-wrapper"
|
||||||
data-is-macos-electron={isMacosDesktop}
|
data-is-macos-electron={isMacosDesktop}
|
||||||
data-is-floating={appSidebarFloating}
|
data-has-background={environment.isDesktop && props.hasBackground}
|
||||||
data-has-background={props.hasBackground}
|
|
||||||
data-enable-animation={enableAnimation && !isResizing}
|
|
||||||
>
|
>
|
||||||
<nav className={navStyle} ref={navRef} data-testid="app-sidebar">
|
<nav className={navStyle} data-testid="app-sidebar">
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
router={props.router}
|
router={props.router}
|
||||||
generalShortcutsInfo={props.generalShortcutsInfo}
|
generalShortcutsInfo={props.generalShortcutsInfo}
|
||||||
@@ -118,12 +105,11 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<ResizeIndicator targetElement={navRef.current} />
|
</ResizePanel>
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
data-testid="app-sidebar-float-mask"
|
data-testid="app-sidebar-float-mask"
|
||||||
data-open={open}
|
data-open={open}
|
||||||
data-is-floating={appSidebarFloating}
|
data-is-floating={floating}
|
||||||
className={sidebarFloatMaskStyle}
|
className={sidebarFloatMaskStyle}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
@@ -132,15 +118,12 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AppSidebarFallback = (): ReactElement | null => {
|
export const AppSidebarFallback = (): ReactElement | null => {
|
||||||
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
|
const width = useAtomValue(appSidebarWidthAtom);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={assignInlineVars({
|
style={{ width }}
|
||||||
[navWidthVar]: `${appSidebarWidth}px`,
|
className={navWrapperStyle}
|
||||||
})}
|
data-has-border
|
||||||
className={clsx(navWrapperStyle, {
|
|
||||||
'has-border': true,
|
|
||||||
})}
|
|
||||||
data-open="true"
|
data-open="true"
|
||||||
>
|
>
|
||||||
<nav className={navStyle}>
|
<nav className={navStyle}>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
export const resizerContainer = style({
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
top: '16px',
|
|
||||||
bottom: '16px',
|
|
||||||
width: '16px',
|
|
||||||
zIndex: 'calc(var(--affine-z-index-modal) + 1)',
|
|
||||||
transform: 'translateX(50%)',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
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"]': {
|
|
||||||
opacity: 1,
|
|
||||||
transition: 'width .3s, min-width .3s, max-width .3s',
|
|
||||||
},
|
|
||||||
'&[data-open="false"]': {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
'&[data-open="open"]': {
|
|
||||||
display: 'block',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const resizerInner = style({
|
|
||||||
position: 'absolute',
|
|
||||||
height: '100%',
|
|
||||||
width: '4px',
|
|
||||||
left: '6px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: 'var(--affine-primary-color)',
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
appSidebarOpenAtom,
|
|
||||||
appSidebarResizingAtom,
|
|
||||||
appSidebarWidthAtom,
|
|
||||||
} from '../index.jotai';
|
|
||||||
import * as styles from './index.css';
|
|
||||||
|
|
||||||
type ResizeIndicatorProps = {
|
|
||||||
targetElement: HTMLElement | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ResizeIndicator = (props: ResizeIndicatorProps): ReactElement => {
|
|
||||||
const setWidth = useSetAtom(appSidebarWidthAtom);
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
|
|
||||||
const [isResizing, setIsResizing] = useAtom(appSidebarResizingAtom);
|
|
||||||
|
|
||||||
const onResizeStart = useCallback(() => {
|
|
||||||
let resized = false;
|
|
||||||
assertExists(props.targetElement);
|
|
||||||
const { left: anchorLeft } = props.targetElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
function onMouseMove(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!props.targetElement) return;
|
|
||||||
const newWidth = Math.min(480, Math.max(e.clientX - anchorLeft, 256));
|
|
||||||
setWidth(newWidth);
|
|
||||||
setIsResizing(true);
|
|
||||||
resized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener(
|
|
||||||
'mouseup',
|
|
||||||
() => {
|
|
||||||
// if not resized, toggle sidebar
|
|
||||||
if (!resized) {
|
|
||||||
setSidebarOpen(o => !o);
|
|
||||||
}
|
|
||||||
setIsResizing(false);
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}, [props.targetElement, setIsResizing, setSidebarOpen, setWidth]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.resizerContainer}
|
|
||||||
data-testid="app-sidebar-resizer"
|
|
||||||
data-resizing={isResizing}
|
|
||||||
data-open={sidebarOpen}
|
|
||||||
onMouseDown={onResizeStart}
|
|
||||||
>
|
|
||||||
<div className={styles.resizerInner} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './resize-panel';
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const panelWidthVar = createVar('panel-width');
|
||||||
|
export const resizeHandleOffsetVar = createVar('resize-handle-offset');
|
||||||
|
export const resizeHandleVerticalPadding = createVar(
|
||||||
|
'resize-handle-vertical-padding'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
vars: {
|
||||||
|
[panelWidthVar]: '256px',
|
||||||
|
[resizeHandleOffsetVar]: '0',
|
||||||
|
},
|
||||||
|
position: 'relative',
|
||||||
|
width: panelWidthVar,
|
||||||
|
minWidth: 0,
|
||||||
|
height: '100%',
|
||||||
|
selectors: {
|
||||||
|
'&[data-is-floating="true"]': {
|
||||||
|
position: 'absolute',
|
||||||
|
width: `calc(${panelWidthVar})`,
|
||||||
|
zIndex: 4,
|
||||||
|
},
|
||||||
|
'&[data-open="true"]': {
|
||||||
|
maxWidth: '50%',
|
||||||
|
},
|
||||||
|
'&[data-open="false"][data-handle-position="right"]': {
|
||||||
|
marginLeft: `calc(${panelWidthVar} * -1)`,
|
||||||
|
},
|
||||||
|
'&[data-open="false"][data-handle-position="left"]': {
|
||||||
|
marginRight: `calc(${panelWidthVar} * -1)`,
|
||||||
|
},
|
||||||
|
'&[data-enable-animation="true"]': {
|
||||||
|
transition: 'margin-left .3s .05s, margin-right .3s .05s, width .3s .05s',
|
||||||
|
},
|
||||||
|
'&[data-is-floating="false"][data-transparent=true]': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const panelContent = style({
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resizeHandleContainer = style({
|
||||||
|
position: 'absolute',
|
||||||
|
right: resizeHandleOffsetVar,
|
||||||
|
top: resizeHandleVerticalPadding,
|
||||||
|
bottom: resizeHandleVerticalPadding,
|
||||||
|
width: 16,
|
||||||
|
zIndex: '1',
|
||||||
|
transform: 'translateX(50%)',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
opacity: 0,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'ew-resize',
|
||||||
|
'@media': {
|
||||||
|
'(max-width: 600px)': {
|
||||||
|
// do not allow resizing on small screen
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transition: 'opacity 0.15s ease 0.1s',
|
||||||
|
selectors: {
|
||||||
|
'&[data-resizing="true"], &:hover': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
'&[data-open="false"]': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'&[data-open="open"]': {
|
||||||
|
display: 'block',
|
||||||
|
},
|
||||||
|
'&[data-handle-position="left"]': {
|
||||||
|
left: resizeHandleOffsetVar,
|
||||||
|
right: 'auto',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resizerInner = style({
|
||||||
|
position: 'absolute',
|
||||||
|
height: '100%',
|
||||||
|
width: '2px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
backgroundColor: 'var(--affine-primary-color)',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
transform: 'translateX(0.5px)',
|
||||||
|
selectors: {
|
||||||
|
[`${resizeHandleContainer}[data-resizing="true"] &`]: {
|
||||||
|
width: '4px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './resize-panel.css';
|
||||||
|
|
||||||
|
export interface ResizeHandleProps
|
||||||
|
extends React.HtmlHTMLAttributes<HTMLDivElement> {
|
||||||
|
resizing: boolean;
|
||||||
|
open: boolean;
|
||||||
|
minWidth: number;
|
||||||
|
maxWidth: number;
|
||||||
|
resizeHandlePos: 'left' | 'right';
|
||||||
|
resizeHandleOffset?: number;
|
||||||
|
resizeHandleVerticalPadding?: number;
|
||||||
|
onOpen: (open: boolean) => void;
|
||||||
|
onResizing: (resizing: boolean) => void;
|
||||||
|
onWidthChange: (width: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResizePanelProps
|
||||||
|
extends React.HtmlHTMLAttributes<HTMLDivElement> {
|
||||||
|
resizing: boolean;
|
||||||
|
open: boolean;
|
||||||
|
floating?: boolean;
|
||||||
|
minWidth: number;
|
||||||
|
maxWidth: number;
|
||||||
|
resizeHandlePos: 'left' | 'right';
|
||||||
|
resizeHandleOffset?: number;
|
||||||
|
resizeHandleVerticalPadding?: number;
|
||||||
|
enableAnimation?: boolean;
|
||||||
|
width: number;
|
||||||
|
onOpen: (open: boolean) => void;
|
||||||
|
onResizing: (resizing: boolean) => void;
|
||||||
|
onWidthChange: (width: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResizeHandle = ({
|
||||||
|
className,
|
||||||
|
resizing,
|
||||||
|
minWidth,
|
||||||
|
maxWidth,
|
||||||
|
resizeHandlePos,
|
||||||
|
resizeHandleOffset,
|
||||||
|
resizeHandleVerticalPadding,
|
||||||
|
open,
|
||||||
|
onOpen,
|
||||||
|
onResizing,
|
||||||
|
onWidthChange,
|
||||||
|
...rest
|
||||||
|
}: ResizeHandleProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const onResizeStart = useCallback(() => {
|
||||||
|
let resized = false;
|
||||||
|
const panelContainer = ref.current?.parentElement;
|
||||||
|
assertExists(
|
||||||
|
panelContainer,
|
||||||
|
'parent element not found for resize indicator'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { left: anchorLeft, right: anchorRight } =
|
||||||
|
panelContainer.getBoundingClientRect();
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!panelContainer) return;
|
||||||
|
const newWidth = Math.min(
|
||||||
|
maxWidth,
|
||||||
|
Math.max(
|
||||||
|
resizeHandlePos === 'right'
|
||||||
|
? e.clientX - anchorLeft
|
||||||
|
: anchorRight - e.clientX,
|
||||||
|
minWidth
|
||||||
|
)
|
||||||
|
);
|
||||||
|
onWidthChange(newWidth);
|
||||||
|
onResizing(true);
|
||||||
|
resized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener(
|
||||||
|
'mouseup',
|
||||||
|
() => {
|
||||||
|
// if not resized, toggle sidebar
|
||||||
|
if (!resized) {
|
||||||
|
onOpen(false);
|
||||||
|
}
|
||||||
|
onResizing(false);
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}, [maxWidth, resizeHandlePos, minWidth, onWidthChange, onResizing, onOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
data-testid="resize-handle"
|
||||||
|
ref={ref}
|
||||||
|
style={assignInlineVars({
|
||||||
|
[styles.resizeHandleOffsetVar]: `${resizeHandleOffset ?? 0}px`,
|
||||||
|
[styles.resizeHandleVerticalPadding]: `${
|
||||||
|
resizeHandleVerticalPadding ?? 0
|
||||||
|
}px`,
|
||||||
|
})}
|
||||||
|
className={clsx(styles.resizeHandleContainer, className)}
|
||||||
|
data-handle-position={resizeHandlePos}
|
||||||
|
data-resizing={resizing}
|
||||||
|
data-open={open}
|
||||||
|
onMouseDown={onResizeStart}
|
||||||
|
>
|
||||||
|
<div className={styles.resizerInner} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function useEnableAnimation() {
|
||||||
|
const [enable, setEnable] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setEnable(true);
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
return enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||||
|
function ResizePanel(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
resizing,
|
||||||
|
minWidth,
|
||||||
|
maxWidth,
|
||||||
|
width,
|
||||||
|
floating,
|
||||||
|
enableAnimation: _enableAnimation = true,
|
||||||
|
open,
|
||||||
|
onOpen,
|
||||||
|
onResizing,
|
||||||
|
onWidthChange,
|
||||||
|
resizeHandlePos,
|
||||||
|
resizeHandleOffset,
|
||||||
|
resizeHandleVerticalPadding,
|
||||||
|
...rest
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const enableAnimation = useEnableAnimation() && _enableAnimation;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
style={assignInlineVars({
|
||||||
|
[styles.panelWidthVar]: `${width}px`,
|
||||||
|
})}
|
||||||
|
className={clsx(className, styles.root)}
|
||||||
|
data-open={open}
|
||||||
|
data-is-floating={floating}
|
||||||
|
data-handle-position={resizeHandlePos}
|
||||||
|
data-enable-animation={enableAnimation && !resizing}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ResizeHandle
|
||||||
|
resizeHandlePos={resizeHandlePos}
|
||||||
|
resizeHandleOffset={resizeHandleOffset}
|
||||||
|
resizeHandleVerticalPadding={resizeHandleVerticalPadding}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
minWidth={minWidth}
|
||||||
|
onOpen={onOpen}
|
||||||
|
onResizing={onResizing}
|
||||||
|
onWidthChange={onWidthChange}
|
||||||
|
open={open}
|
||||||
|
resizing={resizing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -60,6 +60,8 @@ export const mainContainerStyle = style({
|
|||||||
margin: '8px',
|
margin: '8px',
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
// todo: is this performance intensive?
|
||||||
|
filter: 'drop-shadow(0px 0px 4px rgba(66,65,73,.14))',
|
||||||
'@media': {
|
'@media': {
|
||||||
print: {
|
print: {
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
@@ -86,39 +88,6 @@ export const mainContainerStyle = style({
|
|||||||
},
|
},
|
||||||
} as ComplexStyleRule);
|
} as ComplexStyleRule);
|
||||||
|
|
||||||
// These styles override the default styles of the react-resizable-panels
|
|
||||||
// as the default styles make the overflow part hidden when printing to PDF.
|
|
||||||
// See https://github.com/toeverything/AFFiNE/pull/3893
|
|
||||||
globalStyle(`${mainContainerStyle} > div[data-panel-group]`, {
|
|
||||||
'@media': {
|
|
||||||
print: {
|
|
||||||
overflow: 'visible !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// These styles override the default styles of the react-resizable-panels
|
|
||||||
// as the default styles make the overflow part hidden when printing to PDF.
|
|
||||||
// See https://github.com/toeverything/AFFiNE/pull/3893
|
|
||||||
globalStyle(`${mainContainerStyle} > div[data-panel-group] > div[data-panel]`, {
|
|
||||||
'@media': {
|
|
||||||
print: {
|
|
||||||
overflow: 'visible !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hack margin so that it works normally when sidebar is closed
|
|
||||||
globalStyle(
|
|
||||||
`[data-testid=app-sidebar-wrapper][data-open=true][data-is-floating=false][data-has-background=false]
|
|
||||||
~ ${mainContainerStyle}[data-show-padding="true"]`,
|
|
||||||
{
|
|
||||||
// transition added here to prevent the transition from being applied on page load
|
|
||||||
transition: 'margin-left .3s ease-in-out',
|
|
||||||
marginLeft: '0',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const toolStyle = style({
|
export const toolStyle = style({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '30px',
|
right: '30px',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function registerAffineSettingsCommands({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
//Font styles
|
// Font styles
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
registerAffineCommand({
|
registerAffineCommand({
|
||||||
id: 'affine:change-font-style-to-sans',
|
id: 'affine:change-font-style-to-sans',
|
||||||
@@ -144,7 +144,7 @@ export function registerAffineSettingsCommands({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
//Display Language
|
// Display Language
|
||||||
languagesList.forEach(language => {
|
languagesList.forEach(language => {
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
registerAffineCommand({
|
registerAffineCommand({
|
||||||
@@ -162,7 +162,7 @@ export function registerAffineSettingsCommands({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Layout Style
|
// Layout Style
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
registerAffineCommand({
|
registerAffineCommand({
|
||||||
id: `affine:change-client-border-style`,
|
id: `affine:change-client-border-style`,
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export const mainContainer = style({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: '100%',
|
minWidth: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${root}[data-client-border] &`]: {
|
[`${root}[data-client-border=true] &`]: {
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -27,68 +28,31 @@ export const editorContainer = style({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
zIndex: 0, // it will create stacking context to limit layer of child elements and be lower than after auto zIndex
|
zIndex: 0,
|
||||||
});
|
|
||||||
|
|
||||||
export const resizeHandle = style({
|
|
||||||
width: '1px',
|
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: 'var(--affine-border-color)',
|
|
||||||
selectors: {
|
|
||||||
'&[data-collapsed=true]': {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
[`${root}[data-client-border] &`]: {
|
|
||||||
width: '8px',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const resizeHandleInner = style({
|
|
||||||
height: '100%',
|
|
||||||
width: '10px', // this is the real hit box
|
|
||||||
position: 'absolute',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
zIndex: 10,
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
'::before': {
|
|
||||||
content: '""',
|
|
||||||
width: '0px',
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: '2px',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
},
|
|
||||||
selectors: {
|
|
||||||
[`${root}[data-client-border] &`]: {
|
|
||||||
transform: 'translateX(-1px)',
|
|
||||||
},
|
|
||||||
[`:is(${resizeHandle}:hover, ${resizeHandle}[data-resize-handle-active]) &::before`]:
|
|
||||||
{
|
|
||||||
width: '2px',
|
|
||||||
backgroundColor: 'var(--affine-primary-color)',
|
|
||||||
},
|
|
||||||
[`${resizeHandle}[data-resize-handle-active] &::before`]: {
|
|
||||||
width: '4px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sidebarContainer = style({
|
export const sidebarContainer = style({
|
||||||
transition: 'flex 0.2s ease-in-out',
|
display: 'flex',
|
||||||
|
flexShrink: 0,
|
||||||
|
height: '100%',
|
||||||
|
selectors: {
|
||||||
|
[`${root}[data-client-border=true] &`]: {
|
||||||
|
paddingLeft: 9,
|
||||||
|
},
|
||||||
|
[`${root}[data-client-border=false] &`]: {
|
||||||
|
borderLeft: '1px solid var(--affine-border-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sidebarContainerInner = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${resizeHandle}[data-resize-handle-active] + &`]: {
|
[`${root}[data-client-border=true] &`]: {
|
||||||
transition: 'none',
|
|
||||||
},
|
|
||||||
[`${root}[data-disable-animation] &`]: {
|
|
||||||
transition: 'none',
|
|
||||||
},
|
|
||||||
[`${root}[data-client-border] &`]: {
|
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
createTagFilter,
|
createTagFilter,
|
||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
} from '@affine/component/page-list';
|
} from '@affine/component/page-list';
|
||||||
|
import { ResizePanel } from '@affine/component/resize-panel';
|
||||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||||
import { SyncEngineStep } from '@affine/workspace/providers';
|
import { SyncEngineStep } from '@affine/workspace/providers';
|
||||||
@@ -21,16 +22,8 @@ import {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { PanelOnResize } from 'react-resizable-panels';
|
|
||||||
import {
|
|
||||||
type ImperativePanelHandle,
|
|
||||||
Panel,
|
|
||||||
PanelGroup,
|
|
||||||
PanelResizeHandle,
|
|
||||||
} from 'react-resizable-panels';
|
|
||||||
import { type LoaderFunction, useParams } from 'react-router-dom';
|
import { type LoaderFunction, useParams } from 'react-router-dom';
|
||||||
import type { Map as YMap } from 'yjs';
|
import type { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
@@ -56,6 +49,7 @@ import { DetailPageHeader, RightSidebarHeader } from './detail-page-header';
|
|||||||
import {
|
import {
|
||||||
EditorSidebar,
|
EditorSidebar,
|
||||||
editorSidebarOpenAtom,
|
editorSidebarOpenAtom,
|
||||||
|
editorSidebarResizingAtom,
|
||||||
editorSidebarStateAtom,
|
editorSidebarStateAtom,
|
||||||
editorSidebarWidthAtom,
|
editorSidebarWidthAtom,
|
||||||
} from './editor-sidebar';
|
} from './editor-sidebar';
|
||||||
@@ -67,16 +61,8 @@ interface DetailPageLayoutProps {
|
|||||||
sidebar: ReactNode;
|
sidebar: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable animation to avoid UI flash
|
const MIN_SIDEBAR_WIDTH = 320;
|
||||||
function useEnableAnimation() {
|
const MAX_SIDEBAR_WIDTH = 800;
|
||||||
const [enable, setEnable] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
setEnable(true);
|
|
||||||
}, 500);
|
|
||||||
}, []);
|
|
||||||
return enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: consider move to a shared place if we also want to reuse the layout for other routes
|
// todo: consider move to a shared place if we also want to reuse the layout for other routes
|
||||||
const DetailPageLayout = ({
|
const DetailPageLayout = ({
|
||||||
@@ -87,85 +73,39 @@ const DetailPageLayout = ({
|
|||||||
}: DetailPageLayoutProps): ReactElement => {
|
}: DetailPageLayoutProps): ReactElement => {
|
||||||
const sidebarState = useAtomValue(editorSidebarStateAtom);
|
const sidebarState = useAtomValue(editorSidebarStateAtom);
|
||||||
const setSidebarWidth = useSetAtom(editorSidebarWidthAtom);
|
const setSidebarWidth = useSetAtom(editorSidebarWidthAtom);
|
||||||
const setSidebarOpen = useSetAtom(editorSidebarOpenAtom);
|
|
||||||
const { clientBorder } = useAtomValue(appSettingAtom);
|
const { clientBorder } = useAtomValue(appSettingAtom);
|
||||||
|
const setResizing = useSetAtom(editorSidebarResizingAtom);
|
||||||
const sidebarRef = useRef<ImperativePanelHandle>(null);
|
const setOpen = useSetAtom(editorSidebarOpenAtom);
|
||||||
|
|
||||||
const onExpandSidebar = useCallback(() => {
|
|
||||||
setSidebarOpen(true);
|
|
||||||
}, [setSidebarOpen]);
|
|
||||||
|
|
||||||
const onCollapseSidebar = useCallback(() => {
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}, [setSidebarOpen]);
|
|
||||||
|
|
||||||
const onResize: PanelOnResize = useCallback(
|
|
||||||
e => {
|
|
||||||
if (e.sizePixels > 0) {
|
|
||||||
setSidebarWidth(e.sizePixels);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setSidebarWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const panelHandle = sidebarRef.current;
|
|
||||||
if (!panelHandle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sidebarState.isOpen) {
|
|
||||||
panelHandle.expand();
|
|
||||||
} else {
|
|
||||||
panelHandle.collapse();
|
|
||||||
}
|
|
||||||
}, [sidebarState.isOpen]);
|
|
||||||
|
|
||||||
const enableAnimation = useEnableAnimation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelGroup
|
<div
|
||||||
direction="horizontal"
|
|
||||||
className={styles.root}
|
className={styles.root}
|
||||||
dataAttributes={{
|
data-client-border={clientBorder && sidebarState.isOpen}
|
||||||
'data-disable-animation': !enableAnimation ? 'true' : undefined,
|
|
||||||
'data-client-border': clientBorder ? 'true' : undefined,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Panel id="editor" className={styles.mainContainer}>
|
<div className={styles.mainContainer}>
|
||||||
{header}
|
{header}
|
||||||
{main}
|
{main}
|
||||||
{footer}
|
{footer}
|
||||||
</Panel>
|
</div>
|
||||||
{sidebar ? (
|
{sidebar ? (
|
||||||
<>
|
<ResizePanel
|
||||||
<PanelResizeHandle
|
enableAnimation={false}
|
||||||
dataAttributes={{
|
resizeHandlePos="left"
|
||||||
'data-collapsed': !sidebarState.isOpen,
|
resizeHandleOffset={clientBorder ? 4 : 0}
|
||||||
}}
|
width={sidebarState.width}
|
||||||
className={styles.resizeHandle}
|
className={styles.sidebarContainer}
|
||||||
>
|
onResizing={setResizing}
|
||||||
<div className={styles.resizeHandleInner} />
|
resizing={sidebarState.resizing}
|
||||||
</PanelResizeHandle>
|
open={sidebarState.isOpen}
|
||||||
<Panel
|
onOpen={setOpen}
|
||||||
id="editor-sidebar"
|
onWidthChange={setSidebarWidth}
|
||||||
className={styles.sidebarContainer}
|
minWidth={MIN_SIDEBAR_WIDTH}
|
||||||
onResize={onResize}
|
maxWidth={MAX_SIDEBAR_WIDTH}
|
||||||
collapsedSizePixels={0}
|
>
|
||||||
collapsible
|
{sidebar}
|
||||||
onCollapse={onCollapseSidebar}
|
</ResizePanel>
|
||||||
onExpand={onExpandSidebar}
|
|
||||||
ref={sidebarRef}
|
|
||||||
defaultSizePixels={Math.max(sidebarState.width, 240)}
|
|
||||||
minSizePixels={sidebarState.isOpen ? 240 : 0}
|
|
||||||
maxSizePercentage={50}
|
|
||||||
>
|
|
||||||
{sidebar}
|
|
||||||
</Panel>
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
</PanelGroup>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,10 +202,10 @@ const DetailPageImpl = ({ page }: { page: Page }) => {
|
|||||||
footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
|
footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
|
||||||
sidebar={
|
sidebar={
|
||||||
!isInTrash ? (
|
!isInTrash ? (
|
||||||
<>
|
<div className={styles.sidebarContainerInner}>
|
||||||
<RightSidebarHeader />
|
<RightSidebarHeader />
|
||||||
<EditorSidebar />
|
<EditorSidebar />
|
||||||
</>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ export const extensions: EditorExtension[] = [outlineExtension];
|
|||||||
export interface EditorSidebarState {
|
export interface EditorSidebarState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
resizing: boolean;
|
||||||
activeExtension?: EditorExtension;
|
activeExtension?: EditorExtension;
|
||||||
extensions: EditorExtension[];
|
extensions: EditorExtension[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseStateAtom = atom<EditorSidebarState>({
|
const baseStateAtom = atom<EditorSidebarState>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
resizing: false,
|
||||||
width: 300, // todo: should be resizable
|
width: 300, // todo: should be resizable
|
||||||
activeExtension: extensions[0],
|
activeExtension: extensions[0],
|
||||||
extensions: extensions, // todo: maybe should be dynamic (by feature flag?)
|
extensions: extensions, // todo: maybe should be dynamic (by feature flag?)
|
||||||
@@ -27,6 +29,7 @@ const baseStateAtom = atom<EditorSidebarState>({
|
|||||||
export const editorSidebarStateAtom = atom(get => get(baseStateAtom));
|
export const editorSidebarStateAtom = atom(get => get(baseStateAtom));
|
||||||
|
|
||||||
const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen);
|
const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen);
|
||||||
|
const resizingAtom = selectAtom(baseStateAtom, state => state.resizing);
|
||||||
const activeExtensionAtom = selectAtom(
|
const activeExtensionAtom = selectAtom(
|
||||||
baseStateAtom,
|
baseStateAtom,
|
||||||
state => state.activeExtension
|
state => state.activeExtension
|
||||||
@@ -48,6 +51,16 @@ export const editorSidebarOpenAtom = atom(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// get/set sidebar resizing state
|
||||||
|
export const editorSidebarResizingAtom = atom(
|
||||||
|
get => get(resizingAtom),
|
||||||
|
(_, set, resizing: boolean) => {
|
||||||
|
set(baseStateAtom, prev => {
|
||||||
|
return { ...prev, resizing };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// get/set active extension
|
// get/set active extension
|
||||||
export const editorSidebarActiveExtensionAtom = atom(
|
export const editorSidebarActiveExtensionAtom = atom(
|
||||||
get => get(activeExtensionAtom),
|
get => get(activeExtensionAtom),
|
||||||
|
|||||||
@@ -7,10 +7,5 @@ export const EditorSidebar = () => {
|
|||||||
const sidebarState = useAtomValue(editorSidebarStateAtom);
|
const sidebarState = useAtomValue(editorSidebarStateAtom);
|
||||||
const Component = sidebarState.activeExtension?.Component;
|
const Component = sidebarState.activeExtension?.Component;
|
||||||
|
|
||||||
// do we need this?
|
|
||||||
if (!sidebarState.isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={styles.root}>{Component ? <Component /> : null}</div>;
|
return <div className={styles.root}>{Component ? <Component /> : null}</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const button = style({
|
|||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-active=true]': {
|
'&[data-active=true]': {
|
||||||
color: 'var(--affine-primary-color)',
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ test('Click resizer can close sidebar', async ({ page }) => {
|
|||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).toBeVisible();
|
await expect(sliderBarArea).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId('app-sidebar-resizer').click();
|
await page
|
||||||
|
.getByTestId('app-sidebar-wrapper')
|
||||||
|
.getByTestId('resize-handle')
|
||||||
|
.click();
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,7 +47,9 @@ test('Drag resizer can resize sidebar', async ({ page }) => {
|
|||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).toBeVisible();
|
await expect(sliderBarArea).toBeVisible();
|
||||||
|
|
||||||
const sliderResizer = page.getByTestId('app-sidebar-resizer');
|
const sliderResizer = page
|
||||||
|
.getByTestId('app-sidebar-wrapper')
|
||||||
|
.getByTestId('resize-handle');
|
||||||
await sliderResizer.hover();
|
await sliderResizer.hover();
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(400, 300, {
|
await page.mouse.move(400, 300, {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||||
import { expect, type Page } from '@playwright/test';
|
import { expect, type Locator, type Page } from '@playwright/test';
|
||||||
|
|
||||||
const openQuickSearchByShortcut = async (page: Page, checkVisible = true) => {
|
const openQuickSearchByShortcut = async (page: Page, checkVisible = true) => {
|
||||||
await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 }));
|
await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 }));
|
||||||
@@ -50,10 +50,9 @@ async function assertTitle(page: Page, text: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkElementIsInView(page: Page, searchText: string) {
|
async function checkElementIsInView(page: Page, locator: Locator) {
|
||||||
const element = page.getByText(searchText);
|
|
||||||
// check if the element is in view
|
// check if the element is in view
|
||||||
const elementRect = await element.boundingBox();
|
const elementRect = await locator.boundingBox();
|
||||||
const viewportHeight = page.viewportSize()?.height;
|
const viewportHeight = page.viewportSize()?.height;
|
||||||
|
|
||||||
if (!elementRect || !viewportHeight) {
|
if (!elementRect || !viewportHeight) {
|
||||||
@@ -401,7 +400,7 @@ test('can use cmdk to search page content and scroll to it, then the block will
|
|||||||
await page.keyboard.press('Enter', { delay: 10 });
|
await page.keyboard.press('Enter', { delay: 10 });
|
||||||
}
|
}
|
||||||
await page.keyboard.insertText('123456');
|
await page.keyboard.insertText('123456');
|
||||||
const textBlock = page.getByText('123456');
|
const textBlock = page.locator('editor-container').getByText('123456');
|
||||||
await expect(textBlock).toBeVisible();
|
await expect(textBlock).toBeVisible();
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
@@ -413,7 +412,10 @@ test('can use cmdk to search page content and scroll to it, then the block will
|
|||||||
]);
|
]);
|
||||||
await page.locator('[cmdk-item] [data-testid=cmdk-label]').first().click();
|
await page.locator('[cmdk-item] [data-testid=cmdk-label]').first().click();
|
||||||
await waitForScrollToFinish(page);
|
await waitForScrollToFinish(page);
|
||||||
const isVisitable = await checkElementIsInView(page, '123456');
|
const isVisitable = await checkElementIsInView(
|
||||||
|
page,
|
||||||
|
page.locator('editor-container').getByText('123456')
|
||||||
|
);
|
||||||
expect(isVisitable).toBe(true);
|
expect(isVisitable).toBe(true);
|
||||||
const selectionElement = page.locator('affine-block-selection');
|
const selectionElement = page.locator('affine-block-selection');
|
||||||
await expect(selectionElement).toBeVisible();
|
await expect(selectionElement).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user