feat(ui): cohesive frontend polish — design system + per-screen refinement

Distill the UI into a single, recognizable voice: a precision
instrument for one operator on one machine. Bloomberg-coded chrome,
warm-paper detail surfaces, mono-heavy numerics, with one editorial
serif accent for moments of weight.

Design system
- Three-font stack: Geist Sans (UI), Geist Mono (data), Instrument
  Serif (editorial display). No Inter, no system fonts.
- Two surfaces: dark chrome (--background, --accent, --signal) and
  warm paper detail surfaces (--surface, --surface-ink*).
- Inbox has its own Ticker Tape terminal palette (--tt-*).
- Shared component classes: .eyebrow, .mono, .display, .surface,
  .surface-2, .row-hover, .nav-active, .kbd, .editorial, .hairline.
- --m-* token aliases for the legacy drawer components so test-
  asserted class strings keep resolving to the same hue family.

Drawers (Claim + Remit)
- Editorial display face for totals (paid/adjustment amounts).
- Color-coded money tiles: green-tinted paid card, amber-tinted
  adjustment card when non-zero, muted otherwise.
- Tabs get accent-blue underline + CSS-driven active state.
- Validation banners with proper background opacity + ring-around-
  dot success badge.
- StateHistoryTimeline: dashed border, ring around dots, ↳ prefix
  for remit ids.
- DiagnosesList, PartiesGrid, MatchedRemitCard, CasAdjustmentsPanel:
  refined typography, dashed dividers, italic descriptions, mono
  amounts, font-semibold totals, hover row tints.

Inbox Ticker Tape
- Custom RowCheckbox with sr-only input + amber accent.
- Alternating row striping, hover tints, accent rail with inset
  shadow on selection.
- Refined sparkline with glow at high values.
- BulkBar: bottom-floating bar with amber count chip, larger shadow.
- CandidateBreakdown: animated progress bars with amber gradient.
- InboxHeader: Instrument Serif headline, mono day/date stamp,
  pulsing amber status dot.

Dialogs & search
- NewClaimDialog: editorial title, mono NPI/CPT/amount fields.
- SearchBar: refined input row with mono, footer with pulsing
  loading indicator.
- KeyboardCheatsheet: monogram icon chip, hover row states, refined
  eyebrow header.

Primitives
- StatusBadge / ClaimStateBadge: per-state dot indicators.
- SelectItem: data-[highlighted]:outline tokens for keyboard a11y.
- Table primitives: refined header treatment, hover/focus states.

Tests + build
- 354/354 tests passing across 59 files.
- Vite build clean (53.84 kB CSS / 560.86 kB JS).
- Eyebrow assertions updated to match the consolidated .eyebrow
  class (intact visual contract, abstracted class string).
- Badge variant tokens updated to the polished bg-muted/80 /
  /0.14 opacity scale.
