Scaffold
Composable desktop workspace: an icon rail on a soft gradient backdrop, a canvas of closable/swappable panels, a floating actions cluster, and an inspector drawer that slides in over the canvas.
Preview and props
Expand Props below the centered preview to change the component’s properties.
Examples
Add and switch workspace panels, then open the inspector to explore the layout.
Usage
import {
Scaffold,
SCAFFOLD_PANEL_CAPACITY,
useScaffold,
} from "@exegia/corpora-ui"
function App() {
const scaffold = useScaffold()
return (
<Scaffold.Root {...scaffold.providerProps} className="h-svh">
<Scaffold.Sidebar>{/* icon buttons */}</Scaffold.Sidebar>
<Scaffold.Main>
<Scaffold.Actions
onAdd={panels.length < SCAFFOLD_PANEL_CAPACITY ? addPanel : undefined}
>
<Scaffold.Tab
key="draft"
onClose={panels.length > 1 ? close : undefined}
>
Draft
</Scaffold.Tab>
</Scaffold.Actions>
<Scaffold.Canvas>
<Scaffold.Panel
key="draft"
SecondaryPanel={<PromptBar />}
onSwap={swap}
onCloseSecondary={hidePromptBar}
>
<Editor />
</Scaffold.Panel>
</Scaffold.Canvas>
<Scaffold.Inspector>
<Details />
</Scaffold.Inspector>
</Scaffold.Main>
</Scaffold.Root>
)
}
// scaffold.toggleInspector() from anywhere — a rail icon, a shortcut.
// Or skip the hook: name the instance and drive it by id, no controller held.
// <Scaffold.Root scaffoldId="workspace" />
// useScaffoldActions("workspace").toggleInspector()
// useScaffoldState("workspace").hiddenPanelIdsAPI notes
| Prop | Type | Default | Description |
|---|---|---|---|
| Root · scaffoldId / inspectorOpen / defaultInspectorOpen / onInspectorOpenChange | string / boolean / boolean / (open) => void | — | State lives in Jotai atom families keyed by scaffoldId — omit it and the root keys off useId, dropping its state on unmount; name it and the state outlives the component, with useScaffoldState(id) / useScaffoldActions(id) reading and driving it from anywhere (title-bar buttons, a command palette, shortcuts). useScaffold().providerProps carries an id for you. A controlled inspectorOpen stays the source of truth: store writes are gated off and actions report through onInspectorOpenChange. inspectorWidth (px, default 272) sizes the drawer and how far Actions slides aside. |
| Panel · id / SecondaryPanel / onSwap / onCloseSecondary | string / ReactNode / () => void / () => void | — | id opts the panel into responsive hiding: each id'd panel keeps at least SCAFFOLD_PANEL_MIN_WIDTH (320px), and when the canvas can't grant every panel that width the scaffold hides panels (least-recently-activated first) until the rest fit — pair with a Scaffold.Tab carrying the matching panelId. SecondaryPanel renders the utility strip as its own card below the primary surface, with the seam menu between the two — a ⋯ toggle that morphs into Expand | Swap | Close. onSwap powers Swap: a layout morph trades the two cards; flip the slot content in the callback. onCloseSecondary powers Close — remove SecondaryPanel there. Panels themselves close from the Actions tabs, and flex to share the canvas. |
| Actions · onAdd / children | () => void / ReactNode | — | The pill cluster in the top-right (the design's panel context menu). onAdd powers the Add segment, which renders only while present — omit it once the canvas holds SCAFFOLD_PANEL_CAPACITY (3) panels. Children render before Add: one Scaffold.Tab per open panel, plus any extra segments. |
| Tab · children / panelId / onClose / closeLabel | ReactNode / string / () => void / string | — | A panel's tab inside the Actions pill — its label plus a close button. With panelId (a Scaffold.Panel's id) the tab fronts that panel's visibility: a hidden panel dims its tab behind an eye-off icon, and pressing the label toggles it — showing one past capacity hides the least-recently-activated panel in its place. Hovering the tab spotlights its panel — every other id'd panel fades to 0.35 opacity. The close button renders only while onClose is present; omit it on the last remaining panel's tab so at least one panel stays open. Key each tab — closes animate out of the pill. |
| Inspector · name / children | string / ReactNode | — | Drawer content. Stays mounted and inert while closed so it can slide back out; opening also slides the Actions cluster leftward in step. |
Resize and control panels from anywhere
The workspace uses motion-panels for column edges, secondary-panel seams, and the inspector edge. Arrow keys resize a focused separator; Shift uses larger steps, Enter collapses, and double-click resets. The last visible workspace panel remains available, and secondary panels collapse to their existing strip.
Use stable panel IDs and a scaffoldId to share layout state under one ExegiaProvider:
const actions = useScaffoldActions("workspace")
const state = useScaffoldState("workspace")
actions.resizePanel("editor", 480)
actions.resizeSecondaryPanel("editor", 160)
actions.setSecondaryExpanded("editor", true)
actions.setPanelSwapped("editor", true)
actions.resizeInspector(360)
actions.toggleInspector()
actions.togglePanel("editor")
// state.panelLayouts and state.inspectorWidth retain the requested sizes.Widths are constrained to the available workspace; hidden panels retain their layout when reopened. reset() clears saved panel sizes and expansion state too.
Last updated on
Shell
Inset application shell with a collapsible sidebar, workspace header, command-style search affordance, top actions, and scrollable content area.
Chat
The chat surface end to end: the chrome-less AiPanel rail (scoped selections, generated answers, recommendation cards) and the Bubble-atom messages that carry attachments and AI content. The panel is the main example; the examples below show message exchanges, attachments and an AI reply.