Private
Public Access
Room info panel is now user-resizable (fixing a layout clip at narrow widths), and every user-selection spot (room membership, admin ownership transfer) uses a new searchable UserPicker instead of raw text input or prompt(). Member rows fold role + actions into a single inline dropdown instead of a row of buttons, so the member list stays usable as rooms grow. Room invites (the accept/decline flow) are replaced by adding a user to a room directly -- an admin/owner picks someone and they're a member immediately, with a "you've been added" notification email instead of an invite email. Drops the now-unused room_invites table.
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState, type PointerEvent } from 'react'
|
|
|
|
interface UseResizableWidthOptions {
|
|
storageKey: string
|
|
defaultWidth: number
|
|
min: number
|
|
max: number
|
|
}
|
|
|
|
// Right-anchored resizable panel: width is the distance from the cursor to
|
|
// the viewport's right edge, so a handle on the panel's left edge drags
|
|
// naturally. Persists to localStorage so it survives a reload.
|
|
export function useResizableWidth({ storageKey, defaultWidth, min, max }: UseResizableWidthOptions) {
|
|
const [width, setWidth] = useState(() => {
|
|
const stored = Number(localStorage.getItem(storageKey))
|
|
return stored >= min && stored <= max ? stored : defaultWidth
|
|
})
|
|
const widthRef = useRef(width)
|
|
widthRef.current = width
|
|
const draggingRef = useRef(false)
|
|
|
|
useEffect(() => {
|
|
function handleMove(e: PointerEvent<Window> | globalThis.PointerEvent) {
|
|
if (!draggingRef.current) return
|
|
const next = Math.min(max, Math.max(min, window.innerWidth - e.clientX))
|
|
setWidth(next)
|
|
}
|
|
function handleUp() {
|
|
if (!draggingRef.current) return
|
|
draggingRef.current = false
|
|
localStorage.setItem(storageKey, String(widthRef.current))
|
|
}
|
|
window.addEventListener('pointermove', handleMove as (e: globalThis.PointerEvent) => void)
|
|
window.addEventListener('pointerup', handleUp)
|
|
return () => {
|
|
window.removeEventListener('pointermove', handleMove as (e: globalThis.PointerEvent) => void)
|
|
window.removeEventListener('pointerup', handleUp)
|
|
}
|
|
}, [max, min, storageKey])
|
|
|
|
const startResize = useCallback((e: PointerEvent) => {
|
|
e.preventDefault()
|
|
draggingRef.current = true
|
|
}, [])
|
|
|
|
return { width, startResize }
|
|
}
|