From 7aba71490a3111ab1cf1560a5036794728dac74b Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 12 Sep 2026 20:28:41 -0400 Subject: [PATCH] fix(a11y): reduced-motion for spinner/call-avatar; tap-to-skip boot; AA accent contrast - SendingSpinClass and CallAvatarAnimation respect prefers-reduced-motion (static opacity fallback), matching MsgAppearClass. - Terminal boot overlay dismisses on click/tap and is aria-hidden. - Primary.Main: Midnight #6b7ca8 -> #6f80aa (4.29 -> 4.51:1), Lotus Terminal Light #c44e00 -> #ba4a00 (4.14 -> 4.52:1), hue preserved. Fixes #84 Fixes #86 Fixes #52 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/styles/Animations.css.ts | 35 +++++++++++++++++++++++++------- src/colors.css.ts | 8 ++++++-- src/lotus-boot.ts | 6 ++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/app/styles/Animations.css.ts b/src/app/styles/Animations.css.ts index 30c1aad36..222168c60 100644 --- a/src/app/styles/Animations.css.ts +++ b/src/app/styles/Animations.css.ts @@ -36,25 +36,46 @@ const glowPulse = keyframes({ }, }); +// Wobble/glow/spin only run under the same no-preference guard as +// MsgAppearClass below; prefers-reduced-motion users get a static state. export const WobbleAnimation = style({ - animation: `${wobble} 2000ms ease-in-out`, - animationIterationCount: 'infinite', + '@media': { + '(prefers-reduced-motion: no-preference)': { + animation: `${wobble} 2000ms ease-in-out`, + animationIterationCount: 'infinite', + }, + }, }); export const GlowAnimation = style({ - animation: `${glowPulse} 2000ms ease-out`, - animationIterationCount: 'infinite', + '@media': { + '(prefers-reduced-motion: no-preference)': { + animation: `${glowPulse} 2000ms ease-out`, + animationIterationCount: 'infinite', + }, + }, }); export const CallAvatarAnimation = style({ - animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`, - animationIterationCount: 'infinite', + '@media': { + '(prefers-reduced-motion: no-preference)': { + animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`, + animationIterationCount: 'infinite', + }, + }, }); export const SendingSpinClass = style({ display: 'inline-block', - animation: `${spin} 900ms linear infinite`, transformOrigin: 'center', + // Static fallback: dim instead of spinning when motion is reduced. + opacity: 0.6, + '@media': { + '(prefers-reduced-motion: no-preference)': { + animation: `${spin} 900ms linear infinite`, + opacity: 1, + }, + }, }); const msgAppearKeyframe = keyframes({ diff --git a/src/colors.css.ts b/src/colors.css.ts index f917843cf..397a41330 100644 --- a/src/colors.css.ts +++ b/src/colors.css.ts @@ -500,7 +500,9 @@ export const midnightTheme = createTheme(color, { }, Primary: { - Main: '#6b7ca8', + // #6b7ca8 measured 4.29:1 against Background.Container (#111827), below + // WCAG AA 4.5:1. Lightened along the same hue/saturation to 4.51:1. + Main: '#6f80aa', MainHover: '#7989b1', MainActive: '#8493b8', MainLine: '#8f9dbf', @@ -651,7 +653,9 @@ export const lotusTerminalLightTheme = createTheme(color, { OnContainer: '#2d3d56', }, Primary: { - Main: '#c44e00', + // #c44e00 measured 4.14:1 against Background.Container (#edf0f5), below + // WCAG AA 4.5:1. Darkened along the same hue/saturation to 4.52:1. + Main: '#ba4a00', MainHover: '#a33f00', MainActive: '#8f3700', MainLine: '#d45800', diff --git a/src/lotus-boot.ts b/src/lotus-boot.ts index 391352b79..1f97c3446 100644 --- a/src/lotus-boot.ts +++ b/src/lotus-boot.ts @@ -34,6 +34,8 @@ export function runLotusBootSequence(force = false): void { const overlay = document.createElement('div'); overlay.id = 'lt-boot'; + // Decorative boot animation, not content — hide it from assistive tech. + overlay.setAttribute('aria-hidden', 'true'); overlay.style.cssText = [ 'position:fixed', 'inset:0', @@ -79,6 +81,7 @@ export function runLotusBootSequence(force = false): void { const dismiss = (): void => { clearInterval(interval); document.removeEventListener('keydown', onKey); + overlay.removeEventListener('click', dismiss); overlay.style.transition = 'opacity 0.4s ease'; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 400); @@ -88,6 +91,9 @@ export function runLotusBootSequence(force = false): void { if (e.key === 'Escape') dismiss(); }; document.addEventListener('keydown', onKey); + // No touch/click dismiss previously existed — mouse/touch users had no way + // to skip besides the keyboard-only Escape hint. + overlay.addEventListener('click', dismiss); let i = 0; let text = '';