mirror of
https://github.com/alula/hbpatcher.git
synced 2025-11-24 10:34:55 +00:00
Fix dark/light theme bug and introduce automatic option
This commit is contained in:
parent
7205c16735
commit
633b1fd3a5
@ -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"
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
18
src/main.tsx
18
src/main.tsx
@ -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 />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user