Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added apps/explorer/public/fonts/pilat/Pilat-Bold.woff2
Binary file not shown.
Binary file added apps/explorer/public/fonts/pilat/Pilat-Book.woff2
Binary file not shown.
Binary file added apps/explorer/public/fonts/pilat/Pilat-Demi.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added apps/explorer/public/fonts/pilat/Pilat-Thin.woff2
Binary file not shown.
132 changes: 132 additions & 0 deletions apps/explorer/public/landing-orb-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions apps/explorer/public/landing-orb-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 65 additions & 13 deletions apps/explorer/src/comps/BlockCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,8 @@ export function BlockCard(props: BlockCard.Props) {
)}
</div>
</div>
{/* the 15px font size needs to match the block number wrapper font size to make sure they align */}
{/* 22 chars/line * (1ch + 1px tracking) */}
<div className="text-[15px] font-mono font-normal leading-[18px] tracking-[1px] text-primary break-all max-w-[calc(22ch+22px)]">
{/* 33 chars/line fits the 66-char hash in exactly 2 lines */}
<div className="text-[13px] font-mono font-normal leading-[16px] tracking-[0.5px] text-primary break-all max-w-[calc(33ch+16px)]">
{hash}
</div>
</button>
Expand Down Expand Up @@ -263,17 +262,20 @@ export namespace BlockCard {
const str = String(value).padStart(15, '0')
const zerosEnd = str.match(/^0*/)?.[0].length ?? 0
return (
// the 15px font size is used to set the same width as the block hash
<div className="text-[15px] max-w-[calc(22ch+22px)] font-mono">
<div className="text-[13px] max-w-[calc(33ch+16px)] font-mono">
<span className="flex justify-between gap-px text-[22px] text-tertiary select-none">
{str.split('').map((char, index) => (
<span
key={`${index}-${char}`}
className={index >= zerosEnd ? 'text-primary' : undefined}
>
{char}
</span>
))}
{str.split('').map((char, index) => {
const isNonZero = index >= zerosEnd
return (
<BlockCard.LottoDigit
key={`${index}-${char}`}
char={char}
animate={isNonZero}
delayMs={isNonZero ? (index - zerosEnd) * 40 : 0}
className={isNonZero ? 'text-primary' : undefined}
/>
)
})}
</span>
</div>
)
Expand All @@ -285,6 +287,56 @@ export namespace BlockCard {
}
}

export function LottoDigit(props: LottoDigit.Props) {
const { char, animate, delayMs = 0, className } = props
const ref = React.useRef<HTMLSpanElement>(null)

React.useEffect(() => {
if (!animate) return
const el = ref.current
if (!el) return

// ~500ms roll: 60ms per scramble tick, then settle on the final char.
const scrambleMs = 500
const tickMs = 60
const ticks = Math.floor(scrambleMs / tickMs)
let tickCount = 0
let intervalId: ReturnType<typeof setInterval> | null = null

const startTimer = setTimeout(() => {
intervalId = setInterval(() => {
if (tickCount >= ticks) {
el.textContent = char
if (intervalId) clearInterval(intervalId)
return
}
el.textContent = String(Math.floor(Math.random() * 10))
tickCount += 1
}, tickMs)
}, delayMs)

return () => {
clearTimeout(startTimer)
if (intervalId) clearInterval(intervalId)
}
}, [animate, char, delayMs])

return (
<span ref={ref} className={className}>
{animate ? '0' : char}
</span>
)
}

export namespace LottoDigit {
export interface Props {
char: string
animate: boolean
delayMs?: number
className?: string | undefined
}
}

