diff --git a/src/app/components/BetaBadge.tsx b/src/app/components/BetaBadge.tsx new file mode 100644 index 0000000..e7f88e6 --- /dev/null +++ b/src/app/components/BetaBadge.tsx @@ -0,0 +1,16 @@ +/** + * Static "BETA" badge shown next to the title. + * + * We deliberately do NOT show a version number here. This is the WebUI client; + * it cannot reliably know which EvoScientist backend version the user actually + * has installed, so showing the latest PyPI release would falsely imply they're + * running it. "BETA" honestly signals an early-stage product without making a + * version claim — and needs no network request. + */ +export function BetaBadge() { + return ( + + BETA + + ); +} diff --git a/src/app/components/ThemeToggle.tsx b/src/app/components/ThemeToggle.tsx new file mode 100644 index 0000000..1844e54 --- /dev/null +++ b/src/app/components/ThemeToggle.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Moon, Sun } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useTheme } from "@/providers/ThemeProvider"; + +/** + * Top-bar light/dark toggle. The theme defaults to "system"; clicking flips + * between explicit light and dark based on what is currently showing. + * + * Until mounted, both server and first client render show the Sun icon so they + * match (no hydration warning); the resolved icon appears right after mount. + */ +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + const isDark = mounted && resolvedTheme === "dark"; + + return ( + + ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 2727188..a9d2334 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -135,56 +135,58 @@ --chart-5: 27 87% 67%; } - @media (prefers-color-scheme: dark) { - :root { - /* App-specific color variables */ - --color-primary: #1c3c3c; - --color-user-message: #065a8a; - --color-user-message-bg: #2d2d2d; - --color-avatar-bg: #1c3c3c; - --color-secondary: #bbc4c4; - --color-success: #34d399; - --color-warning: #fbbf24; - --color-error: #f87171; - --color-background: #202020; - --color-subagent-hover: #1e3f3f; - --color-surface: #2a2a2a; - --color-border: #404040; - --color-border-light: #353535; - --color-text-primary: #f3f4f6; - --color-text-secondary: #9ca3af; - --color-text-tertiary: #6b7280; - --color-file-button: #2a2a2a; - --color-file-button-hover: #353535; + /* Dark theme — applied via the `.dark` class on (set by ThemeProvider + and the inline pre-paint script). Tailwind's darkMode: ["class", …] drives + `dark:` utilities off the same class, so CSS variables and utilities switch + together. The default theme follows the system preference. */ + .dark { + /* App-specific color variables */ + --color-primary: #1c3c3c; + --color-user-message: #065a8a; + --color-user-message-bg: #2d2d2d; + --color-avatar-bg: #1c3c3c; + --color-secondary: #bbc4c4; + --color-success: #34d399; + --color-warning: #fbbf24; + --color-error: #f87171; + --color-background: #202020; + --color-subagent-hover: #1e3f3f; + --color-surface: #2a2a2a; + --color-border: #404040; + --color-border-light: #353535; + --color-text-primary: #f3f4f6; + --color-text-secondary: #9ca3af; + --color-text-tertiary: #6b7280; + --color-file-button: #2a2a2a; + --color-file-button-hover: #353535; - /* Tailwind/Radix UI component variables for dark mode */ - --radius: 0.5rem; - --background: 0 0% 13%; - --foreground: 220 13% 95%; - --card: 0 0% 18%; - --card-foreground: 220 13% 95%; - --popover: 0 0% 18%; - --popover-foreground: 220 13% 95%; - --primary: 174 72% 56%; - --primary-foreground: 0 0% 13%; - --secondary: 0 0% 25%; - --secondary-foreground: 220 13% 95%; - --muted: 0 0% 22%; - --muted-foreground: 220 9% 70%; - --accent: 0 0% 22%; - --accent-foreground: 220 13% 95%; - --destructive: 0 63% 71%; - --destructive-foreground: 0 0% 13%; - --border: 0 0% 28%; - --input: 0 0% 28%; - --ring: 174 72% 56%; - --sidebar: 0 0% 18%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } + /* Tailwind/Radix UI component variables for dark mode */ + --radius: 0.5rem; + --background: 0 0% 13%; + --foreground: 220 13% 95%; + --card: 0 0% 18%; + --card-foreground: 220 13% 95%; + --popover: 0 0% 18%; + --popover-foreground: 220 13% 95%; + --primary: 174 72% 56%; + --primary-foreground: 0 0% 13%; + --secondary: 0 0% 25%; + --secondary-foreground: 220 13% 95%; + --muted: 0 0% 22%; + --muted-foreground: 220 9% 70%; + --accent: 0 0% 22%; + --accent-foreground: 220 13% 95%; + --destructive: 0 63% 71%; + --destructive-foreground: 0 0% 13%; + --border: 0 0% 28%; + --input: 0 0% 28%; + --ring: 174 72% 56%; + --sidebar: 0 0% 18%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e2ccd98..7f88d0c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,8 @@ import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import { NuqsAdapter } from "nuqs/adapters/next/app"; -import { Toaster } from "sonner"; +import { ThemeProvider, ThemedToaster } from "@/providers/ThemeProvider"; +import { THEME_STORAGE_KEY } from "@/lib/theme"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); @@ -13,10 +14,20 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#f9f9f9", - colorScheme: "light", + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#f9f9f9" }, + { media: "(prefers-color-scheme: dark)", color: "#212121" }, + ], + colorScheme: "light dark", }; +// Runs before paint so the right theme class is on immediately — no flash +// of the wrong theme. Mirrors ThemeProvider's resolution (default: follow the +// system); ThemeProvider takes over once React mounts. Kept inline + minimal. +const themeScript = `(function(){try{var k=${JSON.stringify( + THEME_STORAGE_KEY +)};var t=localStorage.getItem(k)||"system";var d=t==="dark"||(t!=="light"&&window.matchMedia("(prefers-color-scheme: dark)").matches);var e=document.documentElement;e.classList.toggle("dark",d);e.style.colorScheme=d?"dark":"light";}catch(e){}})();`; + export default function RootLayout({ children, }: { @@ -31,8 +42,13 @@ export default function RootLayout({ className={inter.className} suppressHydrationWarning > - {children} - +