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
105 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|