feat(explorer): dual-chain wallet metadata, native coin pricing, and UI refresh.
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 20s
Validate Explorer / frontend (push) Failing after 24s
Validate Explorer / smoke-e2e (push) Has been skipped

Add Chain 138 wallet network metadata and stats coin-price enrichment; sync frontend explorer SPA, command center, and address/token pages with backend config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-06-19 16:16:17 -07:00
parent 0f02e6e54f
commit b87ebee6a1
94 changed files with 7648 additions and 1124 deletions

View File

@@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
import Navbar from './Navbar'
import Footer from './Footer'
import ExplorerAgentTool from './ExplorerAgentTool'
import ExplorerDocumentHead from './ExplorerDocumentHead'
import { UiModeProvider } from './UiModeContext'
import { PostureGlossaryProvider } from './PostureGlossaryProvider'
@@ -9,6 +10,7 @@ export default function ExplorerChrome({ children }: { children: ReactNode }) {
return (
<UiModeProvider>
<PostureGlossaryProvider>
<ExplorerDocumentHead />
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<a
href="#main-content"

View File

@@ -0,0 +1,56 @@
'use client'
import Head from 'next/head'
import { useRouter } from 'next/router'
const BASE_TITLE = 'DBIS Explorer'
function resolveDocumentTitle(pathname: string, query: Record<string, string | string[] | undefined>): string {
if (pathname === '/') return `${BASE_TITLE} — Chain 138`
if (pathname === '/search') return `Search — ${BASE_TITLE}`
if (pathname === '/wallet') return `Wallet Tools — ${BASE_TITLE}`
if (pathname === '/protocols') return `Official Protocol Contracts — ${BASE_TITLE}`
if (pathname.startsWith('/protocols/')) {
const id = typeof query.id === 'string' ? query.id : pathname.split('/').pop() || 'Protocol'
return `${id} — Protocols — ${BASE_TITLE}`
}
if (pathname === '/pools') return `Pool Registry — ${BASE_TITLE}`
if (pathname.startsWith('/pools/')) return `Pool Detail — ${BASE_TITLE}`
if (pathname === '/liquidity') return `Liquidity — ${BASE_TITLE}`
if (pathname === '/operations') return `Operations — ${BASE_TITLE}`
if (pathname === '/docs') return `Documentation — ${BASE_TITLE}`
if (pathname === '/tokens') return `Tokens — ${BASE_TITLE}`
if (pathname.startsWith('/tokens/')) return `Token — ${BASE_TITLE}`
if (pathname === '/addresses') return `Addresses — ${BASE_TITLE}`
if (pathname.startsWith('/addresses/')) return `Address — ${BASE_TITLE}`
if (pathname === '/blocks') return `Blocks — ${BASE_TITLE}`
if (pathname === '/transactions') return `Transactions — ${BASE_TITLE}`
if (pathname.startsWith('/transactions/')) return `Transaction — ${BASE_TITLE}`
if (pathname === '/bridge') return `Bridge — ${BASE_TITLE}`
if (pathname === '/routes') return `Routes — ${BASE_TITLE}`
if (pathname === '/analytics') return `Analytics — ${BASE_TITLE}`
if (pathname === '/operator') return `Operator — ${BASE_TITLE}`
if (pathname === '/system') return `System — ${BASE_TITLE}`
if (pathname === '/weth') return `WETH — ${BASE_TITLE}`
if (pathname === '/watchlist') return `Watchlist — ${BASE_TITLE}`
if (pathname === '/access') return `Account Access — ${BASE_TITLE}`
const segment = pathname.split('/').filter(Boolean)[0]
if (segment) {
const label = segment.charAt(0).toUpperCase() + segment.slice(1)
return `${label}${BASE_TITLE}`
}
return BASE_TITLE
}
export default function ExplorerDocumentHead() {
const router = useRouter()
const pathname = router.pathname || '/'
const title = resolveDocumentTitle(pathname, router.query)
return (
<Head>
<title>{title}</title>
</Head>
)
}

View File

@@ -47,6 +47,7 @@ export default function Footer() {
<li><Link className={footerLinkClass} href="/routes">Routes</Link></li>
<li><Link className={footerLinkClass} href="/liquidity">Liquidity</Link></li>
<li><Link className={footerLinkClass} href="/pools">Pools</Link></li>
<li><Link className={footerLinkClass} href="/protocols">Protocols</Link></li>
<li><Link className={footerLinkClass} href="/analytics">Analytics</Link></li>
<li><Link className={footerLinkClass} href="/operator">Operator</Link></li>
<li><Link className={footerLinkClass} href="/system">System</Link></li>
@@ -87,9 +88,9 @@ export default function Footer() {
</p>
<p>
Command center:{' '}
<a className={footerLinkClass} href="/chain138-command-center.html" target="_blank" rel="noopener noreferrer">
<Link className={footerLinkClass} href="/topology">
Chain 138 visual map
</a>
</Link>
</p>
<p className="text-xs leading-5 text-gray-500 dark:text-gray-500">
Questions about the explorer, chain metadata, route discovery, or liquidity access

View File

@@ -490,6 +490,7 @@ export default function Navbar() {
pathname.startsWith('/tokens') ||
pathname.startsWith('/analytics') ||
pathname.startsWith('/pools') ||
pathname.startsWith('/protocols') ||
pathname.startsWith('/watchlist')
const isOperationsActive =
pathname.startsWith('/bridge') ||
@@ -593,6 +594,7 @@ export default function Navbar() {
{ 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: '/protocols', label: 'Protocols', description: 'Review official upstream protocol contracts and production guardrails on Chain 138.' },
{ href: '/watchlist', label: 'Watchlist', description: 'Jump into tracked addresses and saved explorer entities.' },
],
[],
@@ -603,10 +605,11 @@ export default function Navbar() {
{ 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: '/protocols', label: 'Protocols', description: 'Official DODO, UniV2/V3, CCIP, and integration contracts on Chain 138.' },
{ 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 },
{ href: '/topology', label: 'Command Center', description: 'Open the visual topology and architecture map.' },
],
[],
)

View File

@@ -1,60 +1,97 @@
import { buildPaginationItems } from '@/utils/pagination'
interface PaginationControlsProps {
page: number
pageCount: number
onPageChange: (page: number) => void
label?: string
className?: string
disabled?: boolean
ariaLabel?: string
/** Known total pages for bounded pagination. Omit for cursor-style list pagination. */
pageCount?: number
/** Used when pageCount is omitted. */
hasNextPage?: boolean
}
const buttonClassName =
'rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300'
const activeButtonClassName = 'rounded-lg bg-primary-600 px-3 py-2 text-sm font-semibold text-white'
export default function PaginationControls({
page,
pageCount,
hasNextPage = false,
onPageChange,
label = 'Rows',
className = '',
disabled = false,
ariaLabel,
}: PaginationControlsProps) {
if (pageCount <= 1) return null
const isBounded = typeof pageCount === 'number'
const boundedPageCount = pageCount ?? 1
const pages = Array.from({ length: pageCount }, (_, index) => index + 1)
if (isBounded && boundedPageCount <= 1) {
return null
}
if (!isBounded && page <= 1 && !hasNextPage) {
return null
}
const paginationItems = isBounded ? buildPaginationItems(page, boundedPageCount) : []
const canGoPrevious = page > 1
const canGoNext = isBounded ? page < boundedPageCount : hasNextPage
const navLabel = ariaLabel || `${label} pagination`
const statusText = isBounded ? `${label}: page ${page} of ${boundedPageCount}` : `${label}: page ${page}`
return (
<div className={`mt-4 flex flex-wrap items-center justify-between gap-3 ${className}`}>
<div className="text-sm text-gray-600 dark:text-gray-400">
{label}: page {page} of {pageCount}
</div>
<nav
className={`mt-4 flex flex-wrap items-center justify-between gap-3 ${className}`}
aria-label={navLabel}
>
<div className="text-sm text-gray-600 dark:text-gray-400">{statusText}</div>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => onPageChange(Math.max(1, page - 1))}
disabled={page <= 1}
className="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300"
disabled={disabled || !canGoPrevious}
className={buttonClassName}
>
Previous
</button>
{pages.map((candidate) => (
<button
key={candidate}
type="button"
onClick={() => onPageChange(candidate)}
aria-current={candidate === page ? 'page' : undefined}
className={
candidate === page
? 'rounded-lg bg-primary-600 px-3 py-2 text-sm font-semibold text-white'
: 'rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300'
}
>
{candidate}
</button>
))}
{isBounded ? (
<ul className="flex flex-wrap items-center gap-2">
{paginationItems.map((item) =>
item.type === 'ellipsis' ? (
<li key={item.key} className="px-1 text-sm text-gray-500 dark:text-gray-400" aria-hidden="true">
</li>
) : (
<li key={item.page}>
<button
type="button"
onClick={() => onPageChange(item.page)}
disabled={disabled}
aria-current={item.page === page ? 'page' : undefined}
className={item.page === page ? activeButtonClassName : buttonClassName}
>
{item.page}
</button>
</li>
),
)}
</ul>
) : null}
<button
type="button"
onClick={() => onPageChange(Math.min(pageCount, page + 1))}
disabled={page >= pageCount}
className="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300"
onClick={() => onPageChange(isBounded ? Math.min(boundedPageCount, page + 1) : page + 1)}
disabled={disabled || !canGoNext}
className={buttonClassName}
>
Next
</button>
</div>
</div>
</nav>
)
}

View File

@@ -1,3 +1,5 @@
import { useCallback, useId } from 'react'
export interface SectionTab<T extends string> {
id: T
label: string
@@ -9,6 +11,21 @@ interface SectionTabsProps<T extends string> {
activeTab: T
onChange: (tab: T) => void
className?: string
idPrefix?: string
ariaLabel?: string
}
export function sectionTabPanelProps<T extends string>(
idPrefix: string,
tabId: T,
activeTab: T,
) {
return {
id: `${idPrefix}-panel-${tabId}`,
role: 'tabpanel' as const,
'aria-labelledby': `${idPrefix}-tab-${tabId}`,
hidden: activeTab !== tabId,
}
}
export default function SectionTabs<T extends string>({
@@ -16,29 +33,87 @@ export default function SectionTabs<T extends string>({
activeTab,
onChange,
className = '',
idPrefix,
ariaLabel = 'Sections',
}: SectionTabsProps<T>) {
const generatedId = useId().replace(/:/g, '')
const tabListPrefix = idPrefix || generatedId
const focusTab = useCallback(
(tabId: T) => {
const element = document.getElementById(`${tabListPrefix}-tab-${tabId}`)
element?.focus()
},
[tabListPrefix],
)
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
const currentIndex = tabs.findIndex((tab) => tab.id === activeTab)
if (currentIndex < 0) {
return
}
let nextIndex = currentIndex
if (event.key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % tabs.length
} else if (event.key === 'ArrowLeft') {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length
} else if (event.key === 'Home') {
nextIndex = 0
} else if (event.key === 'End') {
nextIndex = tabs.length - 1
} else {
return
}
event.preventDefault()
const nextTab = tabs[nextIndex]
onChange(nextTab.id)
focusTab(nextTab.id)
},
[activeTab, focusTab, onChange, tabs],
)
return (
<div className={`sticky top-0 z-20 border-b border-gray-200 bg-white/95 py-3 backdrop-blur dark:border-gray-800 dark:bg-gray-950/95 ${className}`}>
<div className="flex gap-2 overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => onChange(tab.id)}
className={
activeTab === tab.id
? 'whitespace-nowrap rounded-lg bg-primary-600 px-3 py-2 text-sm font-semibold text-white'
: 'whitespace-nowrap rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300'
}
>
{tab.label}
{typeof tab.count === 'number' ? (
<span className={activeTab === tab.id ? 'ml-2 text-primary-100' : 'ml-2 text-gray-500 dark:text-gray-400'}>
{tab.count.toLocaleString()}
</span>
) : null}
</button>
))}
<div
className={`sticky top-0 z-20 border-b border-gray-200 bg-white/95 py-3 backdrop-blur dark:border-gray-800 dark:bg-gray-950/95 ${className}`}
>
<div
role="tablist"
aria-label={ariaLabel}
className="flex gap-2 overflow-x-auto"
onKeyDown={handleKeyDown}
>
{tabs.map((tab) => {
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
id={`${tabListPrefix}-tab-${tab.id}`}
type="button"
role="tab"
aria-selected={isActive}
aria-controls={`${tabListPrefix}-panel-${tab.id}`}
tabIndex={isActive ? 0 : -1}
onClick={() => onChange(tab.id)}
className={
isActive
? 'whitespace-nowrap rounded-lg bg-primary-600 px-3 py-2 text-sm font-semibold text-white'
: 'whitespace-nowrap rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300'
}
>
{tab.label}
{typeof tab.count === 'number' ? (
<span className={isActive ? 'ml-2 text-primary-100' : 'ml-2 text-gray-500 dark:text-gray-400'}>
{tab.count.toLocaleString()}
</span>
) : null}
</button>
)
})}
</div>
</div>
)