Refine Batches page with hybrid dark/paper treatment

- Add tone='paper' prop to shared Table/TableHeader/TableRow/TableHead/
  TableCell components; paper-toned chrome swaps dark muted/20 header
  for cream paper, dark hover for warm cream hover, dark selected for
  paper blue tint, muted-foreground text for surface-ink. Dark tone
  is the default and unchanged.
- Extend BatchesList with tone='paper' variant: cream hover, warm
  surface-ink text, paper-tinted open-row (blue tint + ring). Kind
  badge (text-sky-300 / text-amber-300 + data-testid) and row
  data-testid='batch-row-{id}' preserved for the test contract.
- Replace PageHeader with editorial hero: massive 'Parsed *batches.*'
  (clamp 48-80px serif, italic for 'batches.') + ghost 'PARSED'
  watermark + Files pill (837P/835/envelope) + tap-to-open hint
- Add torn-page fold with '↘ THE REGISTER ↙' label and 48-dot perforation
- Cream paper plane (max-w-[1280px] mx-auto) with paper-grain SVG and
  double-bordered title block: 'REGISTER · PARSED BATCHES' over
  'The register.' (clamp 48-96px) plus on-file count column
- Folio system: § 01 Vital signs (4 paper KPI tiles via new
  BatchesKpiTile: On file / 837P / 835 / Claims with ink/blue/amber/
  success accent rails and Icons from lucide), § 02 The register
  (paper-toned BatchesList)
- Error/Loading/Empty states wrapped in paper-toned containers
- Footer: 'End of register / Cyclone · Batches / N rows'

Round 10/11.
This commit is contained in:
Cyclone UI
2026-06-21 14:34:26 -06:00
parent 7db5e448cd
commit 736bf4d333
3 changed files with 730 additions and 101 deletions
+63 -10
View File
@@ -20,6 +20,11 @@ import type { BatchSummary } from "@/lib/api";
*
* Voice mirrors `AckCodeBadge` in `src/pages/Acks.tsx` (uppercase,
* wide tracking, hairline border, low-opacity fill).
*
* The literal class names `text-sky-300` and `text-amber-300` are
* pinned by `Batches.test.tsx` as a contract — the test asserts the
* badge's text color is one of these two strings. Don't replace them
* with arbitrary HSL values or the test will fail.
*/
function KindBadge({ kind }: { kind: BatchSummary["kind"] }) {
const color =
@@ -42,7 +47,8 @@ function KindBadge({ kind }: { kind: BatchSummary["kind"] }) {
/**
* Skeleton rows for the batches table. Mirrors the row count used in
* `Acks.tsx` (5 placeholders) so the loading density matches the rest
* of the app.
* of the app. `data-testid="batches-skeleton"` is pinned by
* `Batches.test.tsx`.
*/
export function BatchesListSkeleton() {
return (
@@ -63,6 +69,13 @@ type BatchesListProps = {
*/
openId: string | null;
onOpen: (id: string) => void;
/**
* When `paper`, the table sits inside a cream "paper plane" section
* and uses the paper-toned color scheme: warm hover, hairline border
* in surface-line, surface-ink text. When `dark` (default), the
* original dark-mode chrome is used.
*/
tone?: "dark" | "paper";
};
/**
@@ -71,15 +84,26 @@ type BatchesListProps = {
* so the numbers tick up from 0 on first render (gives the page a
* little life on load; consistent with the Dashboard KPI cards).
*/
export function BatchesList({ items, openId, onOpen }: BatchesListProps) {
export function BatchesList({
items,
openId,
onOpen,
tone = "dark",
}: BatchesListProps) {
const isPaper = tone === "paper";
return (
<Table data-testid="batches-table">
<Table data-testid="batches-table" tone={tone}>
<TableHeader>
<TableRow>
<TableHead>Kind</TableHead>
<TableHead>Batch</TableHead>
<TableHead>Input file</TableHead>
<TableHead className="text-right">Claims</TableHead>
<TableHead
className="text-right"
style={isPaper ? { color: "hsl(var(--surface-ink-2))" } : undefined}
>
Claims
</TableHead>
<TableHead>Parsed</TableHead>
<TableHead className="w-6" aria-label="Open" />
</TableRow>
@@ -93,28 +117,57 @@ export function BatchesList({ items, openId, onOpen }: BatchesListProps) {
data-open={openId === b.id ? "true" : undefined}
className={cn(
"animate-row-flash cursor-pointer",
openId === b.id && "bg-muted/40",
isPaper && openId === b.id &&
"!bg-[hsl(212_85%_95%)] ring-1 ring-inset ring-[hsl(212_100%_45%_/_0.30)]",
!isPaper && openId === b.id && "bg-muted/40",
)}
>
<TableCell>
<KindBadge kind={b.kind} />
</TableCell>
<TableCell className="display num text-[13px]">
<TableCell
className="display num text-[13px]"
style={isPaper ? { color: "hsl(var(--surface-ink))" } : undefined}
>
{b.id}
</TableCell>
<TableCell className="font-mono text-[12px] text-muted-foreground truncate max-w-[280px]">
<TableCell
className={cn(
"font-mono text-[12px] truncate max-w-[280px]",
isPaper
? "text-[hsl(var(--surface-ink-2))]"
: "text-muted-foreground",
)}
>
{b.inputFilename}
</TableCell>
<TableCell className="text-right display num text-[13px]">
<TableCell
className="text-right display num text-[13px]"
style={isPaper ? { color: "hsl(var(--surface-ink))" } : undefined}
>
<AnimatedNumber
value={b.claimCount}
format={(n) => fmt.num(Math.round(n))}
/>
</TableCell>
<TableCell className="text-muted-foreground num text-[12.5px]">
<TableCell
className={cn(
"num text-[12.5px]",
isPaper
? "text-[hsl(var(--surface-ink-3))]"
: "text-muted-foreground",
)}
>
{b.parsedAt ? fmt.dateShort(b.parsedAt) : "—"}
</TableCell>
<TableCell className="text-muted-foreground text-right">
<TableCell
className={cn(
"text-right",
isPaper
? "text-[hsl(var(--surface-ink-3))]"
: "text-muted-foreground",
)}
>
<span aria-hidden></span>
</TableCell>
</TableRow>