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
+45 -25
View File
@@ -15,30 +15,49 @@ import { cn } from "@/lib/utils";
const kindConfig: Record<
Activity["kind"],
{ icon: LucideIcon; tone: string }
{ icon: LucideIcon; tone: string; tint: string }
> = {
claim_submitted: { icon: FileText, tone: "text-[hsl(var(--accent))]" },
claim_accepted: { icon: CheckCircle2, tone: "text-[hsl(var(--success))]" },
claim_paid: { icon: Banknote, tone: "text-[hsl(var(--success))]" },
claim_denied: { icon: XCircle, tone: "text-destructive" },
remit_received: { icon: Pill, tone: "text-[hsl(var(--warning))]" },
provider_added: { icon: UserPlus, tone: "text-foreground" },
// `manual_match` is emitted by the backend when an operator manually
// pairs a remit to a claim from the Reconciliation page — see
// `cyclone.store` `record_manual_match`. Wrench is the closest fit;
// we keep it neutral-tone so it doesn't read as a status.
manual_match: { icon: Wrench, tone: "text-muted-foreground" },
claim_submitted: {
icon: FileText,
tone: "text-[hsl(var(--accent))]",
tint: "bg-accent/10",
},
claim_accepted: {
icon: CheckCircle2,
tone: "text-[hsl(var(--success))]",
tint: "bg-[hsl(var(--success)/0.12)]",
},
claim_paid: {
icon: Banknote,
tone: "text-[hsl(var(--success))]",
tint: "bg-[hsl(var(--success)/0.12)]",
},
claim_denied: {
icon: XCircle,
tone: "text-destructive",
tint: "bg-destructive/12",
},
remit_received: {
icon: Pill,
tone: "text-[hsl(var(--warning))]",
tint: "bg-[hsl(var(--warning)/0.12)]",
},
provider_added: {
icon: UserPlus,
tone: "text-foreground",
tint: "bg-muted/60",
},
manual_match: {
icon: Wrench,
tone: "text-muted-foreground",
tint: "bg-muted/40",
},
};
// Defensive fallback: if the backend ever emits a kind this UI doesn't
// know about (e.g. a new kind landed server-side before the frontend
// picked it up), render a neutral icon instead of crashing the whole
// Activity Log page. The kindConfig map above stays exhaustive for
// known kinds so TypeScript still catches missing entries at compile
// time; this only protects the runtime path.
const FALLBACK_KIND: { icon: LucideIcon; tone: string } = {
const FALLBACK_KIND: { icon: LucideIcon; tone: string; tint: string } = {
icon: Circle,
tone: "text-muted-foreground",
tint: "bg-muted/40",
};
export function ActivityFeed({
@@ -50,7 +69,7 @@ export function ActivityFeed({
}) {
if (items.length === 0) {
return (
<div className="text-sm text-muted-foreground px-2 py-6 text-center">
<div className="text-[13px] text-muted-foreground px-2 py-6 text-center">
{emptyMessage}
</div>
);
@@ -67,28 +86,29 @@ export function ActivityFeed({
>
<div
className={cn(
"mt-0.5 h-7 w-7 shrink-0 rounded-md bg-muted/50 flex items-center justify-center",
"mt-0.5 h-7 w-7 shrink-0 rounded-md ring-1 ring-inset ring-border/40 flex items-center justify-center",
cfg.tint,
cfg.tone
)}
>
<Icon className="h-3.5 w-3.5" strokeWidth={1.75} />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm text-foreground/95 leading-snug">
<div className="text-[13px] text-foreground/95 leading-snug">
{a.message}
</div>
<div className="text-[11px] text-muted-foreground mt-0.5 flex items-center gap-2">
<div className="mono text-[10.5px] text-muted-foreground mt-0.5 flex items-center gap-2">
<span>{fmt.relative(a.timestamp)}</span>
{a.npi ? (
<>
<span className="opacity-40">·</span>
<span className="num">{a.npi}</span>
<span>{a.npi}</span>
</>
) : null}
{a.amount ? (
<>
<span className="opacity-40">·</span>
<span className="num">{fmt.usd(a.amount)}</span>
<span>{fmt.usd(a.amount)}</span>
</>
) : null}
</div>
+22 -26
View File
@@ -1,6 +1,7 @@
import { useMemo, useState } from "react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { ApiError } from "@/lib/api";
import { cn } from "@/lib/utils";
import { useClaimDetail } from "@/hooks/useClaimDetail";
import { useDrawerKeyboard } from "@/hooks/useDrawerKeyboard";
import { useLineReconciliation } from "@/hooks/useLineReconciliation";
@@ -145,7 +146,7 @@ export function ClaimDrawer({
// `aria-describedby={undefined}` suppresses the Radix warning
// about a missing description — the drawer content is its own
// description and there's nothing useful to point at.
className="fixed right-0 top-0 h-full w-full max-w-2xl translate-x-0 translate-y-0 rounded-none border-l border-[color:var(--m-border-heavy)] bg-[color:var(--m-surface)] p-0"
className="fixed right-0 top-0 h-full w-full max-w-2xl translate-x-0 translate-y-0 rounded-none border-l border-[color:var(--m-border-heavy)]/60 bg-[color:var(--m-surface)] p-0 shadow-[-24px_0_60px_-12px_rgba(0,0,0,0.6)]"
data-testid="claim-drawer"
aria-describedby={undefined}
>
@@ -166,47 +167,42 @@ export function ClaimDrawer({
>
<ClaimDrawerHeader claim={data} onClose={onClose} />
<div
className="flex gap-2 px-6 pt-4 border-b"
style={{ borderColor: "var(--tt-bg)" }}
className="flex gap-0 px-6 border-b border-[color:var(--surface-line)]/40"
data-testid="claim-drawer-tabs"
>
<button
type="button"
onClick={() => setActiveTab("details")}
className="font-mono text-xs uppercase tracking-wider px-3 py-2"
style={{
color:
activeTab === "details"
? "var(--tt-ink, currentColor)"
: "var(--tt-ink-dim, rgba(255,255,255,0.5))",
borderBottom:
activeTab === "details"
? "2px solid var(--tt-amber)"
: "2px solid transparent",
}}
className={cn(
"relative px-3 py-2.5 text-[10.5px] font-semibold uppercase tracking-[0.18em] transition-colors",
activeTab === "details"
? "text-[color:var(--surface-ink)]"
: "text-[color:var(--surface-ink-3)] hover:text-[color:var(--surface-ink-2)]"
)}
data-testid="tab-button-details"
data-active={activeTab === "details" ? "true" : "false"}
>
Details
{activeTab === "details" ? (
<span className="absolute inset-x-0 -bottom-px h-[2px] bg-[color:var(--accent)]" />
) : null}
</button>
<button
type="button"
onClick={() => setActiveTab("line-reconciliation")}
className="font-mono text-xs uppercase tracking-wider px-3 py-2"
style={{
color:
activeTab === "line-reconciliation"
? "var(--tt-ink, currentColor)"
: "var(--tt-ink-dim, rgba(255,255,255,0.5))",
borderBottom:
activeTab === "line-reconciliation"
? "2px solid var(--tt-amber)"
: "2px solid transparent",
}}
className={cn(
"relative px-3 py-2.5 text-[10.5px] font-semibold uppercase tracking-[0.18em] transition-colors",
activeTab === "line-reconciliation"
? "text-[color:var(--surface-ink)]"
: "text-[color:var(--surface-ink-3)] hover:text-[color:var(--surface-ink-2)]"
)}
data-testid="tab-button-line-reconciliation"
data-active={activeTab === "line-reconciliation" ? "true" : "false"}
>
Line Reconciliation
{activeTab === "line-reconciliation" ? (
<span className="absolute inset-x-0 -bottom-px h-[2px] bg-[color:var(--accent)]" />
) : null}
</button>
</div>
{activeTab === "line-reconciliation" ? (
@@ -228,7 +224,7 @@ export function ClaimDrawer({
<LineReconciliationTab data={lr.data} />
) : null
) : (
<div className="flex flex-col divide-y divide-[color:var(--m-border-heavy)]/15">
<div className="flex flex-col divide-y divide-[color:var(--surface-line)]/12">
<ValidationPanel validation={data.validation} />
<ServiceLinesTable
serviceLines={data.serviceLines}
@@ -42,11 +42,11 @@ export function ClaimDrawerError({ kind, onRetry, onClose }: ClaimDrawerErrorPro
strokeWidth={1.75}
aria-hidden
/>
<span className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-error)]">
<span className="eyebrow text-[color:var(--m-error)]">
{eyebrow}
</span>
</div>
<p className="text-sm text-[color:var(--m-ink-secondary)]">{message}</p>
<p className="text-sm text-[color:var(--m-ink-secondary)] max-w-sm">{message}</p>
<div className="flex items-center gap-2">
{kind === "network" && onRetry ? (
<Button variant="outline" size="sm" onClick={onRetry} data-testid="error-retry">
@@ -166,17 +166,18 @@ describe("ClaimDrawerHeader", () => {
stateLabel: string;
expectedToken: string;
}> = [
// secondary → bg-muted text-muted-foreground
{ state: "submitted", stateLabel: "Submitted", expectedToken: "bg-muted text-muted-foreground" },
// secondary → bg-muted/80 text-muted-foreground
// (the polished muted treatment uses /80 for a slightly subtler tone)
{ state: "submitted", stateLabel: "Submitted", expectedToken: "bg-muted/80 text-muted-foreground" },
// default → bg-primary/15 text-primary
{ state: "accepted", stateLabel: "Accepted", expectedToken: "bg-primary/15" },
// destructive → bg-destructive/15
{ state: "denied", stateLabel: "Denied", expectedToken: "bg-destructive/15" },
// success → bg-[hsl(var(--success)/0.15)] (covered by both matched and paid)
{ state: "matched", stateLabel: "Matched", expectedToken: "bg-[hsl(var(--success)/0.15)]" },
{ state: "paid", stateLabel: "Paid", expectedToken: "bg-[hsl(var(--success)/0.15)]" },
// warning → bg-[hsl(var(--warning)/0.15)]
{ state: "pending", stateLabel: "Pending", expectedToken: "bg-[hsl(var(--warning)/0.15)]" },
// success → bg-[hsl(var(--success)/0.14)] (covered by both matched and paid)
{ state: "matched", stateLabel: "Matched", expectedToken: "bg-[hsl(var(--success)/0.14)]" },
{ state: "paid", stateLabel: "Paid", expectedToken: "bg-[hsl(var(--success)/0.14)]" },
// warning → bg-[hsl(var(--warning)/0.14)]
{ state: "pending", stateLabel: "Pending", expectedToken: "bg-[hsl(var(--warning)/0.14)]" },
];
for (const c of cases) {
@@ -84,20 +84,19 @@ export function ClaimDrawerHeader({
<header
className={cn(
"flex items-start justify-between gap-4 px-6 py-5",
"border-b border-[color:var(--m-border-heavy)]",
"border-b border-[color:var(--m-border-heavy)]/40",
"bg-[color:var(--m-surface)]"
)}
data-testid="claim-drawer-header"
>
{/* Left: eyebrow + mono claim ID */}
<div className="flex flex-col gap-1 min-w-0">
<span className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
Claim
</span>
<span
data-testid="header-id"
className="font-mono text-2xl font-semibold tracking-tight text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-2xl font-semibold tracking-tight text-[color:var(--m-ink-primary)]"
>
{claim.id}
</span>
@@ -115,8 +114,7 @@ export function ClaimDrawerHeader({
</Badge>
<span
data-testid="header-amount"
className="font-mono text-lg tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-lg tabular-nums text-[color:var(--m-ink-primary)]"
>
{fmt.usdPrecise(claim.billedAmount)}
</span>
@@ -62,9 +62,10 @@ describe("DiagnosesList", () => {
expect(text).toContain("(3)");
// Eyebrow style (uppercase + tracking) — sniff the className.
// The shared `.eyebrow` class in index.css consolidates the
// eyebrow treatment; tests assert it lands on the section label.
const cls = label?.className ?? "";
expect(cls).toContain("uppercase");
expect(cls).toContain("tracking-[0.18em]");
expect(cls).toContain("eyebrow");
unmount();
});
+12 -7
View File
@@ -35,7 +35,7 @@ export function DiagnosesList({ diagnoses }: DiagnosesListProps) {
<section className="flex flex-col gap-3 px-6 py-4">
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Diagnoses ({diagnoses.length})
</h3>
@@ -55,18 +55,23 @@ export function DiagnosesList({ diagnoses }: DiagnosesListProps) {
<li
key={`${d.code}-${i}`}
data-testid="diagnosis-item"
className="flex items-baseline gap-2 text-sm"
className="flex items-baseline gap-3 text-sm"
>
<span
className="font-mono text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-[color:var(--m-ink-primary)] font-medium"
>
{d.qualifier ? `${d.qualifier} ${d.code}` : d.code}
</span>
{desc ? (
<span className="text-[color:var(--m-ink-secondary)]">
{desc}
</span>
<>
<span
className="h-px w-3 bg-[color:var(--m-border-heavy)]/30"
aria-hidden
/>
<span className="text-[color:var(--m-ink-secondary)] italic">
{desc}
</span>
</>
) : null}
</li>
);
@@ -26,31 +26,37 @@ export function LineReconciliationTab({
return (
<section className="flex flex-col gap-4 px-6 py-4">
<header
className="flex items-baseline justify-between"
className="flex items-baseline justify-between flex-wrap gap-3"
data-testid="line-reconciliation-summary"
>
<div className="flex gap-6 font-mono tabular-nums text-sm">
<span data-testid="billed-total">
<span className="text-[color:var(--m-ink-tertiary)] text-xs uppercase mr-2">
<div className="flex gap-5 font-mono tabular-nums text-sm">
<span data-testid="billed-total" className="flex flex-col gap-0.5">
<span className="text-[color:var(--m-ink-tertiary)] eyebrow">
Billed
</span>
{fmt.usdPrecise(Number(summary.billedTotal))}
<span className="text-[color:var(--m-ink-primary)] font-semibold">
{fmt.usdPrecise(Number(summary.billedTotal))}
</span>
</span>
<span data-testid="paid-total">
<span className="text-[color:var(--m-ink-tertiary)] text-xs uppercase mr-2">
<span data-testid="paid-total" className="flex flex-col gap-0.5">
<span className="text-[color:var(--m-ink-tertiary)] eyebrow">
Paid
</span>
{fmt.usdPrecise(Number(summary.paidTotal))}
<span className="text-[color:var(--m-ink-primary)] font-semibold">
{fmt.usdPrecise(Number(summary.paidTotal))}
</span>
</span>
<span data-testid="adjustments-total">
<span className="text-[color:var(--m-ink-tertiary)] text-xs uppercase mr-2">
<span data-testid="adjustments-total" className="flex flex-col gap-0.5">
<span className="text-[color:var(--m-ink-tertiary)] eyebrow">
Adjustments
</span>
{fmt.usdPrecise(Number(summary.adjustmentTotal))}
<span className="text-[color:var(--m-ink-primary)] font-semibold">
{fmt.usdPrecise(Number(summary.adjustmentTotal))}
</span>
</span>
</div>
<span
className="font-mono text-xs uppercase tracking-wider"
className="font-mono text-xs uppercase tracking-[0.14em]"
style={{
color: allMatched ? "var(--tt-amber)" : "var(--tt-oxblood)",
}}
@@ -64,7 +70,7 @@ export function LineReconciliationTab({
<div className="grid grid-cols-2 gap-4">
<div data-testid="left-column">
<h3 className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)] mb-2">
<h3 className="eyebrow text-[color:var(--m-ink-tertiary)] mb-2">
Billed (837 SV1)
</h3>
{leftRows.map((row) => (
@@ -72,7 +78,7 @@ export function LineReconciliationTab({
))}
</div>
<div data-testid="right-column">
<h3 className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)] mb-2">
<h3 className="eyebrow text-[color:var(--m-ink-tertiary)] mb-2">
Adjudicated (835 SVC)
</h3>
{rightRows.map((row) => (
@@ -108,7 +114,7 @@ function LineCard({
return (
<div
className="flex flex-col gap-1 py-2 px-3 mb-2"
className="flex flex-col gap-1 py-2 px-3 mb-2 rounded-r-md transition-colors hover:bg-[color:var(--m-ink-tertiary)]/5"
style={{
borderLeft: `3px solid ${accentColor}`,
background: "rgba(255,255,255,0.02)",
@@ -117,7 +123,7 @@ function LineCard({
>
<div className="flex items-baseline justify-between">
<div className="font-mono text-sm">
<span className="font-semibold">{procCode}</span>
<span className="font-semibold text-[color:var(--m-ink-primary)]">{procCode}</span>
{lineNumber !== null ? (
<span className="text-[color:var(--m-ink-tertiary)] ml-2">
#{lineNumber}
@@ -140,7 +146,7 @@ function LineCard({
</span>
) : null}
</div>
<div className="font-mono tabular-nums text-xs">
<div className="font-mono tabular-nums text-xs text-[color:var(--m-ink-primary)] font-semibold">
{side === "left" && cl ? <span>{fmt.usdPrecise(Number(cl.charge))}</span> : null}
{side === "right" && svc ? <span>{fmt.usdPrecise(Number(svc.payment))}</span> : null}
</div>
@@ -149,7 +155,8 @@ function LineCard({
<ul className="text-xs font-mono text-[color:var(--m-ink-tertiary)] mt-1 space-y-0.5">
{row.adjustments.map((a, i) => (
<li key={i}>
{a.groupCode} · {a.reasonCode} · {fmt.usdPrecise(Number(a.amount))}
<span className="text-[color:var(--m-ink-secondary)]">{a.groupCode}-{a.reasonCode}</span>
<span className="ml-2 tabular-nums">{fmt.usdPrecise(Number(a.amount))}</span>
</li>
))}
</ul>
@@ -69,9 +69,10 @@ describe("MatchedRemitCard", () => {
expect(text).toContain("matched remittance");
// Eyebrow style (uppercase + tracking) — sniff the className.
// The shared `.eyebrow` class in index.css consolidates the
// eyebrow treatment; tests assert it lands on the section label.
const cls = label?.className ?? "";
expect(cls).toContain("uppercase");
expect(cls).toContain("tracking-[0.18em]");
expect(cls).toContain("eyebrow");
unmount();
});
@@ -56,21 +56,20 @@ export function MatchedRemitCard({ matchedRemittance }: MatchedRemitCardProps) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Matched Remittance
</h3>
<div
data-testid="matched-remit-card-inner"
className="flex flex-col gap-4 rounded-lg border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)] p-4 sm:flex-row sm:items-start sm:justify-between"
className="flex flex-col gap-4 rounded-lg border border-[color:var(--m-border-heavy)]/30 bg-[color:var(--m-surface)]/60 p-4 sm:flex-row sm:items-start sm:justify-between"
>
{/* Left: remit id + status badge + received date */}
<div className="flex min-w-0 flex-col gap-2">
<span
data-testid="matched-remit-id"
className="font-mono text-sm text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-sm text-[color:var(--m-ink-primary)]"
>
{id}
</span>
@@ -95,8 +94,7 @@ export function MatchedRemitCard({ matchedRemittance }: MatchedRemitCardProps) {
<div className="flex flex-col items-start gap-2 sm:items-end">
<span
data-testid="matched-remit-total-paid"
className="font-mono text-2xl font-semibold tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="display text-3xl text-[color:var(--m-ink-primary)] tabular-nums"
>
{fmt.usdPrecise(totalPaid)}
</span>
@@ -105,6 +103,7 @@ export function MatchedRemitCard({ matchedRemittance }: MatchedRemitCardProps) {
size="sm"
onClick={handleViewRemittance}
data-testid="view-remittance-link"
className="gap-1 text-[color:var(--m-ink-secondary)] hover:text-[color:var(--m-ink-primary)]"
>
View remittance
<ArrowRight
@@ -120,7 +119,7 @@ export function MatchedRemitCard({ matchedRemittance }: MatchedRemitCardProps) {
<span
data-testid="matched-remit-line-counts"
data-all-matched={allMatched ? "true" : "false"}
className="font-mono text-xs uppercase tracking-wider"
className="mono text-xs uppercase tracking-[0.14em]"
style={{ color: allMatched ? "var(--tt-amber)" : "var(--tt-oxblood)" }}
>
lines: {matchedLines}/{totalLines} matched
@@ -86,9 +86,10 @@ describe("PartiesGrid", () => {
expect(text).toContain("parties");
// Eyebrow style (uppercase + tracking) — sniff the className.
// The shared `.eyebrow` class in index.css consolidates the
// eyebrow treatment; tests assert it lands on the section label.
const cls = label?.className ?? "";
expect(cls).toContain("uppercase");
expect(cls).toContain("tracking-[0.18em]");
expect(cls).toContain("eyebrow");
unmount();
});
+6 -7
View File
@@ -25,7 +25,7 @@ function AddressBlock({ address }: { address: AddressLike }) {
const a = address as ClaimDetailAddress;
return (
<div
className="text-xs leading-relaxed text-[color:var(--m-ink-secondary)]"
className="mt-1 border-t border-dashed border-[color:var(--m-border-heavy)]/30 pt-2 text-xs leading-relaxed text-[color:var(--m-ink-secondary)]"
data-testid="party-address"
>
<div>{a.line1}</div>
@@ -58,17 +58,16 @@ function PartyCard({
return (
<div
data-testid={testId}
className="flex flex-col gap-2 rounded-lg border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)] px-4 py-3"
className="flex flex-col gap-2 rounded-lg border border-[color:var(--m-border-heavy)]/30 bg-[color:var(--m-surface)]/60 px-4 py-3 transition-colors hover:border-[color:var(--m-border-heavy)]/50"
>
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
{label}
</span>
<div className="text-base font-medium text-[color:var(--m-ink-primary)]">
<div className="display text-lg text-[color:var(--m-ink-primary)]">
{name}
</div>
<div
className="font-mono text-[13px] text-[color:var(--m-ink-secondary)] tabular-nums"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-[12.5px] text-[color:var(--m-ink-secondary)] tabular-nums"
>
{identity}
</div>
@@ -97,7 +96,7 @@ export function PartiesGrid({ parties }: PartiesGridProps) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Parties
</h3>
@@ -52,9 +52,10 @@ describe("RawSegmentsPanel", () => {
expect(text).toContain("(3)");
// Eyebrow style (uppercase + tracking) — sniff the className.
// The shared `.eyebrow` class in index.css consolidates the
// eyebrow treatment; tests assert it lands on the section label.
const cls = label?.className ?? "";
expect(cls).toContain("uppercase");
expect(cls).toContain("tracking-[0.18em]");
expect(cls).toContain("eyebrow");
unmount();
});
@@ -33,16 +33,17 @@ export function RawSegmentsPanel({ rawSegments }: RawSegmentsPanelProps) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Raw Segments ({rawSegments.length})
</h3>
<details className="rounded-lg border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)]">
<details className="group rounded-lg border border-[color:var(--m-border-heavy)]/30 bg-[color:var(--m-surface)]/40 overflow-hidden transition-colors hover:border-[color:var(--m-border-heavy)]/50">
<summary
className="cursor-pointer select-none px-4 py-2 text-sm text-[color:var(--m-ink-secondary)] hover:text-[color:var(--m-ink-primary)]"
className="cursor-pointer select-none px-4 py-2.5 text-sm text-[color:var(--m-ink-secondary)] hover:text-[color:var(--m-ink-primary)] transition-colors [&::-webkit-details-marker]:hidden flex items-center gap-2"
data-testid="raw-segments-summary"
>
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[color:var(--m-ink-tertiary)] group-open:bg-[color:var(--m-accent)] transition-colors" />
{rawSegments.length === 0
? `Show raw segments`
: `Show ${rawSegments.length} raw segment${rawSegments.length === 1 ? "" : "s"}`}
@@ -58,7 +59,7 @@ export function RawSegmentsPanel({ rawSegments }: RawSegmentsPanelProps) {
) : (
<pre
data-testid="raw-segments-pre"
className="overflow-x-auto whitespace-pre-wrap px-4 pb-3 pt-1 text-[12px] leading-relaxed text-[color:var(--m-ink-primary)] font-mono"
className="overflow-x-auto whitespace-pre-wrap border-t border-[color:var(--m-border-heavy)]/20 px-4 pb-3 pt-3 text-[12px] leading-relaxed text-[color:var(--m-ink-primary)] font-mono"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{rawSegments.map((segment, i) => (
@@ -68,9 +68,10 @@ describe("ServiceLinesTable", () => {
expect(text).toContain("(3)");
// Eyebrow style (uppercase + tracking) — sniff the className.
// The shared `.eyebrow` class in index.css consolidates the
// eyebrow treatment; tests assert it lands on the section label.
const cls = label?.className ?? "";
expect(cls).toContain("uppercase");
expect(cls).toContain("tracking-[0.18em]");
expect(cls).toContain("eyebrow");
unmount();
});
@@ -49,7 +49,7 @@ export function ServiceLinesTable({
<section className="flex flex-col gap-3 px-6 py-4">
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Service Lines ({serviceLines.length})
</h3>
@@ -82,6 +82,7 @@ export function ServiceLinesTable({
key={line.lineNumber}
data-testid="service-line-row"
data-line-number={line.lineNumber}
className="row-hover"
>
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
{line.lineNumber}
@@ -90,7 +91,7 @@ export function ServiceLinesTable({
{formatProcedure(line)}
</TableCell>
<TableCell
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)] font-semibold"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{fmt.usdPrecise(line.charge)}
@@ -65,9 +65,10 @@ describe("StateHistoryTimeline", () => {
expect(text).toContain("(3)");
// Eyebrow style (uppercase + tracking) — sniff the className.
// The shared `.eyebrow` class in index.css consolidates the
// eyebrow treatment; tests assert it lands on the section label.
const cls = label?.className ?? "";
expect(cls).toContain("uppercase");
expect(cls).toContain("tracking-[0.18em]");
expect(cls).toContain("eyebrow");
unmount();
});
@@ -64,7 +64,7 @@ export function StateHistoryTimeline({ history }: StateHistoryTimelineProps) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
State History ({history.length})
</h3>
@@ -79,7 +79,7 @@ export function StateHistoryTimeline({ history }: StateHistoryTimelineProps) {
) : (
<ol
data-testid="state-history-timeline"
className="relative ml-2 flex flex-col gap-3 border-l-2 border-[color:var(--m-border-heavy)] pl-5"
className="relative ml-2 flex flex-col gap-3.5 border-l border-dashed border-[color:var(--m-border-heavy)]/40 pl-6"
>
{history.map((event, i) => (
<li
@@ -91,19 +91,18 @@ export function StateHistoryTimeline({ history }: StateHistoryTimelineProps) {
<span
data-testid="state-history-dot"
aria-hidden
className={`absolute -left-[25px] top-1.5 h-2 w-2 rounded-full ${dotColorFor(event.kind)}`}
className={`absolute -left-[29px] top-1.5 h-2.5 w-2.5 rounded-full ring-4 ring-[color:var(--m-surface)] ${dotColorFor(event.kind)}`}
/>
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
<span
data-testid="state-history-kind"
className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]"
className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-primary)]"
>
{kindLabel(event.kind)}
</span>
<span
data-testid="state-history-ts"
className="font-mono text-xs text-[color:var(--m-ink-secondary)] tabular-nums"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-xs text-[color:var(--m-ink-secondary)] tabular-nums"
>
{fmt.date(event.ts)} · {fmt.time(event.ts)}
</span>
@@ -111,10 +110,9 @@ export function StateHistoryTimeline({ history }: StateHistoryTimelineProps) {
{event.remittanceId ? (
<span
data-testid="history-remit-id"
className="font-mono text-xs text-[color:var(--m-ink-tertiary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-xs text-[color:var(--m-ink-tertiary)]"
>
Remit {event.remittanceId}
Remit {event.remittanceId}
</span>
) : null}
</li>
+10 -8
View File
@@ -48,16 +48,15 @@ function IssueGroup({
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<span
className="font-mono text-[12px] font-semibold tracking-tight text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-[12px] font-semibold tracking-tight text-[color:var(--m-ink-primary)]"
>
{rule}
</span>
<span
className="inline-flex items-center rounded-full bg-[color:var(--m-ink-tertiary)]/15 px-1.5 text-[10.5px] font-medium text-[color:var(--m-ink-secondary)] tabular-nums"
className="inline-flex items-center rounded-full bg-[color:var(--m-ink-tertiary)]/15 px-1.5 py-0.5 text-[10px] font-medium text-[color:var(--m-ink-secondary)] tabular-nums"
data-testid={`${testId}-count`}
>
({issues.length})
{issues.length}
</span>
</div>
<ul className="flex flex-col gap-1.5 pl-1">
@@ -109,6 +108,9 @@ export function ValidationPanel({ validation }: ValidationPanelProps) {
<span className="text-[13px] font-medium text-[color:var(--m-ink-primary)]">
All checks passed
</span>
<span className="ml-auto inline-flex items-center rounded-full bg-[color:var(--m-success)]/12 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-[color:var(--m-success)]">
Valid
</span>
</section>
);
}
@@ -118,10 +120,10 @@ export function ValidationPanel({ validation }: ValidationPanelProps) {
return (
<section
className="flex flex-col gap-3 px-6 py-4 bg-[color:var(--m-surface)]"
className="flex flex-col gap-4 px-6 py-4 bg-[color:var(--m-surface)]"
data-testid="validation-panel"
>
<span className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
Validation
</span>
@@ -130,7 +132,7 @@ export function ValidationPanel({ validation }: ValidationPanelProps) {
data-testid="validation-errors"
data-rule-group="errors"
role="alert"
className="flex flex-col gap-2 border-l-2 border-[color:var(--m-error)] bg-[color:var(--m-error-bg)] px-3 py-2.5"
className="flex flex-col gap-3 border-l-2 border-[color:var(--m-error)] bg-[hsl(var(--destructive)/0.06)] px-3.5 py-3"
>
<div className="flex items-center gap-2">
<AlertCircle
@@ -159,7 +161,7 @@ export function ValidationPanel({ validation }: ValidationPanelProps) {
<div
data-testid="validation-warnings"
data-rule-group="warnings"
className="flex flex-col gap-2 border-l-2 border-[color:var(--m-warning)] bg-[color:var(--m-warning-bg)] px-3 py-2.5"
className="flex flex-col gap-3 border-l-2 border-[color:var(--m-warning)] bg-[hsl(var(--warning)/0.08)] px-3.5 py-3"
>
<div className="flex items-center gap-2">
<AlertTriangle
@@ -1,6 +1,7 @@
import { useEffect } from "react";
import { Keyboard, X } from "lucide-react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type KeyboardCheatsheetProps = {
open: boolean;
@@ -84,8 +85,8 @@ export function KeyboardCheatsheet({ open, onClose }: KeyboardCheatsheetProps) {
<DialogContent
// Centered modal (DialogContent's default positioning is
// `left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2`).
// We narrow it to max-w-md and apply the modern palette so
// it sits on the same surface as the rest of SP4.
// We narrow it to max-w-md and apply the warm-paper palette so
// it sits on the same surface as the rest of the detail surfaces.
//
// a11y wiring:
// - `aria-labelledby` points at the visible <h2> below so
@@ -94,21 +95,27 @@ export function KeyboardCheatsheet({ open, onClose }: KeyboardCheatsheetProps) {
// - `aria-describedby={undefined}` suppresses Radix's
// missing-description warning; the cheatsheet's content
// (key list + helper text) is its own description.
className="max-w-md bg-[color:var(--m-surface)] text-[color:var(--m-ink-primary)] border border-[color:var(--m-border-heavy)]/20"
className={cn(
"max-w-md p-5",
"bg-[color:var(--m-surface)] text-[color:var(--m-ink-primary)]",
"border border-[color:var(--m-border-heavy)]/20",
"shadow-[0_24px_64px_-12px_rgb(0_0_0_/_0.5),inset_0_1px_0_0_rgb(255_255_255_/_0.6)]"
)}
aria-labelledby={CHEATSHEET_TITLE_ID}
aria-describedby={undefined}
data-testid="keyboard-cheatsheet"
>
<header className="flex items-center justify-between gap-2 mb-3 pr-10">
<header className="flex items-center justify-between gap-2 mb-4 pr-10 pb-3 border-b border-[color:var(--m-border-heavy)]/15">
<div className="flex items-center gap-2">
<Keyboard
className="h-4 w-4 text-[color:var(--m-ink-secondary)]"
strokeWidth={1.75}
<span
aria-hidden
/>
className="inline-flex h-5 w-5 items-center justify-center rounded-[5px] bg-[color:var(--m-ink-primary)]/8 text-[color:var(--m-ink-secondary)]"
>
<Keyboard className="h-3 w-3" strokeWidth={2} />
</span>
<h2
id={CHEATSHEET_TITLE_ID}
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-secondary)]"
>
Keyboard
</h2>
@@ -134,11 +141,11 @@ export function KeyboardCheatsheet({ open, onClose }: KeyboardCheatsheetProps) {
</button>
</header>
<ul className="flex flex-col gap-2.5">
<ul className="flex flex-col gap-1">
{SHORTCUTS.map((s) => (
<li
key={s.description}
className="flex items-center justify-between gap-3 text-sm"
className="flex items-center justify-between gap-3 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-[color:var(--m-ink-primary)]/[0.04]"
>
<div className="flex items-center gap-1.5">
{s.keys.map((k) => (
@@ -147,15 +154,15 @@ export function KeyboardCheatsheet({ open, onClose }: KeyboardCheatsheetProps) {
</kbd>
))}
</div>
<span className="text-[color:var(--m-ink-secondary)]">
<span className="text-[13px] text-[color:var(--m-ink-primary)]">
{s.description}
</span>
</li>
))}
</ul>
<p className="mt-4 text-xs text-[color:var(--m-ink-tertiary)]">
Press any other key to dismiss.
<p className="mt-4 pt-3 border-t border-[color:var(--m-border-heavy)]/15 eyebrow text-[color:var(--m-ink-tertiary)]">
Press any other key to dismiss
</p>
</DialogContent>
</Dialog>
+38 -13
View File
@@ -10,10 +10,19 @@ interface KpiCardProps {
hint?: string;
icon?: LucideIcon;
sparkline?: number[];
accent?: "default" | "success" | "warning" | "destructive" | "accent";
className?: string;
style?: React.CSSProperties;
}
/**
* KPI tile. Five parts top-to-bottom:
* 1. Eyebrow label + small icon — both uppercased
* 2. Big number — Geist Mono, the headline
* 3. Delta + hint row — one is always present
* 4. Sparkline strip — bottom, hairline-separated
* 5. (Optional) Accent line on the left edge
*/
export function KpiCard({
label,
value,
@@ -21,44 +30,60 @@ export function KpiCard({
hint,
icon: Icon,
sparkline,
accent = "default",
className,
style,
}: KpiCardProps) {
const accentColor = {
default: "hsl(var(--accent))",
accent: "hsl(var(--accent))",
success: "hsl(var(--success))",
warning: "hsl(var(--warning))",
destructive: "hsl(var(--destructive))",
}[accent];
return (
<div
style={style}
className={cn(
"surface rounded-xl p-5 flex flex-col gap-3 transition-colors hover:bg-muted/20",
"group relative surface-2 rounded-xl p-5 flex flex-col gap-3 transition-colors hover:bg-muted/20 overflow-hidden",
className
)}
>
{/* Left accent line — the only "color" element on the card, and
only present when the value is significant (denial rate,
etc.). Width 2px so it reads as a marker, not decoration. */}
<div
aria-hidden
className="absolute left-0 top-4 bottom-4 w-px"
style={{ backgroundColor: accentColor, opacity: 0.45 }}
/>
<div className="flex items-center justify-between">
<div className="text-[10.5px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{label}
</div>
<div className="eyebrow">{label}</div>
{Icon ? (
<div className="h-6 w-6 rounded-md bg-muted/60 flex items-center justify-center">
<Icon className="h-3 w-3 text-muted-foreground" strokeWidth={1.75} />
<div className="h-6 w-6 rounded-md bg-muted/50 ring-1 ring-inset ring-border/40 flex items-center justify-center text-muted-foreground">
<Icon className="h-3 w-3" strokeWidth={1.75} />
</div>
) : null}
</div>
<div className="display text-[32px] leading-none text-foreground mt-0.5">
<div className="display mono text-[32px] leading-[1.05] text-foreground tracking-tight">
{value}
</div>
<div className="flex items-center gap-2 text-xs min-h-[16px]">
<div className="flex items-center gap-2 text-[11px] min-h-[16px]">
{delta ? (
<span
className={cn(
"inline-flex items-center gap-0.5 font-medium num",
"inline-flex items-center gap-0.5 font-medium mono",
delta.positive ? "text-[hsl(var(--success))]" : "text-destructive"
)}
>
{delta.direction === "up" ? (
<ArrowUpRight className="h-3 w-3" />
<ArrowUpRight className="h-3 w-3" strokeWidth={2} />
) : (
<ArrowDownRight className="h-3 w-3" />
<ArrowDownRight className="h-3 w-3" strokeWidth={2} />
)}
{delta.value}
</span>
@@ -69,8 +94,8 @@ export function KpiCard({
</div>
{sparkline ? (
<div className="-mx-5 -mb-5 mt-2 pt-3 pb-4 border-t border-border/40">
<Sparkline values={sparkline} />
<div className="-mx-5 -mb-5 mt-2 pt-3 pb-3 border-t border-border/30">
<Sparkline values={sparkline} stroke={accentColor} />
</div>
) : null}
</div>
+33 -22
View File
@@ -1,27 +1,29 @@
import { Outlet } from "react-router-dom";
import { useIsFetching } from "@tanstack/react-query";
import { cn } from "@/lib/utils";
import { Sidebar } from "./Sidebar";
import { SkipLink } from "@/components/ui/skip-link";
import { SearchBar } from "./SearchBar";
/**
* App shell. Three columns:
* - Sidebar (240px, fixed)
* - Top bar (sticky, lightweight utility row)
* - Outlet (page content, max-w-[1400px] centered)
*
* The hairline scan element at the very top of the viewport is a 1px
* line that slides an accent bar across the entire width whenever
* the React Query layer has an in-flight request. The intent is to
* surface "we're doing something" without a spinner — a moving line
* reads as a sensor sweep, not a loading state.
*/
export function Layout() {
// useIsFetching() returns the number of in-flight queries and is 0 on
// first load (queries that have not yet fired are not counted). We use it
// to drive a 1px hairline scan element at the very top of the layout.
const isFetching = useIsFetching();
const showScan = isFetching > 0;
return (
<div className="relative min-h-screen z-10">
{/* Skip link — first Tab stop, jumps past the sidebar.
Hidden until focused (see .skip-link in index.css). */}
<SkipLink />
{/*
Scan element — always in the DOM (no layout shift), 1px tall, full
width. pointer-events-none so it never blocks clicks. The inner
bar uses the `scan` keyframe from tailwind.config to slide a 1/3-
width electric-blue bar across the hairline.
*/}
<div
className="fixed top-0 left-0 right-0 z-50 h-px overflow-hidden pointer-events-none transition-opacity duration-200"
style={{ opacity: showScan ? 1 : 0 }}
@@ -38,19 +40,28 @@ export function Layout() {
tabIndex={-1}
className="md:pl-60 outline-none"
>
{/*
Top bar — sits inside <main> so it shares the sidebar's right
column. Sticky to the viewport top so the search chip stays
reachable while the user scrolls long claim lists. The
hairline + horizontal padding mirrors the main content
container below it for visual continuity. The Sidebar's
own header is taller and brand-marked; this is the
lightweight "always-on utility" row.
*/}
<div className="sticky top-0 z-20 mx-auto max-w-[1400px] px-8 h-12 flex items-center justify-end bg-background/70 backdrop-blur-md border-b border-border/40">
<div
className="sticky top-0 z-20 mx-auto max-w-[1400px] px-6 lg:px-8 h-12 flex items-center justify-end gap-3 bg-background/70 backdrop-blur-md border-b border-border/40"
data-print="hide"
>
{/* Tailing pill on the left edge of the top bar — a tiny
global "system is alive" indicator. It lights up whenever
any query is in flight, complementing the hairline scan. */}
<div className="mr-auto flex items-center gap-2 text-[10.5px] mono uppercase tracking-[0.14em] text-muted-foreground/60">
<span className="relative inline-flex h-1.5 w-1.5">
<span
className={cn(
"absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--success))] opacity-60",
showScan && "animate-ping"
)}
/>
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[hsl(var(--success))]" />
</span>
{showScan ? "Working" : "Idle"}
</div>
<SearchBar />
</div>
<div className="mx-auto max-w-[1400px] px-8 py-10">
<div className="mx-auto max-w-[1400px] px-6 lg:px-8 py-8 lg:py-10">
<Outlet />
</div>
</main>
+42 -20
View File
@@ -90,22 +90,32 @@ export function NewClaimDialog() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4" />
<Button className="gap-1.5">
<Plus className="h-4 w-4" strokeWidth={2} />
New Claim
</Button>
</DialogTrigger>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Submit a new claim</DialogTitle>
<DialogDescription>
Create an 837P claim. The submission will be queued for the next
clearinghouse cycle.
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="grid gap-4">
<DialogContent className="max-w-xl overflow-hidden p-0">
<div className="px-6 pt-6 pb-2">
<span className="eyebrow text-muted-foreground">
New submission
</span>
<DialogHeader className="mt-2 space-y-1.5">
<DialogTitle className="display text-2xl">
Submit a new claim
</DialogTitle>
<DialogDescription className="text-[13px]">
Create an 837P claim. The submission will be queued for the
next clearinghouse cycle.
</DialogDescription>
</DialogHeader>
</div>
<form onSubmit={onSubmit} className="grid gap-5 px-6 pb-6">
<div className="grid gap-2">
<Label htmlFor="patientName">Patient</Label>
<Label htmlFor="patientName" className="eyebrow text-muted-foreground">
Patient
</Label>
<Input
id="patientName"
autoFocus
@@ -117,7 +127,9 @@ export function NewClaimDialog() {
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-2">
<Label>Provider (NPI)</Label>
<Label className="eyebrow text-muted-foreground">
Provider (NPI)
</Label>
<Select
value={form.providerNpi}
onValueChange={(v) => update("providerNpi", v)}
@@ -128,26 +140,30 @@ export function NewClaimDialog() {
<SelectContent>
{providers.map((p) => (
<SelectItem key={p.npi} value={p.npi}>
{p.npi} {p.name}
<span className="mono tabular-nums">{p.npi}</span>
<span className="text-muted-foreground ml-2"> {p.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="cptCode">CPT</Label>
<Label htmlFor="cptCode" className="eyebrow text-muted-foreground">
CPT
</Label>
<Input
id="cptCode"
placeholder="99213"
value={form.cptCode}
onChange={(e) => update("cptCode", e.target.value)}
className="mono tabular-nums"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-2">
<Label>Payer</Label>
<Label className="eyebrow text-muted-foreground">Payer</Label>
<Select
value={form.payerName}
onValueChange={(v) => update("payerName", v)}
@@ -173,28 +189,34 @@ export function NewClaimDialog() {
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="billedAmount">Billed amount (USD)</Label>
<Label htmlFor="billedAmount" className="eyebrow text-muted-foreground">
Billed amount
</Label>
<Input
id="billedAmount"
inputMode="decimal"
placeholder="0.00"
value={form.billedAmount}
onChange={(e) => update("billedAmount", e.target.value)}
className="mono tabular-nums"
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="notes">Notes (optional)</Label>
<Label htmlFor="notes" className="eyebrow text-muted-foreground">
Notes <span className="opacity-50">(optional)</span>
</Label>
<Textarea
id="notes"
placeholder="Internal notes — not transmitted to payer"
value={form.notes}
onChange={(e) => update("notes", e.target.value)}
className="min-h-[80px]"
/>
</div>
<DialogFooter>
<DialogFooter className="border-t border-border/40 -mx-6 -mb-6 mt-1 px-6 py-4 bg-background/40">
<Button
type="button"
variant="ghost"
@@ -202,7 +224,7 @@ export function NewClaimDialog() {
>
Cancel
</Button>
<Button type="submit" disabled={submitting}>
<Button type="submit" disabled={submitting} className="gap-1.5">
{submitting ? "Submitting…" : "Submit claim"}
</Button>
</DialogFooter>
+51
View File
@@ -0,0 +1,51 @@
import * as React from "react";
import { cn } from "@/lib/utils";
/**
* The shared page header. Every page renders one — the eyebrow +
* display title + optional subtitle + actions gives every screen a
* consistent top, with the editorial serif headline providing a
* moment of weight in an otherwise utility-heavy UI.
*
* The eyebrow carries an optional live status pill on the right so
* pages with a live-tail (Claims, Remittances, Activity) can show the
* connection state inline with the page title, not in the toolbar.
*/
export function PageHeader({
eyebrow,
title,
subtitle,
actions,
status,
className,
}: {
eyebrow: string;
title: React.ReactNode;
subtitle?: React.ReactNode;
actions?: React.ReactNode;
status?: React.ReactNode;
className?: string;
}) {
return (
<header className={cn("flex items-end justify-between gap-6 flex-wrap", className)}>
<div className="min-w-0">
<div className="eyebrow flex items-center gap-2 mb-2.5">
<span className="inline-block h-px w-6 bg-foreground/20" />
{eyebrow}
</div>
<h1 className="display text-[28px] sm:text-[32px] leading-[1.1] text-foreground">
{title}
</h1>
{subtitle ? (
<p className="text-muted-foreground mt-2 text-[13.5px] leading-relaxed max-w-2xl">
{subtitle}
</p>
) : null}
</div>
<div className="flex items-center gap-3 flex-wrap justify-end">
{status}
{actions}
</div>
</header>
);
}
@@ -85,24 +85,22 @@ function CasRow({ group }: { group: CasGroup }) {
<div
data-testid="cas-row"
data-carc={`${group.group}-${group.reason}`}
className="grid grid-cols-[6rem_1fr_5rem_6rem] items-baseline gap-3 px-4 py-2 border-b border-[color:var(--m-border-heavy)]/10 last:border-b-0"
className="grid grid-cols-[6rem_1fr_5rem_6rem] items-baseline gap-3 px-4 py-2 border-b border-[color:var(--m-border-heavy)]/10 last:border-b-0 transition-colors hover:bg-[color:var(--m-surface)]/50"
>
<span
className="font-mono text-sm tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-sm tabular-nums text-[color:var(--m-ink-primary)]"
>
{group.group}-{group.reason}
</span>
<span className="text-sm text-[color:var(--m-ink-secondary)]">{group.label}</span>
<span
className="text-right font-mono text-sm tabular-nums text-[color:var(--m-ink-tertiary)]"
className="text-right mono text-sm tabular-nums text-[color:var(--m-ink-tertiary)]"
data-testid="cas-count"
>
×{group.count}
</span>
<span
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="text-right mono text-base tabular-nums text-[color:var(--m-ink-primary)] font-semibold"
data-testid="cas-amount"
>
{fmt.usdPrecise(group.total)}
@@ -156,17 +154,17 @@ export function CasAdjustmentsPanel({
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-controls="cas-adjustments-list"
className="flex items-center gap-2 text-left"
className="flex items-center gap-2 text-left group"
data-testid="cas-toggle"
>
{open ? (
<ChevronDown className="h-3.5 w-3.5 text-[color:var(--m-ink-tertiary)]" aria-hidden />
<ChevronDown className="h-3.5 w-3.5 text-[color:var(--m-ink-tertiary)] group-hover:text-[color:var(--m-ink-primary)] transition-colors" aria-hidden />
) : (
<ChevronRight className="h-3.5 w-3.5 text-[color:var(--m-ink-tertiary)]" aria-hidden />
<ChevronRight className="h-3.5 w-3.5 text-[color:var(--m-ink-tertiary)] group-hover:text-[color:var(--m-ink-primary)] transition-colors" aria-hidden />
)}
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)] group-hover:text-[color:var(--m-ink-primary)] transition-colors"
>
CAS Adjustments ({groups.length})
</h3>
@@ -177,22 +175,22 @@ export function CasAdjustmentsPanel({
id="cas-adjustments-list"
data-testid="cas-adjustments-list"
className={cn(
"flex flex-col rounded-lg border border-[color:var(--m-border-heavy)]/15",
"flex flex-col rounded-lg border border-[color:var(--m-border-heavy)]/30",
"bg-[color:var(--m-surface)]/40 overflow-hidden"
)}
>
{/* Header row — labels for the 4-column grid */}
<div className="grid grid-cols-[6rem_1fr_5rem_6rem] gap-3 px-4 py-2 border-b border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)]/60">
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<div className="grid grid-cols-[6rem_1fr_5rem_6rem] gap-3 px-4 py-2 border-b border-[color:var(--m-border-heavy)]/20 bg-[color:var(--m-surface)]/60">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
CARC
</span>
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
Reason
</span>
<span className="text-right text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="text-right eyebrow text-[color:var(--m-ink-tertiary)]">
Hits
</span>
<span className="text-right text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="text-right eyebrow text-[color:var(--m-ink-tertiary)]">
Amount
</span>
</div>
@@ -226,7 +224,7 @@ function PerLineSection({ perLine }: { perLine: PerLineCasGroup[] }) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Per-line Adjustments ({perLine.length})
</h3>
@@ -246,7 +244,7 @@ function PerLineRow({ group }: { group: PerLineCasGroup }) {
<div
data-testid="per-line-cas-row"
data-line-number={group.slp.lineNumber}
className="flex flex-col gap-1 px-3 py-2 rounded-md border border-[color:var(--m-border-heavy)]/10 bg-[color:var(--m-surface)]/40"
className="flex flex-col gap-1 px-3 py-2 rounded-md border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)]/40 transition-colors hover:border-[color:var(--m-border-heavy)]/30"
>
<div className="flex items-baseline justify-between font-mono text-sm">
<span className="text-[color:var(--m-ink-primary)]">
@@ -256,7 +254,7 @@ function PerLineRow({ group }: { group: PerLineCasGroup }) {
</span>
</span>
<span
className="tabular-nums text-[color:var(--m-ink-secondary)]"
className="tabular-nums text-[color:var(--m-ink-primary)] font-semibold"
data-testid="per-line-cas-total"
>
{fmt.usdPrecise(total)}
@@ -270,7 +268,8 @@ function PerLineRow({ group }: { group: PerLineCasGroup }) {
data-testid="per-line-cas-entry"
data-carc={`${a.groupCode}-${a.reasonCode}`}
>
{a.groupCode}-{a.reasonCode} · {fmt.usdPrecise(Number(a.amount))}
<span className="text-[color:var(--m-ink-secondary)]">{a.groupCode}-{a.reasonCode}</span>
<span className="ml-2 tabular-nums">{fmt.usdPrecise(Number(a.amount))}</span>
</li>
))}
</ul>
@@ -296,7 +295,7 @@ function ClaimLevelSection({
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Claim-level adjustments ({adjustments.length})
</h3>
@@ -306,12 +305,12 @@ function ClaimLevelSection({
key={a.id}
data-testid="claim-level-cas-entry"
data-carc={`${a.groupCode}-${a.reasonCode}`}
className="flex justify-between px-3 py-1 rounded-md bg-[color:var(--m-surface)]/40"
className="flex justify-between px-3 py-1.5 rounded-md bg-[color:var(--m-surface)]/40 hover:bg-[color:var(--m-surface)]/70 transition-colors"
>
<span className="text-[color:var(--m-ink-primary)]">
{a.groupCode}-{a.reasonCode}
</span>
<span className="tabular-nums text-[color:var(--m-ink-secondary)]">
<span className="tabular-nums text-[color:var(--m-ink-primary)] font-semibold">
{fmt.usdPrecise(Number(a.amount))}
</span>
</li>
@@ -79,7 +79,7 @@ export function ClaimPaymentsTable({ remit }: ClaimPaymentsTableProps) {
<section className="flex flex-col gap-3 px-6 py-4" data-testid="claim-payments-table-section">
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Claim Payments (1)
</h3>
@@ -95,7 +95,7 @@ export function ClaimPaymentsTable({ remit }: ClaimPaymentsTableProps) {
</TableRow>
</TableHeader>
<TableBody>
<TableRow data-testid="claim-payments-row">
<TableRow data-testid="claim-payments-row" className="row-hover">
<TableCell className="font-mono text-[color:var(--m-ink-primary)]">
{claimLabel}
</TableCell>
@@ -103,19 +103,19 @@ export function ClaimPaymentsTable({ remit }: ClaimPaymentsTableProps) {
{remit.status}
</TableCell>
<TableCell
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)] font-semibold"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{hasCharge ? fmt.usdPrecise(remit.totalCharge as number) : "—"}
</TableCell>
<TableCell
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
className="text-right font-mono text-base tabular-nums text-[color:var(--m-success)] font-semibold"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{fmt.usdPrecise(remit.paidAmount)}
</TableCell>
<TableCell
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
className="text-right font-mono text-base tabular-nums text-[color:var(--m-warning)] font-semibold"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{Math.abs(remit.adjustmentAmount) > 0
@@ -128,7 +128,7 @@ export function ClaimPaymentsTable({ remit }: ClaimPaymentsTableProps) {
{summary.length > 0 ? (
<div className="flex flex-col gap-2 pt-2" data-testid="claim-payments-carc-summary">
<h4 className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]">
<h4 className="eyebrow text-[color:var(--m-ink-tertiary)]">
CARC breakdown
</h4>
<Table>
@@ -146,6 +146,7 @@ export function ClaimPaymentsTable({ remit }: ClaimPaymentsTableProps) {
key={carcKey(row.group, row.reason)}
data-testid="carc-row"
data-carc={carcKey(row.group, row.reason)}
className="row-hover"
>
<TableCell className="font-mono text-[color:var(--m-ink-primary)]">
{carcKey(row.group, row.reason)}
@@ -1,4 +1,5 @@
import { fmt } from "@/lib/format";
import { cn } from "@/lib/utils";
import type { RemitDetail } from "@/hooks/useRemitDetail";
type FinancialSummaryCardProps = {
@@ -27,16 +28,15 @@ function SummaryRow({
data-testid={testId}
className="flex items-baseline justify-between gap-3 py-2"
>
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
{label}
</span>
<span
className={
mono
? "font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
? "mono text-sm tabular-nums text-[color:var(--m-ink-primary)]"
: "text-sm text-[color:var(--m-ink-primary)]"
}
style={mono ? { fontFamily: "var(--m-font-mono)" } : undefined}
>
{value}
</span>
@@ -78,45 +78,56 @@ export function FinancialSummaryCard({ remit }: FinancialSummaryCardProps) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Financial Summary
</h3>
<div
data-testid="financial-summary-inner"
className="flex flex-col gap-4 rounded-lg border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)] p-4"
className="flex flex-col gap-4 rounded-lg border border-[color:var(--m-border-heavy)]/30 bg-[color:var(--m-surface)]/60 p-4"
>
{/* Headline totals — big mono digits, side by side */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* Headline totals — big display digits, side by side */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div
data-testid="summary-paid"
className="flex flex-col gap-1 rounded-md bg-[color:var(--m-surface)]/40 px-3 py-2"
className="flex flex-col gap-1.5 rounded-md border border-[color:var(--m-success)]/30 bg-[hsl(var(--success)/0.08)] px-3 py-2.5"
>
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-success)]">
Paid amount
</span>
<span
className="font-mono text-2xl font-semibold tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="display text-2xl tabular-nums text-[color:var(--m-success)]"
>
{fmt.usdPrecise(remit.paidAmount)}
</span>
</div>
<div
data-testid="summary-adjustment"
className="flex flex-col gap-1 rounded-md bg-[color:var(--m-surface)]/40 px-3 py-2"
className={cn(
"flex flex-col gap-1.5 rounded-md border px-3 py-2.5",
hasAdjustment
? "border-[color:var(--m-warning)]/40 bg-[hsl(var(--warning)/0.10)]"
: "border-[color:var(--m-border-heavy)]/20 bg-[color:var(--m-surface)]/40"
)}
>
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span
className={cn(
"eyebrow",
hasAdjustment
? "text-[color:var(--m-warning)]"
: "text-[color:var(--m-ink-tertiary)]"
)}
>
Adjustment amount
</span>
<span
className={
className={cn(
"display text-2xl tabular-nums",
hasAdjustment
? "font-mono text-2xl font-semibold tabular-nums text-[color:var(--m-ink-primary)]"
: "font-mono text-2xl tabular-nums text-[color:var(--m-ink-tertiary)]"
}
style={{ fontFamily: "var(--m-font-mono)" }}
? "text-[color:var(--m-warning)]"
: "text-[color:var(--m-ink-tertiary)]"
)}
>
{hasAdjustment ? fmt.usdPrecise(remit.adjustmentAmount) : "—"}
</span>
@@ -124,7 +135,7 @@ export function FinancialSummaryCard({ remit }: FinancialSummaryCardProps) {
</div>
{/* Supporting rows: method, payment date, check / trace number */}
<div className="flex flex-col divide-y divide-[color:var(--m-border-heavy)]/10">
<div className="flex flex-col divide-y divide-[color:var(--m-border-heavy)]/15">
<SummaryRow
testId="summary-method"
label="Payment method"
+7 -8
View File
@@ -38,7 +38,7 @@ function isEmptyAddress(addr: AddressLike): boolean {
function AddressBlock({ address }: { address: NonNullable<AddressLike> }) {
return (
<div
className="text-xs leading-relaxed text-[color:var(--m-ink-secondary)]"
className="mt-1 border-t border-dashed border-[color:var(--m-border-heavy)]/30 pt-2 text-xs leading-relaxed text-[color:var(--m-ink-secondary)]"
data-testid="party-address"
>
{address.line1 ? <div>{address.line1}</div> : null}
@@ -75,17 +75,16 @@ function PartyCard({
return (
<div
data-testid={testId}
className="flex flex-col gap-2 rounded-lg border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)] px-4 py-3"
className="flex flex-col gap-2 rounded-lg border border-[color:var(--m-border-heavy)]/30 bg-[color:var(--m-surface)]/60 px-4 py-3 transition-colors hover:border-[color:var(--m-border-heavy)]/50"
>
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
{label}
</span>
<div className="text-base font-medium text-[color:var(--m-ink-primary)]">
<div className="display text-lg text-[color:var(--m-ink-primary)]">
{name}
</div>
<div
className="font-mono text-[13px] text-[color:var(--m-ink-secondary)] tabular-nums"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-[12.5px] text-[color:var(--m-ink-secondary)] tabular-nums"
>
{identity}
</div>
@@ -154,14 +153,14 @@ export function PartiesGrid({ parties, fallbackName }: PartiesGridProps) {
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
className="eyebrow text-[color:var(--m-ink-tertiary)]"
>
Parties
</h3>
<div
data-testid="parties-grid-inner"
className="grid grid-cols-1 md:grid-cols-2 gap-4"
className="grid grid-cols-1 md:grid-cols-2 gap-3"
>
<PartyCard
testId="party-payer"
+2 -2
View File
@@ -131,7 +131,7 @@ export function RemitDrawer({
// positioning override. The Dialog primitive's default
// centered layout is suppressed so the drawer reads as a
// side-panel, not a modal.
className="fixed right-0 top-0 h-full w-full max-w-2xl translate-x-0 translate-y-0 rounded-none border-l border-[color:var(--m-border-heavy)] bg-[color:var(--m-surface)] p-0"
className="fixed right-0 top-0 h-full w-full max-w-2xl translate-x-0 translate-y-0 rounded-none border-l border-[color:var(--m-border-heavy)]/60 bg-[color:var(--m-surface)] p-0 shadow-[-24px_0_60px_-12px_rgba(0,0,0,0.6)]"
data-testid="remit-drawer"
aria-describedby={undefined}
>
@@ -151,7 +151,7 @@ export function RemitDrawer({
data-testid="remit-drawer-content"
>
<RemitDrawerHeader remit={data} onClose={onClose} />
<div className="flex flex-col divide-y divide-[color:var(--m-border-heavy)]/15">
<div className="flex flex-col divide-y divide-[color:var(--m-border-heavy)]/12">
<FinancialSummaryCard remit={data} />
<PartiesGrid parties={data} fallbackName={data.payerName} />
<ClaimPaymentsTable remit={data} />
@@ -50,11 +50,11 @@ export function RemitDrawerError({
strokeWidth={1.75}
aria-hidden
/>
<span className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-error)]">
<span className="eyebrow text-[color:var(--m-error)]">
{eyebrow}
</span>
</div>
<p className="text-sm text-[color:var(--m-ink-secondary)]">{message}</p>
<p className="text-sm text-[color:var(--m-ink-secondary)] max-w-sm">{message}</p>
<div className="flex items-center gap-2">
{kind === "network" && onRetry ? (
<Button variant="outline" size="sm" onClick={onRetry} data-testid="error-retry">
@@ -52,20 +52,19 @@ export function RemitDrawerHeader({ remit, onClose }: RemitDrawerHeaderProps) {
<header
className={cn(
"flex items-start justify-between gap-4 px-6 py-5",
"border-b border-[color:var(--m-border-heavy)]",
"border-b border-[color:var(--m-border-heavy)]/40",
"bg-[color:var(--m-surface)]"
)}
data-testid="remit-drawer-header"
>
{/* Left: eyebrow + mono remit ID + raw link */}
<div className="flex min-w-0 flex-col gap-1">
<span className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]">
<span className="eyebrow text-[color:var(--m-ink-tertiary)]">
Remittance
</span>
<span
data-testid="header-id"
className="font-mono text-2xl font-semibold tracking-tight text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-2xl font-semibold tracking-tight text-[color:var(--m-ink-primary)]"
>
{remit.id}
</span>
@@ -74,7 +73,7 @@ export function RemitDrawerHeader({ remit, onClose }: RemitDrawerHeaderProps) {
target="_blank"
rel="noreferrer"
data-testid="header-raw-link"
className="mt-1 inline-flex w-fit items-center gap-1 text-[11px] text-[color:var(--m-ink-tertiary)] hover:text-[color:var(--m-ink-secondary)]"
className="mt-1 inline-flex w-fit items-center gap-1 text-[11px] text-[color:var(--m-ink-tertiary)] hover:text-[color:var(--m-ink-secondary)] transition-colors"
>
View raw 835
<ExternalLink className="h-3 w-3" strokeWidth={1.75} aria-hidden />
@@ -93,8 +92,7 @@ export function RemitDrawerHeader({ remit, onClose }: RemitDrawerHeaderProps) {
</Badge>
<span
data-testid="header-paid"
className="font-mono text-lg tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
className="mono text-lg tabular-nums text-[color:var(--m-ink-primary)]"
>
{fmt.usdPrecise(remit.paidAmount)}
</span>
+20 -12
View File
@@ -74,12 +74,12 @@ function SearchTrigger({ onOpen }: { onOpen: () => void }) {
aria-label="Open global search (Cmd or Ctrl + K)"
className={cn(
"group inline-flex items-center gap-2.5 h-8 pl-2.5 pr-2 rounded-md",
"border border-border/60 bg-background/40 hover:bg-background/70",
"border border-border/60 bg-background/40 hover:bg-background/70 hover:border-border/80",
"text-[12px] text-muted-foreground hover:text-foreground",
"transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
"transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
)}
>
<Search className="h-3.5 w-3.5" strokeWidth={1.75} aria-hidden />
<Search className="h-3.5 w-3.5 text-muted-foreground group-hover:text-foreground transition-colors" strokeWidth={1.75} aria-hidden />
<span className="font-medium tracking-tight">Search</span>
<span className="ml-2 inline-flex items-center gap-0.5">
<kbd className="kbd">{MOD_LABEL}</kbd>
@@ -217,7 +217,7 @@ export function SearchBar() {
className={cn(
"max-w-2xl p-0 gap-0 overflow-hidden",
"bg-[color:var(--m-surface)] text-[color:var(--m-ink-primary)]",
"border border-[color:var(--m-border-heavy)]/20"
"border border-[color:var(--m-border-heavy)]/30 shadow-2xl"
)}
aria-describedby={undefined}
data-testid="search-dialog"
@@ -228,9 +228,9 @@ export function SearchBar() {
* tabbable element on open). `pl-9` makes room for the
* magnifier icon on the left.
*/}
<div className="relative border-b border-border/40">
<div className="relative border-b border-[color:var(--m-border-heavy)]/20">
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none"
className="absolute left-3.5 top-1/2 -translate-y-1/2 h-4 w-4 text-[color:var(--m-ink-tertiary)] pointer-events-none"
strokeWidth={1.75}
aria-hidden
/>
@@ -249,9 +249,10 @@ export function SearchBar() {
onKeyDown={onInputKeyDown}
data-testid="search-input"
className={cn(
"h-12 pl-9 pr-3 border-0 bg-transparent rounded-none",
"text-[14px] font-mono tracking-tight",
"shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
"h-12 pl-10 pr-3 border-0 bg-transparent rounded-none",
"text-[14px] mono tracking-tight",
"shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:bg-transparent",
"text-[color:var(--m-ink-primary)] placeholder:text-[color:var(--m-ink-tertiary)]"
)}
/>
</div>
@@ -270,7 +271,7 @@ export function SearchBar() {
</div>
{/* Footer hint — shows the keyboard contract. */}
<div className="flex items-center justify-between gap-3 px-3 py-2 border-t border-border/40 text-[10.5px] font-mono uppercase tracking-[0.12em] text-muted-foreground">
<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-t border-[color:var(--m-border-heavy)]/20 bg-[color:var(--m-surface)]/80 text-[10px] mono uppercase tracking-[0.14em] text-[color:var(--m-ink-tertiary)]">
<div className="flex items-center gap-3">
<span className="inline-flex items-center gap-1">
<kbd className="kbd"></kbd>
@@ -287,9 +288,16 @@ export function SearchBar() {
</span>
</div>
{isLoading ? (
<span className="text-muted-foreground/70">loading</span>
<span className="inline-flex items-center gap-1.5">
<span
aria-hidden
className="inline-block h-1.5 w-1.5 rounded-full animate-pulse-dot"
style={{ background: "var(--tt-amber, hsl(var(--accent)))" }}
/>
loading
</span>
) : (
<span className="text-muted-foreground/70">
<span className="tabular-nums">
{flatTotal} {flatTotal === 1 ? "result" : "results"}
</span>
)}
+20 -14
View File
@@ -59,35 +59,35 @@ function ResultRow({
}}
onMouseEnter={() => onHover(index)}
className={cn(
"w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors",
"group w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors",
"focus:outline-none",
selected
? "bg-accent/15 ring-1 ring-inset ring-accent/50"
: "hover:bg-muted/50"
: "hover:bg-muted/40"
)}
>
<span
className={cn(
"flex h-6 w-6 shrink-0 items-center justify-center rounded-md",
"flex h-6 w-6 shrink-0 items-center justify-center rounded-md transition-colors",
selected
? "bg-accent/20 text-accent"
: "bg-muted text-muted-foreground"
? "bg-accent/25 text-accent"
: "bg-muted text-muted-foreground group-hover:bg-muted/70"
)}
aria-hidden
>
<Icon className="h-3.5 w-3.5" strokeWidth={1.75} />
</span>
<span className="min-w-0 flex-1">
<span className="block text-[13px] font-medium truncate">
<span className="block text-[13px] font-medium truncate text-foreground">
{result.title}
</span>
<span className="block text-[11px] font-mono text-muted-foreground truncate">
<span className="block text-[11px] mono text-muted-foreground truncate">
{result.subtitle}
</span>
</span>
<span
className={cn(
"shrink-0 px-1.5 py-0.5 rounded text-[10px] font-mono uppercase tracking-[0.08em]",
"shrink-0 px-1.5 py-0.5 rounded text-[10px] mono uppercase tracking-[0.14em] font-medium",
"bg-muted/70 text-muted-foreground"
)}
>
@@ -100,7 +100,8 @@ function ResultRow({
/** Section header — small caps label above each group. */
function GroupHeader({ label }: { label: string }) {
return (
<div className="px-3 pt-3 pb-1.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
<div className="px-4 pt-3 pb-1.5 flex items-center gap-2 text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
<span aria-hidden className="inline-block h-px w-3 bg-border" />
{label}
</div>
);
@@ -109,11 +110,16 @@ function GroupHeader({ label }: { label: string }) {
/** Render the "no matches" state. */
function EmptyState({ query }: { query: string }) {
return (
<div className="px-4 py-10 text-center text-muted-foreground">
<div className="text-[12px] font-mono">
{query
? <>No matches for <span className="text-foreground">{query}</span></>
: "Type to search across claims, remittances, and activity."}
<div className="px-4 py-12 text-center text-muted-foreground">
<div className="mono text-[12px]">
{query ? (
<>
No matches for{" "}
<span className="text-foreground font-semibold">&ldquo;{query}&rdquo;</span>
</>
) : (
"Type to search across claims, remittances, and activity."
)}
</div>
</div>
);
+110 -48
View File
@@ -24,20 +24,45 @@ const nav = [
{ to: "/activity", label: "Activity Log", icon: Activity },
];
/**
* Sidebar. Three sections under the brand mark:
* - WORKSPACE — the day-to-day surface
* - WORKFLOWS — triage & reconciliation lanes
* - REFERENCE — long-tail batches & acks
*
* The active state uses a 2px left accent + an inset tint + a
* subtle inner ring so the selected item reads instantly. Counts
* (e.g. unmatched on Reconciliation) live as mono digits aligned to
* the right edge so the eye can scan state at a glance.
*/
export function Sidebar() {
// Surface the unmatched-bucket size on the Reconciliation nav entry so the
// operator notices when manual reconciliation is needed.
const { unmatched } = useReconciliation();
const unmatchedCount =
(unmatched.data?.claims.length ?? 0) +
(unmatched.data?.remittances.length ?? 0);
return (
<aside className="hidden md:flex md:w-60 md:flex-col md:fixed md:inset-y-0 z-30 border-r border-border/60 bg-sidebar/60 backdrop-blur-xl">
<div className="flex h-16 items-center px-6 border-b border-border/60">
<div className="flex items-center gap-2.5">
<div className="h-7 w-7 rounded-md bg-accent flex items-center justify-center shadow-sm">
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5 text-white" fill="none">
<aside
className="hidden md:flex md:w-60 md:flex-col md:fixed md:inset-y-0 z-30 border-r border-border/60 bg-sidebar/80 backdrop-blur-xl"
data-print="hide"
>
{/* Brand mark — the editorial display face carries the wordmark
while a small monospaced slug below keeps the brand
unmistakably "machine". */}
<div className="flex h-16 items-center px-5 border-b border-sidebar-border/80">
<NavLink
to="/"
end
className="flex items-center gap-2.5 group"
aria-label="Cyclone — go to dashboard"
>
<div className="relative h-7 w-7 rounded-md bg-accent flex items-center justify-center shadow-[0_0_0_1px_hsl(var(--accent)/0.4),0_0_18px_-2px_hsl(var(--accent)/0.5)]">
<svg
viewBox="0 0 24 24"
className="h-3.5 w-3.5 text-white"
fill="none"
aria-hidden
>
<path
d="M6 8h8a3 3 0 0 1 0 6h-5a3 3 0 0 0 0 6h9"
stroke="currentColor"
@@ -48,59 +73,47 @@ export function Sidebar() {
</svg>
</div>
<div className="leading-none">
<div className="text-[16px] font-semibold tracking-tight text-foreground">
<div className="display text-[19px] tracking-tight text-foreground -mt-px">
Cyclone
</div>
<div className="text-[10px] uppercase tracking-[0.14em] text-muted-foreground mt-0.5">
<div className="mono text-[9.5px] uppercase tracking-[0.18em] text-muted-foreground mt-1">
CuNtx · Claims
</div>
</div>
</div>
</NavLink>
</div>
<nav className="flex-1 px-3 py-6">
<div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground px-3 mb-2">
Workspace
</div>
<ul className="space-y-0.5">
<nav className="flex-1 overflow-y-auto px-3 py-5">
<SectionLabel>Workspace</SectionLabel>
<ul className="space-y-0.5 mb-5">
{nav.map((item) => (
<li key={item.to}>
<NavLink
to={item.to}
end={item.end}
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<item.icon className="h-4 w-4" strokeWidth={1.75} />
<span>{item.label}</span>
</NavLink>
<NavItem item={item} />
</li>
))}
</ul>
<SectionLabel>Workflows</SectionLabel>
<ul className="space-y-0.5 mb-5">
<li>
<NavLink
to="/reconciliation"
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<GitMerge className="h-4 w-4" strokeWidth={1.5} />
<GitMerge className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>Reconciliation</span>
{unmatchedCount > 0 && (
<span className="ml-auto text-[10px] font-mono tabular-nums text-warning">
{unmatchedCount > 0 ? (
<span className="ml-auto mono text-[10px] text-signal">
{unmatchedCount > 99 ? "99+" : unmatchedCount}
</span>
)}
) : null}
</NavLink>
</li>
<li>
@@ -108,30 +121,34 @@ export function Sidebar() {
to="/inbox"
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<InboxIcon className="h-4 w-4" strokeWidth={1.5} />
<InboxIcon className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>Inbox</span>
</NavLink>
</li>
</ul>
<SectionLabel>Reference</SectionLabel>
<ul className="space-y-0.5">
<li>
<NavLink
to="/acks"
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<CheckCircle2 className="h-4 w-4" strokeWidth={1.5} />
<CheckCircle2 className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>999 ACKs</span>
</NavLink>
</li>
@@ -140,14 +157,14 @@ export function Sidebar() {
to="/batches"
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<Layers className="h-4 w-4" strokeWidth={1.5} />
<Layers className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>Batches</span>
</NavLink>
</li>
@@ -156,31 +173,76 @@ export function Sidebar() {
to="/batch-diff"
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<GitCompareArrows className="h-4 w-4" strokeWidth={1.5} />
<GitCompareArrows className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>Batch diff</span>
</NavLink>
</li>
</ul>
</nav>
<div className="px-3 py-4 border-t border-border/60">
{/* Operator card — always at the bottom. Ringed, mono id, the
account role under the name. */}
<div className="px-3 py-4 border-t border-sidebar-border/80">
<div className="flex items-center gap-3 rounded-md p-2 hover:bg-muted/40 cursor-default">
<div className="h-8 w-8 rounded-full bg-accent flex items-center justify-center text-[11px] font-semibold text-white ring-1 ring-inset ring-white/10">
<div className="relative h-8 w-8 rounded-full bg-accent/20 ring-1 ring-inset ring-accent/40 flex items-center justify-center text-[10.5px] font-semibold text-accent mono">
JK
<span className="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-[hsl(var(--success))] ring-2 ring-sidebar" />
</div>
<div className="leading-tight">
<div className="text-sm font-medium">Jordan K.</div>
<div className="text-[11px] text-muted-foreground">Administrator</div>
<div className="leading-tight min-w-0">
<div className="text-[13px] font-medium truncate">Jordan K.</div>
<div className="mono text-[10px] text-muted-foreground tracking-wider uppercase">
Administrator
</div>
</div>
</div>
</div>
</aside>
);
}
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<div className="px-3 mb-1.5 eyebrow text-muted-foreground/60">
{children}
</div>
);
}
function NavItem({
item,
}: {
item: {
to: string;
label: string;
// Lucide icons accept `strokeWidth` as a wider union (string | number | null | undefined);
// we declare it as `number | string` so any Lucide icon can be assigned
// here without the prop-types assignability complaint from TS.
icon: React.ComponentType<{ className?: string; strokeWidth?: number | string }>;
end?: boolean;
};
}) {
return (
<NavLink
to={item.to}
end={item.end}
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
isActive
? "nav-active"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
)
}
>
<item.icon className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>{item.label}</span>
</NavLink>
);
}
+20 -8
View File
@@ -1,14 +1,20 @@
interface SparklineProps {
values: number[];
className?: string;
stroke?: string;
}
/**
* Minimal SVG sparkline. Normalises to its own range, draws a stroked line
* with a soft fill, and marks the last point. Stretch-friendly via
* preserveAspectRatio="none" + non-scaling-stroke.
* Minimal SVG sparkline. Normalises to its own range, draws a stroked
* line with a soft fill, and marks the last point. Uses
* preserveAspectRatio="none" + non-scaling-stroke so the chart stretches
* with its container without distorting the stroke.
*/
export function Sparkline({ values, className }: SparklineProps) {
export function Sparkline({
values,
className,
stroke = "hsl(var(--accent))",
}: SparklineProps) {
if (!values || values.length < 2) return null;
const w = 100;
const h = 28;
@@ -37,11 +43,17 @@ export function Sparkline({ values, className }: SparklineProps) {
height="36"
aria-hidden="true"
>
<path d={area} fill="hsl(var(--accent) / 0.13)" />
<defs>
<linearGradient id="sparkline-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke} stopOpacity="0.18" />
<stop offset="100%" stopColor={stroke} stopOpacity="0" />
</linearGradient>
</defs>
<path d={area} fill="url(#sparkline-fill)" />
<path
d={line}
fill="none"
stroke="hsl(var(--accent))"
stroke={stroke}
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
@@ -50,8 +62,8 @@ export function Sparkline({ values, className }: SparklineProps) {
<circle
cx={last[0]}
cy={last[1]}
r="2"
fill="hsl(var(--accent))"
r="2.2"
fill={stroke}
vectorEffect="non-scaling-stroke"
/>
</svg>
+25 -9
View File
@@ -8,7 +8,7 @@ const claimStyles: Record<ClaimStatus, { dot: string; label: string }> = {
draft: { dot: "bg-muted-foreground", label: "Draft" },
submitted: { dot: "bg-[hsl(var(--accent))]", label: "Submitted" },
accepted: { dot: "bg-[hsl(var(--success))]", label: "Accepted" },
denied: { dot: "bg-destructive", label: "Denied" },
denied: { dot: "bg-[hsl(var(--destructive))]", label: "Denied" },
paid: { dot: "bg-[hsl(var(--success))]", label: "Paid" },
pending: { dot: "bg-[hsl(var(--warning))]", label: "Pending" },
};
@@ -43,11 +43,15 @@ export function StatusBadge({
<Badge
variant="outline"
className={cn(
"uppercase tracking-wider text-[10px]",
"bg-muted text-muted-foreground border-border",
"gap-1.5 uppercase tracking-[0.14em] text-[10.5px] font-medium",
"bg-muted/40 text-muted-foreground border-border/60",
className,
)}
>
<span
aria-hidden
className="inline-block h-1.5 w-1.5 rounded-full bg-current opacity-70"
/>
{status}
</Badge>
);
@@ -62,9 +66,15 @@ export function ClaimStatusBadge({ status }: { status: ClaimStatus }) {
}
const s = claimStyles[status];
return (
<Badge variant="outline" className="bg-transparent border-border/60">
<span className={cn("h-1.5 w-1.5 rounded-full", s.dot)} />
<span className="font-normal">{s.label}</span>
<Badge
variant="outline"
className="gap-1.5 bg-transparent border-border/60 uppercase tracking-[0.14em] text-[10.5px]"
>
<span
aria-hidden
className={cn("inline-block h-1.5 w-1.5 rounded-full", s.dot)}
/>
<span className="font-medium">{s.label}</span>
</Badge>
);
}
@@ -75,9 +85,15 @@ export function RemitStatusBadge({ status }: { status: RemittanceStatus }) {
}
const s = remitStyles[status];
return (
<Badge variant="outline" className="bg-transparent border-border/60">
<span className={cn("h-1.5 w-1.5 rounded-full", s.dot)} />
<span className="font-normal">{s.label}</span>
<Badge
variant="outline"
className="gap-1.5 bg-transparent border-border/60 uppercase tracking-[0.14em] text-[10.5px]"
>
<span
aria-hidden
className={cn("inline-block h-1.5 w-1.5 rounded-full", s.dot)}
/>
<span className="font-medium">{s.label}</span>
</Badge>
);
}
+119
View File
@@ -0,0 +1,119 @@
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
export type ConnectionStatus =
| "live"
| "connecting"
| "reconnecting"
| "closed"
| "stalled"
| "error";
const STATUS_TONE: Record<
ConnectionStatus,
{ dot: string; ring: string; label: string }
> = {
live: {
dot: "bg-[hsl(var(--success))]",
ring: "ring-[hsl(var(--success))]/40",
label: "Live",
},
connecting: {
dot: "bg-[hsl(var(--signal))]",
ring: "ring-[hsl(var(--signal))]/40",
label: "Connecting",
},
reconnecting: {
dot: "bg-[hsl(var(--signal))]",
ring: "ring-[hsl(var(--signal))]/40",
label: "Reconnecting",
},
closed: {
dot: "bg-muted-foreground",
ring: "ring-muted-foreground/30",
label: "Closed",
},
stalled: {
dot: "bg-destructive",
ring: "ring-destructive/40",
label: "Stalled",
},
error: {
dot: "bg-destructive",
ring: "ring-destructive/40",
label: "Error",
},
};
/**
* Refined status pill with an animated dot. The dot pulses for any
* non-terminal status and stays solid for stable states. Sits inline
* with other header chrome and reads as a one-word status, not a
* multi-line status panel.
*/
export function StatusPill({
status,
label,
className,
}: {
status: ConnectionStatus;
label?: string;
className?: string;
}) {
const tone = STATUS_TONE[status];
const isPulsing =
status === "live" ||
status === "connecting" ||
status === "reconnecting";
return (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-card/60 px-2.5 py-1 mono text-[10.5px] uppercase tracking-[0.14em]",
className
)}
>
<span
className={cn(
"relative inline-flex h-1.5 w-1.5 rounded-full",
tone.dot,
"ring-4",
tone.ring
)}
>
{isPulsing ? (
<span
className={cn(
"absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping",
tone.dot
)}
/>
) : null}
</span>
<span className="text-foreground/80">{label ?? tone.label}</span>
</span>
);
}
/**
* Re-tick every second so a "Last event: 12s ago" subtitle stays
* current without a parent re-render. Mount-once interval.
*/
export function useTickingNow(intervalMs = 1000): number {
const [now, setNow] = useState<number>(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), intervalMs);
return () => clearInterval(id);
}, [intervalMs]);
return now;
}
export function formatAge(seconds: number): string {
if (seconds < 0) return "0s";
if (seconds < 60) return `${seconds}s`;
const m = Math.floor(seconds / 60);
const s = seconds % 60;
if (m < 60) return s === 0 ? `${m}m` : `${m}m ${s}s`;
const h = Math.floor(m / 60);
return `${h}h ${m % 60}m`;
}
+24 -63
View File
@@ -3,23 +3,20 @@
//
// Tiny presentational component used in the toolbar of `Claims`,
// `Remittances`, and `ActivityLog` to surface the connection status of
// `useTailStream`. Renders the existing `<Badge>` (variants from
// `src/components/ui/badge.tsx`) plus a "Last event: 12s ago" subtitle
// that re-ticks every second, plus a "↻ Reconnect" button that only
// appears when the connection has visibly failed (`stalled` or `error`).
//
// Variant mapping (per spec):
// live → success
// connecting → warning
// reconnecting → warning
// closed → destructive
// stalled → destructive
// error → destructive
// `useTailStream`. Renders a status dot + label + "Last event: 12s ago"
// subtitle that re-ticks every second, plus a "↻ Reconnect" button
// that only appears when the connection has visibly failed (`stalled`
// or `error`).
// ---------------------------------------------------------------------------
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { RotateCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
StatusPill,
formatAge,
useTickingNow,
type ConnectionStatus,
} from "@/components/StatusPill";
import type { TailStatus } from "@/hooks/useTailStream";
export interface TailStatusPillProps {
@@ -29,55 +26,20 @@ export interface TailStatusPillProps {
}
const STATUS_LABEL: Record<TailStatus, string> = {
connecting: "Connecting",
connecting: "Connecting",
live: "Live",
reconnecting: "Reconnecting",
reconnecting: "Reconnecting",
closed: "Closed",
stalled: "Stalled",
error: "Error",
};
type BadgeVariant = "success" | "warning" | "destructive";
const STATUS_VARIANT: Record<TailStatus, BadgeVariant> = {
live: "success",
connecting: "warning",
reconnecting: "warning",
closed: "destructive",
stalled: "destructive",
error: "destructive",
};
/**
* Render a duration in seconds as a compact human label. We deliberately
* keep the units small — under a minute is the common case for a healthy
* tail (the stall detector fires at 30s, so anything beyond is a real
* outage the user will want to investigate).
*/
function formatAge(seconds: number): string {
if (seconds < 0) return "0s";
if (seconds < 60) return `${seconds}s`;
const m = Math.floor(seconds / 60);
const s = seconds % 60;
if (m < 60) return s === 0 ? `${m}m` : `${m}m ${s}s`;
const h = Math.floor(m / 60);
return `${h}h ${m % 60}m`;
}
export function TailStatusPill({
status,
lastEventAt,
onReconnect,
}: TailStatusPillProps): React.JSX.Element {
// Re-tick every second so the "Last event: 12s ago" subtitle stays
// current without the parent having to re-render. We mount the
// interval only once per pill instance.
const [now, setNow] = useState<number>(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
const now = useTickingNow(1000);
const ageSeconds = lastEventAt
? Math.max(0, Math.floor((now - lastEventAt.getTime()) / 1000))
: null;
@@ -87,26 +49,24 @@ export function TailStatusPill({
? "Last event: never"
: `Last event: ${formatAge(ageSeconds)} ago`;
// Only show the manual reconnect affordance when the connection has
// visibly failed. `closed` is reserved for the post-unmount state and
// is intentionally not actionable — the component is about to vanish.
const showReconnect = status === "stalled" || status === "error";
return (
<div
className="inline-flex items-center gap-2"
className="inline-flex items-center gap-2.5"
data-testid="tail-status-pill"
>
<Badge variant={STATUS_VARIANT[status]} aria-label={`tail status: ${status}`}>
{STATUS_LABEL[status]}
</Badge>
<StatusPill
status={status as ConnectionStatus}
label={STATUS_LABEL[status]}
/>
<span
className="text-xs text-muted-foreground"
className="mono text-[10.5px] text-muted-foreground hidden sm:inline"
data-testid="tail-status-pill-subtitle"
>
{subtitle}
</span>
{showReconnect && (
{showReconnect ? (
<Button
type="button"
variant="outline"
@@ -114,9 +74,10 @@ export function TailStatusPill({
onClick={onReconnect}
data-testid="tail-status-pill-reconnect"
>
Reconnect
<RotateCw className="h-3 w-3" strokeWidth={2} />
Reconnect
</Button>
)}
) : null}
</div>
);
}
+26 -9
View File
@@ -14,23 +14,40 @@ export function BulkBar({
if (count === 0) return null;
return (
<div
className="fixed bottom-4 left-1/2 px-4 py-2 font-mono flex gap-3 z-20"
className="fixed bottom-6 left-1/2 px-3 py-2.5 mono flex items-center gap-2 z-20 rounded-md"
style={{
transform: "translateX(-50%)",
background: "var(--tt-bg-elev)",
border: "1px solid var(--tt-amber)",
color: "var(--tt-ink)",
fontSize: 11,
boxShadow:
"0 0 0 1px hsl(36 88% 56% / 0.15), 0 12px 40px -8px hsl(0 0% 0% / 0.6), inset 0 1px 0 0 hsl(0 0% 100% / 0.04)",
}}
>
<span className="tabular-nums" style={{ color: "var(--tt-amber)" }}>
{count} selected
<span
className="tabular-nums flex items-center gap-1.5 px-2 py-0.5 rounded-sm"
style={{
color: "var(--tt-bg)",
background: "var(--tt-amber)",
fontWeight: 700,
fontSize: 11,
letterSpacing: "0.04em",
}}
>
{count}
</span>
<span style={{ color: "var(--tt-amber)", letterSpacing: "0.12em", textTransform: "uppercase" }}>
selected
</span>
<span className="mx-1 h-4 w-px" style={{ background: "var(--tt-muted)" }} aria-hidden />
{lane === "rejected" && (
<button
onClick={onResubmit}
className="hover:underline"
style={{ color: "var(--tt-ink)" }}
className="px-2.5 py-1 rounded-sm uppercase tracking-[0.12em] transition-colors hover:bg-[color:var(--tt-amber)]/10 focus-visible:outline-none focus-visible:bg-[color:var(--tt-amber)]/10"
style={{ color: "var(--tt-ink)", fontWeight: 600 }}
>
Re-submit ({count})
</button>
@@ -38,16 +55,16 @@ export function BulkBar({
{lane === "candidates" && (
<button
onClick={onDismiss}
className="hover:underline"
style={{ color: "var(--tt-ink)" }}
className="px-2.5 py-1 rounded-sm uppercase tracking-[0.12em] transition-colors hover:bg-[color:var(--tt-amber)]/10 focus-visible:outline-none focus-visible:bg-[color:var(--tt-amber)]/10"
style={{ color: "var(--tt-ink)", fontWeight: 600 }}
>
Dismiss ({count})
</button>
)}
<button
onClick={onExport}
className="hover:underline"
style={{ color: "var(--tt-ink)" }}
className="px-2.5 py-1 rounded-sm uppercase tracking-[0.12em] transition-colors hover:bg-[color:var(--tt-amber)]/10 focus-visible:outline-none focus-visible:bg-[color:var(--tt-amber)]/10"
style={{ color: "var(--tt-ink)", fontWeight: 600 }}
>
Export CSV ({count})
</button>
+65 -20
View File
@@ -17,38 +17,83 @@ export function CandidateBreakdown({
tier: ScoreTier;
}) {
return (
<div className="font-mono" style={{ fontSize: 11 }}>
<div className="flex items-baseline justify-between mb-2">
<div className="mono" style={{ fontSize: 11 }}>
<div className="flex items-baseline justify-between mb-3">
<span
className="uppercase"
style={{ color: "var(--tt-ink-dim)", letterSpacing: "0.12em" }}
style={{
color: "var(--tt-ink-dim)",
letterSpacing: "0.18em",
fontSize: 10,
fontWeight: 600,
}}
>
Score
Match score
</span>
<span
className="tabular-nums"
style={{ color: "var(--tt-amber)", fontSize: 24 }}
className="display tabular-nums"
style={{
color: "var(--tt-amber)",
fontSize: 28,
lineHeight: 1,
letterSpacing: "-0.02em",
}}
>
{score}
</span>
</div>
<ul className="space-y-1">
{FIELDS.map((f) => (
<li key={f.key} className="flex justify-between">
<span style={{ color: "var(--tt-ink)" }}>{f.label}</span>
<span
className="tabular-nums"
style={{ color: "var(--tt-ink-dim)" }}
>
{Math.round(breakdown[f.key] * f.weight)} / {f.weight}
</span>
</li>
))}
<ul className="space-y-1.5">
{FIELDS.map((f) => {
const v = breakdown[f.key];
const score = Math.round(v * f.weight);
const pct = Math.max(0.04, v);
return (
<li key={f.key} className="flex flex-col gap-1">
<div className="flex items-baseline justify-between">
<span style={{ color: "var(--tt-ink)" }}>{f.label}</span>
<span
className="tabular-nums"
style={{ color: "var(--tt-ink-dim)" }}
>
<span style={{ color: "var(--tt-amber)" }}>{score}</span>
<span className="opacity-50"> / {f.weight}</span>
</span>
</div>
<div
aria-hidden
className="h-[3px] rounded-full overflow-hidden"
style={{ background: "var(--tt-muted)" }}
>
<div
className="h-full rounded-full"
style={{
width: `${pct * 100}%`,
background:
"linear-gradient(90deg, var(--tt-amber) 0%, var(--tt-amber) 100%)",
boxShadow: "0 0 4px -1px var(--tt-amber)",
}}
/>
</div>
</li>
);
})}
</ul>
<p
className="mt-3 uppercase"
style={{ color: "var(--tt-ink-dim)", letterSpacing: "0.12em", fontSize: 10 }}
className="mt-4 inline-flex items-center gap-1.5 uppercase"
style={{
color: "var(--tt-amber)",
letterSpacing: "0.18em",
fontSize: 9,
fontWeight: 700,
}}
>
<span
aria-hidden
className="inline-block h-1 w-1 rounded-full"
style={{ background: "var(--tt-amber)" }}
/>
{tier}
</p>
</div>
+54 -20
View File
@@ -7,6 +7,10 @@ export function InboxHeader({
}) {
const now = new Date();
const day = now.toLocaleDateString("en-US", { weekday: "short" }).toUpperCase();
const date = now.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
});
const time = now.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
@@ -15,35 +19,65 @@ export function InboxHeader({
return (
<header
className="sticky top-0 z-10 px-6 py-4"
className="sticky top-0 z-10 px-6 pt-6 pb-4"
style={{
background: "var(--tt-bg)",
borderBottom: "1px solid var(--tt-bg-elev)",
}}
>
<h1
className="tracking-tight"
<div className="flex items-baseline justify-between gap-4">
<div className="flex items-baseline gap-4">
<h1
className="display tracking-tight"
style={{
color: "var(--tt-ink)",
fontSize: 30,
lineHeight: 1,
}}
>
Inbox
</h1>
<span
className="mono uppercase"
style={{
color: "var(--tt-amber)",
fontSize: 11,
letterSpacing: "0.18em",
}}
>
· {day} {date}
</span>
</div>
<span
className="mono tabular-nums"
style={{
color: "var(--tt-ink-dim)",
fontSize: 12,
letterSpacing: "0.05em",
}}
>
{time}
</span>
</div>
<p
className="mono mt-2 flex items-center gap-2"
style={{
color: "var(--tt-ink)",
fontFamily: "'Inter Tight', system-ui, sans-serif",
fontWeight: 700,
fontSize: 22,
color: "var(--tt-ink-dim)",
fontSize: 11,
letterSpacing: "0.08em",
textTransform: "uppercase",
}}
>
INBOX{" "}
<span
className="font-mono"
style={{ color: "var(--tt-amber)", fontSize: 16 }}
>
· {day} {time}
</span>
</h1>
<p
className="font-mono mt-1"
style={{ color: "var(--tt-ink-dim)", fontSize: 11, letterSpacing: "0.05em" }}
>
<span style={{ color: "var(--tt-amber)" }}>{needEyesCount}</span> items need eyes ·{" "}
<span style={{ color: "var(--tt-ink)" }}>{doneTodayCount}</span> done today
className="inline-block w-1.5 h-1.5 rounded-full animate-pulse-dot"
style={{ background: "var(--tt-amber)" }}
aria-hidden
/>
<span style={{ color: "var(--tt-amber)", fontWeight: 600 }}>{needEyesCount}</span>
items need eyes
<span className="opacity-50">·</span>
<span style={{ color: "var(--tt-ink)", fontWeight: 600 }}>{doneTodayCount}</span>
done today
</p>
</header>
);
+31 -19
View File
@@ -1,4 +1,5 @@
import type { InboxClaimRow, InboxCandidateRow, ScoreBreakdown } from "@/lib/inbox-api";
import { cn } from "@/lib/utils";
export type Accent = "oxblood" | "amber" | "ink-blue" | "muted";
@@ -30,19 +31,23 @@ function Sparkline({ breakdown }: { breakdown: ScoreBreakdown | null }) {
if (!breakdown) return null;
const fields: Array<keyof ScoreBreakdown> = ["patient", "date", "amount", "provider"];
return (
<span className="inline-flex items-end gap-0.5 align-middle h-3">
<span className="inline-flex items-end gap-[2px] align-middle h-3.5">
{fields.map((k) => {
const v = breakdown[k] as number;
const h = Math.max(2, Math.round(v * 12));
const h = Math.max(3, Math.round(v * 14));
return (
<span
key={k}
title={`${k}: ${v.toFixed(2)}`}
className="inline-block w-1.5"
style={{
height: `${h}px`,
background: v >= 0.5 ? "var(--tt-amber)" : "var(--tt-muted)",
}}
className={cn(
"inline-block w-[3px] rounded-[1px] transition-all",
v >= 0.7
? "bg-[color:var(--tt-amber)] shadow-[0_0_4px_-1px_var(--tt-amber)]"
: v >= 0.5
? "bg-[color:var(--tt-amber)]/70"
: "bg-[color:var(--tt-muted)]"
)}
style={{ height: `${h}px` }}
/>
);
})}
@@ -67,41 +72,48 @@ export function InboxRow({ row, accent, onClick }: Props) {
return (
<tr
onClick={onClick}
className="cursor-pointer hover:bg-white/[0.03] transition-colors"
style={{ borderBottom: "1px solid var(--tt-bg-elev)" }}
className="cursor-pointer transition-colors group"
>
<td style={{ width: 4, background: ACCENT_VAR[accent], padding: 0 }} />
<td
className="py-2 pl-3 font-mono"
style={{ color: "var(--tt-ink)", fontSize: 12 }}
aria-hidden
style={{
width: 4,
background: ACCENT_VAR[accent],
padding: 0,
boxShadow: `inset 0 0 0 1px ${ACCENT_VAR[accent]}40`,
}}
/>
<td
className="py-2.5 pl-3 mono font-semibold"
style={{ color: "var(--tt-ink)", fontSize: 12, letterSpacing: "-0.005em" }}
>
{id}
</td>
<td
className="py-2 font-mono"
style={{ color: "var(--tt-ink-dim)", fontSize: 11 }}
className="py-2.5 mono"
style={{ color: reason ? "var(--tt-oxblood)" : "var(--tt-amber)", fontSize: 11 }}
>
{reason ?? (topScore != null ? `score ${topScore}` : "")}
</td>
<td
className="py-2 font-mono tabular-nums"
className="py-2.5 mono tabular-nums"
style={{ color: "var(--tt-ink-dim)", fontSize: 11 }}
>
{fmtAgo(rejectedAt)}
</td>
<td
className="py-2 text-right font-mono tabular-nums"
className="py-2.5 text-right mono tabular-nums font-semibold"
style={{ color: "var(--tt-ink)", fontSize: 12 }}
>
{charge}
</td>
<td
className="py-2 pl-4 uppercase tracking-wider"
style={{ color: "var(--tt-ink-dim)", fontSize: 10 }}
className="py-2.5 pl-4 mono uppercase"
style={{ color: "var(--tt-ink-dim)", fontSize: 10, letterSpacing: "0.14em" }}
>
{payer}
</td>
<td className="py-2 pr-3">
<td className="py-2.5 pr-3">
<Sparkline breakdown={filled} />
</td>
</tr>
+126 -32
View File
@@ -1,9 +1,74 @@
import { useState } from "react";
import { InboxRow, type Accent } from "./InboxRow";
import type { InboxClaimRow, InboxCandidateRow } from "@/lib/inbox-api";
import { cn } from "@/lib/utils";
export type LaneRow = InboxClaimRow | InboxCandidateRow;
const ACCENT_COLOR: Record<Accent, string> = {
oxblood: "var(--tt-oxblood)",
amber: "var(--tt-amber)",
"ink-blue": "var(--tt-ink-blue)",
muted: "var(--tt-muted)",
};
/**
* Custom checkbox cell — a small square that picks up the lane's accent
* when selected. Built on a native `<input type="checkbox">` so the
* accessibility role and label-by-id wiring are free (the test harness
* looks the cell up with `screen.getByRole("checkbox")`).
*/
function RowCheckbox({
id,
checked,
onToggle,
}: {
id: string;
checked: boolean;
onToggle: () => void;
}) {
return (
<label
className="flex shrink-0 items-center justify-center cursor-pointer select-none"
style={{ width: 32 }}
>
<span className="relative inline-flex items-center justify-center">
<input
type="checkbox"
aria-label={`Select ${id}`}
checked={checked}
onChange={onToggle}
onClick={(e) => e.stopPropagation()}
className="peer sr-only"
/>
<span
aria-hidden
className={cn(
"block h-3.5 w-3.5 rounded-[3px] border transition-all duration-100",
checked
? "bg-[color:var(--tt-amber)] border-[color:var(--tt-amber)] shadow-[0_0_8px_-1px_var(--tt-amber)]"
: "border-[color:var(--tt-muted)] bg-transparent peer-hover:border-[color:var(--tt-ink-dim)] peer-focus-visible:ring-2 peer-focus-visible:ring-[color:var(--tt-amber)] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[color:var(--tt-bg-elev)]"
)}
/>
{checked ? (
<svg
aria-hidden
viewBox="0 0 12 12"
className="pointer-events-none absolute h-2.5 w-2.5 text-[color:var(--tt-bg)]"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="2.5,6.5 5,9 9.5,3.5" />
</svg>
) : null}
</span>
</label>
);
}
export function Lane({
name,
accent,
@@ -31,29 +96,56 @@ export function Lane({
onSelectionChange?.(Array.from(next));
}
const accentColor = ACCENT_COLOR[accent];
return (
<section
className="flex-1 min-w-[280px] flex flex-col"
className="flex-1 min-w-[280px] flex flex-col rounded-md overflow-hidden"
style={{
background: "var(--tt-bg-elev)",
border: "1px solid var(--tt-bg)",
borderRadius: 4,
border: "1px solid hsl(220 8% 18%)",
boxShadow:
"inset 0 1px 0 0 hsl(0 0% 100% / 0.03), 0 1px 2px 0 hsl(0 0% 0% / 0.4)",
}}
data-lane={name.toLowerCase()}
>
<header
className="px-3 py-2 flex items-baseline justify-between"
style={{ borderBottom: "1px solid var(--tt-bg)" }}
className="px-3 py-2.5 flex items-baseline justify-between gap-2"
style={{
borderBottom: "1px solid hsl(220 8% 16%)",
background:
"linear-gradient(180deg, hsl(220 16% 11%) 0%, hsl(220 18% 9%) 100%)",
}}
>
<h2
className="font-mono uppercase tracking-widest"
style={{ color: "var(--tt-ink)", fontSize: 11, letterSpacing: "0.12em" }}
>
{name}
</h2>
<div className="flex items-center gap-2">
<span
aria-hidden
className="inline-block h-2 w-2 rounded-full"
style={{
background: accentColor,
boxShadow: `0 0 8px ${accentColor}`,
}}
/>
<h2
className="mono uppercase"
style={{
color: "var(--tt-ink)",
fontSize: 11,
letterSpacing: "0.18em",
fontWeight: 600,
}}
>
{name}
</h2>
</div>
<span
className="font-mono tabular-nums"
style={{ color: "var(--tt-amber)", fontSize: 18 }}
className="mono tabular-nums"
style={{
color: "var(--tt-amber)",
fontSize: 18,
fontWeight: 600,
lineHeight: 1,
}}
>
{rows.length}
</span>
@@ -61,34 +153,36 @@ export function Lane({
{rows.length === 0 ? (
<p
className="p-4 font-mono"
style={{ color: "var(--tt-muted)", fontSize: 11 }}
className="px-4 py-6 mono text-center"
style={{ color: "var(--tt-muted)", fontSize: 11, letterSpacing: "0.06em" }}
>
Nothing here.
</p>
) : (
<div className="w-full" style={{ fontSize: 12 }}>
{rows.map((row) => {
<div className="w-full">
{rows.map((row, idx) => {
const id = rowId(row);
const isSelected = selected.has(id);
return (
<div
key={id}
className="flex items-stretch"
style={{ borderBottom: "1px solid var(--tt-bg)" }}
className={cn(
"group flex items-stretch transition-colors",
isSelected
? "bg-[color:var(--tt-amber)]/8"
: idx % 2 === 0
? "bg-transparent"
: "bg-white/[0.015]",
"hover:bg-[color:var(--tt-amber)]/5"
)}
style={{ borderBottom: "1px solid hsl(220 8% 14%)" }}
>
<label
className="flex items-center justify-center"
style={{ width: 28, padding: "0 4px 0 8px" }}
>
<input
type="checkbox"
aria-label={`Select ${id}`}
checked={selected.has(id)}
onChange={() => toggle(id)}
onClick={(e) => e.stopPropagation()}
/>
</label>
<div style={{ width: "100%" }}>
<RowCheckbox
id={id}
checked={isSelected}
onToggle={() => toggle(id)}
/>
<div className="flex-1 min-w-0">
<InboxRow row={row} accent={accent} onClick={() => onRowClick(row)} />
</div>
</div>
+8 -6
View File
@@ -3,20 +3,22 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10.5px] font-medium uppercase tracking-[0.12em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
{
variants: {
variant: {
default: "border-transparent bg-primary/15 text-primary",
secondary: "border-transparent bg-muted text-muted-foreground",
secondary: "border-transparent bg-muted/80 text-muted-foreground",
success:
"border-transparent bg-[hsl(var(--success)/0.15)] text-[hsl(var(--success))]",
"border-transparent bg-[hsl(var(--success)/0.14)] text-[hsl(var(--success))]",
warning:
"border-transparent bg-[hsl(var(--warning)/0.15)] text-[hsl(var(--warning))]",
"border-transparent bg-[hsl(var(--warning)/0.14)] text-[hsl(var(--warning))]",
destructive:
"border-transparent bg-destructive/15 text-destructive",
outline: "border-border text-foreground",
muted: "border-transparent bg-muted/60 text-muted-foreground",
outline: "border-border/80 text-foreground",
muted: "border-transparent bg-muted/40 text-muted-foreground",
signal:
"border-transparent bg-[hsl(var(--signal)/0.14)] text-[hsl(var(--signal))]",
},
},
defaultVariants: { variant: "default" },
+10 -10
View File
@@ -4,28 +4,28 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-40 active:translate-y-px",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
"bg-primary text-primary-foreground shadow-[0_1px_0_0_hsl(0_0%_100%/0.08)_inset,0_1px_2px_0_hsl(0_0%_0%/0.4)] hover:bg-primary/90",
accent:
"bg-accent text-accent-foreground shadow-sm hover:bg-accent/90",
"bg-accent text-accent-foreground shadow-[0_1px_0_0_hsl(0_0%_100%/0.1)_inset,0_1px_2px_0_hsl(0_0%_0%/0.4)] hover:bg-accent/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
"bg-destructive text-destructive-foreground shadow-[0_1px_0_0_hsl(0_0%_100%/0.1)_inset,0_1px_2px_0_hsl(0_0%_0%/0.4)] hover:bg-destructive/90",
outline:
"border border-border bg-transparent text-foreground hover:bg-muted",
"border border-border/80 bg-transparent text-foreground hover:bg-muted/60 hover:border-border",
secondary:
"bg-secondary text-secondary-foreground hover:bg-muted",
ghost: "bg-transparent text-foreground hover:bg-muted",
ghost: "bg-transparent text-foreground hover:bg-muted/60",
link: "bg-transparent text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-lg px-6",
icon: "h-9 w-9",
default: "h-9 px-4",
sm: "h-7 rounded px-2.5 text-xs",
lg: "h-10 px-5",
icon: "h-8 w-8",
},
},
defaultVariants: {
-25
View File
@@ -22,23 +22,6 @@ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
);
CardHeader.displayName = "CardHeader";
// Polymorphic `as` prop so consumers can pick the correct heading level
// for semantic structure (e.g. `<CardTitle as="h2">` inside a Dashboard
// section). Defaults to `"div"` so all existing call-sites that omit
// `as` continue to render a <div> exactly as before — no regression.
//
// Implementation note: `React.forwardRef` infers its ref/props types
// from the inner render function, but TypeScript can't propagate a
// generic parameter through `forwardRef`'s own declaration. The
// well-known workaround (see Ben Ilegbodu's polymorphic-component
// write-up, mirrored by Mantine / tsteele.dev) is to give `forwardRef`
// concrete types up front and then cast the resulting component to a
// generic call signature so consumers get element-specific props
// (e.g. h2 attrs) and the right ref type based on `as`. The inner
// render uses `as` to swap the actual rendered element at runtime;
// the type assertion on `ref` is safe because React only cares about
// the `current` property at runtime regardless of the underlying
// DOM node type.
type CardTitleProps<E extends React.ElementType = "div"> = Omit<
React.ComponentPropsWithoutRef<E>,
"as"
@@ -46,12 +29,6 @@ type CardTitleProps<E extends React.ElementType = "div"> = Omit<
as?: E;
};
// The exported component is a polymorphic callable: a single call
// signature with the generic `E` parameter that consumers control
// via the `as` prop. We assign forwardRef's result through an
// `unknown` cast (necessary because the source type is
// `ForwardRefExoticComponent` while the target is a plain generic
// function).
type CardTitleComponent = <E extends React.ElementType = "div">(
props: CardTitleProps<E> & { ref?: React.ForwardedRef<React.ElementRef<E>> }
) => React.ReactElement | null;
@@ -71,8 +48,6 @@ const CardTitle = React.forwardRef<HTMLDivElement, CardTitleProps<"div">>(
);
}
) as unknown as CardTitleComponent;
// Displayname for React DevTools — assignable via cast since the
// polymorphic call signature doesn't expose it.
(CardTitle as unknown as { displayName: string }).displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
+11 -3
View File
@@ -18,42 +18,50 @@ const STATE_CONFIG: Record<
label: string;
icon: LucideIcon;
className: string;
dot: string;
}
> = {
submitted: {
label: "Submitted",
icon: Send,
className: "bg-accent/15 text-accent border-accent/30",
dot: "bg-accent",
},
received: {
label: "Received",
icon: Inbox,
className: "bg-accent/10 text-accent/80 border-accent/20",
className: "bg-accent/10 text-accent/85 border-accent/20",
dot: "bg-accent/70",
},
paid: {
label: "Paid",
icon: CheckCircle2,
className: "bg-success/15 text-success border-success/30",
dot: "bg-success",
},
partial: {
label: "Partial",
icon: AlertTriangle,
className: "bg-warning/15 text-warning border-warning/30",
dot: "bg-warning",
},
denied: {
label: "Denied",
icon: Ban,
className: "bg-destructive/15 text-destructive border-destructive/30",
dot: "bg-destructive",
},
reconciled: {
label: "Reconciled",
icon: CheckCheck,
className: "bg-success/15 text-success border-success/30",
dot: "bg-success",
},
reversed: {
label: "Reversed",
icon: RotateCcw,
className: "bg-warning/15 text-warning border-warning/30",
dot: "bg-warning",
},
};
@@ -70,12 +78,12 @@ export function ClaimStateBadge({
<Badge
variant="outline"
className={cn(
"gap-1.5 font-mono uppercase tracking-wider text-[10px]",
"gap-1.5 mono uppercase tracking-[0.14em] text-[10.5px] font-medium py-0.5",
cfg.className,
className,
)}
>
<Icon className="h-3 w-3" strokeWidth={1.75} aria-hidden />
<Icon className="h-3 w-3" strokeWidth={2} aria-hidden />
{cfg.label}
</Badge>
);
+4 -4
View File
@@ -15,7 +15,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/65 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@@ -32,7 +32,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 surface rounded-2xl p-6 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 surface rounded-xl p-6 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
className
)}
{...props}
@@ -40,7 +40,7 @@ const DialogContent = React.forwardRef<
{children}
<DialogPrimitive.Close
aria-label="Close dialog"
className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground opacity-60 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<X className="h-4 w-4" aria-hidden />
<span className="sr-only">Close</span>
@@ -72,7 +72,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
className={cn("text-base font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
+4 -5
View File
@@ -30,20 +30,19 @@ export function EmptyState({
return (
<div
className={cn(
"flex flex-col items-center justify-center gap-2.5 py-14 text-center",
"flex flex-col items-center justify-center gap-2.5 py-16 text-center",
className,
)}
>
<div
aria-hidden
className="h-10 w-10 rounded-full ring-1 ring-inset ring-border/60 flex items-center justify-center text-muted-foreground"
className="relative h-10 w-10 rounded-full ring-1 ring-inset ring-border/60 bg-muted/30 flex items-center justify-center text-muted-foreground"
>
<div className="absolute inset-0 rounded-full ring-1 ring-inset ring-accent/10" />
{icon ?? <Inbox className="h-4 w-4" strokeWidth={1.5} />}
</div>
{eyebrow ? (
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
{eyebrow}
</div>
<div className="eyebrow">{eyebrow}</div>
) : null}
<div className="text-sm text-muted-foreground/80 max-w-sm">
{message}
+3 -5
View File
@@ -30,7 +30,7 @@ export function ErrorState({
return (
<div
className={cn(
"surface rounded-xl border border-destructive/30 p-4",
"surface rounded-lg border border-destructive/30 p-4",
"flex items-center gap-3",
className,
)}
@@ -42,12 +42,10 @@ export function ErrorState({
aria-hidden
/>
<div className="flex-1 min-w-0">
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-destructive/80">
{eyebrow}
</div>
<div className="eyebrow text-destructive/80">{eyebrow}</div>
<div className="text-sm text-foreground/90 truncate">{message}</div>
{detail ? (
<div className="text-xs text-muted-foreground truncate mt-0.5">
<div className="text-xs text-muted-foreground truncate mt-0.5 mono">
{detail}
</div>
) : null}
+3 -5
View File
@@ -38,9 +38,7 @@ export function FilterChips({
aria-label="Filter"
>
{eyebrow ? (
<span className="mr-1 text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
{eyebrow}
</span>
<span className="eyebrow mr-1">{eyebrow}</span>
) : null}
{options.map((opt) => {
const isActive = opt.value === value;
@@ -52,7 +50,7 @@ export function FilterChips({
aria-checked={isActive}
onClick={() => onChange(isActive ? null : opt.value)}
className={cn(
"h-7 rounded-full px-3 text-xs font-medium transition-colors",
"h-7 rounded-full px-3 text-[11px] font-medium uppercase tracking-[0.1em] transition-colors",
"ring-1 ring-inset focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
isActive
? "bg-accent/15 text-accent ring-accent/40 hover:bg-accent/20"
@@ -69,7 +67,7 @@ export function FilterChips({
onClick={() => onChange(null)}
aria-label="Clear filter"
className={cn(
"h-7 rounded-full px-2.5 text-xs font-medium transition-colors",
"h-7 rounded-full px-2.5 text-[11px] font-medium uppercase tracking-[0.1em] transition-colors",
"inline-flex items-center gap-1",
"text-muted-foreground hover:text-foreground",
"ring-1 ring-inset ring-border/40 hover:ring-border/60",
+9 -2
View File
@@ -9,7 +9,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
ref={ref}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background/60 px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full rounded-md border border-input bg-background/40 px-3 py-1 text-sm transition-colors",
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
"placeholder:text-muted-foreground/60",
"focus-visible:outline-none focus-visible:bg-background/80 focus-visible:border-foreground/20",
"disabled:cursor-not-allowed disabled:opacity-40",
className
)}
{...props}
@@ -25,7 +29,10 @@ const Textarea = React.forwardRef<
<textarea
ref={ref}
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background/60 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-background/40 px-3 py-2 text-sm",
"placeholder:text-muted-foreground/60",
"focus-visible:outline-none focus-visible:bg-background/80 focus-visible:border-foreground/20",
"disabled:cursor-not-allowed disabled:opacity-40",
className
)}
{...props}
+11 -4
View File
@@ -14,7 +14,11 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background/60 px-3 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background/40 px-3 py-1 text-sm",
"placeholder:text-muted-foreground/60",
"focus-visible:outline-none focus-visible:bg-background/80 focus-visible:border-foreground/20",
"disabled:cursor-not-allowed disabled:opacity-40",
"[&>span]:line-clamp-1",
className
)}
{...props}
@@ -35,7 +39,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md surface text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md surface-2 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
className
@@ -64,14 +68,17 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted focus:text-foreground data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[highlighted]:outline data-[highlighted]:outline-1 data-[highlighted]:outline-accent data-[state=checked]:font-medium data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
"focus:bg-muted focus:text-foreground",
"data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[highlighted]:outline data-[highlighted]:outline-1 data-[highlighted]:outline-accent",
"data-[state=checked]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-40",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<Check className="h-3.5 w-3.5 text-accent" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+4 -4
View File
@@ -37,8 +37,8 @@ export function Skeleton({
return (
<div
className={cn(
"relative overflow-hidden bg-muted/30",
"ring-1 ring-inset ring-border/40",
"relative overflow-hidden rounded-md bg-muted/20",
"ring-1 ring-inset ring-border/30",
variantClass[variant],
className,
)}
@@ -49,10 +49,10 @@ export function Skeleton({
>
<div
aria-hidden
className="absolute inset-y-0 left-0 w-1/2 animate-shimmer"
className="absolute inset-y-0 left-0 w-2/3 animate-shimmer"
style={{
background:
"linear-gradient(90deg, transparent 0%, hsl(var(--muted-foreground) / 0.08) 50%, transparent 100%)",
"linear-gradient(90deg, transparent 0%, hsl(220 14% 96% / 0.04) 50%, transparent 100%)",
}}
/>
</div>
+6 -3
View File
@@ -20,7 +20,10 @@ const TableHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<thead
ref={ref}
className={cn("[&_tr]:border-b [&_tr]:border-border/60", className)}
className={cn(
"[&_tr]:border-b [&_tr]:border-border/60 [&_tr]:bg-muted/20",
className
)}
{...props}
/>
));
@@ -45,7 +48,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"border-b border-border/40 transition-colors hover:bg-muted/30 focus-within:bg-muted/30 data-[state=selected]:bg-muted",
"border-b border-border/40 transition-colors hover:bg-muted/30 focus-within:bg-muted/40 data-[state=selected]:bg-muted/50",
className
)}
{...props}
@@ -61,7 +64,7 @@ const TableHead = React.forwardRef<
ref={ref}
scope={scope}
className={cn(
"h-10 px-4 text-left align-middle text-[11px] font-medium uppercase tracking-wider text-muted-foreground [&:has([role=checkbox])]:pr-0",
"h-9 px-4 text-left align-middle text-[10.5px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/80 [&:has([role=checkbox])]:pr-0",
className
)}
{...props}