diff --git a/apps/explorer/.github/screenshots/light-mode/light-address.webp b/apps/explorer/.github/screenshots/light-mode/light-address.webp new file mode 100644 index 000000000..65281bde4 Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-address.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-block-detail.webp b/apps/explorer/.github/screenshots/light-mode/light-block-detail.webp new file mode 100644 index 000000000..45ed9305c Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-block-detail.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-blocks.webp b/apps/explorer/.github/screenshots/light-mode/light-blocks.webp new file mode 100644 index 000000000..9ccd05242 Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-blocks.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-landing.webp b/apps/explorer/.github/screenshots/light-mode/light-landing.webp new file mode 100644 index 000000000..80756afd5 Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-landing.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-receipt.webp b/apps/explorer/.github/screenshots/light-mode/light-receipt.webp new file mode 100644 index 000000000..32b5ed362 Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-receipt.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-token.webp b/apps/explorer/.github/screenshots/light-mode/light-token.webp new file mode 100644 index 000000000..9e46fad88 Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-token.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-tokens.webp b/apps/explorer/.github/screenshots/light-mode/light-tokens.webp new file mode 100644 index 000000000..248341fed Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-tokens.webp differ diff --git a/apps/explorer/.github/screenshots/light-mode/light-tx.webp b/apps/explorer/.github/screenshots/light-mode/light-tx.webp new file mode 100644 index 000000000..420f6918f Binary files /dev/null and b/apps/explorer/.github/screenshots/light-mode/light-tx.webp differ diff --git a/apps/explorer/public/fonts/hb-set/HBSetv0.96-Light.woff2 b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Light.woff2 new file mode 100644 index 000000000..d8c203a92 Binary files /dev/null and b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Light.woff2 differ diff --git a/apps/explorer/public/fonts/hb-set/HBSetv0.96-LightItalic.woff2 b/apps/explorer/public/fonts/hb-set/HBSetv0.96-LightItalic.woff2 new file mode 100644 index 000000000..8b2bca509 Binary files /dev/null and b/apps/explorer/public/fonts/hb-set/HBSetv0.96-LightItalic.woff2 differ diff --git a/apps/explorer/public/fonts/hb-set/HBSetv0.96-Medium.woff2 b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Medium.woff2 new file mode 100644 index 000000000..cb9578e69 Binary files /dev/null and b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Medium.woff2 differ diff --git a/apps/explorer/public/fonts/hb-set/HBSetv0.96-MediumItalic.woff2 b/apps/explorer/public/fonts/hb-set/HBSetv0.96-MediumItalic.woff2 new file mode 100644 index 000000000..700e1baca Binary files /dev/null and b/apps/explorer/public/fonts/hb-set/HBSetv0.96-MediumItalic.woff2 differ diff --git a/apps/explorer/public/fonts/hb-set/HBSetv0.96-Regular2.woff2 b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Regular2.woff2 new file mode 100644 index 000000000..f1641b51f Binary files /dev/null and b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Regular2.woff2 differ diff --git a/apps/explorer/public/fonts/hb-set/HBSetv0.96-Regular2Italic.woff2 b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Regular2Italic.woff2 new file mode 100644 index 000000000..5447bb319 Binary files /dev/null and b/apps/explorer/public/fonts/hb-set/HBSetv0.96-Regular2Italic.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Black.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Black.woff2 new file mode 100644 index 000000000..5e1bb18e8 Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Black.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Bold.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Bold.woff2 new file mode 100644 index 000000000..5f64bba7f Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Bold.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Book.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Book.woff2 new file mode 100644 index 000000000..80f2bb713 Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Book.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Demi.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Demi.woff2 new file mode 100644 index 000000000..1f07f6f2e Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Demi.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Heavy.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Heavy.woff2 new file mode 100644 index 000000000..7e26171e4 Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Heavy.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Light.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Light.woff2 new file mode 100644 index 000000000..2af97cccb Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Light.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Regular.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Regular.woff2 new file mode 100644 index 000000000..f21a34adb Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Regular.woff2 differ diff --git a/apps/explorer/public/fonts/pilat/Pilat-Thin.woff2 b/apps/explorer/public/fonts/pilat/Pilat-Thin.woff2 new file mode 100644 index 000000000..4424ce2ae Binary files /dev/null and b/apps/explorer/public/fonts/pilat/Pilat-Thin.woff2 differ diff --git a/apps/explorer/public/landing-orb-dark.svg b/apps/explorer/public/landing-orb-dark.svg new file mode 100644 index 000000000..cf6f4962d --- /dev/null +++ b/apps/explorer/public/landing-orb-dark.svg @@ -0,0 +1,132 @@ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
diff --git a/apps/explorer/public/landing-orb-light.svg b/apps/explorer/public/landing-orb-light.svg new file mode 100644 index 000000000..c73138b1f --- /dev/null +++ b/apps/explorer/public/landing-orb-light.svg @@ -0,0 +1,92 @@ + + + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + +
diff --git a/apps/explorer/src/comps/BlockCard.tsx b/apps/explorer/src/comps/BlockCard.tsx index 8f10c80ec..67ad1c225 100644 --- a/apps/explorer/src/comps/BlockCard.tsx +++ b/apps/explorer/src/comps/BlockCard.tsx @@ -127,9 +127,8 @@ export function BlockCard(props: BlockCard.Props) { )} - {/* the 15px font size needs to match the block number wrapper font size to make sure they align */} - {/* 22 chars/line * (1ch + 1px tracking) */} -
+ {/* 33 chars/line fits the 66-char hash in exactly 2 lines */} +
{hash}
@@ -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 -
+
- {str.split('').map((char, index) => ( - = zerosEnd ? 'text-primary' : undefined} - > - {char} - - ))} + {str.split('').map((char, index) => { + const isNonZero = index >= zerosEnd + return ( + + ) + })}
) @@ -285,6 +287,56 @@ export namespace BlockCard { } } + export function LottoDigit(props: LottoDigit.Props) { + const { char, animate, delayMs = 0, className } = props + const ref = React.useRef(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 | 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 ( + + {animate ? '0' : char} + + ) + } + + 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 ( diff --git a/apps/explorer/src/comps/Breadcrumbs.tsx b/apps/explorer/src/comps/Breadcrumbs.tsx index 2eb6f1cc1..c6e312aa3 100644 --- a/apps/explorer/src/comps/Breadcrumbs.tsx +++ b/apps/explorer/src/comps/Breadcrumbs.tsx @@ -10,8 +10,17 @@ 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 { @@ -19,51 +28,70 @@ function truncateHash(hash: string, prefixLen = 6, suffixLen = 4): string { return `${hash.slice(0, prefixLen)}…${hash.slice(-suffixLen)}` } -function getLabelForPath(pathname: string): { - label: string - type: Crumb['type'] -} { +function getCrumbForPath(pathname: string): Omit { 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 { @@ -83,28 +111,22 @@ export function BreadcrumbsProvider(props: { children: React.ReactNode }) { const [crumbs, setCrumbs] = React.useState([]) const [slotEl, setSlotEl] = React.useState(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(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 === '/') { @@ -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(() => { - // 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]) @@ -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 @@ -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 ( {isLast ? ( - - {crumb.label} + + {crumb.group} ) : ( - {crumb.label} + {crumb.group} )} + {crumb.id && ( + <> + + {isLast ? ( + + {crumb.id} + + ) : ( + + {crumb.id} + + )} + + )} ) })} @@ -282,6 +330,5 @@ export function BreadcrumbsPortal() { return createPortal(, slotEl) } - // No slot registered - BreadcrumbsSlot handles the loading fallback return null } diff --git a/apps/explorer/src/comps/DataGrid.tsx b/apps/explorer/src/comps/DataGrid.tsx index c8383b2ad..2e705a3d2 100644 --- a/apps/explorer/src/comps/DataGrid.tsx +++ b/apps/explorer/src/comps/DataGrid.tsx @@ -100,17 +100,17 @@ export function DataGrid(props: DataGrid.Props) {
- + {label} {hasSort && ( diff --git a/apps/explorer/src/comps/ErrorBoundary.tsx b/apps/explorer/src/comps/ErrorBoundary.tsx index 193fd830e..0374af584 100644 --- a/apps/explorer/src/comps/ErrorBoundary.tsx +++ b/apps/explorer/src/comps/ErrorBoundary.tsx @@ -46,7 +46,7 @@ export class ErrorBoundary extends React.Component<
-

+

Something went wrong

diff --git a/apps/explorer/src/comps/ExploreInput.tsx b/apps/explorer/src/comps/ExploreInput.tsx index 0805ca01a..bb6b17ca4 100644 --- a/apps/explorer/src/comps/ExploreInput.tsx +++ b/apps/explorer/src/comps/ExploreInput.tsx @@ -248,13 +248,20 @@ export function ExploreInput(props: ExploreInput.Props) { tabIndex={tabIndex} value={value} className={cx( - 'text-search-input bg-surface border-base-border border pl-[16px] pr-[60px] w-full placeholder:text-tertiary rounded-[10px] focus-visible:border-focus outline-0', - size === 'large' ? 'h-[52px]' : 'h-[42px]', + 'bg-surface border-base-border border w-full placeholder:text-tertiary rounded-[10px] focus-visible:border-focus outline-0', + size === 'compact' + ? 'text-search-input-compact h-[34px] pl-[12px] pr-[44px]' + : 'text-search-input pl-[16px] pr-[60px]', + size === 'large' + ? 'h-[52px]' + : size === 'compact' + ? '' + : 'h-[42px]', className, )} data-1p-ignore name="explore-query" - placeholder="Search by Address / Tx Hash / Block / Token" + placeholder="Search by address, hash, or block..." spellCheck={false} type="text" onKeyDown={(event) => { @@ -312,7 +319,11 @@ export function ExploreInput(props: ExploreInput.Props) {

@@ -427,7 +448,7 @@ export namespace ExploreInput { wrapperRef?: React.RefObject value: string onChange: (value: string) => void - size?: 'large' | 'medium' + size?: 'compact' | 'medium' | 'large' className?: string wide?: boolean tabIndex?: number diff --git a/apps/explorer/src/comps/Footer.tsx b/apps/explorer/src/comps/Footer.tsx index 333cad69d..8fba36bf6 100644 --- a/apps/explorer/src/comps/Footer.tsx +++ b/apps/explorer/src/comps/Footer.tsx @@ -1,25 +1,33 @@ import { Link as RouterLink } from '@tanstack/react-router' +import { ThemeToggle } from '#comps/ThemeToggle' export function Footer() { return ( -