Files
sofa/components/status-dot.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

88 lines
2.1 KiB
TypeScript

"use client";
import { motion, useReducedMotion } from "motion/react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
const colors = {
ok: { bg: "bg-green-500", shadow: "0 0 0 0px rgba(74,222,128,0.5)" },
error: { bg: "bg-destructive", shadow: "0 0 0 0px rgba(248,113,113,0.5)" },
warn: { bg: "bg-amber-500", shadow: "0 0 0 0px rgba(251,191,36,0.5)" },
inactive: { bg: "bg-muted-foreground/30", shadow: "" },
};
const pulseColors = {
ok: "0 0 0 4px rgba(74,222,128,0)",
error: "0 0 0 4px rgba(248,113,113,0)",
warn: "0 0 0 4px rgba(251,191,36,0)",
};
const defaultLabels: Record<string, string> = {
ok: "Healthy",
error: "Error",
warn: "Warning",
inactive: "Inactive",
};
/** Small colored status dot with a pulsing ring for active states. */
export function StatusDot({
status,
label,
className,
}: {
status: "ok" | "error" | "warn" | "inactive";
/** Tooltip text. Defaults to a label derived from the status. */
label?: string;
className?: string;
}) {
const { bg, shadow } = colors[status];
const pulse = status !== "inactive";
const tooltipText = label ?? defaultLabels[status];
const prefersReducedMotion = useReducedMotion();
const dotEl = pulse ? (
<motion.span
className={cn(
"inline-block h-2 w-2 shrink-0 rounded-full",
bg,
className,
)}
animate={
prefersReducedMotion
? {}
: {
boxShadow: [
shadow,
pulseColors[status as keyof typeof pulseColors],
],
}
}
transition={{
duration: 1.2,
repeat: Number.POSITIVE_INFINITY,
ease: "easeOut",
repeatDelay: 0.3,
}}
/>
) : (
<span
className={cn(
"inline-block h-2 w-2 shrink-0 rounded-full",
bg,
className,
)}
/>
);
return (
<Tooltip>
<TooltipTrigger className="cursor-default">{dotEl}</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
);
}