From 86779153e88b6bc2e5f2e76618150d3c1193b529 Mon Sep 17 00:00:00 2001 From: demilade-git Date: Sun, 21 Jun 2026 14:55:37 +0000 Subject: [PATCH] feat(ui): add dark/light mode toggle with system preference detection (#513) - Add CSS custom properties for dark (default) and light themes in globals.css - Create useTheme hook: reads localStorage, falls back to prefers-color-scheme - Create ThemeToggle component (sun/moon icon button) for AppShell and Navbar - Add FOUC-prevention inline script to layout.tsx - Convert all hardcoded colors to CSS vars in: AppShell, Card, Pill, Label, AddressChip, SearchBar, TabSwitcher, TransactionResult, AccountResult - Update landing page (Navbar, HeroSection, page.tsx) to use CSS vars - Fix 3 pre-existing TS errors in UseCasesSection.tsx - Add missing eslint-config-prettier dependency --- packages/ui/package-lock.json | 14 ++++ packages/ui/package.json | 14 ++-- packages/ui/src/app/globals.css | 69 +++++++++++++++++-- packages/ui/src/app/layout.tsx | 12 +++- packages/ui/src/app/page.tsx | 21 +++--- packages/ui/src/components/AccountResult.tsx | 55 ++++++--------- packages/ui/src/components/AddressChip.tsx | 7 +- packages/ui/src/components/AppShell.tsx | 64 ++++++++++------- packages/ui/src/components/Card.tsx | 8 ++- packages/ui/src/components/Label.tsx | 5 +- packages/ui/src/components/Pill.tsx | 31 +++++++-- packages/ui/src/components/SearchBar.tsx | 37 +++++++--- packages/ui/src/components/TabSwitcher.tsx | 26 +++++-- packages/ui/src/components/ThemeToggle.tsx | 24 +++++++ .../ui/src/components/TransactionResult.tsx | 50 ++++++-------- .../ui/src/components/landing/HeroSection.tsx | 42 ++++++++--- packages/ui/src/components/landing/Navbar.tsx | 49 +++++++++---- .../components/landing/UseCasesSection.tsx | 3 +- packages/ui/src/hooks/useTheme.ts | 37 ++++++++++ 19 files changed, 401 insertions(+), 167 deletions(-) create mode 100644 packages/ui/src/components/ThemeToggle.tsx create mode 100644 packages/ui/src/hooks/useTheme.ts diff --git a/packages/ui/package-lock.json b/packages/ui/package-lock.json index 3cd8966..9de5ded 100644 --- a/packages/ui/package-lock.json +++ b/packages/ui/package-lock.json @@ -20,6 +20,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.5.3", + "eslint-config-prettier": "^9.1.0", "tailwindcss": "^4", "typescript": "^5" } @@ -2816,6 +2817,19 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", diff --git a/packages/ui/package.json b/packages/ui/package.json index bb2e5d5..057df64 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,22 +8,22 @@ "start": "next start", "lint": "eslint", "smoke-test": "bash ../../scripts/smoke-test.sh" - }, "dependencies": { + "next": "15.5.3", "react": "19.1.0", - "react-dom": "19.1.0", - "next": "15.5.3" + "react-dom": "19.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.3", - "@eslint/eslintrc": "^3" + "eslint-config-prettier": "^9.1.0", + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/packages/ui/src/app/globals.css b/packages/ui/src/app/globals.css index cd6eef6..d60ad8b 100644 --- a/packages/ui/src/app/globals.css +++ b/packages/ui/src/app/globals.css @@ -1,20 +1,73 @@ @import "tailwindcss"; +/* ── Dark theme (default) ── */ :root { - --background: #080c12; - --foreground: #e8eaf0; + --bg-base: #080c12; + --bg-card: rgba(255,255,255,0.04); + --bg-card-hover: rgba(255,255,255,0.06); + --border-subtle: rgba(255,255,255,0.08); + --border-accent: rgba(56,189,248,0.3); + --text-primary: rgba(255,255,255,0.90); + --text-secondary: rgba(255,255,255,0.50); + --text-muted: rgba(255,255,255,0.25); + --text-mono: rgba(255,255,255,0.40); + --accent-sky: #38bdf8; + --accent-sky-dim: rgba(56,189,248,0.1); + --accent-purple: #a78bfa; + --accent-purple-dim: rgba(167,139,250,0.1); + --pill-success-bg: rgba(34,197,94,0.12); + --pill-success-text: #86efac; + --pill-fail-bg: rgba(239,68,68,0.12); + --pill-fail-text: #fca5a5; + --pill-warning-bg: rgba(245,158,11,0.12); + --pill-warning-text: #fcd34d; + --pill-default-bg: rgba(56,189,248,0.12); + --pill-default-text: #7dd3fc; + --grid-line: rgba(255,255,255,0.025); + --glow-sky: rgba(56,189,248,0.06); +} + +/* ── Light theme ── */ +[data-theme="light"] { + --bg-base: #f0f4f8; + --bg-card: rgba(0,0,0,0.03); + --bg-card-hover: rgba(0,0,0,0.05); + --border-subtle: rgba(0,0,0,0.08); + --border-accent: rgba(2,132,199,0.3); + --text-primary: rgba(0,0,0,0.90); + --text-secondary: rgba(0,0,0,0.55); + --text-muted: rgba(0,0,0,0.30); + --text-mono: rgba(0,0,0,0.45); + --accent-sky: #0284c7; + --accent-sky-dim: rgba(2,132,199,0.08); + --accent-purple: #7c3aed; + --accent-purple-dim: rgba(124,58,237,0.08); + --pill-success-bg: rgba(22,163,74,0.10); + --pill-success-text: #15803d; + --pill-fail-bg: rgba(220,38,38,0.10); + --pill-fail-text: #b91c1c; + --pill-warning-bg: rgba(180,83,9,0.10); + --pill-warning-text: #92400e; + --pill-default-bg: rgba(2,132,199,0.10); + --pill-default-text: #0369a1; + --grid-line: rgba(0,0,0,0.04); + --glow-sky: rgba(2,132,199,0.04); } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); + --color-background: var(--bg-base); + --color-foreground: var(--text-primary); --font-sans: var(--font-sans); --font-mono: var(--font-mono); } +html { + transition: background-color 0.2s ease, color 0.2s ease; +} + body { - background: var(--background); - color: var(--foreground); + background: var(--bg-base); + color: var(--text-primary); } @keyframes animate-in { @@ -25,10 +78,12 @@ body { .animate-in { animation: animate-in 0.25s ease-out forwards; } + @keyframes fadeSlideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } + .animate-fadeSlideUp { animation: fadeSlideUp 0.35s ease forwards; -} \ No newline at end of file +} diff --git a/packages/ui/src/app/layout.tsx b/packages/ui/src/app/layout.tsx index d881047..fc75004 100644 --- a/packages/ui/src/app/layout.tsx +++ b/packages/ui/src/app/layout.tsx @@ -2,7 +2,6 @@ import type { Metadata } from "next"; import { IBM_Plex_Mono, IBM_Plex_Sans } from "next/font/google"; import "./globals.css"; import NetworkStatusBanner from "@/components/NetworkStatusBanner"; -// import NetworkStatusBanner from "@/components/NetworkStatusBanner"; const ibmPlexMono = IBM_Plex_Mono({ variable: "--font-mono", @@ -27,12 +26,19 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + + + {/* Prevent flash of wrong theme on initial load */} +