Skip to content

feat(motion): interactive motion kit — SpikeBurst, Reveal, moved primitives (ADR 0013)#211

Open
kilbot wants to merge 3 commits into
mainfrom
claude/interesting-gagarin-597145
Open

feat(motion): interactive motion kit — SpikeBurst, Reveal, moved primitives (ADR 0013)#211
kilbot wants to merge 3 commits into
mainfrom
claude/interesting-gagarin-597145

Conversation

@kilbot

@kilbot kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Scaffolds the interactive motion kit per ADR 0013 → "Interactive kit direction": standardise on motion + three/R3F, gather the pieces under src/components/motion/, and add a flagship 3D centerpiece. Demoed on a throwaway /motion-kit route 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/drei was 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 from ui/; ui/ambient-gradient.tsx is now a re-export shim (the scroll-story branch imports it from there).
  • dot-orbit.tsx — moved from home/scroll-story/acts/; that path is now a shim. Generalized with dots / size / ringRadius / palette props; defaults reproduce the homepage act-4 ring exactly.
  • reveal.tsxnew scroll-into-view primitive: motion whileInView fade+lift, 450ms, transform/opacity only, delay for small staggers. Under prefers-reduced-motion it renders a plain, fully-visible div.
  • spike-burst.tsx + spike-burst-scene.tsxnew flagship. A sphere of ~300 lines radiating from center, dot-tipped, orienting toward the pointer over a slow idle spin (Stripe-homepage energy). The three/R3F scene is a separate lazy chunk (next/dynamic, ssr:false) that only mounts once an IntersectionObserver says it's near the viewport; frameloop pauses 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 #ffd76a accents. 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 run961 unit tests pass (incl. new motion-kit.test.tsx).
  • eslint — clean on all new/touched files.
  • next build — green; /[locale]/motion-kit prerenders.
  • Playwright e2e/motion-kit.spec.ts4 pass on a free port (3211, not 3000), incl. a full-page screenshot attachment.

Heads-up for reviewer

  • ADR number collision: docs/adr/0013-host-keyed-store-environments.md already exists on main; the motion ADR is also 0013 (carried verbatim from the owner's scroll-story-shader-polish branch, fa7f3a1). Two ADRs share number 0013 — owner to reconcile numbering.
  • The ADR file and the moved AmbientGradient shader originate on the unmerged scroll-story-shader-polish branch; this PR includes them so the kit is self-contained on top of main. If that branch merges first, expect an overlap on docs/adr/0013-* and ambient-gradient.tsx to resolve.

Summary by CodeRabbit

  • New Features
    • Added an ambient gradient background effect with a graceful static fallback when advanced graphics aren’t available.
    • Introduced Reveal scroll animation that fades and lifts content into view.
    • Added a configurable dot-orbit decorative effect with pointer-reactive motion.
  • Bug Fixes
    • Improved reduced-motion behavior so animations render as static content instead of looping.
    • Enhanced performance by pausing decorative motion when off-screen or when the tab is inactive.

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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a reusable motion kit with AmbientGradient, DotOrbit, and Reveal, plus a barrel export and tests. Prior homepage and UI imports now re-export from the new motion modules. ADR-0014 records the site-wide motion rules and ambient gradient direction.

Changes

Motion Kit

Layer / File(s) Summary
Motion language ADR
docs/adr/0014-site-motion-language.md
New ADR documents global motion rules, the ambient gradient primitive, and motion kit direction.
AmbientGradient WebGL component
src/components/motion/ambient-gradient.tsx
New client component renders an animated WebGL gradient with shader uniforms, DPR-capped resizing, visibility-gated animation, reduced-motion fallback, and CSS fallback on failure.
DotOrbit canvas component
src/components/motion/dot-orbit.tsx
New client component renders a configurable pointer-reactive dot ring on canvas with IO/visibility gating and reduced-motion static rendering.
Reveal scroll-into-view component
src/components/motion/reveal.tsx
New client component fades and lifts children into view with configurable delay, offset, and once behavior, falling back to a static div under reduced motion.
Barrel exports, tests, and compatibility shims
src/components/motion/index.ts, src/components/motion/motion-kit.test.tsx, src/components/home/scroll-story/acts/dot-orbit.tsx, src/components/ui/ambient-gradient.tsx
New motion-kit barrel re-exports the primitives; tests cover exports and reduced-motion/WebGL behavior; old homepage and UI-path files become re-export shims.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title references SpikeBurst and ADR 0013, but the final changeset removed SpikeBurst and renumbered the ADR to 0014. Revise the title to reflect the finalized motion kit changes, e.g. mention AmbientGradient, DotOrbit, Reveal, and ADR 0014.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/interesting-gagarin-597145

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 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

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@kilbot

kilbot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Slimmed per owner evaluation (2026-07-02): SpikeBurst and the three/@react-three/fiber/@types/three dependencies are removed — 'cool, but we don't really need it.' The kit now ships exactly what the site will use: AmbientGradient (the gradient stays), DotOrbit (generalized with props — owner's preferred accent variant is the dense blue-led one: dots={190} ringRadius={120} size={340}, recorded in the ADR), and Reveal (the scroll-into-view fade for non-homepage pages).

