feat(core): adjust split view ui (#6076)

This commit is contained in:
Cats Juice
2024-03-14 06:41:28 +00:00
parent b9fc848824
commit 7fdb1f2d97
14 changed files with 336 additions and 105 deletions

View File

@@ -2,6 +2,7 @@ import { toast } from '@affine/component';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { Workbench } from '@affine/core/modules/workbench';
import type { Collection, Filter } from '@affine/env/filter'; import type { Collection, Filter } from '@affine/env/filter';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -33,6 +34,7 @@ const usePageOperationsRenderer = () => {
currentWorkspace.docCollection currentWorkspace.docCollection
); );
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workbench = useService(Workbench);
const pageOperationsRenderer = useCallback( const pageOperationsRenderer = useCallback(
(page: DocMeta) => { (page: DocMeta) => {
@@ -48,6 +50,7 @@ const usePageOperationsRenderer = () => {
isPublic={!!page.isPublic} isPublic={!!page.isPublic}
onDisablePublicSharing={onDisablePublicSharing} onDisablePublicSharing={onDisablePublicSharing}
link={`/workspace/${currentWorkspace.id}/${page.id}`} link={`/workspace/${currentWorkspace.id}/${page.id}`}
onOpenInSplitView={() => workbench.openPage(page.id, { at: 'tail' })}
onDuplicate={() => { onDuplicate={() => {
duplicate(page.id, false); duplicate(page.id, false);
}} }}
@@ -70,7 +73,14 @@ const usePageOperationsRenderer = () => {
/> />
); );
}, },
[currentWorkspace.id, setTrashModal, t, toggleFavorite, duplicate] [
currentWorkspace.id,
workbench,
duplicate,
setTrashModal,
toggleFavorite,
t,
]
); );
return pageOperationsRenderer; return pageOperationsRenderer;

View File

