fix: portal-based sidebar tooltips, visible when collapsed
Old tooltips were clipped by overflow-hidden on the aside. New SidebarTooltip renders via createPortal with fixed positioning, 100ms delay, rounded-lg glass styling with border and shadow.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { listSites, type Site } from '@/lib/api/sites'
|
||||
@@ -80,6 +81,45 @@ function Label({ children, collapsed }: { children: React.ReactNode; collapsed:
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Sidebar Tooltip (portal-based, escapes overflow-hidden) ──
|
||||
|
||||
function SidebarTooltip({ children, label }: { children: React.ReactNode; label: string }) {
|
||||
const [show, setShow] = useState(false)
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 })
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
|
||||
const handleEnter = () => {
|
||||
timerRef.current = setTimeout(() => {
|
||||
if (ref.current) {
|
||||
const rect = ref.current.getBoundingClientRect()
|
||||
setPos({ x: rect.right + 8, y: rect.top + rect.height / 2 })
|
||||
setShow(true)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleLeave = () => {
|
||||
clearTimeout(timerRef.current)
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} onMouseEnter={handleEnter} onMouseLeave={handleLeave}>
|
||||
{children}
|
||||
{show && typeof document !== 'undefined' && createPortal(
|
||||
<span
|
||||
className="fixed z-[100] px-2.5 py-1.5 rounded-lg bg-neutral-800 border border-white/[0.08] text-white text-xs font-medium whitespace-nowrap pointer-events-none shadow-lg shadow-black/20 -translate-y-1/2"
|
||||
style={{ left: pos.x, top: pos.y }}
|
||||
>
|
||||
{label}
|
||||
</span>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Nav Item ───────────────────────────────────────────────
|
||||
|
||||
function NavLink({
|
||||
@@ -94,8 +134,7 @@ function NavLink({
|
||||
const matchesPending = pendingHref !== null && (item.matchPrefix ? pendingHref.startsWith(href) : pendingHref === href)
|
||||
const active = matchesPathname || matchesPending
|
||||
|
||||
return (
|
||||
<div className="relative group/nav">
|
||||
const link = (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={() => { onNavigate(href); onClick?.() }}
|
||||
@@ -110,13 +149,10 @@ function NavLink({
|
||||
</span>
|
||||
<Label collapsed={collapsed}>{item.label}</Label>
|
||||
</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>
|
||||
)
|
||||
|
||||
if (collapsed) return <SidebarTooltip label={item.label}>{link}</SidebarTooltip>
|
||||
return link
|
||||
}
|
||||
|
||||
// ─── Settings Button (opens unified modal instead of navigating) ─────
|
||||
@@ -128,8 +164,7 @@ function SettingsButton({
|
||||
}) {
|
||||
const { openUnifiedSettings } = useUnifiedSettings()
|
||||
|
||||
return (
|
||||
<div className="relative group/nav">
|
||||
const btn = (
|
||||
<button
|
||||
onClick={() => {
|
||||
openUnifiedSettings({ context: settingsContext, tab: 'general' })
|
||||
@@ -142,13 +177,10 @@ function SettingsButton({
|
||||
</span>
|
||||
<Label collapsed={collapsed}>{item.label}</Label>
|
||||
</button>
|
||||
{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>
|
||||
)
|
||||
|
||||
if (collapsed) return <SidebarTooltip label={item.label}>{btn}</SidebarTooltip>
|
||||
return btn
|
||||
}
|
||||
|
||||
// ─── Home Nav Link (static href, no siteId) ───────────────
|
||||
@@ -162,8 +194,7 @@ function HomeNavLink({
|
||||
const pathname = usePathname()
|
||||
const active = !external && pathname === href
|
||||
|
||||
return (
|
||||
<div className="relative group/nav">
|
||||
const link = (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
@@ -179,13 +210,10 @@ function HomeNavLink({
|
||||
</span>
|
||||
<Label collapsed={collapsed}>{label}</Label>
|
||||
</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">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (collapsed) return <SidebarTooltip label={label}>{link}</SidebarTooltip>
|
||||
return link
|
||||
}
|
||||
|
||||
// ─── Home Site Link (favicon + name) ───────────────────────
|
||||
@@ -199,8 +227,7 @@ function HomeSiteLink({
|
||||
const href = `/sites/${site.id}`
|
||||
const active = pathname.startsWith(href)
|
||||
|
||||
return (
|
||||
<div className="relative group/nav">
|
||||
const link = (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
@@ -219,13 +246,10 @@ function HomeSiteLink({
|
||||
</span>
|
||||
<Label collapsed={collapsed}>{site.name}</Label>
|
||||
</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">
|
||||
{site.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (collapsed) return <SidebarTooltip label={site.name}>{link}</SidebarTooltip>
|
||||
return link
|
||||
}
|
||||
|
||||
// ─── Sidebar Content ────────────────────────────────────────
|
||||
@@ -354,17 +378,36 @@ function SidebarContent({
|
||||
<div className="border-t border-white/[0.06] px-2 py-3 shrink-0">
|
||||
{/* Notifications, Profile — same layout as nav items */}
|
||||
<div className="space-y-0.5 mb-1">
|
||||
<div className="relative group/notif">
|
||||
{c ? (
|
||||
<SidebarTooltip label="Notifications">
|
||||
<NotificationCenter anchor="right" variant="sidebar">
|
||||
<Label collapsed={c}>Notifications</Label>
|
||||
</NotificationCenter>
|
||||
</SidebarTooltip>
|
||||
) : (
|
||||
<NotificationCenter anchor="right" variant="sidebar">
|
||||
<Label collapsed={c}>Notifications</Label>
|
||||
</NotificationCenter>
|
||||
{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/notif:opacity-100 transition-opacity duration-150 delay-150 z-50">
|
||||
Notifications
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative group/user">
|
||||
{c ? (
|
||||
<SidebarTooltip label={user?.display_name?.trim() || 'Profile'}>
|
||||
<UserMenu
|
||||
auth={auth}
|
||||
LinkComponent={Link}
|
||||
orgs={orgs}
|
||||
activeOrgId={auth.user?.org_id}
|
||||
onSwitchOrganization={onSwitchOrganization}
|
||||
onCreateOrganization={() => router.push('/onboarding')}
|
||||
allowPersonalOrganization={false}
|
||||
onOpenSettings={openSettings}
|
||||
onOpenOrgSettings={openOrgSettings}
|
||||
compact
|
||||
anchor="right"
|
||||
>
|
||||
<Label collapsed={c}>{user?.display_name?.trim() || 'Profile'}</Label>
|
||||
</UserMenu>
|
||||
</SidebarTooltip>
|
||||
) : (
|
||||
<UserMenu
|
||||
auth={auth}
|
||||
LinkComponent={Link}
|
||||
@@ -380,15 +423,10 @@ function SidebarContent({
|
||||
>
|
||||
<Label collapsed={c}>{user?.display_name?.trim() || 'Profile'}</Label>
|
||||
</UserMenu>
|
||||
{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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user