feat(pagespeed): render element screenshots in expandable audit items

Shows node screenshots, labels, HTML snippets, and URLs in audit
detail rows — matching pagespeed.web.dev's failing elements display.
This commit is contained in:
Usman Baig
2026-03-22 19:18:03 +01:00
parent 6b00b8b04a
commit 50960d0556

View File

@@ -552,43 +552,60 @@ function AuditRow({ audit }: { audit: AuditSummary }) {
{audit.description && ( {audit.description && (
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3 leading-relaxed">{audit.description}</p> <p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3 leading-relaxed">{audit.description}</p>
)} )}
{/* Items table */} {/* Items list */}
{audit.details && Array.isArray(audit.details) && audit.details.length > 0 && ( {audit.details && Array.isArray(audit.details) && audit.details.length > 0 && (
<div className="overflow-x-auto"> <div className="space-y-2">
<table className="w-full text-xs"> {audit.details.slice(0, 10).map((item: Record<string, any>, idx: number) => (
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800"> <div key={idx} className="flex items-start gap-3 py-2 border-b border-neutral-100 dark:border-neutral-800 last:border-0 text-xs text-neutral-600 dark:text-neutral-400">
{audit.details.slice(0, 10).map((item: Record<string, any>, idx: number) => ( {/* Element screenshot */}
<tr key={idx} className="text-neutral-600 dark:text-neutral-400"> {item.node?.screenshot?.data && (
{/* URL or label */} <img
<td className="py-1.5 pr-3 max-w-xs truncate"> src={item.node.screenshot.data}
{item.url ? ( alt=""
<span className="font-mono text-xs break-all">{item.url}</span> className="w-20 h-14 object-contain rounded border border-neutral-200 dark:border-neutral-700 flex-shrink-0 bg-neutral-50 dark:bg-neutral-800"
) : item.node?.snippet ? ( />
<code className="text-xs bg-neutral-100 dark:bg-neutral-800 px-1 py-0.5 rounded break-all">{item.node.snippet}</code> )}
) : item.label || item.groupLabel || item.statistic || ''} {/* Content */}
</td> <div className="flex-1 min-w-0">
{/* Wasted bytes */} {/* Label / node explanation */}
{item.wastedBytes != null && ( {(item.node?.nodeLabel || item.label || item.groupLabel) && (
<td className="py-1.5 pr-3 text-right whitespace-nowrap text-amber-600 dark:text-amber-400"> <div className="font-medium text-neutral-900 dark:text-white text-xs mb-0.5">
{item.wastedBytes < 1024 ? `${item.wastedBytes} B` : `${(item.wastedBytes / 1024).toFixed(1)} KiB`} {item.node?.nodeLabel || item.label || item.groupLabel}
</td> </div>
)} )}
{/* Total bytes */} {/* URL */}
{item.totalBytes != null && !item.wastedBytes && ( {item.url && (
<td className="py-1.5 pr-3 text-right whitespace-nowrap"> <div className="font-mono text-xs text-neutral-500 dark:text-neutral-400 break-all">{item.url}</div>
{item.totalBytes < 1024 ? `${item.totalBytes} B` : `${(item.totalBytes / 1024).toFixed(1)} KiB`} )}
</td> {/* HTML snippet */}
)} {item.node?.snippet && (
{/* Wasted ms */} <code className="text-xs bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded break-all mt-1 inline-block">{item.node.snippet}</code>
{item.wastedMs != null && ( )}
<td className="py-1.5 text-right whitespace-nowrap text-amber-600 dark:text-amber-400"> {/* Statistic-type items */}
{item.wastedMs < 1000 ? `${Math.round(item.wastedMs)}ms` : `${(item.wastedMs / 1000).toFixed(1)}s`} {!item.url && !item.node && item.statistic && (
</td> <span>{item.statistic}</span>
)} )}
</tr> </div>
))} {/* Metrics on the right */}
</tbody> <div className="flex-shrink-0 text-right space-y-0.5">
</table> {item.wastedBytes != null && (
<div className="text-amber-600 dark:text-amber-400 whitespace-nowrap">
{item.wastedBytes < 1024 ? `${item.wastedBytes} B` : `${(item.wastedBytes / 1024).toFixed(1)} KiB`}
</div>
)}
{item.totalBytes != null && !item.wastedBytes && (
<div className="whitespace-nowrap">
{item.totalBytes < 1024 ? `${item.totalBytes} B` : `${(item.totalBytes / 1024).toFixed(1)} KiB`}
</div>
)}
{item.wastedMs != null && (
<div className="text-amber-600 dark:text-amber-400 whitespace-nowrap">
{item.wastedMs < 1000 ? `${Math.round(item.wastedMs)}ms` : `${(item.wastedMs / 1000).toFixed(1)}s`}
</div>
)}
</div>
</div>
))}
{audit.details.length > 10 && ( {audit.details.length > 10 && (
<p className="text-xs text-neutral-400 mt-1">+ {audit.details.length - 10} more items</p> <p className="text-xs text-neutral-400 mt-1">+ {audit.details.length - 10} more items</p>
)} )}