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:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user