fix(a11y): stop OS Reduce Motion from freezing and hiding answer progress - #2046
Conversation
…ress The answer-loading panel was reported dead on a physical iPhone in both Safari and the installed PWA. Three prior PRs (#1974, #1989, #1995) diagnosed it as a Mobile WebKit repaint bug and all three failed on the device. The cause was this app's own reduced-motion CSS: with iOS Reduce Motion on, the universal suppression rule froze every animation on the page (including the step spinner) and the ECG trace was additionally set to `opacity: 0`, so the only progress indicator disappeared entirely. Every gate stayed green because playwright.config.ts applies `reducedMotion: "reduce"` suite-wide while the one spec asserting the animation opts out to "no-preference" first, so the default user configuration was never exercised. The only WebKit project is Desktop Safari; there was no mobile-WebKit coverage at all. Three changes: 1. Suppressing motion no longer deletes the indicator. The reduced-motion rules stop the animation but leave a legible static trace instead of `opacity: 0`. 2. New tri-state Motion preference (System / Reduced / Full). "Full" is an explicit opt-in that overrides an OS Reduce Motion request, because iOS Reduce Motion is commonly enabled for app-launch zoom rather than vestibular sensitivity and there was no in-app way to get the feedback back. Default stays "system", so nobody silently gains motion. The `motion-reduce`/`motion-safe` Tailwind variants are redefined via @custom-variant so the override reaches all 164 call sites. 3. The pulse becomes a travelling sweep. The previous whole-line opacity fade (0.2 -> 1.0 over 1.8s) technically ran but was too subtle on a phone hairline to read as motion. It is now a continuously scrolling ECG strip driven only by `transform: translateX`, reusing the compositor recipe `.animate-skeleton-shimmer::after` already relies on. Adds tests/ui-phone-motion.spec.ts covering the exact reported configuration (phone + OS Reduce Motion), which nothing covered before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYndHWrYJzirxbBvt68Tmx
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYndHWrYJzirxbBvt68Tmx
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 15 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 95 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (21)
Comment |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36221231 | Triggered | Generic High Entropy Secret | e9e501d | tests/rag-adversarial-fixtures.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Four inbox requests from the PR #2046 investigation. They travel with their owning product PR rather than a dedicated ledger-only branch, per the AGENTS.md PR-bundling rule. - P2 issue: playwright.config.ts applies reducedMotion:"reduce" suite-wide, so no gate reflects the default user configuration. That inversion is why three PRs shipped green against a broken phone. - P2 task: no mobile-WebKit or display-mode:standalone Playwright project; phone coverage is a narrow viewport on desktop engines, while the only phone-AND-standalone CSS in the repo goes untested. - P3 task: physical iPhone acceptance still owed for this fix, in Safari and the installed PWA with Motion set to Full. - P3 rec: three dead spring easing tokens in globals.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYndHWrYJzirxbBvt68Tmx
…uooj4' into claude/phone-pwa-animation-bug-suooj4
…uooj4' into claude/phone-pwa-animation-bug-suooj4
CI status: one red check, and it is not this diffEvery required check is green — The single failure is GitGuardian Security Checks: Why I am not fixing itIt is not in this PR's changes. That file was introduced by #2036 ( It is pre-existing and was already merged past. #2036 itself completed with It reads as a false positive. The flagged literal is the synthetic canary token Editing #2036's test to satisfy the scanner would weaken a deliberate contract in someone else's just-merged packet, and is outside this PR's scope, so I have left it alone. Suggested remediation (operator action)Triage occurrence Reminder on what this PR still does not provePhysical iPhone acceptance is still owed: Safari and the installed PWA, with Settings → Motion set to Generated by Claude Code |
|
Summary
globals.cssfroze every animation on the page — including the step spinner — and the ECG trace was additionally set toopacity: 0, so the only progress indicator disappeared entirely. Suppressing motion now stops the animation but leaves a legible static trace instead of deleting it.Fullis an explicit opt-in that overrides an OS Reduce Motion request. iOS Reduce Motion is commonly enabled to stop app-launch zoom rather than out of vestibular sensitivity, and there was previously no in-app way to get the feedback back. The default stayssystem, so no existing user silently gains motion, and the existingreducedopt-out still wins over an OS with no preference. Themotion-reduce/motion-safeTailwind variants are redefined via@custom-variantso the override reaches all 164 call sites without editing them.transform: translateX, reusing the compositor recipe.animate-skeleton-shimmer::afteralready relies on in this app.playwright.config.tsappliesreducedMotion: "reduce"suite-wide while the one spec asserting the animation opts out tono-preferencefirst, so the default user configuration was never exercised. The only WebKit project is Desktop Safari; there was no mobile-WebKit coverage at all. Newtests/ui-phone-motion.spec.tscovers the exact reported configuration.Verification
npm run verify:pr-localExit 0. Summary line:
completed: check:runtime, check:installed-lock-parity, format:changed, lint, typecheck, test, build, eval:rag:offline, check:medication-interactions, check:medication-lexicon-report/failed: (none)/not reached: (none). Unit suite within it:Test Files 640 passed (640)/Tests 6832 passed | 4 skipped (6836).npm run verify:cheapwas used as the iteration gate and also exited 0 with the same 640/6832 result.npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changedUI verification not run: the container has Chromium revision 1194 but this Playwright pins 1234, so
check:playwright-browser-revisionfailsnot-installedand the repo's runner refuses to start. This is the known Playwright/image drift tracked as#255, and repo policy is to delegate browser proof to CI Production UI rather than force a mismatched binary. The two directly affected specs were still executed against the available Chromium as a labelled diagnostic, not as a gate claim:tests/ui-phone-motion.spec.ts—3 passed (4.8s)tests/answer-progress-ui-smoke.spec.ts—4 passed (20.9s)Computed style was also probed directly across all four motion states on a phone viewport, which is the evidence that confirms the diagnosis:
noneanswer-ecg-scroll3.2sspin1snoneanswer-ecg-scroll3.2sspin1sBefore this change, row 1 produced
opacity: 0— the blank, frozen panel in the bug report — and row 2 was unreachable.npm run verify:releasebefore release or handoff confidence claimsNot run: no release or handoff confidence is claimed here, and it is provider-backed.
npm run eval:retrieval:quality(must stay 36/36) when retrieval, ranking, selection, chunking, or scoring behavior changedNot applicable: no retrieval, ranking, selection, chunking, or scoring code is touched.
classifyPullRequestFilesreturnsragRanking: falsefor this diff.npm run eval:rag -- --limit 15+npm run eval:quality -- --rag-onlywhen answer generation, the synthesis prompt, or answer post-processing changedNot applicable: answer generation is untouched. This change affects only how the progress panel renders while an answer is being produced.
eval:rag:offlineran insideverify:pr-localand passed.npm run check:production-readinesswhen clinical workflow, privacy, environment, Supabase, source governance, or deployment behavior changedExit 0 with 2 PASS / 5 WARN / 2 FAIL. Both FAIL entries are the documented offline provider capability gap in this container (
Missing server environment variables: NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEYandMissing OPENAI_API_KEY), not a regression introduced by this diff.npm run check:deployment-readinesswhen deployment startup, hosting, or rollout behavior changedNot applicable: no deployment startup, hosting, or rollout behavior is touched.
Risk and rollout
motion-reduce/motion-safevariants are now scoped byhtml:not([data-motion="full"]), which raises their specificity by one attribute selector across every call site. Amotion-reduce:utility that previously lost a specificity tie could now win; since those utilities exist to suppress motion, winning is the intended direction, but it is a repo-wide selector change rather than a local one. The compiled output was inspected directly to confirm both@custom-variantbranches emit correctly and that the--animate-answer-ecgtheme token and@keyframes answer-ecg-scrollsurvive Tailwind's tree-shaking. The animation redesign itself is contained to one component and its CSS. No clinical content, retrieval, ranking, answer text, or document access is touched.preferencesjsonbcolumn with no migration and no DB enum, so a revert leaves any stored"full"value to be coerced back to"system"bynormalizePreferences, which is already covered by a test. Nothing else persists.check:production-readinessis offline and reported its FAIL entries as the expected missing-credential capability gap.Clinical Governance Preflight
Complete this section when the change touches ingestion, answer generation, search/ranking, source rendering, document access, privacy, production env, or clinical output.
Clinical KB Database(sjrfecxgysukkwxsowpy)This diff changes only progress-indicator presentation and a user motion preference. It does not alter answer generation, citation rendering, source governance, retrieval, or document access, so none of the clinical behaviours above move.
Notes
Full— is the acceptance step, and it is exactly the gap that let three previous PRs be declared fixed while the phone stayed broken.tests/ui-phone-motion.spec.tshad to be registered in three places that keep duplicate copies of the production spec allow-list:playwright.config.ts(testMatchandproductionSpecPattern) andscripts/playwright-pr-shards.mjs(productionSpecFilePatternplus a shard-membership row). Its shard timing is a local measurement (~5s) pending a hosted timing refresh.tests/settings-inert-preferences.dom.test.tsxnow expectssettings-row-motion, andtests/ui-smoke.spec.tsmoves Motion into the existing radiogroup geometry loop and re-points its 48px switch tap-target assertion atsettings-row-recent-searches-on-home, the nearest remaining boolean row./issues: the suite-widereducedMotion: "reduce"inplaywright.config.tsmeans no gate reflects default user configuration; there is still no mobile-WebKit ordisplay-mode: standalonePlaywright project; and--spring-tight/--spring-bouncy/--spring-gentleinglobals.cssare dead tokens with zerovar()and zero utility usage.Generated by Claude Code