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 committed 2026-04-25 19:10:00 -04:00
1 parent dd9854bfd7
commit 525c4f9812
2 files changed
+49 -24

No files matched your search

@@ -35,28 +35,8 @@ export function HealthPriorityCard({ check }: HealthPriorityCardProps) {
} }
}; };
return ( const headerInner = (
<motion.div <>
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className={cn(
'group relative overflow-hidden rounded-[2rem] p-px transition-all duration-300',
isError
? 'bg-gradient-to-br from-rose-500/20 via-rose-500/5 to-transparent'
: 'bg-gradient-to-br from-amber-500/20 via-amber-500/5 to-transparent'
)}
>
<div className="relative h-full rounded-[calc(2rem-1px)] bg-background/80 backdrop-blur-md overflow-hidden">
{/* Double-Bezel Inner Highlight */}
<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 */} {/* Status Icon with Ring */}
<div <div
className={cn( className={cn(
@@ -85,7 +65,48 @@ export function HealthPriorityCard({ check }: HealthPriorityCardProps) {
{check.message} {check.message}
</p> </p>
</div> </div>
</div> </>
);
return (
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className={cn(
'group relative overflow-hidden rounded-[2rem] p-px transition-all duration-300',
isError
? 'bg-gradient-to-br from-rose-500/20 via-rose-500/5 to-transparent'
: 'bg-gradient-to-br from-amber-500/20 via-amber-500/5 to-transparent'
)}
>
<div className="relative h-full rounded-[calc(2rem-1px)] bg-background/80 backdrop-blur-md overflow-hidden">
{/* Double-Bezel Inner Highlight */}
<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">
{/*
* 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"
>
{headerInner}
</button>
) : (
<div className="flex w-full items-start gap-4">{headerInner}</div>
)}
<AnimatePresence> <AnimatePresence>
{isExpanded && ( {isExpanded && (
@@ -25,7 +25,11 @@ function formatRelativeTime(
const seconds = Math.floor((Date.now() - timestamp) / 1000); const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 5) return t('health.justNow'); if (seconds < 5) return t('health.justNow');
if (seconds < 60) return t('health.secondsAgo', { count: seconds }); 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({ export function HealthStatusRibbon({