Files
platform/src/workspaces/components/Workspace/WorkspaceRenderer.tsx
T
2026-02-19 12:03:45 +00:00

175 lines
4.9 KiB
TypeScript

import { useCallback, useRef } from 'react';
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '../ui/resizable';
import type { LayoutNode, AppRegistry, PanelComponents } from './types';
import { countPanels } from './layout-utils';
import { PanelSlot } from './PanelSlot';
type WorkspaceRendererProps = {
layout: LayoutNode;
registry: AppRegistry;
components?: PanelComponents;
interactive?: boolean;
onSetApp: (panelId: string, appType: 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,
components,
interactive = false,
onSetApp,
onSplit,
onRemove,
onResized,
}: WorkspaceRendererProps) => {
const totalPanels = countPanels(layout);
return (
<div className="h-full w-full">
<LayoutNodeRenderer
node={layout}
registry={registry}
components={components}
interactive={interactive}
totalPanels={totalPanels}
onSetApp={onSetApp}
onSplit={onSplit}
onRemove={onRemove}
onResized={onResized}
/>
</div>
);
};
type LayoutNodeRendererProps = {
node: LayoutNode;
registry: AppRegistry;
components?: PanelComponents;
interactive: boolean;
totalPanels: number;
onSetApp: (panelId: string, appType: string | null) => void;
onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
onRemove: (panelId: string) => void;
onResized: (groupId: string, sizes: number[]) => void;
};
const getFixedHeight = (node: LayoutNode, registry: AppRegistry): number | undefined => {
if (node.type === 'panel' && node.appType) return registry[node.appType]?.fixedHeight;
return undefined;
};
const LayoutNodeRenderer = ({
node,
registry,
components,
interactive,
totalPanels,
onSetApp,
onSplit,
onRemove,
onResized,
}: LayoutNodeRendererProps) => {
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
const mountedRef = useRef(false);
const handleLayout = useCallback(
(sizes: number[]) => {
if (node.type !== 'group') return;
if (!mountedRef.current) {
mountedRef.current = true;
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}
components={components}
interactive={interactive}
isLastPanel={totalPanels <= 1}
onSetApp={onSetApp}
onSplit={onSplit}
onRemove={onRemove}
/>
);
}
const hasFixedChild = node.direction === 'vertical' && node.children.some((c) => getFixedHeight(c.node, registry) !== undefined);
if (hasFixedChild) {
return (
<div className="flex h-full w-full flex-col">
{node.children.map((child) => {
const fixed = getFixedHeight(child.node, registry);
return (
<div key={child.node.id} className={fixed !== undefined ? 'shrink-0' : 'min-h-0 flex-1'} style={fixed !== undefined ? { height: fixed } : undefined}>
<LayoutNodeRenderer
node={child.node}
registry={registry}
components={components}
interactive={interactive}
totalPanels={totalPanels}
onSetApp={onSetApp}
onSplit={onSplit}
onRemove={onRemove}
onResized={onResized}
/>
</div>
);
})}
</div>
);
}
return (
<ResizablePanelGroup id={node.id} 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 id={child.node.id} order={i} defaultSize={child.size} minSize={5}>
<div className="h-full w-full">
<LayoutNodeRenderer
node={child.node}
registry={registry}
components={components}
interactive={interactive}
totalPanels={totalPanels}
onSetApp={onSetApp}
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}
</>
);
};