From 5fced3817c98eb58ac2edf36656cdd3bac2c1fa0 Mon Sep 17 00:00:00 2001 From: Maofeng Date: Sat, 8 Aug 2026 21:13:54 +0800 Subject: [PATCH] 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. --- .../blocks/src/blocks/appearance/index.ts | 1 + .../blocks/appearance/theme-toggle-button.tsx | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/blocks/src/blocks/appearance/theme-toggle-button.tsx diff --git a/packages/blocks/src/blocks/appearance/index.ts b/packages/blocks/src/blocks/appearance/index.ts index 3331a14..bdf0ae7 100644 --- a/packages/blocks/src/blocks/appearance/index.ts +++ b/packages/blocks/src/blocks/appearance/index.ts @@ -1,5 +1,6 @@ export { appearanceDialogHandle, AppearanceTrigger } from "./dialog" export { AppearanceController } from "./controller" +export { ThemeToggleButton } from "./theme-toggle-button" export { UiStateProvider, type UiStateProviderProps, diff --git a/packages/blocks/src/blocks/appearance/theme-toggle-button.tsx b/packages/blocks/src/blocks/appearance/theme-toggle-button.tsx new file mode 100644 index 0000000..876b196 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/theme-toggle-button.tsx @@ -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, + "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 ( + + ) +}