mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-24 03:26:31 +03:00
feat(core): resize and reorder split-view (#5994)
This commit is contained in:
parent
7b31363c51
commit
2275eee5b2
@ -11,6 +11,7 @@ export const menuContent = style({
|
||||
backgroundColor: cssVar('backgroundOverlayPanelColor'),
|
||||
boxShadow: cssVar('menuShadow'),
|
||||
userSelect: 'none',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
});
|
||||
export const menuItem = style({
|
||||
display: 'flex',
|
||||
|
@ -20,6 +20,7 @@ export class View {
|
||||
}),
|
||||
this.history.location
|
||||
);
|
||||
size = new LiveData(100);
|
||||
|
||||
header = createIsland();
|
||||
body = createIsland();
|
||||
@ -35,4 +36,8 @@ export class View {
|
||||
replace(path: To) {
|
||||
this.history.replace(path);
|
||||
}
|
||||
|
||||
setSize(size?: number) {
|
||||
this.size.next(size ?? 100);
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,48 @@ export class Workbench {
|
||||
return this.views.value[this.indexAt(positionIndex)];
|
||||
}
|
||||
|
||||
close(view: View) {
|
||||
if (this.views.value.length === 1) return;
|
||||
const index = this.views.value.indexOf(view);
|
||||
if (index === -1) return;
|
||||
const newViews = [...this.views.value];
|
||||
newViews.splice(index, 1);
|
||||
this.views.next(newViews);
|
||||
}
|
||||
|
||||
closeOthers(view: View) {
|
||||
view.size.next(100);
|
||||
this.views.next([view]);
|
||||
}
|
||||
|
||||
moveView(from: number, to: number) {
|
||||
const views = [...this.views.value];
|
||||
const [removed] = views.splice(from, 1);
|
||||
views.splice(to, 0, removed);
|
||||
this.views.next(views);
|
||||
this.active(to);
|
||||
}
|
||||
|
||||
/**
|
||||
* resize specified view and the next view
|
||||
* @param view
|
||||
* @param percent from 0 to 1
|
||||
* @returns
|
||||
*/
|
||||
resize(index: number, percent: number) {
|
||||
const view = this.views.value[index];
|
||||
const nextView = this.views.value[index + 1];
|
||||
if (!nextView) return;
|
||||
|
||||
const totalViewSize = this.views.value.reduce(
|
||||
(sum, v) => sum + v.size.value,
|
||||
0
|
||||
);
|
||||
const percentOfTotal = totalViewSize * percent;
|
||||
view.setSize(Number((view.size.value + percentOfTotal).toFixed(4)));
|
||||
nextView.setSize(Number((nextView.size.value - percentOfTotal).toFixed(4)));
|
||||
}
|
||||
|
||||
private indexAt(positionIndex: WorkbenchPosition): number {
|
||||
if (positionIndex === 'active') {
|
||||
return this.activeViewIndex.value;
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const indicator = style({
|
||||
width: 29,
|
||||
height: 14,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
color: cssVar('placeholderColor'),
|
||||
|
||||
selectors: {
|
||||
'&:hover, &:active, &[data-active="true"]': {
|
||||
color: cssVar('brandColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const indicatorInner = style({
|
||||
width: 15,
|
||||
height: 3,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'currentColor',
|
||||
});
|
@ -0,0 +1,28 @@
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, type HTMLAttributes, memo } from 'react';
|
||||
|
||||
import * as styles from './indicator.css';
|
||||
|
||||
export interface SplitViewMenuProps extends HTMLAttributes<HTMLDivElement> {
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export const SplitViewMenuIndicator = memo(
|
||||
forwardRef<HTMLDivElement, SplitViewMenuProps>(
|
||||
function SplitViewMenuIndicator(
|
||||
{ className, active, ...attrs }: SplitViewMenuProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-active={active}
|
||||
className={clsx(className, styles.indicator)}
|
||||
{...attrs}
|
||||
>
|
||||
<div className={styles.indicatorInner} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
@ -0,0 +1,209 @@
|
||||
import { Menu, MenuIcon, MenuItem, type MenuProps } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
CloseIcon,
|
||||
ExpandFullIcon,
|
||||
InsertLeftIcon,
|
||||
InsertRightIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import type { SetStateAction } from 'jotai';
|
||||
import {
|
||||
type Dispatch,
|
||||
type HTMLAttributes,
|
||||
memo,
|
||||
type PropsWithChildren,
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { View } from '../../entities/view';
|
||||
import { Workbench } from '../../entities/workbench';
|
||||
import { SplitViewMenuIndicator } from './indicator';
|
||||
import * as styles from './split-view.css';
|
||||
|
||||
export interface SplitViewPanelProps
|
||||
extends PropsWithChildren<HTMLAttributes<HTMLDivElement>> {
|
||||
view: View;
|
||||
resizeHandle?: React.ReactNode;
|
||||
setSlots?: Dispatch<
|
||||
SetStateAction<Record<string, RefObject<HTMLDivElement | null>>>
|
||||
>;
|
||||
}
|
||||
|
||||
export const SplitViewPanel = memo(function SplitViewPanel({
|
||||
children,
|
||||
view,
|
||||
setSlots,
|
||||
}: SplitViewPanelProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const size = useLiveData(view.size);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const workbench = useService(Workbench);
|
||||
const activeView = useLiveData(workbench.activeView);
|
||||
const views = useLiveData(workbench.views);
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
setNodeRef,
|
||||
setActivatorNodeRef,
|
||||
} = useSortable({ id: view.id, attributes: { role: 'group' } });
|
||||
|
||||
const isActive = activeView === view;
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setSlots?.(slots => ({ ...slots, [view.id]: ref }));
|
||||
}
|
||||
}, [setSlots, view.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
...assignInlineVars({ '--size': size.toString() }),
|
||||
}),
|
||||
[size]
|
||||
);
|
||||
const dragStyle = useMemo(
|
||||
() => ({
|
||||
transform: `translate3d(${transform?.x ?? 0}px, 0, 0)`,
|
||||
transition,
|
||||
}),
|
||||
[transform, transition]
|
||||
);
|
||||
const menuRootOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
open: menuOpen,
|
||||
onOpenChange: setMenuOpen,
|
||||
}) satisfies MenuProps['rootOptions'],
|
||||
[menuOpen]
|
||||
);
|
||||
const menuContentOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
align: 'center',
|
||||
}) satisfies MenuProps['contentOptions'],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={styles.splitViewPanel}
|
||||
data-is-dragging={isDragging}
|
||||
>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={dragStyle}
|
||||
className={styles.splitViewPanelDrag}
|
||||
{...attributes}
|
||||
>
|
||||
<div className={styles.splitViewPanelContent} ref={ref} />
|
||||
{views.length > 1 ? (
|
||||
<Menu
|
||||
contentOptions={menuContentOptions}
|
||||
items={<SplitViewMenu view={view} />}
|
||||
rootOptions={menuRootOptions}
|
||||
>
|
||||
<SplitViewMenuIndicator
|
||||
ref={setActivatorNodeRef}
|
||||
active={isDragging || isActive}
|
||||
className={styles.menuTrigger}
|
||||
{...listeners}
|
||||
/>
|
||||
</Menu>
|
||||
) : null}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface SplitViewMenuProps {
|
||||
view: View;
|
||||
}
|
||||
const SplitViewMenu = ({ view }: SplitViewMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workbench = useService(Workbench);
|
||||
const views = useLiveData(workbench.views);
|
||||
|
||||
const viewIndex = views.findIndex(v => v === view);
|
||||
|
||||
const handleClose = useCallback(
|
||||
() => workbench.close(view),
|
||||
[view, workbench]
|
||||
);
|
||||
const handleMoveLeft = useCallback(() => {
|
||||
workbench.moveView(viewIndex, viewIndex - 1);
|
||||
}, [viewIndex, workbench]);
|
||||
const handleMoveRight = useCallback(() => {
|
||||
workbench.moveView(viewIndex, viewIndex + 1);
|
||||
}, [viewIndex, workbench]);
|
||||
const handleFullScreen = useCallback(() => {
|
||||
workbench.closeOthers(view);
|
||||
}, [view, workbench]);
|
||||
|
||||
const CloseItem =
|
||||
views.length > 1 ? (
|
||||
<MenuItem
|
||||
preFix={<MenuIcon icon={<CloseIcon />} />}
|
||||
onClick={handleClose}
|
||||
>
|
||||
{t['com.affine.workbench.split-view-menu.close']()}
|
||||
</MenuItem>
|
||||
) : null;
|
||||
|
||||
const MoveLeftItem =
|
||||
viewIndex > 0 && views.length > 1 ? (
|
||||
<MenuItem
|
||||
onClick={handleMoveLeft}
|
||||
preFix={<MenuIcon icon={<InsertLeftIcon />} />}
|
||||
>
|
||||
{t['com.affine.workbench.split-view-menu.move-left']()}
|
||||
</MenuItem>
|
||||
) : null;
|
||||
|
||||
const FullScreenItem =
|
||||
views.length > 1 ? (
|
||||
<MenuItem
|
||||
onClick={handleFullScreen}
|
||||
preFix={<MenuIcon icon={<ExpandFullIcon />} />}
|
||||
>
|
||||
{t['com.affine.workbench.split-view-menu.full-screen']()}
|
||||
</MenuItem>
|
||||
) : null;
|
||||
|
||||
const MoveRightItem =
|
||||
viewIndex < views.length - 1 ? (
|
||||
<MenuItem
|
||||
onClick={handleMoveRight}
|
||||
preFix={<MenuIcon icon={<InsertRightIcon />} />}
|
||||
>
|
||||
{t['com.affine.workbench.split-view-menu.move-right']()}
|
||||
</MenuItem>
|
||||
) : null;
|
||||
return (
|
||||
<>
|
||||
{MoveRightItem}
|
||||
{MoveLeftItem}
|
||||
{FullScreenItem}
|
||||
{CloseItem}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import { type HTMLAttributes, useCallback } from 'react';
|
||||
|
||||
import * as styles from './split-view.css';
|
||||
|
||||
interface ResizeHandleProps extends HTMLAttributes<HTMLDivElement> {
|
||||
resizing: boolean;
|
||||
onResizeStart: () => void;
|
||||
onResizeEnd: () => void;
|
||||
onResizing: (offset: { x: number; y: number }) => void;
|
||||
}
|
||||
export const ResizeHandle = ({
|
||||
resizing,
|
||||
onResizing,
|
||||
onResizeStart,
|
||||
onResizeEnd,
|
||||
}: ResizeHandleProps) => {
|
||||
// TODO: touch support
|
||||
const onMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
onResizeStart();
|
||||
const prevPos = { x: e.clientX, y: e.clientY };
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
const dx = e.clientX - prevPos.x;
|
||||
const dy = e.clientY - prevPos.y;
|
||||
onResizing({ x: dx, y: dy });
|
||||
prevPos.x = e.clientX;
|
||||
prevPos.y = e.clientY;
|
||||
}
|
||||
|
||||
function onMouseUp(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
onResizeEnd();
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp, { once: true });
|
||||
},
|
||||
[onResizeEnd, onResizeStart, onResizing]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={onMouseDown}
|
||||
data-resizing={resizing || null}
|
||||
className={styles.resizeHandle}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,135 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
const gap = createVar();
|
||||
const borderRadius = createVar();
|
||||
|
||||
export const splitViewRoot = style({
|
||||
vars: {
|
||||
[gap]: '0px',
|
||||
[borderRadius]: '0px',
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius,
|
||||
gap,
|
||||
|
||||
selectors: {
|
||||
'&[data-client-border="true"]': {
|
||||
vars: {
|
||||
[gap]: '6px',
|
||||
[borderRadius]: '6px',
|
||||
},
|
||||
},
|
||||
'&[data-orientation="vertical"]': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const splitViewPanel = style({
|
||||
flexShrink: 0,
|
||||
flexGrow: 'var(--size, 1)',
|
||||
position: 'relative',
|
||||
borderRadius: 'inherit',
|
||||
|
||||
selectors: {
|
||||
'[data-orientation="vertical"] &': {
|
||||
height: 0,
|
||||
},
|
||||
'[data-orientation="horizontal"] &': {
|
||||
width: 0,
|
||||
},
|
||||
'[data-client-border="false"] &:not(:last-child):not([data-is-dragging="true"])':
|
||||
{
|
||||
borderRight: `1px solid ${cssVar('borderColor')}`,
|
||||
},
|
||||
'&[data-is-dragging="true"]': {
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const splitViewPanelDrag = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'inherit',
|
||||
|
||||
selectors: {
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
},
|
||||
|
||||
'[data-is-dragging="true"] &::after': {
|
||||
boxShadow: `inset 0 0 0 2px ${cssVar('brandColor')}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const splitViewPanelContent = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'inherit',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const resizeHandle = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: -5,
|
||||
width: 10,
|
||||
// to make sure it's above all-pages's header
|
||||
zIndex: 3,
|
||||
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'stretch',
|
||||
cursor: 'col-resize',
|
||||
|
||||
selectors: {
|
||||
'[data-client-border="true"] &': {
|
||||
right: `calc(-5px - ${gap} / 2)`,
|
||||
},
|
||||
|
||||
// horizontal
|
||||
'[data-orientation="horizontal"] &::before, [data-orientation="horizontal"] &::after':
|
||||
{
|
||||
content: '""',
|
||||
width: 2,
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
background: 'transparent',
|
||||
transition: 'background 0.1s',
|
||||
borderRadius: 10,
|
||||
},
|
||||
'[data-orientation="horizontal"] &[data-resizing]::before, [data-orientation="horizontal"] &[data-resizing]::after':
|
||||
{
|
||||
width: 3,
|
||||
},
|
||||
|
||||
'&:hover::before, &[data-resizing]::before': {
|
||||
background: cssVar('brandColor'),
|
||||
},
|
||||
'&:hover::after, &[data-resizing]::after': {
|
||||
boxShadow: `0px 12px 21px 4px ${cssVar('brandColor')}`,
|
||||
opacity: 0.15,
|
||||
},
|
||||
|
||||
// vertical
|
||||
// TODO
|
||||
},
|
||||
});
|
||||
|
||||
export const menuTrigger = style({
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: 3,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 10,
|
||||
});
|
@ -0,0 +1,141 @@
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { View } from '../../entities/view';
|
||||
import { Workbench } from '../../entities/workbench';
|
||||
import { SplitViewPanel } from './panel';
|
||||
import { ResizeHandle } from './resize-handle';
|
||||
import * as styles from './split-view.css';
|
||||
|
||||
export interface SplitViewProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* ⚠️ `vertical` orientation is not supported yet
|
||||
* @default 'horizontal'
|
||||
*/
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
views: View[];
|
||||
renderer: (item: View, index: number) => React.ReactNode;
|
||||
onMove?: (from: number, to: number) => void;
|
||||
}
|
||||
|
||||
type SlotsMap = Record<View['id'], RefObject<HTMLDivElement | null>>;
|
||||
// TODO: vertical orientation support
|
||||
export const SplitView = ({
|
||||
orientation = 'horizontal',
|
||||
className,
|
||||
views,
|
||||
renderer,
|
||||
onMove,
|
||||
...attrs
|
||||
}: SplitViewProps) => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [slots, setSlots] = useState<SlotsMap>({});
|
||||
const [resizingViewId, setResizingViewId] = useState<View['id'] | null>(null);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const workbench = useService(Workbench);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 2,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const onResizing = useCallback(
|
||||
(index: number, { x, y }: { x: number; y: number }) => {
|
||||
const rootEl = rootRef.current;
|
||||
if (!rootEl) return;
|
||||
|
||||
const rootRect = rootEl.getBoundingClientRect();
|
||||
const offset = orientation === 'horizontal' ? x : y;
|
||||
const total =
|
||||
orientation === 'horizontal' ? rootRect.width : rootRect.height;
|
||||
|
||||
const percent = offset / total;
|
||||
workbench.resize(index, percent);
|
||||
},
|
||||
[orientation, workbench]
|
||||
);
|
||||
|
||||
const resizeHandleRenderer = useCallback(
|
||||
(view: View, index: number) =>
|
||||
index < views.length - 1 ? (
|
||||
<ResizeHandle
|
||||
resizing={resizingViewId === view.id}
|
||||
onResizeStart={() => setResizingViewId(view.id)}
|
||||
onResizeEnd={() => setResizingViewId(null)}
|
||||
onResizing={dxy => onResizing(index, dxy)}
|
||||
/>
|
||||
) : null,
|
||||
[onResizing, resizingViewId, views.length]
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
// update order
|
||||
const fromIndex = views.findIndex(v => v.id === active.id);
|
||||
const toIndex = views.findIndex(v => v.id === over?.id);
|
||||
onMove?.(fromIndex, toIndex);
|
||||
}
|
||||
},
|
||||
[onMove, views]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={clsx(styles.splitViewRoot, className)}
|
||||
data-orientation={orientation}
|
||||
data-client-border={appSettings.clientBorder}
|
||||
{...attrs}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={views} strategy={horizontalListSortingStrategy}>
|
||||
{views.map((view, index) => (
|
||||
<SplitViewPanel view={view} key={view.id} setSlots={setSlots}>
|
||||
{resizeHandleRenderer(view, index)}
|
||||
</SplitViewPanel>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{views.map((view, index) => {
|
||||
const slot = slots[view.id]?.current;
|
||||
if (!slot) return null;
|
||||
return createPortal(
|
||||
renderer(view, index),
|
||||
slot,
|
||||
`portalToSplitViewPanel_${view.id}`
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -5,11 +5,6 @@ export const workbenchRootContainer = style({
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
selectors: {
|
||||
[`&[data-client-border="true"]`]: {
|
||||
gap: '8px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const workbenchViewContainer = style({
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { appSettingAtom } from '@toeverything/infra/atom';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@ -9,6 +7,7 @@ import type { View } from '../entities/view';
|
||||
import { Workbench } from '../entities/workbench';
|
||||
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
|
||||
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
|
||||
import { SplitView } from './split-view/split-view';
|
||||
import { ViewRoot } from './view-root';
|
||||
import * as styles from './workbench-root.css';
|
||||
|
||||
@ -29,17 +28,24 @@ export const WorkbenchRoot = () => {
|
||||
|
||||
useAdapter(workbench, basename);
|
||||
|
||||
const { clientBorder } = useAtomValue(appSettingAtom);
|
||||
const panelRenderer = useCallback((view: View, index: number) => {
|
||||
return <WorkbenchView key={view.id} view={view} index={index} />;
|
||||
}, []);
|
||||
|
||||
const onMove = useCallback(
|
||||
(from: number, to: number) => {
|
||||
workbench.moveView(from, to);
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
<SplitView
|
||||
className={styles.workbenchRootContainer}
|
||||
data-client-border={!!clientBorder}
|
||||
>
|
||||
{views.map((view, index) => (
|
||||
<WorkbenchView key={view.id} view={view} index={index} />
|
||||
))}
|
||||
</div>
|
||||
views={views}
|
||||
renderer={panelRenderer}
|
||||
onMove={onMove}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1142,5 +1142,9 @@
|
||||
"com.affine.share-page.footer.create-with": "Create with",
|
||||
"com.affine.share-page.footer.built-with": "Built with",
|
||||
"com.affine.share-page.footer.description": "Empower your sharing with AffiNE Cloud: One-click doc sharing",
|
||||
"com.affine.share-page.footer.get-started": "Get started for free"
|
||||
"com.affine.share-page.footer.get-started": "Get started for free",
|
||||
"com.affine.workbench.split-view-menu.close": "Close",
|
||||
"com.affine.workbench.split-view-menu.move-left": "Move Left",
|
||||
"com.affine.workbench.split-view-menu.move-right": "Move Right",
|
||||
"com.affine.workbench.split-view-menu.full-screen": "Full Screen"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user