From 5aea38ff29632769cb3b0bc299851857dbb2d6bf Mon Sep 17 00:00:00 2001 From: yanhenrique-dev Date: Sun, 6 Sep 2026 09:48:45 -0300 Subject: [PATCH] fix: make Enable animations toggle authoritative over OS reduced-motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Motion card read as Reduce motion, so checking the box disabled animations — the opposite of what activate means. It now reads Enable animations with inverted checked state. Also removes the global prefers-reduced-motion CSS override in the control panel, which killed transitions even with animations enabled on the site. The site preference is now the single source of truth and is initialized from the OS setting on first load. --- src/app/InterfaceSettings.tsx | 10 +++++----- src/control.css | 13 ++++++------- src/interface-preferences.ts | 18 ++++++++++++++++-- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/app/InterfaceSettings.tsx b/src/app/InterfaceSettings.tsx index 37134f3c..860f96c3 100644 --- a/src/app/InterfaceSettings.tsx +++ b/src/app/InterfaceSettings.tsx @@ -220,12 +220,12 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): set("reducedMotion")(!next)} /> p:not(.overline) { margin-top: 1rem; } } -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } -} +/* NOTE: no global prefers-reduced-motion override here on purpose. The + site setting (.reduce-interface-motion above) is the single source of + truth and it is initialized from the OS preference, so an explicit + "Enable animations" choice is never overridden by the OS. */ /* Monochrome workspace — selected from the concept-18 exploration. */ body { height: 100vh; overflow: hidden; } @@ -2409,10 +2410,8 @@ body { height: 100vh; overflow: hidden; } from { max-height: 5rem; opacity: 1; } to { max-height: 0; padding-block: 0; opacity: 0; transform: translateY(-.2rem); } } -@media (prefers-reduced-motion: reduce) { - .toast { animation: none; } - .toast.is-leaving { animation-duration: .01s; } -} +.reduce-interface-motion .toast { animation: none; } +.reduce-interface-motion .toast.is-leaving { animation-duration: .01s; } .control-shell[data-interface-theme="liquid-glass"] .toast-stack { border-color: var(--glass-border-elevated); diff --git a/src/interface-preferences.ts b/src/interface-preferences.ts index a605ac98..6b71e2b6 100644 --- a/src/interface-preferences.ts +++ b/src/interface-preferences.ts @@ -53,20 +53,34 @@ function clampGlassIntensity(value: unknown): number { return Math.min(100, Math.max(0, Math.round(number))); } +/** OS-level preference. Used only as the initial default so an explicit + site choice always wins over the system setting. */ +export function systemPrefersReducedMotion(): boolean { + try { + if (typeof window !== "undefined" && typeof window.matchMedia === "function") { + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; + } + } catch { + // Ignore and fall through to the animated default below. + } + return false; +} + export function loadInterfacePreferences(storage: Storage): InterfacePreferences { try { const saved = JSON.parse(storage.getItem(STORAGE_KEY) ?? "{}") as Partial; return { theme: THEMES.includes(saved.theme as InterfaceTheme) ? saved.theme as InterfaceTheme : "Mono", colorMode: saved.colorMode === "Light" || saved.colorMode === "Dark" ? saved.colorMode : "System", - reducedMotion: saved.reducedMotion === true, + reducedMotion: + typeof saved.reducedMotion === "boolean" ? saved.reducedMotion : systemPrefersReducedMotion(), expandSections: saved.expandSections === true, showExperimental: saved.showExperimental !== false, instantFlash: saved.instantFlash === true, glassIntensity: clampGlassIntensity(saved.glassIntensity), }; } catch { - return { ...DEFAULT_INTERFACE_PREFERENCES }; + return { ...DEFAULT_INTERFACE_PREFERENCES, reducedMotion: systemPrefersReducedMotion() }; } }