From 8173f6de453226d8d05f44f8b0b1dc554ba09e33 Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:34:47 +0000 Subject: [PATCH 1/3] explorer: restore light mode theme support Re-adds ThemeProvider, ThemeToggle, light-dark CSS values, favicon media queries, and scheme-light-dark class that were removed in PR #653. Co-Authored-By: o-az <23618431+o-az@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019da9ae-3e00-7771-a7ad-68ceefbf3187 --- apps/explorer/.env.example | 3 + apps/explorer/src/comps/Header.tsx | 2 + apps/explorer/src/comps/ThemeToggle.tsx | 22 +++++++ apps/explorer/src/lib/theme.tsx | 88 +++++++++++++++++++++++++ apps/explorer/src/routes/__root.tsx | 37 ++++++++++- apps/explorer/src/routes/styles.css | 38 ++++++----- 6 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 apps/explorer/src/comps/ThemeToggle.tsx create mode 100644 apps/explorer/src/lib/theme.tsx diff --git a/apps/explorer/.env.example b/apps/explorer/.env.example index 1ab3e66e2..d1195eb5a 100644 --- a/apps/explorer/.env.example +++ b/apps/explorer/.env.example @@ -30,6 +30,9 @@ VITE_CONTRACT_VERIFICATION_API_BASE_URL="https://contracts.tempo.xyz" # show react query + react router devtools # VITE_ENABLE_DEVTOOLS=true +# enable light/dark mode keyboard toggle +# VITE_ENABLE_COLOR_SCHEME_TOGGLE=false + # (dev/preview) comma-separated list of allowed hosts to use in vite config. See # https://vite.dev/config/server-options # https://vite.dev/config/preview-options#preview-allowedhosts diff --git a/apps/explorer/src/comps/Header.tsx b/apps/explorer/src/comps/Header.tsx index 5b5dd9742..a318691bd 100644 --- a/apps/explorer/src/comps/Header.tsx +++ b/apps/explorer/src/comps/Header.tsx @@ -10,6 +10,7 @@ import { useAnimatedBlockNumber, useLiveBlockNumber } from '#lib/block-number' import { cx } from '#lib/css' import { getTempoEnv, isTestnet } from '#lib/env' import SquareSquare from '~icons/lucide/square-square' +import { ThemeToggle } from '#comps/ThemeToggle' export function Header(): React.JSX.Element { const tempoEnv = getTempoEnv() @@ -32,6 +33,7 @@ export function Header(): React.JSX.Element {
+
diff --git a/apps/explorer/src/comps/ThemeToggle.tsx b/apps/explorer/src/comps/ThemeToggle.tsx new file mode 100644 index 000000000..6c2e1d2c9 --- /dev/null +++ b/apps/explorer/src/comps/ThemeToggle.tsx @@ -0,0 +1,22 @@ +import type * as React from 'react' +import { useTheme } from '#lib/theme' +import Sun from '~icons/lucide/sun' +import Moon from '~icons/lucide/moon' + +export function ThemeToggle(): React.JSX.Element { + const { theme, setTheme } = useTheme() + const nextTheme = theme === 'dark' ? 'light' : 'dark' + const Icon = theme === 'dark' ? Moon : Sun + + return ( + + ) +} diff --git a/apps/explorer/src/lib/theme.tsx b/apps/explorer/src/lib/theme.tsx new file mode 100644 index 000000000..da757e8a5 --- /dev/null +++ b/apps/explorer/src/lib/theme.tsx @@ -0,0 +1,88 @@ +import * as React from 'react' + +export type Theme = 'light' | 'dark' + +interface ThemeContextValue { + theme: Theme + setTheme: (theme: Theme) => void +} + +const ThemeContext = React.createContext(null) + +export function ThemeProvider(props: { children: React.ReactNode }) { + const [theme, setThemeState] = React.useState('light') + const hasManualPreference = React.useRef(false) + const setTheme = React.useCallback((nextTheme: Theme) => { + hasManualPreference.current = true + setThemeState(nextTheme) + }, []) + + React.useEffect(() => { + if (typeof window === 'undefined') return + const readStoredTheme = (): Theme | undefined => { + try { + const stored = window.localStorage.getItem('theme') + if (stored === 'light' || stored === 'dark') { + return stored + } + } catch { + return undefined + } + return undefined + } + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const storedTheme = readStoredTheme() + const applyTheme = (matches: boolean) => { + setThemeState(matches ? 'dark' : 'light') + } + + if (storedTheme) { + hasManualPreference.current = true + setThemeState(storedTheme) + } else { + applyTheme(mediaQuery.matches) + } + const handler = (event: MediaQueryListEvent) => { + if (hasManualPreference.current) return + applyTheme(event.matches) + } + mediaQuery.addEventListener('change', handler) + return () => mediaQuery.removeEventListener('change', handler) + }, []) + + React.useEffect(() => { + if (typeof window === 'undefined') return + if (!hasManualPreference.current) return + try { + window.localStorage.setItem('theme', theme) + } catch { + // Ignore storage errors. + } + }, [theme]) + + React.useEffect(() => { + const root = document.documentElement + root.classList.remove('scheme-light!', 'scheme-dark!') + + if (theme === 'light') { + root.classList.add('scheme-light!') + } else if (theme === 'dark') { + root.classList.add('scheme-dark!') + } + }, [theme]) + + return ( + + {props.children} + + ) +} + +export function useTheme() { + const context = React.useContext(ThemeContext) + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} diff --git a/apps/explorer/src/routes/__root.tsx b/apps/explorer/src/routes/__root.tsx index f03e786f6..b426c9950 100644 --- a/apps/explorer/src/routes/__root.tsx +++ b/apps/explorer/src/routes/__root.tsx @@ -15,6 +15,7 @@ import { AddressHighlightProvider } from '#comps/AddressHighlight' import { BreadcrumbsProvider } from '#comps/Breadcrumbs' import { ErrorBoundary } from '#comps/ErrorBoundary' import { IntroSeenProvider } from '#comps/Intro' +import { ThemeProvider } from '#lib/theme' import { TokenListMembershipProvider } from '#comps/TokenListMembership' import { OG_BASE_URL } from '#lib/og' import { ProgressLine } from '#comps/ProgressLine' @@ -106,27 +107,57 @@ export const Route = createRootRouteWithContext<{ rel: 'stylesheet', href: css, }, + { + rel: 'icon', + type: 'image/svg+xml', + href: '/favicon-light.svg', + media: '(prefers-color-scheme: light)', + }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon-dark.svg', + media: '(prefers-color-scheme: dark)', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32-light.png', + media: '(prefers-color-scheme: dark)', }, { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32-dark.png', + media: '(prefers-color-scheme: light)', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16-light.png', + media: '(prefers-color-scheme: dark)', }, { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16-dark.png', + media: '(prefers-color-scheme: light)', + }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/favicon-light.png', + media: '(prefers-color-scheme: light)', }, { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon-dark.png', + media: '(prefers-color-scheme: dark)', }, ], }), @@ -367,7 +398,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { }) return ( - + @@ -382,7 +413,9 @@ function RootDocument({ children }: { children: React.ReactNode }) { - {children} + + {children} + diff --git a/apps/explorer/src/routes/styles.css b/apps/explorer/src/routes/styles.css index 1ab3f83e6..9bac8e0ed 100644 --- a/apps/explorer/src/routes/styles.css +++ b/apps/explorer/src/routes/styles.css @@ -2,13 +2,15 @@ @import "tw-animate-css"; @import "./fonts.css"; +@custom-variant dark (&:is(.scheme-light-dark *)); + @theme { /* Background Colors */ --color-background-inverse: light-dark(#202020, #fcfcfc); --background-color-inverse: var(--color-background-inverse); - --color-background-primary: light-dark(#191919, #191919); + --color-background-primary: light-dark(#f8f8f8, #111111); --background-color-primary: var(--color-background-primary); - --color-background-surface: light-dark(#1a1a1a, #1a1a1a); + --color-background-surface: light-dark(#ffffff, #1a1a1a); --background-color-surface: var(--color-background-surface); --color-background-alt: light-dark(#fcfcfc, #161616); --background-alt: var(--color-background-alt); @@ -23,11 +25,11 @@ --color-frame: var(--color-frame-background); /* Content/Text Colors */ - --color-content-primary: light-dark(#ffffff, #ffffff); + --color-content-primary: light-dark(#0a0a0a, #f5f5f5); --content-primary: var(--color-content-primary); - --color-content-secondary: light-dark(#6e6e6e, #6e6e6e); + --color-content-secondary: light-dark(#737373, #a0a0a0); --content-secondary: var(--color-content-secondary); - --color-content-tertiary: light-dark(#6e6e6e, #6e6e6e); + --color-content-tertiary: light-dark(#8d8d8d, #6e6e6e); --content-tertiary: var(--color-content-tertiary); --color-content-positive: light-dark(#16a34a, #22c55e); --content-positive: var(--color-content-positive); @@ -55,11 +57,11 @@ --color-focus: var(--color-accent); /* Border Colors */ - --color-border-primary: light-dark(#232323, #232323); + --color-border-primary: light-dark(#f0f0f0, #262626); --border-primary: var(--color-border-primary); - --color-border-secondary: light-dark(#232323, #232323); + --color-border-secondary: light-dark(#f8f8f8, #1a1a1a); --border-secondary: var(--color-border-secondary); - --color-border-tertiary: light-dark(#232323, #232323); + --color-border-tertiary: light-dark(#e5e5e5, #2e2e2e); --border-tertiary: var(--color-border-tertiary); --color-border-negative: light-dark(#dc2626, #ef4444); --border-negative: var(--color-border-negative); @@ -91,17 +93,17 @@ /* Card */ --color-card: light-dark(#f9f9f9, #222222); --color-card-header: light-dark(#f9f9f9, #191919); - --color-card-border: light-dark(#232323, #232323); + --color-card-border: light-dark(#e0e0e0, #2a2a2a); /* Other Colors */ - --color-base-background: light-dark(#191919, #191919); - --color-base-plane: light-dark(#1a1a1a, #1a1a1a); - --color-base-plane-background: light-dark(#1a1a1a, #1a1a1a); + --color-base-background: light-dark(#fcfcfc, #191919); + --color-base-plane: light-dark(#fcfcfc, #222222); + --color-base-plane-background: light-dark(#f9f9f9, #222222); --color-base-plane-interactive: light-dark(#f8f8f8, #1c1c1c); - --color-base-alt: light-dark(#232323, #232323); - --color-base-border: light-dark(#232323, #232323); - --color-base-content: light-dark(#ffffff, #ffffff); - --color-base-content-secondary: light-dark(#6e6e6e, #6e6e6e); + --color-base-alt: light-dark(#e5e5e5, #2a2a2a); + --color-base-border: light-dark(#e0e0e0, #2a2a2a); + --color-base-content: light-dark(#202020, #eeeeee); + --color-base-content-secondary: light-dark(#7b7b7b, #7b7b7b); --color-base-content-positive: light-dark(#30a46c, #30a46c); --color-base-content-negative: light-dark(#e5484d, #e5484d); --color-distinct: light-dark(#e8e8e8, #313131); @@ -116,7 +118,7 @@ html, :host { background-color: var(--color-base-background); color: var(--color-primary); - color-scheme: dark; + color-scheme: light dark; } :root { @@ -125,7 +127,7 @@ html, @layer base { html { - color-scheme: dark; + color-scheme: light dark; } } From 20de20c69d5dc3708e19881194286db5c560ceee Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:46:56 +0000 Subject: [PATCH 2/3] fix: use theme-aware color for footer links in light mode Co-Authored-By: o-az <23618431+o-az@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019da9ae-3e00-7771-a7ad-68ceefbf3187 --- apps/explorer/src/routes/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/src/routes/styles.css b/apps/explorer/src/routes/styles.css index 9bac8e0ed..473644dce 100644 --- a/apps/explorer/src/routes/styles.css +++ b/apps/explorer/src/routes/styles.css @@ -163,7 +163,7 @@ input[type="number"] { } @utility text-ui-meta { - color: #fff; + color: var(--color-content-primary); text-align: center; leading-trim: both; text-edge: cap; From fbe307844053d55f0d1cd50b4ea39a46a967f2f2 Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:53:23 +0000 Subject: [PATCH 3/3] fix: fade background arcs and reduce opacity in light mode Adds gradient mask to soften bottom crop edge and reduces opacity in light mode so the arcs blend naturally with the light background. Co-Authored-By: o-az <23618431+o-az@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019da9ae-3e00-7771-a7ad-68ceefbf3187 --- apps/explorer/src/comps/Sphere.tsx | 2 +- apps/explorer/src/routes/styles.css | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/explorer/src/comps/Sphere.tsx b/apps/explorer/src/comps/Sphere.tsx index fb88df51a..acff060fa 100644 --- a/apps/explorer/src/comps/Sphere.tsx +++ b/apps/explorer/src/comps/Sphere.tsx @@ -23,7 +23,7 @@ export function Sphere(props: Sphere.Props) { }, []) return ( -
+