learning_ai_common_plat/packages/data-viz/src/KpiCard.tsx
saravanakumardb1 d082480849 feat(packages): Wave 4 motion + Wave 5b data-viz + Wave 7 notifications-ui
Three new product-agnostic packages unlock visible elegance lifts
across every product:

═══════════════════════════════════════════════════════════════════════
@bytelyst/motion@0.1.0 — Wave 4 elegance primitives  (2.21 KB / 8 KB)
═══════════════════════════════════════════════════════════════════════

  <Reveal>          — IntersectionObserver-based fade/slide entry,
                      6 directions, configurable spring + delay
  <StaggerList>     — sequenced reveal of children with per-item delay
  <NumberFlow>      — RAF-tweened number counter, cubic-out easing,
                      Intl-formatted, prefers-reduced-motion aware
  <TiltCard>        — 3D perspective tilt + cursor-tracking glare
                      overlay (single-element ref, no React rerenders)
  <ScrollProgress>  — fixed scroll-position bar (window or any element)

Plus exported `SPRINGS` (4 cubic-bezier presets) + `prefersReducedMotion`
helper. Every primitive accepts `disableMotion` for snapshot tests.

═══════════════════════════════════════════════════════════════════════
@bytelyst/data-viz@0.1.0 — Wave 5b viz primitives  (2.63 KB / 10 KB)
═══════════════════════════════════════════════════════════════════════

  <Sparkline>     — line trend with gradient fill + last-point marker
  <BarSparkline>  — discrete-bar mini-chart with max-bar highlight
  <KpiCard>       — label + headline + delta arrow + sparkline; supports
                    'goodWhen=lower' for latency/cost metrics
  <ProgressRing>  — circular progress with center content slot,
                    animated stroke-dashoffset
  <Heatmap>       — GitHub-style calendar grid with color-mix() intensity

All pure SVG / CSS — zero runtime dependencies.

═══════════════════════════════════════════════════════════════════════
@bytelyst/notifications-ui@0.1.0 — Wave 7 essentials (3.31 KB / 10 KB)
═══════════════════════════════════════════════════════════════════════

  <NotificationCenter>  — bell trigger + badge + dropdown panel with
                          All / Unread / Mentions tabs, outside-click
                          + Escape close, mark-all-read action
  <InboxItem>           — single row with unread dot, kind glyph,
                          relative timestamp, optional action buttons
  <BannerStack>         — top-of-page strip with maxVisible + +N more,
                          accent-bordered tone variants, dismissible
  <Announcement>        — inline 'What's new' pill (3 tone variants)

5 notification kinds (info/success/warning/danger/mention) + 5 banner
kinds (... + announcement gradient).

═══════════════════════════════════════════════════════════════════════
Quality gates
═══════════════════════════════════════════════════════════════════════
  All three packages: tsc --noEmit clean, build clean.
  Tests:    motion 16/16  ·  data-viz 14/14  ·  notifications-ui 17/17
  Bundles:  motion 2.21 KB  ·  data-viz 2.63 KB  ·  noti-ui 3.31 KB
  Budgets:  added to .size-limit.cjs (8/10/10 KB respectively)

Refs:
  learning_ai_uxui_web/docs/ROADMAP_2026.md
    §Wave 4 (Motion), §Wave 5b (Charts), §Wave 7 (Productisation)
  Decisions doc §13 (mobile-native = tokens-only) leaves room for these
    web-first packages to be the canonical surface
2026-05-27 13:08:30 -07:00

170 lines
4.7 KiB
TypeScript

import type { CSSProperties, ReactNode } from 'react';
import { Sparkline } from './Sparkline.js';
export interface KpiCardProps {
/** Metric label (e.g. 'Active users'). */
label: string;
/** Pre-formatted display value (e.g. '12.4k', '$8,421'). */
value: ReactNode;
/** Delta percentage (e.g. 6.6 for +6.6%). */
deltaPercent?: number;
/** Override the delta label (e.g. 'vs last 7d'). */
deltaLabel?: string;
/** Inline trend data. Optional. */
trend?: number[];
/** Icon rendered top-right. */
icon?: ReactNode;
/** Sparkline stroke override. */
trendColor?: string;
/** Hide the up/down arrow next to delta. Default false. */
hideDeltaArrow?: boolean;
/** Override the threshold below which a delta is rendered as 'good'.
* Useful for cost / latency KPIs where lower is better. Default `>=0`. */
goodWhen?: 'higher' | 'lower';
className?: string;
style?: CSSProperties;
}
/**
* `<KpiCard>` — token-themed key-performance-indicator tile.
*
* ┌────────────────────────┐
* │ Active users ✦ │
* │ 8,421 │
* │ ▲ +6.6% vs last 7d ╱╲╱ │
* └────────────────────────┘
*
* Renders the headline number prominently, an optional delta with
* color-coded arrow, and an inline `<Sparkline>` for context.
*/
export function KpiCard({
label,
value,
deltaPercent,
deltaLabel = 'vs previous',
trend,
icon,
trendColor,
hideDeltaArrow,
goodWhen = 'higher',
className,
style,
}: KpiCardProps) {
const hasDelta = typeof deltaPercent === 'number';
const up = hasDelta && deltaPercent! >= 0;
const good =
!hasDelta ||
(goodWhen === 'higher' ? up : !up && Math.abs(deltaPercent!) > 0.0001);
const deltaColor = good
? 'var(--bl-success, #22c55e)'
: 'var(--bl-danger, #ef4444)';
return (
<article
data-testid="bl-kpi-card"
className={className}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--bl-space-2, 8px)',
padding: 'var(--bl-space-4, 16px)',
border: '1px solid var(--bl-border, rgba(0,0,0,0.08))',
borderRadius: 'var(--bl-radius-card, 12px)',
background: 'var(--bl-surface-card, #fff)',
color: 'var(--bl-text-primary, inherit)',
minWidth: 180,
...style,
}}
>
<header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 'var(--bl-space-2, 8px)',
}}
>
<span
style={{
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.04em',
color: 'var(--bl-text-tertiary, #888)',
}}
>
{label}
</span>
{icon && (
<span
aria-hidden
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 28,
height: 28,
borderRadius: 'var(--bl-radius-pill, 999px)',
background: 'var(--bl-accent-muted, rgba(99,102,241,0.12))',
color: 'var(--bl-accent, #6366f1)',
}}
>
{icon}
</span>
)}
</header>
<div
data-testid="bl-kpi-value"
style={{
fontSize: '2.1rem',
fontWeight: 700,
lineHeight: 1.05,
fontVariantNumeric: 'tabular-nums',
}}
>
{value}
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--bl-space-2, 8px)',
fontSize: '0.78rem',
}}
>
{hasDelta && (
<span
data-testid="bl-kpi-delta"
data-good={good ? 'true' : 'false'}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 2,
color: deltaColor,
fontWeight: 600,
}}
>
{!hideDeltaArrow && <span aria-hidden>{up ? '▲' : '▼'}</span>}
{(up ? '+' : '') + deltaPercent!.toFixed(1)}%
</span>
)}
{hasDelta && (
<span style={{ color: 'var(--bl-text-tertiary, #888)' }}>{deltaLabel}</span>
)}
{trend && trend.length > 1 && (
<div style={{ marginLeft: 'auto' }}>
<Sparkline
data={trend}
width={88}
height={28}
stroke={trendColor ?? (good ? 'var(--bl-success, #22c55e)' : 'var(--bl-danger, #ef4444)')}
/>
</div>
)}
</div>
</article>
);
}