mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
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:
1 parent
dd9854bfd7
commit
525c4f9812
2 files changed
+58
-33
No files matched your search
@@ -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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
layout
|
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="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="p-6">
|
||||||
<div
|
{/*
|
||||||
className={cn('flex items-start gap-4', hasContent && 'cursor-pointer select-none')}
|
* Header acts as the expand/collapse trigger when there's
|
||||||
onClick={() => hasContent && setIsExpanded(!isExpanded)}
|
* additional content (details, fix, fixable). Render as <button>
|
||||||
>
|
* so keyboard and screen-reader users can toggle it; aria-expanded
|
||||||
{/* Status Icon with Ring */}
|
* reflects state. Without content, render a plain <div>.
|
||||||
<div
|
*/}
|
||||||
className={cn(
|
{hasContent ? (
|
||||||
'flex items-center justify-center w-12 h-12 rounded-2xl shrink-0',
|
<button
|
||||||
isError ? 'bg-rose-500/10 text-rose-500' : 'bg-amber-500/10 text-amber-500'
|
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" />
|
{headerInner}
|
||||||
</div>
|
</button>
|
||||||
|
) : (
|
||||||
<div className="flex-1 min-w-0 space-y-1">
|
<div className="flex w-full items-start gap-4">{headerInner}</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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({
|
||||||
|
|||||||
Reference in new issue
Block a user