Files
PaperClipAI/ui/src/components/FinanceTimelineCard.tsx
T
scotttongandClaude Fable 5 4b3f087d7f refactor(ui): extract font-size and letter-spacing to tokens (Phase 2 batch 2/4)
Replaces hardcoded TYPE values in ui/src/components/** and ui/src/pages/**
with references to new verbatim CSS custom-property tokens in
ui/src/index.css, via a new committed codemod (scripts/codemod-extract-type.mjs).

- 932 sites rewritten across 143 files:
  - 728 arbitrary font-size Tailwind classes (text-[Npx])
  - 202 arbitrary letter-spacing classes (tracking-[N em])
  - 2 inline-style fontSize string literals
  - 0 leading-[...] sites found (none existed)
- 17 new tokens minted, no normalizing: 8 --fs-* (9/10/11/12/13/14/15px +
  0.7rem, kept in its own unit) and 9 --ls-* (0.08-0.24em, 9 distinct
  values) — the micro type-size and letter-spacing clusters TOKEN-AUDIT.md
  flagged are now materialized as tokens, still un-collapsed pending a
  human scale decision.
- 2 sites allowlisted (both already-documented functional/third-party
  fontSize forms: CompanyEnvironments.tsx xterm.js config, CompanySkills.tsx
  computed Math.round() value) — no new allowlist entries needed since
  neither is a class-string or literal-fontSize site the codemod targets.
- Verified: rg gates clean, pnpm build-storybook exit 0, Storybook visual
  suite 510/510 passed (first attempt), pnpm typecheck exit 0, codemod
  re-run confirmed idempotent (0 sites on second pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:29:51 -07:00

72 lines
3.1 KiB
TypeScript

import type { FinanceEvent } from "@paperclipai/shared";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
financeDirectionDisplayName,
financeEventKindDisplayName,
formatCents,
formatDateTime,
providerDisplayName,
} from "@/lib/utils";
interface FinanceTimelineCardProps {
rows: FinanceEvent[];
emptyMessage?: string;
}
export function FinanceTimelineCard({
rows,
emptyMessage = "No financial events in this period.",
}: FinanceTimelineCardProps) {
return (
<Card>
<CardHeader className="px-4 pt-4 pb-1">
<CardTitle className="text-base">Recent financial events</CardTitle>
<CardDescription>Top-ups, fees, credits, commitments, and other non-request charges.</CardDescription>
</CardHeader>
<CardContent className="space-y-3 px-4 pb-4 pt-3">
{rows.length === 0 ? (
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
) : (
rows.map((row) => (
<div
key={row.id}
className="border border-border p-3"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{financeEventKindDisplayName(row.eventKind)}</Badge>
<Badge variant={row.direction === "credit" ? "outline" : "secondary"}>
{financeDirectionDisplayName(row.direction)}
</Badge>
<span className="text-xs text-muted-foreground">{formatDateTime(row.occurredAt)}</span>
</div>
<div className="text-sm font-medium">
{providerDisplayName(row.biller)}
{row.provider ? ` -> ${providerDisplayName(row.provider)}` : ""}
{row.model ? <span className="ml-1 font-mono text-xs text-muted-foreground">{row.model}</span> : null}
</div>
{(row.description || row.externalInvoiceId || row.region || row.pricingTier) && (
<div className="space-y-1 text-xs text-muted-foreground">
{row.description ? <div>{row.description}</div> : null}
{row.externalInvoiceId ? <div>invoice {row.externalInvoiceId}</div> : null}
{row.region ? <div>region {row.region}</div> : null}
{row.pricingTier ? <div>tier {row.pricingTier}</div> : null}
</div>
)}
</div>
<div className="text-right tabular-nums">
<div className="text-sm font-semibold">{formatCents(row.amountCents)}</div>
<div className="text-xs text-muted-foreground">{row.currency}</div>
{row.estimated ? <div className="text-(length:--fs-11) uppercase tracking-(--ls-0_12) text-amber-600">estimated</div> : null}
</div>
</div>
</div>
))
)}
</CardContent>
</Card>
);
}