From 22ad26f24c79604242087bf171ba434d62eed50d Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:40:55 +0200 Subject: [PATCH] feat(app): add reduce motion appearance setting (#57103) --- packages/app/src/index.css | 4 +-- packages/app/src/new-session/pin-board.tsx | 3 ++- packages/app/src/runtime/i18n/en.ts | 5 ++++ .../src/session/composer/activity-chips.css | 2 +- .../app/src/session/timeline/virtualizer.tsx | 3 ++- .../src/settings/about/animated-wordmark.tsx | 3 ++- .../src/settings/appearance/appearance.tsx | 22 ++++++++++++++++ .../app/src/settings/general/controllers.ts | 5 ++++ packages/app/src/settings/model.test.ts | 2 ++ packages/app/src/settings/model.tsx | 20 +++++++++++++- packages/app/src/settings/search-catalog.ts | 6 +++++ packages/app/src/settings/search.css | 2 +- packages/app/src/settings/settings.css | 6 ++--- .../app/src/settings/workspaces/project.css | 2 +- packages/app/src/shell/mobile-drawer.css | 2 +- packages/app/src/shell/titlebar/tab-nav.css | 2 +- .../app/src/shell/titlebar/tab-popover.css | 2 +- .../gui-extensions/src/browser/comment.css | 2 +- .../gui-extensions/src/details/details.css | 2 +- .../session-ui/src/components/basic-tool.css | 2 +- .../session-ui/src/components/markdown.css | 2 +- .../src/components/message-part.css | 4 +-- .../src/components/question-prompt.css | 2 +- .../src/components/tool-count-label.css | 2 +- .../src/components/tool-count-summary.css | 2 +- .../src/components/tool-status-title.css | 2 +- .../session-progress-indicator-v2.css | 2 +- packages/storybook/.storybook/preview.tsx | 15 +++++++++++ .../ui/src/components/animated-number.css | 2 +- packages/ui/src/components/text-reveal.css | 2 +- .../ui/src/components/text-strikethrough.css | 2 +- packages/ui/src/feedback/loader/loader.css | 2 +- packages/ui/src/feedback/toast/toast.css | 2 +- packages/ui/src/feedback/toast/toast.tsx | 3 ++- packages/ui/src/motion/README.md | 2 +- packages/ui/src/motion/reduced.ts | 26 +++++++++++++++++-- .../segmented-control/segmented-control.tsx | 5 ++-- packages/ui/src/overlays/tooltip/tooltip.css | 2 +- packages/ui/src/styles/tailwind/index.css | 4 +++ .../typography/text-shimmer/text-shimmer.css | 2 +- 40 files changed, 142 insertions(+), 40 deletions(-) diff --git a/packages/app/src/index.css b/packages/app/src/index.css index db868801aa9..d1eb442a50b 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -178,7 +178,7 @@ transform-origin: 50% 50%; } - @media (prefers-reduced-motion: reduce) { + :root[data-motion="reduce"] { [data-slot="titlebar-update-loader"] { animation: none; } @@ -262,7 +262,7 @@ --session-dropzone-card: var(--v2-background-bg-layer-01); } - @media (prefers-reduced-motion: reduce) { + :root[data-motion="reduce"] { [data-component="session-dropzone"][data-visible="true"], [data-component="session-dropzone"][data-visible="true"] [data-slot="session-dropzone-content"], [data-component="session-dropzone"][data-visible="true"] [data-slot="session-dropzone-upload"], diff --git a/packages/app/src/new-session/pin-board.tsx b/packages/app/src/new-session/pin-board.tsx index f8760e2a382..e79a6ec3054 100644 --- a/packages/app/src/new-session/pin-board.tsx +++ b/packages/app/src/new-session/pin-board.tsx @@ -1,6 +1,7 @@ import { onCleanup, onMount } from "solid-js" import { createQuads, type Quads, type Rgba } from "./pin-quads" import { PinSound } from "./pin-sound" +import { prefersReducedMotion } from "@opencode/ui/motion" // The wordmark as a 78×14 pin grid: 1 = counter shadow, 2 = "open", 3 = "code". const GRID = [ @@ -98,7 +99,7 @@ function mountPinBoard(back: HTMLCanvasElement, front: HTMLCanvasElement, logo: const stage = back.parentElement! // The pins behind the composer and the ones popped in front of it are each one layer of quads. const layers = { back: createQuads(back, () => wake()), front: createQuads(front, () => wake()) } - const still = matchMedia("(prefers-reduced-motion: reduce)").matches + const still = prefersReducedMotion() const pins: Pin[] = GRID.flatMap((row, r) => [...row].flatMap((ch, c) => (ch === "." ? [] : [createPin(c, r, Number(ch))])), ) diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index fd7030312a0..df60d4ce141 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -753,6 +753,11 @@ export const dict = { "settings.general.row.interfaceMode.inbox.description": "Every session across your servers, sorted by what needs you next.", "settings.general.row.interfaceMode.experimental": "Experimental", + "settings.appearance.row.reduceMotion.title": "Reduce motion", + "settings.appearance.row.reduceMotion.description": "Reduce animations or match your system", + "settings.appearance.row.reduceMotion.system": "System", + "settings.appearance.row.reduceMotion.on": "On", + "settings.appearance.row.reduceMotion.off": "Off", "settings.notifications.description": "Choose when to receive notifications and hear sounds", "settings.shortcuts.description": "Customize shortcuts for common actions", "settings.servers.description": "Manage server connections", diff --git a/packages/app/src/session/composer/activity-chips.css b/packages/app/src/session/composer/activity-chips.css index 205209ffd35..646f1382658 100644 --- a/packages/app/src/session/composer/activity-chips.css +++ b/packages/app/src/session/composer/activity-chips.css @@ -56,7 +56,7 @@ margin: 4px -4px; } - @media (prefers-reduced-motion: reduce) { + :root[data-motion="reduce"] & { animation: none; } } diff --git a/packages/app/src/session/timeline/virtualizer.tsx b/packages/app/src/session/timeline/virtualizer.tsx index a78d1e92ce3..8f0e1042a3e 100644 --- a/packages/app/src/session/timeline/virtualizer.tsx +++ b/packages/app/src/session/timeline/virtualizer.tsx @@ -28,6 +28,7 @@ import { createMediaQuery } from "@solid-primitives/media" import type { createTimelineProjection } from "./projection" import { observeElementOffsetReconnectAware } from "./observe-element-offset" import { filterVirtualIndexes } from "./virtual-items" +import { prefersReducedMotion } from "@opencode/ui/motion" const fallbackItemSize = 60 @@ -869,7 +870,7 @@ export function createTimelineVirtualizer(input: Input) { "box-shadow": "var(--v2-elevation-raised), 0px 2px 8px var(--v2-background-bg-base)", }} onClick={() => - input.onResumeScroll(matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth") + input.onResumeScroll(prefersReducedMotion() ? "auto" : "smooth") } >