Files
sharper.ly/apps/web/app/components/ui/Card/CardStat.tsx
T
2026-09-11 02:46:56 +02:00

107 lines
3.4 KiB
TypeScript

/* ============================================================================
Card.Stat - Figma 25:792 (260 x 166)
----------------------------------------------------------------------------
The figure card used by the home page stats strip: a large accent number,
an optional trend chip, the label it counts, a rule and a caption. Never a
link - a number is not a destination.
USAGE
-----
<ul className="grid gap-lg sm:grid-cols-2 lg:grid-cols-4">
<Card.Stat
as="li"
value="2,450"
label="Participants reached"
caption="Cumulative regional outreach, updated each quarter."
trend={{ label: "12%", direction: "up", description: "up 12% on last quarter" }}
/>
<Card.Stat as="li" value="18" label="Municipalities engaged" />
</ul>
// Arabic - Eastern Arabic numerals belong in the value string itself.
<Card.Stat value="٢٬٤٥٠" label="عدد المشاركين" />
========================================================================= */
import { CardIcon, CardShell, cx } from "./CardShell";
import type { CardElement } from "./types";
export interface CardStatTrend {
/** Visible text, e.g. "12%". */
label: string;
/** Up is the accent fill, down the danger fill. Defaults to up. */
direction?: "up" | "down";
/**
* Spoken instead of the bare figure, e.g. "up 12% on last quarter". The
* arrow is decorative, so without this a screen reader hears only "12%".
*/
description?: string;
}
export interface CardStatProps {
/** Pre-formatted for the locale - the card never formats a number itself. */
value: string;
label: string;
caption?: string;
trend?: CardStatTrend;
as?: CardElement;
className?: string;
}
export function CardStat({
value,
label,
caption,
trend,
as = "article",
className,
}: CardStatProps) {
const isDown = trend?.direction === "down";
return (
<CardShell
as={as}
className={cx("flex-col items-start gap-md p-lg", className)}
>
<div className="flex w-full items-center justify-between gap-sm">
<p className="display-sm tabular-nums text-text-accent">{value}</p>
{trend ? (
/* Figma paints the chip label in text/accent on a background/accent
fill - green on green, invisible. Kept as the accent fill with the
always-white on-accent label, and the danger fill when the figure
is falling. */
<span
className={cx(
"inline-flex shrink-0 items-center gap-2xs rounded-control-lg px-xs py-2xs",
"label-md text-text-on-accent",
isDown ? "bg-surface-danger" : "bg-surface-accent",
)}
>
<CardIcon
name="arrow-up"
size={10}
className={isDown ? "rotate-180" : undefined}
/>
{trend.description ? (
<>
<span aria-hidden="true">{trend.label}</span>
<span className="sr-only">{trend.description}</span>
</>
) : (
trend.label
)}
</span>
) : null}
</div>
<p className="body-md-bold text-text-primary">{label}</p>
<div aria-hidden="true" className="w-full border-t border-border-subtle" />
{caption ? (
<p className="body-sm text-text-secondary">{caption}</p>
) : null}
</CardShell>
);
}