diff --git a/components/moderation/MarketStatusBadge.tsx b/components/moderation/MarketStatusBadge.tsx index 8ffb8ea2..71347077 100644 --- a/components/moderation/MarketStatusBadge.tsx +++ b/components/moderation/MarketStatusBadge.tsx @@ -37,6 +37,7 @@ export function MarketStatusBadge({ state, className, showTooltip = true }: Mark removed: 'neutral', resolving: 'info', }; + const badge = ( diff --git a/components/moderation/__tests__/MarketStatusBadge.test.tsx b/components/moderation/__tests__/MarketStatusBadge.test.tsx index 73cede0f..6da2c891 100644 --- a/components/moderation/__tests__/MarketStatusBadge.test.tsx +++ b/components/moderation/__tests__/MarketStatusBadge.test.tsx @@ -2,12 +2,16 @@ import { render, screen } from '@testing-library/react'; import { MarketStatusBadge } from '../MarketStatusBadge'; import type { ModerationState } from '@/types/moderation'; +// The animation class uses the Tailwind `motion-safe:` variant so the pulse is +// suppressed automatically when the user has enabled "prefers-reduced-motion". +const PULSE_CLASS = 'motion-safe:animate-status-live-pulse'; + describe('MarketStatusBadge', () => { it('renders the resolving badge with a live glow-pulse class', () => { render(); const badge = screen.getByRole('status'); - expect(badge).toHaveClass('animate-status-live-pulse'); + expect(badge).toHaveClass(PULSE_CLASS); }); it('announces "Resolving now" in the aria-label for the resolving state', () => { @@ -23,7 +27,7 @@ describe('MarketStatusBadge', () => { render(); const badge = screen.getByRole('status'); - expect(badge).not.toHaveClass('animate-status-live-pulse'); + expect(badge).not.toHaveClass(PULSE_CLASS); expect(badge.getAttribute('aria-label')).not.toContain('Resolving now'); } ); @@ -32,12 +36,12 @@ describe('MarketStatusBadge', () => { const { rerender } = render(); let badge = screen.getByRole('status'); - expect(badge).toHaveClass('animate-status-live-pulse'); + expect(badge).toHaveClass(PULSE_CLASS); rerender(); badge = screen.getByRole('status'); - expect(badge).not.toHaveClass('animate-status-live-pulse'); + expect(badge).not.toHaveClass(PULSE_CLASS); expect(badge.getAttribute('aria-label')).not.toContain('Resolving now'); }); }); diff --git a/tailwind.config.ts b/tailwind.config.ts index 1dacc435..1d08107a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -124,11 +124,21 @@ const config: Config = { to: { height: '0' } - } + }, + // Subtle glow-pulse used by MarketStatusBadge on the `resolving` state. + // Respects prefers-reduced-motion via the Tailwind motion-safe variant. + 'status-live-pulse': { + '0%, 100%': { opacity: '1', boxShadow: '0 0 0 0 transparent' }, + '50%': { opacity: '0.75', boxShadow: '0 0 0 4px hsl(var(--ring) / 0.35)' }, + }, }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' + 'accordion-up': 'accordion-up 0.2s ease-out', + // 2 s loop, ease-in-out — intentionally gentle so it doesn't distract. + // The `motion-safe:` prefix in the component ensures it is skipped when + // the user has "reduce motion" enabled in their OS accessibility settings. + 'status-live-pulse': 'status-live-pulse 2s ease-in-out infinite', } } },