diff --git a/docs/superpowers/specs/2026-04-15-sidebar-section-layout-design.md b/docs/superpowers/specs/2026-04-15-sidebar-section-layout-design.md new file mode 100644 index 0000000..55b64d1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-15-sidebar-section-layout-design.md @@ -0,0 +1,187 @@ +# Sidebar Section Layout: Top/Bottom Positioning & Scrollable Sections + +## Summary + +Extend `Sidebar.Section` with two new optional props: + +- `position: 'top' | 'bottom'` — controls whether a section stacks from the top of the sidebar or from the bottom (above the footer). Default: `'top'`. +- `maxHeight: string` — CSS length value (e.g. `"250px"`, `"30vh"`) that constrains the section's content area. When content exceeds this height, it scrolls. The section header/toggle always remains visible. + +This enables a layout where primary sections (Applications, Agents) occupy the top of the sidebar, while secondary sections (Routes, Starred) cluster near the bottom — with a flexible spacer absorbing remaining vertical space between the two groups. + +## API Changes + +### `Sidebar.Section` — new optional props + +```tsx +interface SidebarSectionProps { + icon: ReactNode + label: string + open: boolean + onToggle: () => void + active?: boolean + children: ReactNode + className?: string + position?: 'top' | 'bottom' // default: 'top' + maxHeight?: string // CSS length, e.g. "250px", "30vh" +} +``` + +No changes to `Sidebar.Header`, `Sidebar.Footer`, `Sidebar.FooterLink`, or `SidebarRoot` props. + +### Consumer usage + +```tsx + + + + + + + + + + + + + + + + + ... + + + ... + +``` + +## Layout Model + +### SidebarRoot child partitioning + +`SidebarRoot` already inspects children to extract `Header`. This extends the same pattern: + +1. Extract `Header` children (existing) +2. Extract `Footer` children +3. Partition remaining children into `topSections` and `bottomSections` based on `position` prop (default `'top'`) + +### Render structure + +``` + +``` + +When there are no bottom sections, the spacer is omitted. The layout behaves identically to today — footer's `margin-top: auto` handles positioning. Zero breaking changes for existing consumers. + +### SidebarSection content wrapper + +A new `.sectionContent` div wraps only the `children` inside `SidebarSection` (not the toggle header). When `maxHeight` is provided, it receives the value as an inline `max-height` style. + +```tsx +{open && ( +
+ {children} +
+)} +``` + +## CSS Changes + +### Group wrappers + +```css +.sectionGroup { + flex: 0 1 auto; + overflow-y: auto; + min-height: 0; +} + +.sectionSpacer { + flex: 1 0 0; +} +``` + +### Section content scrolling + +```css +.sectionContent { + overflow-y: auto; +} +``` + +`maxHeight` is applied as an inline style, not a CSS class, since it varies per section instance. + +### Custom scrollbars + +Applied to both `.sectionGroup` and `.sectionContent` to keep the dark sidebar aesthetic: + +```css +/* Standard (Firefox, modern Chrome/Edge) */ +scrollbar-width: thin; +scrollbar-color: rgba(255, 255, 255, 0.15) transparent; + +/* WebKit (Safari, older Chrome) */ +::-webkit-scrollbar { + width: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 2px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} +``` + +## Collapsed Sidebar Behavior + +In collapsed (icon rail) mode, sections render as single icon buttons with no scrollable content: + +- **`position`**: Respected — bottom sections render in the bottom group, so their icons cluster near the footer in the rail. +- **`maxHeight`**: Ignored — no content to constrain. + +The group wrapper and spacer structure remains active in collapsed mode for consistent icon positioning. + +## Edge Cases + +| Scenario | Behavior | +|----------|----------| +| All sections `"top"` (no bottom sections) | No spacer rendered. Identical to current layout. | +| All sections `"bottom"` | Top group empty. Sections cluster above footer. | +| `maxHeight` set but content is shorter | No visual effect — wrapper is naturally smaller than max. | +| Very short viewport | Both group wrappers scroll independently via `overflow-y: auto` on `.sectionGroup`. | + +## Files Changed + +- `src/design-system/layout/Sidebar/Sidebar.tsx` — add `position` and `maxHeight` props to `SidebarSectionProps`, add `sectionContent` wrapper to `SidebarSection`, partition children in `SidebarRoot` +- `src/design-system/layout/Sidebar/Sidebar.module.css` — add `.sectionGroup`, `.sectionSpacer`, `.sectionContent`, custom scrollbar styles +- `src/design-system/layout/Sidebar/Sidebar.test.tsx` — new test cases for positioning and scrolling behavior + +## Tests + +- Section with `maxHeight` renders content wrapper with correct inline style and `overflow-y: auto` +- Sections with `position="bottom"` render inside the bottom group wrapper +- Default `position` (omitted) renders in the top group +- When no bottom sections exist, no spacer is rendered +- Collapsed sidebar renders bottom sections in the bottom group