From 3fbbfc8996f7197100b9780478d9810f480a510e Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 26 Apr 2026 09:00:56 +0300 Subject: [PATCH] web: review fixes for projects/stats SPA routes - ProjectsTable: drop the inner navigate call from handleOpen; the parent route already navigates via the onOpen callback, so the second push was creating a duplicate history entry on every row click. Matches the SessionsTable pattern. - HorizontalBars: replace href:string with onActivate(row) callback. The earlier shape passed a pre-encoded path string straight into TanStack ; routing the navigation through the typed (to, params) form via the caller avoids any double-encoding ambiguity around splat params and decouples the primitive from a specific route. - stats.css: drop duplicated .card / .card-head / .card-body blocks. The same rules already live in shell.css (loaded globally), so any future divergence between the two copies would silently desync. --- internal/server/web/dist/index.html | 4 +-- web/src/features/projects/ProjectsTable.tsx | 10 +------ web/src/primitives/HorizontalBars.tsx | 33 +++++++++++++-------- web/src/routes/stats.tsx | 10 +++++-- web/src/styles/stats.css | 24 +-------------- 5 files changed, 33 insertions(+), 48 deletions(-) diff --git a/internal/server/web/dist/index.html b/internal/server/web/dist/index.html index 6c2fdbb5c5dfb8d4c7ae8de8687bf52878d63716..2bb89178bd50c00f5830b1719e73a54b8d6de5c6 100644 --- a/internal/server/web/dist/index.html +++ b/internal/server/web/dist/index.html @@ -10,8 +10,8 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" /> - - + +
diff --git a/web/src/features/projects/ProjectsTable.tsx b/web/src/features/projects/ProjectsTable.tsx index 01551aa9860cd82eda2c918f0393677ecfcb6edd..ad9c5092728de64adf5fa767c191a87e1b1b1f8a 100644 --- a/web/src/features/projects/ProjectsTable.tsx +++ b/web/src/features/projects/ProjectsTable.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { useNavigate } from '@tanstack/react-router' import { EmptyState, ToolDot, Spark } from '../../primitives' import type { Project } from '../../api/adapters' @@ -39,8 +38,6 @@ function formatLastActive(iso: string): string { } export function ProjectsTable({ projects, cursor, onCursor, onOpen }: ProjectsTableProps): React.JSX.Element { - const navigate = useNavigate() - if (projects.length === 0) { return (
@@ -49,11 +46,6 @@ export function ProjectsTable({ projects, cursor, onCursor, onOpen }: ProjectsTa ) } - function handleOpen(p: Project) { - onOpen(p) - void navigate({ to: '/project/$', params: { _splat: encodeURIComponent(p.cwd) } }) - } - const maxSessions = Math.max(...projects.map(p => p.sessions), 1) return ( @@ -75,7 +67,7 @@ export function ProjectsTable({ projects, cursor, onCursor, onOpen }: ProjectsTa key={p.cwd} className={'projects-row projects-cols' + (isCursor ? ' cursor' : '')} style={{ gridTemplateColumns: COLS }} - onClick={() => { onCursor(i); handleOpen(p) }} + onClick={() => { onCursor(i); onOpen(p) }} onMouseEnter={() => onCursor(i)} > {p.cwd} diff --git a/web/src/primitives/HorizontalBars.tsx b/web/src/primitives/HorizontalBars.tsx index 4cb0c24ae4f5fea4fce5117584365d53877cdb72..da0e57f9267308d73c951ea744339db4265993b2 100644 --- a/web/src/primitives/HorizontalBars.tsx +++ b/web/src/primitives/HorizontalBars.tsx @@ -1,34 +1,43 @@ import type React from 'react' -import { Link } from '@tanstack/react-router' interface HorizontalBarsRow { label: string count: number - href?: string } -interface HorizontalBarsProps { - rows: HorizontalBarsRow[] +interface HorizontalBarsProps { + rows: R[] max: number + onActivate?: (row: R) => void } -export function HorizontalBars({ rows, max }: HorizontalBarsProps): React.JSX.Element { +export function HorizontalBars( + { rows, max, onActivate }: HorizontalBarsProps, +): React.JSX.Element { const safeMax = max > 0 ? max : 1 return (
{rows.map((row, i) => { const ratio = row.count / safeMax - const label = row.href != null ? ( - onActivate(row) : undefined} + role={activatable ? 'button' : undefined} + tabIndex={activatable ? 0 : undefined} + onKeyDown={activatable ? (e) => { if (e.key === 'Enter') onActivate(row) } : undefined} > {row.label} - - ) : ( - {row.label} + ) return ( diff --git a/web/src/routes/stats.tsx b/web/src/routes/stats.tsx index 487bdbb0a2e8f4cb30b710b2a3a8adde9b9892fb..64c102941f903ba13b803bb68eafd11cf1f5b046 100644 --- a/web/src/routes/stats.tsx +++ b/web/src/routes/stats.tsx @@ -245,7 +245,7 @@ function StatsRoute(): React.JSX.Element { const topCwdRows = s.topCwd.map(r => ({ label: r.cwd, count: r.count, - href: `/project/${encodeURIComponent(r.cwd)}`, + cwd: r.cwd, })) const topCwdCard = (
@@ -254,7 +254,13 @@ function StatsRoute(): React.JSX.Element { {s.topCwd.length === 0 ? ( ) : ( - + { + void navigate({ to: '/project/$', params: { _splat: encodeURIComponent(row.cwd) } }) + }} + /> )}
diff --git a/web/src/styles/stats.css b/web/src/styles/stats.css index a0925c47c47bb67dc2659e7e19492d2cac7d44bf..f3bec170580037773eb459207d6dcd96808b0a31 100644 --- a/web/src/styles/stats.css +++ b/web/src/styles/stats.css @@ -12,29 +12,7 @@ grid-column: 1 / -1; } -/* Card base (mirrors prototype.css) */ -.card { - border: 1px solid var(--rule-2); - border-radius: 4px; - background: var(--paper-4); -} - -.card .card-head { - font-family: var(--mono); - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--ink-3); - padding: 8px 10px 6px; - border-bottom: 1px solid var(--rule-2); - display: flex; - align-items: center; - gap: 8px; -} - -.card .card-body { - padding: 10px; -} +/* .card / .card-head / .card-body live in shell.css (loaded globally). */ /* Per-tool rollup rows */ .per-tool-row {