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

View File

@ -4,7 +4,7 @@ export default function Header() {
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">
<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
</span>
</header>

View File

@ -114,7 +114,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
return (
<>
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
className="fixed inset-0 bg-black/60 z-50"
onClick={onClose}
role="button"
aria-label="Close"
@ -128,7 +128,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
>
<div key={receipt.id} className="p-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
ref={closeButtonRef}
onClick={onClose}

View File

@ -50,14 +50,14 @@ export default function ReceiptList({
if (receipts.length === 0) {
return (
<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>
);
}
return (
<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">Tier</span>
<span role="columnheader">Time</span>

View File

@ -103,7 +103,7 @@ function SessionCard({
{expanded && (
<div className="border-t border-neutral-800 p-3">
<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">Tier</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"
>
<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 className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}>
{r.tier}
@ -162,7 +162,7 @@ export default function SessionView({ receipts, onSelectReceipt }: Props) {
if (groups.length === 0) {
return (
<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>
);
}

View File

@ -38,7 +38,7 @@ export default function StatsCards({ stats, isLoading, error }: Props) {
>
<div className="flex items-center gap-2 mb-2">
<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}
</span>
</div>