Private
Public Access
Add collapse/expand for the DM and Room sidebar sections (#62)
Each section header is now a button with a disclosure chevron; collapsed/expanded state persists per section in localStorage (same pattern as the existing sidebar-width preference) and the two sections toggle independently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<string> {
|
||||
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<string>): 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 (
|
||||
<button type="button" className="sidebar-section-label" onClick={onToggle} aria-expanded={!collapsed}>
|
||||
<svg
|
||||
className={`sidebar-section-chevron${collapsed ? ' sidebar-section-chevron-collapsed' : ''}`}
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M2 3.5 5 7 8 3.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.4"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
rooms: MyRoomItem[]
|
||||
activeRoomId: string | undefined
|
||||
@@ -24,6 +80,18 @@ export function Sidebar({
|
||||
onOpenPeople,
|
||||
unavailableOffline,
|
||||
}: SidebarProps) {
|
||||
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(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,7 +171,12 @@ export function Sidebar({
|
||||
<>
|
||||
{directMessages.length > 0 && (
|
||||
<>
|
||||
<div className="sidebar-section-label">Direct Messages</div>
|
||||
<SidebarSectionHeader
|
||||
label="Direct Messages"
|
||||
collapsed={collapsedSections.has('dm')}
|
||||
onToggle={() => toggleSection('dm')}
|
||||
/>
|
||||
{!collapsedSections.has('dm') && (
|
||||
<nav>
|
||||
{directMessages.map((room, i) => (
|
||||
<RoomRow
|
||||
@@ -114,9 +187,17 @@ export function Sidebar({
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{regularRooms.length > 0 && <div className="sidebar-section-label">Rooms</div>}
|
||||
{regularRooms.length > 0 && (
|
||||
<>
|
||||
<SidebarSectionHeader
|
||||
label="Rooms"
|
||||
collapsed={collapsedSections.has('rooms')}
|
||||
onToggle={() => toggleSection('rooms')}
|
||||
/>
|
||||
{!collapsedSections.has('rooms') && (
|
||||
<nav>
|
||||
{regularRooms.map((room, i) => (
|
||||
<RoomRow
|
||||
@@ -127,6 +208,9 @@ export function Sidebar({
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user