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:
@@ -1,5 +1,6 @@
|
|||||||
export { appearanceDialogHandle, AppearanceTrigger } from "./dialog"
|
export { appearanceDialogHandle, AppearanceTrigger } from "./dialog"
|
||||||
export { AppearanceController } from "./controller"
|
export { AppearanceController } from "./controller"
|
||||||
|
export { ThemeToggleButton } from "./theme-toggle-button"
|
||||||
export {
|
export {
|
||||||
UiStateProvider,
|
UiStateProvider,
|
||||||
type UiStateProviderProps,
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user