Workspaces

This commit is contained in:
2026-02-18 02:36:15 +00:00
parent c2660cd125
commit 826904d74f
22 changed files with 851 additions and 437 deletions
@@ -0,0 +1,135 @@
import { useCallback, useRef } from 'react';
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '../ui/resizable';
import type { LayoutNode, WidgetRegistry } from './types';
import { pruneEmptyPanels, countPanels } from './layout-utils';
import { PanelSlot } from './PanelSlot';
type WorkspaceRendererProps = {
layout: LayoutNode;
registry: WidgetRegistry;
editing: boolean;
onSetWidget: (panelId: string, widgetType: string | null) => void;
onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
onRemove: (panelId: string) => void;
onResized: (groupId: string, sizes: number[]) => void;
};
export const WorkspaceRenderer = ({
layout,
registry,
editing,
onSetWidget,
onSplit,
onRemove,
onResized,
}: WorkspaceRendererProps) => {
const displayLayout = editing ? layout : pruneEmptyPanels(layout);
if (!displayLayout) {
return <div className="flex h-full items-center justify-center text-sm text-muted-foreground">No widgets</div>;
}
const totalPanels = countPanels(layout);
return (
<div className="h-full w-full">
<LayoutNodeRenderer
node={displayLayout}
registry={registry}
editing={editing}
totalPanels={totalPanels}
onSetWidget={onSetWidget}
onSplit={onSplit}
onRemove={onRemove}
onResized={onResized}
/>
</div>
);
};
type LayoutNodeRendererProps = {
node: LayoutNode;
registry: WidgetRegistry;
editing: boolean;
totalPanels: number;
onSetWidget: (panelId: string, widgetType: string | null) => void;
onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
onRemove: (panelId: string) => void;
onResized: (groupId: string, sizes: number[]) => void;
};
const LayoutNodeRenderer = ({
node,
registry,
editing,
totalPanels,
onSetWidget,
onSplit,
onRemove,
onResized,
}: LayoutNodeRendererProps) => {
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
const handleLayout = useCallback(
(sizes: number[]) => {
if (node.type !== 'group') return;
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
onResized(node.id, sizes);
}, 500);
},
[node, onResized],
);
if (node.type === 'panel') {
return (
<PanelSlot
panel={node}
registry={registry}
editing={editing}
isLastPanel={totalPanels <= 1}
onSetWidget={onSetWidget}
onSplit={onSplit}
onRemove={onRemove}
/>
);
}
return (
<ResizablePanelGroup direction={node.direction} onLayout={handleLayout} className="h-full w-full">
{node.children.map((child, i) => (
<ChildEntry key={child.node.id} index={i} total={node.children.length}>
<ResizablePanel defaultSize={child.size} minSize={5}>
<div className="h-full w-full p-1">
<LayoutNodeRenderer
node={child.node}
registry={registry}
editing={editing}
totalPanels={totalPanels}
onSetWidget={onSetWidget}
onSplit={onSplit}
onRemove={onRemove}
onResized={onResized}
/>
</div>
</ResizablePanel>
</ChildEntry>
))}
</ResizablePanelGroup>
);
};
type ChildEntryProps = {
index: number;
total: number;
children: React.ReactNode;
};
const ChildEntry = ({ index, total, children }: ChildEntryProps) => {
if (index === 0) return <>{children}</>;
return (
<>
<ResizableHandle className="bg-transparent after:bg-transparent" />
{children}
</>
);
};