# 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(
    <Wrapper>
      <Sidebar>
        <Sidebar.Section
          icon={<span>ic</span>}
          label="Apps"
          open
          onToggle={vi.fn()}
          maxHeight="200px"
        >
          <div>child</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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(
    <Wrapper>
      <Sidebar>
        <Sidebar.Section
          icon={<span>ic</span>}
          label="Apps"
          open
          onToggle={vi.fn()}
        >
          <div>child</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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(
    <Wrapper>
      <Sidebar>
        <Sidebar.Section
          icon={<span>ic</span>}
          label="Apps"
          open={false}
          onToggle={vi.fn()}
          maxHeight="200px"
        >
          <div>child</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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 && (
  <div
    className={styles.sectionContent}
    style={maxHeight ? { maxHeight } : undefined}
  >
    {children}
  </div>
)}
```

- [ ] **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(
    <Wrapper>
      <Sidebar>
        <Sidebar.Section icon={<span>ic</span>} label="Apps" open onToggle={vi.fn()}>
          <div>apps content</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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(
    <Wrapper>
      <Sidebar>
        <Sidebar.Section icon={<span>ic</span>} label="Apps" open onToggle={vi.fn()}>
          <div>apps content</div>
        </Sidebar.Section>
        <Sidebar.Section icon={<span>ic</span>} label="Routes" open onToggle={vi.fn()} position="bottom">
          <div>routes content</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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(
    <Wrapper>
      <Sidebar>
        <Sidebar.Section icon={<span>ic</span>} label="Apps" open onToggle={vi.fn()}>
          <div>apps content</div>
        </Sidebar.Section>
        <Sidebar.Section icon={<span>ic</span>} label="Agents" open onToggle={vi.fn()}>
          <div>agents content</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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(
    <Wrapper>
      <Sidebar collapsed onCollapseToggle={vi.fn()}>
        <Sidebar.Section icon={<span>ic</span>} label="Apps" open={false} onToggle={vi.fn()}>
          <div>apps</div>
        </Sidebar.Section>
        <Sidebar.Section icon={<span>ic</span>} label="Routes" open={false} onToggle={vi.fn()} position="bottom">
          <div>routes</div>
        </Sidebar.Section>
      </Sidebar>
    </Wrapper>,
  )

  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 `<aside>` (lines 198–240) with the following:

```tsx
{(() => {
  const childArray = Children.toArray(children)

  // Extract header
  const headerIdx = childArray.findIndex(
    (child) => isValidElement(child) && child.type === SidebarHeader,
  )
  const header = headerIdx >= 0 ? childArray[headerIdx] : null
  const rest = headerIdx >= 0
    ? [...childArray.slice(0, headerIdx), ...childArray.slice(headerIdx + 1)]
    : childArray

  // Extract footer
  const footerIdx = rest.findIndex(
    (child) => isValidElement(child) && child.type === SidebarFooter,
  )
  const footer = footerIdx >= 0 ? rest[footerIdx] : null
  const sections = footerIdx >= 0
    ? [...rest.slice(0, footerIdx), ...rest.slice(footerIdx + 1)]
    : rest

  // Partition sections into top/bottom by position prop
  const topSections: typeof sections = []
  const bottomSections: typeof sections = []
  for (const child of sections) {
    if (
      isValidElement<SidebarSectionProps>(child) &&
      child.props.position === 'bottom'
    ) {
      bottomSections.push(child)
    } else {
      topSections.push(child)
    }
  }

  const hasBottomSections = bottomSections.length > 0

  return (
    <>
      {header}
      {onSearchChange && !collapsed && (
        <div className={styles.searchWrap}>
          <div className={styles.searchInner}>
            <span className={styles.searchIcon} aria-hidden="true">
              <Search size={12} />
            </span>
            <input
              className={styles.searchInput}
              type="text"
              placeholder="Filter..."
              value={searchValue ?? ''}
              onChange={(e) => onSearchChange(e.target.value)}
            />
            {searchValue && (
              <button
                type="button"
                className={styles.searchClear}
                onClick={() => onSearchChange('')}
                aria-label="Clear search"
              >
                <X size={12} />
              </button>
            )}
          </div>
        </div>
      )}
      <div className={styles.sectionGroup}>
        {topSections}
      </div>
      {hasBottomSections && <div className={styles.sectionSpacer} />}
      {hasBottomSections && (
        <div className={styles.sectionGroup}>
          {bottomSections}
        </div>
      )}
      {footer}
    </>
  )
})()}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `npx vitest run src/design-system/layout/Sidebar/Sidebar.test.tsx`
Expected: ALL PASS

- [ ] **Step 5: Run the full test suite to check for regressions**

Run: `npx vitest run`
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: partition sidebar sections into top/bottom groups with spacer"
```

---

### Task 4: Update `LayoutShell` to use `position="bottom"` on Routes and Starred sections

**Files:**
- Modify: `src/layout/LayoutShell.tsx:378-429` (the Routes and Starred section declarations)

- [ ] **Step 1: Add `position="bottom"` to the Routes section**

In `LayoutShell.tsx`, find the Routes `<Sidebar.Section>` (around line 378) and add `position="bottom"`:

```tsx
<Sidebar.Section
  label="Routes"
  icon={<GitBranch size={14} />}
  open={!routesCollapsed}
  onToggle={toggleRoutesCollapsed}
  active={location.pathname.startsWith('/routes')}
  position="bottom"
>
```

- [ ] **Step 2: Add `position="bottom"` to the Starred section**

Find the Starred `<Sidebar.Section>` (around line 396) and add `position="bottom"`:

```tsx
{hasStarred && (
  <Sidebar.Section
    label="★ Starred"
    icon={<span />}
    open={true}
    onToggle={() => {}}
    active={false}
    position="bottom"
  >
```

- [ ] **Step 3: Run the full test suite**

Run: `npx vitest run`
Expected: ALL PASS

- [ ] **Step 4: Commit**

```bash
git add src/layout/LayoutShell.tsx
git commit -m "feat: position Routes and Starred sections at bottom of sidebar"
```

---

### Task 5: Visual verification in the browser

**Files:** None (manual verification)

- [ ] **Step 1: Start the dev server**

Run: `npm run dev`

- [ ] **Step 2: Verify top/bottom layout**

Open the app in a browser. Confirm:
- Applications and Agents sections are at the top
- Routes and Starred sections are at the bottom, above the footer
- A flexible gap separates the two groups

- [ ] **Step 3: Verify scrollbar styling**

If a section has enough content to overflow (or temporarily add `maxHeight="100px"` to the Applications section in `LayoutShell.tsx`), confirm:
- Content scrolls within the section
- Scrollbar is thin (4px), with a muted thumb on a transparent track
- Scrollbar thumb brightens on hover

- [ ] **Step 4: Verify collapsed sidebar**

Collapse the sidebar and confirm:
- Top section icons stay at the top
- Bottom section icons cluster near the footer
- Spacer separates the two groups in the rail

- [ ] **Step 5: Verify short viewport**

Resize the browser window very short. Confirm:
- Both top and bottom groups scroll independently
- No content is permanently clipped or inaccessible

- [ ] **Step 6: Remove any temporary `maxHeight` props added for testing**

If you added `maxHeight="100px"` to Applications in Step 3, remove it now.
