# 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 - `SidePanelFrame` renders docked, sheet, or embedded chrome and applies the selected content layout. - `SidePanelTabs` and `SidePanelTab` render projected visual tab data and own keyboard navigation, close focus recovery, overflow visibility, and pointer/keyboard reordering. - `SidePanelLauncher` renders caller-provided sections from the same model as either a plus-button popover or the empty panel body. - `useSidePanelTabs` owns serializable open/select/close/reorder/reset state. Its optional `onStateChange` callback 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. Its left-aligned text tabs share one width: 100px when every label fits, or 125px when any label needs more room. They shrink equally as the panel narrows, then scroll once the usable width floor is reached. Clipped labels fade. The tabs keep a subtle active fill and omit dividers. The active tab shows its round close action; inactive tabs reveal theirs on hover. Every close action has its own hover highlight. The default appearance remains the 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 1. Define a serializable payload union for that page, such as `details | activity | history`. 2. Build stable `SidePanelTabRecord` descriptors and initialize `useSidePanelTabs` with validated state. 3. Project descriptors into `SidePanelTabItem` values for the tab strip. 4. Render active content in the page adapter by switching on `payload.kind`. 5. Supply the same launcher sections to the plus popover and empty panel. 6. 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`.