diff --git a/docs/superpowers/plans/2026-04-15-sidebar-section-layout.md b/docs/superpowers/plans/2026-04-15-sidebar-section-layout.md new file mode 100644 index 0000000..f78913a --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-sidebar-section-layout.md @@ -0,0 +1,541 @@ +# Sidebar Section Layout Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `position` and `maxHeight` props to `Sidebar.Section` so sections can stack at the top or bottom of the sidebar with optional scrollable content areas, and style all scrollbars to match the dark sidebar aesthetic. + +**Architecture:** Extend the existing flexbox column layout inside `SidebarRoot` by partitioning section children into top/bottom groups with a flex spacer between them. Each group wrapper scrolls independently when the viewport is short. `SidebarSection` gets a content wrapper div that accepts an inline `maxHeight` and scrolls its children. Custom thin scrollbar styles are applied via CSS. + +**Tech Stack:** React, CSS Modules, Vitest + React Testing Library + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|----------------| +| `src/design-system/layout/Sidebar/Sidebar.tsx` | Modify | Add `position` and `maxHeight` props to `SidebarSectionProps`, wrap children in `.sectionContent` div, partition children in `SidebarRoot` into top/bottom groups | +| `src/design-system/layout/Sidebar/Sidebar.module.css` | Modify | Add `.sectionGroup`, `.sectionSpacer`, `.sectionContent` classes, custom scrollbar styles | +| `src/design-system/layout/Sidebar/Sidebar.test.tsx` | Modify | Add tests for position partitioning, maxHeight content wrapper, spacer rendering, collapsed behavior | + +--- + +### Task 1: Add CSS classes for section groups, spacer, content wrapper, and scrollbars + +**Files:** +- Modify: `src/design-system/layout/Sidebar/Sidebar.module.css:392` (before the bottom links section) + +- [ ] **Step 1: Add `.sectionGroup`, `.sectionSpacer`, `.sectionContent`, and scrollbar styles** + +Insert the following block before the `/* ── Bottom links */` comment at line 392: + +```css +/* ── Section groups (top/bottom positioning) ───────────────────────────── */ + +.sectionGroup { + flex: 0 1 auto; + overflow-y: auto; + min-height: 0; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.15) transparent; +} + +.sectionGroup::-webkit-scrollbar { + width: 4px; +} + +.sectionGroup::-webkit-scrollbar-track { + background: transparent; +} + +.sectionGroup::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 2px; +} + +.sectionGroup::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +.sectionSpacer { + flex: 1 0 0; +} + +/* ── Section content (scrollable maxHeight) ────────────────────────────── */ + +.sectionContent { + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.15) transparent; +} + +.sectionContent::-webkit-scrollbar { + width: 4px; +} + +.sectionContent::-webkit-scrollbar-track { + background: transparent; +} + +.sectionContent::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 2px; +} + +.sectionContent::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/design-system/layout/Sidebar/Sidebar.module.css +git commit -m "style: add section group, spacer, content, and scrollbar CSS classes" +``` + +--- + +### Task 2: Add `maxHeight` prop and content wrapper to `SidebarSection` + +**Files:** +- Modify: `src/design-system/layout/Sidebar/Sidebar.tsx:21-29` (SidebarSectionProps) +- Modify: `src/design-system/layout/Sidebar/Sidebar.tsx:78-131` (SidebarSection component) +- Test: `src/design-system/layout/Sidebar/Sidebar.test.tsx` + +- [ ] **Step 1: Write the failing tests** + +Add the following tests to the end of the `describe` block in `Sidebar.test.tsx`: + +```tsx +// 17. renders sectionContent wrapper with maxHeight when open +it('renders section content wrapper with maxHeight style when open', () => { + const { container } = render( + + + ic} + label="Apps" + open + onToggle={vi.fn()} + maxHeight="200px" + > +
child
+
+
+
, + ) + + const contentWrapper = container.querySelector('.sectionContent') + expect(contentWrapper).toBeInTheDocument() + expect(contentWrapper).toHaveStyle({ maxHeight: '200px' }) + expect(screen.getByText('child')).toBeInTheDocument() +}) + +// 18. renders sectionContent wrapper without maxHeight when not provided +it('renders section content wrapper without inline maxHeight when maxHeight is not provided', () => { + const { container } = render( + + + ic} + label="Apps" + open + onToggle={vi.fn()} + > +
child
+
+
+
, + ) + + const contentWrapper = container.querySelector('.sectionContent') + expect(contentWrapper).toBeInTheDocument() + expect(contentWrapper).not.toHaveStyle({ maxHeight: '200px' }) + expect(screen.getByText('child')).toBeInTheDocument() +}) + +// 19. does not render sectionContent wrapper when section is closed +it('does not render section content wrapper when section is closed', () => { + const { container } = render( + + + ic} + label="Apps" + open={false} + onToggle={vi.fn()} + maxHeight="200px" + > +
child
+
+
+
, + ) + + const contentWrapper = container.querySelector('.sectionContent') + expect(contentWrapper).not.toBeInTheDocument() +}) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx vitest run src/design-system/layout/Sidebar/Sidebar.test.tsx` +Expected: FAIL — `maxHeight` prop is not recognized, no `.sectionContent` wrapper exists. + +- [ ] **Step 3: Add `position` and `maxHeight` to `SidebarSectionProps`** + +Update the interface at line 21: + +```tsx +interface SidebarSectionProps { + icon: ReactNode + label: string + open: boolean + onToggle: () => void + active?: boolean + children: ReactNode + className?: string + position?: 'top' | 'bottom' + maxHeight?: string +} +``` + +- [ ] **Step 4: Update `SidebarSection` to destructure new props and wrap children** + +Update the function signature at line 78 to destructure the new props (they are accepted but `position` is only used by `SidebarRoot`): + +```tsx +function SidebarSection({ + icon, + label, + open, + onToggle, + active, + children, + className, + position: _position, + maxHeight, +}: SidebarSectionProps) { +``` + +Then replace `{open && children}` (line 128) with: + +```tsx +{open && ( +
+ {children} +
+)} +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `npx vitest run src/design-system/layout/Sidebar/Sidebar.test.tsx` +Expected: ALL PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/design-system/layout/Sidebar/Sidebar.tsx src/design-system/layout/Sidebar/Sidebar.test.tsx +git commit -m "feat: add maxHeight prop with sectionContent wrapper to SidebarSection" +``` + +--- + +### Task 3: Partition children in `SidebarRoot` into top/bottom groups + +**Files:** +- Modify: `src/design-system/layout/Sidebar/Sidebar.tsx:167-244` (SidebarRoot component) +- Test: `src/design-system/layout/Sidebar/Sidebar.test.tsx` + +- [ ] **Step 1: Write the failing tests** + +Add the following tests to the end of the `describe` block in `Sidebar.test.tsx`: + +```tsx +// 20. renders top sections in sectionGroup wrapper +it('renders sections in top group wrapper by default', () => { + const { container } = render( + + + ic} label="Apps" open onToggle={vi.fn()}> +
apps content
+
+
+
, + ) + + const topGroup = container.querySelector('.sectionGroup') + expect(topGroup).toBeInTheDocument() + expect(topGroup!.textContent).toContain('Apps') +}) + +// 21. renders bottom sections in separate group with spacer +it('renders bottom sections in a separate group with spacer between', () => { + const { container } = render( + + + ic} label="Apps" open onToggle={vi.fn()}> +
apps content
+
+ ic} label="Routes" open onToggle={vi.fn()} position="bottom"> +
routes content
+
+
+
, + ) + + const groups = container.querySelectorAll('.sectionGroup') + expect(groups).toHaveLength(2) + expect(groups[0].textContent).toContain('Apps') + expect(groups[1].textContent).toContain('Routes') + + const spacer = container.querySelector('.sectionSpacer') + expect(spacer).toBeInTheDocument() +}) + +// 22. does not render spacer when no bottom sections +it('does not render spacer when all sections are top', () => { + const { container } = render( + + + ic} label="Apps" open onToggle={vi.fn()}> +
apps content
+
+ ic} label="Agents" open onToggle={vi.fn()}> +
agents content
+
+
+
, + ) + + const spacer = container.querySelector('.sectionSpacer') + expect(spacer).not.toBeInTheDocument() +}) + +// 23. collapsed sidebar renders bottom sections in bottom group +it('renders bottom sections in bottom group when sidebar is collapsed', () => { + const { container } = render( + + + ic} label="Apps" open={false} onToggle={vi.fn()}> +
apps
+
+ ic} label="Routes" open={false} onToggle={vi.fn()} position="bottom"> +
routes
+
+
+
, + ) + + const groups = container.querySelectorAll('.sectionGroup') + expect(groups).toHaveLength(2) + + // Bottom group should contain the Routes rail item + const bottomGroup = groups[1] + expect(bottomGroup.querySelector('[title="Routes"]')).toBeInTheDocument() +}) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx vitest run src/design-system/layout/Sidebar/Sidebar.test.tsx` +Expected: FAIL — no `.sectionGroup` wrappers exist yet, sections render directly in `{rest}`. + +- [ ] **Step 3: Rewrite the child partitioning logic in `SidebarRoot`** + +Replace the entire IIFE inside the `