Files
cyclone/src/components/BatchesList.tsx
T
Cyclone UI 736bf4d333 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.
2026-06-21 14:34:26 -06:00

179 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AnimatedNumber } from "@/components/AnimatedNumber";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { fmt } from "@/lib/format";
import type { BatchSummary } from "@/lib/api";
/**
* Colored kind badge — 837p in cool blue, 835 in warm amber. The colors
* are deliberately not the project-wide `--accent` electric blue (that's
* reserved for active nav + primary CTAs) so the rows scan as a
* two-flavor instrument instead of a uniform accent.
*
* 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 =
kind === "837p"
? "text-sky-300 border-sky-400/30 bg-sky-400/10"
: "text-amber-300 border-amber-400/30 bg-amber-400/10";
return (
<span
data-testid={`kind-badge-${kind}`}
className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10.5px] font-semibold uppercase tracking-[0.14em] font-mono",
color,
)}
>
{kind}
</span>
);
}
/**
* 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. `data-testid="batches-skeleton"` is pinned by
* `Batches.test.tsx`.
*/
export function BatchesListSkeleton() {
return (
<div className="p-4 space-y-2" data-testid="batches-skeleton">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} variant="row" />
))}
</div>
);
}
type BatchesListProps = {
items: BatchSummary[];
/**
* Currently-open batch id (or null when drawer is closed). The open
* row gets a subtle background tint so the operator knows which
* batch the drawer is showing.
*/
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";
};
/**
* Tabular list of every persisted batch. Rows are clickable — clicking
* opens the detail drawer. `AnimatedNumber` is used for `claimCount`
* 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,
tone = "dark",
}: BatchesListProps) {
const isPaper = tone === "paper";
return (
<Table data-testid="batches-table" tone={tone}>
<TableHeader>
<TableRow>
<TableHead>Kind</TableHead>
<TableHead>Batch</TableHead>
<TableHead>Input file</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>
</TableHeader>
<TableBody>
{items.map((b) => (
<TableRow
key={b.id}
onClick={() => onOpen(b.id)}
data-testid={`batch-row-${b.id}`}
data-open={openId === b.id ? "true" : undefined}
className={cn(
"animate-row-flash cursor-pointer",
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]"
style={isPaper ? { color: "hsl(var(--surface-ink))" } : undefined}
>
{b.id}
</TableCell>
<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]"
style={isPaper ? { color: "hsl(var(--surface-ink))" } : undefined}
>
<AnimatedNumber
value={b.claimCount}
format={(n) => fmt.num(Math.round(n))}
/>
</TableCell>
<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={cn(
"text-right",
isPaper
? "text-[hsl(var(--surface-ink-3))]"
: "text-muted-foreground",
)}
>
<span aria-hidden></span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}