fix(ui): address PR-Agent feedback on health redesign

PR-Agent flagged two real bugs at score 84:

1. Accessibility (HealthPriorityCard): the priority card header was a
   clickable <div> with onClick, making expand/collapse mouse-only.
   Promote it to a <button type="button"> with aria-expanded reflecting
   state and a contextual aria-label, and a focus-visible ring. The
   inner content is extracted into a shared fragment so the button and
   the no-content fallback <div> render the same markup.

2. Time display (HealthStatusRibbon): formatRelativeTime lost the hours
   branch from the original health.tsx, so a 2-hour-old scan showed as
   '120 minutes ago' instead of '2 hours ago'. Restore the hours
   rollover with health.hoursAgo i18n key.
This commit is contained in:
Tam Nhu Tran
2026-04-25 19:10:00 -04:00
parent dd9854bfd7
commit 525c4f9812
2 changed files with 58 additions and 33 deletions
@@ -35,6 +35,39 @@ export function HealthPriorityCard({ check }: HealthPriorityCardProps) {
}
};
const headerInner = (
<>
{/* Status Icon with Ring */}
<div
className={cn(
'flex items-center justify-center w-12 h-12 rounded-2xl shrink-0',
isError ? 'bg-rose-500/10 text-rose-500' : 'bg-amber-500/10 text-amber-500'
)}
>
<Icon className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold tracking-tight">{check.name}</h3>
{hasContent && (
<div className="p-1 hover:bg-muted rounded-full transition-colors">
<ChevronRight
className={cn(
'w-5 h-5 text-muted-foreground transition-transform',
isExpanded && 'rotate-90'
)}
/>
</div>
)}
</div>
<p className="text-sm font-medium leading-relaxed text-muted-foreground/80">
{check.message}
</p>
</div>
</>
);
return (
<motion.div
layout
@@ -53,39 +86,27 @@ export function HealthPriorityCard({ check }: HealthPriorityCardProps) {
<div className="absolute inset-0 rounded-[calc(2rem-1px)] shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)] pointer-events-none" />
<div className="p-6">
<div
className={cn('flex items-start gap-4', hasContent && 'cursor-pointer select-none')}
onClick={() => hasContent && setIsExpanded(!isExpanded)}
>
{/* Status Icon with Ring */}
<div
className={cn(
'flex items-center justify-center w-12 h-12 rounded-2xl shrink-0',
isError ? 'bg-rose-500/10 text-rose-500' : 'bg-amber-500/10 text-amber-500'
)}
{/*
* Header acts as the expand/collapse trigger when there's
* additional content (details, fix, fixable). Render as <button>
* so keyboard and screen-reader users can toggle it; aria-expanded
* reflects state. Without content, render a plain <div>.
*/}
{hasContent ? (
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
aria-expanded={isExpanded}
aria-label={
isExpanded ? `Collapse ${check.name} details` : `Expand ${check.name} details`
}
className="flex w-full select-none items-start gap-4 rounded-2xl text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<Icon className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold tracking-tight">{check.name}</h3>
{hasContent && (
<div className="p-1 hover:bg-muted rounded-full transition-colors">
<ChevronRight
className={cn(
'w-5 h-5 text-muted-foreground transition-transform',
isExpanded && 'rotate-90'
)}
/>
</div>
)}
</div>
<p className="text-sm font-medium leading-relaxed text-muted-foreground/80">
{check.message}
</p>
</div>
</div>
{headerInner}
</button>
) : (
<div className="flex w-full items-start gap-4">{headerInner}</div>
)}
<AnimatePresence>
{isExpanded && (
@@ -25,7 +25,11 @@ function formatRelativeTime(
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 5) return t('health.justNow');
if (seconds < 60) return t('health.secondsAgo', { count: seconds });
return t('health.minutesAgo', { count: Math.floor(seconds / 60) });
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return t('health.minutesAgo', { count: minutes });
// Roll up to hours so a stale "120 minutes ago" reads as "2 hours ago"
const hours = Math.floor(minutes / 60);
return t('health.hoursAgo', { count: hours });
}
export function HealthStatusRibbon({