feat(release): v0.2.0 — batch 837 export, ClaimCard, theme tokens
Backend:
- New POST /api/batches/{id}/export-837: regenerate X12 837 files
for a list of claim_ids into a ZIP using HCPF file naming standards,
with a unique interchange/group control number per export. Wire
the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
(NM1*40) blocks so the serializer no longer falls back to
CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
batch_id in both JSON and NDJSON response shapes so the frontend
can hit batch-scoped endpoints without an extra listBatches
round-trip.
- Filename helpers and the 837 serializer updated to match the new
HCPF envelope; tests cover batch export, parse batch_id, and the
serializer's control-number uniqueness guarantee.
Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
EditorialNote, ExportBar, TickerTape, and a charts/ set
(BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
colors to Tailwind theme tokens (bg-card, text-foreground,
border/60, etc.) for consistency with the rest of the instrument
chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
reworked to compose the new shared components and consume the new
batch-scoped API surface (notably ExportBar wired into Batches).
Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
components and the useBatchExport hook.
Rolls up into the v0.2.0 release tag.
This commit is contained in:
+392
-703
File diff suppressed because it is too large
Load Diff
+350
-573
File diff suppressed because it is too large
Load Diff
+159
-491
@@ -4,7 +4,9 @@ import { Files, GitBranch, Layers, Receipt } from "lucide-react";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { ErrorState } from "@/components/ui/error-state";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { KpiCard } from "@/components/KpiCard";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { BatchesList, BatchesListSkeleton } from "@/components/BatchesList";
|
||||
import {
|
||||
BatchDetailContent,
|
||||
@@ -16,6 +18,7 @@ import { useBatches } from "@/hooks/useBatches";
|
||||
import { useDrawerKeyboard } from "@/hooks/useDrawerKeyboard";
|
||||
import { api, ApiError, type BatchSummary } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fmt } from "@/lib/format";
|
||||
import type { ParseResult837, ParseResult835 } from "@/types";
|
||||
|
||||
function readBatchId(): string | null {
|
||||
@@ -120,12 +123,16 @@ function useBatchDetail(id: string | null): {
|
||||
const HELP_NOOP = () => {};
|
||||
|
||||
/**
|
||||
* Batches page — hybrid Magazine Spread treatment.
|
||||
* Batches page — dark instrument treatment, in the same chrome
|
||||
* language as Dashboard / Upload / Reconciliation / Acks.
|
||||
*
|
||||
* Dark editorial hero → torn-page fold → cream paper plane with the
|
||||
* batch register (KPI strip + tabular list). Click a row to open the
|
||||
* detail drawer (rendered above the page, dimmed via the body
|
||||
* opacity).
|
||||
* PageHeader hero (eyebrow + display title + status pill + ghost
|
||||
* watermark) → KpiCard strip (4 tiles) → single Card wrapping the
|
||||
* tabular register → quiet hairline footer.
|
||||
*
|
||||
* Click a row to open the detail drawer (rendered above the page,
|
||||
* dimmed via the body opacity). Keyboard: ← / → step through
|
||||
* batches while the drawer is open; Esc closes.
|
||||
*/
|
||||
export function Batches() {
|
||||
const { data, isLoading, isError, error, refetch } = useBatches(100);
|
||||
@@ -166,7 +173,7 @@ export function Batches() {
|
||||
const errKind = batchErrorKind(detailError);
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Aggregate metrics for the § 01 KPI strip
|
||||
// Aggregate metrics for the KPI strip + hero copy.
|
||||
// -----------------------------------------------------------------
|
||||
const totals = items.reduce(
|
||||
(acc, b) => ({
|
||||
@@ -178,14 +185,20 @@ export function Batches() {
|
||||
{ count: 0, p837: 0, p835: 0, claims: 0 },
|
||||
);
|
||||
|
||||
const lastParsed = items[0]?.parsedAt;
|
||||
|
||||
// The ghost watermark carries the on-file count, scaled like the
|
||||
// other pages (clamp 72–140px, 4.5% opacity, right-anchored).
|
||||
const watermark = totals.count > 0 ? totals.count.toLocaleString() : "BATCH";
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Stagger choreography (matches Acks / Reconciliation / Upload)
|
||||
// Stagger choreography — hero lands first, KPI strip at 140ms,
|
||||
// register at 240ms, footer at 320ms. Total < 500ms.
|
||||
// -----------------------------------------------------------------
|
||||
const heroDelay = 0;
|
||||
const foldDelay = 220;
|
||||
const titleDelay = 320;
|
||||
const kpiDelay = 460;
|
||||
const tableDelay = 600;
|
||||
const kpiDelay = 140;
|
||||
const tableDelay = 240;
|
||||
const footerDelay = 320;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -219,541 +232,196 @@ export function Batches() {
|
||||
<div
|
||||
data-testid="batches-page-body"
|
||||
className={cn(
|
||||
"space-y-0 animate-fade-in transition-opacity",
|
||||
"space-y-6 lg:space-y-10 animate-fade-in transition-opacity",
|
||||
dimBackground && "pointer-events-none opacity-60",
|
||||
)}
|
||||
>
|
||||
{/* =================================================================
|
||||
HERO — DARK EDITORIAL HEADER
|
||||
HERO — dark editorial header
|
||||
================================================================= */}
|
||||
<section
|
||||
className="relative pt-6 pb-8 lg:pt-9 lg:pb-10"
|
||||
className="relative animate-fade-in overflow-hidden"
|
||||
style={{ animationDelay: `${heroDelay}ms` }}
|
||||
>
|
||||
{/* Ghost "PARSED" watermark — a print-shop stamp behind the title. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none select-none absolute inset-x-0 top-[58%] -translate-y-1/2 whitespace-nowrap display text-center"
|
||||
className="pointer-events-none select-none absolute -right-4 top-1/2 -translate-y-1/2 whitespace-nowrap display text-foreground"
|
||||
style={{
|
||||
fontSize: "clamp(160px, 20vw, 300px)",
|
||||
letterSpacing: "-0.05em",
|
||||
opacity: 0.05,
|
||||
fontSize: "clamp(72px, 12vw, 140px)",
|
||||
letterSpacing: "-0.04em",
|
||||
opacity: 0.045,
|
||||
lineHeight: 1,
|
||||
color: "hsl(var(--surface-ink))",
|
||||
}}
|
||||
>
|
||||
PARSED
|
||||
{watermark}
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-[1fr,auto] gap-8 items-end mb-7">
|
||||
<div className="min-w-0 max-w-3xl">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="h-px w-14 bg-foreground/25" />
|
||||
<span className="mono text-[12px] uppercase tracking-[0.22em] text-muted-foreground">
|
||||
Batches · Register
|
||||
</span>
|
||||
<span
|
||||
className="mono text-[12px] uppercase tracking-[0.22em] text-muted-foreground/60 hidden sm:inline"
|
||||
aria-hidden
|
||||
>
|
||||
· {totals.count} on file
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="display text-[48px] sm:text-[64px] lg:text-[80px] leading-[0.92] text-foreground tracking-[-0.04em]">
|
||||
Parsed{" "}
|
||||
<span className="italic text-muted-foreground/85">batches.</span>
|
||||
</h1>
|
||||
<div className="relative z-10 flex items-end justify-between gap-4 sm:gap-6 flex-wrap">
|
||||
<div className="min-w-0">
|
||||
<PageHeader
|
||||
eyebrow={`Batches · Register · ${totals.count} on file`}
|
||||
title={
|
||||
<>
|
||||
Parsed{" "}
|
||||
<span className="italic text-muted-foreground/85">
|
||||
batches.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
subtitle={
|
||||
<>
|
||||
Every 837P and 835 file the backend has ingested. Click a
|
||||
row for envelope, summary, and a peek at the contained
|
||||
claims.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start lg:items-end gap-3">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/60 px-3.5 py-2 text-[11.5px] mono uppercase tracking-[0.14em] text-muted-foreground backdrop-blur">
|
||||
<Files
|
||||
className="h-3.5 w-3.5"
|
||||
strokeWidth={1.75}
|
||||
style={{ color: "hsl(var(--accent))" }}
|
||||
/>
|
||||
837P · 835 · envelope & summary
|
||||
</div>
|
||||
<kbd
|
||||
className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/70"
|
||||
>
|
||||
Tap a row to open the drawer
|
||||
</kbd>
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/60 px-3 py-1.5 text-[11px] mono uppercase tracking-[0.12em] text-muted-foreground backdrop-blur">
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[hsl(var(--accent))]">
|
||||
<span className="absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--accent))] opacity-60 animate-ping" />
|
||||
</span>
|
||||
<Files className="h-3.5 w-3.5" strokeWidth={1.75} />
|
||||
837P · 835 · envelope & summary
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-2xl">
|
||||
<p className="text-[14px] text-muted-foreground leading-relaxed">
|
||||
Every 837P and 835 file the backend has ingested. Click a row
|
||||
for envelope, summary, and a peek at the contained claims.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* =================================================================
|
||||
FOLD — TORN PAGE
|
||||
KPI STRIP — four standard KpiCards, accent tokens instead of
|
||||
paper-tinted custom tiles.
|
||||
================================================================= */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="relative h-14"
|
||||
style={{ animationDelay: `${foldDelay}ms` }}
|
||||
<section
|
||||
aria-label="Batch register metrics"
|
||||
className="grid gap-3.5 grid-cols-2 md:grid-cols-4 animate-fade-in-up"
|
||||
style={{ animationDelay: `${kpiDelay}ms` }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-1/2"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, hsl(0 0% 0% / 0.55), transparent)",
|
||||
}}
|
||||
<KpiCard
|
||||
label="On file"
|
||||
value={fmt.num(totals.count)}
|
||||
icon={Layers}
|
||||
accent="default"
|
||||
hint={totals.count === 1 ? "batch" : "batches"}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 h-1/2"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, hsl(30 14% 14% / 0.18), transparent)",
|
||||
}}
|
||||
<KpiCard
|
||||
label="837P"
|
||||
value={fmt.num(totals.p837)}
|
||||
icon={GitBranch}
|
||||
accent="accent"
|
||||
hint="professional claims"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-px"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to right, transparent, hsl(30 14% 14% / 0.5) 12%, hsl(30 14% 14% / 0.7) 50%, hsl(30 14% 14% / 0.5) 88%, transparent)",
|
||||
}}
|
||||
<KpiCard
|
||||
label="835"
|
||||
value={fmt.num(totals.p835)}
|
||||
icon={Receipt}
|
||||
accent="warning"
|
||||
hint="remittance advice"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-x-0 top-1/2 -translate-y-1/2 flex justify-between px-2"
|
||||
style={{ pointerEvents: "none" }}
|
||||
>
|
||||
{Array.from({ length: 48 }, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="block h-[3px] w-[3px] rounded-full"
|
||||
style={{ backgroundColor: "hsl(30 14% 14% / 0.22)" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative z-10 mx-auto h-full flex items-center justify-center gap-3 bg-background px-4 w-fit">
|
||||
<span
|
||||
className="display italic"
|
||||
style={{ color: "hsl(var(--muted-foreground))", fontSize: 15 }}
|
||||
>
|
||||
↘
|
||||
</span>
|
||||
<span
|
||||
className="mono uppercase tracking-[0.24em]"
|
||||
style={{
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
The register
|
||||
</span>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{ color: "hsl(var(--muted-foreground))", fontSize: 15 }}
|
||||
>
|
||||
↙
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
label="Claims"
|
||||
value={fmt.num(totals.claims)}
|
||||
accent="success"
|
||||
hint="total in register"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* =================================================================
|
||||
PAPER PLANE
|
||||
REGISTER — single Card wrapping the tabular list. Same
|
||||
chrome language as the rest of the app: eyebrow + display
|
||||
title + sidecar hint, dark-tone table.
|
||||
================================================================= */}
|
||||
<div
|
||||
className="relative max-w-[1280px] mx-auto"
|
||||
style={{
|
||||
animationDelay: `${titleDelay}ms`,
|
||||
backgroundColor: "hsl(var(--surface))",
|
||||
boxShadow:
|
||||
"inset 0 1px 0 0 hsl(0 0% 100% / 0.5), 0 1px 0 0 hsl(30 14% 22% / 0.06), 0 30px 80px -24px hsl(0 0% 0% / 0.45)",
|
||||
}}
|
||||
<section
|
||||
aria-label="Batch register"
|
||||
className="animate-fade-in-up"
|
||||
style={{ animationDelay: `${tableDelay}ms` }}
|
||||
>
|
||||
{/* Paper grain */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.04]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.1 0 0 0 0 0.08 0 0 0 0 0.05 0 0 0 0.9 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>\")",
|
||||
backgroundSize: "160px 160px",
|
||||
mixBlendMode: "multiply",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Title block */}
|
||||
<div
|
||||
className="relative px-8 lg:px-14 pt-12 pb-9 border-b animate-fade-in-up"
|
||||
style={{
|
||||
animationDelay: `${titleDelay}ms`,
|
||||
borderColor: "hsl(30 14% 14% / 0.12)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute left-7 lg:left-12 top-0 bottom-0 w-px"
|
||||
style={{ backgroundColor: "hsl(30 14% 14% / 0.14)" }}
|
||||
/>
|
||||
<div className="flex items-end justify-between gap-8 flex-wrap">
|
||||
<div>
|
||||
<div
|
||||
className="mono text-[12px] uppercase tracking-[0.24em] mb-4 font-medium"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
Register · Parsed batches
|
||||
</div>
|
||||
<h2
|
||||
className="display leading-[0.92] tracking-[-0.04em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(48px, 7vw, 96px)",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
The register.
|
||||
</h2>
|
||||
<div
|
||||
className="mt-4 display italic"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink-2))",
|
||||
fontSize: "clamp(15px, 1.3vw, 18px)",
|
||||
lineHeight: 1.4,
|
||||
maxWidth: "32ch",
|
||||
}}
|
||||
>
|
||||
One row per ingested file — the kind, the input filename,
|
||||
how many claims it carried, and the day it was parsed.
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div
|
||||
className="mono text-[11px] uppercase tracking-[0.24em] mb-1.5 font-medium"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
On file
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: 26,
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
{totals.count}
|
||||
</div>
|
||||
<div
|
||||
className="mono text-[11px] mt-1"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
batch{totals.count === 1 ? "" : "es"} · {totals.claims}{" "}
|
||||
claim{totals.claims === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI strip — § 01 Vital signs */}
|
||||
<section
|
||||
aria-label="Batch register metrics"
|
||||
className="relative px-8 lg:px-14 py-7 animate-fade-in-up"
|
||||
style={{ animationDelay: `${kpiDelay}ms` }}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute left-7 lg:left-12 top-7 flex flex-col items-center gap-1"
|
||||
>
|
||||
<span
|
||||
className="mono text-[10px] uppercase tracking-[0.18em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
§ 01
|
||||
</span>
|
||||
<div
|
||||
className="w-px h-10"
|
||||
style={{ backgroundColor: "hsl(30 14% 14% / 0.18)" }}
|
||||
/>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink-2))",
|
||||
fontSize: 11,
|
||||
writingMode: "vertical-rl",
|
||||
transform: "rotate(180deg)",
|
||||
letterSpacing: "0.16em",
|
||||
}}
|
||||
>
|
||||
Vital signs
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<BatchesKpiTile
|
||||
label="On file"
|
||||
value={totals.count}
|
||||
icon={<Layers className="h-3 w-3" strokeWidth={1.75} />}
|
||||
tone="ink"
|
||||
/>
|
||||
<BatchesKpiTile
|
||||
label="837P"
|
||||
value={totals.p837}
|
||||
icon={<GitBranch className="h-3 w-3" strokeWidth={1.75} />}
|
||||
tone="blue"
|
||||
/>
|
||||
<BatchesKpiTile
|
||||
label="835"
|
||||
value={totals.p835}
|
||||
icon={<Receipt className="h-3 w-3" strokeWidth={1.75} />}
|
||||
tone="amber"
|
||||
/>
|
||||
<BatchesKpiTile
|
||||
label="Claims"
|
||||
value={totals.claims}
|
||||
tone="success"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Table — § 02 The register */}
|
||||
<section
|
||||
aria-label="Batch register"
|
||||
className="relative px-8 lg:px-14 pb-8 lg:pb-10 animate-fade-in-up"
|
||||
style={{ animationDelay: `${tableDelay}ms` }}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute left-7 lg:left-12 top-9 flex flex-col items-center gap-1"
|
||||
>
|
||||
<span
|
||||
className="mono text-[10px] uppercase tracking-[0.18em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
§ 02
|
||||
</span>
|
||||
<div
|
||||
className="w-px h-12"
|
||||
style={{ backgroundColor: "hsl(30 14% 14% / 0.18)" }}
|
||||
/>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink-2))",
|
||||
fontSize: 11,
|
||||
writingMode: "vertical-rl",
|
||||
transform: "rotate(180deg)",
|
||||
letterSpacing: "0.16em",
|
||||
}}
|
||||
>
|
||||
The register
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="pt-6 border-t"
|
||||
style={{
|
||||
borderTopStyle: "double",
|
||||
borderTopWidth: 3,
|
||||
borderColor: "hsl(30 14% 14% / 0.12)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-end justify-between gap-6 flex-wrap mb-5">
|
||||
<div>
|
||||
<div
|
||||
className="mono text-[11.5px] uppercase tracking-[0.24em] font-semibold mb-2"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="p-6 lg:p-7 space-y-5">
|
||||
<div className="flex items-end justify-between gap-6 flex-wrap">
|
||||
<div className="min-w-0">
|
||||
<div className="eyebrow flex items-center gap-2 mb-2">
|
||||
<span className="inline-block h-px w-6 bg-foreground/20" />
|
||||
The register
|
||||
</div>
|
||||
<h3
|
||||
className="display leading-[0.98] tracking-[-0.03em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(24px, 2.6vw, 32px)",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
<h2 className="display text-[26px] leading-[1.05] tracking-[-0.02em]">
|
||||
All <span className="italic">ingests</span>, newest first.
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="text-[12.5px] display italic"
|
||||
style={{ color: "hsl(var(--surface-ink-2))", maxWidth: 380 }}
|
||||
>
|
||||
<p className="text-[12.5px] text-muted-foreground/80 max-w-sm">
|
||||
Tap a row to open the envelope drawer. Use{" "}
|
||||
<kbd
|
||||
className="mono not-italic text-[10.5px] uppercase tracking-[0.14em] px-1 py-0.5 rounded-sm border"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.20)",
|
||||
color: "hsl(var(--surface-ink))",
|
||||
}}
|
||||
>
|
||||
<kbd className="rounded-sm border border-border/60 bg-card/40 px-1.5 py-0.5 not-italic text-foreground/80 mono text-[10.5px] uppercase tracking-[0.14em]">
|
||||
←
|
||||
</kbd>{" "}
|
||||
/{" "}
|
||||
<kbd
|
||||
className="mono not-italic text-[10.5px] uppercase tracking-[0.14em] px-1 py-0.5 rounded-sm border"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.20)",
|
||||
color: "hsl(var(--surface-ink))",
|
||||
}}
|
||||
>
|
||||
<kbd className="rounded-sm border border-border/60 bg-card/40 px-1.5 py-0.5 not-italic text-foreground/80 mono text-[10.5px] uppercase tracking-[0.14em]">
|
||||
→
|
||||
</kbd>{" "}
|
||||
inside the drawer to step.
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isError ? (
|
||||
<div
|
||||
className="rounded-md border p-5"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.16)",
|
||||
backgroundColor: "hsl(36 22% 96%)",
|
||||
}}
|
||||
>
|
||||
<div className="pt-5 border-t border-border/40">
|
||||
{isError ? (
|
||||
<ErrorState
|
||||
message="Couldn't load batches from the backend."
|
||||
detail={error instanceof Error ? error.message : String(error)}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div
|
||||
className="rounded-md border p-4 space-y-2"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.10)",
|
||||
backgroundColor: "hsl(36 22% 96%)",
|
||||
}}
|
||||
>
|
||||
) : isLoading ? (
|
||||
<BatchesListSkeleton />
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div
|
||||
className="rounded-md border p-10 text-center"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.16)",
|
||||
borderStyle: "dashed",
|
||||
backgroundColor: "hsl(36 22% 96%)",
|
||||
}}
|
||||
>
|
||||
<EmptyState
|
||||
eyebrow="Batches · awaiting first ingest"
|
||||
message="Upload an 837P or 835 file on the Upload page to populate this list."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-md 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)",
|
||||
}}
|
||||
>
|
||||
<BatchesList
|
||||
items={items}
|
||||
openId={batchId}
|
||||
onOpen={open}
|
||||
tone="paper"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
) : items.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-border/60 bg-card/40">
|
||||
<EmptyState
|
||||
eyebrow="Batches · awaiting first ingest"
|
||||
message="Upload an 837P or 835 file on the Upload page to populate this list."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-border/60 overflow-hidden bg-card/40">
|
||||
<BatchesList
|
||||
items={items}
|
||||
openId={batchId}
|
||||
onOpen={open}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
className="relative px-8 lg:px-14 py-5 border-t flex items-center justify-between mono text-[10.5px] uppercase tracking-[0.18em]"
|
||||
style={{
|
||||
borderColor: "hsl(30 14% 14% / 0.12)",
|
||||
color: "hsl(var(--surface-ink-3))",
|
||||
}}
|
||||
>
|
||||
<span>End of register</span>
|
||||
<span>Cyclone · Batches</span>
|
||||
{/* =================================================================
|
||||
FOOTER — single hairline-separated status row. Replaces the
|
||||
warm-paper "End of register" treatment with a quiet
|
||||
instrument-style status line in the same rhythm as the
|
||||
other pages.
|
||||
================================================================= */}
|
||||
<section
|
||||
aria-label="Register status"
|
||||
className="animate-fade-in-up flex items-center justify-between gap-4 flex-wrap pt-5 border-t border-border/40"
|
||||
style={{ animationDelay: `${footerDelay}ms` }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/60">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[hsl(var(--accent))]" />
|
||||
Cyclone · Batches
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/60">
|
||||
<span>
|
||||
{totals.count} {totals.count === 1 ? "row" : "rows"}
|
||||
</span>
|
||||
<span className="text-muted-foreground/30" aria-hidden>
|
||||
·
|
||||
</span>
|
||||
<span>last parsed {lastParsed ? fmt.dateShort(lastParsed) : "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/60">
|
||||
<span>open drawer</span>
|
||||
<kbd className="rounded-sm border border-border/60 bg-card/40 px-1.5 py-0.5 not-italic text-foreground/80">
|
||||
↩
|
||||
</kbd>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BatchesKpiTile — paper-toned metric for the batch register.
|
||||
// Mirrors AckKpiTile / KpiTile in the other hybrid pages.
|
||||
// ---------------------------------------------------------------------------
|
||||
function BatchesKpiTile({
|
||||
label,
|
||||
value,
|
||||
tone,
|
||||
icon,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
tone: "blue" | "amber" | "ink" | "success";
|
||||
icon?: React.ReactNode;
|
||||
}) {
|
||||
const accentMap = {
|
||||
blue: "hsl(212 100% 45%)",
|
||||
amber: "hsl(36 92% 50%)",
|
||||
ink: "hsl(var(--surface-ink))",
|
||||
success: "hsl(152 64% 38%)",
|
||||
} as const;
|
||||
const tintMap = {
|
||||
blue: "hsl(212 85% 92%)",
|
||||
amber: "hsl(36 82% 92%)",
|
||||
ink: "hsl(36 22% 90%)",
|
||||
success: "hsl(152 50% 88%)",
|
||||
} as const;
|
||||
const accent = accentMap[tone];
|
||||
const tint = tintMap[tone];
|
||||
return (
|
||||
<div
|
||||
className="relative rounded-xl p-5 overflow-hidden border"
|
||||
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-5 bottom-5 w-[3px] rounded-r-sm"
|
||||
style={{ backgroundColor: accent, opacity: 0.85 }}
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-2.5">
|
||||
<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 ?? (
|
||||
<span
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{ backgroundColor: accent }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums tracking-[-0.04em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(28px, 3vw, 40px)",
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+136
-106
@@ -224,9 +224,26 @@ export default function Inbox() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen"
|
||||
className="min-h-screen relative"
|
||||
style={{ background: "var(--tt-bg)", color: "var(--tt-ink)" }}
|
||||
>
|
||||
{/* Ambient halo — a soft, fixed radial that creates a subtle
|
||||
lighter zone behind the InboxHeader. The inbox has its own
|
||||
dark ticker-tape background so it doesn't receive the body
|
||||
wash; this overlay restores the "lit from above" feel that
|
||||
the main pages get from the body::before softbox, in the
|
||||
same warm-amber accent family as the rest of the surface.
|
||||
Pointer-events disabled. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-x-0 top-0 z-0 h-[55vh]"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(ellipse 60% 38% at 50% 0%, hsla(36, 60%, 30%, 0.10), transparent 65%),
|
||||
radial-gradient(ellipse 35% 28% at 88% 8%, hsla(36, 70%, 50%, 0.07), transparent 60%)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* SP21 Phase 4 Task 4.4: RemitDrawer mounts here so a row click
|
||||
in the candidates / unmatched lanes drills into the parent
|
||||
remit. The drawer portals into document.body (Radix Dialog),
|
||||
@@ -245,50 +262,11 @@ export default function Inbox() {
|
||||
/>
|
||||
<InboxHeader needEyesCount={needEyes} doneTodayCount={lanes.done_today.length} />
|
||||
|
||||
{/* Fold — a thin amber rule + italic serif annotation that
|
||||
transitions the editorial header into the lane grid below. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="relative h-10 flex items-center"
|
||||
style={{ background: "var(--tt-bg)" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-px"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to right, transparent, var(--tt-amber) 12%, var(--tt-amber) 88%, transparent)",
|
||||
opacity: 0.35,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="relative z-10 mx-auto px-3 flex items-center gap-2"
|
||||
style={{ background: "var(--tt-bg)" }}
|
||||
>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{ color: "var(--tt-amber)", fontSize: 14 }}
|
||||
>
|
||||
↘
|
||||
</span>
|
||||
<span
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 10.5,
|
||||
letterSpacing: "0.24em",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Five lanes · one queue
|
||||
</span>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{ color: "var(--tt-amber)", fontSize: 14 }}
|
||||
>
|
||||
↙
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Lane grid transitions directly into the summary strip below.
|
||||
The previous "fold" divider with italic serif arrows and
|
||||
"Five lanes · one queue" caption was the page's last warm-
|
||||
paper print-shop artifact — removed so the ticker-tape
|
||||
aesthetic carries all the way through. */}
|
||||
|
||||
<main className="px-6 lg:px-10 pb-24 pt-4 flex gap-4 items-stretch flex-wrap">
|
||||
<Lane
|
||||
@@ -374,64 +352,101 @@ export default function Inbox() {
|
||||
</main>
|
||||
|
||||
{/* ----------------------------------------------------------------
|
||||
PAPER-TONED SUMMARY STRIP
|
||||
A cream "ledger" panel below the lanes that shows the aggregate
|
||||
eye-flow across the queue. Mirrors the "Magazine Spread" pattern
|
||||
used on the Dashboard/Claims/etc pages so the Inbox feels part
|
||||
of the same document set, not a stand-alone terminal.
|
||||
TICKER-TAPE QUEUE SUMMARY
|
||||
A dark terminal panel below the lanes that aggregates the
|
||||
eye-flow across the queue. Sits on the same --tt-bg-elev
|
||||
surface as the Lane cards so the page reads as one continuous
|
||||
instrument instead of mixing a paper-toned "ledger" panel
|
||||
into the terminal aesthetic.
|
||||
---------------------------------------------------------------- */}
|
||||
<section
|
||||
aria-label="Queue summary"
|
||||
className="relative mx-6 lg:mx-10 mt-6 mb-4"
|
||||
className="relative mx-6 lg:mx-10 mt-5 mb-6"
|
||||
>
|
||||
<div
|
||||
className="relative rounded-xl overflow-hidden border"
|
||||
className="relative rounded-md overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: "hsl(var(--surface))",
|
||||
borderColor: "hsl(30 14% 14% / 0.10)",
|
||||
// "Lit from above" gradient — same treatment as the lane
|
||||
// cards and the main .surface panels, so the queue
|
||||
// summary reads as a slightly lifted, subtly lighter
|
||||
// surface against the ticker-tape backdrop.
|
||||
background:
|
||||
"linear-gradient(180deg, hsl(220 18% 11.5%) 0%, hsl(220 18% 9.5%) 55%, hsl(220 18% 8.5%) 100%)",
|
||||
border: "1px solid hsl(220 8% 18%)",
|
||||
boxShadow:
|
||||
"inset 0 1px 0 0 hsl(0 0% 100% / 0.5), 0 1px 0 0 hsl(30 14% 22% / 0.06), 0 12px 40px -16px hsl(0 0% 0% / 0.45)",
|
||||
"inset 0 1px 0 0 hsl(0 0% 100% / 0.05), 0 1px 2px 0 hsl(0 0% 0% / 0.4)",
|
||||
}}
|
||||
>
|
||||
{/* Title row */}
|
||||
{/* Header row — machine-tag eyebrow + editorial headline +
|
||||
aggregate Need eyes count, separated from the lane metrics
|
||||
by a hairline. */}
|
||||
<div
|
||||
className="px-5 lg:px-7 py-4 border-b flex items-center justify-between gap-4 flex-wrap"
|
||||
style={{ borderColor: "hsl(30 14% 14% / 0.10)" }}
|
||||
className="px-5 lg:px-6 py-3.5 flex items-center justify-between gap-4 flex-wrap"
|
||||
style={{ borderBottom: "1px solid hsl(220 8% 16%)" }}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="mono text-[10.5px] uppercase tracking-[0.20em] font-semibold mb-1.5"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
Queue ledger
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2.5 mb-1.5">
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block h-1.5 w-1.5 rounded-full animate-pulse-dot"
|
||||
style={{ background: "var(--tt-amber)" }}
|
||||
/>
|
||||
<span
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-amber)",
|
||||
fontSize: 10.5,
|
||||
letterSpacing: "0.22em",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Eye-flow
|
||||
</span>
|
||||
<span
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-muted)",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.18em",
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
· five lanes, one queue
|
||||
</span>
|
||||
</div>
|
||||
<h2
|
||||
className="display leading-[0.95] tracking-[-0.03em]"
|
||||
className="display italic"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(22px, 2.2vw, 28px)",
|
||||
color: "var(--tt-ink)",
|
||||
fontSize: "clamp(20px, 1.9vw, 24px)",
|
||||
lineHeight: 1.1,
|
||||
fontWeight: 400,
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
The eye-flow, <span className="italic">at a glance.</span>
|
||||
at a glance.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="text-right"
|
||||
aria-label="Aggregate metrics"
|
||||
>
|
||||
<div className="text-right" aria-label="Aggregate metrics">
|
||||
<div
|
||||
className="mono text-[10px] uppercase tracking-[0.18em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.20em",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Need eyes
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: 24,
|
||||
lineHeight: 1.1,
|
||||
color: "var(--tt-amber)",
|
||||
fontSize: 28,
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{needEyes}
|
||||
@@ -439,8 +454,13 @@ export default function Inbox() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lane metrics row — paper tiles that mirror each lane's accent */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 divide-x divide-y lg:divide-y-0" style={{ borderColor: "hsl(30 14% 14% / 0.08)" }}>
|
||||
{/* Lane metrics row — each tile carries its own accent dot
|
||||
and count, so the operator can read the queue breakdown
|
||||
at a glance without scanning back up to the Lane grid. */}
|
||||
<div
|
||||
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5"
|
||||
style={{ borderTop: "1px solid hsl(220 8% 16%)" }}
|
||||
>
|
||||
<SummaryTile
|
||||
label="Rejected"
|
||||
value={lanes.rejected.length}
|
||||
@@ -629,33 +649,31 @@ export default function Inbox() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SummaryTile — paper-toned metric for the queue ledger strip. Mirrors
|
||||
// the lane accent (oxblood / amber / ink-blue / muted) so the eye-flow
|
||||
// reads at a glance.
|
||||
// SummaryTile — ticker-tape metric for the eye-flow strip. Each tile
|
||||
// carries the lane's accent dot + label + count in the same color
|
||||
// family the Lane component uses (oxblood / amber / ink-blue / muted),
|
||||
// so the operator can read the queue breakdown at a glance without
|
||||
// scanning back up to the lane grid.
|
||||
// ---------------------------------------------------------------------------
|
||||
const SUMMARY_ACCENTS: Record<
|
||||
"oxblood" | "amber" | "ink" | "muted",
|
||||
{ dot: string; tint: string; text: string }
|
||||
{ dot: string; value: string }
|
||||
> = {
|
||||
oxblood: {
|
||||
dot: "hsl(358 70% 42%)",
|
||||
tint: "hsl(358 70% 92%)",
|
||||
text: "hsl(358 70% 30%)",
|
||||
dot: "var(--tt-oxblood)",
|
||||
value: "var(--tt-ink)",
|
||||
},
|
||||
amber: {
|
||||
dot: "hsl(36 92% 50%)",
|
||||
tint: "hsl(36 82% 88%)",
|
||||
text: "hsl(36 92% 30%)",
|
||||
dot: "var(--tt-amber)",
|
||||
value: "var(--tt-amber)",
|
||||
},
|
||||
ink: {
|
||||
dot: "hsl(212 80% 45%)",
|
||||
tint: "hsl(212 85% 92%)",
|
||||
text: "hsl(212 80% 30%)",
|
||||
dot: "var(--tt-ink-blue)",
|
||||
value: "var(--tt-ink)",
|
||||
},
|
||||
muted: {
|
||||
dot: "hsl(30 8% 50%)",
|
||||
tint: "hsl(36 22% 90%)",
|
||||
text: "hsl(30 8% 30%)",
|
||||
dot: "var(--tt-muted)",
|
||||
value: "var(--tt-ink-dim)",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -672,41 +690,53 @@ function SummaryTile({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative px-4 py-4 first:pl-5 lg:first:pl-7 last:pr-5 lg:last:pr-7"
|
||||
"relative px-4 py-4 first:pl-5 lg:first:pl-6 last:pr-5 lg:last:pr-6"
|
||||
)}
|
||||
style={{ borderRight: "1px solid hsl(220 8% 14%)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
aria-hidden
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{ backgroundColor: a.dot }}
|
||||
style={{
|
||||
backgroundColor: a.dot,
|
||||
boxShadow: `0 0 6px ${a.dot}`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="mono text-[10px] uppercase tracking-[0.20em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 10.5,
|
||||
letterSpacing: "0.18em",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums tracking-[-0.03em]"
|
||||
className="display tabular-nums"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(24px, 2vw, 30px)",
|
||||
color: a.value,
|
||||
fontSize: "clamp(22px, 1.9vw, 28px)",
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<div
|
||||
className="mono text-[10px] uppercase tracking-[0.16em] mt-1.5 inline-flex items-center gap-1 px-1.5 py-0.5 rounded-sm"
|
||||
className="mono uppercase mt-1.5"
|
||||
style={{
|
||||
color: a.text,
|
||||
backgroundColor: a.tint,
|
||||
color: "var(--tt-muted)",
|
||||
fontSize: 9.5,
|
||||
letterSpacing: "0.16em",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{value === 0 ? "Clear" : value === 1 ? "row" : "rows"}
|
||||
{value === 0 ? "clear" : value === 1 ? "row" : "rows"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+326
-781
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@ import React, { act, useEffect } from "react";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { MemoryRouter, useLocation } from "react-router-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { ClaimCard837, ClaimCard835 } from "./Upload";
|
||||
import { ClaimCard837 } from "@/components/ClaimCard837";
|
||||
import { ClaimCard835 } from "./Upload";
|
||||
import { useAppStore } from "@/store";
|
||||
import type { ClaimOutput, ClaimPayment, ParsedBatch } from "@/types";
|
||||
|
||||
|
||||
+537
-1020
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user