feat(ui): visual polish primitives + token additions (Phase A)
Adds the design tokens the polish PRs (10a-e) will draw from:
shadow-xs/sm/md/lg/glow, radius scale tuned to spec, gradient utilities,
spring/smooth eases, and fast/base/slow durations. Introduces
StatusPill, KPITile, and EmptyState primitives plus a polished
PageHeader variant ('gradient') with optional eyebrow + KPI sub-line —
existing PageHeader callers stay on the plain variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
src/components/ui/kpi-tile.tsx
Normal file
70
src/components/ui/kpi-tile.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface KPITileProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title: string;
|
||||
value: React.ReactNode;
|
||||
/** Signed delta vs. prior period; positive = green, negative = red, undefined = no chip. */
|
||||
delta?: number;
|
||||
/** Pre-rendered sparkline (recharts) — caller decides shape. */
|
||||
sparkline?: React.ReactNode;
|
||||
/** Optional accent stripe colour token; defaults to brand. */
|
||||
accent?: 'brand' | 'success' | 'warning' | 'mint' | 'teal' | 'purple';
|
||||
}
|
||||
|
||||
const ACCENT_STRIPES: Record<NonNullable<KPITileProps['accent']>, string> = {
|
||||
brand: 'bg-gradient-brand',
|
||||
success: 'bg-success',
|
||||
warning: 'bg-warning',
|
||||
mint: 'bg-mint',
|
||||
teal: 'bg-teal',
|
||||
purple: 'bg-purple',
|
||||
};
|
||||
|
||||
export function KPITile({
|
||||
title,
|
||||
value,
|
||||
delta,
|
||||
sparkline,
|
||||
accent = 'brand',
|
||||
className,
|
||||
...props
|
||||
}: KPITileProps) {
|
||||
const deltaClass =
|
||||
typeof delta === 'number'
|
||||
? delta > 0
|
||||
? 'text-success'
|
||||
: delta < 0
|
||||
? 'text-error'
|
||||
: 'text-muted-foreground'
|
||||
: '';
|
||||
const deltaPrefix = typeof delta === 'number' ? (delta > 0 ? '+' : '') : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative overflow-hidden rounded-xl border border-slate-200 bg-gradient-brand-soft p-5 shadow-sm transition-all duration-base ease-smooth hover:shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('absolute inset-x-0 top-0 h-1', ACCENT_STRIPES[accent])} aria-hidden />
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{title}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-semibold tabular-nums text-foreground">{value}</div>
|
||||
{typeof delta === 'number' ? (
|
||||
<div className={cn('mt-1 text-xs font-medium', deltaClass)}>
|
||||
{deltaPrefix}
|
||||
{delta}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{sparkline ? <div className="h-12 w-24 shrink-0 opacity-80">{sparkline}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user