diff --git a/frontend/src/components/Sidebar.css b/frontend/src/components/Sidebar.css index 5fec284..879be0f 100644 --- a/frontend/src/components/Sidebar.css +++ b/frontend/src/components/Sidebar.css @@ -99,12 +99,35 @@ } .sidebar-section-label { + display: flex; + align-items: center; + gap: 6px; + width: 100%; padding: 10px 16px 6px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; color: var(--ds-muted); text-transform: uppercase; + background: transparent; + border: none; + cursor: pointer; + text-align: left; +} + +.sidebar-section-label:hover { + color: var(--ds-text); +} + +.sidebar-section-chevron { + flex: none; + /* Points down (expanded) by default -- rotated to point right when the + section is collapsed, the standard disclosure-triangle convention. */ + transition: transform 0.15s ease; +} + +.sidebar-section-chevron-collapsed { + transform: rotate(-90deg); } .sidebar-offline-note { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 84957fc..9696e88 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,8 +1,64 @@ +import { useState } from 'react' import { useResizableWidth } from '../hooks/useResizableWidth' import type { MyRoomItem } from '../types' import { RoomRow } from './RoomRow' import './Sidebar.css' +// #62: which of the two sections (keyed 'dm'/'rooms') are collapsed -- +// persisted the same way sidebar-width already is (see useResizableWidth), +// a per-viewer cosmetic preference with no reason to live server-side. +const COLLAPSED_SECTIONS_KEY = 'sidebar-collapsed-sections' + +function loadCollapsedSections(): Set { + try { + const raw = localStorage.getItem(COLLAPSED_SECTIONS_KEY) + if (!raw) return new Set() + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? new Set(parsed.filter((s) => typeof s === 'string')) : new Set() + } catch { + return new Set() + } +} + +function saveCollapsedSections(sections: Set): void { + try { + localStorage.setItem(COLLAPSED_SECTIONS_KEY, JSON.stringify([...sections])) + } catch { + // storage unavailable (private browsing, quota) -- collapse state just + // won't persist this session, not fatal. + } +} + +interface SidebarSectionHeaderProps { + label: string + collapsed: boolean + onToggle: () => void +} + +function SidebarSectionHeader({ label, collapsed, onToggle }: SidebarSectionHeaderProps) { + return ( + + ) +} + interface SidebarProps { rooms: MyRoomItem[] activeRoomId: string | undefined @@ -24,6 +80,18 @@ export function Sidebar({ onOpenPeople, unavailableOffline, }: SidebarProps) { + const [collapsedSections, setCollapsedSections] = useState>(loadCollapsedSections) + + function toggleSection(key: string) { + setCollapsedSections((prev) => { + const next = new Set(prev) + if (next.has(key)) next.delete(key) + else next.add(key) + saveCollapsedSections(next) + return next + }) + } + const query = searchQuery.trim().toLowerCase() // A DM's `name` is an internal token, never what a user would search for // -- matched against the partner's display name/username instead. @@ -103,30 +171,46 @@ export function Sidebar({ <> {directMessages.length > 0 && ( <> -
Direct Messages
- + toggleSection('dm')} + /> + {!collapsedSections.has('dm') && ( + + )} )} - {regularRooms.length > 0 &&
Rooms
} - + {!collapsedSections.has('rooms') && ( + + )} + + )} )}