Files
hapi/web/src/hooks/useSidebarResize.ts
T
TEEKandGitHub 81934cf354 fix(web): use dedicated split breakpoint for compact tablets (#1141)
* fix(web): use dedicated split breakpoint for compact tablets

Some compact Android tablets (e.g. OPPO Pad mini) report a landscape
CSS viewport below Tailwind's `lg` (1024px) despite having enough
physical screen space, so the sessions layout fell back to a single
column. Add a dedicated `split` breakpoint at 920px and use it for the
sessions split layout and the sidebar width/resize CSS, leaving the
global `lg` breakpoint (and all other pages) untouched.

* fix(web): cap sidebar width against viewport on compact split

A persisted sidebar width (up to 600px from resizing on desktop) could
shrink the detail pane to 316px at the new 920px split breakpoint, below
the previous 1024px worst case of 420px. Cap the sidebar width at
min(var(--sidebar-w), calc(100vw - 424px)) so the detail pane keeps at
least 420px down to 920px, with no effect on desktop.

* fix(web): seed sidebar drag from rendered width

When the compact-split viewport cap renders the sidebar narrower than the
persisted width, dragging the handle to shrink it had a dead zone until
the stored width fell below the rendered width. Seed the drag from the
sidebar's rendered width so it responds immediately; unchanged on desktop
where rendered and stored widths match.
2026-07-24 10:54:21 +08:00

94 lines
3.3 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react'
const STORAGE_KEY = 'hapi-sidebar-width'
const MIN_WIDTH = 280
const MAX_WIDTH = 600
const DEFAULT_WIDTH = 420
function clamp(value: number): number {
return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, value))
}
function loadWidth(): number {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = Number(stored)
if (Number.isFinite(parsed)) return clamp(parsed)
}
return DEFAULT_WIDTH
}
export function useSidebarResize() {
const [width, setWidth] = useState(loadWidth)
const [isDragging, setIsDragging] = useState(false)
const startXRef = useRef(0)
const startWidthRef = useRef(0)
const activePointerIdRef = useRef<number | null>(null)
const onPointerDown = useCallback((e: React.PointerEvent) => {
e.preventDefault()
// The sidebar (the handle's previous sibling) can render narrower than the
// stored width when the viewport cap in index.css kicks in on a compact
// split. Seed the drag from the actual rendered width so there's no dead
// zone before the sidebar responds. Falls back to the stored width when the
// element or its measured width is unavailable (e.g. non-DOM test env).
const sidebarEl = e.currentTarget.previousElementSibling as HTMLElement | null
const renderedWidth = sidebarEl?.getBoundingClientRect().width
activePointerIdRef.current = e.pointerId
startXRef.current = e.clientX
startWidthRef.current = renderedWidth || width
setIsDragging(true)
}, [width])
// Global listeners ensure pointerup is always captured even if cursor leaves the handle
useEffect(() => {
if (!isDragging) return
const onMove = (e: PointerEvent) => {
if (e.pointerId !== activePointerIdRef.current) return
const delta = e.clientX - startXRef.current
setWidth(clamp(startWidthRef.current + delta))
}
const onUp = (e: PointerEvent) => {
if (e.pointerId !== activePointerIdRef.current) return
activePointerIdRef.current = null
setIsDragging(false)
}
document.addEventListener('pointermove', onMove)
document.addEventListener('pointerup', onUp)
document.addEventListener('pointercancel', onUp)
return () => {
document.removeEventListener('pointermove', onMove)
document.removeEventListener('pointerup', onUp)
document.removeEventListener('pointercancel', onUp)
}
}, [isDragging])
// Persist width to localStorage when drag ends
useEffect(() => {
if (!isDragging) {
localStorage.setItem(STORAGE_KEY, String(width))
}
}, [isDragging, width])
// Prevent text selection while dragging
useEffect(() => {
if (isDragging) {
document.body.style.userSelect = 'none'
document.body.style.cursor = 'col-resize'
} else {
document.body.style.userSelect = ''
document.body.style.cursor = ''
}
return () => {
document.body.style.userSelect = ''
document.body.style.cursor = ''
}
}, [isDragging])
return { width, isDragging, onPointerDown }
}