Files
TUIkit/docs/app/components/StatCard.tsx
T
phranck a6930f17be feat(dashboard): add stargazer avatar marquee with horizontal scroll
- Create generic AvatarMarquee component for horizontal scrolling avatars
- Extract StargazerPopoverContent for social links display
- Refactor StargazersPanel to use AvatarMarquee
- Reorder StatCards: Stars, Contributors, Forks, Releases | Commits, Branches, Open PRs, Merged PRs
- Add fade-in/out masks at edges
- Smooth brake/accelerate on hover
- Popover visibility bounded to visible area
- Add cursor-pointer to Refresh button
- Performance: memoized callbacks, stable props
- Add .env.local to .gitignore
2026-02-04 21:23:15 +01:00

71 lines
2.4 KiB
TypeScript

"use client";
import type { IconName } from "./Icon";
import Icon from "./Icon";
interface StatCardProps {
/** The stat label shown next to the icon. */
label: string;
/** The numeric value to display. */
value: number;
/** SF Symbol icon name displayed next to the label. */
icon: IconName;
/** Whether data is still loading (shows skeleton). */
loading?: boolean;
/** Optional click handler — makes the card interactive. */
onClick?: () => void;
/** Whether this card is currently in active/expanded state. */
active?: boolean;
/** Optional ID for targeting (e.g., for arrow positioning). */
id?: string;
}
/**
* A single metric card with icon + label on top and the number on the right.
*
* When `onClick` is provided, renders as a `<button>` with native keyboard
* and focus support. Otherwise renders as a static `<div>`.
*/
export default function StatCard({ label, value, icon, loading = false, onClick, active = false, id }: StatCardProps) {
const interactive = !!onClick;
const baseClasses = "flex w-full items-center justify-between rounded-xl border p-5 backdrop-blur-xl transition-all duration-300";
const stateClasses = active
? "border-accent/50 bg-accent/10"
: "border-border bg-frosted-glass hover:border-accent/30";
const interactiveClasses = interactive
? "cursor-pointer hover:bg-accent/5 hover:scale-[1.02] active:scale-[0.98]"
: "";
const className = `${baseClasses} ${stateClasses} ${interactiveClasses}`;
const content = loading ? (
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-md bg-accent/10 animate-skeleton" />
<div className="h-5 w-16 rounded-md bg-accent/10 animate-skeleton" />
</div>
<div className="h-8 w-14 rounded-md bg-accent/10 animate-skeleton" />
</div>
) : (
<>
<p className="flex items-center gap-2 text-lg text-muted">
<Icon name={icon} size={22} className="text-accent" />
{label}
</p>
<p className="text-3xl font-bold text-foreground text-glow tabular-nums">
{value.toLocaleString()}
</p>
</>
);
if (interactive) {
return (
<button type="button" id={id} onClick={onClick} className={className}>
{content}
</button>
);
}
return <div id={id} className={className}>{content}</div>;
}