──────────────────────────────────────────────────────────────────
motion@0.2.1 — Parallax + TiltGallery
──────────────────────────────────────────────────────────────────
+ Parallax.tsx
- scroll-driven translate3d via rAF + window.scroll
- speed multiplier + axis (y / x) + reduced-motion bypass
- listener cleanup + cancelAnimationFrame on unmount
- WAVE 13.D.1
+ TiltGallery.tsx
- horizontally-scrolling rail of <TiltCard>-style tiles
- per-tile cursor-tracking rotateX/Y + glare gradient
- role=region + arrow-key scrolling (←/→) + scroll-snap
- reduced-motion strips tilt + glare, keeps the rail
- WAVE 13.D.5
+ 5 new tests (Parallax x 2, TiltGallery x 3) — 28/28 passing
+ index.ts: exports both + types
+ package.json: 0.2.0 → 0.2.1
──────────────────────────────────────────────────────────────────
ai-ui@0.6.0 — Wave 9.E composition surfaces
──────────────────────────────────────────────────────────────────
+ Markdown.tsx
- dep-free subset renderer: h1-h3 / **bold** / *italic* /
`code` / fenced code / ul + ol / [text](url)
- inline `[cite:<id>]` chips resolved from a citations
registry (missing ids render as [?] — failure mode is loud)
- WAVE 9.E.1
+ CodeDiff.tsx
- line-LCS diff in <100 LOC, zero deps
- split (2-col) and unified views; tinted add/del rows
- WAVE 9.E.2
+ ExplainThis.tsx
- listens for selectionchange, pops 'Explain' CTA over the
selection rect when inside the wrapper + ≥ minLength chars
- fires onExplain({ text, rect }) so hosts can open a richer
side panel if preferred
- WAVE 9.E.3
+ usePromptHistory.ts
- bash-style ↑/↓ recall with localStorage persistence
(storage key configurable; null = in-memory for tests/SSR)
- dedupes consecutive duplicates + trims to capacity
- WAVE 9.E.4
+ useTokenCount.ts
- cheap estimator (default ~4 chars/token; configurable for
code/CJK) + optional USD cost
- memoised — stable across re-renders
- WAVE 9.E.5
+ 19 new tests in src/__tests__/composition.test.tsx — 98/98 passing
+ index.ts: '0.6 surfaces' section exports all 5 + types
+ package.json: 0.5.0 → 0.6.0
Showcase routes + roadmap flips land in the paired showcase commit.
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import {
|
||
useEffect,
|
||
useRef,
|
||
type CSSProperties,
|
||
type ReactNode,
|
||
} from 'react';
|
||
import { prefersReducedMotion } from './utils.js';
|
||
|
||
export interface ParallaxProps {
|
||
/** Slot moved on scroll. */
|
||
children: ReactNode;
|
||
/**
|
||
* Translation speed multiplier. 0 = stationary, 1 = scrolls with the
|
||
* page (default), 0.5 = scrolls half-speed, -0.5 = scrolls upward.
|
||
* Sensible range: −1 to 1.
|
||
*/
|
||
speed?: number;
|
||
/** Translation axis. Default `'y'`. */
|
||
axis?: 'y' | 'x';
|
||
/** Bypass entirely — useful for tests / reduced-motion. */
|
||
disableMotion?: boolean;
|
||
className?: string;
|
||
style?: CSSProperties;
|
||
}
|
||
|
||
/**
|
||
* `<Parallax>` — scroll-driven translation wrapper.
|
||
*
|
||
* Uses `transform: translate3d(…)` updated inside a `requestAnimationFrame`
|
||
* loop driven by `window.scroll`. The transform is computed relative to
|
||
* the element's vertical centre crossing the viewport's centre — so the
|
||
* effect peaks while the element is on-screen and never accumulates
|
||
* unbounded translation.
|
||
*
|
||
* Honours `prefers-reduced-motion`: with `disableMotion` (or the OS
|
||
* setting) the wrapper renders un-transformed.
|
||
*
|
||
* Wave 13.D.1. Zero deps, ~1.4 KB gzipped. Pairs with `<TiltCard>`
|
||
* and `<TiltGallery>` for the spatial-hero pattern.
|
||
*/
|
||
export function Parallax({
|
||
children,
|
||
speed = 0.3,
|
||
axis = 'y',
|
||
disableMotion,
|
||
className,
|
||
style,
|
||
}: ParallaxProps) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
const reduced = disableMotion ?? prefersReducedMotion();
|
||
|
||
useEffect(() => {
|
||
if (reduced) return;
|
||
const el = ref.current;
|
||
if (!el) return;
|
||
let raf = 0;
|
||
const apply = () => {
|
||
raf = 0;
|
||
const rect = el.getBoundingClientRect();
|
||
const vh = window.innerHeight || 1;
|
||
// Distance, in px, of the element's centre from viewport centre.
|
||
const delta = rect.top + rect.height / 2 - vh / 2;
|
||
const t = -delta * speed;
|
||
el.style.transform =
|
||
axis === 'y'
|
||
? `translate3d(0, ${t.toFixed(2)}px, 0)`
|
||
: `translate3d(${t.toFixed(2)}px, 0, 0)`;
|
||
};
|
||
const onScroll = () => {
|
||
if (raf) return;
|
||
raf = window.requestAnimationFrame(apply);
|
||
};
|
||
apply();
|
||
window.addEventListener('scroll', onScroll, { passive: true });
|
||
window.addEventListener('resize', onScroll);
|
||
return () => {
|
||
window.removeEventListener('scroll', onScroll);
|
||
window.removeEventListener('resize', onScroll);
|
||
if (raf) window.cancelAnimationFrame(raf);
|
||
el.style.transform = '';
|
||
};
|
||
}, [reduced, speed, axis]);
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
data-testid="bl-parallax"
|
||
data-reduced={reduced ? 'true' : 'false'}
|
||
data-axis={axis}
|
||
className={className}
|
||
style={{ willChange: reduced ? undefined : 'transform', ...style }}
|
||
>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|