175 lines
4.9 KiB
TypeScript
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}
|
|
</>
|
|
);
|
|
};
|