Application
Workspace
Resizable panes, a focus mode, an inspector and a local layout that survives a reload. Not a drag-and-drop dashboard: the layout is the component tree, so a pane cannot be moved somewhere the code does not describe.
import {
InspectorPane,
InspectorRow,
InspectorSection,
InspectorToggle,
Workspace,
WorkspaceGroup,
WorkspacePane,
WorkspacePaneBody,
WorkspacePaneFooter,
WorkspacePaneHeader,
WorkspaceResizeHandle,
} from "@luc/ui/workspace";Table, inspector, persistence
Drag the divider, or focus it with Tab and resize with the arrow keys. Sizes persist under this demo's key; Reset returns to the defaults. Below 64rem the panes stack and the handles disappear.
Research runs
48 prepared rows
Selection is not in the URL; it is not shareable state.
| Equity | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| NDX | Mean reversion | Failed | Trend | 0.48 | 25.4% | -13.7% | 297 | 59.9% | Sep 9 | |||
| NDX | Volatility carry | Live | Stress | 1.55 | 23.0% | -26.0% | 423 | 45.5% | Sep 9 | |||
| AAPL | Cross-sectional momentum | Live | Stress | 1.73 | -3.8% | -32.4% | 394 | 40.2% | Aug 24 | |||
| MSFT | Trend breakout | Failed | Range | 0.33 | 9.4% | -26.8% | 223 | 23.3% | Sep 21 | |||
| BTC-USD | Volatility carry | Paused | Range | 2.00 | 41.7% | -29.1% | 341 | 56.7% | Sep 13 | |||
| NVDA | Mean reversion | Paused | Stress | 1.32 | -2.0% | -15.0% | 532 | 93.3% | Aug 19 | |||
| MSFT | Regime switch | Paused | Stress | 0.26 | 7.0% | -12.0% | 730 | 86.0% | Aug 20 | |||
| MSFT | Trend breakout | Live | Stress | 1.22 | 30.9% | -15.5% | 588 | 89.8% | Sep 2 | |||
| SPX | Regime switch | Paused | Stress | 0.31 | 36.5% | -17.1% | 232 | 64.3% | Aug 13 | |||
| AAPL | Mean reversion | Paused | Range | -0.16 | 36.6% | -33.7% | 136 | 53.4% | Sep 5 | |||
| SOL-USD | Trend breakout | Live | Stress | -0.13 | 13.8% | -36.5% | 99 | 23.4% | Aug 26 | |||
| NVDA | Trend breakout | Live | Trend | 1.54 | 21.7% | -10.5% | 769 | 57.3% | Sep 19 | |||
| MSFT | Regime switch | Live | Range | 1.23 | 0.3% | -3.6% | 756 | 83.4% | Aug 19 | |||
| SPX | Volatility carry | Paused | Trend | -0.27 | 8.2% | -16.9% | 214 | 87.1% | Sep 9 | |||
| SOL-USD | Funding basis | Failed | Stress | 1.05 | -3.8% | -5.0% | 709 | 33.2% | Aug 22 | |||
| AAPL | Mean reversion | Failed | Stress | 0.23 | 26.7% | -19.4% | 442 | 24.3% | Aug 28 | |||
| NVDA | Cross-sectional momentum | Live | Range | 0.11 | 17.2% | -28.3% | 615 | 42.8% | Sep 7 | |||
| GC | Trend breakout | Draft | Trend | -0.41 | 38.2% | -29.9% | 806 | 23.4% | Aug 15 | |||
| SPX | Mean reversion | Failed | Range | -0.08 | 33.7% | -30.8% | 285 | 40.5% | Sep 15 | |||
| ETH-USD | Volatility carry | Draft | Stress | 1.36 | 37.9% | -3.2% | 24 | 61.0% | Sep 16 |
Notes
A second pane in a nested split
Vertical splits are ordinary groups inside a pane. A pane may hold a chart, a table, a form, an editor or another workspace — the layout engine only knows about boxes.
Inspector
NDX
Selected run
/* Excerpt of the live specimen: the tree and the props that carry behaviour.
The full table configuration (columns, toolbar, facets) is elided. */
<Workspace className="h-[30rem] overflow-hidden rounded-lg border border-line-subtle bg-surface-canvas">
<WorkspaceGroup orientation="horizontal" persistKey="product:research" persistVersion={1}>
<WorkspacePane id="results" minSize="20rem">
<WorkspaceGroup orientation="vertical" persistKey="product:research-notes" persistVersion={1}>
<WorkspacePane id="runs" minSize="12rem">
<WorkspacePaneHeader icon={<RiTableLine />} title="Research runs" paneId="runs" showReset />
<WorkspacePaneBody flush>
<DataTable
ariaLabel="Research runs"
data={runs}
columns={columns}
getRowId={(row) => row.id}
enableRowSelection="single"
rowSelection={selectedId ? { [selectedId]: true } : {}}
onRowSelectionChange={…}
onRowClick={(row) => { setSelectedId(row.id); setInspectorOpen(true); }}
/>
</WorkspacePaneBody>
</WorkspacePane>
<WorkspaceResizeHandle label="Resize notes" />
<WorkspacePane id="notes" minSize="8rem" defaultSize="30%">…</WorkspacePane>
</WorkspaceGroup>
</WorkspacePane>
<WorkspaceResizeHandle label="Resize inspector" />
<InspectorPane
id="inspector"
open={inspectorOpen}
onOpenChange={setInspectorOpen}
minSize="16rem"
defaultSize="20rem"
>…</InspectorPane>
</WorkspaceGroup>
</Workspace>Panes, handles, focus mode
A handle is a real separator with a value, a name and arrow-key resizing; the visible line is a hairline while the hit area is wide. Focus mode expands one pane to fill its group and Escape leaves it.
Sizes are personal state
A layout is stored per pane set, versioned, and validated on read: an incompatible shape is discarded rather than migrated. Sizes are never written to the URL — a shared link describes a view, not your window.
<WorkspaceGroup
orientation="horizontal"
persistKey="product:research"
persistVersion={1}
>
<WorkspacePane id="chart" minSize="18rem">…</WorkspacePane>
<WorkspaceResizeHandle label="Resize inspector" />
<InspectorPane id="inspector" open={open} onOpenChange={setOpen}>
…
</InspectorPane>
</WorkspaceGroup>When not to use a workspace
- A document: use the page and the shell.
- Two fixed columns: use the grid primitives.
- A phone: it stacks, so design the stacked order first.
- Anything a reader must not resize: pass locked.
Exports
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
useWorkspace | export | () => WorkspaceContextValue |
useWorkspaceGroup | export | () => WorkspaceGroupContextValue |
Workspace | 4 documented props | ({ className, mode: modeProp, focusedId: focusedIdProp, onFocusedIdChange, density, children, ...props }: WorkspaceProps) => import("@types/react").JSX.Element |
WorkspaceFocusBanner | 1 documented props | ({ label }: WorkspaceFocusBannerProps) => import("@types/react").JSX.Element | null |
WorkspaceGroup | 7 documented props | ({ className, orientation, persistKey, persistVersion, storage, defaultLayout, panelIds, locked, children, ...props }: WorkspaceGroupProps) => import("@types/react").JSX.Element |
WorkspacePane | 9 documented props | ({ id, defaultSize, minSize, maxSize, collapsible, collapsedSize, hideOnStacked, panelRef, onSizeChange, className, children, ...props }: WorkspacePaneProps) => import("@types/react").JSX.Element | n… |
WorkspacePaneBody | 2 documented props | ({ className, flush, ariaLabel, ...props }: WorkspacePaneBodyProps) => import("@types/react").JSX.Element |
WorkspacePaneFooter | export | ({ className, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element |
WorkspacePaneHeader | 6 documented props | ({ title, description, icon, actions, paneId, showReset, className, children, ...props }: WorkspacePaneHeaderProps) => import("@types/react").JSX.Element |
WorkspaceResizeHandle | 1 documented props | ({ className, label, ...props }: WorkspaceResizeHandleProps) => import("@types/react").JSX.Element | null |
| Export | Kind | Signature |
|---|---|---|
InspectorPane | 16 documented props | ({ id, open: openProp, defaultOpen, onOpenChange, title, description, actions, footer, side, minSize, defaultSize, className, children, ...props }: InspectorPaneProps) => import("@types/react").JSX.E… |
InspectorRow | 3 documented props | ({ label, value, numeric, className, ...props }: InspectorRowProps) => import("@types/react").JSX.Element |
InspectorSection | 1 documented props | ({ label, className, children, ...props }: InspectorSectionProps) => import("@types/react").JSX.Element |
InspectorSkeleton | export | () => import("@types/react").JSX.Element |
InspectorToggle | 4 documented props | ({ open, onOpenChange, label, className }: InspectorToggleProps) => import("@types/react").JSX.Element |
Workspace props
Type: WorkspaceProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
density? | "compact" | "comfortable" | undefined | — | |
focusedId? | string | null | undefined | — | Controlled focus-mode pane. |
mode? | WorkspaceMode | undefined | — | Force a mode. By default the viewport decides: panes at 64rem and up. |
onFocusedIdChange? | ((id: string | null) => void) | undefined | — |
InspectorPane props
Type: InspectorPaneProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
actions? | ReactNode | — | |
children | ReactNode | — | |
defaultOpen? | boolean | undefined | — | |
defaultSize? | string | number | undefined | — | |
description? | ReactNode | — | |
footer? | ReactNode | — | Rendered under the inspector body, e.g. a primary action row. |
hideOnStacked? | boolean | undefined | — | Remove the pane instead of stacking it on a narrow viewport. |
id | string | — | Pane id. Required: it keys the saved layout, so an implicit id would make the stored layout depend on render order. |
maxSize? | string | number | undefined | — | |
minSize? | string | number | undefined | — | |
onOpenChange? | ((open: boolean) => void) | undefined | — | |
onSizeChange? | ((collapsed: boolean) => void) | undefined | — | Reports collapse crossing zero. Used by `InspectorPane`. |
open? | boolean | undefined | — | Controlled open state. |
panelRef? | Ref<PanelImperativeHandle | null> | undefined | — | Forwarded to the engine so a consumer can expand or collapse the pane. |
side? | "start" | "end" | undefined | — | Which edge the pane sits on. Affects only the border it draws. |
title? | ReactNode | — |
WorkspaceFocusBanner props1 props
WorkspaceFocusBanner props
Type: WorkspaceFocusBannerProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
label? | string | undefined | — | Wording for the banner. |
WorkspaceGroup props7 props
WorkspaceGroup props
Type: WorkspaceGroupProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
defaultLayout? | Layout | undefined | — | Explicit starting layout, keyed by pane id. |
locked? | boolean | undefined | — | Disable dragging entirely. |
orientation? | "horizontal" | "vertical" | undefined | — | Horizontal splits side by side; vertical splits stack. |
panelIds? | readonly string[] | undefined | — | Panel ids the stored layout must match. Derived from the children by default. |
persistKey? | string | undefined | — | Persist this group's layout locally under this key. Omit to keep the layout for the session only. |
persistVersion? | number | undefined | — | |
storage? | KeyValueStorage | undefined | — |
WorkspacePane props9 props
WorkspacePane props
Type: WorkspacePaneProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
collapsedSize? | string | number | undefined | — | |
collapsible? | boolean | undefined | — | Allow the pane to be dragged down to `collapsedSize`. |
defaultSize? | string | number | undefined | — | |
hideOnStacked? | boolean | undefined | — | Remove the pane instead of stacking it on a narrow viewport. |
id | string | — | Required: pane ids key the saved layout. |
maxSize? | string | number | undefined | — | |
minSize? | string | number | undefined | — | |
onSizeChange? | ((collapsed: boolean) => void) | undefined | — | Reports collapse crossing zero. Used by `InspectorPane`. |
panelRef? | Ref<PanelImperativeHandle | null> | undefined | — | Forwarded to the engine so a consumer can expand or collapse the pane. |
WorkspacePaneBody props2 props
WorkspacePaneBody props
Type: WorkspacePaneBodyProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
ariaLabel? | string | undefined | — | Accessible name for the scrollable region. |
flush? | boolean | undefined | — | Remove the padding for a pane that fills itself, e.g. a chart or a table. |
WorkspacePaneHeader props6 props
WorkspacePaneHeader props
Type: WorkspacePaneHeaderProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
actions? | ReactNode | — | Trailing controls. |
description? | ReactNode | — | |
icon? | ReactNode | — | Leading marker, e.g. an icon. Decorative. |
paneId? | string | undefined | — | Adds a focus-mode toggle for this pane. |
showReset? | boolean | undefined | — | Adds a reset action when the group persists its layout. |
title | ReactNode | — |
WorkspaceResizeHandle props1 props
WorkspaceResizeHandle props
Type: WorkspaceResizeHandleProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
label? | string | undefined | — | Accessible name; a separator without one is hard to use by voice. |
InspectorRow props3 props
InspectorRow props
Type: InspectorRowProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
label | ReactNode | — | |
numeric? | boolean | undefined | — | Use tabular figures for the value. |
value | ReactNode | — |
InspectorSection props1 props
InspectorSection props
Type: InspectorSectionProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
label | ReactNode | — |
InspectorToggle props4 props
InspectorToggle props
Type: InspectorToggleProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
className? | string | undefined | — | |
label? | string | undefined | — | Accessible name. Defaults to open/close wording. |
onOpenChange | (open: boolean) => void | — | |
open | boolean | — |
Related