Split Workspace
alphav0.2.0Splittable, mergeable canvas of editor areas — a dynamic layout primitive for dashboards, dev tools, and data apps.
Context
SplitWorkspace is the registry's foundational layout primitive: a single root container that tiles its viewport with rectangular editor areas (no float, no overlap). Each area picks from a consumer-supplied registry of components via a top-left dropdown. Users split areas by dragging a corner inward, merge by dragging a corner out into a neighboring area, and resize by dragging shared edges. The component is content-agnostic — consumers register what's pluggable. Designed for web apps where one fixed layout never fits everyone's workflow.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/split-workspaceAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/split-workspace-fixturesPreview
Try splitting an area by dragging from a corner, resize via the boundary, or click a divider and press Arrow keys to nudge it (new in v0.1.2). Mobile widths collapse to a card stack whose item height you control via cardStackItemHeight — 420px in this demo. Validation issues surface via onErrorbelow the canvas.
Demo source
Usage
When to use
Reach for SplitWorkspacewhen one fixed layout never fits every user's workflow — dashboards with diverse widgets, dev tools that mix code / preview / console, data-exploration apps that need side-by-side views. Users split areas with corner drags, swap each area's content from a registry, and save common arrangements as presets.
Basic example
import { SplitWorkspace, type SplitWorkspaceComponent } from "@/components/workspace"
const components: SplitWorkspaceComponent[] = [
{ id: "chart", name: "Chart", category: "Data", render: () => <ChartPanel /> },
{ id: "table", name: "Table", category: "Data", render: () => <TablePanel /> },
{ id: "filter", name: "Filter", category: "Tools", render: () => <FilterPanel /> },
]
export function Example() {
return (
<div className="h-screen w-full">
<SplitWorkspace
components={components}
defaultComponentId="chart"
/>
</div>
)
}Inside a registered component
Every component's render() runs inside an area context. Call useAreaContext()to read live dimensions, the area's id, and whether it currently holds focus.
import { useAreaContext } from "@/components/workspace"
function ChartPanel() {
const { width, height, isFocused } = useAreaContext()
return <Chart width={width} height={height} highlight={isFocused} />
}Gestures (desktop)
- Corner-drag inward — split the area; orientation is inferred from the drag direction.
- Corner-drag outward into a neighbor — merge; the neighbor is replaced.
- Drag a shared edge — resize. Boundaries clamp to
minAreaSize. - Top-left dropdown— change the area's component.
Keyboard alternatives
Tab— focus an area.- Header chevron menu — split / merge / pick a component (works without a mouse).
Alt+Shift+Arrowon a focused area — nudge the adjacent boundary.- Click a divider, then
Arrowkeys — resize the divider directly (new in v0.1.2).
Notes
- State preservation: splitting keeps the original area's component instance intact (state, scroll position, focus); the new sibling mounts fresh. Merging and switching component-id remount.
- Mobile (viewport width below
breakpoints.mobile) renders as a 1-column card stack regardless of the underlying tree. Tree state is preserved internally and restored when widening back. maxSplitDepthis a hard cap, applied per leaf and configurable per breakpoint (default{ mobile: 0, tablet: 3, desktop: 7 }). When the originating leaf is at cap, splitting is inert (no preview, no toast); merging into a neighbor on the same gesture still works. Devtools console logs once per session.- Set a height on the wrapper (
h-[600px],h-screen,flex-1, etc.) — SplitWorkspace fills its container. - Pass
layout+onLayoutChangefor controlled mode (consumer owns persistence). Omitlayoutand passdefaultLayoutfor uncontrolled. Note:onLayoutChangefires per animation frame during edge-drag (~60Hz) — debounce in your handler if you persist to storage. v0.2.0 will split this into a per-frameonResizeand a debouncedonLayoutChange. onError(v0.1.2) surfaces tree-validation issues (unregisteredcomponentId, duplicate ids, bad ratios) alongside the existingconsole.error.cardStackItemHeight(v0.1.2) overrides the mobile card height (default 320px).
Features
- Splittable / mergeable canvas via corner-drag gestures (with keyboard parity)
- Per-area component registry with a dropdown selector
- Per-breakpoint hard cap on split depth (preventive + adaptive)
- Responsive collapse to a 1-column card stack on mobile
- State preservation: splitting keeps the original area's component instance and state
- Saved presets switchable via tabs