Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/app/InterfaceSettings.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -220,12 +220,12 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }):

<SwitchCard
overline="MOTION"
title="Animation"
blurb="Disable interface transitions and animated state changes."
label="Reduce motion"
title="Animations"
blurb="Enable interface transitions and animated state changes."
label="Enable animations"
id="interface-reduced-motion"
checked={preferences.reducedMotion}
onChange={set("reducedMotion")}
checked={!preferences.reducedMotion}
onChange={(next) => set("reducedMotion")(!next)}
/>
<SwitchCard
overline="WRITES"
Expand Down
13 changes: 6 additions & 7 deletions src/control.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -892,9 +892,10 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
.control-shell.is-empty .empty-state > 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; }
Expand DownExpand Up@@ -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);
Expand Down
18 changes: 16 additions & 2 deletions src/interface-preferences.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<InterfacePreferences>;
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() };
}
}

Expand Down