feat: migrate frontend to Astro SSG with design system
- Replace monolithic React SPA (App.tsx) with 7 Astro pages (+ routing) - Implement full design system from design/ (OKLCH tokens, Tailwind 4 @theme) - Add 12 Astro components (Button, Badge, Card, Sidebar, Header, states) - Add 11 React islands (AuthGuard, MessageFeed, VoiceControls, AudioVisualizer, etc.) - Add 3 Zustand stores (UI, voice, message state) - Add CSS architecture: dark/light themes, glassmorphism, fluid typography, animations - Add 404, login, settings, recordings pages - Remove 40+ legacy React SPA files - Add Particles background and MascotChat deferred islands - Wire WebSocket bridge for real-time messages and voice events Build: 7 pages in ~900ms Typecheck: clean (0 errors) Lint: clean (0 errors)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// ─── SettingsForm.tsx — Appearance settings island ─────────────────────────
|
||||
// Reads/writes theme preference (dark/light/system) to localStorage key
|
||||
// "bete-dashboard-theme", applies data-theme attribute and .dark class.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
const STORAGE_KEY = "bete-dashboard-theme";
|
||||
|
||||
const THEME_OPTIONS: { value: Theme; label: string }[] = [
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "system", label: "System" },
|
||||
];
|
||||
|
||||
function getStoredTheme(): Theme {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === "dark" || stored === "light" || stored === "system") {
|
||||
return stored;
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
return "dark";
|
||||
}
|
||||
|
||||
function resolveTheme(theme: Theme): "dark" | "light" {
|
||||
if (theme === "system") {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme): void {
|
||||
const root = document.documentElement;
|
||||
const actual = resolveTheme(theme);
|
||||
root.setAttribute("data-theme", actual);
|
||||
root.classList.toggle("dark", actual === "dark");
|
||||
}
|
||||
|
||||
function storeTheme(theme: Theme): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
} catch {
|
||||
// quota exceeded or unavailable
|
||||
}
|
||||
}
|
||||
|
||||
export default function SettingsForm() {
|
||||
const [theme, setTheme] = useState<Theme>(getStoredTheme);
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
storeTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
const handleThemeChange = useCallback((value: Theme) => {
|
||||
setTheme(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-semibold text-foreground">Appearance</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Choose your preferred color scheme for the dashboard.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`btn ${
|
||||
theme === option.value ? "btn--primary" : "btn--outline"
|
||||
} btn--sm`}
|
||||
onClick={() => handleThemeChange(option.value)}
|
||||
aria-pressed={theme === option.value}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user