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
>
-