export function InfoRow(props: InfoRow.Props) {
const { label, children } = props
return (
Expand Down
139 changes: 93 additions & 46 deletions apps/explorer/src/comps/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,88 @@ const MAX_CRUMBS = 3

export interface Crumb {
path: string
label: string
type: 'home' | 'tx' | 'receipt' | 'address' | 'token' | 'block' | 'other'
group: string
groupType:
| 'home'
| 'transaction'
| 'receipt'
| 'address'
| 'token'
| 'block'
| 'other'
id?: string
idMono?: boolean
}

function truncateHash(hash: string, prefixLen = 6, suffixLen = 4): string {
if (hash.length <= prefixLen + suffixLen + 2) return hash
return `${hash.slice(0, prefixLen)}…${hash.slice(-suffixLen)}`
}

function getLabelForPath(pathname: string): {
label: string
type: Crumb['type']
} {
function getCrumbForPath(pathname: string): Omit<Crumb, 'path'> {
if (pathname === '/') {
return { label: 'Home', type: 'home' }
return { group: 'Home', groupType: 'home' }
}

const txMatch = pathname.match(/^\/tx\/(0x[a-fA-F0-9]+)$/)
if (txMatch) {
return { label: `Tx ${truncateHash(txMatch[1])}`, type: 'tx' }
return {
group: 'Transaction',
groupType: 'transaction',
id: truncateHash(txMatch[1]),
idMono: true,
}
}

const receiptMatch = pathname.match(/^\/receipt\/(0x[a-fA-F0-9]+)$/)
if (receiptMatch) {
return {
label: `Receipt ${truncateHash(receiptMatch[1])}`,
type: 'receipt',
group: 'Receipt',
groupType: 'receipt',
id: truncateHash(receiptMatch[1]),
idMono: true,
}
}

const addressMatch = pathname.match(/^\/address\/(0x[a-fA-F0-9]+)$/)
if (addressMatch) {
return { label: `Addr ${truncateHash(addressMatch[1])}`, type: 'address' }
return {
group: 'Address',
groupType: 'address',
id: truncateHash(addressMatch[1]),
idMono: true,
}
}

const tokenMatch = pathname.match(/^\/token\/(0x[a-fA-F0-9]+)$/)
if (tokenMatch) {
return { label: `Token ${truncateHash(tokenMatch[1])}`, type: 'token' }
return {
group: 'Token',
groupType: 'token',
id: truncateHash(tokenMatch[1]),
idMono: true,
}
}

const blockMatch = pathname.match(/^\/block\/(\d+|latest)$/)
if (blockMatch) {
return { label: `Block ${blockMatch[1]}`, type: 'block' }
return {
group: 'Block',
groupType: 'block',
id: blockMatch[1],
idMono: true,
}
}

if (pathname === '/blocks') {
return { label: 'Blocks', type: 'other' }
return { group: 'Blocks', groupType: 'other' }
}

if (pathname === '/tokens') {
return { label: 'Tokens', type: 'other' }
return { group: 'Tokens', groupType: 'other' }
}

return { label: pathname, type: 'other' }
return { group: pathname, groupType: 'other' }
}

interface BreadcrumbsContextValue {
Expand All @@ -83,28 +111,22 @@ export function BreadcrumbsProvider(props: { children: React.ReactNode }) {
const [crumbs, setCrumbs] = React.useState<Crumb[]>([])
const [slotEl, setSlotEl] = React.useState<HTMLElement | null>(null)

// Track resolved location for committing to history
// Fall back to current location if resolvedLocation is not yet available
const resolvedPathname = useRouterState({
select: (state) =>
state.resolvedLocation?.pathname ?? state.location.pathname,
})

// Track current location for pending/optimistic display
const currentPathname = useRouterState({
select: (state) => state.location.pathname,
})

// Track previous resolved pathname to detect changes
const prevResolvedRef = React.useRef<string | null>(null)

// Commit crumbs when navigation resolves successfully
React.useEffect(() => {
// Skip if pathname hasn't changed
if (prevResolvedRef.current === resolvedPathname) return
prevResolvedRef.current = resolvedPathname

const { label, type } = getLabelForPath(resolvedPathname)
const next = getCrumbForPath(resolvedPathname)

setCrumbs((prev) => {
if (resolvedPathname === '/') {
Expand All @@ -116,32 +138,28 @@ export function BreadcrumbsProvider(props: { children: React.ReactNode }) {
return prev.slice(0, existingIndex + 1)
}

const newCrumb: Crumb = { path: resolvedPathname, label, type }
const newCrumb: Crumb = { path: resolvedPathname, ...next }
return [...prev, newCrumb].slice(-MAX_CRUMBS)
})
}, [resolvedPathname])

// Compute pending crumb for immediate UI feedback during navigation
const pendingCrumb = React.useMemo<Crumb | null>(() => {
// Only show pending crumb if navigating to a different path
if (currentPathname === resolvedPathname || currentPathname === '/') {
return null
}
// Don't show if it's already in crumbs
if (crumbs.some((c) => c.path === currentPathname)) {
return null
}
const { label, type } = getLabelForPath(currentPathname)
return { path: currentPathname, label, type }
const next = getCrumbForPath(currentPathname)
return { path: currentPathname, ...next }
}, [currentPathname, resolvedPathname, crumbs])

const clearCrumbs = React.useCallback(() => {
// Keep only the current page as a single crumb
if (resolvedPathname === '/') {
setCrumbs([])
} else {
const { label, type } = getLabelForPath(resolvedPathname)
setCrumbs([{ path: resolvedPathname, label, type }])
const next = getCrumbForPath(resolvedPathname)
setCrumbs([{ path: resolvedPathname, ...next }])
}
}, [resolvedPathname])

Expand Down Expand Up @@ -174,7 +192,6 @@ export function Breadcrumbs(props: Breadcrumbs.Props) {
state.resolvedLocation?.pathname ?? state.location.pathname,
})

// Combine committed crumbs with pending crumb for display
const displayCrumbs = pendingCrumb ? [...crumbs, pendingCrumb] : crumbs
const hasPendingCrumb = pendingCrumb !== null

Expand Down Expand Up @@ -206,30 +223,61 @@ export function Breadcrumbs(props: Breadcrumbs.Props) {
{displayCrumbs.map((crumb, index) => {
const isLast = index === displayCrumbs.length - 1
const isPending = isLast && hasPendingCrumb
const groupClasses = cx(
'truncate max-w-[120px]',
isLast && !crumb.id
? isPending
? 'font-medium text-secondary animate-pulse'
: 'font-medium text-primary'
: 'font-normal text-secondary',
)
const idClasses = cx(
'truncate max-w-[120px]',
crumb.idMono && 'font-mono tabular-nums',
isPending
? 'font-medium text-secondary animate-pulse'
: 'font-medium text-primary',
)
return (
<React.Fragment key={crumb.path}>
<ChevronRight className="size-3 text-tertiary shrink-0" />
{isLast ? (
<span
className={cx(
'font-medium truncate max-w-[120px]',
isPending
? 'text-secondary animate-pulse'
: 'text-primary',
)}
title={crumb.path}
>
{crumb.label}
<span className={groupClasses} title={crumb.path}>
{crumb.group}
</span>
) : (
<Link
to={crumb.path}
className="text-secondary hover:text-accent press-down truncate max-w-[120px] outline-none focus-visible:text-accent"
className={cx(
groupClasses,
'hover:text-accent press-down outline-none focus-visible:text-accent',
)}
title={crumb.path}
>
{crumb.label}
{crumb.group}
</Link>
)}
{crumb.id && (
<>
<ChevronRight className="size-3 text-tertiary shrink-0" />
{isLast ? (
<span className={idClasses} title={crumb.path}>
{crumb.id}
</span>
) : (
<Link
to={crumb.path}
className={cx(
idClasses,
'hover:text-accent press-down outline-none focus-visible:text-accent',
)}
title={crumb.path}
>
{crumb.id}
</Link>
)}
</>
)}
</React.Fragment>
)
})}
Expand Down Expand Up @@ -282,6 +330,5 @@ export function BreadcrumbsPortal() {
return createPortal(<Breadcrumbs />, slotEl)
}

// No slot registered - BreadcrumbsSlot handles the loading fallback
return null
}
6 changes: 3 additions & 3 deletions apps/explorer/src/comps/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,17 @@ export function DataGrid(props: DataGrid.Props) {
<div
key={key}
className={cx(
'px-[10px] first:pl-[16px] last:pr-[16px] h-9 flex items-center gap-[6px]',
'px-[10px] first:pl-[16px] last:pr-[16px] h-9 flex items-center gap-[6px] min-w-0 overflow-hidden',
'text-[13px] text-tertiary font-normal whitespace-nowrap font-sans',
column.align === 'end' ? 'justify-end' : 'justify-start',
)}
>
<span className="inline-flex items-center gap-[4px]">
<span className="inline-flex items-center gap-[4px] min-w-0 truncate">
{label}
{hasSort && (
<ChevronDownIcon
className={cx(
'size-[12px] text-tertiary',
'size-[12px] text-tertiary shrink-0',
sortDir === 'asc' && 'rotate-180',
)}
/>
Expand Down
2 changes: 1 addition & 1 deletion apps/explorer/src/comps/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class ErrorBoundary extends React.Component<
<Header />
<section className="flex flex-1 flex-col size-full items-center justify-center px-[16px] max-w-[600px] gap-[16px] m-auto">
<div className="flex flex-col items-center gap-[8px]">
<h1 className="text-[24px] lg:text-[40px] font-medium text-base-content">
<h1 className="text-[24px] lg:text-[40px] font-normal text-base-content">
Something went wrong
</h1>
<p className="text-base-content-secondary text-[15px] lg:text-[18px] text-center">
Expand Down
Loading
Loading