'use client' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from 'react' import { accessApi, institutionalTierLabels, type WalletAccessSession } from '@/services/api/access' import BrandLockup from './BrandLockup' import HeaderCommandPalette, { type HeaderCommandItem } from './HeaderCommandPalette' import { useUiMode } from './UiModeContext' type MenuItem = { href?: string label: string description?: string external?: boolean onSelect?: () => void | Promise } type MenuTone = 'default' | 'emphasis' const desktopLinkBase = 'inline-flex items-center rounded-xl px-3 py-2 text-[15px] font-medium text-gray-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:text-gray-200 dark:focus-visible:ring-offset-gray-900' const desktopLinkIdle = 'hover:bg-gray-100 hover:text-gray-950 dark:hover:bg-gray-800 dark:hover:text-white' const desktopLinkActive = 'bg-gray-100 text-gray-950 ring-1 ring-gray-200 dark:bg-gray-800 dark:text-white dark:ring-gray-700' function shortAddress(address: string) { if (!address) return 'Wallet' return `${address.slice(0, 6)}...${address.slice(-4)}` } function SearchIcon({ className = 'h-4 w-4' }: { className?: string }) { return ( ) } function ChevronIcon({ open, className = 'h-4 w-4' }: { open?: boolean; className?: string }) { return ( ) } function MenuDropdown({ label, items, active = false, tone = 'default', align = 'left', menuHeader, }: { label: ReactNode items: MenuItem[] active?: boolean tone?: MenuTone align?: 'left' | 'right' menuHeader?: ReactNode }) { const { mode } = useUiMode() const [open, setOpen] = useState(false) const wrapperRef = useRef(null) const triggerRef = useRef(null) const itemRefs = useRef>([]) const menuId = useId() useEffect(() => { if (!open) return const handlePointerDown = (event: MouseEvent | TouchEvent) => { const target = event.target as Node | null if (!target || !wrapperRef.current?.contains(target)) { setOpen(false) } } document.addEventListener('mousedown', handlePointerDown) document.addEventListener('touchstart', handlePointerDown) return () => { document.removeEventListener('mousedown', handlePointerDown) document.removeEventListener('touchstart', handlePointerDown) } }, [open]) const focusMenuItem = (index: number) => { const item = itemRefs.current[index] item?.focus() } const handleTriggerKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') { event.preventDefault() setOpen(true) requestAnimationFrame(() => focusMenuItem(0)) } if (event.key === 'ArrowUp') { event.preventDefault() setOpen(true) requestAnimationFrame(() => focusMenuItem(items.length - 1)) } if (event.key === 'Escape') { setOpen(false) } } const handleMenuKeyDown = (event: React.KeyboardEvent) => { const currentIndex = itemRefs.current.findIndex((node) => node === document.activeElement) if (event.key === 'ArrowDown') { event.preventDefault() focusMenuItem((currentIndex + 1 + items.length) % items.length) } if (event.key === 'ArrowUp') { event.preventDefault() focusMenuItem((currentIndex - 1 + items.length) % items.length) } if (event.key === 'Home') { event.preventDefault() focusMenuItem(0) } if (event.key === 'End') { event.preventDefault() focusMenuItem(items.length - 1) } if (event.key === 'Escape') { event.preventDefault() setOpen(false) triggerRef.current?.focus() } if (event.key === 'Tab') { setOpen(false) } } return (
{ const nextTarget = event.relatedTarget as Node | null if (!nextTarget || !wrapperRef.current?.contains(nextTarget)) { setOpen(false) } }} > {open ? ( ) : null}
) } function SearchControl({ active, mobile = false, onSelect, }: { active: boolean mobile?: boolean onSelect?: () => void }) { const { mode } = useUiMode() if (mobile) { return ( ) } return ( ) } function getAccessTier(walletSession: WalletAccessSession) { if (walletSession.institutionalTier) { return institutionalTierLabels[walletSession.institutionalTier] ?? walletSession.institutionalTier } const permissions = walletSession.permissions || [] if (permissions.some((permission) => permission.startsWith('operator.'))) { return 'Operator Tier' } if (permissions.some((permission) => permission.startsWith('analytics.'))) { return 'Analytics Tier' } if (permissions.some((permission) => permission.includes('enhanced') || permission.includes('full'))) { return 'Enhanced Explorer Tier' } return 'Explorer Tier' } function getSessionSummary(walletSession: WalletAccessSession) { const permissionCount = walletSession.permissions?.length || 0 const tierLabel = getAccessTier(walletSession) const institutionSuffix = walletSession.institutionName ? ` (${walletSession.institutionName})` : '' if (permissionCount > 0) { return `${tierLabel}${institutionSuffix} · ${permissionCount} permission${permissionCount === 1 ? '' : 's'}` } return `${tierLabel}${institutionSuffix} · Explorer access active` } function UiModeToggle({ mobile = false }: { mobile?: boolean }) { const { mode, toggleMode } = useUiMode() const className = mobile ? 'inline-flex items-center gap-2 rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm font-medium text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300' : 'hidden lg:inline-flex items-center gap-2 rounded-2xl border border-gray-200 bg-white px-3.5 py-2.5 text-sm font-medium text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300 dark:focus-visible:ring-offset-gray-900' return ( ) } function AccountButton({ walletSession, connectingWallet, connectError, onConnect, onCopyAddress, onSwitchWallet, onDisconnect, }: { walletSession: WalletAccessSession | null connectingWallet: boolean connectError?: string | null onConnect: () => void onCopyAddress: () => void onSwitchWallet: () => void onDisconnect: () => void }) { const { mode } = useUiMode() const accountItems: MenuItem[] = [ { href: '/access', label: 'Account', description: 'Open API access, subscriptions, and account-linked explorer features.', }, { href: '/wallet', label: 'Wallet tools', description: 'Review network, token-list, and wallet configuration guidance.', }, { label: 'Copy address', description: walletSession?.address || 'Copy the connected wallet address.', onSelect: onCopyAddress, }, { label: 'Switch wallet', description: 'Connect a different wallet without leaving the explorer.', onSelect: onSwitchWallet, }, { label: 'Disconnect wallet', description: 'End the current wallet connection on this device.', onSelect: onDisconnect, }, ] if (!walletSession) { return (
{connectError ? (

{connectError}

) : null}
) } const sessionSummary = getSessionSummary(walletSession) const tierLabel = getAccessTier(walletSession) const expiresLabel = walletSession.expiresAt ? new Date(walletSession.expiresAt).toLocaleString() : 'Session expiry unavailable' return ( {shortAddress(walletSession.address)} } items={accountItems} align="right" tone="emphasis" active={false} menuHeader={
Wallet connected
{mode === 'guided' ? `${sessionSummary}. Access tier is derived from current explorer permissions.` : sessionSummary}
Active
{walletSession.address}
Access tier: {tierLabel}
Session expires {expiresLabel}
} /> ) } export default function Navbar() { const router = useRouter() const { mode, setMode } = useUiMode() const pathname = usePathname() ?? '' const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) const [walletSession, setWalletSession] = useState(null) const [connectingWallet, setConnectingWallet] = useState(false) const [walletConnectError, setWalletConnectError] = useState(null) const mobilePanelId = useId() const isExploreActive = pathname === '/' || pathname.startsWith('/blocks') || pathname.startsWith('/transactions') || pathname.startsWith('/addresses') const isDataActive = pathname.startsWith('/tokens') || pathname.startsWith('/analytics') || pathname.startsWith('/pools') || pathname.startsWith('/watchlist') const isOperationsActive = pathname.startsWith('/bridge') || pathname.startsWith('/routes') || pathname.startsWith('/liquidity') || pathname.startsWith('/operations') || pathname.startsWith('/system') || pathname.startsWith('/operator') || pathname.startsWith('/weth') const isDocsActive = pathname.startsWith('/docs') const isSearchActive = pathname.startsWith('/search') useEffect(() => { const syncWalletSession = () => { setWalletSession(accessApi.getStoredWalletSession()) } syncWalletSession() window.addEventListener('storage', syncWalletSession) window.addEventListener('explorer-access-session-changed', syncWalletSession) const handleShortcut = (event: KeyboardEvent) => { const target = event.target as HTMLElement | null const tag = target?.tagName?.toLowerCase() const isEditable = tag === 'input' || tag === 'textarea' || target?.isContentEditable if (isEditable) return if (event.key === '/' || ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k')) { event.preventDefault() setCommandPaletteOpen(true) } } window.addEventListener('keydown', handleShortcut) return () => { window.removeEventListener('storage', syncWalletSession) window.removeEventListener('explorer-access-session-changed', syncWalletSession) window.removeEventListener('keydown', handleShortcut) } }, [router]) const handleConnectWallet = async () => { try { setConnectingWallet(true) setWalletConnectError(null) await accessApi.connectWalletSession() setMobileMenuOpen(false) router.push('/wallet') } catch (error) { console.error('Wallet connect failed', error) const message = error instanceof Error ? error.message : 'Wallet connection failed.' setWalletConnectError(message) } finally { setConnectingWallet(false) } } const handleCopyAddress = async () => { if (!walletSession?.address || typeof navigator === 'undefined' || !navigator.clipboard) return try { await navigator.clipboard.writeText(walletSession.address) } catch (error) { console.error('Failed to copy wallet address', error) } } const handleDisconnectWallet = () => { accessApi.clearSession() accessApi.clearWalletSession() setMobileMenuOpen(false) } const handleCopyCurrentUrl = async () => { if (typeof window === 'undefined' || !navigator.clipboard) return try { await navigator.clipboard.writeText(window.location.href) } catch (error) { console.error('Failed to copy current URL', error) } } const handleSwitchWallet = async () => { await handleConnectWallet() } const exploreItems: MenuItem[] = useMemo( () => [ { href: '/', label: 'Overview', description: 'Return to the main explorer dashboard and network summary.' }, { href: '/blocks', label: 'Blocks', description: 'Browse recent block production and block detail pages.' }, { href: '/transactions', label: 'Transactions', description: 'Inspect indexed transactions and their linked entities.' }, { href: '/addresses', label: 'Addresses', description: 'Open recent address activity and address detail pages.' }, ], [], ) const dataItems: MenuItem[] = useMemo( () => [ { href: '/tokens', label: 'Tokens', description: 'Review curated assets, standards, and token detail pages.' }, { href: '/analytics', label: 'Analytics', description: 'Open explorer-visible transaction and block activity summaries.' }, { href: '/pools', label: 'Pools', description: 'Browse mission-control pool inventory and route-backed liquidity context.' }, { href: '/watchlist', label: 'Watchlist', description: 'Jump into tracked addresses and saved explorer entities.' }, ], [], ) const operationsItems: MenuItem[] = useMemo( () => [ { href: '/operations', label: 'Operations hub', description: 'Open the consolidated operator surface for live support workflows.' }, { href: '/bridge', label: 'Bridge', description: 'Inspect relay lanes, queue posture, and bridge trace tooling.' }, { href: '/routes', label: 'Routes', description: 'Review live route coverage, same-chain lanes, and bridge paths.' }, { href: '/liquidity', label: 'Liquidity', description: 'Check planner-backed route access and live liquidity posture.' }, { href: '/system', label: 'System', description: 'Inspect topology, RPC capability, and public integration inventory.' }, { href: '/operator', label: 'Operator', description: 'Open planner, route, and relay shortcuts in one public page.' }, { href: '/weth', label: 'WETH', description: 'Review wrapped-asset references and bridge-oriented WETH context.' }, { href: '/chain138-command-center.html', label: 'Command Center', description: 'Open the visual command-center reference.', external: true }, ], [], ) const commandItems: HeaderCommandItem[] = [ { label: 'Copy current page link', description: 'Copy the current explorer URL for operator handoff or support notes.', section: 'Actions', keywords: ['copy', 'link', 'share', 'url'], onSelect: () => void handleCopyCurrentUrl(), }, walletSession ? { href: '/access', label: 'Open connected account', description: `Review ${getAccessTier(walletSession)} permissions, subscriptions, and access features.`, section: 'Actions', keywords: ['account', 'session', 'access', 'permissions'], } : { label: connectingWallet ? 'Connecting wallet…' : 'Connect wallet now', description: 'Start wallet connection and open the account access surface.', section: 'Actions', keywords: ['wallet', 'connect', 'account', 'signin'], onSelect: () => void handleConnectWallet(), }, mode === 'guided' ? { label: 'Switch to Expert Mode', description: 'Reduce helper text and keep the interface denser.', section: 'Actions', keywords: ['guided', 'expert', 'mode', 'density'], onSelect: () => setMode('expert'), } : { label: 'Switch to Guided Mode', description: 'Restore fuller explanations, helper text, and more interpreted labels.', section: 'Actions', keywords: ['guided', 'expert', 'mode', 'density'], onSelect: () => setMode('guided'), }, walletSession ? { label: 'Disconnect wallet', description: 'End the current wallet connection on this device.', section: 'Actions', keywords: ['wallet', 'disconnect', 'sign out'], onSelect: handleDisconnectWallet, } : { href: '/wallet', label: 'Open wallet tools', description: 'Review supported networks, token lists, and wallet integration guidance.', section: 'Actions', keywords: ['wallet', 'settings', 'metamask', 'network'], }, ...exploreItems.map((item) => ({ href: item.href || '/', label: item.label, description: item.description, section: 'Explore', keywords: ['overview', 'browse'], })), ...dataItems.map((item) => ({ href: item.href || '/', label: item.label, description: item.description, section: 'Data', keywords: ['tokens', 'analytics', 'pools', 'watchlist'], })), { href: '/docs', label: 'Docs', description: 'Open the canonical explorer documentation surface.', section: 'Docs', keywords: ['documentation', 'guide', 'reference'], }, ...operationsItems .filter((item) => !item.external) .map((item) => ({ href: item.href || '/', label: item.label, description: item.description, section: 'Operations', keywords: ['ops', 'bridge', 'routes', 'liquidity', 'system'], })), { href: '/wallet', label: 'Wallet Tools', description: 'Open network, token-list, and wallet integration tooling.', section: 'Wallet', keywords: ['wallet', 'tokens', 'catalog', 'metamask'], }, { href: '/access', label: 'Account Access', description: 'Open account-linked explorer access and subscription tools.', section: 'Account', keywords: ['access', 'permissions', 'session', 'account'], }, ] return ( <>
setMobileMenuOpen(false)} aria-label="Go to DBIS Explorer home" >
setCommandPaletteOpen(true)} /> void handleConnectWallet()} onCopyAddress={() => void handleCopyAddress()} onSwitchWallet={() => void handleSwitchWallet()} onDisconnect={handleDisconnectWallet} />
{walletSession ? ( {shortAddress(walletSession.address)} ) : ( )}
{mobileMenuOpen ? (
{ setMobileMenuOpen(false) setCommandPaletteOpen(true) }} /> {walletConnectError ? (

{walletConnectError}

) : null}
Explore
{exploreItems.map((item) => ( setMobileMenuOpen(false)} className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300" > {item.label} {item.description ? ( {item.description} ) : null} ))}
Data & Docs
{dataItems.map((item) => ( setMobileMenuOpen(false)} className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300" > {item.label} {item.description ? ( {item.description} ) : null} ))} setMobileMenuOpen(false)} className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300" > Docs Open the canonical explorer documentation surface.
Operations
{operationsItems.map((item) => item.external ? ( setMobileMenuOpen(false)} className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300" > {item.label} {item.description ? ( {item.description} ) : null} ) : ( setMobileMenuOpen(false)} className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-800 shadow-sm transition-colors hover:border-primary-300 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-primary-500 dark:hover:text-primary-300" > {item.label} {item.description ? ( {item.description} ) : null} ), )}
{walletSession ? (
{getAccessTier(walletSession)}
{mode === 'guided' ? (
Connected wallet access stays aligned with the same account state shown in the desktop header.
) : null}
setMobileMenuOpen(false)} className="rounded-2xl bg-gray-950 px-4 py-3 text-sm font-semibold text-white dark:bg-white dark:text-gray-950" > Account · {shortAddress(walletSession.address)}
) : ( )}
) : null}
setCommandPaletteOpen(false)} items={commandItems} /> ) }