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:
@@ -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>
|
||||
|
||||
+109
-68
@@ -1,87 +1,128 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table
|
||||
//
|
||||
// Shared table primitive used by Claims, Remittances, Batches, Acks, etc.
|
||||
//
|
||||
// `tone="paper"` swaps the dark-mode row chrome (muted/20 header, muted/30
|
||||
// hover) for paper-toned chrome (cream surface, soft hairline border,
|
||||
// tinted hover). Paper-toned tables sit inside the cream "paper plane"
|
||||
// sections of the hybrid Magazine Spread layout. The default `tone="dark"`
|
||||
// is unchanged from the original look so existing callers keep their
|
||||
// behavior. Pages pass the tone prop once on `<Table>` and the children
|
||||
// inherit the matching colors via the data-tone attribute — no need to
|
||||
// rewrite every TableHead/TableRow/TableCell.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Tone = "dark" | "paper";
|
||||
|
||||
type TableProps = React.HTMLAttributes<HTMLTableElement> & {
|
||||
tone?: Tone;
|
||||
};
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
||||
({ className, tone = "dark", ...props }, ref) => (
|
||||
<div
|
||||
className={cn("relative w-full overflow-auto", className)}
|
||||
data-tone={tone}
|
||||
>
|
||||
<table ref={ref} className="w-full caption-bottom text-sm" {...props} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"[&_tr]:border-b [&_tr]:border-border/60 [&_tr]:bg-muted/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
type TableSectionProps = React.HTMLAttributes<HTMLTableSectionElement>;
|
||||
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, TableSectionProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
// Paper-tone: cream-papered header band, soft border, no dark muted fill.
|
||||
return (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"[&_tr]:border-b [&_tr]:border-border/60 [&_tr]:bg-muted/20",
|
||||
// When the parent <Table> is paper-toned, swap to cream chrome.
|
||||
"[[data-tone=paper]_&]:bg-[hsl(36_22%_92%)]",
|
||||
"[[data-tone=paper]_&]:[&_tr]:bg-[hsl(36_22%_92%)]",
|
||||
"[[data-tone=paper]_&]:[&_tr]:border-[hsl(30_14%_14%_/_0.10)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
const TableBody = React.forwardRef<HTMLTableSectionElement, TableSectionProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b border-border/40 transition-colors hover:bg-muted/30 focus-within:bg-muted/40 data-[state=selected]:bg-muted/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
type TableRowProps = React.HTMLAttributes<HTMLTableRowElement>;
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b border-border/40 transition-colors hover:bg-muted/30 focus-within:bg-muted/40 data-[state=selected]:bg-muted/50",
|
||||
// Paper-tone: warm cream hover, soft hairline between rows.
|
||||
"[[data-tone=paper]_&]:border-[hsl(30_14%_14%_/_0.08)]",
|
||||
"[[data-tone=paper]_&]:hover:bg-[hsl(36_22%_94%)]",
|
||||
"[[data-tone=paper]_&]:focus-within:bg-[hsl(36_22%_94%)]",
|
||||
"[[data-tone=paper]_&]:data-[state=selected]:bg-[hsl(212_85%_95%)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, scope = "col", ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
scope={scope}
|
||||
className={cn(
|
||||
"h-9 px-4 text-left align-middle text-[10.5px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/80 [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
type TableHeadProps = React.ThHTMLAttributes<HTMLTableCellElement>;
|
||||
|
||||
const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
||||
({ className, scope = "col", ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
scope={scope}
|
||||
className={cn(
|
||||
"h-9 px-4 text-left align-middle text-[10.5px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/80 [&:has([role=checkbox])]:pr-0",
|
||||
// Paper-tone: surface-ink-2 (warm dark) instead of cool muted-foreground.
|
||||
"[[data-tone=paper]_&]:text-[hsl(var(--surface-ink-2))]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("px-4 py-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
const TableCell = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-4 py-3 align-middle [&:has([role=checkbox])]:pr-0",
|
||||
// Paper-tone: warm foreground (surface-ink) for the primary text.
|
||||
"[[data-tone=paper]_&]:text-[hsl(var(--surface-ink))]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell };
|
||||
export type { Tone as TableTone };
|
||||
|
||||
Reference in New Issue
Block a user