learning_ai_common_plat/packages/motion/src/Parallax.tsx
saravanakumardb1 87e3bc490a feat: Wave 9.E (ai-ui@0.6.0) + Wave 13.D.1/.5 (motion@0.2.1)
──────────────────────────────────────────────────────────────────
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.
2026-05-27 16:59:28 -07:00

97 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}