The About section's portrait image now checks useReducedMotion() and
disables parallax scroll motion when the user has set OS-level reduced
motion preferences.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds Hero3DScene, a procedurally-built rotating braid mesh rendered via
@react-three/fiber, replacing the flat CSS-circle Hero3D component. Falls
back to a static gradient circle under prefers-reduced-motion, matching the
old component's fallback behavior.
Also adds a ResizeObserver mock to the test setup, required by
@react-three/fiber's Canvas (via react-use-measure) in jsdom, which doesn't
implement it natively.
Replace 'global.IntersectionObserver' with 'globalThis.IntersectionObserver' to fix
TS2304 "Cannot find name 'global'" error. globalThis is the standard environment-agnostic
reference that type-checks cleanly in browser-targeted tsconfig. Runtime behavior
identical to global in Node/jsdom contexts.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Implements a modal component for collecting product inquiries with name, email, and notes fields. Validates name (required) and email (must contain @) client-side, displays inline error messages for invalid input, and shows a confirmation message on valid submit.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Implement ProductCard component with:
- Renders product details (name, price, description)
- "Ask about this" button disabled when out of stock
- Shows "Currently unavailable" badge when out of stock
- Calls onInquire callback when clicked
Implement ServiceCard component with:
- Renders service details (name, formatted duration, price)
- Clickable button card that calls onSelect with service id
- formatDuration helper for human-readable time format
Both components include CSS modules with hover effects and proper styling.
All 4 tests pass (2 for each component).
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
framer-motion's useReducedMotion() hook caches its result at the module level
and never re-reads window.matchMedia on subsequent calls. This caused the second
test to fail because the cache was locked to the first test's matchMedia mock.
Fixed by using vi.doMock() with vi.resetModules() to mock useReducedMotion
directly with the desired value for each test, ensuring each test runs with a
fresh module context where the hook returns the expected value.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
- Implements Hero3D component with CSS 3D transforms
- Uses framer-motion's useScroll/useTransform for scroll-linked rotation
- Respects prefers-reduced-motion preference for static rendering
- Component structure: wrapper with perspective, inner motif with 3D rotation
- Test setup: added window.matchMedia mock for framer-motion compatibility
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
- Create Layout component that renders Nav > Outlet > Footer
- Wire Layout as parent route in App.tsx to wrap all page routes
- Update App.test.tsx to verify Nav/Footer render on every route
Claude-Session: https://claude.ai/code
Implement ScrollReveal component that fades and rises sections into view
on scroll with framer-motion. Respects prefers-reduced-motion by
rendering children unanimated when that setting is enabled.
- Add ScrollReveal component with fade+rise animation on scroll
- Add test coverage for both animated and reduced-motion paths
- Add IntersectionObserver mock to test setup
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH