Tokens and components in styles.css, shared Ui (dialogs, toasts), icons, bottom tab bar on mobile, keyboard/gamepad navigation, library shelf with filters and conversion badges, hero game page, restyled player screens. Games list now returns per-game version counts. Adds docs/UI_DESIGN.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
|
|
export type ThemeChoice = "system" | "dark" | "light";
|
|
const KEY = "pg-theme";
|
|
|
|
function read(): ThemeChoice {
|
|
try {
|
|
const v = localStorage.getItem(KEY);
|
|
return v === "dark" || v === "light" ? v : "system";
|
|
} catch {
|
|
return "system";
|
|
}
|
|
}
|
|
|
|
function apply(choice: ThemeChoice) {
|
|
const root = document.documentElement;
|
|
if (choice === "system") delete root.dataset.theme;
|
|
else root.dataset.theme = choice;
|
|
const dark = choice === "dark" || (choice === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
|
|
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", dark ? "#0e1016" : "#f4f1ec");
|
|
}
|
|
|
|
export function useTheme() {
|
|
const [choice, setChoice] = useState<ThemeChoice>(read);
|
|
useEffect(() => {
|
|
apply(choice);
|
|
try {
|
|
if (choice === "system") localStorage.removeItem(KEY);
|
|
else localStorage.setItem(KEY, choice);
|
|
} catch {
|
|
/* private mode */
|
|
}
|
|
}, [choice]);
|
|
const cycle = useCallback(
|
|
() => setChoice((c) => (c === "system" ? "dark" : c === "dark" ? "light" : "system")),
|
|
[],
|
|
);
|
|
return { choice, setChoice, cycle };
|
|
}
|