Corpora UI
Blocks

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.

Transcription
Witness α — fol. 12r, lines 3–9
Witness β — fol. 12r, lines 3–9
Witness γ — fol. 12r, lines 3–9
Transcription

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").hiddenPanelIds

API notes

PropTypeDefaultDescription
Root · scaffoldId / inspectorOpen / defaultInspectorOpen / onInspectorOpenChangestring / 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 / onCloseSecondarystring / 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 / closeLabelReactNode / 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 / childrenstring / 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

On this page