Improve explorer SSR, hydration, compaction, and smoke coverage.
Some checks failed
Validate Explorer / frontend (push) Failing after 14m45s
Deploy Explorer Live / deploy (push) Failing after 14m52s
Validate Explorer / smoke-e2e (push) Has been cancelled

Defer heavy getServerSideProps on home, operator, addresses, and wallet to cut TTFB; centralize locale-safe formatters with client-only relative times; add compact ops UX, bridge/operator relay pagination, and Playwright route/scroll smoke in deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-06-22 15:52:47 -07:00
parent 7e82b917f5
commit 0cb31cfa9d
74 changed files with 4274 additions and 2934 deletions

View File

@@ -1,8 +1,9 @@
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import EntityBadge from '@/components/common/EntityBadge'
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
import type { ChainActivityContext } from '@/utils/activityContext'
import { formatRelativeAge, formatTimestamp } from '@/utils/format'
import { formatInteger, formatTimestamp } from '@/utils/format'
import { Explain, useUiMode } from './UiModeContext'
function resolveTone(state: ChainActivityContext['state']): 'success' | 'warning' | 'neutral' {
@@ -58,9 +59,14 @@ export default function ActivityContextPanel({
const { mode } = useUiMode()
const tone = resolveTone(context.state)
const dualTimelineLabel =
context.latest_block_timestamp && context.latest_transaction_timestamp
? `${formatRelativeAge(context.latest_block_timestamp)} head · ${formatRelativeAge(context.latest_transaction_timestamp)} latest tx`
: 'Dual timeline unavailable'
context.latest_block_timestamp && context.latest_transaction_timestamp ? (
<>
<ClientRelativeTime value={context.latest_block_timestamp} suffix=" head" /> ·{' '}
<ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" latest tx" />
</>
) : (
'Dual timeline unavailable'
)
return (
<Card className="border border-sky-200 bg-sky-50/60 dark:border-sky-900/40 dark:bg-sky-950/20" title={title}>
@@ -86,11 +92,11 @@ export default function ActivityContextPanel({
<span className="font-semibold text-gray-900 dark:text-white">
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
</span>{' '}
was {formatRelativeAge(context.latest_block_timestamp)}. Latest visible transaction was{' '}
was <ClientRelativeTime value={context.latest_block_timestamp} suffix=" ago" />. Latest visible transaction was{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
</span>{' '}
{formatRelativeAge(context.latest_transaction_timestamp)}.{' '}
<ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />.{' '}
{mode === 'guided'
? 'Use the block gap and last non-empty block above to tell low activity apart from an indexing issue.'
: dualTimelineLabel}
@@ -104,11 +110,11 @@ export default function ActivityContextPanel({
<span className="font-semibold text-gray-900 dark:text-white">
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
</span>{' '}
was {formatRelativeAge(context.latest_block_timestamp)}. Latest visible transaction was{' '}
was <ClientRelativeTime value={context.latest_block_timestamp} suffix=" ago" />. Latest visible transaction was{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
</span>{' '}
{formatRelativeAge(context.latest_transaction_timestamp)}.
<ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />.
</div>
</div>
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
@@ -135,7 +141,7 @@ export default function ActivityContextPanel({
) : null}
{context.block_gap_to_latest_transaction != null ? (
<span>
Block gap to latest visible transaction: {context.block_gap_to_latest_transaction.toLocaleString()}
Block gap to latest visible transaction: {formatInteger(context.block_gap_to_latest_transaction)}
</span>
) : null}
{context.latest_transaction_timestamp ? (

View File

@@ -0,0 +1,57 @@
'use client'
import { useEffect, useState } from 'react'
import { formatRelativeAgeAt } from '@/utils/format'
const PLACEHOLDER = '…'
export default function ClientRelativeTime({
value,
prefix = '',
suffix = '',
fallback = 'Unknown',
className,
}: {
value?: string | null
prefix?: string
suffix?: string
fallback?: string
className?: string
}) {
const [label, setLabel] = useState(PLACEHOLDER)
useEffect(() => {
if (!value) {
setLabel(fallback)
return
}
const parsed = Date.parse(value)
if (!Number.isFinite(parsed)) {
setLabel(fallback)
return
}
const update = () => setLabel(formatRelativeAgeAt(Date.now(), value))
update()
const id = window.setInterval(update, 30_000)
return () => window.clearInterval(id)
}, [value, fallback])
const content = label === PLACEHOLDER ? PLACEHOLDER : label
if (className) {
return (
<span className={className} suppressHydrationWarning>
{prefix}
{content}
{suffix}
</span>
)
}
return (
<span suppressHydrationWarning>
{prefix}
{content}
{suffix}
</span>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
export type CompactMetric = {
label: string
value: string
}
export default function CompactMetricBar({
metrics,
className = 'mb-4',
}: {
metrics: CompactMetric[]
className?: string
}) {
if (metrics.length === 0) return null
return (
<div
className={`flex flex-wrap items-center gap-x-4 gap-y-1 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-300 ${className}`}
>
{metrics.map((metric) => (
<span key={metric.label}>
<span className="text-gray-500 dark:text-gray-400">{metric.label}:</span>{' '}
<span className="font-medium text-gray-900 dark:text-white">{metric.value}</span>
</span>
))}
</div>
)
}

View File

@@ -0,0 +1,70 @@
'use client'
import { useId, useState, type ReactNode } from 'react'
interface DisclosureSectionProps {
title: string
children: ReactNode
defaultOpen?: boolean
className?: string
headingClassName?: string
/** When true, content is always visible and the toggle is hidden (desktop layout). */
forceOpen?: boolean
/** When true, keep accordion behavior on all breakpoints (no md:auto-expand). */
alwaysCollapsible?: boolean
}
export default function DisclosureSection({
title,
children,
defaultOpen = false,
className = '',
headingClassName = '',
forceOpen = false,
alwaysCollapsible = false,
}: DisclosureSectionProps) {
const [open, setOpen] = useState(defaultOpen || forceOpen)
const panelId = useId().replace(/:/g, '')
const isOpen = forceOpen || open
return (
<section className={className}>
{forceOpen ? (
<div className={`mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 ${headingClassName}`}>
{title}
</div>
) : (
<button
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setOpen((current) => !current)}
className={`flex w-full items-center justify-between gap-3 rounded-lg border border-gray-200 bg-gray-50/80 px-3 py-2.5 text-left text-sm font-semibold uppercase tracking-wide text-gray-700 transition hover:border-primary-300 dark:border-gray-800 dark:bg-gray-900/40 dark:text-gray-200 dark:hover:border-primary-700 ${alwaysCollapsible ? '' : 'md:hidden'} ${headingClassName}`}
>
<span>{title}</span>
<span aria-hidden="true" className="text-base text-gray-500 dark:text-gray-400">
{isOpen ? '' : '+'}
</span>
</button>
)}
<div
id={panelId}
className={
forceOpen
? ''
: alwaysCollapsible
? `${isOpen ? 'block' : 'hidden'} mt-3`
: `${isOpen ? 'block' : 'hidden'} mt-3 md:mt-0 md:block`
}
>
{!forceOpen && !alwaysCollapsible ? (
<div className="mb-3 hidden text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 md:block">
{title}
</div>
) : null}
{children}
</div>
</section>
)
}

View File

@@ -0,0 +1,43 @@
'use client'
import type { ReactNode } from 'react'
import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
import type { ExplorerStats } from '@/services/api/stats'
import type { ChainActivityContext } from '@/utils/activityContext'
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
import DisclosureSection from '@/components/common/DisclosureSection'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
export default function ExplorerFreshnessDisclosure({
context,
stats,
bridgeStatus,
scopeLabel,
activityTitle = 'Recency',
title = 'Index freshness',
className = 'mb-4',
children,
}: {
context: ChainActivityContext
stats?: ExplorerStats | null
bridgeStatus?: MissionControlBridgeStatusResponse | null
scopeLabel: string
activityTitle?: string
title?: string
className?: string
children?: ReactNode
}) {
return (
<DisclosureSection title={title} defaultOpen={false} alwaysCollapsible className={className}>
<ActivityContextPanel compact context={context} title={activityTitle} />
<FreshnessTrustNote
className="mt-2"
context={context}
stats={stats}
bridgeStatus={bridgeStatus}
scopeLabel={scopeLabel}
/>
{children}
</DisclosureSection>
)
}

View File

@@ -1,4 +1,6 @@
import Link from 'next/link'
import DisclosureSection from '@/components/common/DisclosureSection'
import FooterLinkGroups from '@/components/common/FooterLinkGroups'
import FooterPublicApiLinks from '@/components/common/FooterPublicApiLinks'
const footerLinkClass =
@@ -8,96 +10,53 @@ export default function Footer() {
const year = new Date().getFullYear()
return (
<footer className="mt-auto border-t border-gray-200 dark:border-gray-700 bg-white/90 dark:bg-gray-900/90 backdrop-blur">
<div className="container mx-auto px-4 py-6 sm:py-8">
<div className="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-4">
<div className="space-y-3 rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
DBIS Explorer
</div>
<p className="max-w-xl text-sm leading-6 text-gray-600 dark:text-gray-400">
Built on Blockscout for the DBIS Chain 138 explorer surface.
Explorer data is powered by Blockscout, Chain 138 RPC, and the companion MetaMask Snap.
</p>
<p className="max-w-xl text-xs leading-5 text-gray-500 dark:text-gray-500">
Primary public explorer access is served at <code>explorer.d-bis.org</code>.
<code> blockscout.defi-oracle.io</code> is the Blockscout companion domain for the same Chain 138 explorer surface.
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
© {year} DBIS. All rights reserved.
<footer className="mt-auto border-t border-gray-200 bg-white/90 backdrop-blur dark:border-gray-700 dark:bg-gray-900/90">
<div className="container mx-auto px-4 py-4 sm:py-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 shrink-0 lg:max-w-xs">
<div className="text-sm font-semibold text-gray-900 dark:text-white">DBIS Explorer</div>
<p className="mt-1 text-xs leading-5 text-gray-600 dark:text-gray-400">
Chain 138 explorer on Blockscout. Primary domain: <code>explorer.d-bis.org</code>.
</p>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-500">© {year} DBIS</p>
</div>
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Resources
</div>
<ul className="space-y-2 text-sm">
<li><Link className={footerLinkClass} href="/search">Search</Link></li>
<li><Link className={footerLinkClass} href="/docs">Documentation</Link></li>
<li><Link className={footerLinkClass} href="/blocks">Blocks</Link></li>
<li><Link className={footerLinkClass} href="/transactions">Transactions</Link></li>
<li><Link className={footerLinkClass} href="/tokens">Tokens</Link></li>
<li><Link className={footerLinkClass} href="/addresses">Addresses</Link></li>
<li><Link className={footerLinkClass} href="/watchlist">Watchlist</Link></li>
<li><Link className={footerLinkClass} href="/access">Account access</Link></li>
<li><Link className={footerLinkClass} href="/wallet">Wallet tools</Link></li>
<li><Link className={footerLinkClass} href="/operations">Operations hub</Link></li>
<li><Link className={footerLinkClass} href="/bridge">Bridge</Link></li>
<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>
<li><Link className={footerLinkClass} href="/weth">WETH</Link></li>
<li><a className={footerLinkClass} href="/privacy.html">Privacy Policy</a></li>
<li><a className={footerLinkClass} href="/terms.html">Terms of Service</a></li>
<li><a className={footerLinkClass} href="/acknowledgments.html">Acknowledgments</a></li>
</ul>
<div className="hidden min-w-0 flex-1 lg:block">
<FooterLinkGroups />
</div>
</div>
<DisclosureSection
title="Site navigation"
alwaysCollapsible
className="mt-3 lg:hidden"
>
<FooterLinkGroups />
</DisclosureSection>
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Public APIs
</div>
<FooterPublicApiLinks />
<p className="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-500">
Read-only JSON endpoints on the public explorer domain. No API key required.
</p>
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
<DisclosureSection title="Public APIs" alwaysCollapsible className="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<FooterPublicApiLinks compact />
</DisclosureSection>
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Contact
</div>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<DisclosureSection title="Contact" alwaysCollapsible className="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<div className="space-y-1.5 text-sm text-gray-600 dark:text-gray-400">
<p>
Support:{' '}
<a className={footerLinkClass} href="mailto:support@d-bis.org">
support@d-bis.org
</a>
</p>
<p>
Snap site:{' '}
<Link className={footerLinkClass} href="/topology">
Chain 138 map
</Link>
{' · '}
<a className={footerLinkClass} href="/snap/" target="_blank" rel="noopener noreferrer">
/snap/ on the current explorer domain
Snap site
</a>
</p>
<p>
Command center:{' '}
<Link className={footerLinkClass} href="/topology">
Chain 138 visual map
</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
can be sent to the support mailbox above.
</p>
</div>
</div>
</DisclosureSection>
</div>
</div>
</footer>

View File

@@ -0,0 +1,46 @@
'use client'
import Link from 'next/link'
import { footerNavGroups, type FooterNavLink } from '@/data/footerNav'
const footerLinkClass =
'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
function FooterNavLinkItem({ link }: { link: FooterNavLink }) {
if (link.external) {
return (
<li>
<a className={footerLinkClass} href={link.href}>
{link.label}
</a>
</li>
)
}
return (
<li>
<Link className={footerLinkClass} href={link.href}>
{link.label}
</Link>
</li>
)
}
export default function FooterLinkGroups() {
return (
<div className="grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-4">
{footerNavGroups.map((group) => (
<div key={group.id}>
<div className="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{group.title}
</div>
<ul className="space-y-1 text-sm">
{group.links.map((link) => (
<FooterNavLinkItem key={`${group.id}-${link.href}`} link={link} />
))}
</ul>
</div>
))}
</div>
)
}

View File

@@ -12,7 +12,7 @@ function absoluteApiUrl(href: string): string {
return `${window.location.origin}${href.startsWith('/') ? href : `/${href}`}`
}
export default function FooterPublicApiLinks() {
export default function FooterPublicApiLinks({ compact = false }: { compact?: boolean }) {
const [copiedHref, setCopiedHref] = useState<string | null>(null)
const copyUrl = async (href: string) => {
@@ -27,15 +27,23 @@ export default function FooterPublicApiLinks() {
}
return (
<ul className="space-y-3 text-sm">
<ul className={compact ? 'space-y-1.5 text-sm' : 'space-y-3 text-sm'}>
{explorerPublicApiLinks.map((link) => (
<li key={link.href}>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="min-w-0 flex-1">
<a className={footerLinkClass} href={link.href} target="_blank" rel="noopener noreferrer">
<a
className={footerLinkClass}
href={link.href}
target="_blank"
rel="noopener noreferrer"
title={compact ? link.description : undefined}
>
{link.label}
</a>
<p className="mt-0.5 text-xs leading-5 text-gray-500 dark:text-gray-500">{link.description}</p>
{!compact ? (
<p className="mt-0.5 text-xs leading-5 text-gray-500 dark:text-gray-500">{link.description}</p>
) : null}
</div>
<button
type="button"

View File

@@ -0,0 +1,68 @@
import type { ChainActivityContext } from '@/utils/activityContext'
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
import { formatInteger } from '@/utils/format'
export default function FreshnessDetail({
context,
diagnosticExplanation,
activityState,
}: {
context: ChainActivityContext
diagnosticExplanation?: string | null
activityState?: string | null
}) {
if (diagnosticExplanation) {
return <>{diagnosticExplanation}</>
}
if (activityState === 'quiet_chain') {
const latestNonEmptyBlock =
context.last_non_empty_block_number != null ? `#${formatInteger(context.last_non_empty_block_number)}` : 'unknown'
const blockGap =
context.block_gap_to_latest_transaction != null
? `${formatInteger(context.block_gap_to_latest_transaction)} blocks`
: 'unknown'
return (
<>
Quiet-chain signal: head blocks may be empty while the chain remains current. Block gap to latest visible
transaction: {blockGap}. Last non-empty block: {latestNonEmptyBlock}.
</>
)
}
if (context.transaction_visibility_unavailable) {
return <>Use chain-head visibility and the last non-empty block as the current trust anchors.</>
}
const latestNonEmptyBlock =
context.last_non_empty_block_number != null ? `#${formatInteger(context.last_non_empty_block_number)}` : 'unknown'
const blockGap =
context.block_gap_to_latest_transaction != null
? `${formatInteger(context.block_gap_to_latest_transaction)} blocks`
: null
if (context.head_is_idle) {
return (
<>
Latest visible transaction: <ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />. Block
gap: {blockGap || 'unknown'}. Last non-empty block: {latestNonEmptyBlock}.
</>
)
}
if (context.state === 'active') {
return (
<>
Latest visible transaction: <ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />.
Recent indexed activity remains close to the tip.
</>
)
}
return (
<>
Latest visible transaction: <ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />. Recent
head blocks may be quiet even while the chain remains current.
</>
)
}

View File

@@ -5,7 +5,7 @@ import {
resolveFreshnessSourceLabel,
summarizeFreshnessConfidence,
} from '@/utils/explorerFreshness'
import { formatRelativeAge } from '@/utils/format'
import FreshnessDetail from './FreshnessDetail'
import { useUiMode } from './UiModeContext'
function buildSummary(context: ChainActivityContext, activityState?: string | null) {
@@ -32,44 +32,6 @@ function buildSummary(context: ChainActivityContext, activityState?: string | nu
return 'Freshness context is based on the latest visible public explorer evidence.'
}
function buildDetail(context: ChainActivityContext, diagnosticExplanation?: string | null, activityState?: string | null) {
if (diagnosticExplanation) {
return diagnosticExplanation
}
if (activityState === 'quiet_chain') {
const latestNonEmptyBlock =
context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number.toLocaleString()}` : 'unknown'
const blockGap =
context.block_gap_to_latest_transaction != null
? `${context.block_gap_to_latest_transaction.toLocaleString()} blocks`
: 'unknown'
return `Quiet-chain signal: head blocks may be empty while the chain remains current. Block gap to latest visible transaction: ${blockGap}. Last non-empty block: ${latestNonEmptyBlock}.`
}
if (context.transaction_visibility_unavailable) {
return 'Use chain-head visibility and the last non-empty block as the current trust anchors.'
}
const latestTxAge = formatRelativeAge(context.latest_transaction_timestamp)
const latestNonEmptyBlock =
context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number.toLocaleString()}` : 'unknown'
const blockGap =
context.block_gap_to_latest_transaction != null
? `${context.block_gap_to_latest_transaction.toLocaleString()} blocks`
: null
if (context.head_is_idle) {
return `Latest visible transaction: ${latestTxAge}. Block gap: ${blockGap || 'unknown'}. Last non-empty block: ${latestNonEmptyBlock}.`
}
if (context.state === 'active') {
return `Latest visible transaction: ${latestTxAge}. Recent indexed activity remains close to the tip.`
}
return `Latest visible transaction: ${latestTxAge}. Recent head blocks may be quiet even while the chain remains current.`
}
function normalizeSentence(value?: string | null): string {
if (!value) return ''
return value.trim().replace(/[.\s]+$/, '')
@@ -120,8 +82,12 @@ export default function FreshnessTrustNote({
<div className={`rounded-2xl border border-gray-200 bg-white/80 px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/40${normalizedClassName}`}>
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context, activityState)}</div>
<div className="mt-1 text-gray-600 dark:text-gray-400">
{normalizeSentence(buildDetail(context, diagnosticExplanation, activityState))}.{' '}
{scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
<FreshnessDetail
context={context}
diagnosticExplanation={diagnosticExplanation}
activityState={activityState}
/>
. {scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
{normalizeSentence(sourceLabel)}.
</div>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">

View File

@@ -0,0 +1,45 @@
'use client'
import { formatInteger } from '@/utils/format'
interface ListFilterBarProps {
query: string
onQueryChange: (value: string) => void
placeholder?: string
resultCount?: number
totalCount?: number
className?: string
}
export default function ListFilterBar({
query,
onQueryChange,
placeholder = 'Filter…',
resultCount,
totalCount,
className = '',
}: ListFilterBarProps) {
const showCounts = typeof resultCount === 'number' && typeof totalCount === 'number'
return (
<div className={`flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between ${className}`}>
<label className="block min-w-0 flex-1">
<span className="sr-only">{placeholder}</span>
<input
type="search"
value={query}
onChange={(event) => onQueryChange(event.target.value)}
placeholder={placeholder}
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500"
/>
</label>
{showCounts ? (
<div className="shrink-0 text-sm text-gray-600 dark:text-gray-400">
{resultCount === totalCount
? `${formatInteger(totalCount)} items`
: `${formatInteger(resultCount)} of ${formatInteger(totalCount)}`}
</div>
) : null}
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { formatRelativeAge, formatTimestamp } from '@/utils/format'
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
import { formatTimestamp } from '@/utils/format'
function formatSource(source?: string | null): string {
switch (source) {
@@ -26,14 +27,33 @@ export default function MarketEvidenceNote({
method?: string
compact?: boolean
}) {
const freshness = lastUpdated ? `${formatRelativeAge(lastUpdated)} (${formatTimestamp(lastUpdated)})` : 'timestamp unavailable'
const text = compact
? `Updated ${freshness} · ${formatSource(source)}`
: `Source: ${formatSource(source)}. Updated: ${freshness}. Method: ${method}`
return (
<p className={`${compact ? 'mt-1' : 'mt-3'} text-xs leading-5 text-gray-500 dark:text-gray-400`}>
{text}
{compact ? (
<>
Updated{' '}
{lastUpdated ? (
<>
<ClientRelativeTime value={lastUpdated} suffix={` (${formatTimestamp(lastUpdated)})`} />
</>
) : (
'timestamp unavailable'
)}{' '}
· {formatSource(source)}
</>
) : (
<>
Source: {formatSource(source)}. Updated:{' '}
{lastUpdated ? (
<>
<ClientRelativeTime value={lastUpdated} suffix={` (${formatTimestamp(lastUpdated)})`} />
</>
) : (
'timestamp unavailable'
)}
. Method: {method}
</>
)}
</p>
)
}

View File

@@ -2,8 +2,8 @@
import { Card } from '@/libs/frontend-ui-primitives'
import EntityBadge from '@/components/common/EntityBadge'
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
import type { MissionControlMode } from '@/services/api/missionControl'
import { formatRelativeAge } from '@/utils/format'
const REASON_LABELS: Record<string, string> = {
live_homepage_stream_not_attached: 'Live homepage stream is not attached; relay posture uses snapshot polling.',
@@ -57,7 +57,9 @@ export default function MissionDeliveryModePanel({
<div className="flex flex-wrap items-center gap-2">
<EntityBadge label={`mode ${mode.kind}`} tone={modeTone(mode.kind)} />
{mode.updated_at ? (
<span className="text-xs text-gray-600 dark:text-gray-400">Updated {formatRelativeAge(mode.updated_at)}</span>
<span className="text-xs text-gray-600 dark:text-gray-400">
Updated <ClientRelativeTime value={mode.updated_at} suffix=" ago" />
</span>
) : null}
</div>
{mode.reason ? (

View File

@@ -7,6 +7,7 @@ import { accessApi, institutionalTierLabels, type WalletAccessSession } from '@/
import BrandLockup from './BrandLockup'
import HeaderCommandPalette, { type HeaderCommandItem } from './HeaderCommandPalette'
import { useUiMode } from './UiModeContext'
import { formatTimestamp } from '@/utils/format'
type MenuItem = {
href?: string
@@ -430,7 +431,7 @@ function AccountButton({
const sessionSummary = getSessionSummary(walletSession)
const tierLabel = getAccessTier(walletSession)
const expiresLabel = walletSession.expiresAt
? new Date(walletSession.expiresAt).toLocaleString()
? formatTimestamp(walletSession.expiresAt)
: 'Session expiry unavailable'
return (
@@ -488,6 +489,7 @@ export default function Navbar() {
pathname.startsWith('/addresses')
const isDataActive =
pathname.startsWith('/tokens') ||
pathname.startsWith('/wallet') ||
pathname.startsWith('/analytics') ||
pathname.startsWith('/pools') ||
pathname.startsWith('/protocols') ||
@@ -592,6 +594,7 @@ export default function Navbar() {
const dataItems: MenuItem[] = useMemo(
() => [
{ href: '/tokens', label: 'Tokens', description: 'Review curated assets, standards, and token detail pages.' },
{ href: '/wallet', label: 'Wallet tools', description: 'Add Chain 138 to MetaMask and import tokens via EIP-747.' },
{ 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.' },

View File

@@ -10,39 +10,53 @@ export default function PageIntro({
title,
description,
actions = [],
compact = false,
}: {
eyebrow?: string
title: string
description: string
actions?: PageIntroAction[]
compact?: boolean
}) {
return (
<section className="mb-5 border-b border-gray-200 pb-5 dark:border-gray-800 sm:mb-6 sm:pb-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<section
className={
compact
? 'mb-4 border-b border-gray-200 pb-3 dark:border-gray-800'
: 'mb-5 border-b border-gray-200 pb-5 dark:border-gray-800 sm:mb-6 sm:pb-6'
}
>
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div className="min-w-0">
{eyebrow ? (
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-primary-700 dark:text-primary-300">
<div className="mb-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-primary-700 dark:text-primary-300">
{eyebrow}
</div>
) : null}
<h1 className="text-2xl font-semibold tracking-normal text-gray-950 dark:text-white sm:text-3xl">{title}</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
<h1 className={`font-semibold tracking-normal text-gray-950 dark:text-white ${compact ? 'text-xl sm:text-2xl' : 'text-2xl sm:text-3xl'}`}>
{title}
</h1>
<p
className={`mt-1.5 max-w-3xl text-sm text-gray-600 dark:text-gray-400 ${
compact ? 'line-clamp-2 leading-5' : 'leading-6'
}`}
>
{description}
</p>
</div>
{actions.length > 0 ? (
<div className="flex flex-wrap gap-2 lg:justify-end">
{actions.map((action) => (
<Link
key={`${action.href}-${action.label}`}
href={action.href}
className="rounded-lg border border-gray-300 bg-white 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:bg-gray-900 dark:text-gray-200 dark:hover:text-primary-300"
>
{action.label}
</Link>
))}
</div>
) : null}
{actions.length > 0 ? (
<div className="flex flex-wrap gap-2 lg:justify-end">
{actions.map((action) => (
<Link
key={`${action.href}-${action.label}`}
href={action.href}
className="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:hover:text-primary-300"
>
{action.label}
</Link>
))}
</div>
) : null}
</div>
</section>
)

View File

@@ -1,4 +1,5 @@
import { useCallback, useId } from 'react'
import { useCallback, useId, type ReactNode } from 'react'
import { formatInteger } from '@/utils/format'
export interface SectionTab<T extends string> {
id: T
@@ -28,6 +29,33 @@ export function sectionTabPanelProps<T extends string>(
}
}
interface TabPanelProps<T extends string> {
idPrefix: string
tabId: T
activeTab: T
children: ReactNode
className?: string
}
/** Renders tab content only while active to keep pages short and avoid hidden-panel scroll. */
export function TabPanel<T extends string>({
idPrefix,
tabId,
activeTab,
children,
className = '',
}: TabPanelProps<T>) {
if (activeTab !== tabId) {
return null
}
return (
<div {...sectionTabPanelProps(idPrefix, tabId, activeTab)} className={className}>
{children}
</div>
)
}
export default function SectionTabs<T extends string>({
tabs,
activeTab,
@@ -78,7 +106,7 @@ export default function SectionTabs<T extends string>({
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}`}
className={`sticky top-0 z-20 border-b border-gray-200 bg-white/95 py-2 backdrop-blur dark:border-gray-800 dark:bg-gray-950/95 ${className}`}
>
<div
role="tablist"
@@ -88,6 +116,10 @@ export default function SectionTabs<T extends string>({
>
{tabs.map((tab) => {
const isActive = activeTab === tab.id
const accessibleLabel =
typeof tab.count === 'number'
? `${tab.label} (${formatInteger(tab.count)})`
: tab.label
return (
<button
@@ -97,6 +129,7 @@ export default function SectionTabs<T extends string>({
role="tab"
aria-selected={isActive}
aria-controls={`${tabListPrefix}-panel-${tab.id}`}
aria-label={accessibleLabel}
tabIndex={isActive ? 0 : -1}
onClick={() => onChange(tab.id)}
className={
@@ -107,8 +140,11 @@ export default function SectionTabs<T extends string>({
>
{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
aria-hidden="true"
className={isActive ? 'ml-2 text-primary-100' : 'ml-2 text-gray-500 dark:text-gray-400'}
>
{formatInteger(tab.count)}
</span>
) : null}
</button>

View File

@@ -1,7 +1,7 @@
import { Card } from '@/libs/frontend-ui-primitives'
import { useUiMode } from './UiModeContext'
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
import type { MissionControlSubsystemStatus } from '@/services/api/missionControl'
import { formatRelativeAge } from '@/utils/format'
function subsystemLabel(key: string): string {
const labels: Record<string, string> = {
@@ -107,7 +107,13 @@ export default function SubsystemPosturePanel({
</span>
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{subsystem.updated_at ? `Updated ${formatRelativeAge(subsystem.updated_at)}` : 'Update time unavailable'}
{subsystem.updated_at ? (
<>
Updated <ClientRelativeTime value={subsystem.updated_at} suffix=" ago" />
</>
) : (
'Update time unavailable'
)}
</div>
<div className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{compactMeta(subsystem)}