Three new primitives that unlock the §3.6 'visionOS-inspired surfaces'
column of v3.1 — and the MAG.1 customer-magnet hero in §9.1.
──────────────────────────────────────────────────────────────────
<Spotlight> · cursor-tracking radial gradient
──────────────────────────────────────────────────────────────────
packages/motion/src/Spotlight.tsx (new)
- Tracks pointer via two CSS custom props (--bl-spot-x/y); zero
React re-renders on move
- color-mix(in srgb, var(--bl-accent) 22%, transparent) default
- prefers-reduced-motion → renders static centred gradient
- data-testid + data-reduced for Playwright + visual review
──────────────────────────────────────────────────────────────────
<Magnetic> · Arc-browser-style pointer attraction wrapper
──────────────────────────────────────────────────────────────────
packages/motion/src/Magnetic.tsx (new)
- Field radius (default 120 px) — child translates toward cursor
only inside that radius, with strength clamp
- window-level pointermove listener so the field works even
before hover
- SPRINGS.snappy transition on release (280 ms)
- reduced-motion → transition: none, no translation
──────────────────────────────────────────────────────────────────
<MeshBackground> · ambient OKLCH gradient
──────────────────────────────────────────────────────────────────
packages/motion/src/MeshBackground.tsx (new)
- 4-stop color-mix() palette (token-driven, sRGB fallback)
- Three mood tiers: calm (24s) · focus (16s) · celebrate (10s)
- Pure CSS keyframes (translate3d + scale) emitted inline
- reduced-motion → renders static blobs (no <style>)
- isolation: isolate so children get their own stacking context
──────────────────────────────────────────────────────────────────
Quality gates
──────────────────────────────────────────────────────────────────
✓ pnpm -F @bytelyst/motion test → 23/23 passing (was 16/16)
+7 new cases: Spotlight (3) · Magnetic (2) · MeshBackground (2)
✓ pnpm -F @bytelyst/motion typecheck → clean
✓ Exports wired in src/index.ts; doc-block bumped to mention
Wave 13.D additions; package.json 0.1.0 → 0.2.0
✓ Description string lists all 8 primitives
──────────────────────────────────────────────────────────────────
Roadmap tracker — 5 boxes flipped (§11)
──────────────────────────────────────────────────────────────────
13.D.2 Spotlight shipped
13.D.3 Magnetic shipped
13.D.4 MeshBackground shipped
13.D.6 spatial-hero showcase (lands in paired showcase commit)
MAG.1 the customer-magnet hero ✨
13.D.1 (Parallax) + 13.D.5 (TiltGallery) explicitly deferred to
motion@0.3.x with notes in the tracker.
Wave 13 Futurism: 0/39 → 5/39 (13%) · TOTAL 14/202 → 19/202 (9%)
Vendored snapshot + showcase /futurism routes land in the paired
showcase commit.
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import type { CSSProperties, ReactNode } from 'react';
|
|
import { prefersReducedMotion } from './utils.js';
|
|
|
|
export interface MeshBackgroundProps {
|
|
/** Optional content rendered on top of the mesh. */
|
|
children?: ReactNode;
|
|
/**
|
|
* Mood tier — controls the overall saturation + animation cadence.
|
|
* - `calm` — slow drift, low saturation. Default.
|
|
* - `focus` — slightly faster + a touch more saturation.
|
|
* - `celebrate` — vibrant, faster oscillation. Use sparingly.
|
|
*/
|
|
mood?: 'calm' | 'focus' | 'celebrate';
|
|
/** Override the gradient stop colour palette (CSS colours). */
|
|
colors?: [string, string, string, string];
|
|
/** Bypass animation (static mesh) — respects prefers-reduced-motion. */
|
|
disableMotion?: boolean;
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
/**
|
|
* `<MeshBackground>` — ambient OKLCH-flavoured gradient wash with two
|
|
* blurred conic "blobs" drifting slowly. Pure CSS, zero JS once mounted.
|
|
*
|
|
* Designed as a background layer behind landing-page heroes. Subtle by
|
|
* default (calm mood); never use for content surfaces (kills contrast).
|
|
*
|
|
* Honours `prefers-reduced-motion`: the blobs render static when the
|
|
* user opts out.
|
|
*/
|
|
export function MeshBackground({
|
|
children,
|
|
mood = 'calm',
|
|
colors,
|
|
disableMotion,
|
|
className,
|
|
style,
|
|
}: MeshBackgroundProps) {
|
|
const reduced = disableMotion ?? prefersReducedMotion();
|
|
const palette =
|
|
colors ??
|
|
([
|
|
// Default palette uses tokens with sensible sRGB fallbacks.
|
|
'color-mix(in srgb, var(--bl-accent, #6366f1) 60%, transparent)',
|
|
'color-mix(in srgb, var(--bl-success, #34d399) 50%, transparent)',
|
|
'color-mix(in srgb, var(--bl-warning, #fbbf24) 50%, transparent)',
|
|
'color-mix(in srgb, var(--bl-danger, #f472b6) 50%, transparent)',
|
|
] as const);
|
|
|
|
const speed: Record<NonNullable<MeshBackgroundProps['mood']>, string> = {
|
|
calm: '24s',
|
|
focus: '16s',
|
|
celebrate: '10s',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
data-testid="bl-mesh-background"
|
|
data-mood={mood}
|
|
data-reduced={reduced ? 'true' : 'false'}
|
|
className={className}
|
|
style={{
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
isolation: 'isolate',
|
|
...style,
|
|
}}
|
|
>
|
|
{/* Two blurred blobs drifting on independent paths. */}
|
|
<div
|
|
aria-hidden="true"
|
|
style={{
|
|
position: 'absolute',
|
|
inset: '-20%',
|
|
backgroundImage: [
|
|
`radial-gradient(40% 35% at 20% 25%, ${palette[0]}, transparent 70%)`,
|
|
`radial-gradient(35% 30% at 80% 30%, ${palette[1]}, transparent 70%)`,
|
|
`radial-gradient(45% 40% at 30% 80%, ${palette[2]}, transparent 70%)`,
|
|
`radial-gradient(40% 35% at 75% 75%, ${palette[3]}, transparent 70%)`,
|
|
].join(', '),
|
|
filter: 'blur(48px)',
|
|
opacity: 0.85,
|
|
animation: reduced ? undefined : `bl-mesh-drift ${speed[mood]} ease-in-out infinite alternate`,
|
|
zIndex: -1,
|
|
}}
|
|
/>
|
|
{/* Keyframes are global because we don't want a styled-components
|
|
dependency. Authoring them via a <style> tag keeps the package
|
|
framework-agnostic. */}
|
|
{!reduced && (
|
|
<style>{`@keyframes bl-mesh-drift {
|
|
0% { transform: translate3d(0, 0, 0) scale(1); }
|
|
50% { transform: translate3d(2%, -2%, 0) scale(1.05); }
|
|
100% { transform: translate3d(-2%, 2%, 0) scale(0.98); }
|
|
}`}</style>
|
|
)}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|