feat(sidebar): mobile exit animation, site picker entrance, hover nudge, CSS tooltips
This commit is contained in:
@@ -193,7 +193,7 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute left-3 top-full mt-1 z-50 w-[240px] bg-neutral-900 border border-neutral-700 rounded-xl shadow-xl overflow-hidden">
|
<div className="absolute left-3 top-full mt-1 z-50 w-[240px] bg-neutral-900 border border-neutral-700 rounded-xl shadow-xl overflow-hidden animate-in fade-in zoom-in-95 duration-150">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -262,21 +262,27 @@ function NavLink({
|
|||||||
const active = matchesPathname || matchesPending
|
const active = matchesPathname || matchesPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div className="relative group/nav">
|
||||||
href={href}
|
<Link
|
||||||
onClick={() => { onNavigate(href); onClick?.() }}
|
href={href}
|
||||||
title={collapsed ? item.label : undefined}
|
onClick={() => { onNavigate(href); onClick?.() }}
|
||||||
className={`flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium overflow-hidden ${
|
className={`flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium overflow-hidden transition-all duration-150 ${
|
||||||
active
|
active
|
||||||
? 'bg-brand-orange/10 text-brand-orange'
|
? 'bg-brand-orange/10 text-brand-orange'
|
||||||
: 'text-neutral-400 hover:text-white hover:bg-neutral-800'
|
: 'text-neutral-400 hover:text-white hover:bg-neutral-800 hover:translate-x-0.5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
||||||
<item.icon className="w-[18px] h-[18px]" weight={active ? 'fill' : 'regular'} />
|
<item.icon className="w-[18px] h-[18px]" weight={active ? 'fill' : 'regular'} />
|
||||||
</span>
|
</span>
|
||||||
<Label collapsed={collapsed}>{item.label}</Label>
|
<Label collapsed={collapsed}>{item.label}</Label>
|
||||||
</Link>
|
</Link>
|
||||||
|
{collapsed && (
|
||||||
|
<span className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 rounded-md bg-neutral-800 text-white text-xs whitespace-nowrap opacity-0 group-hover/nav:opacity-100 transition-opacity duration-150 delay-150 z-50">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,12 +371,17 @@ function SidebarContent({
|
|||||||
<div className="border-t border-neutral-800/60 px-2 py-3 shrink-0">
|
<div className="border-t border-neutral-800/60 px-2 py-3 shrink-0">
|
||||||
{/* Notifications, Profile — same layout as nav items */}
|
{/* Notifications, Profile — same layout as nav items */}
|
||||||
<div className="space-y-0.5 mb-1">
|
<div className="space-y-0.5 mb-1">
|
||||||
<span title={c ? 'Notifications' : undefined}>
|
<div className="relative group/notif">
|
||||||
<NotificationCenter anchor="right" variant="sidebar">
|
<NotificationCenter anchor="right" variant="sidebar">
|
||||||
<Label collapsed={c}>Notifications</Label>
|
<Label collapsed={c}>Notifications</Label>
|
||||||
</NotificationCenter>
|
</NotificationCenter>
|
||||||
</span>
|
{c && (
|
||||||
<span title={c ? (user?.display_name?.trim() || 'Profile') : undefined}>
|
<span className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 rounded-md bg-neutral-800 text-white text-xs whitespace-nowrap opacity-0 group-hover/notif:opacity-100 transition-opacity duration-150 delay-150 z-50">
|
||||||
|
Notifications
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="relative group/user">
|
||||||
<UserMenu
|
<UserMenu
|
||||||
auth={auth}
|
auth={auth}
|
||||||
LinkComponent={Link}
|
LinkComponent={Link}
|
||||||
@@ -385,22 +396,33 @@ function SidebarContent({
|
|||||||
>
|
>
|
||||||
<Label collapsed={c}>{user?.display_name?.trim() || 'Profile'}</Label>
|
<Label collapsed={c}>{user?.display_name?.trim() || 'Profile'}</Label>
|
||||||
</UserMenu>
|
</UserMenu>
|
||||||
</span>
|
{c && (
|
||||||
|
<span className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 rounded-md bg-neutral-800 text-white text-xs whitespace-nowrap opacity-0 group-hover/user:opacity-100 transition-opacity duration-150 delay-150 z-50">
|
||||||
|
{user?.display_name?.trim() || 'Profile'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Settings + Collapse */}
|
{/* Settings + Collapse */}
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<button
|
<div className="relative group/collapse">
|
||||||
onClick={onToggle}
|
<button
|
||||||
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-400 dark:text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 w-full overflow-hidden"
|
onClick={onToggle}
|
||||||
title={collapsed ? 'Expand sidebar (press [)' : 'Collapse sidebar (press [)'}
|
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-400 dark:text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 w-full overflow-hidden"
|
||||||
>
|
>
|
||||||
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
||||||
<CollapseLeftIcon className={`w-[18px] h-[18px] transition-transform duration-200 ${c ? 'rotate-180' : ''}`} />
|
<CollapseLeftIcon className={`w-[18px] h-[18px] transition-transform duration-200 ${c ? 'rotate-180' : ''}`} />
|
||||||
</span>
|
</span>
|
||||||
<Label collapsed={c}>{c ? 'Expand' : 'Collapse'}</Label>
|
<Label collapsed={c}>{c ? 'Expand' : 'Collapse'}</Label>
|
||||||
</button>
|
</button>
|
||||||
|
{c && (
|
||||||
|
<span className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 rounded-md bg-neutral-800 text-white text-xs whitespace-nowrap opacity-0 group-hover/collapse:opacity-100 transition-opacity duration-150 delay-150 z-50">
|
||||||
|
Expand (press [)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,6 +446,7 @@ export default function Sidebar({
|
|||||||
const [sites, setSites] = useState<Site[]>([])
|
const [sites, setSites] = useState<Site[]>([])
|
||||||
const [orgs, setOrgs] = useState<OrganizationMember[]>([])
|
const [orgs, setOrgs] = useState<OrganizationMember[]>([])
|
||||||
const [pendingHref, setPendingHref] = useState<string | null>(null)
|
const [pendingHref, setPendingHref] = useState<string | null>(null)
|
||||||
|
const [mobileClosing, setMobileClosing] = useState(false)
|
||||||
const wasCollapsedRef = useRef(false)
|
const wasCollapsedRef = useRef(false)
|
||||||
const pickerOpenCallbackRef = useRef<(() => void) | null>(null)
|
const pickerOpenCallbackRef = useRef<(() => void) | null>(null)
|
||||||
// Safe to read localStorage directly — this component is loaded with ssr:false
|
// Safe to read localStorage directly — this component is loaded with ssr:false
|
||||||
@@ -477,6 +500,14 @@ export default function Sidebar({
|
|||||||
setCollapsed(true); localStorage.setItem(SIDEBAR_KEY, 'true')
|
setCollapsed(true); localStorage.setItem(SIDEBAR_KEY, 'true')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleMobileClose = useCallback(() => {
|
||||||
|
setMobileClosing(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setMobileClosing(false)
|
||||||
|
onMobileClose()
|
||||||
|
}, 200)
|
||||||
|
}, [onMobileClose])
|
||||||
|
|
||||||
const handleNavigate = useCallback((href: string) => { setPendingHref(href) }, [])
|
const handleNavigate = useCallback((href: string) => { setPendingHref(href) }, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -514,13 +545,24 @@ export default function Sidebar({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Mobile overlay */}
|
{/* Mobile overlay */}
|
||||||
{mobileOpen && (
|
{(mobileOpen || mobileClosing) && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40 bg-black/30 md:hidden" onClick={onMobileClose} />
|
<div
|
||||||
<aside className="fixed inset-y-0 left-0 z-50 w-72 bg-neutral-900 border-r border-neutral-800 shadow-xl md:hidden animate-in slide-in-from-left duration-200">
|
className={`fixed inset-0 z-40 md:hidden transition-opacity duration-200 ${
|
||||||
|
mobileClosing ? 'bg-black/0' : 'bg-black/30'
|
||||||
|
}`}
|
||||||
|
onClick={handleMobileClose}
|
||||||
|
/>
|
||||||
|
<aside
|
||||||
|
className={`fixed inset-y-0 left-0 z-50 w-72 bg-neutral-900 border-r border-neutral-800 shadow-xl md:hidden ${
|
||||||
|
mobileClosing
|
||||||
|
? 'animate-out slide-out-to-left duration-200 fill-mode-forwards'
|
||||||
|
: 'animate-in slide-in-from-left duration-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-800">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-800">
|
||||||
<span className="text-sm font-semibold text-white">Navigation</span>
|
<span className="text-sm font-semibold text-white">Navigation</span>
|
||||||
<button onClick={onMobileClose} className="p-1.5 text-neutral-400 hover:text-neutral-300">
|
<button onClick={handleMobileClose} className="p-1.5 text-neutral-400 hover:text-neutral-300">
|
||||||
<XIcon className="w-5 h-5" />
|
<XIcon className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -532,7 +574,7 @@ export default function Sidebar({
|
|||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
pendingHref={pendingHref}
|
pendingHref={pendingHref}
|
||||||
onNavigate={handleNavigate}
|
onNavigate={handleNavigate}
|
||||||
onMobileClose={onMobileClose}
|
onMobileClose={handleMobileClose}
|
||||||
onExpand={expand}
|
onExpand={expand}
|
||||||
onCollapse={collapse}
|
onCollapse={collapse}
|
||||||
onToggle={toggle}
|
onToggle={toggle}
|
||||||
|
|||||||
Reference in New Issue
Block a user