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:
parent
b2c6b055ee
commit
5657dad29d
@ -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" ? (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user