workspace panel drag
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import type { DropPosition } from './layout-utils';
|
||||||
|
import { useWorkspace } from './WorkspaceContext';
|
||||||
|
|
||||||
|
type DropTarget = {
|
||||||
|
panelId: string;
|
||||||
|
zone: DropPosition;
|
||||||
|
rect: DOMRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDropZone(rect: DOMRect, x: number, y: number): DropPosition {
|
||||||
|
const fx = (x - rect.left) / rect.width;
|
||||||
|
const fy = (y - rect.top) / rect.height;
|
||||||
|
|
||||||
|
if (fx > 0.3 && fx < 0.7 && fy > 0.3 && fy < 0.7) return 'center';
|
||||||
|
|
||||||
|
const edges: [DropPosition, number][] = [
|
||||||
|
['left', fx],
|
||||||
|
['right', 1 - fx],
|
||||||
|
['top', fy],
|
||||||
|
['bottom', 1 - fy],
|
||||||
|
];
|
||||||
|
|
||||||
|
return edges.reduce((min, cur) => (cur[1] < min[1] ? cur : min))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorStyle(rect: DOMRect, zone: DropPosition): React.CSSProperties {
|
||||||
|
switch (zone) {
|
||||||
|
case 'center':
|
||||||
|
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
||||||
|
case 'top':
|
||||||
|
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height / 2 };
|
||||||
|
case 'bottom':
|
||||||
|
return { left: rect.left, top: rect.top + rect.height / 2, width: rect.width, height: rect.height / 2 };
|
||||||
|
case 'left':
|
||||||
|
return { left: rect.left, top: rect.top, width: rect.width / 2, height: rect.height };
|
||||||
|
case 'right':
|
||||||
|
return { left: rect.left + rect.width / 2, top: rect.top, width: rect.width / 2, height: rect.height };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DragOverlay = () => {
|
||||||
|
const { dragSourceId, setDragSourceId, onMove } = useWorkspace();
|
||||||
|
const [target, setTarget] = useState<DropTarget | null>(null);
|
||||||
|
const panelRectsRef = useRef<Map<string, DOMRect>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dragSourceId) {
|
||||||
|
panelRectsRef.current.clear();
|
||||||
|
setTarget(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const panels = document.querySelectorAll<HTMLElement>('[data-panel-id]');
|
||||||
|
const map = new Map<string, DOMRect>();
|
||||||
|
panels.forEach((el) => {
|
||||||
|
const id = el.dataset.panelId!;
|
||||||
|
if (id !== dragSourceId) map.set(id, el.getBoundingClientRect());
|
||||||
|
});
|
||||||
|
panelRectsRef.current = map;
|
||||||
|
}, [dragSourceId]);
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback((ev: React.PointerEvent) => {
|
||||||
|
const { clientX: x, clientY: y } = ev;
|
||||||
|
let found: DropTarget | null = null;
|
||||||
|
|
||||||
|
for (const [panelId, rect] of panelRectsRef.current) {
|
||||||
|
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
||||||
|
found = { panelId, zone: getDropZone(rect, x, y), rect };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTarget(found);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePointerUp = useCallback(() => {
|
||||||
|
if (dragSourceId && target) {
|
||||||
|
onMove(dragSourceId, target.panelId, target.zone);
|
||||||
|
}
|
||||||
|
setDragSourceId(null);
|
||||||
|
}, [dragSourceId, target, onMove, setDragSourceId]);
|
||||||
|
|
||||||
|
if (!dragSourceId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 cursor-grabbing"
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
>
|
||||||
|
{target && (
|
||||||
|
<div
|
||||||
|
className="absolute rounded-lg border-2 border-duck-teal/60 bg-duck-teal/15 pointer-events-none"
|
||||||
|
style={{ ...getIndicatorStyle(target.rect, target.zone), transition: 'all 75ms ease-out' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ArrowLeftRight } from 'lucide-react';
|
import { useCallback } from 'react';
|
||||||
|
import { ArrowLeftRight, GripVertical } from 'lucide-react';
|
||||||
import type { LayoutPanel, AppRegistry, PanelComponents } from './types';
|
import type { LayoutPanel, AppRegistry, PanelComponents } from './types';
|
||||||
import { useWorkspace } from './WorkspaceContext';
|
import { useWorkspace } from './WorkspaceContext';
|
||||||
import { Card } from '../Card';
|
import { Card } from '../Card';
|
||||||
@@ -48,9 +49,7 @@ const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onC
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)}
|
)}
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
{swapSourceId === panelId ? (
|
{swapSourceId ? (
|
||||||
<ContextMenuItem onClick={() => setSwapSourceId(null)}>Cancel swap</ContextMenuItem>
|
|
||||||
) : swapSourceId ? (
|
|
||||||
<ContextMenuItem onClick={() => setSwapSourceId(null)}>Cancel swap</ContextMenuItem>
|
<ContextMenuItem onClick={() => setSwapSourceId(null)}>Cancel swap</ContextMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<ContextMenuItem onClick={() => setSwapSourceId(panelId)}>Swap with...</ContextMenuItem>
|
<ContextMenuItem onClick={() => setSwapSourceId(panelId)}>Swap with...</ContextMenuItem>
|
||||||
@@ -61,7 +60,7 @@ const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onC
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SwapOverlay = ({ panelId }: { panelId: string }) => {
|
const SwapOverlay = ({ panelId }: { panelId: string }) => {
|
||||||
const { swapSourceId, setSwapSourceId, onSwap } = useWorkspace();
|
const { swapSourceId, onSwap } = useWorkspace();
|
||||||
|
|
||||||
if (!swapSourceId || swapSourceId === panelId) return null;
|
if (!swapSourceId || swapSourceId === panelId) return null;
|
||||||
|
|
||||||
@@ -99,6 +98,35 @@ const SwapSourceIndicator = ({ panelId }: { panelId: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DragHandle = ({ panelId }: { panelId: string }) => {
|
||||||
|
const { setDragSourceId, dragSourceId } = useWorkspace();
|
||||||
|
|
||||||
|
const onPointerDown = useCallback(
|
||||||
|
(ev: React.PointerEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setDragSourceId(panelId);
|
||||||
|
},
|
||||||
|
[panelId, setDragSourceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dragSourceId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute top-2 right-2 z-10 p-1.5 rounded-md bg-duck-dark/5 text-duck-dark/30 opacity-0 group-hover/panel:opacity-100 hover:!bg-duck-dark/10 hover:!text-duck-dark/60 transition-all cursor-grab"
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
>
|
||||||
|
<GripVertical className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DragSourceDimmer = ({ panelId }: { panelId: string }) => {
|
||||||
|
const { dragSourceId } = useWorkspace();
|
||||||
|
if (dragSourceId !== panelId) return null;
|
||||||
|
return <div className="absolute inset-0 z-20 rounded-lg bg-duck-dark/10 pointer-events-none" />;
|
||||||
|
};
|
||||||
|
|
||||||
export const PanelSlot = ({ panel, registry, components, interactive, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => {
|
export const PanelSlot = ({ panel, registry, components, interactive, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => {
|
||||||
const PanelComponent = components?.[panel.id];
|
const PanelComponent = components?.[panel.id];
|
||||||
const entry = panel.appType ? registry[panel.appType] : null;
|
const entry = panel.appType ? registry[panel.appType] : null;
|
||||||
@@ -112,14 +140,14 @@ export const PanelSlot = ({ panel, registry, components, interactive, isLastPane
|
|||||||
)
|
)
|
||||||
: (content: React.ReactNode) => <>{content}</>;
|
: (content: React.ReactNode) => <>{content}</>;
|
||||||
|
|
||||||
const swapOverlays = interactive
|
const overlays = interactive ? (
|
||||||
? (
|
<>
|
||||||
<>
|
<SwapOverlay panelId={panel.id} />
|
||||||
<SwapOverlay panelId={panel.id} />
|
<SwapSourceIndicator panelId={panel.id} />
|
||||||
<SwapSourceIndicator panelId={panel.id} />
|
<DragHandle panelId={panel.id} />
|
||||||
</>
|
<DragSourceDimmer panelId={panel.id} />
|
||||||
)
|
</>
|
||||||
: null;
|
) : null;
|
||||||
|
|
||||||
if (!AppComponent) {
|
if (!AppComponent) {
|
||||||
if (!interactive) {
|
if (!interactive) {
|
||||||
@@ -131,28 +159,28 @@ export const PanelSlot = ({ panel, registry, components, interactive, isLastPane
|
|||||||
}
|
}
|
||||||
|
|
||||||
return contextMenu(
|
return contextMenu(
|
||||||
<div className="relative h-full w-full p-1">
|
<div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
|
||||||
<Card className="relative h-full w-full flex flex-col items-center justify-center gap-3 p-4">
|
<Card className="relative h-full w-full flex flex-col items-center justify-center gap-3 p-4">
|
||||||
<AppPicker registry={registry} onSelect={(type) => onSetApp(panel.id, type)} />
|
<AppPicker registry={registry} onSelect={(type) => onSetApp(panel.id, type)} />
|
||||||
</Card>
|
</Card>
|
||||||
{swapOverlays}
|
{overlays}
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry?.transparent) {
|
if (entry?.transparent) {
|
||||||
return contextMenu(
|
return contextMenu(
|
||||||
<div className="relative h-full w-full p-1">
|
<div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
<AppComponent panelId={panel.id} />
|
<AppComponent panelId={panel.id} />
|
||||||
</div>
|
</div>
|
||||||
{swapOverlays}
|
{overlays}
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return contextMenu(
|
return contextMenu(
|
||||||
<div className="relative h-full w-full p-1">
|
<div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
|
||||||
<div
|
<div
|
||||||
className="relative h-full w-full overflow-hidden rounded-lg border backdrop-blur-xl p-2"
|
className="relative h-full w-full overflow-hidden rounded-lg border backdrop-blur-xl p-2"
|
||||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }}
|
style={{ backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }}
|
||||||
@@ -161,7 +189,7 @@ export const PanelSlot = ({ panel, registry, components, interactive, isLastPane
|
|||||||
<AppComponent panelId={panel.id} />
|
<AppComponent panelId={panel.id} />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
{swapOverlays}
|
{overlays}
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import type { DropPosition } from './layout-utils';
|
||||||
|
|
||||||
type WorkspaceContextValue = {
|
type WorkspaceContextValue = {
|
||||||
workspaceId: string | null;
|
workspaceId: string | null;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
swapSourceId: string | null;
|
swapSourceId: string | null;
|
||||||
setSwapSourceId: (id: string | null) => void;
|
setSwapSourceId: (id: string | null) => void;
|
||||||
onSwap: (sourceId: string, targetId: string) => void;
|
onSwap: (sourceId: string, targetId: string) => void;
|
||||||
|
dragSourceId: string | null;
|
||||||
|
setDragSourceId: (id: string | null) => void;
|
||||||
|
onMove: (sourceId: string, targetId: string, position: DropPosition) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -16,6 +21,9 @@ const WorkspaceContext = createContext<WorkspaceContextValue>({
|
|||||||
swapSourceId: null,
|
swapSourceId: null,
|
||||||
setSwapSourceId: noop,
|
setSwapSourceId: noop,
|
||||||
onSwap: noop,
|
onSwap: noop,
|
||||||
|
dragSourceId: null,
|
||||||
|
setDragSourceId: noop,
|
||||||
|
onMove: noop,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WorkspaceProvider = WorkspaceContext.Provider;
|
export const WorkspaceProvider = WorkspaceContext.Provider;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const WorkspaceLayout = ({ layout, onLayoutChange, registry, components,
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceProvider value={{ workspaceId: workspaceId ?? null, cwd: cwd ?? '~', swapSourceId: null, setSwapSourceId: noop, onSwap: noop }}>
|
<WorkspaceProvider value={{ workspaceId: workspaceId ?? null, cwd: cwd ?? '~', swapSourceId: null, setSwapSourceId: noop, onSwap: noop, dragSourceId: null, setDragSourceId: noop, onMove: noop }}>
|
||||||
<WorkspaceRenderer
|
<WorkspaceRenderer
|
||||||
layout={layout}
|
layout={layout}
|
||||||
registry={registry}
|
registry={registry}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import type { LayoutNode, WorkspaceDefinition, AppRegistry } from './types';
|
import type { LayoutNode, WorkspaceDefinition, AppRegistry } from './types';
|
||||||
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, countPanels } from './layout-utils';
|
import type { DropPosition } from './layout-utils';
|
||||||
|
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils';
|
||||||
import { WorkspaceProvider } from './WorkspaceContext';
|
import { WorkspaceProvider } from './WorkspaceContext';
|
||||||
import { WorkspaceRenderer } from './WorkspaceRenderer';
|
import { WorkspaceRenderer } from './WorkspaceRenderer';
|
||||||
|
import { DragOverlay } from './DragOverlay';
|
||||||
|
|
||||||
type WorkspaceViewProps = {
|
type WorkspaceViewProps = {
|
||||||
workspace: WorkspaceDefinition | null;
|
workspace: WorkspaceDefinition | null;
|
||||||
@@ -13,6 +15,7 @@ type WorkspaceViewProps = {
|
|||||||
|
|
||||||
export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: WorkspaceViewProps) => {
|
export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: WorkspaceViewProps) => {
|
||||||
const [swapSourceId, setSwapSourceId] = useState<string | null>(null);
|
const [swapSourceId, setSwapSourceId] = useState<string | null>(null);
|
||||||
|
const [dragSourceId, setDragSourceId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSetApp = useCallback(
|
const handleSetApp = useCallback(
|
||||||
(panelId: string, appType: string | null) => {
|
(panelId: string, appType: string | null) => {
|
||||||
@@ -51,14 +54,33 @@ export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: W
|
|||||||
[layout, onLayoutChange],
|
[layout, onLayoutChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleMove = useCallback(
|
||||||
|
(sourceId: string, targetId: string, position: DropPosition) => {
|
||||||
|
onLayoutChange(movePanel(layout, sourceId, targetId, position));
|
||||||
|
setDragSourceId(null);
|
||||||
|
},
|
||||||
|
[layout, onLayoutChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const startDrag = useCallback(
|
||||||
|
(id: string | null) => {
|
||||||
|
setSwapSourceId(null);
|
||||||
|
setDragSourceId(id);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!swapSourceId) return;
|
if (!swapSourceId && !dragSourceId) return;
|
||||||
const onKeyDown = (ev: KeyboardEvent) => {
|
const onKeyDown = (ev: KeyboardEvent) => {
|
||||||
if (ev.key === 'Escape') setSwapSourceId(null);
|
if (ev.key === 'Escape') {
|
||||||
|
setSwapSourceId(null);
|
||||||
|
setDragSourceId(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
return () => window.removeEventListener('keydown', onKeyDown);
|
return () => window.removeEventListener('keydown', onKeyDown);
|
||||||
}, [swapSourceId]);
|
}, [swapSourceId, dragSourceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceProvider
|
<WorkspaceProvider
|
||||||
@@ -68,6 +90,9 @@ export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: W
|
|||||||
swapSourceId,
|
swapSourceId,
|
||||||
setSwapSourceId,
|
setSwapSourceId,
|
||||||
onSwap: handleSwap,
|
onSwap: handleSwap,
|
||||||
|
dragSourceId,
|
||||||
|
setDragSourceId: startDrag,
|
||||||
|
onMove: handleMove,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorkspaceRenderer
|
<WorkspaceRenderer
|
||||||
@@ -79,6 +104,7 @@ export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: W
|
|||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
onResized={handleResized}
|
onResized={handleResized}
|
||||||
/>
|
/>
|
||||||
|
<DragOverlay />
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, AppRegistry, AppRegistryEntry, PanelComponents } from './types';
|
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, AppRegistry, AppRegistryEntry, PanelComponents } from './types';
|
||||||
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
|
export type { DropPosition } from './layout-utils';
|
||||||
|
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
|
||||||
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
|
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
|
||||||
export { WorkspaceView } from './WorkspaceView';
|
export { WorkspaceView } from './WorkspaceView';
|
||||||
export { WorkspaceLayout } from './WorkspaceLayout';
|
export { WorkspaceLayout } from './WorkspaceLayout';
|
||||||
|
|||||||
@@ -139,6 +139,54 @@ export function pruneEmptyPanels(root: LayoutNode): LayoutNode | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DropPosition = 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||||
|
|
||||||
|
export function movePanel(root: LayoutNode, sourceId: string, targetId: string, position: DropPosition): LayoutNode {
|
||||||
|
if (sourceId === targetId) return root;
|
||||||
|
if (position === 'center') return swapPanels(root, sourceId, targetId);
|
||||||
|
|
||||||
|
const sourceApp = findPanelApp(root, sourceId);
|
||||||
|
if (sourceApp === undefined) return root;
|
||||||
|
|
||||||
|
let result = removePanel(root, sourceId);
|
||||||
|
const direction = position === 'top' || position === 'bottom' ? 'vertical' : 'horizontal';
|
||||||
|
const before = position === 'top' || position === 'left';
|
||||||
|
return insertPanel(result, targetId, direction, before, sourceApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertPanel(node: LayoutNode, targetId: string, direction: 'horizontal' | 'vertical', before: boolean, appType: string | null): LayoutNode {
|
||||||
|
if (node.type === 'panel') {
|
||||||
|
if (node.id !== targetId) return node;
|
||||||
|
const newPanel: LayoutPanel = { type: 'panel', id: uid(), appType };
|
||||||
|
const children = before
|
||||||
|
? [{ node: newPanel, size: 50 }, { node, size: 50 }]
|
||||||
|
: [{ node, size: 50 }, { node: newPanel, size: 50 }];
|
||||||
|
return { type: 'group', id: uid(), direction, children };
|
||||||
|
}
|
||||||
|
|
||||||
|
const childIdx = node.children.findIndex((c) => c.node.type === 'panel' && c.node.id === targetId);
|
||||||
|
if (childIdx !== -1 && node.direction === direction) {
|
||||||
|
const newPanel: LayoutPanel = { type: 'panel', id: uid(), appType };
|
||||||
|
const insertIdx = before ? childIdx : childIdx + 1;
|
||||||
|
const newChildren = [
|
||||||
|
...node.children.slice(0, insertIdx),
|
||||||
|
{ node: newPanel, size: 0 },
|
||||||
|
...node.children.slice(insertIdx),
|
||||||
|
];
|
||||||
|
const size = 100 / newChildren.length;
|
||||||
|
return { ...node, children: newChildren.map((c) => ({ ...c, size })) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChildren = node.children.map((child) => ({
|
||||||
|
...child,
|
||||||
|
node: insertPanel(child.node, targetId, direction, before, appType),
|
||||||
|
}));
|
||||||
|
if (newChildren.some((c, i) => c.node !== node.children[i]!.node)) {
|
||||||
|
return { ...node, children: newChildren };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
export function swapPanels(root: LayoutNode, idA: string, idB: string): LayoutNode {
|
export function swapPanels(root: LayoutNode, idA: string, idB: string): LayoutNode {
|
||||||
const appA = findPanelApp(root, idA);
|
const appA = findPanelApp(root, idA);
|
||||||
const appB = findPanelApp(root, idB);
|
const appB = findPanelApp(root, idB);
|
||||||
|
|||||||
Reference in New Issue
Block a user