feat(web): draw the sidebar's scrollbar only under the pointer
The session list overflows after a handful of sessions, and its scrollbar was drawn permanently in a column that is at rest most of the time. SidebarRoot now tracks the pointer over the whole column and rebinds ui-theme's scrollbar indirection pair to `transparent` while it is outside, keeping the thumb for 2s after the pointer leaves so it does not blink out on the way past. Rebinding colour leaves the list's `scrollbar-gutter: stable` reservation in force, so revealing the bar moves no row. ui-theme's gate now states the widened contract: a rebind targets an -l2 token pair or `transparent`, and nothing else.
This commit is contained in:
@@ -24,6 +24,19 @@
|
||||
padding: 18px 10px 6px;
|
||||
}
|
||||
|
||||
/* Scrollbars in the column are a pointer affordance: the shell adds this
|
||||
class whenever the pointer is not inside (SidebarRoot.tsx owns the linger),
|
||||
and rebinding ui-theme's indirection pair to `transparent` takes the thumb
|
||||
out of every scroll region nested under it — the workspace browser's
|
||||
session list today. `transparent` rather than `display: none` on the bar:
|
||||
the reservation (`scrollbar-gutter: stable` on the list) stays in force, so
|
||||
revealing the thumb never reflows a row. Rebinding contract and the two
|
||||
rendering paths it reaches: ui-theme's README. */
|
||||
.root.quietBars {
|
||||
--dsh-scrollbar-thumb: transparent;
|
||||
--dsh-scrollbar-thumb-hover: transparent;
|
||||
}
|
||||
|
||||
/* Collapse phase 1: the whole frozen-width content fades out in place over
|
||||
150ms; at settle the children unmount/snap to the rail layout. */
|
||||
.fading > * {
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
* button and the foot is the `sidebar.workspaces` registrant's, and the foot
|
||||
* is the `sidebar.settings` registrant's; the shell hands them the wide flag
|
||||
* (plus an expand request callback for the browser).
|
||||
*
|
||||
* The column also owns whether the scroll regions nested in it draw a
|
||||
* scrollbar at all: the shell tracks the pointer and rebinds ui-theme's
|
||||
* scrollbar indirection away while it is elsewhere, so a list the user is not
|
||||
* pointing at carries no bar.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -22,6 +27,14 @@ import css from './SidebarRoot.module.css'
|
||||
/** Wide-content unmount delay; matches the 150ms wide-content fade-out. */
|
||||
const COLLAPSE_SETTLE_MS = 150
|
||||
|
||||
/**
|
||||
* How long the column's scrollbars stay drawn after the pointer leaves it.
|
||||
* The bar is a pointer affordance here, and hiding it on the leave event
|
||||
* itself makes it blink out while the pointer is only crossing the column's
|
||||
* edge — on the way to the conversation, or around a portalled menu.
|
||||
*/
|
||||
const SCROLLBAR_LINGER_MS = 2000
|
||||
|
||||
/**
|
||||
* Render the sidebar column shell.
|
||||
* @param props - composed slot props (runtime share + injected callbacks, contract/slots.ts).
|
||||
@@ -56,10 +69,29 @@ export function SidebarRoot({
|
||||
const everWide = useRef(!collapsed)
|
||||
if (!collapsed) everWide.current = true
|
||||
|
||||
// Scrollbars in the column follow the pointer (.quietBars rebinds them
|
||||
// away): drawn while it is inside, and for SCROLLBAR_LINGER_MS after it
|
||||
// leaves. A pointer that returns within that window cancels the pending
|
||||
// hide rather than restarting from a hidden bar.
|
||||
const [pointerInside, setPointerInside] = useState(false)
|
||||
const lingerTimer = useRef<number | undefined>(undefined)
|
||||
useEffect(() => () => { window.clearTimeout(lingerTimer.current) }, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(css.root, !wide && css.collapsed, !wide && everWide.current && css.railIn, collapsed && wide && css.fading)}
|
||||
className={clsx(
|
||||
css.root, !wide && css.collapsed, !wide && everWide.current && css.railIn,
|
||||
collapsed && wide && css.fading, !pointerInside && css.quietBars,
|
||||
)}
|
||||
style={wide ? { width: collapsed ? lastWideWidth.current : width } : undefined}
|
||||
onPointerEnter={() => {
|
||||
window.clearTimeout(lingerTimer.current)
|
||||
setPointerInside(true)
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
window.clearTimeout(lingerTimer.current)
|
||||
lingerTimer.current = window.setTimeout(() => { setPointerInside(false) }, SCROLLBAR_LINGER_MS)
|
||||
}}
|
||||
>
|
||||
<div className={css.logoRow}>
|
||||
{/* Expanded, the wordmark doubles as a New Session shortcut; the
|
||||
|
||||
Reference in New Issue
Block a user