Make the room list sidebar resizable

Generalizes useResizableWidth with a left/right anchor option and wires
it into the sidebar the same way the room info panel already uses it,
so both sides of the chat shell can be dragged to a comfortable width.
This commit is contained in:
2026-08-14 20:45:07 -06:00
parent 91589ee647
commit 8e3b6a16bd
3 changed files with 47 additions and 8 deletions
+20 -2
View File
@@ -1,6 +1,8 @@
.sidebar { .sidebar {
width: 300px; position: relative;
min-width: 300px; flex: none;
min-width: 220px;
max-width: 480px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: 1px solid var(--ds-border); border-right: 1px solid var(--ds-border);
@@ -8,6 +10,22 @@
min-height: 0; min-height: 0;
} }
.sidebar-resize-handle {
position: absolute;
top: 0;
bottom: 0;
right: -3px;
width: 6px;
cursor: col-resize;
z-index: 5;
touch-action: none;
}
.sidebar-resize-handle:hover,
.sidebar-resize-handle:active {
background: color-mix(in srgb, var(--ds-accent) 40%, transparent);
}
.sidebar-toolbar { .sidebar-toolbar {
padding: var(--sp-3); padding: var(--sp-3);
display: flex; display: flex;
+11 -1
View File
@@ -1,3 +1,4 @@
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'
@@ -24,8 +25,17 @@ export function Sidebar({
const query = searchQuery.trim().toLowerCase() const query = searchQuery.trim().toLowerCase()
const filtered = query ? rooms.filter((r) => r.name.toLowerCase().includes(query)) : rooms const filtered = query ? rooms.filter((r) => r.name.toLowerCase().includes(query)) : rooms
const { width, startResize } = useResizableWidth({
storageKey: 'sidebar-width',
defaultWidth: 300,
min: 220,
max: 480,
anchor: 'left',
})
return ( return (
<aside className="sidebar"> <aside className="sidebar" style={{ width }}>
<div className="sidebar-resize-handle" onPointerDown={startResize} />
<div className="sidebar-toolbar"> <div className="sidebar-toolbar">
<div className="sidebar-search"> <div className="sidebar-search">
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
+16 -5
View File
@@ -5,12 +5,22 @@ interface UseResizableWidthOptions {
defaultWidth: number defaultWidth: number
min: number min: number
max: number max: number
// 'right' (default): panel sits against the viewport's right edge, width
// is measured from the cursor to that edge -- a handle on the panel's
// LEFT edge drags naturally. 'left': panel sits against the left edge,
// width is just the cursor's x position -- a handle on the panel's RIGHT
// edge drags naturally.
anchor?: 'left' | 'right'
} }
// Right-anchored resizable panel: width is the distance from the cursor to // Persists to localStorage so it survives a reload.
// the viewport's right edge, so a handle on the panel's left edge drags export function useResizableWidth({
// naturally. Persists to localStorage so it survives a reload. storageKey,
export function useResizableWidth({ storageKey, defaultWidth, min, max }: UseResizableWidthOptions) { defaultWidth,
min,
max,
anchor = 'right',
}: UseResizableWidthOptions) {
const [width, setWidth] = useState(() => { const [width, setWidth] = useState(() => {
const stored = Number(localStorage.getItem(storageKey)) const stored = Number(localStorage.getItem(storageKey))
return stored >= min && stored <= max ? stored : defaultWidth return stored >= min && stored <= max ? stored : defaultWidth
@@ -22,7 +32,8 @@ export function useResizableWidth({ storageKey, defaultWidth, min, max }: UseRes
useEffect(() => { useEffect(() => {
function handleMove(e: PointerEvent<Window> | globalThis.PointerEvent) { function handleMove(e: PointerEvent<Window> | globalThis.PointerEvent) {
if (!draggingRef.current) return if (!draggingRef.current) return
const next = Math.min(max, Math.max(min, window.innerWidth - e.clientX)) const raw = anchor === 'left' ? e.clientX : window.innerWidth - e.clientX
const next = Math.min(max, Math.max(min, raw))
setWidth(next) setWidth(next)
} }
function handleUp() { function handleUp() {