Files
cyclone/src/components/TickerTape.tsx
T
Tyler 9bca4b608a 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.
2026-06-22 11:01:58 -06:00

152 lines
4.9 KiB
TypeScript

// ---------------------------------------------------------------------------
// TickerTape — live vital-signs strip for the dashboard hero.
//
// A horizontal tape of small numeric readouts separated by hairline
// dividers, rendered on the dark canvas. Each cell shows a label
// (uppercase mono) and a value (mono). One cell can be marked
// `accent` to draw the eye, and the optional `live` cell shows a
// pulsing dot — the dashboard's only acknowledgement of time.
//
// This is a static ticker (not scrolling); the cells are just laid
// out left-to-right with dividers. We use it to give the dark hero
// data density without committing to a real chart.
// ---------------------------------------------------------------------------
import { cn } from "@/lib/utils";
export interface TickerCell {
/** Stable id used as React key. */
id: string;
/** Uppercase mono label (e.g. "Billed", "Pending", "Denial"). */
label: string;
/** The value, rendered verbatim in mono. Format it before passing. */
value: string;
/** Optional secondary value shown muted (e.g. delta). */
delta?: string;
/** Highlight this cell with the accent colour. */
accent?: boolean;
}
export interface TickerTapeProps {
cells: TickerCell[];
/** Show the "LIVE" pulse on the leading edge. */
live?: boolean;
/** Live label (default "Live"). */
liveLabel?: string;
className?: string;
}
export function TickerTape({
cells,
live = false,
liveLabel = "Live",
className,
}: TickerTapeProps) {
return (
<div
className={cn(
"relative flex items-stretch w-full overflow-hidden",
"rounded-md border border-border/50 bg-card/40 backdrop-blur",
className
)}
>
{/* Left rail — amber signal accent for the LIVE indicator */}
{live ? (
<div
className="flex items-center gap-2 px-3 border-r border-border/60 shrink-0"
style={{
background:
"linear-gradient(180deg, hsl(36 92% 56% / 0.06), transparent)",
}}
>
<span className="relative inline-flex h-1.5 w-1.5 shrink-0">
<span
className="absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping"
style={{ backgroundColor: "hsl(var(--signal))" }}
/>
<span
className="relative inline-flex h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: "hsl(var(--signal))" }}
/>
</span>
<span
className="mono uppercase tracking-[0.18em] font-semibold"
style={{
color: "hsl(var(--signal))",
fontSize: 10,
}}
>
{liveLabel}
</span>
</div>
) : null}
{/* Cells */}
<div className="flex items-stretch flex-1 min-w-0 overflow-x-auto">
{cells.map((c, i) => (
<div
key={c.id}
className={cn(
"flex-1 min-w-[110px] px-4 py-2.5 flex flex-col gap-0.5",
i > 0 ? "border-l border-border/40" : ""
)}
>
<span
className="mono uppercase tracking-[0.18em] font-medium"
style={{
color: c.accent
? "hsl(var(--accent))"
: "hsl(var(--muted-foreground))",
fontSize: 9.5,
}}
>
{c.label}
</span>
<div className="flex items-baseline gap-1.5">
<span
className="display mono tabular-nums"
style={{
color: c.accent
? "hsl(var(--foreground))"
: "hsl(var(--foreground))",
fontSize: 16,
letterSpacing: "-0.01em",
}}
>
{c.value}
</span>
{c.delta ? (
<span
className="mono tabular-nums"
style={{
color: "hsl(var(--muted-foreground))",
fontSize: 10.5,
}}
>
{c.delta}
</span>
) : null}
</div>
</div>
))}
</div>
{/* Right rail — perforated tick pattern, decoratively marks the
end of the tape without using a heavy border. */}
<div
aria-hidden
className="shrink-0 w-3 self-stretch"
style={{
backgroundImage:
"radial-gradient(circle, hsl(36 14% 88% / 0.16) 1px, transparent 1.5px)",
backgroundSize: "6px 6px",
backgroundPosition: "0 center",
maskImage:
"linear-gradient(to right, transparent, black 30%, black 100%)",
WebkitMaskImage:
"linear-gradient(to right, transparent, black 30%, black 100%)",
}}
/>
</div>
);
}