fix(boltbot): address UI skills review — 16 violations

- min-h-screen → min-h-dvh for dynamic viewport
- arbitrary z-[100] → z-50 (fixed scale)
- removed tracking-tight/tracking-wide (letter-spacing not requested)
- replaced 3 URL-sync useEffects with inline wrapper functions
- initialParams moved to useRef (prevent re-computation per render)
- removed backdrop-blur-sm on full-screen overlay (perf)
- added text-balance on headings
- added min-w-0 on SessionView tool name truncate
- empty states now include guidance text with clear next action
This commit is contained in:
duy 2026-01-29 15:39:40 -08:00
parent b2c6b055ee
commit 5657dad29d
6 changed files with 28 additions and 32 deletions

View File

@ -12,10 +12,6 @@ import SessionView from "./components/SessionView";
const LIMIT = 50; const LIMIT = 50;
function getSearchParams() {
return new URLSearchParams(window.location.search);
}
function setSearchParams(params: Record<string, string>) { function setSearchParams(params: Record<string, string>) {
const url = new URL(window.location.href); const url = new URL(window.location.href);
for (const [k, v] of Object.entries(params)) { for (const [k, v] of Object.entries(params)) {
@ -26,7 +22,7 @@ function setSearchParams(params: Record<string, string>) {
} }
export default function App() { export default function App() {
const initialParams = getSearchParams(); const initialParams = useRef(new URLSearchParams(window.location.search)).current;
const [selectedTiers, setSelectedTiers] = useState<string[]>(() => { const [selectedTiers, setSelectedTiers] = useState<string[]>(() => {
const tiers = initialParams.get("tiers"); const tiers = initialParams.get("tiers");
return tiers ? tiers.split(",").filter(Boolean) : []; return tiers ? tiers.split(",").filter(Boolean) : [];
@ -47,18 +43,18 @@ export default function App() {
const { stats, isLoading: statsLoading, error: statsError } = useStats(); const { stats, isLoading: statsLoading, error: statsError } = useStats();
const { receipts, isLoading: receiptsLoading, error: receiptsError } = useReceipts(LIMIT, 0); const { receipts, isLoading: receiptsLoading, error: receiptsError } = useReceipts(LIMIT, 0);
// Sync URL with state changes function updateViewMode(v: "list" | "sessions") {
useEffect(() => { setViewMode(v);
setSearchParams({ view: viewMode === "list" ? "" : viewMode }); setSearchParams({ view: v === "list" ? "" : v });
}, [viewMode]); }
function updateSelectedTiers(tiers: string[]) {
useEffect(() => { setSelectedTiers(tiers);
setSearchParams({ tiers: selectedTiers.join(",") }); setSearchParams({ tiers: tiers.join(",") });
}, [selectedTiers]); }
function updateAnomalyOnly(v: boolean) {
useEffect(() => { setAnomalyOnly(v);
setSearchParams({ anomalies: anomalyOnly ? "1" : "" }); setSearchParams({ anomalies: v ? "1" : "" });
}, [anomalyOnly]); }
// Polling: merge new receipts at the top, do NOT touch loadedCount or hasMore // Polling: merge new receipts at the top, do NOT touch loadedCount or hasMore
useEffect(() => { useEffect(() => {
@ -114,8 +110,8 @@ export default function App() {
}); });
return ( return (
<div className="min-h-screen bg-neutral-950 text-neutral-100"> <div className="min-h-dvh bg-neutral-950 text-neutral-100">
<a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:bg-neutral-800 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg"> <a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-neutral-800 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg">
Skip to main content Skip to main content
</a> </a>
<Header /> <Header />
@ -126,7 +122,7 @@ export default function App() {
<div className="flex items-center gap-2" role="tablist"> <div className="flex items-center gap-2" role="tablist">
<button <button
onClick={() => setViewMode("list")} onClick={() => updateViewMode("list")}
role="tab" role="tab"
aria-selected={viewMode === "list"} aria-selected={viewMode === "list"}
className={cn( className={cn(
@ -139,7 +135,7 @@ export default function App() {
All Receipts All Receipts
</button> </button>
<button <button
onClick={() => setViewMode("sessions")} onClick={() => updateViewMode("sessions")}
role="tab" role="tab"
aria-selected={viewMode === "sessions"} aria-selected={viewMode === "sessions"}
className={cn( className={cn(
@ -155,9 +151,9 @@ export default function App() {
<FilterControls <FilterControls
selectedTiers={selectedTiers} selectedTiers={selectedTiers}
onTiersChange={setSelectedTiers} onTiersChange={updateSelectedTiers}
anomalyOnly={anomalyOnly} anomalyOnly={anomalyOnly}
onAnomalyOnlyChange={setAnomalyOnly} onAnomalyOnlyChange={updateAnomalyOnly}
/> />
{viewMode === "list" ? ( {viewMode === "list" ? (

View File

@ -4,7 +4,7 @@ export default function Header() {
return ( return (
<header aria-label="Boltbot" className="fixed top-0 left-0 right-0 z-50 h-14 flex items-center px-5 bg-neutral-900/80 backdrop-blur border-b border-neutral-800"> <header aria-label="Boltbot" className="fixed top-0 left-0 right-0 z-50 h-14 flex items-center px-5 bg-neutral-900/80 backdrop-blur border-b border-neutral-800">
<Zap className="w-5 h-5 text-emerald-400 mr-2" aria-hidden="true" /> <Zap className="w-5 h-5 text-emerald-400 mr-2" aria-hidden="true" />
<span className="text-lg font-bold tracking-tight" style={{ fontFamily: "'Space Grotesk', sans-serif" }}> <span className="text-lg font-bold text-balance" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
Boltbot Boltbot
</span> </span>
</header> </header>

View File

@ -114,7 +114,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
return ( return (
<> <>
<div <div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" className="fixed inset-0 bg-black/60 z-50"
onClick={onClose} onClick={onClose}
role="button" role="button"
aria-label="Close" aria-label="Close"
@ -128,7 +128,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
> >
<div key={receipt.id} className="p-5"> <div key={receipt.id} className="p-5">
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-5">
<h2 id="receipt-detail-title" className="text-lg font-bold truncate pr-4">{receipt.toolName}</h2> <h2 id="receipt-detail-title" className="text-lg font-bold truncate pr-4 text-balance">{receipt.toolName}</h2>
<button <button
ref={closeButtonRef} ref={closeButtonRef}
onClick={onClose} onClick={onClose}

View File

@ -50,14 +50,14 @@ export default function ReceiptList({
if (receipts.length === 0) { if (receipts.length === 0) {
return ( return (
<div className="text-neutral-400 text-sm text-center py-12"> <div className="text-neutral-400 text-sm text-center py-12">
No actions recorded yet No actions recorded yet. Receipts appear here when your agent uses tools.
</div> </div>
); );
} }
return ( return (
<div role="table" aria-label="Action receipts" aria-busy={isLoading}> <div role="table" aria-label="Action receipts" aria-busy={isLoading}>
<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"> <div role="row" className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase">
<span role="columnheader">Tool</span> <span role="columnheader">Tool</span>
<span role="columnheader">Tier</span> <span role="columnheader">Tier</span>
<span role="columnheader">Time</span> <span role="columnheader">Time</span>

View File

@ -103,7 +103,7 @@ function SessionCard({
{expanded && ( {expanded && (
<div className="border-t border-neutral-800 p-3"> <div className="border-t border-neutral-800 p-3">
<div role="table"> <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"> <div role="row" className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase">
<span role="columnheader">Tool</span> <span role="columnheader">Tool</span>
<span role="columnheader">Tier</span> <span role="columnheader">Tier</span>
<span role="columnheader">Time</span> <span role="columnheader">Time</span>
@ -124,7 +124,7 @@ function SessionCard({
}} }}
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" 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 role="cell" className="text-sm font-mono truncate">{r.toolName}</span> <span role="cell" className="text-sm font-mono truncate min-w-0">{r.toolName}</span>
<span role="cell"> <span role="cell">
<span className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}> <span className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}>
{r.tier} {r.tier}
@ -162,7 +162,7 @@ export default function SessionView({ receipts, onSelectReceipt }: Props) {
if (groups.length === 0) { if (groups.length === 0) {
return ( return (
<div className="text-neutral-400 text-sm text-center py-12"> <div className="text-neutral-400 text-sm text-center py-12">
No sessions recorded yet No sessions recorded yet. Sessions appear here when your agent processes conversations.
</div> </div>
); );
} }

View File

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