From c5bf53f9a889428ec99dbb4d9162c2af9edce54f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 13:35:30 +0000 Subject: [PATCH 1/2] feat(desktop): add Reduce motion accessibility setting to Appearance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a renderer-only "Reduce motion" preference to Settings → Appearance → Interface. When enabled the app renders inside so every motion/react component short-circuits its animations, sets data-reduce-motion="true" on 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. --- .../components/settings/SettingsToggle.tsx | 4 + .../renderer/context/ReduceMotionContext.tsx | 82 +++++++++++++++ apps/electron/src/renderer/index.css | 15 +++ .../src/renderer/lib/local-storage.ts | 1 + apps/electron/src/renderer/main.tsx | 9 +- .../pages/settings/AppearanceSettingsPage.tsx | 11 +++ .../pages/settings/SettingsNavigator.tsx | 6 +- docs/loop/feature-ledger.md | 5 +- e2e/assertions/reduce-motion.assert.ts | 99 +++++++++++++++++++ packages/shared/src/i18n/locales/de.json | 2 + packages/shared/src/i18n/locales/en.json | 2 + packages/shared/src/i18n/locales/es.json | 2 + packages/shared/src/i18n/locales/hu.json | 2 + packages/shared/src/i18n/locales/ja.json | 2 + packages/shared/src/i18n/locales/pl.json | 2 + packages/shared/src/i18n/locales/zh-Hans.json | 2 + 16 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 apps/electron/src/renderer/context/ReduceMotionContext.tsx create mode 100644 e2e/assertions/reduce-motion.assert.ts diff --git a/apps/electron/src/renderer/components/settings/SettingsToggle.tsx b/apps/electron/src/renderer/components/settings/SettingsToggle.tsx index c69178a07..00e2a02e9 100644 --- a/apps/electron/src/renderer/components/settings/SettingsToggle.tsx +++ b/apps/electron/src/renderer/components/settings/SettingsToggle.tsx @@ -25,6 +25,8 @@ export interface SettingsToggleProps { className?: string /** Whether the toggle is inside a card (affects padding) */ inCard?: boolean + /** Optional test id forwarded to the underlying switch (for e2e) */ + testId?: string } /** @@ -48,6 +50,7 @@ export function SettingsToggle({ disabled, className, inCard = true, + testId, }: SettingsToggleProps) { const id = React.useId() @@ -73,6 +76,7 @@ export function SettingsToggle({ onCheckedChange={onCheckedChange} disabled={disabled} data-layout="settings-control" + data-testid={testId} className="ml-4 shrink-0" /> diff --git a/apps/electron/src/renderer/context/ReduceMotionContext.tsx b/apps/electron/src/renderer/context/ReduceMotionContext.tsx new file mode 100644 index 000000000..d0b5aa705 --- /dev/null +++ b/apps/electron/src/renderer/context/ReduceMotionContext.tsx @@ -0,0 +1,82 @@ +/** + * ReduceMotionContext + * + * App-wide "Reduce motion" accessibility preference. + * + * When enabled it: + * - renders the app inside `` so every + * `motion/react` component short-circuits its animations (MotionConfig + * propagates through React context to all `motion` components in the tree, + * including those in `packages/ui`); + * - sets `data-reduce-motion="true"` on `` so the global CSS guard in + * `index.css` can collapse plain CSS animations/transitions too. + * + * When disabled it uses `reducedMotion="user"`, which still honors the OS + * `prefers-reduced-motion` setting — a better default than always animating. + * + * The preference is persisted in `localStorage` (renderer-only, no backend), + * mirroring the other lightweight UI prefs in `lib/local-storage.ts`. + */ + +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, + type ReactNode, +} from 'react' +import { MotionConfig } from 'motion/react' +import * as storage from '@/lib/local-storage' + +interface ReduceMotionContextType { + reduceMotion: boolean + setReduceMotion: (value: boolean) => void +} + +const ReduceMotionContext = createContext(null) + +const REDUCE_MOTION_ATTR = 'data-reduce-motion' + +/** Reflect the preference onto so the global CSS guard can react. */ +function applyReduceMotionAttribute(enabled: boolean): void { + const root = document.documentElement + if (enabled) { + root.setAttribute(REDUCE_MOTION_ATTR, 'true') + } else { + root.removeAttribute(REDUCE_MOTION_ATTR) + } +} + +export function ReduceMotionProvider({ children }: { children: ReactNode }) { + const [reduceMotion, setReduceMotionState] = useState(() => + storage.get(storage.KEYS.reduceMotion, false), + ) + + // Keep the DOM attribute in sync (also covers the initial value on mount). + useEffect(() => { + applyReduceMotionAttribute(reduceMotion) + }, [reduceMotion]) + + const setReduceMotion = useCallback((value: boolean) => { + setReduceMotionState(value) + storage.set(storage.KEYS.reduceMotion, value) + applyReduceMotionAttribute(value) + }, []) + + return ( + + + {children} + + + ) +} + +export function useReduceMotion(): ReduceMotionContextType { + const ctx = useContext(ReduceMotionContext) + if (!ctx) { + throw new Error('useReduceMotion must be used within a ReduceMotionProvider') + } + return ctx +} diff --git a/apps/electron/src/renderer/index.css b/apps/electron/src/renderer/index.css index 2779d4d9e..5efb958b0 100644 --- a/apps/electron/src/renderer/index.css +++ b/apps/electron/src/renderer/index.css @@ -1371,3 +1371,18 @@ html.dark[data-scenic] .fullscreen-overlay-background { min-height: 31px; padding: 6px 8px; } + +/* Reduce motion: when the user enables "Reduce motion" in Appearance settings, + near-instantly complete CSS animations/transitions across the app. This + complements `motion/react`'s MotionConfig reducedMotion="always" (which handles + `motion` components) by covering plain CSS effects too. */ +[data-reduce-motion='true'] *, +[data-reduce-motion='true'] *::before, +[data-reduce-motion='true'] *::after { + animation-duration: 0.001ms !important; + animation-delay: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + transition-delay: 0ms !important; + scroll-behavior: auto !important; +} diff --git a/apps/electron/src/renderer/lib/local-storage.ts b/apps/electron/src/renderer/lib/local-storage.ts index 8381b07ea..4d6446afd 100644 --- a/apps/electron/src/renderer/lib/local-storage.ts +++ b/apps/electron/src/renderer/lib/local-storage.ts @@ -52,6 +52,7 @@ export const KEYS = { // Appearance showConnectionIcons: 'show-connection-icons', + reduceMotion: 'reduce-motion', // Minimize animations/transitions app-wide // What's New whatsNewLastSeenVersion: 'whats-new-last-seen-version', diff --git a/apps/electron/src/renderer/main.tsx b/apps/electron/src/renderer/main.tsx index 9ead80381..14729366d 100644 --- a/apps/electron/src/renderer/main.tsx +++ b/apps/electron/src/renderer/main.tsx @@ -6,6 +6,7 @@ import { captureConsoleIntegration } from '@sentry/react' import { Provider as JotaiProvider, useAtomValue } from 'jotai' import App from './App' import { ThemeProvider } from './context/ThemeContext' +import { ReduceMotionProvider } from './context/ReduceMotionContext' import { windowWorkspaceIdAtom } from './atoms/sessions' import { Toaster } from '@/components/ui/sonner' import { PetWindowController } from '@/components/pet/PetWindowController' @@ -106,9 +107,11 @@ function Root() { return ( - - - + + + + + ) } diff --git a/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx b/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx index 874a7995f..4da9ba24c 100644 --- a/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx +++ b/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx @@ -14,6 +14,7 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { HeaderMenu } from '@/components/ui/HeaderMenu' import { EditPopover, EditButton, getEditConfig } from '@/components/ui/EditPopover' import { useTheme } from '@/context/ThemeContext' +import { useReduceMotion } from '@/context/ReduceMotionContext' import { useAppShellContext } from '@/context/AppShellContext' import { routes } from '@/lib/navigate' import { FolderOpen, Monitor, RefreshCw, Sun, Moon } from 'lucide-react' @@ -139,6 +140,9 @@ export default function AppearanceSettingsPage() { await window.electronAPI?.setRichToolDescriptions?.(checked) }, []) + // Reduce motion toggle (renderer-only preference, persisted in localStorage) + const { reduceMotion, setReduceMotion } = useReduceMotion() + // Pet companion settings + custom pets (synced via shared Jotai atoms) const { pets, @@ -376,6 +380,13 @@ export default function AppearanceSettingsPage() { checked={richToolDescriptions} onCheckedChange={handleRichToolDescriptionsChange} /> + diff --git a/apps/electron/src/renderer/pages/settings/SettingsNavigator.tsx b/apps/electron/src/renderer/pages/settings/SettingsNavigator.tsx index 435a7f6e3..f62509111 100644 --- a/apps/electron/src/renderer/pages/settings/SettingsNavigator.tsx +++ b/apps/electron/src/renderer/pages/settings/SettingsNavigator.tsx @@ -67,7 +67,11 @@ function SettingsItemRow({ item, isSelected, isFirst, onSelect }: SettingsItemRo } return ( -
+
{/* Separator - only show if not first */} {!isFirst && (
diff --git a/docs/loop/feature-ledger.md b/docs/loop/feature-ledger.md index d992798ab..127d4351c 100644 --- a/docs/loop/feature-ledger.md +++ b/docs/loop/feature-ledger.md @@ -32,6 +32,9 @@ log, not the system of record. | slug | title | source | feasibility | status | issue | pr | branch | updated | notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | pr-open | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-02 | `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). typecheck/`bun test` zero-delta vs main (3578 pass/56 fail on both); renderer build ✅. **CDP could not run locally**: this sandbox's egress policy 403s the Electron binary download and the `libsignal` GitHub dep (WhatsApp worker), so the app can't be built/launched here — assertion included for CI/reviewer. | +| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | in-progress | [#50](https://github.com/modelstudioai/openwork/issues/50) | — | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `` + `data-reduce-motion` on `` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. | +| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. | +| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. | +| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). | | command-palette | Global command palette (⌘K/Ctrl+K) to search & run any action | Claude Code Desktop ⌘K / VS Code & Codex ⌘⇧P / Linear ⌘K | frontend-only | merged | [#41](https://github.com/modelstudioai/openwork/issues/41) | [#42](https://github.com/modelstudioai/openwork/pull/42) | loop/command-palette | 2026-07-02 | Merged into `main`. Reuses action registry `execute()` + cmdk primitives; zero new i18n keys. CDP e2e 2/2 pass. typecheck/test +0 vs main. | | settings-search | Searchable/filterable settings navigation | Claude Code Desktop / VS Code / Codex desktop settings search | frontend-only | merged | [#39](https://github.com/modelstudioai/openwork/issues/39) | [#40](https://github.com/modelstudioai/openwork/pull/40) | loop/settings-search | 2026-07-01 | Merged into `main`. Filters `SettingsNavigator` by title+description; reuses `common.search`/`common.noResultsFound` (no new locale keys). Also hardened `e2e/app.ts` teardown (per-launch profile dir + setsid process-group kill) so multiple CDP assertions run under headless xvfb. | diff --git a/e2e/assertions/reduce-motion.assert.ts b/e2e/assertions/reduce-motion.assert.ts new file mode 100644 index 000000000..99b7ed881 --- /dev/null +++ b/e2e/assertions/reduce-motion.assert.ts @@ -0,0 +1,99 @@ +/** + * Feature assertion: the "Reduce motion" toggle in Settings → Appearance + * actually applies and persists an app-wide reduced-motion preference. + * + * Drives the real UI over CDP entirely in the draft/no-session state (no seeded + * conversation, no backend connection): opens Settings → Appearance, flips the + * toggle, and asserts the observable effects — the switch state, the + * `data-reduce-motion` attribute on , and the persisted localStorage value. + * Toggling twice proves it both applies and reverts, not merely renders. + */ + +import type { Assertion } from '../runner'; + +const SETTINGS_NAV = '[data-testid="nav:settings"]'; +const APPEARANCE_NAV = '[data-testid="settings-nav-appearance"]'; +const TOGGLE = '[data-testid="reduce-motion-toggle"]'; +const STORAGE_KEY = 'craft-reduce-motion'; + +/** Read the toggle's aria-checked ("true" | "false" | null). */ +function ariaCheckedExpr(): string { + return `(() => { + const el = document.querySelector(${JSON.stringify(TOGGLE)}); + return el ? el.getAttribute('aria-checked') : null; + })()`; +} + +/** True when carries the reduce-motion marker attribute. */ +function htmlMarkedExpr(): string { + return `document.documentElement.getAttribute('data-reduce-motion') === 'true'`; +} + +/** The persisted localStorage value for the preference. */ +function storedValueExpr(): string { + return `window.localStorage.getItem(${JSON.stringify(STORAGE_KEY)})`; +} + +const assertion: Assertion = { + name: 'reduce-motion toggle applies and persists an app-wide preference', + async run(app) { + const { session } = app; + + // App fully mounted. + await session.waitForFunction( + '!document.getElementById("_loader") && (document.getElementById("root")?.childElementCount ?? 0) > 0', + { timeoutMs: 30000, message: 'app did not mount' }, + ); + + // Open Settings → Appearance (real user path). + await session.click(SETTINGS_NAV, { timeoutMs: 15000 }); + await session.click(APPEARANCE_NAV, { timeoutMs: 15000 }); + + // The toggle is the feature under test — its presence is the first signal. + await session.waitForSelector(TOGGLE, { + timeoutMs: 15000, + message: 'reduce-motion toggle did not render', + }); + + // Initial state: off, no marker on , not persisted true. + const initialChecked = await session.evaluate(ariaCheckedExpr()); + if (initialChecked !== 'false') { + throw new Error(`expected toggle off initially, saw aria-checked=${initialChecked}`); + } + if (await session.evaluate(htmlMarkedExpr())) { + throw new Error('expected no data-reduce-motion attribute before enabling'); + } + + // Enable → toggle on, marked, persisted true. + await session.click(TOGGLE); + await session.waitForFunction( + `${ariaCheckedExpr()} === 'true'`, + { timeoutMs: 5000, message: 'toggle did not switch on' }, + ); + await session.waitForFunction(htmlMarkedExpr(), { + timeoutMs: 5000, + message: 'data-reduce-motion was not applied to when enabled', + }); + const storedOn = await session.evaluate(storedValueExpr()); + if (storedOn !== 'true') { + throw new Error(`expected persisted "true" after enabling, saw ${JSON.stringify(storedOn)}`); + } + + // Disable → toggle off, marker removed, persisted false. + await session.click(TOGGLE); + await session.waitForFunction( + `${ariaCheckedExpr()} === 'false'`, + { timeoutMs: 5000, message: 'toggle did not switch off' }, + ); + await session.waitForFunction(`!(${htmlMarkedExpr()})`, { + timeoutMs: 5000, + message: 'data-reduce-motion was not removed from when disabled', + }); + const storedOff = await session.evaluate(storedValueExpr()); + if (storedOff !== 'false') { + throw new Error(`expected persisted "false" after disabling, saw ${JSON.stringify(storedOff)}`); + } + }, +}; + +export default assertion; diff --git a/packages/shared/src/i18n/locales/de.json b/packages/shared/src/i18n/locales/de.json index ad23be300..d407e5dbb 100644 --- a/packages/shared/src/i18n/locales/de.json +++ b/packages/shared/src/i18n/locales/de.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "Modus", "settings.appearance.noToolIcons": "Keine Werkzeugsymbol-Zuordnungen gefunden", "settings.appearance.richToolDescriptions": "Ausführliche Werkzeugbeschreibungen", + "settings.appearance.reduceMotion": "Bewegung reduzieren", + "settings.appearance.reduceMotionDesc": "Animationen und Übergänge in der gesamten App minimieren.", "settings.appearance.pet": "Begleiter", "settings.appearance.petDesc": "Ein Begleiter, der auf die Aktivität des Agents reagiert.", "settings.appearance.petEnabled": "Begleiter anzeigen", diff --git a/packages/shared/src/i18n/locales/en.json b/packages/shared/src/i18n/locales/en.json index 515571e0a..6a06e456e 100644 --- a/packages/shared/src/i18n/locales/en.json +++ b/packages/shared/src/i18n/locales/en.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "Mode", "settings.appearance.noToolIcons": "No tool icon mappings found", "settings.appearance.richToolDescriptions": "Rich tool descriptions", + "settings.appearance.reduceMotion": "Reduce motion", + "settings.appearance.reduceMotionDesc": "Minimize animations and transitions throughout the app.", "settings.appearance.pet": "Pet", "settings.appearance.petDesc": "A companion that reacts to what the agent is doing.", "settings.appearance.petEnabled": "Show pet companion", diff --git a/packages/shared/src/i18n/locales/es.json b/packages/shared/src/i18n/locales/es.json index 0ae1c140c..6461320a6 100644 --- a/packages/shared/src/i18n/locales/es.json +++ b/packages/shared/src/i18n/locales/es.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "Modo", "settings.appearance.noToolIcons": "No se encontraron asignaciones de iconos de herramientas", "settings.appearance.richToolDescriptions": "Descripciones detalladas de herramientas", + "settings.appearance.reduceMotion": "Reducir movimiento", + "settings.appearance.reduceMotionDesc": "Minimiza las animaciones y transiciones en toda la aplicación.", "settings.appearance.pet": "Mascota", "settings.appearance.petDesc": "Un compañero que reacciona a lo que hace el agente.", "settings.appearance.petEnabled": "Mostrar mascota", diff --git a/packages/shared/src/i18n/locales/hu.json b/packages/shared/src/i18n/locales/hu.json index 9f613dd4f..641c6d16f 100644 --- a/packages/shared/src/i18n/locales/hu.json +++ b/packages/shared/src/i18n/locales/hu.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "Mód", "settings.appearance.noToolIcons": "Nem található eszközikon-hozzárendelés", "settings.appearance.richToolDescriptions": "Részletes eszközleírások", + "settings.appearance.reduceMotion": "Mozgás csökkentése", + "settings.appearance.reduceMotionDesc": "Az animációk és átmenetek minimalizálása az egész alkalmazásban.", "settings.appearance.pet": "Kabala", "settings.appearance.petDesc": "Egy társ, aki reagál arra, amit az ügynök csinál.", "settings.appearance.petEnabled": "Kabala megjelenítése", diff --git a/packages/shared/src/i18n/locales/ja.json b/packages/shared/src/i18n/locales/ja.json index 6c09b71d4..5e2191990 100644 --- a/packages/shared/src/i18n/locales/ja.json +++ b/packages/shared/src/i18n/locales/ja.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "モード", "settings.appearance.noToolIcons": "ツールアイコンのマッピングが見つかりません", "settings.appearance.richToolDescriptions": "リッチなツール説明", + "settings.appearance.reduceMotion": "モーションを減らす", + "settings.appearance.reduceMotionDesc": "アプリ全体のアニメーションとトランジションを最小限にします。", "settings.appearance.pet": "ペット", "settings.appearance.petDesc": "エージェントの動きに反応するコンパニオン。", "settings.appearance.petEnabled": "ペットを表示", diff --git a/packages/shared/src/i18n/locales/pl.json b/packages/shared/src/i18n/locales/pl.json index 049c922ce..f85aa9c9f 100644 --- a/packages/shared/src/i18n/locales/pl.json +++ b/packages/shared/src/i18n/locales/pl.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "Tryb", "settings.appearance.noToolIcons": "Nie znaleziono mapowań ikon narzędzi", "settings.appearance.richToolDescriptions": "Rozbudowane opisy narzędzi", + "settings.appearance.reduceMotion": "Ogranicz ruch", + "settings.appearance.reduceMotionDesc": "Ogranicz animacje i przejścia w całej aplikacji.", "settings.appearance.pet": "Maskotka", "settings.appearance.petDesc": "Towarzysz reagujący na to, co robi agent.", "settings.appearance.petEnabled": "Pokaż maskotkę", diff --git a/packages/shared/src/i18n/locales/zh-Hans.json b/packages/shared/src/i18n/locales/zh-Hans.json index 1879d2df7..845231913 100644 --- a/packages/shared/src/i18n/locales/zh-Hans.json +++ b/packages/shared/src/i18n/locales/zh-Hans.json @@ -785,6 +785,8 @@ "settings.appearance.mode": "模式", "settings.appearance.noToolIcons": "未找到工具图标映射", "settings.appearance.richToolDescriptions": "丰富的工具描述", + "settings.appearance.reduceMotion": "减少动态效果", + "settings.appearance.reduceMotionDesc": "在整个应用中尽量减少动画和过渡效果。", "settings.appearance.pet": "宠物", "settings.appearance.petDesc": "一个会根据 agent 当前状态做出反应的小伙伴。", "settings.appearance.petEnabled": "显示宠物伙伴", From 9d93c9ecc63aa2f4bd216b243360209a751cb243 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 13:41:24 +0000 Subject: [PATCH 2/2] docs(loop): mark reduce-motion pr-open (#51) --- docs/loop/feature-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/loop/feature-ledger.md b/docs/loop/feature-ledger.md index 127d4351c..51235562a 100644 --- a/docs/loop/feature-ledger.md +++ b/docs/loop/feature-ledger.md @@ -32,7 +32,7 @@ log, not the system of record. | slug | title | source | feasibility | status | issue | pr | branch | updated | notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | in-progress | [#50](https://github.com/modelstudioai/openwork/issues/50) | — | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `` + `data-reduce-motion` on `` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. | +| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `` + `data-reduce-motion` on `` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion included; **could not run locally** (egress 403s Electron binary download). | | composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. | | scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. | | thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). |