learning_ai_common_plat/packages/data-viz/src/ProgressRing.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

105 lines
2.7 KiB
TypeScript

import { useMemo, type CSSProperties, type ReactNode } from 'react';
export interface ProgressRingProps {
/** Progress 0..1 (values outside are clamped). */
value: number;
/** Outer diameter in px. Default 96. */
size?: number;
/** Ring thickness in px. Default 8. */
thickness?: number;
/** Track color. Default `var(--bl-border)`. */
track?: string;
/** Active arc color. Default `var(--bl-accent)`. */
color?: string;
/** Slot rendered in the center. */
children?: ReactNode;
/** Aria label. */
ariaLabel?: string;
className?: string;
style?: CSSProperties;
}
/**
* `<ProgressRing>` — circular progress with an inner content slot.
* Perfect for onboarding checklists, usage meters, and goal trackers.
*
* Smoothly animates `stroke-dashoffset` so the ring sweeps when `value`
* changes — no JS tween needed.
*/
export function ProgressRing({
value,
size = 96,
thickness = 8,
track = 'var(--bl-border, rgba(0,0,0,0.08))',
color = 'var(--bl-accent, #6366f1)',
children,
ariaLabel,
className,
style,
}: ProgressRingProps) {
const clamped = Math.max(0, Math.min(1, value));
const { radius, circumference, dashOffset } = useMemo(() => {
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
return {
radius: r,
circumference: c,
dashOffset: c * (1 - clamped),
};
}, [size, thickness, clamped]);
return (
<div
data-testid="bl-progress-ring"
role="img"
aria-label={ariaLabel ?? `${Math.round(clamped * 100)} percent`}
className={className}
style={{
position: 'relative',
width: size,
height: size,
display: 'inline-grid',
placeItems: 'center',
...style,
}}
>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
style={{
position: 'absolute',
inset: 0,
transform: 'rotate(-90deg)',
}}
>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={track}
strokeWidth={thickness}
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth={thickness}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
style={{
transition: 'stroke-dashoffset 600ms cubic-bezier(0.2, 0.9, 0.1, 1)',
}}
/>
</svg>
{children !== undefined && (
<div style={{ position: 'relative', textAlign: 'center' }}>{children}</div>
)}
</div>
);
}