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:
+50
-78
@@ -15,11 +15,12 @@ import { ErrorState } from "@/components/ui/error-state";
|
||||
import { FilterChips, type FilterChipOption } from "@/components/ui/filter-chips";
|
||||
import { Pagination } from "@/components/ui/pagination";
|
||||
import { KeyboardCheatsheet } from "@/components/KeyboardCheatsheet";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { TailStatusPill } from "@/components/TailStatusPill";
|
||||
import { useRemittances } from "@/hooks/useRemittances";
|
||||
import { useRowKeyboard } from "@/hooks/useRowKeyboard";
|
||||
import { useTailStream } from "@/hooks/useTailStream";
|
||||
import { useMergedTail } from "@/hooks/useMergedTail";
|
||||
import { TailStatusPill } from "@/components/TailStatusPill";
|
||||
import { fmt } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CasAdjustment, Remittance, RemittanceStatus } from "@/types";
|
||||
@@ -39,19 +40,19 @@ const STATUS_OPTIONS: FilterChipOption[] = [
|
||||
*/
|
||||
function AdjustmentRow({ adj }: { adj: CasAdjustment }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 py-1.5 border-b border-border/40 last:border-0">
|
||||
<div className="flex items-start justify-between gap-4 py-2 border-b border-border/30 last:border-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-mono text-[11px] text-muted-foreground">
|
||||
<div className="mono text-[10.5px] text-muted-foreground">
|
||||
{adj.group}-{adj.reason}
|
||||
{adj.quantity !== null ? (
|
||||
<span className="ml-2 text-muted-foreground/70">
|
||||
<span className="ml-2 text-muted-foreground/60">
|
||||
qty {adj.quantity}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-[13px] text-foreground/90 truncate">{adj.label}</div>
|
||||
<div className="text-[12.5px] text-foreground/90 truncate">{adj.label}</div>
|
||||
</div>
|
||||
<div className="display num text-[13px] tabular-nums whitespace-nowrap text-muted-foreground">
|
||||
<div className="display mono text-[12.5px] tabular-nums whitespace-nowrap text-muted-foreground">
|
||||
{fmt.usdPrecise(adj.amount)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,9 +63,6 @@ export function Remittances() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState<RemittanceStatus | null>(null);
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
||||
// Row navigation (j/k) lives in parallel with `expanded`; selection
|
||||
// is purely a focus-state indicator and does NOT auto-expand the
|
||||
// row (so j/k doesn't fight the existing click-to-expand gesture).
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
|
||||
@@ -75,13 +73,6 @@ export function Remittances() {
|
||||
offset: (page - 1) * PAGE_SIZE,
|
||||
});
|
||||
|
||||
// SP5 live-tail wiring (sub-project 5, Phase 5 Task 23). The tail
|
||||
// stream emits every remittance that lands — including ones that
|
||||
// don't match the user's current `status` filter chip. Drop those
|
||||
// tail items so a newly-arrived remit that wouldn't match the page's
|
||||
// intent doesn't flash into the table. Base items are NOT filtered
|
||||
// here (they reflect whatever the list query returned, matching the
|
||||
// existing page behavior).
|
||||
const tailFilterFn = useMemo(
|
||||
() => (r: Remittance) => {
|
||||
if (status && r.status !== status) return false;
|
||||
@@ -123,15 +114,10 @@ export function Remittances() {
|
||||
setSelectedIndex((i) => {
|
||||
if (items.length === 0) return null;
|
||||
if (i === null) return items.length - 1;
|
||||
// Add items.length before the modulo so the negative index
|
||||
// (first row → wrap to last) wraps correctly.
|
||||
return (i - 1 + items.length) % items.length;
|
||||
});
|
||||
}, [items.length]);
|
||||
|
||||
// `enabled: !helpOpen` so j/k yields to the cheatsheet while it's
|
||||
// open (the cheatsheet's own dismiss listener still closes it on
|
||||
// any non-`?` keypress). See Acks.tsx for the full rationale.
|
||||
useRowKeyboard({
|
||||
enabled: !helpOpen && items.length > 0,
|
||||
onNext: moveNext,
|
||||
@@ -146,17 +132,19 @@ export function Remittances() {
|
||||
open={helpOpen}
|
||||
onClose={() => setHelpOpen(false)}
|
||||
/>
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
<header>
|
||||
<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" />
|
||||
Remittances
|
||||
</div>
|
||||
<h1 className="text-[22px] font-semibold tracking-tight">835 remittances</h1>
|
||||
<p className="text-muted-foreground mt-1.5 text-[14px]">
|
||||
Electronic remittance advice from payers, matched to submitted claims.
|
||||
</p>
|
||||
</header>
|
||||
<div className="space-y-6 lg:space-y-8 animate-fade-in">
|
||||
<PageHeader
|
||||
eyebrow="Remittances"
|
||||
title={<>835 <span className="display italic text-muted-foreground">remittances</span></>}
|
||||
subtitle="Electronic remittance advice from payers, matched to submitted claims."
|
||||
actions={
|
||||
<TailStatusPill
|
||||
status={tailStatus}
|
||||
lastEventAt={tailLastEventAt}
|
||||
onReconnect={forceReconnect}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{isError ? (
|
||||
<ErrorState
|
||||
@@ -166,47 +154,37 @@ export function Remittances() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-3">
|
||||
<div className="surface rounded-xl p-5">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||
Remits
|
||||
<div className="grid gap-3.5 grid-cols-2 lg:grid-cols-3">
|
||||
<div className="surface-2 rounded-xl p-5">
|
||||
<div className="eyebrow">Remits</div>
|
||||
<div className="display mono text-[28px] leading-none mt-3 text-foreground">
|
||||
{fmt.num(data?.total ?? 0)}
|
||||
</div>
|
||||
<div className="display text-[28px] leading-none mt-3">{fmt.num(data?.total ?? 0)}</div>
|
||||
</div>
|
||||
<div className="surface rounded-xl p-5">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||
Total paid
|
||||
<div className="surface-2 rounded-xl p-5">
|
||||
<div className="eyebrow">Total paid</div>
|
||||
<div className="display mono text-[28px] leading-none mt-3 text-[hsl(var(--success))]">
|
||||
{fmt.usd(total.paid)}
|
||||
</div>
|
||||
<div className="display text-[28px] leading-none mt-3">{fmt.usd(total.paid)}</div>
|
||||
</div>
|
||||
<div className="surface rounded-xl p-5">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||
Adjustments
|
||||
<div className="surface-2 rounded-xl p-5">
|
||||
<div className="eyebrow">Adjustments</div>
|
||||
<div className="display mono text-[28px] leading-none mt-3 text-[hsl(var(--warning))]">
|
||||
{fmt.usd(total.adjustments)}
|
||||
</div>
|
||||
<div className="display text-[28px] leading-none mt-3">{fmt.usd(total.adjustments)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface rounded-xl p-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<FilterChips
|
||||
options={STATUS_OPTIONS}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setStatus((v as RemittanceStatus | null) ?? null);
|
||||
setPage(1);
|
||||
}}
|
||||
eyebrow="Status"
|
||||
/>
|
||||
{/* SP5: live-tail status pill, right-aligned in the toolbar. */}
|
||||
<div className="ml-auto">
|
||||
<TailStatusPill
|
||||
status={tailStatus}
|
||||
lastEventAt={tailLastEventAt}
|
||||
onReconnect={forceReconnect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FilterChips
|
||||
options={STATUS_OPTIONS}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setStatus((v as RemittanceStatus | null) ?? null);
|
||||
setPage(1);
|
||||
}}
|
||||
eyebrow="Status"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="surface rounded-xl overflow-hidden">
|
||||
@@ -251,12 +229,6 @@ export function Remittances() {
|
||||
className={cn(
|
||||
"animate-row-flash",
|
||||
isSelected && [
|
||||
// Same accent-tinted highlight as Acks:
|
||||
// background tint + inset ring + left
|
||||
// accent strip, layered on top of the
|
||||
// TableRow primitive's
|
||||
// `data-[state=selected]:bg-muted` to
|
||||
// win visually.
|
||||
"bg-accent/10 ring-1 ring-inset ring-accent/40 shadow-[inset_2px_0_0_0_hsl(var(--accent))]",
|
||||
],
|
||||
)}
|
||||
@@ -283,28 +255,28 @@ export function Remittances() {
|
||||
)
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="display num text-[13px]">{r.id}</TableCell>
|
||||
<TableCell className="display num text-[13px] text-muted-foreground">
|
||||
<TableCell className="display mono text-[12.5px]">{r.id}</TableCell>
|
||||
<TableCell className="display mono text-[12.5px] text-muted-foreground">
|
||||
{r.claimId}
|
||||
</TableCell>
|
||||
<TableCell>{r.payerName}</TableCell>
|
||||
<TableCell className="text-right display num">
|
||||
<TableCell className="text-right display mono">
|
||||
{fmt.usdPrecise(r.paidAmount)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right display num text-muted-foreground">
|
||||
<TableCell className="text-right display mono text-muted-foreground">
|
||||
{r.adjustmentAmount > 0 ? fmt.usdPrecise(r.adjustmentAmount) : "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RemitStatusBadge status={r.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground num text-[13px]">
|
||||
<TableCell className="text-muted-foreground mono text-[12px]">
|
||||
{fmt.dateShort(r.receivedDate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isOpen && hasAdjustments ? (
|
||||
<TableRow
|
||||
key={`${r.id}-${dataUpdatedAt}-detail`}
|
||||
className="bg-muted/30 hover:bg-muted/30"
|
||||
className="bg-muted/20 hover:bg-muted/20"
|
||||
>
|
||||
<TableCell />
|
||||
<TableCell colSpan={7} className="py-3">
|
||||
@@ -314,7 +286,7 @@ export function Remittances() {
|
||||
strokeWidth={1.5}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||
<div className="eyebrow">
|
||||
Adjustments ({r.adjustments!.length})
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,4 +320,4 @@ export function Remittances() {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user