Harvested from delta. StatCard integrates AnimatedNumber + intent colors; EmptyState and Spinner generalize delta's local versions for reuse.
This commit is contained in:
@@ -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.
|
5. Don't nest cards inside cards. Don't put gray text on colored fills.
|
||||||
|
|
||||||
## Included
|
## 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.
|
Motion: FadeIn, Stagger/StaggerItem.
|
||||||
Animated: AnimatedNumber, SpotlightCard.
|
Animated: AnimatedNumber, SpotlightCard.
|
||||||
Blocks: AppShell, CommandPalette, Hero, FeatureGrid, Pricing.
|
Blocks: AppShell, CommandPalette, Hero, FeatureGrid, Pricing.
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@jason/ui-kit",
|
"name": "@jason/ui-kit",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"description": "Shared design foundation — branded tokens, Tailwind preset, shadcn-style components, motion, app shell + marketing blocks. Guarded by Impeccable.",
|
"description": "Shared design foundation \u2014 branded tokens, Tailwind preset, shadcn-style components, motion, app shell + marketing blocks. Guarded by Impeccable.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Jason <jason@messagepoint.tv>",
|
"author": "Jason <jason@messagepoint.tv>",
|
||||||
|
|||||||
Executable
+31
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+21
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+52
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ export {
|
|||||||
} from "./components/dropdown-menu";
|
} from "./components/dropdown-menu";
|
||||||
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./components/table";
|
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./components/table";
|
||||||
export { Toaster, toast } from "./components/toast";
|
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
|
// Motion
|
||||||
export { FadeIn, Stagger, StaggerItem, staggerContainer, staggerItem, motion } from "./motion";
|
export { FadeIn, Stagger, StaggerItem, staggerContainer, staggerItem, motion } from "./motion";
|
||||||
|
|||||||
Reference in New Issue
Block a user