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:
Cyclone UI
2026-06-21 14:37:36 -06:00
parent 736bf4d333
commit 388ea6e49b
2 changed files with 866 additions and 136 deletions
+245 -35
View File
@@ -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>