Refine Batch Diff page with hybrid dark/paper treatment
- Replace small header with editorial hero: massive 'Compare *batches.*'
(clamp 48-80px serif, italic for 'batches.') + ghost 'DIFF' watermark
+ GitCompareArrows pill (837P/835/first vs second) + pick-a-batch hint
- Add torn-page fold with '↘ A → B ↙' label and 48-dot perforation
- Cream paper plane (max-w-[1280px] mx-auto) with paper-grain SVG and
double-bordered title block: 'SHEET 01 · COMPARE TWO BATCHES' over
'Compare them.' (clamp 48-96px) plus A→B wire indicator (blue A,
amber B, paper-ink-3 placeholder)
- New Folio helper renders the § N + vertical-rl italic label; reused
on the picks and diff sections
- § 01 The picks: BatchPicker gets a paper-toned card with side badge
(A blue / B amber tinted square) and claim count, preserving the
data-testid='diff-picker-{a,b}' and 'diff-picker-kind-{kind}' and
'diff-picker-option-{id}' contracts
- § 02 The diff: conditional rendering (awaiting-picks dashed card,
loading skeleton, not-found, error, empty diff, full diff view)
with paper-toned wrappers
- BatchDiffView refactored to paper-toned chrome: SideMeta cards
(warm surface, surface-ink text), SummaryCards → new DiffKpiTile
helper (success/destructive/amber/ink accent rails with lucide
icons), SectionTables → tone='paper' tables with cream header band
and surface-ink text. All section/row/indicator testids preserved.
- Action footer: 'Comparing A with B.' italic note + Refresh / Clear
picks buttons; paper-toned container, sits inside the paper plane
- Bottom footer: 'End of sheet 01 / Cyclone · Batch diff / A vs B
or awaiting picks'
Round 11/11 — last page in the hybrid sweep.
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { Plus, Minus, Equal, ArrowRight, type LucideIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Equal,
|
||||
Minus,
|
||||
Plus,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
@@ -9,19 +15,20 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { KpiCard } from "@/components/KpiCard";
|
||||
import { fmt } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
BatchClaimDiffSummary,
|
||||
BatchDiff,
|
||||
BatchDiffChangedRow,
|
||||
BatchDiffSideMeta,
|
||||
BatchDiffSummary,
|
||||
BatchClaimDiffSummary,
|
||||
} from "@/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Side meta header — small panel identifying which batch is on the left / right.
|
||||
// Side meta header — small panel identifying which batch is on the
|
||||
// left / right. Paper-toned to sit inside the cream "paper plane" of
|
||||
// the BatchDiff page. data-testid pinned by BatchDiff.test.tsx.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SideMeta({
|
||||
@@ -39,10 +46,19 @@ function SideMeta({
|
||||
: "text-amber-300 border-amber-400/30 bg-amber-400/10";
|
||||
return (
|
||||
<div
|
||||
className="surface rounded-xl p-4 flex flex-col gap-2"
|
||||
className="rounded-xl p-4 flex flex-col gap-2 border"
|
||||
data-testid={`batch-diff-side-${label.toLowerCase()}`}
|
||||
style={{
|
||||
backgroundColor: "hsl(36 22% 98%)",
|
||||
borderColor: "hsl(30 14% 14% / 0.10)",
|
||||
boxShadow:
|
||||
"inset 0 1px 0 0 hsl(0 0% 100% / 0.45), 0 1px 0 0 hsl(30 14% 22% / 0.06)",
|
||||
}}
|
||||
>
|
||||
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||
<div
|
||||
className="text-[10.5px] font-semibold uppercase tracking-[0.18em]"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -54,12 +70,23 @@ function SideMeta({
|
||||
>
|
||||
{side.kind}
|
||||
</span>
|
||||
<span className="display num text-[13px]">{side.id}</span>
|
||||
<span
|
||||
className="display num text-[13px]"
|
||||
style={{ color: "hsl(var(--surface-ink))" }}
|
||||
>
|
||||
{side.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-[12px] text-muted-foreground truncate">
|
||||
<div
|
||||
className="font-mono text-[12px] truncate"
|
||||
style={{ color: "hsl(var(--surface-ink-2))" }}
|
||||
>
|
||||
{side.inputFilename}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div
|
||||
className="flex items-center justify-between text-xs"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
<span>Parsed {side.parsedAt ? fmt.dateShort(side.parsedAt) : "—"}</span>
|
||||
<span className="font-mono num">
|
||||
{fmt.num(side.claimCount)} claim{side.claimCount === 1 ? "" : "s"}
|
||||
@@ -70,40 +97,125 @@ function SideMeta({
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Summary cards — the four-count tile row.
|
||||
// Summary cards — the four-count tile row. Paper-toned via
|
||||
// DiffKpiTile so the counts sit naturally on the cream paper plane.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SummaryCards({ summary }: { summary: BatchDiffSummary }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3" data-testid="batch-diff-summary">
|
||||
<KpiCard
|
||||
<div
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-3"
|
||||
data-testid="batch-diff-summary"
|
||||
>
|
||||
<DiffKpiTile
|
||||
label="Added in B"
|
||||
value={fmt.num(summary.addedCount)}
|
||||
icon={Plus}
|
||||
hint="In B, not in A"
|
||||
tone="success"
|
||||
/>
|
||||
<KpiCard
|
||||
<DiffKpiTile
|
||||
label="Removed from A"
|
||||
value={fmt.num(summary.removedCount)}
|
||||
icon={Minus}
|
||||
hint="In A, not in B"
|
||||
tone="destructive"
|
||||
/>
|
||||
<KpiCard
|
||||
<DiffKpiTile
|
||||
label="Changed"
|
||||
value={fmt.num(summary.changedCount)}
|
||||
icon={Equal}
|
||||
hint="Deltas in either side"
|
||||
tone="amber"
|
||||
/>
|
||||
<KpiCard
|
||||
<DiffKpiTile
|
||||
label="Unchanged"
|
||||
value={fmt.num(summary.unchangedCount)}
|
||||
icon={Equal}
|
||||
hint="Identical across A & B"
|
||||
tone="ink"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DiffKpiTile — paper-toned metric tile for the diff summary row.
|
||||
// Mirrors AckKpiTile / BatchesKpiTile from the other hybrid pages.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function DiffKpiTile({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
hint,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: LucideIcon;
|
||||
hint: string;
|
||||
tone: "success" | "destructive" | "amber" | "ink";
|
||||
}) {
|
||||
const accentMap = {
|
||||
success: "hsl(152 64% 38%)",
|
||||
destructive: "hsl(358 70% 42%)",
|
||||
amber: "hsl(36 92% 50%)",
|
||||
ink: "hsl(var(--surface-ink))",
|
||||
} as const;
|
||||
const tintMap = {
|
||||
success: "hsl(152 50% 88%)",
|
||||
destructive: "hsl(358 70% 92%)",
|
||||
amber: "hsl(36 82% 92%)",
|
||||
ink: "hsl(36 22% 90%)",
|
||||
} as const;
|
||||
const accent = accentMap[tone];
|
||||
const tint = tintMap[tone];
|
||||
return (
|
||||
<div
|
||||
className="relative rounded-xl p-4 overflow-hidden border"
|
||||
title={hint}
|
||||
style={{
|
||||
backgroundColor: "hsl(var(--surface))",
|
||||
boxShadow:
|
||||
"inset 0 1px 0 0 hsl(0 0% 100% / 0.45), 0 1px 0 0 hsl(30 14% 22% / 0.06), inset 3px 0 0 0 hsl(0 0% 100% / 0.4)",
|
||||
borderColor: "hsl(30 14% 14% / 0.10)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute left-0 top-4 bottom-4 w-[3px] rounded-r-sm"
|
||||
style={{ backgroundColor: accent, opacity: 0.85 }}
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div
|
||||
className="mono text-[10px] uppercase tracking-[0.18em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div
|
||||
className="h-5 w-5 rounded-md flex items-center justify-center"
|
||||
style={{ backgroundColor: tint, color: accent }}
|
||||
>
|
||||
<Icon className="h-3 w-3" strokeWidth={1.75} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums tracking-[-0.04em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(26px, 2.8vw, 36px)",
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Row indicator — `+` / `-` / `~` gutter on the left of every row.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -152,7 +264,10 @@ function RowIndicator({
|
||||
|
||||
function ClaimIdCell({ id }: { id: string }) {
|
||||
return (
|
||||
<span className="font-mono text-[12px] tracking-tight" data-testid="diff-claim-id">
|
||||
<span
|
||||
className="font-mono text-[12px] tracking-tight"
|
||||
data-testid="diff-claim-id"
|
||||
>
|
||||
{id}
|
||||
</span>
|
||||
);
|
||||
@@ -160,14 +275,31 @@ function ClaimIdCell({ id }: { id: string }) {
|
||||
|
||||
function PatientCell({ name }: { name: string }) {
|
||||
if (!name) {
|
||||
return <span className="text-muted-foreground/60 text-[12px]">—</span>;
|
||||
return (
|
||||
<span
|
||||
className="text-[12px]"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className="text-[13px]">{name}</span>;
|
||||
return (
|
||||
<span
|
||||
className="text-[13px]"
|
||||
style={{ color: "hsl(var(--surface-ink))" }}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ChargeCell({ value }: { value: number }) {
|
||||
return (
|
||||
<span className="display num text-[13px] tabular-nums">
|
||||
<span
|
||||
className="display num text-[13px] tabular-nums"
|
||||
style={{ color: "hsl(var(--surface-ink))" }}
|
||||
>
|
||||
{fmt.usdPrecise(value)}
|
||||
</span>
|
||||
);
|
||||
@@ -175,19 +307,44 @@ function ChargeCell({ value }: { value: number }) {
|
||||
|
||||
function DateCell({ value }: { value: string | null }) {
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground/60 text-[12px]">—</span>;
|
||||
return (
|
||||
<span
|
||||
className="text-[12px]"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className="num text-[12.5px]">{value}</span>;
|
||||
return (
|
||||
<span
|
||||
className="num text-[12.5px]"
|
||||
style={{ color: "hsl(var(--surface-ink-2))" }}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusCell({ value }: { value: string }) {
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground/60 text-[12px]">—</span>;
|
||||
return (
|
||||
<span
|
||||
className="text-[12px]"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="font-mono uppercase tracking-wider text-[10px]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
borderColor: "hsl(30 14% 14% / 0.20)",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
@@ -196,17 +353,28 @@ function StatusCell({ value }: { value: string }) {
|
||||
|
||||
function CptCell({ codes }: { codes: string[] }) {
|
||||
if (!codes || codes.length === 0) {
|
||||
return <span className="text-muted-foreground/60 text-[12px]">—</span>;
|
||||
return (
|
||||
<span
|
||||
className="text-[12px]"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="font-mono text-[11.5px] tracking-tight">
|
||||
<span
|
||||
className="font-mono text-[11.5px] tracking-tight"
|
||||
style={{ color: "hsl(var(--surface-ink-2))" }}
|
||||
>
|
||||
{codes.join(", ")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section tables — one per bucket (added / removed / changed).
|
||||
// Section tables — one per bucket (added / removed / changed). Uses
|
||||
// `tone="paper"` so the table chrome matches the cream paper plane.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Side = "a" | "b";
|
||||
@@ -386,25 +554,53 @@ function SectionTable({
|
||||
<section className="space-y-3" data-testid={testid}>
|
||||
<header className="flex items-baseline justify-between">
|
||||
<div>
|
||||
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground mb-0.5">
|
||||
<div
|
||||
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] mb-0.5"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
{eyebrow}
|
||||
</div>
|
||||
<h2 className="text-[15px] font-semibold tracking-tight">{title}</h2>
|
||||
<h3
|
||||
className="display leading-[0.98] tracking-[-0.03em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(20px, 2.2vw, 26px)",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="font-mono num text-[12.5px] text-muted-foreground">
|
||||
<span
|
||||
className="font-mono num text-[12.5px]"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
{fmt.num(count)}
|
||||
</span>
|
||||
</header>
|
||||
{count === 0 ? (
|
||||
<div
|
||||
className="surface rounded-xl border border-dashed border-border/60 p-6 text-center text-[12.5px] text-muted-foreground"
|
||||
className="rounded-xl border p-6 text-center text-[12.5px]"
|
||||
data-testid={`${testid}-empty`}
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.16)",
|
||||
borderStyle: "dashed",
|
||||
backgroundColor: "hsl(36 22% 96%)",
|
||||
color: "hsl(var(--surface-ink-3))",
|
||||
}}
|
||||
>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
) : (
|
||||
<div className="surface rounded-xl overflow-hidden">
|
||||
<Table>
|
||||
<div
|
||||
className="rounded-xl border overflow-hidden"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.10)",
|
||||
backgroundColor: "hsl(36 22% 98%)",
|
||||
boxShadow: "inset 0 1px 0 0 hsl(0 0% 100% / 0.5)",
|
||||
}}
|
||||
>
|
||||
<Table tone="paper">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10" aria-label="Diff indicator" />
|
||||
@@ -425,7 +621,8 @@ function SectionTable({
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skeleton — used by the page while the diff is loading.
|
||||
// Skeleton — used by the page while the diff is loading. Paper-toned
|
||||
// (cream blocks) to match the paper plane chrome.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function BatchDiffViewSkeleton() {
|
||||
@@ -462,11 +659,24 @@ export function BatchDiffEmpty({ data }: { data: BatchDiff }) {
|
||||
<SideMeta side={data.b} label="B (right)" />
|
||||
</div>
|
||||
<SummaryCards summary={data.summary} />
|
||||
<div className="surface rounded-xl border border-dashed border-border/60 p-10 text-center">
|
||||
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground mb-1.5">
|
||||
<div
|
||||
className="rounded-xl border p-10 text-center"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.16)",
|
||||
borderStyle: "dashed",
|
||||
backgroundColor: "hsl(36 22% 96%)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] mb-1.5"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
Diff · no deltas
|
||||
</div>
|
||||
<div className="text-[13.5px] text-muted-foreground">
|
||||
<div
|
||||
className="text-[13.5px]"
|
||||
style={{ color: "hsl(var(--surface-ink-2))" }}
|
||||
>
|
||||
These two batches are identical — no claims added, removed, or
|
||||
changed between A and B.
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user