fix(boltbot): address Rams design review — 11 a11y and visual fixes

- SessionView: aria-expanded on session toggle, role=table/row/cell
  semantics on sub-table, keyboard access on expanded rows
- ReceiptDetail: backdrop role=button, focus trap sentinel, external
  link changed from <a href="#"> to <button>
- App: view toggle uses role=tablist/tab with aria-selected
- StatsCards: aria-hidden on decorative icons
- FilterControls: increased touch targets to 44px minimum
- ReceiptList: border-l-2 border-transparent on non-selected rows
  to prevent layout shift on selection
This commit is contained in:
duy 2026-01-29 15:19:01 -08:00
parent 58e556a2d7
commit 146a55836c
6 changed files with 41 additions and 21 deletions

View File

@ -88,9 +88,11 @@ export default function App() {
<div className="max-w-6xl mx-auto space-y-6 pt-6">
<StatsCards stats={stats} isLoading={statsLoading} error={statsError} />
<div className="flex items-center gap-2">
<div className="flex items-center gap-2" role="tablist">
<button
onClick={() => setViewMode("list")}
role="tab"
aria-selected={viewMode === "list"}
className={cn(
"px-4 py-1.5 text-sm rounded-lg transition-colors",
viewMode === "list"
@ -102,6 +104,8 @@ export default function App() {
</button>
<button
onClick={() => setViewMode("sessions")}
role="tab"
aria-selected={viewMode === "sessions"}
className={cn(
"px-4 py-1.5 text-sm rounded-lg transition-colors",
viewMode === "sessions"

View File

@ -54,7 +54,7 @@ export default function FilterControls({
onClick={() => toggleTier(t.value)}
aria-pressed={isActive}
className={cn(
"px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors",
"px-3 py-2.5 rounded-lg text-xs font-medium border transition-colors",
isActive ? style.active : style.inactive,
)}
>
@ -67,7 +67,7 @@ export default function FilterControls({
onClick={() => onAnomalyOnlyChange(!anomalyOnly)}
aria-pressed={anomalyOnly}
className={cn(
"px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors",
"px-3 py-2.5 rounded-lg text-xs font-medium border transition-colors",
anomalyOnly
? "bg-red-500/20 text-red-400 border-red-500"
: "border-neutral-700 text-neutral-400 hover:border-red-500/50",

View File

@ -116,6 +116,9 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
onClick={onClose}
role="button"
aria-label="Close"
tabIndex={-1}
/>
<div
role="dialog"
@ -200,13 +203,13 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
<span className="font-mono text-xs break-all text-neutral-300 flex-1">
{receipt.daCommitment}
</span>
<a
href="#"
<button
type="button"
aria-label="View on EigenDA explorer"
className="shrink-0 p-1 rounded hover:bg-neutral-700 transition-colors"
>
<ExternalLink className="w-3.5 h-3.5 text-neutral-400" aria-hidden="true" />
</a>
</button>
</div>
) : (
<div className="text-neutral-400 text-sm">
@ -221,6 +224,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
</div>
</Accordion>
</div>
<div tabIndex={0} onFocus={() => closeButtonRef.current?.focus()} aria-hidden="true" />
</div>
</>
);

View File

@ -82,7 +82,7 @@ export default function ReceiptList({
"grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 items-center px-3 py-2 rounded-lg cursor-pointer transition-colors",
selectedId === r.id
? "bg-neutral-800/50 border-l-2 border-emerald-400"
: "hover:bg-neutral-800/30",
: "hover:bg-neutral-800/30 border-l-2 border-transparent",
)}
>
<span role="cell" className="text-sm font-mono truncate">{r.toolName}</span>

View File

@ -67,6 +67,7 @@ function SessionCard({
<div className="bg-neutral-900 rounded-xl border border-neutral-800">
<button
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
className="w-full flex items-center gap-3 p-4 text-left hover:bg-neutral-800/30 transition-colors rounded-xl"
>
<ChevronDown
@ -74,6 +75,7 @@ function SessionCard({
"w-4 h-4 text-neutral-400 shrink-0 transition-transform",
expanded && "rotate-180",
)}
aria-hidden="true"
/>
<span className="font-mono text-xs truncate flex-1 text-neutral-300">
{group.sessionKey}
@ -100,36 +102,45 @@ function SessionCard({
</button>
{expanded && (
<div className="border-t border-neutral-800 p-3">
<div className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase tracking-wide">
<span>Tool</span>
<span>Tier</span>
<span>Time</span>
<span>Status</span>
<span>Anomaly</span>
</div>
{group.receipts.map((r) => (
<div role="table">
<div role="row" className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase tracking-wide">
<span role="columnheader">Tool</span>
<span role="columnheader">Tier</span>
<span role="columnheader">Time</span>
<span role="columnheader">Status</span>
<span role="columnheader">Anomaly</span>
</div>
{group.receipts.map((r) => (
<div
key={r.id}
role="row"
tabIndex={0}
onClick={() => onSelectReceipt(r)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelectReceipt(r);
}
}}
className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 items-center px-3 py-2 rounded-lg cursor-pointer hover:bg-neutral-800/30 transition-colors"
>
<span className="text-sm font-mono truncate">{r.toolName}</span>
<span>
<span role="cell" className="text-sm font-mono truncate">{r.toolName}</span>
<span role="cell">
<span className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}>
{r.tier}
</span>
</span>
<span className="text-xs text-neutral-400">
<span role="cell" className="text-xs text-neutral-400">
{formatRelativeTime(r.timestamp)}
</span>
<span>
<span role="cell">
{r.success ? (
<CheckCircle className="w-4 h-4 text-emerald-400" aria-label="Success" />
) : (
<XCircle className="w-4 h-4 text-red-400" aria-label="Failed" />
)}
</span>
<span>
<span role="cell">
{r.anomalies.length > 0 ? (
<AlertTriangle className="w-4 h-4 text-amber-400" aria-label="Has anomalies" />
) : (
@ -138,6 +149,7 @@ function SessionCard({
</span>
</div>
))}
</div>
</div>
)}
</div>

View File

@ -37,7 +37,7 @@ export default function StatsCards({ stats, isLoading, error }: Props) {
className="bg-neutral-900 rounded-xl border border-neutral-800 p-4"
>
<div className="flex items-center gap-2 mb-2">
<card.icon className={`w-4 h-4 ${card.color}`} />
<card.icon className={`w-4 h-4 ${card.color}`} aria-hidden="true" />
<span className="text-xs text-neutral-400 uppercase tracking-wide">
{card.label}
</span>