diff --git a/src/renderer/src/components/UpdateCard.tsx b/src/renderer/src/components/UpdateCard.tsx index 639575a8e31..791955cfb8f 100644 --- a/src/renderer/src/components/UpdateCard.tsx +++ b/src/renderer/src/components/UpdateCard.tsx @@ -2,6 +2,7 @@ renderer surface. Keeping the state machine and its presentation variants together avoids scattering tightly coupled update behavior across multiple files. */ import { useEffect, useRef, useState } from 'react' +import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion' import { useAppStore } from '../store' import { Card } from './ui/card' import { Button } from './ui/button' @@ -197,14 +198,7 @@ export function UpdateCard() { }, [status.state]) // ── Prefers-reduced-motion ────────────────────────────────────────── - const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) - useEffect(() => { - const mq = window.matchMedia('(prefers-reduced-motion: reduce)') - setPrefersReducedMotion(mq.matches) - const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches) - mq.addEventListener('change', handler) - return () => mq.removeEventListener('change', handler) - }, []) + const prefersReducedMotion = usePrefersReducedMotion() useEffect(() => { return () => { diff --git a/src/renderer/src/components/feature-wall/feature-wall-modal-helpers.ts b/src/renderer/src/components/feature-wall/feature-wall-modal-helpers.ts index 9903df13cfe..afdb12316e5 100644 --- a/src/renderer/src/components/feature-wall/feature-wall-modal-helpers.ts +++ b/src/renderer/src/components/feature-wall/feature-wall-modal-helpers.ts @@ -1,7 +1,5 @@ -import { useEffect, useState } from 'react' import type { FeatureWallOpenSourceTelemetry } from '../../../../shared/telemetry-events' - -const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)' +export { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion' export function getFeatureWallOpenSource( modalData: Record @@ -11,27 +9,3 @@ export function getFeatureWallOpenSource( ? source : 'unknown' } - -export function usePrefersReducedMotion(): boolean { - const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => { - if (typeof window === 'undefined' || !window.matchMedia) { - return false - } - return window.matchMedia(REDUCED_MOTION_QUERY).matches - }) - - useEffect(() => { - if (typeof window === 'undefined' || !window.matchMedia) { - return - } - const media = window.matchMedia(REDUCED_MOTION_QUERY) - setPrefersReducedMotion(media.matches) - const onChange = (event: MediaQueryListEvent): void => { - setPrefersReducedMotion(event.matches) - } - media.addEventListener('change', onChange) - return () => media.removeEventListener('change', onChange) - }, []) - - return prefersReducedMotion -} diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx index d434410d943..a3fc537c383 100644 --- a/src/renderer/src/components/pet/PetOverlay.tsx +++ b/src/renderer/src/components/pet/PetOverlay.tsx @@ -1,4 +1,5 @@ import { useEffect, useId, useRef, useState } from 'react' +import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion' import { usePetUrl } from './usePetUrl' import type { DetectedSpriteCacheEntry } from './pet-blob-cache' import type { CustomPet } from '../../../../shared/types' @@ -176,25 +177,6 @@ function useDocumentVisible(): boolean { return visible } -function usePrefersReducedMotion(): boolean { - const [reduced, setReduced] = useState(() => { - if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { - return false - } - return window.matchMedia('(prefers-reduced-motion: reduce)').matches - }) - useEffect(() => { - if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { - return - } - const mq = window.matchMedia('(prefers-reduced-motion: reduce)') - const onChange = (event: MediaQueryListEvent): void => setReduced(event.matches) - mq.addEventListener('change', onChange) - return () => mq.removeEventListener('change', onChange) - }, []) - return reduced -} - // Why: keep a default for the cached helpers below; the live size now comes // from the store so the user can resize from the status-bar menu. const SIZE = 180 diff --git a/src/renderer/src/hooks/usePrefersReducedMotion.ts b/src/renderer/src/hooks/usePrefersReducedMotion.ts new file mode 100644 index 00000000000..56195a77587 --- /dev/null +++ b/src/renderer/src/hooks/usePrefersReducedMotion.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react' + +const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)' + +function readPrefersReducedMotion(): boolean { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { + return false + } + return window.matchMedia(REDUCED_MOTION_QUERY).matches +} + +export function usePrefersReducedMotion(): boolean { + const [prefersReducedMotion, setPrefersReducedMotion] = useState(readPrefersReducedMotion) + + useEffect(() => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { + return + } + const media = window.matchMedia(REDUCED_MOTION_QUERY) + setPrefersReducedMotion(media.matches) + const onChange = (event: MediaQueryListEvent): void => { + setPrefersReducedMotion(event.matches) + } + media.addEventListener('change', onChange) + return () => media.removeEventListener('change', onChange) + }, []) + + return prefersReducedMotion +}