This commit is contained in:
Tyler
2026-06-20 22:27:01 -06:00
parent 02841d7e6e
commit fbe9940a3f
70 changed files with 2582 additions and 1663 deletions
+181 -214
View File
@@ -25,6 +25,7 @@ import { EmptyState } from "@/components/ui/empty-state";
import { ErrorState } from "@/components/ui/error-state";
import { FilterChips, type FilterChipOption } from "@/components/ui/filter-chips";
import { Pagination } from "@/components/ui/pagination";
import { PageHeader } from "@/components/PageHeader";
import { useClaims } from "@/hooks/useClaims";
import { useDrawerUrlState } from "@/hooks/useDrawerUrlState";
import { useTailStream } from "@/hooks/useTailStream";
@@ -55,10 +56,6 @@ export function Claims() {
const [helpOpen, setHelpOpen] = useState(false);
const searchRef = useRef<HTMLInputElement>(null);
// SP4 drawer wiring: the URL is the source of truth for "which claim is
// open". `open(id)` pushState's a new history entry (Back returns to the
// list); `close()` pushState's the bare URL; `setClaimId(id)` replaceState's
// so j/k navigation doesn't pollute history with one entry per keystroke.
const { claimId, open, close, setClaimId } = useDrawerUrlState();
const providers = useAppStore((s) => s.providers);
@@ -78,15 +75,6 @@ export function Claims() {
const { data, isLoading, isError, error, refetch, dataUpdatedAt } = useClaims(params);
// SP5 live-tail wiring (sub-project 5, Phase 5 Task 22).
//
// The tail stream emits every claim that lands in the DB — including
// ones that don't match the user's current `status` / `provider_npi`
// filter. Mirror the server-side filter the page already applies via
// `useClaims` so a newly-arrived item that wouldn't pass the same
// predicate is dropped before it can show up in the table. `useMergedTail`
// applies this to the tail slice only; base items are already filtered
// server-side.
const tailFilterFn = useMemo(
() => (c: Claim) => {
if (status !== ALL && c.status !== status) return false;
@@ -108,7 +96,6 @@ export function Claims() {
[items]
);
// Local search filter on the current page (cheap; no extra fetch).
const visible = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return items;
@@ -122,21 +109,12 @@ export function Claims() {
}, [items, query]);
const filtered = status !== ALL || npi !== ALL;
// Dim the background list while the drawer is open so the user's eye
// lands on the drawer. `pointer-events-none` prevents accidental clicks
// on the dimmed list (e.g., clicking a row would open yet another
// drawer on top).
const dimBackground = claimId !== null;
return (
<>
<ClaimDrawer
claimId={claimId}
// Pass the full ordered list of ids from the current page so j/k
// can wrap around without an extra round-trip. `items` is the API
// response (post-pagination, post-search); that's the same set the
// user is looking at, so wrap-around feels right.
claims={items.map((c) => ({ id: c.id }))}
onClose={close}
onNavigate={setClaimId}
@@ -149,211 +127,200 @@ export function Claims() {
<div
data-testid="claims-page-body"
className={cn(
"space-y-8 animate-fade-in transition-opacity",
"space-y-6 lg:space-y-8 animate-fade-in transition-opacity",
dimBackground && "pointer-events-none opacity-60"
)}
>
<header className="flex items-end justify-between gap-6 flex-wrap">
<div>
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground mb-2 flex items-center gap-2">
<span className="inline-block h-px w-6 bg-border" />
Claims
</div>
<h1 className="text-[22px] font-semibold tracking-tight">All claims</h1>
</div>
<NewClaimDialog />
</header>
{isError ? (
<ErrorState
message="Couldn't load claims from the backend."
detail={error instanceof Error ? error.message : String(error)}
onRetry={() => refetch()}
/>
) : null}
<div className="surface rounded-xl p-4">
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[240px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={searchRef}
placeholder="Search by claim ID, patient, payer, CPT…"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 pr-16"
/>
{query ? (
<button
type="button"
onClick={() => setQuery("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground p-1.5 rounded-md"
aria-label="Clear search"
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
</div>
<Select
value={npi}
onValueChange={(v) => {
setNpi(v);
setPage(1);
}}
>
<SelectTrigger className="w-[240px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All providers</SelectItem>
{providers.map((p) => (
<SelectItem key={p.npi} value={p.npi}>
{p.npi} {p.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* SP5: live-tail status pill. `ml-auto` pins it to the right
edge of the toolbar so it doesn't crowd the search input. */}
<div className="ml-auto">
<PageHeader
eyebrow="Claims"
title={<>All <span className="display italic text-muted-foreground">claims</span></>}
subtitle="Every claim submitted, accepted, denied, or paid — filtered, sorted, and live."
actions={
<TailStatusPill
status={tailStatus}
lastEventAt={tailLastEventAt}
onReconnect={forceReconnect}
/>
</div>
</div>
}
status={
<NewClaimDialog />
}
/>
<div className="mt-3">
<FilterChips
options={STATUS_OPTIONS}
value={status === ALL ? null : status}
onChange={(v) => {
setStatus((v as ClaimStatus | null) ?? ALL);
setPage(1);
}}
eyebrow="Status"
{isError ? (
<ErrorState
message="Couldn't load claims from the backend."
detail={error instanceof Error ? error.message : String(error)}
onRetry={() => refetch()}
/>
</div>
) : null}
<div className="flex items-center gap-6 mt-4 pt-4 border-t border-border/40 text-xs text-muted-foreground">
<span>
<span className="num text-foreground font-medium">
{fmt.num(data?.total ?? 0)}
</span>{" "}
claims
</span>
<span>
Billed{" "}
<span className="num text-foreground font-medium">
{fmt.usd(totals.billed)}
</span>
</span>
<span>
Received{" "}
<span className="num text-foreground font-medium">
{fmt.usd(totals.received)}
</span>
</span>
</div>
</div>
<div className="surface rounded-xl overflow-hidden">
{isLoading ? (
<div className="p-4 space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} variant="row" />
))}
</div>
) : items.length === 0 ? (
<EmptyState
eyebrow="Claims · inbox idle"
message={
filtered
? "No claims match the current filters."
: "Drop an 837P file on the Upload page to populate this list."
}
/>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Claim</TableHead>
<TableHead>Patient</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Payer</TableHead>
<TableHead className="text-right">Billed</TableHead>
<TableHead className="text-right">Received</TableHead>
<TableHead>Status</TableHead>
<TableHead>Submitted</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visible.map((c) => {
const provider = providerMap[c.providerNpi];
return (
<TableRow
key={`${c.id}-${dataUpdatedAt}`}
// Click anywhere on the row to open the drawer for
// this claim. `cursor-pointer` makes the affordance
// obvious; the existing `hover:bg-muted/30` from the
// TableRow primitive stays in place.
onClick={() => open(c.id)}
className={cn(
"animate-row-flash cursor-pointer",
// Re-keying on dataUpdatedAt re-mounts the row on
// every refetch (initial load, filter change, parse
// invalidation) so the row-flash keyframe replays.
)}
>
<TableCell>
<div className="display num text-[13px]">{c.id}</div>
<div className="text-[11px] text-muted-foreground num">
CPT {c.cptCode}
</div>
</TableCell>
<TableCell className="font-medium">{c.patientName}</TableCell>
<TableCell>
<div className="text-sm">{provider?.name ?? "Unknown"}</div>
<div className="text-[11px] text-muted-foreground num">
{c.providerNpi}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{c.payerName}
</TableCell>
<TableCell className="text-right display num">
{fmt.usd(c.billedAmount)}
</TableCell>
<TableCell className="text-right display num text-muted-foreground">
{c.receivedAmount > 0 ? fmt.usd(c.receivedAmount) : "—"}
</TableCell>
<TableCell>
<StatusBadge status={c.status} />
</TableCell>
<TableCell className="text-muted-foreground text-[13px] num">
{fmt.dateShort(c.submissionDate)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="px-4 pb-4">
<Pagination
page={page}
pageSize={PAGE_SIZE}
total={data?.total ?? 0}
onPageChange={setPage}
<div className="surface rounded-xl p-4">
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[240px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={searchRef}
placeholder="Search by claim ID, patient, payer, CPT…"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 pr-10"
/>
{query ? (
<button
type="button"
onClick={() => setQuery("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground p-1.5 rounded-md transition-colors hover:bg-muted/60"
aria-label="Clear search"
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
</div>
</>
)}
<Select
value={npi}
onValueChange={(v) => {
setNpi(v);
setPage(1);
}}
>
<SelectTrigger className="w-[240px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All providers</SelectItem>
{providers.map((p) => (
<SelectItem key={p.npi} value={p.npi}>
{p.npi} {p.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="mt-3.5">
<FilterChips
options={STATUS_OPTIONS}
value={status === ALL ? null : status}
onChange={(v) => {
setStatus((v as ClaimStatus | null) ?? ALL);
setPage(1);
}}
eyebrow="Status"
/>
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 mt-4 pt-4 border-t border-border/30 mono text-[11px] text-muted-foreground">
<span>
<span className="text-foreground font-medium">
{fmt.num(data?.total ?? 0)}
</span>{" "}
claims
</span>
<span>
Billed{" "}
<span className="text-foreground font-medium">
{fmt.usd(totals.billed)}
</span>
</span>
<span>
Received{" "}
<span className="text-foreground font-medium">
{fmt.usd(totals.received)}
</span>
</span>
<span className="ml-auto text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground/70">
Press <span className="kbd mx-1">?</span> for shortcuts
</span>
</div>
</div>
<div className="surface rounded-xl overflow-hidden">
{isLoading ? (
<div className="p-4 space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} variant="row" />
))}
</div>
) : items.length === 0 ? (
<EmptyState
eyebrow="Claims · inbox idle"
message={
filtered
? "No claims match the current filters."
: "Drop an 837P file on the Upload page to populate this list."
}
/>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Claim</TableHead>
<TableHead>Patient</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Payer</TableHead>
<TableHead className="text-right">Billed</TableHead>
<TableHead className="text-right">Received</TableHead>
<TableHead>Status</TableHead>
<TableHead>Submitted</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visible.map((c) => {
const provider = providerMap[c.providerNpi];
return (
<TableRow
key={`${c.id}-${dataUpdatedAt}`}
onClick={() => open(c.id)}
className={cn("animate-row-flash cursor-pointer")}
>
<TableCell>
<div className="display mono text-[12.5px] text-foreground">{c.id}</div>
<div className="mono text-[10.5px] text-muted-foreground">
CPT {c.cptCode}
</div>
</TableCell>
<TableCell className="font-medium text-[13px]">{c.patientName}</TableCell>
<TableCell>
<div className="text-[13px]">{provider?.name ?? "Unknown"}</div>
<div className="mono text-[10.5px] text-muted-foreground">
{c.providerNpi}
</div>
</TableCell>
<TableCell className="text-muted-foreground text-[13px]">
{c.payerName}
</TableCell>
<TableCell className="text-right display mono">
{fmt.usd(c.billedAmount)}
</TableCell>
<TableCell className="text-right display mono text-muted-foreground">
{c.receivedAmount > 0 ? fmt.usd(c.receivedAmount) : "—"}
</TableCell>
<TableCell>
<StatusBadge status={c.status} />
</TableCell>
<TableCell className="text-muted-foreground mono text-[12px]">
{fmt.dateShort(c.submissionDate)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="px-4 pb-4">
<Pagination
page={page}
pageSize={PAGE_SIZE}
total={data?.total ?? 0}
onPageChange={setPage}
/>
</div>
</>
)}
</div>
</div>
</div>
</>
);
}