Skip to content
ilinxa/pro-ui

Split Workspace

alphav0.2.0

Splittable, mergeable canvas of editor areas — a dynamic layout primitive for dashboards, dev tools, and data apps.

Category: LayoutUpdated: 2026-08-11Created: 2026-04-27Author: ilinxa

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

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/split-workspace

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/split-workspace-fixtures

Preview

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

demo.tsxtsx

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+Arrow on a focused area — nudge the adjacent boundary.
  • Click a divider, then Arrow keys — 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.
  • maxSplitDepth is 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 + onLayoutChange for controlled mode (consumer owns persistence). Omit layout and pass defaultLayout for uncontrolled. Note: onLayoutChange fires 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-frame onResize and a debounced onLayoutChange.
  • onError (v0.1.2) surfaces tree-validation issues (unregistered componentId, duplicate ids, bad ratios) alongside the existing console.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

Tags

split-workspacelayoutsplitpanelstilingdashboard

Dependencies

shadcn primitives: dropdown-menu, scroll-area, tabs
npm peer deps: lucide-react@^1.11.0