feat: add Spinner, EmptyState, StatCard (v0.4.0)
ci / build-and-design (push) Failing after 1s

Harvested from delta. StatCard integrates AnimatedNumber + intent colors;
EmptyState and Spinner generalize delta's local versions for reuse.
This commit is contained in:
Jason
2026-07-16 07:50:47 -05:00
parent ea311c502a
commit 27d8225369
6 changed files with 111 additions and 3 deletions
+2 -1
View File
@@ -40,7 +40,8 @@ Never Inter, Arial, or system defaults for headings (Impeccable anti-slop rule).
5. Don't nest cards inside cards. Don't put gray text on colored fills.
## Included
Primitives: Button, Card, Input, Badge, Dialog, DropdownMenu, Table, Toaster.
Primitives: Button, Card, Input, Select (themed Radix popover), Textarea, Label,
Badge, Dialog, DropdownMenu, Table, Toaster, Spinner, EmptyState, StatCard.
Motion: FadeIn, Stagger/StaggerItem.
Animated: AnimatedNumber, SpotlightCard.
Blocks: AppShell, CommandPalette, Hero, FeatureGrid, Pricing.
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@jason/ui-kit",
"version": "0.3.0",
"description": "Shared design foundation branded tokens, Tailwind preset, shadcn-style components, motion, app shell + marketing blocks. Guarded by Impeccable.",
"version": "0.4.0",
"description": "Shared design foundation \u2014 branded tokens, Tailwind preset, shadcn-style components, motion, app shell + marketing blocks. Guarded by Impeccable.",
"type": "module",
"license": "MIT",
"author": "Jason <jason@messagepoint.tv>",
+31
View File
@@ -0,0 +1,31 @@
import * as React from "react";
import { Inbox, type LucideIcon } from "lucide-react";
import { cn } from "../lib/cn";
export interface EmptyStateProps {
icon?: LucideIcon;
title: string;
hint?: string;
/** Optional call-to-action (e.g. a <Button>). */
action?: React.ReactNode;
className?: string;
}
/** Friendly placeholder for empty lists / no-data states. */
export function EmptyState({ icon: Icon = Inbox, title, hint, action, className }: EmptyStateProps) {
return (
<div
className={cn(
"flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-surface/50 px-6 py-12 text-center",
className
)}
>
<Icon className="size-7 text-muted" />
<div>
<p className="font-display font-medium text-text">{title}</p>
{hint && <p className="mt-1 text-sm text-muted">{hint}</p>}
</div>
{action}
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { Loader2 } from "lucide-react";
import { cn } from "../lib/cn";
export interface SpinnerProps {
/** Optional text shown next to the spinner. */
label?: string;
/** Icon size in px. */
size?: number;
className?: string;
}
/** Inline loading indicator. */
export function Spinner({ label, size = 18, className }: SpinnerProps) {
return (
<div className={cn("flex items-center gap-2 text-muted", className)} role="status" aria-live="polite">
<Loader2 size={size} className="animate-spin text-brand" />
{label && <span className="text-sm">{label}</span>}
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react";
import { AnimatedNumber } from "../animated/animated-number";
import { cn } from "../lib/cn";
export type StatIntent = "default" | "brand" | "success" | "warning" | "danger";
const INTENT: Record<StatIntent, string> = {
default: "text-text",
brand: "text-brand",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
};
export interface StatCardProps {
label: string;
/** Static value; ignored when `animate` is provided. */
value?: React.ReactNode;
/** Animated count-up target. */
animate?: number;
/** Formatter for the animated value (e.g. n => `${n}%`). */
format?: (n: number) => string;
sub?: React.ReactNode;
intent?: StatIntent;
icon?: React.ReactNode;
className?: string;
}
/** Dashboard metric tile. Pass `animate` for a count-up, or `value` for static. */
export function StatCard({
label,
value,
animate,
format,
sub,
intent = "default",
icon,
className,
}: StatCardProps) {
return (
<div className={cn("rounded-lg border border-border bg-surface p-4 shadow-sm", className)}>
<div className="flex items-center justify-between gap-2">
<p className="text-xs font-medium uppercase tracking-wide text-muted">{label}</p>
{icon && <span className="text-muted [&_svg]:size-4">{icon}</span>}
</div>
<p className={cn("mt-1 font-display text-2xl font-bold", INTENT[intent])}>
{animate != null ? <AnimatedNumber value={animate} format={format} /> : value}
</p>
{sub && <p className="mt-0.5 text-xs text-muted">{sub}</p>}
</div>
);
}
+3
View File
@@ -21,6 +21,9 @@ export {
} from "./components/dropdown-menu";
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./components/table";
export { Toaster, toast } from "./components/toast";
export { Spinner, type SpinnerProps } from "./components/spinner";
export { EmptyState, type EmptyStateProps } from "./components/empty-state";
export { StatCard, type StatCardProps, type StatIntent } from "./components/stat-card";
// Motion
export { FadeIn, Stagger, StaggerItem, staggerContainer, staggerItem, motion } from "./motion";