Files
sofa/app/(pages)/titles/[id]/_components/status-button.tsx
T
jakeandClaude Opus 4.6 fc982bee1b Comprehensive Web Interface Guidelines audit: accessibility, motion, and typography
- Add global MotionConfig reducedMotion="user" in root layout
- Add color-scheme: dark, theme-color meta, skip-to-main-content link
- Add aria-hidden={true} to ~60+ decorative icons across 33 files
- Add aria-label to all icon-only buttons (logout, star rating, play, delete, etc.)
- Replace transition-all with specific properties (11 files)
- Add motion-safe: prefix for CSS hover transforms and animate-pulse
- Add prefers-reduced-motion: reduce override in globals.css
- Add useReducedMotion guard to status-dot pulsing animation
- Fix focus-visible styles on auth form inputs (focus → focus-visible, stronger ring)
- Add text-balance to all headings (13 files)
- Replace "..." with "…" (U+2026) in all loading/placeholder text
- Add autocomplete, spellCheck, role="alert" to auth form
- Add aria-label to Switch controls and filmography select
- Fix heading hierarchy (h3 → h2 where h2 was skipped)
- Add break-words to title overview, overscroll-contain to drawer
- Add confirmation dialog for destructive library removal
- Add group-focus-within:opacity-100 for keyboard-accessible backup actions
- Add role="radio" + aria-checked to star rating buttons
- Add aria-label to carousel region, role="img" to TMDB logo SVG
- Add text-foreground to native select for dark mode consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:20:25 -05:00

91 lines
3.0 KiB
TypeScript

"use client";
import {
IconCheck,
IconPlayerPlayFilled,
IconPlus,
IconX,
} from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react";
const watchingStyle = {
label: "Watching",
icon: IconPlayerPlayFilled,
class: "text-status-watching",
bgClass: "bg-status-watching/10 hover:bg-status-watching/15",
borderClass: "ring-status-watching/20",
};
const statusConfig = {
watchlist: watchingStyle,
in_progress: watchingStyle,
completed: {
label: "Completed",
icon: IconCheck,
class: "text-status-completed",
bgClass: "bg-status-completed/10 hover:bg-status-completed/15",
borderClass: "ring-status-completed/20",
},
} as const;
interface StatusButtonProps {
currentStatus: string | null;
onChange: (status: string | null) => void;
}
export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
const config =
statusConfig[currentStatus as keyof typeof statusConfig] ?? null;
return (
<AnimatePresence mode="wait" initial={false}>
{!config ? (
<motion.button
key="add"
type="button"
onClick={() => onChange("watchlist")}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }}
className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]"
>
<IconPlus aria-hidden={true} className="size-3.5" strokeWidth={2.5} />
Watchlist
</motion.button>
) : (
<motion.button
key="status"
type="button"
onClick={() => onChange(null)}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }}
title="Remove from library"
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:ring-destructive/30 hover:bg-destructive/10 hover:text-destructive`}
>
<span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1">
<config.icon
aria-hidden={true}
className="size-3.5 transition-opacity group-hover:opacity-0"
/>
<IconX
aria-hidden={true}
className="size-3.5 opacity-0 text-destructive transition-opacity group-hover:opacity-100"
/>
</span>
<span className="grid [&>span]:col-start-1 [&>span]:row-start-1">
<span className="transition-opacity group-hover:opacity-0">
{config.label}
</span>
<span className="opacity-0 transition-opacity group-hover:opacity-100">
Remove
</span>
</span>
</motion.button>
)}
</AnimatePresence>
);
}