mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 11:59:51 +08:00
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:
@@ -86,7 +86,7 @@ export function registerAffineSettingsCommands({
|
||||
})
|
||||
);
|
||||
|
||||
//Font styles
|
||||
// Font styles
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:change-font-style-to-sans',
|
||||
@@ -144,7 +144,7 @@ export function registerAffineSettingsCommands({
|
||||
})
|
||||
);
|
||||
|
||||
//Display Language
|
||||
// Display Language
|
||||
languagesList.forEach(language => {
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
@@ -162,7 +162,7 @@ export function registerAffineSettingsCommands({
|
||||
);
|
||||
});
|
||||
|
||||
//Layout Style
|
||||
// Layout Style
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `affine:change-client-border-style`,
|
||||
|
||||
@@ -13,9 +13,10 @@ export const mainContainer = style({
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
selectors: {
|
||||
[`${root}[data-client-border] &`]: {
|
||||
[`${root}[data-client-border=true] &`]: {
|
||||
borderRadius: '4px',
|
||||
},
|
||||
},
|
||||
@@ -27,68 +28,31 @@ export const editorContainer = style({
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
zIndex: 0, // it will create stacking context to limit layer of child elements and be lower than after auto zIndex
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
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',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
selectors: {
|
||||
[`${resizeHandle}[data-resize-handle-active] + &`]: {
|
||||
transition: 'none',
|
||||
},
|
||||
[`${root}[data-disable-animation] &`]: {
|
||||
transition: 'none',
|
||||
},
|
||||
[`${root}[data-client-border] &`]: {
|
||||
[`${root}[data-client-border=true] &`]: {
|
||||
borderRadius: '4px',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createTagFilter,
|
||||
useCollectionManager,
|
||||
} from '@affine/component/page-list';
|
||||
import { ResizePanel } from '@affine/component/resize-panel';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||
import { SyncEngineStep } from '@affine/workspace/providers';
|
||||
@@ -21,16 +22,8 @@ import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} 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 { Map as YMap } from 'yjs';
|
||||
|
||||
@@ -56,6 +49,7 @@ import { DetailPageHeader, RightSidebarHeader } from './detail-page-header';
|
||||
import {
|
||||
EditorSidebar,
|
||||
editorSidebarOpenAtom,
|
||||
editorSidebarResizingAtom,
|
||||
editorSidebarStateAtom,
|
||||
editorSidebarWidthAtom,
|
||||
} from './editor-sidebar';
|
||||
@@ -67,16 +61,8 @@ interface DetailPageLayoutProps {
|
||||
sidebar: ReactNode;
|
||||
}
|
||||
|
||||
// disable animation to avoid UI flash
|
||||
function useEnableAnimation() {
|
||||
const [enable, setEnable] = useState(false);
|
||||
useEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
setEnable(true);
|
||||
}, 500);
|
||||
}, []);
|
||||
return enable;
|
||||
}
|
||||
const MIN_SIDEBAR_WIDTH = 320;
|
||||
const MAX_SIDEBAR_WIDTH = 800;
|
||||
|
||||
// todo: consider move to a shared place if we also want to reuse the layout for other routes
|
||||
const DetailPageLayout = ({
|
||||
@@ -87,85 +73,39 @@ const DetailPageLayout = ({
|
||||
}: DetailPageLayoutProps): ReactElement => {
|
||||
const sidebarState = useAtomValue(editorSidebarStateAtom);
|
||||
const setSidebarWidth = useSetAtom(editorSidebarWidthAtom);
|
||||
const setSidebarOpen = useSetAtom(editorSidebarOpenAtom);
|
||||
const { clientBorder } = useAtomValue(appSettingAtom);
|
||||
|
||||
const sidebarRef = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
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();
|
||||
const setResizing = useSetAtom(editorSidebarResizingAtom);
|
||||
const setOpen = useSetAtom(editorSidebarOpenAtom);
|
||||
|
||||
return (
|
||||
<PanelGroup
|
||||
direction="horizontal"
|
||||
<div
|
||||
className={styles.root}
|
||||
dataAttributes={{
|
||||
'data-disable-animation': !enableAnimation ? 'true' : undefined,
|
||||
'data-client-border': clientBorder ? 'true' : undefined,
|
||||
}}
|
||||
data-client-border={clientBorder && sidebarState.isOpen}
|
||||
>
|
||||
<Panel id="editor" className={styles.mainContainer}>
|
||||
<div className={styles.mainContainer}>
|
||||
{header}
|
||||
{main}
|
||||
{footer}
|
||||
</Panel>
|
||||
</div>
|
||||
{sidebar ? (
|
||||
<>
|
||||
<PanelResizeHandle
|
||||
dataAttributes={{
|
||||
'data-collapsed': !sidebarState.isOpen,
|
||||
}}
|
||||
className={styles.resizeHandle}
|
||||
>
|
||||
<div className={styles.resizeHandleInner} />
|
||||
</PanelResizeHandle>
|
||||
<Panel
|
||||
id="editor-sidebar"
|
||||
className={styles.sidebarContainer}
|
||||
onResize={onResize}
|
||||
collapsedSizePixels={0}
|
||||
collapsible
|
||||
onCollapse={onCollapseSidebar}
|
||||
onExpand={onExpandSidebar}
|
||||
ref={sidebarRef}
|
||||
defaultSizePixels={Math.max(sidebarState.width, 240)}
|
||||
minSizePixels={sidebarState.isOpen ? 240 : 0}
|
||||
maxSizePercentage={50}
|
||||
>
|
||||
{sidebar}
|
||||
</Panel>
|
||||
</>
|
||||
<ResizePanel
|
||||
enableAnimation={false}
|
||||
resizeHandlePos="left"
|
||||
resizeHandleOffset={clientBorder ? 4 : 0}
|
||||
width={sidebarState.width}
|
||||
className={styles.sidebarContainer}
|
||||
onResizing={setResizing}
|
||||
resizing={sidebarState.resizing}
|
||||
open={sidebarState.isOpen}
|
||||
onOpen={setOpen}
|
||||
onWidthChange={setSidebarWidth}
|
||||
minWidth={MIN_SIDEBAR_WIDTH}
|
||||
maxWidth={MAX_SIDEBAR_WIDTH}
|
||||
>
|
||||
{sidebar}
|
||||
</ResizePanel>
|
||||
) : null}
|
||||
</PanelGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -262,10 +202,10 @@ const DetailPageImpl = ({ page }: { page: Page }) => {
|
||||
footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
|
||||
sidebar={
|
||||
!isInTrash ? (
|
||||
<>
|
||||
<div className={styles.sidebarContainerInner}>
|
||||
<RightSidebarHeader />
|
||||
<EditorSidebar />
|
||||
</>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -13,12 +13,14 @@ export const extensions: EditorExtension[] = [outlineExtension];
|
||||
export interface EditorSidebarState {
|
||||
isOpen: boolean;
|
||||
width: number;
|
||||
resizing: boolean;
|
||||
activeExtension?: EditorExtension;
|
||||
extensions: EditorExtension[];
|
||||
}
|
||||
|
||||
const baseStateAtom = atom<EditorSidebarState>({
|
||||
isOpen: false,
|
||||
resizing: false,
|
||||
width: 300, // todo: should be resizable
|
||||
activeExtension: extensions[0],
|
||||
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));
|
||||
|
||||
const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen);
|
||||
const resizingAtom = selectAtom(baseStateAtom, state => state.resizing);
|
||||
const activeExtensionAtom = selectAtom(
|
||||
baseStateAtom,
|
||||
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
|
||||
export const editorSidebarActiveExtensionAtom = atom(
|
||||
get => get(activeExtensionAtom),
|
||||
|
||||
-5
@@ -7,10 +7,5 @@ export const EditorSidebar = () => {
|
||||
const sidebarState = useAtomValue(editorSidebarStateAtom);
|
||||
const Component = sidebarState.activeExtension?.Component;
|
||||
|
||||
// do we need this?
|
||||
if (!sidebarState.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={styles.root}>{Component ? <Component /> : null}</div>;
|
||||
};
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export const button = style({
|
||||
|
||||
selectors: {
|
||||
'&[data-active=true]': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user