feat(motion): interactive motion kit — SpikeBurst, Reveal, moved primitives (ADR 0013)#211
feat(motion): interactive motion kit — SpikeBurst, Reveal, moved primitives (ADR 0013)#211kilbot wants to merge 3 commits into
Conversation
Adds src/components/motion/ as the home for the site motion language: - three@0.185.1 + @react-three/fiber@9.6.1 (React 19 compatible; drei not needed) added as deps. - AmbientGradient and DotOrbit moved into the kit; ui/ambient-gradient.tsx and scroll-story/acts/dot-orbit.tsx become re-export shims so existing imports keep working. DotOrbit generalized with dots/size/ringRadius/ palette props (defaults reproduce the homepage ring exactly). - New Reveal primitive: motion whileInView fade+lift, <500ms, transform/ opacity only, plain visible div under reduced motion. - New flagship SpikeBurst: pointer-reactive 3D spike sphere (R3F), lazy chunk mounted via IntersectionObserver, frameloop paused off-screen and on hidden tabs, static pose under reduced motion, DPR capped 1.5, CSS fallback on WebGL failure. Calm-blue palette with sparse pink/yellow accents (no red/orange washes). - Prototype /motion-kit route (noindexed, not in nav) demoing all four pieces for evaluation. Tests: motion-kit.test.tsx unit coverage for the kit exports; Playwright e2e/motion-kit.spec.ts smoke + screenshot. Full suite 961 unit pass, lint clean, next build green, e2e 4 pass on a free port.
There was a problem hiding this comment.
kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
📝 WalkthroughWalkthroughAdds a reusable motion kit with ChangesMotion Kit
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
🚀 Preview: https://wcpos-g9bq8s9tj-wcpos.vercel.app |
Owner evaluation 2026-07-02 (live demo on /motion-kit): the 3D spike ball is 'cool, but we don't really need it' — not worth carrying three + @react-three/fiber for a site with no other 3D need. The kit keeps the gradient (AmbientGradient), the dots (DotOrbit — owner likes the dense blue-led variant: dots=190 ringRadius=120 size=340) and Reveal. - delete spike-burst.tsx, spike-burst-scene.tsx, their tests and the three/@react-three/fiber/@types/three dependencies - delete the /motion-kit demo route + e2e spec (evaluation done) - renumber ADR 0013-site-motion-language → 0014 (0013 is taken by host-keyed-store-environments on main) and record the no-3D decision in the kit-direction section
There was a problem hiding this comment.
kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Slimmed per owner evaluation (2026-07-02): SpikeBurst and the Also in Validation after removal: 960/960 unit tests, eslint clean, tsc clean (net −857 lines). Ready to merge. |
|
🚀 Preview: https://wcpos-4we3z1fel-wcpos.vercel.app |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/components/motion/motion-kit.test.tsx (1)
108-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding reduced-motion coverage for
DotOrbit/AmbientGradient.Only
Reveal's reduced-motion branch is unit-tested;DotOrbit's static-ring render andAmbientGradient's single-frame render underprefers-reduced-motionaren't covered here (the existingstubObservers({ reducedMotion })helper already supports this).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/motion/motion-kit.test.tsx` around lines 108 - 142, Add reduced-motion test coverage for the motion components in motion-kit.test.tsx: extend the existing DotOrbit and AmbientGradient suites to exercise the `prefers-reduced-motion` path using `stubObservers({ reducedMotion })`. Verify `DotOrbit` renders its static ring state instead of the animated canvas behavior, and verify `AmbientGradient` uses its single-frame reduced-motion render; use the existing `DotOrbit` and `AmbientGradient` component identifiers to locate the right test blocks.src/components/motion/ambient-gradient.tsx (1)
121-130: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
colorseffect dependency isn't reference-stable, unlikeDotOrbit'spaletteKey.
DotOrbitdeliberately uses a joined string key (palette.join('|')) as the effect dependency so a new-but-equal array literal doesn't retrigger setup (dot-orbit.tsx:82,189).AmbientGradientusescolorsdirectly (line 270), so any caller passing an inlinecolorsarray will tear down and rebuild the whole WebGL pipeline (shaders, buffers, context) on every re-render.♻️ Proposed fix
const canvasRef = React.useRef<HTMLCanvasElement>(null) const [fallback, setFallback] = React.useState(false) + // stable key so a new array literal with the same colors does not tear + // down and recreate the WebGL context on every render + const colorsKey = colors.join('|') React.useEffect(() => { ... - }, [colors]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [colorsKey])Also applies to: 270-270
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/motion/ambient-gradient.tsx` around lines 121 - 130, AmbientGradient’s effect is depending directly on the colors array, so inline array literals will retrigger the WebGL setup on every render. Update AmbientGradient to derive a stable dependency key from colors, similar to DotOrbit’s paletteKey approach, and use that key in the effect that initializes the shaders/buffers/context instead of the raw array reference. Keep the effect behavior otherwise unchanged, but ensure setup/cleanup only runs when the actual color values change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/adr/0014-site-motion-language.md`:
- Line 30: The ADR currently points to the shim instead of the real ambient
gradient implementation, so update the reference in the ADR to point at
src/components/motion/ambient-gradient.tsx rather than
src/components/ui/ambient-gradient.tsx. Keep the wording aligned with the actual
shader location and ensure any mention of the ambient gradient implementation
refers to the real component, not the re-export shim.
In `@src/components/motion/ambient-gradient.tsx`:
- Around line 241-247: The reduced-motion branch in ambient-gradient.tsx
disconnects the ResizeObserver immediately, which prevents the canvas from
updating on later size changes. Keep the observer active in the reducedMotion
path and make sure the same resize handling used by the effect continues to
update the canvas resolution after mount; only clean it up in the returned
teardown alongside the webglcontextlost listener.
---
Nitpick comments:
In `@src/components/motion/ambient-gradient.tsx`:
- Around line 121-130: AmbientGradient’s effect is depending directly on the
colors array, so inline array literals will retrigger the WebGL setup on every
render. Update AmbientGradient to derive a stable dependency key from colors,
similar to DotOrbit’s paletteKey approach, and use that key in the effect that
initializes the shaders/buffers/context instead of the raw array reference. Keep
the effect behavior otherwise unchanged, but ensure setup/cleanup only runs when
the actual color values change.
In `@src/components/motion/motion-kit.test.tsx`:
- Around line 108-142: Add reduced-motion test coverage for the motion
components in motion-kit.test.tsx: extend the existing DotOrbit and
AmbientGradient suites to exercise the `prefers-reduced-motion` path using
`stubObservers({ reducedMotion })`. Verify `DotOrbit` renders its static ring
state instead of the animated canvas behavior, and verify `AmbientGradient` uses
its single-frame reduced-motion render; use the existing `DotOrbit` and
`AmbientGradient` component identifiers to locate the right test blocks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3956e1d0-41db-4882-b4c3-8aca55fe82f8
📒 Files selected for processing (8)
docs/adr/0014-site-motion-language.mdsrc/components/home/scroll-story/acts/dot-orbit.tsxsrc/components/motion/ambient-gradient.tsxsrc/components/motion/dot-orbit.tsxsrc/components/motion/index.tssrc/components/motion/motion-kit.test.tsxsrc/components/motion/reveal.tsxsrc/components/ui/ambient-gradient.tsx
There was a problem hiding this comment.
kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Review feedback triage after 7ef0378:
Skipped threads: none. Unresolved inline review threads after resolution: 0. Validation:
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/components/motion/motion-kit.test.tsx (2)
211-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo coverage for the
webglcontextlostfallback path.
AmbientGradientregisters awebglcontextlostlistener that stops the animation and setsfallbackstate (seeonContextLostinambient-gradient.tsx), but no test dispatches that event to verify the fallback UI appears after context loss.🧪 Example test to add
it('falls back after webglcontextlost', () => { stubObservers() stubAnimationFrame() stubWebglContext() render(<AmbientGradient />) const canvas = screen.getByTestId('ambient-gradient') const event = new Event('webglcontextlost', { cancelable: true }) canvas.dispatchEvent(event) expect(screen.getByTestId('ambient-gradient-fallback')).toBeInTheDocument() })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/motion/motion-kit.test.tsx` around lines 211 - 256, The AmbientGradient tests are missing coverage for the webglcontextlost recovery path. Add a test in motion-kit.test.tsx that renders AmbientGradient, dispatches a webglcontextlost event on the canvas, and verifies the fallback UI is shown; use the existing AmbientGradient, onContextLost behavior, and ambient-gradient-fallback test id to locate the relevant path. Make sure the test also uses the current WebGL/animation stubs so it exercises the listener registered by AmbientGradient rather than the initial WebGL-unavailable fallback.
51-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnimation-start path is never exercised.
IntersectionObserverStubrecords.observe()calls but never invokes the stored callback, so theisIntersecting/start()branch inAmbientGradientandDotOrbit(which triggersrequestAnimationFrame) is never tested. Current tests only assert RAF is not called under reduced motion — none confirm it is called once the element intersects under normal motion.Consider extending
IntersectionObserverStubto optionally invoke its callback (e.g. return atrigger(isIntersecting)helper) so a positive animation-start assertion can be added.Also applies to: 224-239
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/motion/motion-kit.test.tsx` around lines 51 - 64, The current IntersectionObserverStub only captures observe calls and never exercises the intersection callback, so the animation-start path in AmbientGradient and DotOrbit is untested. Extend the stub setup in the motion-kit tests to expose a helper or trigger method that can invoke the stored IntersectionObserver callback with an intersecting entry, then use it in a positive test to assert requestAnimationFrame is called when the element starts intersecting under normal motion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/components/motion/motion-kit.test.tsx`:
- Around line 211-256: The AmbientGradient tests are missing coverage for the
webglcontextlost recovery path. Add a test in motion-kit.test.tsx that renders
AmbientGradient, dispatches a webglcontextlost event on the canvas, and verifies
the fallback UI is shown; use the existing AmbientGradient, onContextLost
behavior, and ambient-gradient-fallback test id to locate the relevant path.
Make sure the test also uses the current WebGL/animation stubs so it exercises
the listener registered by AmbientGradient rather than the initial
WebGL-unavailable fallback.
- Around line 51-64: The current IntersectionObserverStub only captures observe
calls and never exercises the intersection callback, so the animation-start path
in AmbientGradient and DotOrbit is untested. Extend the stub setup in the
motion-kit tests to expose a helper or trigger method that can invoke the stored
IntersectionObserver callback with an intersecting entry, then use it in a
positive test to assert requestAnimationFrame is called when the element starts
intersecting under normal motion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d2902462-b858-43fa-a01d-440fe4eac71f
📒 Files selected for processing (3)
docs/adr/0014-site-motion-language.mdsrc/components/motion/ambient-gradient.tsxsrc/components/motion/motion-kit.test.tsx
✅ Files skipped from review due to trivial changes (1)
- docs/adr/0014-site-motion-language.md
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/motion/ambient-gradient.tsx
|
🚀 Preview: https://wcpos-jxx5f7y17-wcpos.vercel.app |
What
Scaffolds the interactive motion kit per ADR 0013 → "Interactive kit direction": standardise on
motion+three/R3F, gather the pieces undersrc/components/motion/, and add a flagship 3D centerpiece. Demoed on a throwaway/motion-kitroute for evaluation — not yet wired into any production page.Changes
Dependencies (verified latest, React 19 compatible)
three@0.185.1,@react-three/fiber@9.6.1.@react-three/dreiwas not needed — the scene is hand-built, so we avoid the extra weight.src/components/motion/(new home for the kit)ambient-gradient.tsx— moved fromui/;ui/ambient-gradient.tsxis now a re-export shim (the scroll-story branch imports it from there).dot-orbit.tsx— moved fromhome/scroll-story/acts/; that path is now a shim. Generalized withdots/size/ringRadius/paletteprops; defaults reproduce the homepage act-4 ring exactly.reveal.tsx— new scroll-into-view primitive:motionwhileInViewfade+lift, 450ms, transform/opacity only,delayfor small staggers. Underprefers-reduced-motionit renders a plain, fully-visible div.spike-burst.tsx+spike-burst-scene.tsx— new flagship. A sphere of ~300 lines radiating from center, dot-tipped, orienting toward the pointer over a slow idle spin (Stripe-homepage energy). Thethree/R3F scene is a separate lazy chunk (next/dynamic,ssr:false) that only mounts once anIntersectionObserversays it's near the viewport;framelooppauses off-screen and on hidden tabs; static pose under reduced motion; DPR capped at 1.5; CSS-gradient fallback behind an error boundary if WebGL fails.index.ts— barrel export.Palette — calm blues (
#8ad2ff/#5b8def/#2b6cb0) with sparse pink#ff9ec8/ yellow#ffd76aaccents. No large red/orange washes (owner backdrop rule).Route —
/motion-kit(noindexed, English-only, not in nav). Delete or fold into real pages once the kit is adopted.Validation
vitest run— 961 unit tests pass (incl. newmotion-kit.test.tsx).eslint— clean on all new/touched files.next build— green;/[locale]/motion-kitprerenders.e2e/motion-kit.spec.ts— 4 pass on a free port (3211, not 3000), incl. a full-page screenshot attachment.Heads-up for reviewer
docs/adr/0013-host-keyed-store-environments.mdalready exists onmain; the motion ADR is also0013(carried verbatim from the owner'sscroll-story-shader-polishbranch,fa7f3a1). Two ADRs share number 0013 — owner to reconcile numbering.AmbientGradientshader originate on the unmergedscroll-story-shader-polishbranch; this PR includes them so the kit is self-contained on top ofmain. If that branch merges first, expect an overlap ondocs/adr/0013-*andambient-gradient.tsxto resolve.Summary by CodeRabbit
Revealscroll animation that fades and lifts content into view.