feat(blocks): add reusable theme toggle button

Expose a shared appearance control that resolves system color preference, reacts to preference changes, and toggles between light and dark modes. Disable interaction until client mounting to keep the initial rendered state stable.
This commit is contained in:
Maofeng
2026-08-08 21:13:54 +08:00
parent c64864b3b5
commit 5fced3817c
2 changed files with 58 additions and 0 deletions
@@ -1,5 +1,6 @@
export { appearanceDialogHandle, AppearanceTrigger } from "./dialog"
export { AppearanceController } from "./controller"
export { ThemeToggleButton } from "./theme-toggle-button"
export {
UiStateProvider,
type UiStateProviderProps,
@@ -0,0 +1,57 @@
import React from "react"
import { Button } from "@workspace/ui/components/button"
import { useUiState } from "./ui-state"
import { Icon, Moon02Icon, Sun03Icon } from "@workspace/icons"
import { cn } from "@workspace/ui/lib/utils"
type ThemeToggleButtonProps = Omit<
React.ComponentProps<typeof Button>,
"children" | "onClick"
>
export function ThemeToggleButton({
className,
...props
}: ThemeToggleButtonProps) {
const [themeMode, setThemeMode] = useUiState("theme-mode")
const [systemTheme, setSystemTheme] = React.useState<"light" | "dark">(
"light"
)
const [mounted, setMounted] = React.useState(false)
const resolvedTheme = themeMode === "system" ? systemTheme : themeMode
React.useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const updateSystemTheme = () => {
setSystemTheme(mediaQuery.matches ? "dark" : "light")
}
updateSystemTheme()
mediaQuery.addEventListener("change", updateSystemTheme)
setMounted(true)
return () => mediaQuery.removeEventListener("change", updateSystemTheme)
}, [])
return (
<Button
variant="ghost"
size="icon-lg"
disabled={!mounted}
aria-label={
resolvedTheme === "dark" ? "切换为浅色模式" : "切换为深色模式"
}
className={cn(
"rounded-lg [&_svg]:opacity-45 [&_svg]:transition-opacity hover:[&_svg]:opacity-100",
className
)}
{...props}
onClick={() => setThemeMode(resolvedTheme === "dark" ? "light" : "dark")}
>
<Icon
strokeWidth={2.5}
data={resolvedTheme === "dark" ? Sun03Icon : Moon02Icon}
/>
</Button>
)
}