736bf4d333
- 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.
179 lines
5.6 KiB
TypeScript
179 lines
5.6 KiB
TypeScript
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>
|
||
);
|
||
}
|