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 {
|
.sidebar-section-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
padding: 10px 16px 6px;
|
padding: 10px 16px 6px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--ds-muted);
|
color: var(--ds-muted);
|
||||||
text-transform: uppercase;
|
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 {
|
.sidebar-offline-note {
|
||||||
|
|||||||
@@ -1,8 +1,64 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useResizableWidth } from '../hooks/useResizableWidth'
|
import { useResizableWidth } from '../hooks/useResizableWidth'
|
||||||
import type { MyRoomItem } from '../types'
|
import type { MyRoomItem } from '../types'
|
||||||
import { RoomRow } from './RoomRow'
|
import { RoomRow } from './RoomRow'
|
||||||
import './Sidebar.css'
|
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 {
|
interface SidebarProps {
|
||||||
rooms: MyRoomItem[]
|
rooms: MyRoomItem[]
|
||||||
activeRoomId: string | undefined
|
activeRoomId: string | undefined
|
||||||
@@ -24,6 +80,18 @@ export function Sidebar({
|
|||||||
onOpenPeople,
|
onOpenPeople,
|
||||||
unavailableOffline,
|
unavailableOffline,
|
||||||
}: SidebarProps) {
|
}: 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()
|
const query = searchQuery.trim().toLowerCase()
|
||||||
// A DM's `name` is an internal token, never what a user would search for
|
// A DM's `name` is an internal token, never what a user would search for
|
||||||
// -- matched against the partner's display name/username instead.
|
// -- matched against the partner's display name/username instead.
|
||||||
@@ -103,7 +171,12 @@ export function Sidebar({
|
|||||||
<>
|
<>
|
||||||
{directMessages.length > 0 && (
|
{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>
|
<nav>
|
||||||
{directMessages.map((room, i) => (
|
{directMessages.map((room, i) => (
|
||||||
<RoomRow
|
<RoomRow
|
||||||
@@ -114,9 +187,17 @@ export function Sidebar({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</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>
|
<nav>
|
||||||
{regularRooms.map((room, i) => (
|
{regularRooms.map((room, i) => (
|
||||||
<RoomRow
|
<RoomRow
|
||||||
@@ -127,6 +208,9 @@ export function Sidebar({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user