Files

94 lines
2.7 KiB
TypeScript
Raw Permalink Normal View History

2026-08-13 03:29:33 +07:00
"use client";
2026-08-13 01:58:09 +07:00
import type { LucideIcon } from "lucide-react";
2026-08-13 03:29:33 +07:00
import { useRef } from "react";
2026-08-13 01:58:09 +07:00
import { cn } from "@/lib/cn";
2026-08-13 03:29:33 +07:00
import { gsap, motionEase, prefersReducedMotion, useGSAP } from "@/lib/motion";
2026-08-13 01:58:09 +07:00
export type StatItem = {
label: string;
value: number | string;
icon?: LucideIcon;
tone?: "default" | "positive" | "warning";
};
export function StatStrip({
items,
className,
}: {
items: readonly StatItem[];
className?: string;
}) {
2026-08-13 03:29:33 +07:00
const ref = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const root = ref.current;
if (!root) return;
const cells = root.querySelectorAll("[data-stat-cell]");
if (prefersReducedMotion()) {
gsap.set(cells, { autoAlpha: 1, y: 0 });
return;
}
gsap.fromTo(
cells,
{ autoAlpha: 0, y: 12 },
{ autoAlpha: 1, y: 0, duration: 0.45, stagger: 0.05, ease: motionEase.out }
);
for (const valueEl of root.querySelectorAll<HTMLElement>("[data-stat-value]")) {
const raw = valueEl.dataset.statValue;
const numeric = raw !== undefined ? Number(raw) : Number.NaN;
if (!Number.isFinite(numeric)) continue;
const state = { value: 0 };
gsap.to(state, {
value: numeric,
duration: 0.7,
ease: motionEase.out,
onUpdate: () => {
valueEl.textContent = String(Math.round(state.value));
},
});
}
},
{ scope: ref, dependencies: [items.map((item) => `${item.label}:${item.value}`).join("|")] }
);
2026-08-13 01:58:09 +07:00
return (
<div
2026-08-13 03:29:33 +07:00
ref={ref}
2026-08-13 01:58:09 +07:00
className={cn(
"grid divide-x divide-border border bg-card shadow-[2px_2px_0_color-mix(in_oklch,var(--foreground)_8%,transparent)]",
className
)}
style={{ gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))` }}
>
{items.map(({ label, value, icon: Icon, tone = "default" }) => (
2026-08-13 03:29:33 +07:00
<div key={label} data-stat-cell="" className="min-w-20 px-3 py-2.5 sm:min-w-28 sm:px-4">
2026-08-13 01:58:09 +07:00
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
{Icon && (
<Icon
className={cn(
"size-3.5",
tone === "positive" && "text-success",
tone === "warning" && "text-warning"
)}
/>
)}
<span className="truncate font-mono text-[9px] font-bold uppercase tracking-[0.12em]">
{label}
</span>
</div>
2026-08-13 03:29:33 +07:00
<strong
data-stat-value={typeof value === "number" ? value : undefined}
className="block text-xl leading-none tabular-nums sm:text-2xl"
>
{value}
</strong>
2026-08-13 01:58:09 +07:00
</div>
))}
</div>
);
}