Fix dark/light theme bug and introduce automatic option

This commit is contained in:
Alula 2025-11-24 02:04:54 +01:00
parent 7205c16735
commit 633b1fd3a5
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
3 changed files with 96 additions and 11 deletions

View File

@ -1,6 +1,23 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type Theme = "dark" | "light"; type Theme = "dark" | "light" | "automatic";
const DiagonalSplitPreview = () => {
return (
<svg
width="64"
height="40"
viewBox="0 0 64 40"
className="w-full h-full"
preserveAspectRatio="none"
>
{/* Upper triangle (light) */}
<polygon points="0,0 64,0 64,40" fill="#ebebeb" />
{/* Lower triangle (dark) */}
<polygon points="0,0 0,40 64,40" fill="#2d2d2d" />
</svg>
);
};
const ThemeOption = ({ const ThemeOption = ({
value, value,
@ -12,7 +29,7 @@ const ThemeOption = ({
}: { }: {
value: Theme; value: Theme;
label: string; label: string;
previewColor: string; previewColor: string | "diagonalSplit";
theme: Theme; theme: Theme;
setTheme: (theme: Theme) => void; setTheme: (theme: Theme) => void;
isLast?: boolean; isLast?: boolean;
@ -40,10 +57,16 @@ const ThemeOption = ({
> >
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
{/* Preview Box */} {/* Preview Box */}
<div className="w-16 h-10 border border-switch-line-sep overflow-hidden">
{previewColor === "diagonalSplit" ? (
<DiagonalSplitPreview />
) : (
<div <div
className="w-16 h-10 border border-switch-line-sep shadow-sm" className="w-full h-full"
style={{ backgroundColor: previewColor }} style={{ backgroundColor: previewColor }}
/> />
)}
</div>
{/* Label */} {/* Label */}
<span className="text-xl font-normal text-switch-text">{label}</span> <span className="text-xl font-normal text-switch-text">{label}</span>
@ -68,19 +91,66 @@ const ThemeOption = ({
); );
}; };
function getSystemTheme(): "dark" | "light" {
if (typeof window === "undefined") return "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function getEffectiveTheme(theme: Theme): "dark" | "light" {
if (theme === "automatic") {
return getSystemTheme();
}
return theme;
}
export function ThemeSelector() { export function ThemeSelector() {
const [theme, setTheme] = useState<Theme>(() => { const [theme, setTheme] = useState<Theme>(() => {
const saved = localStorage.getItem("theme") as Theme | null; const saved = localStorage.getItem("theme") as Theme | null;
return saved || "dark"; // Default to "automatic" if no saved preference
return saved || "automatic";
}); });
useEffect(() => { useEffect(() => {
document.documentElement.setAttribute("data-theme", theme); const effectiveTheme = getEffectiveTheme(theme);
document.documentElement.setAttribute("data-theme", effectiveTheme);
localStorage.setItem("theme", theme); localStorage.setItem("theme", theme);
}, [theme]); }, [theme]);
// Listen to system theme changes when "automatic" is selected
useEffect(() => {
if (theme !== "automatic") return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
if (theme === "automatic") {
const newSystemTheme = getSystemTheme();
document.documentElement.setAttribute("data-theme", newSystemTheme);
}
};
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}
// Fallback for older browsers
else if (mediaQuery.addListener) {
mediaQuery.addListener(handleChange);
return () => mediaQuery.removeListener(handleChange);
}
}, [theme]);
return ( return (
<div className="space-y-0" role="radiogroup" aria-label="Theme Selection"> <div className="space-y-0" role="radiogroup" aria-label="Theme Selection">
<ThemeOption
value="automatic"
label="Automatic"
previewColor="diagonalSplit"
theme={theme}
setTheme={setTheme}
/>
<ThemeOption <ThemeOption
value="light" value="light"
label="Basic White" label="Basic White"

View File

@ -80,9 +80,6 @@
min-height: 100vh; min-height: 100vh;
background-color: var(--color-switch-selected-bg); background-color: var(--color-switch-selected-bg);
color: var(--switch-text); color: var(--switch-text);
transition:
background-color 0.3s,
color 0.3s;
} }
/* Custom Scrollbar */ /* Custom Scrollbar */

View File

@ -3,6 +3,24 @@ import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import App from "./App.tsx"; import App from "./App.tsx";
// Set initial theme before React renders to avoid flash
function initializeTheme() {
const saved = localStorage.getItem("theme");
let effectiveTheme: "dark" | "light";
if (saved === "light" || saved === "dark") {
effectiveTheme = saved;
} else {
// Default to automatic (system preference)
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
effectiveTheme = prefersDark ? "dark" : "light";
}
document.documentElement.setAttribute("data-theme", effectiveTheme);
}
initializeTheme();
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<App /> <App />