diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index f63f2db..585667f 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -22,6 +22,8 @@ import { } from '@ciphera-net/ui' const SIDEBAR_KEY = 'pulse_sidebar_collapsed' +const EXPANDED = 256 +const COLLAPSED = 56 type IconWeight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone' @@ -32,10 +34,7 @@ interface NavItem { matchPrefix?: boolean } -interface NavGroup { - label: string - items: NavItem[] -} +interface NavGroup { label: string; items: NavItem[] } const NAV_GROUPS: NavGroup[] = [ { @@ -58,23 +57,24 @@ const NAV_GROUPS: NavGroup[] = [ ] const SETTINGS_ITEM: NavItem = { - label: 'Settings', - href: (id) => `/sites/${id}/settings`, - icon: SettingsIcon, - matchPrefix: true, + label: 'Settings', href: (id) => `/sites/${id}/settings`, icon: SettingsIcon, matchPrefix: true, +} + +// Label that fades with the sidebar — always in the DOM, never removed +function Label({ children, collapsed }: { children: React.ReactNode; collapsed: boolean }) { + return ( + + {children} + + ) } // ─── Site Picker ──────────────────────────────────────────── -function SitePicker({ - sites, - siteId, - collapsed, -}: { - sites: Site[] - siteId: string - collapsed: boolean -}) { +function SitePicker({ sites, siteId, collapsed }: { sites: Site[]; siteId: string; collapsed: boolean }) { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const ref = useRef(null) @@ -85,10 +85,7 @@ function SitePicker({ useEffect(() => { const handler = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false) - setSearch('') - } + if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); setSearch('') } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) @@ -96,38 +93,33 @@ function SitePicker({ const switchSite = (id: string) => { router.push(`/sites/${id}${pathname.replace(/^\/sites\/[^/]+/, '')}`) - setOpen(false) - setSearch('') + setOpen(false); setSearch('') } const filtered = sites.filter( - (s) => - s.name.toLowerCase().includes(search.toLowerCase()) || - s.domain.toLowerCase().includes(search.toLowerCase()) + (s) => s.name.toLowerCase().includes(search.toLowerCase()) || s.domain.toLowerCase().includes(search.toLowerCase()) ) return ( -
+
{open && ( -
+
))} - {filtered.length === 0 && ( -

No sites found

- )} + {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-neutral-50 dark:hover:bg-neutral-800 rounded-lg" - > + setOpen(false)} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg"> Add new site @@ -181,52 +167,30 @@ function SitePicker({ // ─── Nav Item ─────────────────────────────────────────────── function NavLink({ - item, - siteId, - collapsed, - onClick, - pendingHref, - onNavigate, + item, siteId, collapsed, onClick, pendingHref, onNavigate, }: { - item: NavItem - siteId: string - collapsed: boolean - onClick?: () => void - pendingHref: string | null - onNavigate: (href: string) => void + item: NavItem; siteId: string; collapsed: boolean; onClick?: () => void + pendingHref: string | null; onNavigate: (href: string) => void }) { const pathname = usePathname() const href = item.href(siteId) - - // Active if pathname matches OR if this link was just clicked (optimistic) const matchesPathname = item.matchPrefix ? pathname.startsWith(href) : pathname === href const matchesPending = pendingHref !== null && (item.matchPrefix ? pendingHref.startsWith(href) : pendingHref === href) const active = matchesPathname || matchesPending - const handleClick = () => { - onNavigate(href) - onClick?.() - } - return ( { onNavigate(href); onClick?.() }} title={collapsed ? item.label : undefined} - className={`flex items-center rounded-lg py-2 text-sm font-medium ${ - collapsed ? 'justify-center px-0' : 'gap-2.5 px-2.5' - } ${ + className={`flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium overflow-hidden ${ active ? 'bg-brand-orange/10 text-brand-orange' : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800' }`} > - {!collapsed && ( - - {item.label} - - )} + ) } @@ -234,15 +198,9 @@ function NavLink({ // ─── Main Sidebar ─────────────────────────────────────────── export default function Sidebar({ - siteId, - mobileOpen, - onMobileClose, - onMobileOpen, + siteId, mobileOpen, onMobileClose, onMobileOpen, }: { - siteId: string - mobileOpen: boolean - onMobileClose: () => void - onMobileOpen: () => void + siteId: string; mobileOpen: boolean; onMobileClose: () => void; onMobileOpen: () => void }) { const { user } = useAuth() const canEdit = user?.role === 'owner' || user?.role === 'admin' @@ -254,24 +212,15 @@ export default function Sidebar({ return localStorage.getItem(SIDEBAR_KEY) === 'true' }) - useEffect(() => { - listSites().then(setSites).catch(() => {}) - }, []) + useEffect(() => { listSites().then(setSites).catch(() => {}) }, []) + useEffect(() => { setPendingHref(null); onMobileClose() }, [pathname, onMobileClose]) - // Clear pending href once navigation completes - useEffect(() => { - setPendingHref(null) - onMobileClose() - }, [pathname, onMobileClose]) - - // Keyboard shortcut: [ to toggle useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === '[' && !e.metaKey && !e.ctrlKey && !e.altKey) { const tag = (e.target as HTMLElement)?.tagName if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return - e.preventDefault() - toggle() + e.preventDefault(); toggle() } } document.addEventListener('keydown', handler) @@ -279,66 +228,39 @@ export default function Sidebar({ }, [collapsed]) const toggle = useCallback(() => { - setCollapsed((prev) => { - const next = !prev - localStorage.setItem(SIDEBAR_KEY, String(next)) - return next - }) + setCollapsed((prev) => { const next = !prev; localStorage.setItem(SIDEBAR_KEY, String(next)); return next }) }, []) - const handleNavigate = useCallback((href: string) => { - setPendingHref(href) - }, []) + const handleNavigate = useCallback((href: string) => { setPendingHref(href) }, []) const sidebarContent = (isMobile: boolean) => { - const isCollapsed = isMobile ? false : collapsed + const c = isMobile ? false : collapsed return ( -
- {/* Logo */} - - Pulse - {!isCollapsed && ( - - Pulse - - )} +
+ {/* Logo — fixed layout, text fades */} + + Pulse + + Pulse + {/* Site Picker */} - + {/* Nav Groups */} -