Portable side panel
The components exported by @/components/side-panel are controlled, domain-free navigation primitives. They do not import task data, APIs, routing, feature flags, or persistence.
Composition boundary
SidePanelFramerenders docked, sheet, or embedded chrome and applies the selected content layout.SidePanelTabsandSidePanelTabrender projected visual tab data and own keyboard navigation, close focus recovery, overflow visibility, and pointer/keyboard reordering.SidePanelLauncherrenders caller-provided sections from the same model as either a plus-button popover or the empty panel body.useSidePanelTabs<TPayload>owns serializable open/select/close/reorder/reset state. Its optionalonStateChangecallback is the persistence boundary; it never reads storage itself.- The host adapter owns payload validation, data queries, URL synchronization, content rendering, and persistence scope.
Never put React nodes or fetched content in a persisted SidePanelTabRecord. Project icons and live status into SidePanelTabItem at render time.
SidePanelTabs also accepts appearance="streamlined-task" for the experimental task-detail shell. That appearance preserves the approved pre-rebase Codex-inspired treatment—equal-width text tabs, separated surfaces, edge fades, and hover-revealed close actions—while the default appearance remains master's portable side-panel chrome.
Use headerSize="task-detail" on SidePanelFrame when the panel shares the 60px task breadcrumb row. It keeps the task-detail footer treatment while matching the default portable header height.
SidePanelWindowControls defaults to the production visibility-toggle icon. Streamlined task-detail panels use closeControl="close" for an explicit X while retaining the same close behavior and accessible labeling.
The Streamlined task adapter registers a counted Subtasks tab only when the current task has child tasks. Closing it moves the surface back into the + launcher for the remainder of the session; tasks without children do not advertise the tab.
Registering another page
- Define a serializable payload union for that page, such as
details | activity | history. - Build stable
SidePanelTabRecord<PagePayload>descriptors and initializeuseSidePanelTabswith validated state. - Project descriptors into
SidePanelTabItemvalues for the tab strip. - Render active content in the page adapter by switching on
payload.kind. - Supply the same launcher sections to the plus popover and empty panel.
- If state is persisted, version and scope it in the adapter and pass writes through
onStateChange.
Accessibility and review
The tab strip uses the tabs pattern with roving focus, Arrow/Home/End navigation, named close actions, middle-click close, and live reorder/close announcements. Closing the active tab chooses the right neighbor, then the left; closing the final tab sends focus to the launcher. Reduced-motion preferences disable component transitions and smooth overflow scrolling.
Review Navigation/Side Panel in Storybook before changing production adapters. Compare light/dark, minimum/default/wide/maximized, mobile, overflow, empty, launcher, unavailable-resource, and task-composition stories. Tune spacing, radius, contrast, icon size, and motion centrally through the side-panel tokens in ui/src/index.css.