refactor(appearance): organize components and hooks
This commit is contained in:
@@ -26,14 +26,33 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./color-vision": "./src/color-vision.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",
|
"./components/*": "./src/components/*.tsx",
|
||||||
"./display": "./src/display.ts",
|
"./display": "./src/display.ts",
|
||||||
"./effects": "./src/effects.ts",
|
"./effects": "./src/effects.ts",
|
||||||
"./globals.css": "./src/styles/globals.css",
|
"./globals.css": "./src/styles/globals.css",
|
||||||
|
"./hooks": "./src/hooks/index.ts",
|
||||||
|
"./hooks/*": "./src/hooks/*.ts",
|
||||||
"./locales": "./src/locales/catalogs.ts",
|
"./locales": "./src/locales/catalogs.ts",
|
||||||
"./locales/*": "./src/locales/*.ts",
|
"./locales/*": "./src/locales/*.ts",
|
||||||
"./preferences": "./src/preferences.ts",
|
"./preferences": "./src/preferences.ts",
|
||||||
"./theme": "./src/theme.ts",
|
"./theme": "./src/theme.ts",
|
||||||
"./use-resolved-theme": "./src/use-resolved-theme.ts"
|
"./use-resolved-theme": "./src/hooks/use-resolved-theme.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ThemeScheme } from "./themes"
|
import type { ThemeScheme } from "./internal/themes"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color-vision assistance modes.
|
* Color-vision assistance modes.
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import { I18nProvider } from "@workspace/i18n"
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
import { messages as englishMessages } from "../locales/en-US"
|
import { messages as englishMessages } from "../locales/en-US"
|
||||||
import { BrightnessField } from "./brightness-field"
|
import { BrightnessField } from "./fields/brightness-field"
|
||||||
import { ColorVisionModeField } from "./color-vision-mode-field"
|
import { ColorVisionModeField } from "./fields/color-vision-mode-field"
|
||||||
import { MotionModeField } from "./motion-mode-field"
|
import { MotionModeField } from "./fields/motion-mode-field"
|
||||||
import { TextScaleField } from "./text-scale-field"
|
import { TextScaleField } from "./fields/text-scale-field"
|
||||||
import { TextScaleOptionList } from "./text-scale-option-list"
|
import { TextScaleOptionList } from "./option-lists/text-scale-option-list"
|
||||||
import { accentColorSwatches, ThemeColorPicker } from "./theme-color-picker"
|
import { ThemeModeOptionList } from "./option-lists/theme-mode-option-list"
|
||||||
import { ThemeModeOptionList } from "./theme-mode-option-list"
|
import {
|
||||||
|
accentColorSwatches,
|
||||||
|
ThemeColorPicker,
|
||||||
|
} from "./theme/theme-color-picker"
|
||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import { usePreference } from "@workspace/preferences"
|
|
||||||
|
|
||||||
import { useResolvedTheme } from "../use-resolved-theme"
|
import { useToggleTheme } from "../hooks/use-toggle-theme"
|
||||||
import {
|
import {
|
||||||
AppearanceDialog,
|
AppearanceDialog,
|
||||||
AppearanceDialogContent,
|
AppearanceDialogContent,
|
||||||
@@ -20,8 +19,7 @@ export function AppearanceController({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: AppearanceControllerProps) {
|
}: AppearanceControllerProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [, setThemeMode] = usePreference("theme-mode")
|
const toggleTheme = useToggleTheme()
|
||||||
const resolvedTheme = useResolvedTheme()
|
|
||||||
|
|
||||||
const toggleDialog = useCallback(() => {
|
const toggleDialog = useCallback(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -31,10 +29,6 @@ export function AppearanceController({
|
|||||||
}
|
}
|
||||||
}, [handle, isOpen])
|
}, [handle, isOpen])
|
||||||
|
|
||||||
const toggleTheme = useCallback(() => {
|
|
||||||
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
|
|
||||||
}, [resolvedTheme, setThemeMode])
|
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
setIsOpen(open)
|
setIsOpen(open)
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { usePreference } from "@workspace/preferences"
|
|
||||||
|
|
||||||
import { useResolvedTheme } from "../use-resolved-theme"
|
import {
|
||||||
import { BrightnessField } from "./brightness-field"
|
useBrightness,
|
||||||
import { ColorVisionModeField } from "./color-vision-mode-field"
|
useColorVisionMode,
|
||||||
import { MotionModeField } from "./motion-mode-field"
|
useMotionMode,
|
||||||
import { TextScaleField } from "./text-scale-field"
|
useTextScale,
|
||||||
import { ThemeConfigCard } from "./theme-config-card"
|
useThemeConfig,
|
||||||
import { ThemeModeField } from "./theme-mode-field"
|
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 {
|
export interface AppearanceSettingsProps {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppearanceSettings({ className }: AppearanceSettingsProps) {
|
export function AppearanceSettings({ className }: AppearanceSettingsProps) {
|
||||||
const [themeMode, setThemeMode] = usePreference("theme-mode")
|
const [themeMode, setThemeMode] = useThemeMode()
|
||||||
const [accentColor, setAccentColor] = usePreference("accent-color")
|
const [themeConfig, setThemeConfig] = useThemeConfig()
|
||||||
const [baseColor, setBaseColor] = usePreference("base-color")
|
const [brightness, setBrightness] = useBrightness()
|
||||||
const [brightness, setBrightness] = usePreference("brightness")
|
const [colorVisionMode, setColorVisionMode] = useColorVisionMode()
|
||||||
const [colorVisionMode, setColorVisionMode] =
|
const [motionMode, setMotionMode] = useMotionMode()
|
||||||
usePreference("color-vision-mode")
|
const [textScale, setTextScale] = useTextScale()
|
||||||
const [motionMode, setMotionMode] = usePreference("motion-mode")
|
|
||||||
const [textScale, setTextScale] = usePreference("text-scale")
|
|
||||||
const resolvedTheme = useResolvedTheme()
|
const resolvedTheme = useResolvedTheme()
|
||||||
const themeConfig = { accentColor, baseColor }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-10", className)}>
|
<div className={cn("space-y-10", className)}>
|
||||||
@@ -39,10 +43,7 @@ export function AppearanceSettings({ className }: AppearanceSettingsProps) {
|
|||||||
</div>
|
</div>
|
||||||
<ThemeConfigCard
|
<ThemeConfigCard
|
||||||
config={themeConfig}
|
config={themeConfig}
|
||||||
onConfigChange={(config) => {
|
onConfigChange={setThemeConfig}
|
||||||
setBaseColor(config.baseColor)
|
|
||||||
setAccentColor(config.accentColor)
|
|
||||||
}}
|
|
||||||
scheme={resolvedTheme}
|
scheme={resolvedTheme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ import {
|
|||||||
} from "@workspace/ui/components/select"
|
} from "@workspace/ui/components/select"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import type { Brightness } from "../display"
|
import type { Brightness } from "../../display"
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
|
|
||||||
export interface BrightnessFieldProps {
|
export interface BrightnessFieldProps {
|
||||||
className?: string
|
className?: string
|
||||||
+2
-2
@@ -9,8 +9,8 @@ import {
|
|||||||
} from "@workspace/ui/components/select"
|
} from "@workspace/ui/components/select"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import type { ColorVisionMode } from "../color-vision"
|
import type { ColorVisionMode } from "../../color-vision"
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
|
|
||||||
export interface ColorVisionModeFieldProps {
|
export interface ColorVisionModeFieldProps {
|
||||||
className?: string
|
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"
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import {
|
|||||||
} from "@workspace/ui/components/select"
|
} from "@workspace/ui/components/select"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import type { MotionMode } from "../display"
|
import type { MotionMode } from "../../display"
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
|
|
||||||
export interface MotionModeFieldProps {
|
export interface MotionModeFieldProps {
|
||||||
className?: string
|
className?: string
|
||||||
+2
-2
@@ -9,8 +9,8 @@ import {
|
|||||||
} from "@workspace/ui/components/select"
|
} from "@workspace/ui/components/select"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import type { TextScale } from "../display"
|
import type { TextScale } from "../../display"
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
|
|
||||||
export interface TextScaleFieldProps {
|
export interface TextScaleFieldProps {
|
||||||
className?: string
|
className?: string
|
||||||
+2
-2
@@ -9,8 +9,8 @@ import {
|
|||||||
} from "@workspace/ui/components/select"
|
} from "@workspace/ui/components/select"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
import type { ThemeMode } from "../preferences"
|
import type { ThemeMode } from "../../preferences"
|
||||||
|
|
||||||
export interface ThemeModeFieldProps {
|
export interface ThemeModeFieldProps {
|
||||||
className?: string
|
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"
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { useMessage } from "@workspace/i18n"
|
import { useMessage } from "@workspace/i18n"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import type { TextScale } from "../display"
|
import type { TextScale } from "../../display"
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
import {
|
import {
|
||||||
AppearanceOptionList,
|
AppearanceOptionList,
|
||||||
type AppearanceOption,
|
type AppearanceOption,
|
||||||
+2
-2
@@ -2,8 +2,8 @@ import { useMessage } from "@workspace/i18n"
|
|||||||
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
import type { ThemeMode } from "../preferences"
|
import type { ThemeMode } from "../../preferences"
|
||||||
import {
|
import {
|
||||||
AppearanceOptionList,
|
AppearanceOptionList,
|
||||||
type AppearanceOption,
|
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"
|
||||||
+2
-2
@@ -3,14 +3,14 @@ import { useMessage } from "@workspace/i18n"
|
|||||||
import { CheckIcon } from "@workspace/ui/components/icon"
|
import { CheckIcon } from "@workspace/ui/components/icon"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import { themeColorMessages } from "../messages"
|
import { themeColorMessages } from "../../messages"
|
||||||
import {
|
import {
|
||||||
accentColors,
|
accentColors,
|
||||||
baseColors,
|
baseColors,
|
||||||
getTheme,
|
getTheme,
|
||||||
type AccentColor,
|
type AccentColor,
|
||||||
type BaseColor,
|
type BaseColor,
|
||||||
} from "../theme"
|
} from "../../theme"
|
||||||
|
|
||||||
export type ThemeColor = BaseColor | AccentColor
|
export type ThemeColor = BaseColor | AccentColor
|
||||||
export type ThemeColorPickerVariant = "pill" | "swatch"
|
export type ThemeColorPickerVariant = "pill" | "swatch"
|
||||||
+2
-2
@@ -2,8 +2,8 @@ import { LocalizedText } from "@workspace/i18n"
|
|||||||
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import { appearanceMessages } from "../messages"
|
import { appearanceMessages } from "../../messages"
|
||||||
import type { ThemeColorConfig, ThemeScheme } from "../theme"
|
import type { ThemeColorConfig, ThemeScheme } from "../../theme"
|
||||||
import {
|
import {
|
||||||
accentColorSwatches,
|
accentColorSwatches,
|
||||||
baseColorSwatches,
|
baseColorSwatches,
|
||||||
+6
-10
@@ -1,13 +1,13 @@
|
|||||||
import type { ComponentProps } from "react"
|
import type { ComponentProps } from "react"
|
||||||
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
||||||
import { useMessage } from "@workspace/i18n"
|
import { useMessage } from "@workspace/i18n"
|
||||||
import { usePreference } from "@workspace/preferences"
|
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
import { appearanceMessages } from "../messages"
|
import { useResolvedTheme } from "../../hooks/use-resolved-theme"
|
||||||
import type { ThemeScheme } from "../theme"
|
import { useToggleTheme } from "../../hooks/use-toggle-theme"
|
||||||
import { useResolvedTheme } from "../use-resolved-theme"
|
import { appearanceMessages } from "../../messages"
|
||||||
|
import type { ThemeScheme } from "../../theme"
|
||||||
|
|
||||||
export interface ThemeToggleProps extends Omit<
|
export interface ThemeToggleProps extends Omit<
|
||||||
ComponentProps<typeof Button>,
|
ComponentProps<typeof Button>,
|
||||||
@@ -50,14 +50,10 @@ export type ThemeToggleButtonProps = Omit<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export function ThemeToggleButton(props: ThemeToggleButtonProps) {
|
export function ThemeToggleButton(props: ThemeToggleButtonProps) {
|
||||||
const [, setThemeMode] = usePreference("theme-mode")
|
|
||||||
const resolvedTheme = useResolvedTheme()
|
const resolvedTheme = useResolvedTheme()
|
||||||
|
const toggleTheme = useToggleTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeToggle
|
<ThemeToggle {...props} theme={resolvedTheme} onThemeChange={toggleTheme} />
|
||||||
{...props}
|
|
||||||
theme={resolvedTheme}
|
|
||||||
onThemeChange={setThemeMode}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ThemeScheme } from "./themes"
|
import type { ThemeScheme } from "./internal/themes"
|
||||||
|
|
||||||
export type Brightness = "dim" | "normal" | "bright"
|
export type Brightness = "dim" | "normal" | "bright"
|
||||||
export type MotionMode = "system" | "reduce" | "full"
|
export type MotionMode = "system" | "reduce" | "full"
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ import {
|
|||||||
BRIGHTNESS_ATTRIBUTE,
|
BRIGHTNESS_ATTRIBUTE,
|
||||||
MOTION_ATTRIBUTE,
|
MOTION_ATTRIBUTE,
|
||||||
TEXT_SCALE_ATTRIBUTE,
|
TEXT_SCALE_ATTRIBUTE,
|
||||||
type ResolvedMotionMode,
|
|
||||||
} from "./display"
|
} from "./display"
|
||||||
|
import { buildThemeStyleSheet, THEME_STYLE_ELEMENT_ID } from "./theme"
|
||||||
import {
|
import {
|
||||||
buildThemeStyleSheet,
|
getSystemMotion,
|
||||||
THEME_STYLE_ELEMENT_ID,
|
getSystemTheme,
|
||||||
type ThemeScheme,
|
subscribeToSystemMotion,
|
||||||
} from "./theme"
|
subscribeToSystemTheme,
|
||||||
|
} from "./internal/system-preferences"
|
||||||
const systemThemeQuery = "(prefers-color-scheme: dark)"
|
|
||||||
const reducedMotionQuery = "(prefers-reduced-motion: reduce)"
|
|
||||||
const appearancePreferenceKeys = [
|
const appearancePreferenceKeys = [
|
||||||
"accent-color",
|
"accent-color",
|
||||||
"base-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) {
|
export function applyAppearancePreferences(preferences: AppearancePreferences) {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
|
|
||||||
@@ -123,14 +113,12 @@ export const subscribeSystemThemePreferenceEffect: PreferenceEffectCallback = ({
|
|||||||
const appearancePreferences = preferences as AppearancePreferences
|
const appearancePreferences = preferences as AppearancePreferences
|
||||||
if (appearancePreferences["theme-mode"] !== "system") return
|
if (appearancePreferences["theme-mode"] !== "system") return
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia(systemThemeQuery)
|
|
||||||
const applySystemTheme = () => {
|
const applySystemTheme = () => {
|
||||||
disableThemeTransitionsTemporarily()
|
disableThemeTransitionsTemporarily()
|
||||||
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
|
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaQuery.addEventListener("change", applySystemTheme)
|
return subscribeToSystemTheme(applySystemTheme)
|
||||||
return () => mediaQuery.removeEventListener("change", applySystemTheme)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Keeps system-mode motion synchronized with the operating system setting. */
|
/** Keeps system-mode motion synchronized with the operating system setting. */
|
||||||
@@ -139,13 +127,11 @@ export const subscribeSystemMotionPreferenceEffect: PreferenceEffectCallback =
|
|||||||
const appearancePreferences = preferences as AppearancePreferences
|
const appearancePreferences = preferences as AppearancePreferences
|
||||||
if (appearancePreferences["motion-mode"] !== "system") return
|
if (appearancePreferences["motion-mode"] !== "system") return
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia(reducedMotionQuery)
|
|
||||||
const applySystemMotion = () => {
|
const applySystemMotion = () => {
|
||||||
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
|
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaQuery.addEventListener("change", applySystemMotion)
|
return subscribeToSystemMotion(applySystemMotion)
|
||||||
return () => mediaQuery.removeEventListener("change", applySystemMotion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Subscribes to every operating-system preference used by Appearance. */
|
/** 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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
@@ -23,34 +23,34 @@ export {
|
|||||||
AppearanceSettings,
|
AppearanceSettings,
|
||||||
type AppearanceSettingsProps,
|
type AppearanceSettingsProps,
|
||||||
} from "./components/appearance-settings"
|
} 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 {
|
export {
|
||||||
AppearanceOptionList,
|
AppearanceOptionList,
|
||||||
|
TextScaleOptionList,
|
||||||
|
ThemeModeOptionList,
|
||||||
type AppearanceOption,
|
type AppearanceOption,
|
||||||
type AppearanceOptionListProps,
|
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,
|
type TextScaleOptionListProps,
|
||||||
} from "./components/text-scale-option-list"
|
type ThemeModeOptionListProps,
|
||||||
|
} from "./components/option-lists"
|
||||||
export {
|
export {
|
||||||
ThemeColorPicker,
|
ThemeColorPicker,
|
||||||
ThemeColorSwatchButton,
|
ThemeColorSwatchButton,
|
||||||
|
ThemeConfigCard,
|
||||||
|
ThemeToggle,
|
||||||
|
ThemeToggleButton,
|
||||||
accentColorSwatches,
|
accentColorSwatches,
|
||||||
baseColorSwatches,
|
baseColorSwatches,
|
||||||
createThemeColorSwatches,
|
createThemeColorSwatches,
|
||||||
@@ -60,26 +60,10 @@ export {
|
|||||||
type ThemeColorSwatch,
|
type ThemeColorSwatch,
|
||||||
type ThemeColorSwatchButtonProps,
|
type ThemeColorSwatchButtonProps,
|
||||||
type ThemeColorSwatchScheme,
|
type ThemeColorSwatchScheme,
|
||||||
} from "./components/theme-color-picker"
|
|
||||||
export {
|
|
||||||
ThemeConfigCard,
|
|
||||||
type ThemeConfigCardProps,
|
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 ThemeToggleButtonProps,
|
||||||
type ThemeToggleProps,
|
type ThemeToggleProps,
|
||||||
} from "./components/theme-toggle-button"
|
} from "./components/theme"
|
||||||
export {
|
export {
|
||||||
COLOR_VISION_ATTRIBUTE,
|
COLOR_VISION_ATTRIBUTE,
|
||||||
colorVisionModes,
|
colorVisionModes,
|
||||||
@@ -125,4 +109,4 @@ export {
|
|||||||
type ThemeMode,
|
type ThemeMode,
|
||||||
} from "./preferences"
|
} from "./preferences"
|
||||||
export * from "./theme"
|
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"
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type BaseColor,
|
type BaseColor,
|
||||||
type ThemeColorConfig,
|
type ThemeColorConfig,
|
||||||
type ThemeScheme,
|
type ThemeScheme,
|
||||||
} from "./themes"
|
} from "./internal/themes"
|
||||||
import {
|
import {
|
||||||
getColorVisionCssVariables,
|
getColorVisionCssVariables,
|
||||||
type ColorVisionMode,
|
type ColorVisionMode,
|
||||||
@@ -20,7 +20,7 @@ export {
|
|||||||
type Theme,
|
type Theme,
|
||||||
type ThemeColorConfig,
|
type ThemeColorConfig,
|
||||||
type ThemeScheme,
|
type ThemeScheme,
|
||||||
} from "./themes"
|
} from "./internal/themes"
|
||||||
|
|
||||||
export const THEME_STYLE_ELEMENT_ID = "ui-theme-variables"
|
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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user