refactor: replace flyout popups with full-height resizable drawer

Sentry-style drawer slides out from rail edge, fills viewport height.
Drag handle on right edge to resize (180-400px range).
No more tooltip-style popups that cause layout jitter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-22 01:26:25 -04:00
parent 00ab9f1832
commit 812a22b2b1

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
import { Link, useLocation } from 'react-router-dom'
import type { LucideIcon } from 'lucide-react'
import {
@@ -185,6 +185,33 @@ export function Sidebar() {
if (flyoutTimeout.current) clearTimeout(flyoutTimeout.current)
}
/* ── Drawer resize ───────────────────────────────── */
const [drawerWidth, setDrawerWidth] = useState(240)
const isResizing = useRef(false)
const handleResizeStart = (e: ReactPointerEvent) => {
e.preventDefault()
isResizing.current = true
const startX = e.clientX
const startWidth = drawerWidth
const onMove = (ev: globalThis.PointerEvent) => {
if (!isResizing.current) return
const newWidth = Math.max(180, Math.min(400, startWidth + (ev.clientX - startX)))
setDrawerWidth(newWidth)
}
const onUp = () => {
isResizing.current = false
document.removeEventListener('pointermove', onMove)
document.removeEventListener('pointerup', onUp)
}
document.addEventListener('pointermove', onMove)
document.addEventListener('pointerup', onUp)
}
/* Close flyout on Escape */
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
@@ -247,45 +274,7 @@ export function Sidebar() {
</span>
</Link>
{/* Flyout panel (icon rail only) */}
{hasChildren && !sidebarPinned && flyoutIndex === key && (
<div
className="absolute left-full top-0 z-50 ml-1"
style={{
maxHeight: '70vh',
overflowY: 'auto',
}}
onMouseEnter={keepFlyout}
onMouseLeave={closeFlyout}
>
<div
className="w-[220px] rounded-lg p-2 animate-fade-in"
style={{
background: '#14161d',
border: '1px solid #1e2130',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
}}
>
{item.children!.map(child => (
<Link
key={child.href}
to={child.href}
className={cn(
'flex items-center justify-between rounded-md px-3 py-2 text-[0.8125rem] transition-colors',
isChildActive(child)
? 'bg-[rgba(34,211,238,0.10)] text-[#67e8f9]'
: 'text-[#848b9b] hover:bg-[#191c25] hover:text-[#e2e5eb]'
)}
>
<span>{child.label}</span>
{child.count !== undefined && (
<span className="text-[0.6875rem] font-mono text-[#4f5666]">{child.count}</span>
)}
</Link>
))}
</div>
</div>
)}
{/* Flyout rendered as drawer below */}
</div>
)
}
@@ -364,15 +353,12 @@ export function Sidebar() {
)
}
/* ── Flyout positioning: use data attribute for lookup ── */
/* ── Find active flyout group for drawer ── */
const renderRailItemWithRef = (item: NavEntry, key: string) => {
return (
<div key={key} data-flyout-key={key}>
{renderRailItem(item, key + '-inner')}
</div>
)
}
const activeFlyoutGroup = flyoutIndex && !sidebarPinned
? railGroups.find((_, i) => `rail-${i}` === flyoutIndex) ||
footerItems.find((_, i) => `footer-${i}` === flyoutIndex)
: null
/* ── Main render ──────────────────────────────────── */
@@ -419,40 +405,91 @@ export function Sidebar() {
/* Icon Rail (default) — 5 grouped icons, Sentry-style */
return (
<nav
ref={sidebarRef}
className="sidebar flex flex-col items-center h-full"
style={{ background: '#0f1118', borderRight: '1px solid #1e2130' }}
onWheel={handleWheel}
<div
className="flex h-full"
onMouseLeave={closeFlyout}
>
{/* Grouped nav items */}
<div className="flex flex-col items-center w-full px-1.5 pt-3 space-y-1">
{railGroups.map((item, i) => (
<div key={`rail-${i}`} data-flyout-key={`rail-${i}`}>
{renderRailItem(item, `rail-${i}-inner`)}
</div>
))}
</div>
{/* Rail */}
<nav
ref={sidebarRef}
className="sidebar flex flex-col items-center h-full shrink-0"
style={{ background: '#0f1118', borderRight: '1px solid #1e2130', width: '72px' }}
onWheel={handleWheel}
>
{/* Grouped nav items */}
<div className="flex flex-col items-center w-full px-1.5 pt-3 space-y-1">
{railGroups.map((item, i) => renderRailItem(item, `rail-${i}`))}
</div>
<div className="flex-1" />
<div className="flex-1" />
{/* Footer: Account + Pin */}
<div className="flex flex-col items-center w-full px-1.5 pb-5 pt-2 space-y-1" style={{ borderTop: '1px solid #1e2130' }}>
{footerItems.map((item, i) => (
<div key={`footer-${i}`} data-flyout-key={`footer-${i}`}>
{renderRailItem(item, `footer-${i}-inner`)}
</div>
))}
<button
type="button"
onClick={toggleSidebarPinned}
className="flex flex-col items-center justify-center rounded-lg px-1 py-2 text-[#6b7280] hover:text-[#848b9b] transition-colors"
title="Pin sidebar"
{/* Footer: Account + Pin */}
<div className="flex flex-col items-center w-full px-1.5 pb-5 pt-2 space-y-1" style={{ borderTop: '1px solid #1e2130' }}>
{footerItems.map((item, i) => renderRailItem(item, `footer-${i}`))}
<button
type="button"
onClick={toggleSidebarPinned}
className="flex flex-col items-center justify-center rounded-lg px-1 py-2 text-[#6b7280] hover:text-[#848b9b] transition-colors"
title="Pin sidebar"
>
<Pin size={18} className="opacity-60 hover:opacity-85" />
<span className="mt-1 text-[0.5625rem] font-mono leading-tight">Pin</span>
</button>
</div>
</nav>
{/* Drawer panel — full height, resizable */}
{activeFlyoutGroup && activeFlyoutGroup.children && (
<div
className="flex h-full shrink-0"
onMouseEnter={keepFlyout}
style={{ width: drawerWidth }}
>
<Pin size={18} className="opacity-60 hover:opacity-85" />
<span className="mt-1 text-[0.5625rem] font-mono leading-tight">Pin</span>
</button>
</div>
</nav>
<div
className="flex flex-col h-full flex-1 overflow-y-auto py-4 px-2"
style={{
background: '#0f1118',
borderRight: '1px solid #1e2130',
boxShadow: '4px 0 12px rgba(0,0,0,0.2)',
}}
>
{/* Drawer header */}
<div className="px-3 mb-3">
<h3 className="text-[0.6875rem] font-mono uppercase tracking-[0.12em] text-[#4f5666]">
{activeFlyoutGroup.label}
</h3>
</div>
{/* Drawer items */}
<div className="space-y-0.5 px-1">
{activeFlyoutGroup.children.map(child => (
<Link
key={child.href}
to={child.href}
className={cn(
'flex items-center justify-between rounded-md px-3 py-2 text-[0.8125rem] transition-colors',
isChildActive(child)
? 'bg-[rgba(34,211,238,0.10)] text-[#67e8f9]'
: 'text-[#848b9b] hover:bg-[#191c25] hover:text-[#e2e5eb]'
)}
>
<span>{child.label}</span>
{child.count !== undefined && (
<span className="text-[0.6875rem] font-mono text-[#4f5666]">{child.count}</span>
)}
</Link>
))}
</div>
</div>
{/* Resize handle */}
<div
className="w-1 cursor-col-resize hover:bg-[#22d3ee]/20 active:bg-[#22d3ee]/30 transition-colors shrink-0"
onPointerDown={handleResizeStart}
title="Drag to resize"
/>
</div>
)}
</div>
)
}