mercury-web/src/ds/ThemeToggle.tsx

87 lines
3 KiB
TypeScript

"use client";
import * as React from "react";
import {
SegmentedControlRoot,
SegmentedControlItem,
SegmentedControlItemHiddenInput,
SegmentedControlIndicator,
} from "@seed-design/react";
/** "system" follows the OS via prefers-color-scheme; "light"/"dark" pin it. */
export type MercuryTheme = "system" | "light" | "dark";
const STORAGE_KEY = "mercury.theme";
/** Mirrors the inline script in app/layout.tsx (kept in sync manually — the
* script must stay dependency-free/pre-hydration, so it can't import this). */
function toColorMode(theme: MercuryTheme): "system" | "light-only" | "dark-only" {
if (theme === "light") return "light-only";
if (theme === "dark") return "dark-only";
return "system";
}
export function getStoredTheme(): MercuryTheme {
try {
const v = localStorage.getItem(STORAGE_KEY);
if (v === "light" || v === "dark" || v === "system") return v;
} catch {
// localStorage unavailable (private mode, SSR) — fall through to default.
}
return "system";
}
function persistTheme(theme: MercuryTheme): void {
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// Best-effort; the toggle still works for the current page load.
}
}
function applyTheme(theme: MercuryTheme): void {
if (typeof document === "undefined") return;
document.documentElement.setAttribute("data-seed-color-mode", toColorMode(theme));
}
export interface ThemeToggleProps {
labels?: { system: string; light: string; dark: string };
}
const DEFAULT_LABELS = { system: "Систем", light: "Гэрэл", dark: "Харанхуй" };
/** 3-way Систем / Гэрэл / Харанхуй display-mode control. Updates
* localStorage and the live `data-seed-color-mode` attribute on `<html>`
* immediately, so every `--seed-color-*` token flips without a reload. */
export function ThemeToggle({ labels = DEFAULT_LABELS }: ThemeToggleProps) {
// Lazy initializer runs on the client at mount (same pattern as
// HideAmountsToggle/isHidden) so it picks up the real preference
// immediately — layout.tsx's inline script has already applied it to the
// DOM before this ever mounts, so both agree from the first paint.
const [theme, setTheme] = React.useState<MercuryTheme>(() => getStoredTheme());
function handleChange(next: string) {
const nextTheme = next as MercuryTheme;
setTheme(nextTheme);
persistTheme(nextTheme);
applyTheme(nextTheme);
}
return (
<SegmentedControlRoot value={theme} onValueChange={handleChange} aria-label="Дэлгэцийн горим">
<SegmentedControlIndicator />
<SegmentedControlItem value="system">
<SegmentedControlItemHiddenInput />
{labels.system}
</SegmentedControlItem>
<SegmentedControlItem value="light">
<SegmentedControlItemHiddenInput />
{labels.light}
</SegmentedControlItem>
<SegmentedControlItem value="dark">
<SegmentedControlItemHiddenInput />
{labels.dark}
</SegmentedControlItem>
</SegmentedControlRoot>
);
}