Also in 488c982: the /motion-kit demo route + its e2e spec are deleted (evaluation done), and the ADR is renumbered 0013 → 0014 (0013 belongs to host-keyed-store-environments on main) with the no-3D decision recorded so nobody re-proposes a 3D library for decorative motion.

Validation after removal: 960/960 unit tests, eslint clean, tsc clean (net −857 lines). Ready to merge.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-4we3z1fel-wcpos.vercel.app

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/components/motion/motion-kit.test.tsx (1)

108-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding reduced-motion coverage for DotOrbit/AmbientGradient.

Only Reveal's reduced-motion branch is unit-tested; DotOrbit's static-ring render and AmbientGradient's single-frame render under prefers-reduced-motion aren't covered here (the existing stubObservers({ 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

colors effect dependency isn't reference-stable, unlike DotOrbit's paletteKey.

DotOrbit deliberately 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). AmbientGradient uses colors directly (line 270), so any caller passing an inline colors array 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

📥 Commits

Reviewing files that changed from the base of the PR and between 507c58a and 488c982.

📒 Files selected for processing (8)
  • docs/adr/0014-site-motion-language.md
  • src/components/home/scroll-story/acts/dot-orbit.tsx
  • src/components/motion/ambient-gradient.tsx
  • src/components/motion/dot-orbit.tsx
  • src/components/motion/index.ts
  • src/components/motion/motion-kit.test.tsx
  • src/components/motion/reveal.tsx
  • src/components/ui/ambient-gradient.tsx

Comment thread docs/adr/0014-site-motion-language.md Outdated
Comment thread src/components/motion/ambient-gradient.tsx

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@wcpos-bot

wcpos-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review feedback triage after 7ef0378:

Thread File Issue Decision Commit
CodeRabbit inline 0014-site-motion-language.md ADR pointed to the UI shim instead of the real AmbientGradient implementation. Fixed; thread replied and resolved. 7ef0378
CodeRabbit inline ambient-gradient.tsx Reduced-motion canvas disconnected ResizeObserver immediately after mount. Fixed; thread replied and resolved. 7ef0378
CodeRabbit review body motion-kit.test.tsx Add reduced-motion coverage for DotOrbit and AmbientGradient. Fixed with focused unit coverage. 7ef0378
CodeRabbit review body ambient-gradient.tsx AmbientGradient effect depended on the raw colors array instead of a stable key. Fixed with a colorsKey dependency matching DotOrbit's paletteKey approach. 7ef0378

Skipped threads: none.

Unresolved inline review threads after resolution: 0.

Validation:

  • pnpm install --frozen-lockfile passed.
  • pnpm lint exited 0 with 5 existing warnings outside this change.
  • pnpm exec tsc --noEmit passed.
  • pnpm exec vitest run src/components/motion/motion-kit.test.tsx passed, 9 tests.
  • pnpm test:unit passed, 139 files / 963 tests.
  • pnpm test:e2e --project=chromium was attempted but local Chromium cannot launch in this container because libglib-2.0.so.0 is missing; every observed e2e failure had that browser-launch error.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/components/motion/motion-kit.test.tsx (2)

211-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for the webglcontextlost fallback path.

AmbientGradient registers a webglcontextlost listener that stops the animation and sets fallback state (see onContextLost in ambient-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 win

Animation-start path is never exercised.

IntersectionObserverStub records .observe() calls but never invokes the stored callback, so the isIntersecting/start() branch in AmbientGradient and DotOrbit (which triggers requestAnimationFrame) 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 IntersectionObserverStub to optionally invoke its callback (e.g. return a trigger(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

📥 Commits

Reviewing files that changed from the base of the PR and between 488c982 and 7ef0378.

📒 Files selected for processing (3)
  • docs/adr/0014-site-motion-language.md
  • src/components/motion/ambient-gradient.tsx
  • src/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

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-jxx5f7y17-wcpos.vercel.app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant