Workspace
ProDocked, resizable panes for workstation screens: explorer, canvas, inspector and a bottom panel, collapsible to rails, remembered across reloads and stacked on small screens.
npx prodular-ui add workspacePart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Operations console
Sites, a map canvas, events and an inspector in compact density. Drag the handles, fold panels with the header buttons, reload and the sizes stay.
- Severity
- High
- Region
- eu-central-1
- Site ID
- fra-07
- Owner
- Not set
import * as React from "react";import { CircleIcon, FilterIcon, MapIcon, PanelBottomIcon, PanelLeftIcon, PanelRightIcon,} from "lucide-react";
import { Button } from "@/components/ui/button";import { PanelHeader, PanelHeaderActions, PanelHeaderCount, PanelHeaderIcon, PanelHeaderTitle,} from "@/components/ui/panel-header";import { Property, PropertyLabel, PropertyList, PropertyValue,} from "@/components/ui/property-list";import { Severity } from "@/components/ui/severity";import { Status } from "@/components/ui/status";import { StatusBar, StatusBarGroup, StatusBarItem, StatusBarSpacer,} from "@/components/ui/status-bar";import { Tree, type TreeNode } from "@/components/ui/tree";import { Workspace, WorkspaceArea, WorkspaceHandle, WorkspaceMain, WorkspacePanel, WorkspacePanelToggle, WorkspaceProvider,} from "@/components/ui/workspace";
const sites: TreeNode[] = [ { id: "eu", label: "Europe", meta: "3", children: [ { id: "eu/fra", label: "Frankfurt", meta: ( <Severity level="high" size="xs" appearance="plain"> High </Severity> ), }, { id: "eu/ams", label: "Amsterdam", meta: ( <Severity level="routine" size="xs"> Routine </Severity> ), }, { id: "eu/dub", label: "Dublin", meta: ( <Severity level="elevated" size="xs"> Elevated </Severity> ), }, ], }, { id: "us", label: "North America", meta: "2", children: [ { id: "us/iad", label: "Ashburn" }, { id: "us/sjc", label: "San Jose" }, ], },];
export default function WorkspaceConsole() { const [selected, setSelected] = React.useState(["eu/fra"]); return ( <WorkspaceProvider> <div className="flex h-[30rem] w-full flex-col overflow-hidden rounded-lg border bg-background" data-density="compact" > <div className="flex h-(--control-lg) items-center gap-1 border-b px-2"> <WorkspacePanelToggle panel="sites"> <PanelLeftIcon /> </WorkspacePanelToggle> <span className="text-(length:--text-ui) font-medium"> Edge operations </span> <div className="ml-auto flex items-center gap-1"> <WorkspacePanelToggle panel="events"> <PanelBottomIcon /> </WorkspacePanelToggle> <WorkspacePanelToggle panel="inspector"> <PanelRightIcon /> </WorkspacePanelToggle> </div> </div> <Workspace storageKey="prodular-demo-console" stackBelow={640}> <WorkspacePanel id="sites" label="Sites" defaultSize="14rem" minSize="10rem" maxSize="22rem" collapsible > <PanelHeader> <PanelHeaderTitle>Sites</PanelHeaderTitle> <PanelHeaderCount>5</PanelHeaderCount> <PanelHeaderActions> <Button variant="ghost" size="icon-xs" aria-label="Filter sites" > <FilterIcon /> </Button> </PanelHeaderActions> </PanelHeader> <div className="min-h-0 flex-1 overflow-auto p-1"> <Tree aria-label="Sites" items={sites} defaultExpanded={["eu"]} selected={selected} onSelectedChange={setSelected} /> </div> </WorkspacePanel> <WorkspaceHandle /> <WorkspaceMain> <WorkspaceArea className="grid place-items-center bg-muted/40"> <div className="flex flex-col items-center gap-2 text-(length:--text-ui) text-muted-foreground"> <MapIcon className="size-6" /> Map canvas </div> </WorkspaceArea> <WorkspaceHandle /> <WorkspacePanel id="events" label="Events" side="bottom" defaultSize="9rem" minSize="6rem" collapsible > <PanelHeader> <PanelHeaderTitle>Events</PanelHeaderTitle> <PanelHeaderCount>12</PanelHeaderCount> </PanelHeader> <div className="p-3 text-(length:--text-ui) text-muted-foreground"> 14:24 Checkout latency over budget in Frankfurt. </div> </WorkspacePanel> </WorkspaceMain> <WorkspaceHandle /> <WorkspacePanel id="inspector" label="Inspector" side="end" defaultSize="16rem" minSize="12rem" collapsible > <PanelHeader> <PanelHeaderIcon> <CircleIcon /> </PanelHeaderIcon> <PanelHeaderTitle>Frankfurt</PanelHeaderTitle> </PanelHeader> <PropertyList className="px-3 py-1" labelWidth="6rem"> <Property> <PropertyLabel>Severity</PropertyLabel> <PropertyValue> <Severity level="high" size="sm" /> </PropertyValue> </Property> <Property> <PropertyLabel>Region</PropertyLabel> <PropertyValue>eu-central-1</PropertyValue> </Property> <Property> <PropertyLabel>Site ID</PropertyLabel> <PropertyValue mono copy copyLabel="Copy site ID"> fra-07 </PropertyValue> </Property> <Property> <PropertyLabel>Owner</PropertyLabel> <PropertyValue /> </Property> </PropertyList> </WorkspacePanel> </Workspace> <StatusBar> <StatusBarGroup> <StatusBarItem live> <Status variant="success" size="xs"> Connected </Status> </StatusBarItem> </StatusBarGroup> <StatusBarGroup> <StatusBarItem>5 sites</StatusBarItem> </StatusBarGroup> <StatusBarSpacer /> <StatusBarItem>Updated 14:31 UTC</StatusBarItem> </StatusBar> </div> </WorkspaceProvider> );}Anatomy
<AppShell layout="rail" fill nav={nav}> <Workspace storageKey="ops"> <WorkspacePanel id="explorer" label="Explorer" collapsible>…</WorkspacePanel> <WorkspaceHandle /> <WorkspaceMain> <WorkspaceArea>…</WorkspaceArea> <WorkspaceHandle /> <WorkspacePanel id="timeline" label="Timeline" side="bottom" collapsible>…</WorkspacePanel> </WorkspaceMain> <WorkspaceHandle /> <WorkspacePanel id="inspector" label="Inspector" side="end" collapsible>…</WorkspacePanel> </Workspace></AppShell>Guidance
Use it when
- Screens people keep open all day and arrange to their task: consoles, editors, investigation tools.
- AppShell with fill around it, so the page area does not scroll.
- PanelHeader at the top of each panel.
Avoid
- Workspace for regular SaaS pages. Use the page layouts and MasterDetail.
- More than four panes. Put secondary views in tabs inside a pane.
- Collapsing by dragging only. Add WorkspacePanelToggle buttons, and a hotkey if the app has them.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Alternate panes and WorkspaceHandle: panel, handle, main, handle, panel.
- Give every WorkspacePanel a unique id and a label; the label names the rail and the toggle.
- Set storageKey so sizes and collapsed panes persist.
- Wrap the screen in AppShell fill (or a parent with a fixed height). Workspace fills its parent and never scrolls itself; panes scroll inside.
- Use WorkspacePanelToggle or useWorkspacePanel(id).toggle for buttons and shortcuts instead of resizing panels yourself.
- Pass sizes in rem or px strings ("18rem"), numbers for pixels, or unitless strings for percentages.
API
Workspace
| Prop | Type | Default | Description |
|---|---|---|---|
| storageKey | string | Keeps sizes and collapsed panes in localStorage. | |
| stackBelow | number | 768 | Viewport width in px under which panes stack vertically. |
WorkspacePanel
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | Unique id, also used by WorkspacePanelToggle. | |
| label | string | Names the pane, its rail and its toggle. | |
| side | "start" | "end" | "bottom" | "start" | Where it docks. |
| collapsible | boolean | false | Folds to a rail below minSize or from its toggle. |
| defaultSize | number | string | "18rem" | Also minSize and maxSize. |
WorkspacePanelToggle
| Prop | Type | Default | Description |
|---|---|---|---|
| panel | string | The id of the panel it folds and opens. |
AppShell
| Prop | Type | Default | Description |
|---|---|---|---|
| fill | boolean | false | The page area fills the space under the top bar and does not scroll. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add workspace# or, with the registry in components.jsonnpx shadcn@latest add @prodular/workspace