diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index 39179d3..8da7fed 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -1,6 +1,8 @@ 'use client' import { useState, useEffect, useRef, useCallback } from 'react' +import { createPortal } from 'react-dom' +import { motion, AnimatePresence } from 'framer-motion' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { listSites, type Site } from '@/lib/api/sites' @@ -123,17 +125,35 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps const [faviconFailed, setFaviconFailed] = useState(false) const [faviconLoaded, setFaviconLoaded] = useState(false) const ref = useRef(null) + const panelRef = useRef(null) + const buttonRef = useRef(null) + const [fixedPos, setFixedPos] = useState<{ left: number; top: number } | null>(null) const pathname = usePathname() const router = useRouter() const currentSite = sites.find((s) => s.id === siteId) const faviconUrl = currentSite?.domain ? `${FAVICON_SERVICE_URL}?domain=${currentSite.domain}&sz=64` : null + const updatePosition = useCallback(() => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect() + let top = rect.bottom + 4 + if (panelRef.current) { + const maxTop = window.innerHeight - panelRef.current.offsetHeight - 8 + top = Math.min(top, Math.max(8, maxTop)) + } + setFixedPos({ left: rect.left, top }) + } + }, []) + useEffect(() => { const handler = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) { + const target = e.target as Node + if ( + ref.current && !ref.current.contains(target) && + (!panelRef.current || !panelRef.current.contains(target)) + ) { if (open) { setOpen(false); setSearch('') - // Re-collapse if we auto-expanded if (wasCollapsed.current) { onCollapse(); wasCollapsed.current = false } } } @@ -142,20 +162,91 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps return () => document.removeEventListener('mousedown', handler) }, [open, onCollapse, wasCollapsed]) + useEffect(() => { + if (open) { + updatePosition() + requestAnimationFrame(() => updatePosition()) + } + }, [open, updatePosition]) + + const closePicker = () => { + setOpen(false); setSearch('') + if (wasCollapsed.current) { onCollapse(); wasCollapsed.current = false } + } + const switchSite = (id: string) => { router.push(`/sites/${id}${pathname.replace(/^\/sites\/[^/]+/, '')}`) - setOpen(false); setSearch('') - // Re-collapse if we auto-expanded - if (wasCollapsed.current) { onCollapse(); wasCollapsed.current = false } + closePicker() } const filtered = sites.filter( (s) => s.name.toLowerCase().includes(search.toLowerCase()) || s.domain.toLowerCase().includes(search.toLowerCase()) ) + const dropdown = ( + + {open && ( + +
+ setSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') closePicker() + }} + className="w-full px-3 py-1.5 text-sm bg-white/[0.04] border border-white/[0.08] rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-white placeholder:text-neutral-400" + autoFocus + /> +
+
+ {filtered.map((site) => ( + + ))} + {filtered.length === 0 &&

No sites found

} +
+
+ closePicker()} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-white/[0.06] rounded-lg"> + + Add new site + +
+
+ )} +
+ ) + return (
- {open && ( -
-
- setSearch(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Escape') { - setOpen(false) - setSearch('') - if (wasCollapsed.current) { onCollapse(); wasCollapsed.current = false } - } - }} - className="w-full px-3 py-1.5 text-sm bg-white/[0.04] border border-white/[0.08] rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-white placeholder:text-neutral-400" - autoFocus - /> -
-
- {filtered.map((site) => ( - - ))} - {filtered.length === 0 &&

No sites found

} -
-
- setOpen(false)} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-white/[0.06] rounded-lg"> - - Add new site - -
-
- )} + {typeof document !== 'undefined' ? createPortal(dropdown, document.body) : dropdown}
) }