refactor(appearance): organize components and hooks

This commit is contained in:
Maofeng
2026-09-29 22:12:40 +08:00
parent 352db2c4c0
commit 642cb71b93
32 changed files with 497 additions and 161 deletions
+20 -1
View File
@@ -26,14 +26,33 @@
"exports": {
".": "./src/index.ts",
"./color-vision": "./src/color-vision.ts",
"./components/appearance-option-list": "./src/components/option-lists/appearance-option-list.tsx",
"./components/brightness-field": "./src/components/fields/brightness-field.tsx",
"./components/color-vision-mode-field": "./src/components/fields/color-vision-mode-field.tsx",
"./components/motion-mode-field": "./src/components/fields/motion-mode-field.tsx",
"./components/text-scale-field": "./src/components/fields/text-scale-field.tsx",
"./components/text-scale-option-list": "./src/components/option-lists/text-scale-option-list.tsx",
"./components/theme-color-picker": "./src/components/theme/theme-color-picker.tsx",
"./components/theme-config-card": "./src/components/theme/theme-config-card.tsx",
"./components/theme-mode-field": "./src/components/fields/theme-mode-field.tsx",
"./components/theme-mode-option-list": "./src/components/option-lists/theme-mode-option-list.tsx",
"./components/theme-toggle-button": "./src/components/theme/theme-toggle-button.tsx",
"./components/fields": "./src/components/fields/index.ts",
"./components/fields/*": "./src/components/fields/*.tsx",
"./components/option-lists": "./src/components/option-lists/index.ts",
"./components/option-lists/*": "./src/components/option-lists/*.tsx",
"./components/theme": "./src/components/theme/index.ts",
"./components/theme/*": "./src/components/theme/*.tsx",
"./components/*": "./src/components/*.tsx",
"./display": "./src/display.ts",
"./effects": "./src/effects.ts",
"./globals.css": "./src/styles/globals.css",
"./hooks": "./src/hooks/index.ts",
"./hooks/*": "./src/hooks/*.ts",
"./locales": "./src/locales/catalogs.ts",
"./locales/*": "./src/locales/*.ts",
"./preferences": "./src/preferences.ts",
"./theme": "./src/theme.ts",
"./use-resolved-theme": "./src/use-resolved-theme.ts"
"./use-resolved-theme": "./src/hooks/use-resolved-theme.ts"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ThemeScheme } from "./themes"
import type { ThemeScheme } from "./internal/themes"
/**
* Color-vision assistance modes.
@@ -5,13 +5,16 @@ import { I18nProvider } from "@workspace/i18n"
import { afterEach, describe, expect, it, vi } from "vitest"
import { messages as englishMessages } from "../locales/en-US"
import { BrightnessField } from "./brightness-field"
import { ColorVisionModeField } from "./color-vision-mode-field"
import { MotionModeField } from "./motion-mode-field"
import { TextScaleField } from "./text-scale-field"
import { TextScaleOptionList } from "./text-scale-option-list"
import { accentColorSwatches, ThemeColorPicker } from "./theme-color-picker"
import { ThemeModeOptionList } from "./theme-mode-option-list"
import { BrightnessField } from "./fields/brightness-field"
import { ColorVisionModeField } from "./fields/color-vision-mode-field"
import { MotionModeField } from "./fields/motion-mode-field"
import { TextScaleField } from "./fields/text-scale-field"
import { TextScaleOptionList } from "./option-lists/text-scale-option-list"
import { ThemeModeOptionList } from "./option-lists/theme-mode-option-list"
import {
accentColorSwatches,
ThemeColorPicker,
} from "./theme/theme-color-picker"
afterEach(cleanup)
@@ -1,7 +1,6 @@
import { useCallback, useState } from "react"
import { usePreference } from "@workspace/preferences"
import { useResolvedTheme } from "../use-resolved-theme"
import { useToggleTheme } from "../hooks/use-toggle-theme"
import {
AppearanceDialog,
AppearanceDialogContent,
@@ -20,8 +19,7 @@ export function AppearanceController({
onOpenChange,
}: AppearanceControllerProps) {
const [isOpen, setIsOpen] = useState(false)
const [, setThemeMode] = usePreference("theme-mode")
const resolvedTheme = useResolvedTheme()
const toggleTheme = useToggleTheme()
const toggleDialog = useCallback(() => {
if (isOpen) {
@@ -31,10 +29,6 @@ export function AppearanceController({
}
}, [handle, isOpen])
const toggleTheme = useCallback(() => {
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setThemeMode])
const handleOpenChange = useCallback(
(open: boolean) => {
setIsOpen(open)
@@ -1,29 +1,33 @@
import { cn } from "@workspace/ui/lib/utils"
import { usePreference } from "@workspace/preferences"
import { useResolvedTheme } from "../use-resolved-theme"
import { BrightnessField } from "./brightness-field"
import { ColorVisionModeField } from "./color-vision-mode-field"
import { MotionModeField } from "./motion-mode-field"
import { TextScaleField } from "./text-scale-field"
import { ThemeConfigCard } from "./theme-config-card"
import { ThemeModeField } from "./theme-mode-field"
import {
useBrightness,
useColorVisionMode,
useMotionMode,
useTextScale,
useThemeConfig,
useThemeMode,
} from "../hooks/use-appearance-preference"
import { useResolvedTheme } from "../hooks/use-resolved-theme"
import { BrightnessField } from "./fields/brightness-field"
import { ColorVisionModeField } from "./fields/color-vision-mode-field"
import { MotionModeField } from "./fields/motion-mode-field"
import { TextScaleField } from "./fields/text-scale-field"
import { ThemeModeField } from "./fields/theme-mode-field"
import { ThemeConfigCard } from "./theme/theme-config-card"
export interface AppearanceSettingsProps {
className?: string
}
export function AppearanceSettings({ className }: AppearanceSettingsProps) {
const [themeMode, setThemeMode] = usePreference("theme-mode")
const [accentColor, setAccentColor] = usePreference("accent-color")
const [baseColor, setBaseColor] = usePreference("base-color")
const [brightness, setBrightness] = usePreference("brightness")
const [colorVisionMode, setColorVisionMode] =
usePreference("color-vision-mode")
const [motionMode, setMotionMode] = usePreference("motion-mode")
const [textScale, setTextScale] = usePreference("text-scale")
const [themeMode, setThemeMode] = useThemeMode()
const [themeConfig, setThemeConfig] = useThemeConfig()
const [brightness, setBrightness] = useBrightness()
const [colorVisionMode, setColorVisionMode] = useColorVisionMode()
const [motionMode, setMotionMode] = useMotionMode()
const [textScale, setTextScale] = useTextScale()
const resolvedTheme = useResolvedTheme()
const themeConfig = { accentColor, baseColor }
return (
<div className={cn("space-y-10", className)}>
@@ -39,10 +43,7 @@ export function AppearanceSettings({ className }: AppearanceSettingsProps) {
</div>
<ThemeConfigCard
config={themeConfig}
onConfigChange={(config) => {
setBaseColor(config.baseColor)
setAccentColor(config.accentColor)
}}
onConfigChange={setThemeConfig}
scheme={resolvedTheme}
/>
</div>
@@ -9,8 +9,8 @@ import {
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { Brightness } from "../display"
import { appearanceMessages } from "../messages"
import type { Brightness } from "../../display"
import { appearanceMessages } from "../../messages"
export interface BrightnessFieldProps {
className?: string
@@ -9,8 +9,8 @@ import {
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { ColorVisionMode } from "../color-vision"
import { appearanceMessages } from "../messages"
import type { ColorVisionMode } from "../../color-vision"
import { appearanceMessages } from "../../messages"
export interface ColorVisionModeFieldProps {
className?: string
@@ -0,0 +1,12 @@
export { BrightnessField, type BrightnessFieldProps } from "./brightness-field"
export {
ColorVisionModeField,
type ColorVisionModeFieldProps,
} from "./color-vision-mode-field"
export { MotionModeField, type MotionModeFieldProps } from "./motion-mode-field"
export { TextScaleField, type TextScaleFieldProps } from "./text-scale-field"
export {
ThemeModeField,
ThemeModeIcon,
type ThemeModeFieldProps,
} from "./theme-mode-field"
@@ -8,8 +8,8 @@ import {
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { MotionMode } from "../display"
import { appearanceMessages } from "../messages"
import type { MotionMode } from "../../display"
import { appearanceMessages } from "../../messages"
export interface MotionModeFieldProps {
className?: string
@@ -9,8 +9,8 @@ import {
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { TextScale } from "../display"
import { appearanceMessages } from "../messages"
import type { TextScale } from "../../display"
import { appearanceMessages } from "../../messages"
export interface TextScaleFieldProps {
className?: string
@@ -9,8 +9,8 @@ import {
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../messages"
import type { ThemeMode } from "../preferences"
import { appearanceMessages } from "../../messages"
import type { ThemeMode } from "../../preferences"
export interface ThemeModeFieldProps {
className?: string
@@ -0,0 +1,13 @@
export {
AppearanceOptionList,
type AppearanceOption,
type AppearanceOptionListProps,
} from "./appearance-option-list"
export {
TextScaleOptionList,
type TextScaleOptionListProps,
} from "./text-scale-option-list"
export {
ThemeModeOptionList,
type ThemeModeOptionListProps,
} from "./theme-mode-option-list"
@@ -1,8 +1,8 @@
import { useMessage } from "@workspace/i18n"
import { cn } from "@workspace/ui/lib/utils"
import type { TextScale } from "../display"
import { appearanceMessages } from "../messages"
import type { TextScale } from "../../display"
import { appearanceMessages } from "../../messages"
import {
AppearanceOptionList,
type AppearanceOption,
@@ -2,8 +2,8 @@ import { useMessage } from "@workspace/i18n"
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../messages"
import type { ThemeMode } from "../preferences"
import { appearanceMessages } from "../../messages"
import type { ThemeMode } from "../../preferences"
import {
AppearanceOptionList,
type AppearanceOption,
@@ -0,0 +1,20 @@
export {
ThemeColorPicker,
ThemeColorSwatchButton,
accentColorSwatches,
baseColorSwatches,
createThemeColorSwatches,
type ThemeColor,
type ThemeColorPickerProps,
type ThemeColorPickerVariant,
type ThemeColorSwatch,
type ThemeColorSwatchButtonProps,
type ThemeColorSwatchScheme,
} from "./theme-color-picker"
export { ThemeConfigCard, type ThemeConfigCardProps } from "./theme-config-card"
export {
ThemeToggle,
ThemeToggleButton,
type ThemeToggleButtonProps,
type ThemeToggleProps,
} from "./theme-toggle-button"
@@ -3,14 +3,14 @@ import { useMessage } from "@workspace/i18n"
import { CheckIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { themeColorMessages } from "../messages"
import { themeColorMessages } from "../../messages"
import {
accentColors,
baseColors,
getTheme,
type AccentColor,
type BaseColor,
} from "../theme"
} from "../../theme"
export type ThemeColor = BaseColor | AccentColor
export type ThemeColorPickerVariant = "pill" | "swatch"
@@ -2,8 +2,8 @@ import { LocalizedText } from "@workspace/i18n"
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../messages"
import type { ThemeColorConfig, ThemeScheme } from "../theme"
import { appearanceMessages } from "../../messages"
import type { ThemeColorConfig, ThemeScheme } from "../../theme"
import {
accentColorSwatches,
baseColorSwatches,
@@ -1,13 +1,13 @@
import type { ComponentProps } from "react"
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { useMessage } from "@workspace/i18n"
import { usePreference } from "@workspace/preferences"
import { Button } from "@workspace/ui/components/button"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../messages"
import type { ThemeScheme } from "../theme"
import { useResolvedTheme } from "../use-resolved-theme"
import { useResolvedTheme } from "../../hooks/use-resolved-theme"
import { useToggleTheme } from "../../hooks/use-toggle-theme"
import { appearanceMessages } from "../../messages"
import type { ThemeScheme } from "../../theme"
export interface ThemeToggleProps extends Omit<
ComponentProps<typeof Button>,
@@ -50,14 +50,10 @@ export type ThemeToggleButtonProps = Omit<
>
export function ThemeToggleButton(props: ThemeToggleButtonProps) {
const [, setThemeMode] = usePreference("theme-mode")
const resolvedTheme = useResolvedTheme()
const toggleTheme = useToggleTheme()
return (
<ThemeToggle
{...props}
theme={resolvedTheme}
onThemeChange={setThemeMode}
/>
<ThemeToggle {...props} theme={resolvedTheme} onThemeChange={toggleTheme} />
)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ThemeScheme } from "./themes"
import type { ThemeScheme } from "./internal/themes"
export type Brightness = "dim" | "normal" | "bright"
export type MotionMode = "system" | "reduce" | "full"
+8 -22
View File
@@ -9,16 +9,14 @@ import {
BRIGHTNESS_ATTRIBUTE,
MOTION_ATTRIBUTE,
TEXT_SCALE_ATTRIBUTE,
type ResolvedMotionMode,
} from "./display"
import { buildThemeStyleSheet, THEME_STYLE_ELEMENT_ID } from "./theme"
import {
buildThemeStyleSheet,
THEME_STYLE_ELEMENT_ID,
type ThemeScheme,
} from "./theme"
const systemThemeQuery = "(prefers-color-scheme: dark)"
const reducedMotionQuery = "(prefers-reduced-motion: reduce)"
getSystemMotion,
getSystemTheme,
subscribeToSystemMotion,
subscribeToSystemTheme,
} from "./internal/system-preferences"
const appearancePreferenceKeys = [
"accent-color",
"base-color",
@@ -49,14 +47,6 @@ function disableThemeTransitionsTemporarily() {
})
}
function getSystemTheme(): ThemeScheme {
return window.matchMedia(systemThemeQuery).matches ? "dark" : "light"
}
function getSystemMotion(): ResolvedMotionMode {
return window.matchMedia(reducedMotionQuery).matches ? "reduce" : "full"
}
export function applyAppearancePreferences(preferences: AppearancePreferences) {
if (typeof document === "undefined") return
@@ -123,14 +113,12 @@ export const subscribeSystemThemePreferenceEffect: PreferenceEffectCallback = ({
const appearancePreferences = preferences as AppearancePreferences
if (appearancePreferences["theme-mode"] !== "system") return
const mediaQuery = window.matchMedia(systemThemeQuery)
const applySystemTheme = () => {
disableThemeTransitionsTemporarily()
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
}
mediaQuery.addEventListener("change", applySystemTheme)
return () => mediaQuery.removeEventListener("change", applySystemTheme)
return subscribeToSystemTheme(applySystemTheme)
}
/** Keeps system-mode motion synchronized with the operating system setting. */
@@ -139,13 +127,11 @@ export const subscribeSystemMotionPreferenceEffect: PreferenceEffectCallback =
const appearancePreferences = preferences as AppearancePreferences
if (appearancePreferences["motion-mode"] !== "system") return
const mediaQuery = window.matchMedia(reducedMotionQuery)
const applySystemMotion = () => {
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
}
mediaQuery.addEventListener("change", applySystemMotion)
return () => mediaQuery.removeEventListener("change", applySystemMotion)
return subscribeToSystemMotion(applySystemMotion)
}
/** Subscribes to every operating-system preference used by Appearance. */
@@ -0,0 +1,107 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
import { PreferencesProvider } from "@workspace/preferences"
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
import { defaultAppearancePreferences } from "../preferences"
import {
useAccentColor,
useAppearancePreferences,
useThemeConfig,
useThemeMode,
} from "./use-appearance-preference"
import { useResolvedMotion } from "./use-resolved-motion"
import { useResolvedTheme } from "./use-resolved-theme"
import { useToggleTheme } from "./use-toggle-theme"
beforeAll(() => {
vi.stubGlobal(
"matchMedia",
vi.fn().mockImplementation((query: string) => ({
addEventListener: vi.fn(),
matches: false,
media: query,
removeEventListener: vi.fn(),
}))
)
})
afterEach(cleanup)
function renderPreferences(node: React.ReactNode) {
return render(
<PreferencesProvider initialPreferences={defaultAppearancePreferences}>
{node}
</PreferencesProvider>
)
}
function PreferenceProbe() {
const [accentColor, setAccentColor] = useAccentColor()
const [themeMode] = useThemeMode()
const preferences = useAppearancePreferences()
return (
<button type="button" onClick={() => setAccentColor("rose")}>
{accentColor}:{themeMode}:{preferences.brightness}
</button>
)
}
function ThemeConfigProbe() {
const [config, setConfig] = useThemeConfig()
return (
<button
type="button"
onClick={() => setConfig({ accentColor: "violet", baseColor: "zinc" })}
>
{config.accentColor}:{config.baseColor}
</button>
)
}
function ResolvedPreferenceProbe() {
const resolvedTheme = useResolvedTheme()
const resolvedMotion = useResolvedMotion()
const toggleTheme = useToggleTheme()
return (
<button type="button" onClick={toggleTheme}>
{resolvedTheme}:{resolvedMotion}
</button>
)
}
describe("appearance hooks", () => {
it("exposes typed preference-specific hooks and the complete snapshot", () => {
renderPreferences(<PreferenceProbe />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("blue:system:normal")
fireEvent.click(button)
expect(button.textContent).toBe("rose:system:normal")
})
it("updates accent and base colors through one theme config API", () => {
renderPreferences(<ThemeConfigProbe />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("blue:neutral")
fireEvent.click(button)
expect(button.textContent).toBe("violet:zinc")
})
it("resolves system preferences and toggles to an explicit theme", () => {
renderPreferences(<ResolvedPreferenceProbe />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("light:full")
fireEvent.click(button)
expect(button.textContent).toBe("dark:full")
})
})
+16
View File
@@ -0,0 +1,16 @@
export {
useAccentColor,
useAppearancePreference,
useAppearancePreferences,
useBaseColor,
useBrightness,
useColorVisionMode,
useMotionMode,
useTextScale,
useThemeConfig,
useThemeMode,
type AppearancePreferenceState,
} from "./use-appearance-preference"
export { useResolvedMotion, useSystemMotion } from "./use-resolved-motion"
export { useResolvedTheme, useSystemTheme } from "./use-resolved-theme"
export { useToggleTheme } from "./use-toggle-theme"
@@ -0,0 +1,112 @@
import React from "react"
import { usePreference, usePreferences } from "@workspace/preferences"
import type {
AppearancePreferenceKey,
AppearancePreferences,
AppearancePreferenceValue,
} from "../preferences"
import type { ThemeColorConfig } from "../theme"
export type AppearancePreferenceState<Key extends AppearancePreferenceKey> = [
AppearancePreferenceValue<Key>,
React.Dispatch<React.SetStateAction<AppearancePreferenceValue<Key>>>,
]
/** Reads and updates one preference owned by the Appearance package. */
export function useAppearancePreference<Key extends AppearancePreferenceKey>(
key: Key
): AppearancePreferenceState<Key> {
return usePreference(key)
}
/** Returns the complete Appearance-owned portion of the shared preferences. */
export function useAppearancePreferences(): AppearancePreferences {
const preferences = usePreferences()
const accentColor = preferences["accent-color"]
const baseColor = preferences["base-color"]
const brightness = preferences.brightness
const colorVisionMode = preferences["color-vision-mode"]
const motionMode = preferences["motion-mode"]
const textScale = preferences["text-scale"]
const themeMode = preferences["theme-mode"]
return React.useMemo(
() => ({
"accent-color": accentColor,
"base-color": baseColor,
brightness,
"color-vision-mode": colorVisionMode,
"motion-mode": motionMode,
"text-scale": textScale,
"theme-mode": themeMode,
}),
[
accentColor,
baseColor,
brightness,
colorVisionMode,
motionMode,
textScale,
themeMode,
]
)
}
export function useAccentColor() {
return useAppearancePreference("accent-color")
}
export function useBaseColor() {
return useAppearancePreference("base-color")
}
export function useBrightness() {
return useAppearancePreference("brightness")
}
export function useColorVisionMode() {
return useAppearancePreference("color-vision-mode")
}
export function useMotionMode() {
return useAppearancePreference("motion-mode")
}
export function useTextScale() {
return useAppearancePreference("text-scale")
}
export function useThemeMode() {
return useAppearancePreference("theme-mode")
}
/** Treats the two color preferences as one controlled theme configuration. */
export function useThemeConfig(): [
ThemeColorConfig,
React.Dispatch<React.SetStateAction<ThemeColorConfig>>,
] {
const [accentColor, setAccentColor] = useAccentColor()
const [baseColor, setBaseColor] = useBaseColor()
const config = React.useMemo(
() => ({ accentColor, baseColor }),
[accentColor, baseColor]
)
const configRef = React.useRef(config)
configRef.current = config
const setConfig = React.useCallback<
React.Dispatch<React.SetStateAction<ThemeColorConfig>>
>(
(action) => {
const nextConfig =
typeof action === "function" ? action(configRef.current) : action
configRef.current = nextConfig
setAccentColor(nextConfig.accentColor)
setBaseColor(nextConfig.baseColor)
},
[setAccentColor, setBaseColor]
)
return React.useMemo(() => [config, setConfig], [config, setConfig])
}
@@ -0,0 +1,26 @@
import React from "react"
import type { ResolvedMotionMode } from "../display"
import {
getServerSystemMotion,
getSystemMotion,
subscribeToSystemMotion,
} from "../internal/system-preferences"
import { useMotionMode } from "./use-appearance-preference"
/** Tracks the operating system reduced-motion preference. */
export function useSystemMotion(): ResolvedMotionMode {
return React.useSyncExternalStore(
subscribeToSystemMotion,
getSystemMotion,
getServerSystemMotion
)
}
/** Resolves `system` to the current operating system motion preference. */
export function useResolvedMotion(): ResolvedMotionMode {
const [motionMode] = useMotionMode()
const systemMotion = useSystemMotion()
return motionMode === "system" ? systemMotion : motionMode
}
@@ -0,0 +1,26 @@
import React from "react"
import {
getServerSystemTheme,
getSystemTheme,
subscribeToSystemTheme,
} from "../internal/system-preferences"
import type { ThemeScheme } from "../theme"
import { useThemeMode } from "./use-appearance-preference"
/** Tracks the operating system color scheme independently of theme mode. */
export function useSystemTheme(): ThemeScheme {
return React.useSyncExternalStore(
subscribeToSystemTheme,
getSystemTheme,
getServerSystemTheme
)
}
/** Resolves `system` to the current operating system color scheme. */
export function useResolvedTheme(): ThemeScheme {
const [themeMode] = useThemeMode()
const systemTheme = useSystemTheme()
return themeMode === "system" ? systemTheme : themeMode
}
@@ -0,0 +1,14 @@
import React from "react"
import { useThemeMode } from "./use-appearance-preference"
import { useResolvedTheme } from "./use-resolved-theme"
/** Returns a stable callback that toggles between explicit light and dark modes. */
export function useToggleTheme(): VoidFunction {
const [, setThemeMode] = useThemeMode()
const resolvedTheme = useResolvedTheme()
return React.useCallback(() => {
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setThemeMode])
}
+22 -38
View File
@@ -23,34 +23,34 @@ export {
AppearanceSettings,
type AppearanceSettingsProps,
} from "./components/appearance-settings"
export {
BrightnessField,
ColorVisionModeField,
MotionModeField,
TextScaleField,
ThemeModeField,
ThemeModeIcon,
type BrightnessFieldProps,
type ColorVisionModeFieldProps,
type MotionModeFieldProps,
type TextScaleFieldProps,
type ThemeModeFieldProps,
} from "./components/fields"
export {
AppearanceOptionList,
TextScaleOptionList,
ThemeModeOptionList,
type AppearanceOption,
type AppearanceOptionListProps,
} from "./components/appearance-option-list"
export {
BrightnessField,
type BrightnessFieldProps,
} from "./components/brightness-field"
export {
ColorVisionModeField,
type ColorVisionModeFieldProps,
} from "./components/color-vision-mode-field"
export {
MotionModeField,
type MotionModeFieldProps,
} from "./components/motion-mode-field"
export {
TextScaleField,
type TextScaleFieldProps,
} from "./components/text-scale-field"
export {
TextScaleOptionList,
type TextScaleOptionListProps,
} from "./components/text-scale-option-list"
type ThemeModeOptionListProps,
} from "./components/option-lists"
export {
ThemeColorPicker,
ThemeColorSwatchButton,
ThemeConfigCard,
ThemeToggle,
ThemeToggleButton,
accentColorSwatches,
baseColorSwatches,
createThemeColorSwatches,
@@ -60,26 +60,10 @@ export {
type ThemeColorSwatch,
type ThemeColorSwatchButtonProps,
type ThemeColorSwatchScheme,
} from "./components/theme-color-picker"
export {
ThemeConfigCard,
type ThemeConfigCardProps,
} from "./components/theme-config-card"
export {
ThemeModeField,
ThemeModeIcon,
type ThemeModeFieldProps,
} from "./components/theme-mode-field"
export {
ThemeModeOptionList,
type ThemeModeOptionListProps,
} from "./components/theme-mode-option-list"
export {
ThemeToggle,
ThemeToggleButton,
type ThemeToggleButtonProps,
type ThemeToggleProps,
} from "./components/theme-toggle-button"
} from "./components/theme"
export {
COLOR_VISION_ATTRIBUTE,
colorVisionModes,
@@ -125,4 +109,4 @@ export {
type ThemeMode,
} from "./preferences"
export * from "./theme"
export { useResolvedTheme } from "./use-resolved-theme"
export * from "./hooks"
@@ -0,0 +1,39 @@
import type { ResolvedMotionMode } from "../display"
import type { ThemeScheme } from "../theme"
export const SYSTEM_THEME_QUERY = "(prefers-color-scheme: dark)"
export const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)"
function subscribeToMediaQuery(query: string, listener: VoidFunction) {
if (typeof window === "undefined") return () => {}
const mediaQuery = window.matchMedia(query)
mediaQuery.addEventListener("change", listener)
return () => mediaQuery.removeEventListener("change", listener)
}
export function subscribeToSystemTheme(listener: VoidFunction) {
return subscribeToMediaQuery(SYSTEM_THEME_QUERY, listener)
}
export function getSystemTheme(): ThemeScheme {
if (typeof window === "undefined") return getServerSystemTheme()
return window.matchMedia(SYSTEM_THEME_QUERY).matches ? "dark" : "light"
}
export function getServerSystemTheme(): ThemeScheme {
return "light"
}
export function subscribeToSystemMotion(listener: VoidFunction) {
return subscribeToMediaQuery(REDUCED_MOTION_QUERY, listener)
}
export function getSystemMotion(): ResolvedMotionMode {
if (typeof window === "undefined") return getServerSystemMotion()
return window.matchMedia(REDUCED_MOTION_QUERY).matches ? "reduce" : "full"
}
export function getServerSystemMotion(): ResolvedMotionMode {
return "full"
}
+2 -2
View File
@@ -4,7 +4,7 @@ import {
type BaseColor,
type ThemeColorConfig,
type ThemeScheme,
} from "./themes"
} from "./internal/themes"
import {
getColorVisionCssVariables,
type ColorVisionMode,
@@ -20,7 +20,7 @@ export {
type Theme,
type ThemeColorConfig,
type ThemeScheme,
} from "./themes"
} from "./internal/themes"
export const THEME_STYLE_ELEMENT_ID = "ui-theme-variables"
@@ -1,32 +0,0 @@
import React from "react"
import { usePreference } from "@workspace/preferences"
import type { ThemeMode } from "./preferences"
import type { ThemeScheme } from "./theme"
const systemThemeQuery = "(prefers-color-scheme: dark)"
function subscribeToSystemTheme(listener: VoidFunction) {
const mediaQuery = window.matchMedia(systemThemeQuery)
mediaQuery.addEventListener("change", listener)
return () => mediaQuery.removeEventListener("change", listener)
}
function getSystemTheme(): ThemeScheme {
return window.matchMedia(systemThemeQuery).matches ? "dark" : "light"
}
function getServerSystemTheme(): ThemeScheme {
return "light"
}
export function useResolvedTheme(): ThemeScheme {
const themeMode: ThemeMode = usePreference("theme-mode")[0]
const systemTheme = React.useSyncExternalStore(
subscribeToSystemTheme,
getSystemTheme,
getServerSystemTheme
)
return themeMode === "system" ? systemTheme : themeMode
}