@@ -7,6 +7,7 @@ import {
toast, toast,
Tooltip, Tooltip,
} from '@affine/component'; } from '@affine/component';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
@@ -20,6 +21,7 @@ import {
MoreVerticalIcon, MoreVerticalIcon,
OpenInNewIcon, OpenInNewIcon,
ResetIcon, ResetIcon,
SplitViewIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -45,6 +47,7 @@ export interface PageOperationCellProps {
onRemoveToTrash: () => void; onRemoveToTrash: () => void;
onDuplicate: () => void; onDuplicate: () => void;
onDisablePublicSharing: () => void; onDisablePublicSharing: () => void;
onOpenInSplitView: () => void;
} }
export const PageOperationCell = ({ export const PageOperationCell = ({
@@ -55,8 +58,10 @@ export const PageOperationCell = ({
onRemoveToTrash, onRemoveToTrash,
onDuplicate, onDuplicate,
onDisablePublicSharing, onDisablePublicSharing,
onOpenInSplitView,
}: PageOperationCellProps) => { }: PageOperationCellProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const { appSettings } = useAppSettingHelper();
const [openDisableShared, setOpenDisableShared] = useState(false); const [openDisableShared, setOpenDisableShared] = useState(false);
const OperationMenu = ( const OperationMenu = (
<> <>
@@ -84,6 +89,20 @@ export const PageOperationCell = ({
? t['com.affine.favoritePageOperation.remove']() ? t['com.affine.favoritePageOperation.remove']()
: t['com.affine.favoritePageOperation.add']()} : t['com.affine.favoritePageOperation.add']()}
</MenuItem> </MenuItem>
{environment.isDesktop && appSettings.enableMultiView ? (
<MenuItem
onClick={onOpenInSplitView}
preFix={
<MenuIcon>
<SplitViewIcon />
</MenuIcon>
}
>
{t['com.affine.workbench.split-view.page-menu-open']()}
</MenuItem>
) : null}
{!environment.isDesktop && ( {!environment.isDesktop && (
<Link <Link
className={styles.clearLinkStyle} className={styles.clearLinkStyle}

View File

@@ -4,9 +4,16 @@ import {
MenuItem, MenuItem,
type MenuItemProps, type MenuItemProps,
} from '@affine/component'; } from '@affine/component';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { Workbench } from '@affine/core/modules/workbench';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteIcon, EditIcon, FilterIcon } from '@blocksuite/icons'; import {
DeleteIcon,
EditIcon,
FilterIcon,
SplitViewIcon,
} from '@blocksuite/icons';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { import {
type PropsWithChildren, type PropsWithChildren,
@@ -35,7 +42,9 @@ export const CollectionOperations = ({
config: AllPageListConfig; config: AllPageListConfig;
openRenameModal?: () => void; openRenameModal?: () => void;
}>) => { }>) => {
const { appSettings } = useAppSettingHelper();
const service = useService(CollectionService); const service = useService(CollectionService);
const workbench = useService(Workbench);
const { open: openEditCollectionModal, node: editModal } = const { open: openEditCollectionModal, node: editModal } =
useEditCollection(config); useEditCollection(config);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@@ -71,6 +80,10 @@ export const CollectionOperations = ({
}); });
}, [openEditCollectionModal, collection, service]); }, [openEditCollectionModal, collection, service]);
const openCollectionSplitView = useCallback(() => {
workbench.openCollection(collection.id, { at: 'tail' });
}, [collection.id, workbench]);
const actions = useMemo< const actions = useMemo<
Array< Array<
| { | {
@@ -104,6 +117,19 @@ export const CollectionOperations = ({
name: t['com.affine.collection.menu.edit'](), name: t['com.affine.collection.menu.edit'](),
click: showEdit, click: showEdit,
}, },
...(appSettings.enableMultiView
? [
{
icon: (
<MenuIcon>
<SplitViewIcon />
</MenuIcon>
),
name: t['com.affine.workbench.split-view.page-menu-open'](),
click: openCollectionSplitView,
},
]
: []),
{ {
element: <div key="divider" className={styles.divider}></div>, element: <div key="divider" className={styles.divider}></div>,
}, },
@@ -120,7 +146,16 @@ export const CollectionOperations = ({
type: 'danger', type: 'danger',
}, },
], ],
[t, showEditName, showEdit, service, info, collection.id] [
t,
showEditName,
showEdit,
appSettings.enableMultiView,
openCollectionSplitView,
service,
info,
collection.id,
]
); );
return ( return (
<> <>

View File

@@ -4,6 +4,7 @@ import {
type MenuItemProps, type MenuItemProps,
MenuSeparator, MenuSeparator,
} from '@affine/component'; } from '@affine/component';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
DeleteIcon, DeleteIcon,
@@ -11,6 +12,7 @@ import {
FavoriteIcon, FavoriteIcon,
FilterMinusIcon, FilterMinusIcon,
LinkedPageIcon, LinkedPageIcon,
SplitViewIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { type ReactElement, useMemo } from 'react'; import { type ReactElement, useMemo } from 'react';
@@ -24,6 +26,7 @@ type OperationItemsProps = {
onAddLinkedPage: () => void; onAddLinkedPage: () => void;
onRemoveFromFavourites?: () => void; onRemoveFromFavourites?: () => void;
onDelete: () => void; onDelete: () => void;
onOpenInSplitView: () => void;
}; };
export const OperationItems = ({ export const OperationItems = ({
@@ -35,7 +38,9 @@ export const OperationItems = ({
onAddLinkedPage, onAddLinkedPage,
onRemoveFromFavourites, onRemoveFromFavourites,
onDelete, onDelete,
onOpenInSplitView,
}: OperationItemsProps) => { }: OperationItemsProps) => {
const { appSettings } = useAppSettingHelper();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const actions = useMemo< const actions = useMemo<
Array< Array<
@@ -81,9 +86,6 @@ export const OperationItems = ({
name: t['Remove from favorites'](), name: t['Remove from favorites'](),
click: onRemoveFromFavourites, click: onRemoveFromFavourites,
}, },
{
element: <MenuSeparator />,
},
] ]
: []), : []),
...(inAllowList && onRemoveFromAllowList ...(inAllowList && onRemoveFromAllowList
@@ -97,18 +99,27 @@ export const OperationItems = ({
name: t['Remove special filter'](), name: t['Remove special filter'](),
click: onRemoveFromAllowList, click: onRemoveFromAllowList,
}, },
{
element: <MenuSeparator />,
},
] ]
: []), : []),
...(isReferencePage
...(appSettings.enableMultiView
? [ ? [
// open split view
{ {
element: <MenuSeparator />, icon: (
<MenuIcon>
<SplitViewIcon />
</MenuIcon>
),
name: t['com.affine.workbench.split-view.page-menu-open'](),
click: onOpenInSplitView,
}, },
] ]
: []), : []),
{
element: <MenuSeparator />,
},
{ {
icon: ( icon: (
<MenuIcon> <MenuIcon>
@@ -121,14 +132,16 @@ export const OperationItems = ({
}, },
], ],
[ [
t,
onRename, onRename,
onAddLinkedPage, onAddLinkedPage,
inFavorites, inFavorites,
onRemoveFromFavourites, onRemoveFromFavourites,
isReferencePage, isReferencePage,
t,
inAllowList, inAllowList,
onRemoveFromAllowList, onRemoveFromAllowList,
appSettings.enableMultiView,
onOpenInSplitView,
onDelete, onDelete,
] ]
); );

View File

@@ -1,9 +1,11 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { IconButton } from '@affine/component/ui/button'; import { IconButton } from '@affine/component/ui/button';
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { Workbench } from '@affine/core/modules/workbench';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { MoreHorizontalIcon } from '@blocksuite/icons'; import { MoreHorizontalIcon } from '@blocksuite/icons';
import type { DocCollection } from '@blocksuite/store'; import type { DocCollection } from '@blocksuite/store';
import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
@@ -37,6 +39,7 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
const { createLinkedPage } = usePageHelper(docCollection); const { createLinkedPage } = usePageHelper(docCollection);
const { setTrashModal } = useTrashModalHelper(docCollection); const { setTrashModal } = useTrashModalHelper(docCollection);
const { removeFromFavorite } = useBlockSuiteMetaHelper(docCollection); const { removeFromFavorite } = useBlockSuiteMetaHelper(docCollection);
const workbench = useService(Workbench);
const handleRename = useCallback(() => { const handleRename = useCallback(() => {
setRenameModalOpen?.(); setRenameModalOpen?.();
@@ -64,6 +67,10 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
removeFromAllowList?.(pageId); removeFromAllowList?.(pageId);
}, [pageId, removeFromAllowList]); }, [pageId, removeFromAllowList]);
const handleOpenInSplitView = useCallback(() => {
workbench.openPage(pageId, { at: 'tail' });
}, [pageId, workbench]);
return ( return (
<Menu <Menu
items={ items={
@@ -73,6 +80,7 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
onRemoveFromAllowList={handleRemoveFromAllowList} onRemoveFromAllowList={handleRemoveFromAllowList}
onRemoveFromFavourites={handleRemoveFromFavourites} onRemoveFromFavourites={handleRemoveFromFavourites}
onRename={handleRename} onRename={handleRename}
onOpenInSplitView={handleOpenInSplitView}
inAllowList={inAllowList} inAllowList={inAllowList}
inFavorites={inFavorites} inFavorites={inFavorites}
isReferencePage={isReferencePage} isReferencePage={isReferencePage}

View File

@@ -8,15 +8,24 @@ export const sidebarContainerInner = style({
overflow: 'hidden', overflow: 'hidden',
height: '100%', height: '100%',
width: '100%', width: '100%',
borderRadius: 'inherit',
selectors: {
['[data-client-border=true][data-is-floating="true"] &']: {
boxShadow: cssVar('shadow3'),
border: `1px solid ${cssVar('borderColor')}`,
},
},
}); });
export const sidebarContainer = style({ export const sidebarContainer = style({
display: 'flex', display: 'flex',
flexShrink: 0, flexShrink: 0,
height: '100%', height: '100%',
right: 0,
selectors: { selectors: {
[`&[data-client-border=true]`]: { [`&[data-client-border=true]`]: {
paddingLeft: 9, paddingLeft: 8,
borderRadius: 6,
}, },
[`&[data-client-border=false]`]: { [`&[data-client-border=false]`]: {
borderLeft: `1px solid ${cssVar('borderColor')}`, borderLeft: `1px solid ${cssVar('borderColor')}`,

View File

@@ -1,9 +1,10 @@
import { ResizePanel } from '@affine/component/resize-panel'; import { ResizePanel } from '@affine/component/resize-panel';
import { appSidebarOpenAtom } from '@affine/core/components/app-sidebar';
import { appSettingAtom } from '@toeverything/infra/atom'; import { appSettingAtom } from '@toeverything/infra/atom';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { RightSidebar } from '../entities/right-sidebar'; import { RightSidebar } from '../entities/right-sidebar';
import * as styles from './container.css'; import * as styles from './container.css';
@@ -20,6 +21,18 @@ export const RightSidebarContainer = () => {
const frontView = useLiveData(rightSidebar.front); const frontView = useLiveData(rightSidebar.front);
const open = useLiveData(rightSidebar.isOpen) && frontView !== undefined; const open = useLiveData(rightSidebar.isOpen) && frontView !== undefined;
const [floating, setFloating] = useState(false);
const appSidebarOpened = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
const onResize = () =>
setFloating(!!(window.innerWidth < 1200 && appSidebarOpened));
onResize();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, [appSidebarOpened]);
const handleOpenChange = useCallback( const handleOpenChange = useCallback(
(open: boolean) => { (open: boolean) => {
@@ -38,8 +51,9 @@ export const RightSidebarContainer = () => {
return ( return (
<ResizePanel <ResizePanel
floating={floating}
resizeHandlePos="left" resizeHandlePos="left"
resizeHandleOffset={clientBorder ? 4 : 0} resizeHandleOffset={clientBorder ? 3.5 : 0}
width={width} width={width}
resizing={resizing} resizing={resizing}
onResizing={setResizing} onResizing={setResizing}

View File

@@ -5,7 +5,12 @@ import { combineLatest, map, switchMap } from 'rxjs';
import { View } from './view'; import { View } from './view';
export type WorkbenchPosition = 'beside' | 'active' | number; export type WorkbenchPosition = 'beside' | 'active' | 'head' | 'tail' | number;
interface WorkbenchOpenOptions {
at?: WorkbenchPosition;
replaceHistory?: boolean;
}
export class Workbench { export class Workbench {
readonly views = new LiveData([new View()]); readonly views = new LiveData([new View()]);
@@ -37,10 +42,7 @@ export class Workbench {
open( open(
to: To, to: To,
{ { at = 'active', replaceHistory = false }: WorkbenchOpenOptions = {}
at = 'active',
replaceHistory = false,
}: { at?: WorkbenchPosition; replaceHistory?: boolean } = {}
) { ) {
let view = this.viewAt(at); let view = this.viewAt(at);
if (!view) { if (!view) {
@@ -58,32 +60,32 @@ export class Workbench {
} }
} }
openPage(pageId: string) { openPage(pageId: string, options?: WorkbenchOpenOptions) {
this.open(`/${pageId}`); this.open(`/${pageId}`, options);
} }
openCollections() { openCollections(options?: WorkbenchOpenOptions) {
this.open('/collection'); this.open('/collection', options);
} }
openCollection(collectionId: string) { openCollection(collectionId: string, options?: WorkbenchOpenOptions) {
this.open(`/collection/${collectionId}`); this.open(`/collection/${collectionId}`, options);
} }
openAll() { openAll(options?: WorkbenchOpenOptions) {
this.open('/all'); this.open('/all', options);
} }
openTrash() { openTrash(options?: WorkbenchOpenOptions) {
this.open('/trash'); this.open('/trash', options);
} }
openTags() { openTags(options?: WorkbenchOpenOptions) {
this.open('/tag'); this.open('/tag', options);
} }
openTag(tagId: string) { openTag(tagId: string, options?: WorkbenchOpenOptions) {
this.open(`/tag/${tagId}`); this.open(`/tag/${tagId}`, options);
} }
viewAt(positionIndex: WorkbenchPosition): View | undefined { viewAt(positionIndex: WorkbenchPosition): View | undefined {
@@ -96,12 +98,16 @@ export class Workbench {
if (index === -1) return; if (index === -1) return;
const newViews = [...this.views.value]; const newViews = [...this.views.value];
newViews.splice(index, 1); newViews.splice(index, 1);
if (index !== 0) {
this.active(index - 1);
}
this.views.next(newViews); this.views.next(newViews);
} }
closeOthers(view: View) { closeOthers(view: View) {
view.size.next(100); view.size.next(100);
this.views.next([view]); this.views.next([view]);
this.active(0);
} }
moveView(from: number, to: number) { moveView(from: number, to: number) {
@@ -128,8 +134,15 @@ export class Workbench {
0 0
); );
const percentOfTotal = totalViewSize * percent; const percentOfTotal = totalViewSize * percent;
view.setSize(Number((view.size.value + percentOfTotal).toFixed(4))); const newSize = Number((view.size.value + percentOfTotal).toFixed(4));
nextView.setSize(Number((nextView.size.value - percentOfTotal).toFixed(4))); const newNextSize = Number(
(nextView.size.value - percentOfTotal).toFixed(4)
);
// TODO: better strategy to limit size
if (newSize / totalViewSize < 0.2 || newNextSize / totalViewSize < 0.2)
return;
view.setSize(newSize);
nextView.setSize(newNextSize);
} }
private indexAt(positionIndex: WorkbenchPosition): number { private indexAt(positionIndex: WorkbenchPosition): number {
@@ -139,6 +152,12 @@ export class Workbench {
if (positionIndex === 'beside') { if (positionIndex === 'beside') {
return this.activeViewIndex.value + 1; return this.activeViewIndex.value + 1;
} }
if (positionIndex === 'head') {
return 0;
}
if (positionIndex === 'tail') {
return this.views.value.length;
}
return positionIndex; return positionIndex;
} }
} }

View File

@@ -1,13 +1,35 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const indicatorWrapper = style({
position: 'absolute',
zIndex: 4,
top: 0,
left: '50%',
transform: 'translateX(-50%)',
width: '50%',
maxWidth: 300,
minWidth: 120,
height: 15,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
['WebkitAppRegion' as string]: 'no-drag',
});
export const menuTrigger = style({
position: 'absolute',
width: 0,
height: 0,
pointerEvents: 'none',
});
export const indicator = style({ export const indicator = style({
width: 29, width: 29,
height: 14, height: 15,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer', cursor: 'grab',
['WebkitAppRegion' as string]: 'no-drag', ['WebkitAppRegion' as string]: 'no-drag',
color: cssVar('placeholderColor'), color: cssVar('placeholderColor'),
@@ -19,8 +41,16 @@ export const indicator = style({
}); });
export const indicatorInner = style({ export const indicatorInner = style({
width: 15, width: 16,
height: 3, height: 3,
borderRadius: 10, borderRadius: 10,
backgroundColor: 'currentColor', backgroundColor: 'currentColor',
transition: 'all 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
selectors: {
'[data-is-dragging="true"] &': {
width: 24,
height: 2,
},
},
}); });

View File

@@ -1,23 +1,61 @@
import { Menu, type MenuProps } from '@affine/component';
import clsx from 'clsx'; import clsx from 'clsx';
import { forwardRef, type HTMLAttributes, memo } from 'react'; import {
forwardRef,
type HTMLAttributes,
memo,
type MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
import * as styles from './indicator.css'; import * as styles from './indicator.css';
export interface SplitViewMenuProps extends HTMLAttributes<HTMLDivElement> { export interface SplitViewMenuProps extends HTMLAttributes<HTMLDivElement> {
active?: boolean; active?: boolean;
open?: boolean;
onOpenMenu?: () => void;
setPressed: (v: boolean) => void;
} }
export const SplitViewMenuIndicator = memo( export const SplitViewMenuIndicator = memo(
forwardRef<HTMLDivElement, SplitViewMenuProps>( forwardRef<HTMLDivElement, SplitViewMenuProps>(
function SplitViewMenuIndicator( function SplitViewMenuIndicator(
{ className, active, ...attrs }: SplitViewMenuProps, {
className,
active,
open,
setPressed,
onOpenMenu,
...attrs
}: SplitViewMenuProps,
ref ref
) { ) {
// dnd's `isDragging` changes after mouseDown and mouseMoved
const onMouseDown = useCallback(() => {
const t = setTimeout(() => setPressed(true), 100);
window.addEventListener(
'mouseup',
() => {
clearTimeout(t);
setPressed(false);
},
{ once: true }
);
}, [setPressed]);
const onClick: MouseEventHandler = useCallback(() => {
!open && onOpenMenu?.();
}, [onOpenMenu, open]);
return ( return (
<div <div
ref={ref} ref={ref}
data-active={active} data-active={active}
className={clsx(className, styles.indicator)} className={clsx(className, styles.indicator)}
onClick={onClick}
onMouseDown={onMouseDown}
{...attrs} {...attrs}
> >
<div className={styles.indicatorInner} /> <div className={styles.indicatorInner} />
@@ -26,3 +64,66 @@ export const SplitViewMenuIndicator = memo(
} }
) )
); );
interface SplitViewIndicatorProps extends HTMLAttributes<HTMLDivElement> {
isDragging?: boolean;
isActive?: boolean;
menuItems?: React.ReactNode;
// import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities' is not allowed
listeners?: any;
setPressed?: (pressed: boolean) => void;
}
export const SplitViewIndicator = ({
isDragging,
isActive,
menuItems,
listeners,
setPressed,
}: SplitViewIndicatorProps) => {
const active = isActive || isDragging;
const [menuOpen, setMenuOpen] = useState(false);
// prevent menu from opening when dragging
const setOpenMenuManually = useCallback((open: boolean) => {
if (open) return;
setMenuOpen(open);
}, []);
const openMenu = useCallback(() => {
setMenuOpen(true);
}, []);
const menuRootOptions = useMemo(
() =>
({
open: menuOpen,
onOpenChange: setOpenMenuManually,
}) satisfies MenuProps['rootOptions'],
[menuOpen, setOpenMenuManually]
);
const menuContentOptions = useMemo(
() =>
({
align: 'center',
}) satisfies MenuProps['contentOptions'],
[]
);
return (
<div data-is-dragging={isDragging} className={styles.indicatorWrapper}>
<Menu
contentOptions={menuContentOptions}
items={menuItems}
rootOptions={menuRootOptions}
>
<div className={styles.menuTrigger} />
</Menu>
<SplitViewMenuIndicator
open={menuOpen}
onOpenMenu={openMenu}
active={active}
setPressed={setPressed}
{...listeners}
/>
</div>
);
};

View File

@@ -1,10 +1,10 @@
import { Menu, MenuIcon, MenuItem, type MenuProps } from '@affine/component'; import { MenuIcon, MenuItem } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
CloseIcon, ExpandCloseIcon,
ExpandFullIcon, KeepThisOneIcon,
InsertLeftIcon, MoveToLeftIcon,
InsertRightIcon, MoveToRightIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useSortable } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
@@ -26,7 +26,7 @@ import {
import type { View } from '../../entities/view'; import type { View } from '../../entities/view';
import { Workbench } from '../../entities/workbench'; import { Workbench } from '../../entities/workbench';
import { SplitViewMenuIndicator } from './indicator'; import { SplitViewIndicator } from './indicator';
import * as styles from './split-view.css'; import * as styles from './split-view.css';
export interface SplitViewPanelProps export interface SplitViewPanelProps
@@ -43,22 +43,24 @@ export const SplitViewPanel = memo(function SplitViewPanel({
view, view,
setSlots, setSlots,
}: SplitViewPanelProps) { }: SplitViewPanelProps) {
const [indicatorPressed, setIndicatorPressed] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const size = useLiveData(view.size); const size = useLiveData(view.size);
const [menuOpen, setMenuOpen] = useState(false);
const workbench = useService(Workbench); const workbench = useService(Workbench);
const activeView = useLiveData(workbench.activeView); const activeView = useLiveData(workbench.activeView);
const views = useLiveData(workbench.views); const views = useLiveData(workbench.views);
const isLast = views[views.length - 1] === view;
const { const {
attributes, attributes,
listeners, listeners,
transform, transform,
transition, transition,
isDragging, isDragging: dndIsDragging,
setNodeRef, setNodeRef,
setActivatorNodeRef,
} = useSortable({ id: view.id, attributes: { role: 'group' } }); } = useSortable({ id: view.id, attributes: { role: 'group' } });
const isDragging = dndIsDragging || indicatorPressed;
const isActive = activeView === view; const isActive = activeView === view;
useEffect(() => { useEffect(() => {
@@ -67,12 +69,6 @@ export const SplitViewPanel = memo(function SplitViewPanel({
} }
}, [setSlots, view.id]); }, [setSlots, view.id]);
useEffect(() => {
if (isDragging) {
setMenuOpen(false);
}
}, [isDragging]);
const style = useMemo( const style = useMemo(
() => ({ () => ({
...assignInlineVars({ '--size': size.toString() }), ...assignInlineVars({ '--size': size.toString() }),
@@ -86,27 +82,14 @@ export const SplitViewPanel = memo(function SplitViewPanel({
}), }),
[transform, transition] [transform, transition]
); );
const menuRootOptions = useMemo(
() =>
({
open: menuOpen,
onOpenChange: setMenuOpen,
}) satisfies MenuProps['rootOptions'],
[menuOpen]
);
const menuContentOptions = useMemo(
() =>
({
align: 'center',
}) satisfies MenuProps['contentOptions'],
[]
);
return ( return (
<div <div
style={style} style={style}
className={styles.splitViewPanel} className={styles.splitViewPanel}
data-is-dragging={isDragging} data-is-dragging={isDragging}
data-is-active={isActive && views.length > 1}
data-is-last={isLast}
> >
<div <div
ref={setNodeRef} ref={setNodeRef}
@@ -116,18 +99,13 @@ export const SplitViewPanel = memo(function SplitViewPanel({
> >
<div className={styles.splitViewPanelContent} ref={ref} /> <div className={styles.splitViewPanelContent} ref={ref} />
{views.length > 1 ? ( {views.length > 1 ? (
<Menu <SplitViewIndicator
contentOptions={menuContentOptions} listeners={listeners}
items={<SplitViewMenu view={view} />} isDragging={isDragging}
rootOptions={menuRootOptions} isActive={isActive}
> menuItems={<SplitViewMenu view={view} />}
<SplitViewMenuIndicator setPressed={setIndicatorPressed}
ref={setActivatorNodeRef} />
active={isDragging || isActive}
className={styles.menuTrigger}
{...listeners}
/>
</Menu>
) : null} ) : null}
</div> </div>
{children} {children}
@@ -135,10 +113,7 @@ export const SplitViewPanel = memo(function SplitViewPanel({
); );
}); });
interface SplitViewMenuProps { const SplitViewMenu = ({ view }: { view: View }) => {
view: View;
}
const SplitViewMenu = ({ view }: SplitViewMenuProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workbench = useService(Workbench); const workbench = useService(Workbench);
const views = useLiveData(workbench.views); const views = useLiveData(workbench.views);
@@ -155,14 +130,14 @@ const SplitViewMenu = ({ view }: SplitViewMenuProps) => {
const handleMoveRight = useCallback(() => { const handleMoveRight = useCallback(() => {
workbench.moveView(viewIndex, viewIndex + 1); workbench.moveView(viewIndex, viewIndex + 1);
}, [viewIndex, workbench]); }, [viewIndex, workbench]);
const handleFullScreen = useCallback(() => { const handleCloseOthers = useCallback(() => {
workbench.closeOthers(view); workbench.closeOthers(view);
}, [view, workbench]); }, [view, workbench]);
const CloseItem = const CloseItem =
views.length > 1 ? ( views.length > 1 ? (
<MenuItem <MenuItem
preFix={<MenuIcon icon={<CloseIcon />} />} preFix={<MenuIcon icon={<ExpandCloseIcon />} />}
onClick={handleClose} onClick={handleClose}
> >
{t['com.affine.workbench.split-view-menu.close']()} {t['com.affine.workbench.split-view-menu.close']()}
@@ -173,7 +148,7 @@ const SplitViewMenu = ({ view }: SplitViewMenuProps) => {
viewIndex > 0 && views.length > 1 ? ( viewIndex > 0 && views.length > 1 ? (
<MenuItem <MenuItem
onClick={handleMoveLeft} onClick={handleMoveLeft}
preFix={<MenuIcon icon={<InsertLeftIcon />} />} preFix={<MenuIcon icon={<MoveToLeftIcon />} />}
> >
{t['com.affine.workbench.split-view-menu.move-left']()} {t['com.affine.workbench.split-view-menu.move-left']()}
</MenuItem> </MenuItem>
@@ -182,10 +157,10 @@ const SplitViewMenu = ({ view }: SplitViewMenuProps) => {
const FullScreenItem = const FullScreenItem =
views.length > 1 ? ( views.length > 1 ? (
<MenuItem <MenuItem
onClick={handleFullScreen} onClick={handleCloseOthers}
preFix={<MenuIcon icon={<ExpandFullIcon />} />} preFix={<MenuIcon icon={<KeepThisOneIcon />} />}
> >
{t['com.affine.workbench.split-view-menu.full-screen']()} {t['com.affine.workbench.split-view-menu.keep-this-one']()}
</MenuItem> </MenuItem>
) : null; ) : null;
@@ -193,7 +168,7 @@ const SplitViewMenu = ({ view }: SplitViewMenuProps) => {
viewIndex < views.length - 1 ? ( viewIndex < views.length - 1 ? (
<MenuItem <MenuItem
onClick={handleMoveRight} onClick={handleMoveRight}
preFix={<MenuIcon icon={<InsertRightIcon />} />} preFix={<MenuIcon icon={<MoveToRightIcon />} />}
> >
{t['com.affine.workbench.split-view-menu.move-right']()} {t['com.affine.workbench.split-view-menu.move-right']()}
</MenuItem> </MenuItem>

View File

@@ -17,7 +17,7 @@ export const splitViewRoot = style({
selectors: { selectors: {
'&[data-client-border="true"]': { '&[data-client-border="true"]': {
vars: { vars: {
[gap]: '6px', [gap]: '8px',
[borderRadius]: '6px', [borderRadius]: '6px',
}, },
}, },
@@ -40,7 +40,7 @@ export const splitViewPanel = style({
'[data-orientation="horizontal"] &': { '[data-orientation="horizontal"] &': {
width: 0, width: 0,
}, },
'[data-client-border="false"] &:not(:last-child):not([data-is-dragging="true"])': '[data-client-border="false"] &:not([data-is-last="true"]):not([data-is-dragging="true"])':
{ {
borderRight: `1px solid ${cssVar('borderColor')}`, borderRight: `1px solid ${cssVar('borderColor')}`,
}, },
@@ -63,6 +63,10 @@ export const splitViewPanelDrag = style({
borderRadius: 'inherit', borderRadius: 'inherit',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 10, zIndex: 10,
// animate border in/out
boxShadow: `inset 0 0 0 0 transparent`,
transition: 'box-shadow 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
}, },
'[data-is-dragging="true"] &::after': { '[data-is-dragging="true"] &::after': {
@@ -125,11 +129,3 @@ export const resizeHandle = style({
// TODO // TODO
}, },
}); });
export const menuTrigger = style({
position: 'absolute',
left: '50%',
top: 3,
transform: 'translateX(-50%)',
zIndex: 10,
});

View File

@@ -58,7 +58,7 @@ export const SplitView = ({
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { useSensor(PointerSensor, {
activationConstraint: { activationConstraint: {
distance: 2, distance: 0,
}, },
}) })
); );

View File

@@ -1163,5 +1163,7 @@
"com.affine.delete-tags.count": "{{count}} tag deleted", "com.affine.delete-tags.count": "{{count}} tag deleted",
"com.affine.delete-tags.count_one": "{{count}} tag deleted", "com.affine.delete-tags.count_one": "{{count}} tag deleted",
"com.affine.delete-tags.count_other": "{{count}} tags deleted", "com.affine.delete-tags.count_other": "{{count}} tags deleted",
"com.affine.workbench.split-view-menu.keep-this-one": "Keep this one",
"com.affine.workbench.split-view.page-menu-open": "Split View",
"com.affine.search-tags.placeholder": "Type here ..." "com.affine.search-tags.placeholder": "Type here ..."
} }