Skip to content

Add a "Reduce motion" accessibility setting to Appearance#51

Open
DragonnZhang wants to merge 2 commits into
mainfrom
loop/reduce-motion
Open

Add a "Reduce motion" accessibility setting to Appearance#51
DragonnZhang wants to merge 2 commits into
mainfrom
loop/reduce-motion

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

Closes #50

What & why

OpenWork animates heavily — the transcript, session list, dropdowns, toasts, the
pet, and the empty-composer entrance all use motion/react (framer-motion)
motion / AnimatePresence transitions — and there was no way to turn them
down
. The app never consulted the OS prefers-reduced-motion preference and
offered no in-app control, which is a real accessibility gap (vestibular
sensitivity, photosensitivity) and a common desktop expectation: Claude's desktop
app, macOS, and Windows all expose a Reduce motion control.

This adds a "Reduce motion" toggle to Settings → Appearance → Interface
(next to "Rich tool descriptions"). When enabled it:

  • renders the app inside <MotionConfig reducedMotion="always"> so every
    motion/react component short-circuits its animations app-wide (MotionConfig
    propagates through React context to all motion components in the tree,
    including those in packages/ui);
  • sets data-reduce-motion="true" on <html>, and a global CSS guard collapses
    plain CSS animations/transitions (animation-duration / transition-duration
    → ~0ms) so non-motion effects are covered too;
  • persists the choice in the renderer's localStorage (the same
    lib/local-storage.ts mechanism as other renderer UI prefs), surviving restarts.

When disabled it uses reducedMotion="user", which still honors the OS
prefers-reduced-motion setting — a strictly better default than today's
always-animate behavior.

Frontend-only. No backend / qwen-code change: motion is already a dependency
in both apps/electron and packages/ui, persistence reuses the existing
local-storage helper (no IPC, no config.json), and the toggle reuses the
existing SettingsToggle + Appearance page structure.

Changes

  • context/ReduceMotionContext.tsx (new) — ReduceMotionProvider (owns the
    pref, applies the <html> attribute, wraps children in MotionConfig) +
    useReduceMotion() hook.
  • main.tsx — mount ReduceMotionProvider at the renderer root (inside
    ThemeProvider, wrapping App / Toaster / pet), so the whole app inherits it.
  • lib/local-storage.ts — new reduceMotion key (craft-reduce-motion).
  • index.css — global [data-reduce-motion='true'] CSS guard.
  • pages/settings/AppearanceSettingsPage.tsx — the toggle in the Interface
    section.
  • components/settings/SettingsToggle.tsx — optional testId prop forwarded
    to the underlying switch (for e2e; backward-compatible).
  • pages/settings/SettingsNavigator.tsxdata-testid={settings-nav-${id}}
    on each nav item so the assertion can open the Appearance page (per the harness
    README's "add a data-testid in the same PR" guidance).
  • packages/shared/src/i18n/locales/*.jsonsettings.appearance.reduceMotion
    / ...Desc in all 7 locales (reuses the existing settings.appearance.interface
    section heading).
  • e2e/assertions/reduce-motion.assert.ts (new) — CDP assertion (below).

Verification (DoD)

  • bun run typecheck:all — introduces no new errors. Delta vs main is
    zero: the only errors are the 11 pre-existing ones in apps/electron
    (auto-update.ts owner/repo, a settings-default-thinking test tuple, two
    test files importing vitest) — none in the files this PR touches.
    packages/{core,shared,server-core,server,session-tools-core,ui} all pass clean.
  • bun test — the failing set is byte-for-byte identical to main,
    verified by diffing the sorted unique failure lists from a clean-main run and
    this branch in the same environment: 56 pre-existing failures on both
    (BrowserCDP, RPC handler profiles, RoutedClient, startWebuiHttpServer,
    i18n sorted checks, resource-bundle, …). This change adds zero new
    failures. (The raw N fail total is order-dependent when the whole suite runs
    unisolated — the deterministic signal is the failing-test set, which is
    unchanged.)
  • bun run lint:i18n:parity — OK (1544 keys per locale). The two new keys are
    added to every locale.
  • Renderer build (bun run build:renderer, where this change lives) — ✅
    builds cleanly.
  • CDP assertion transpiles (bun build e2e/assertions/reduce-motion.assert.ts)
    — ✅.

CDP e2e assertion (reduce-motion.assert.ts)

Drives the real built app over CDP entirely in the draft/no-session state — no
seeded conversation and no backend connection (it only touches Settings):

  1. Open Settings → Appearance; the Reduce motion toggle renders and is
    initially off (aria-checked="false"), with no data-reduce-motion
    attribute on <html>.
  2. Click the toggle → aria-checked="true", <html> gains
    data-reduce-motion="true", and localStorage["craft-reduce-motion"] is
    "true" — proving it applies and persists, not merely renders.
  3. Click again → aria-checked="false", the attribute is removed, and the
    persisted value is "false".

⚠️ Local CDP run was blocked by this sandbox's egress policy, not by the
feature. The Electron binary download (GitHub release host) returns 403 from
the egress proxy — its allowlist covers npm/pypi/crates but not GitHub release
hosts — so the app can't be built/launched here (xvfb is present; only the
binary fetch fails). The assertion transpiles and is included so CI / a reviewer
can run bun run e2e in an environment with normal network access. Everything
that doesn't require launching Electron (typecheck, unit tests, i18n parity,
renderer build) passes with zero delta vs main.

Part of the autonomous desktop-feature loop (loop-bot).

🤖 Generated with Claude Code


Generated by Claude Code

Add a renderer-only "Reduce motion" preference to Settings → Appearance →
Interface. When enabled the app renders inside <MotionConfig reducedMotion=
"always"> so every motion/react component short-circuits its animations, sets
data-reduce-motion="true" on <html> for a global CSS guard that collapses plain
CSS animations/transitions, and persists the choice in localStorage. When
disabled it uses reducedMotion="user", still honoring the OS
prefers-reduced-motion setting.

- New ReduceMotionProvider wired at the renderer root (main.tsx).
- Appearance toggle reuses SettingsToggle (gains an optional testId prop).
- data-testid on settings-navigator items for e2e navigation.
- Two new i18n keys across all 7 locales.
- CDP e2e assertion: e2e/assertions/reduce-motion.assert.ts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a "Reduce motion" accessibility setting to Appearance

2 participants