Feature/mobile#12
Conversation
Reviewer's GuideImplements a mobile-optimized variant of the landing experience that swaps the horizontal scroller for a vertical snap-scrolling flow on small screens, adds safe-area aware layouts and visual tweaks for each panel, and introduces a rail section-driven background color transition, along with minor theming and dependency updates. Sequence diagram for CapyRailSection-driven background transitionsequenceDiagram
actor User
participant Browser as BrowserWindow
participant Lander as LanderComponent
participant CapyRailSection as CapyRailSectionComponent
participant IntersectionObserver as RailObserver
participant DOMBody as DOMBodyElement
participant CSSEngine
User->>Browser: load_capital_lander_url
Browser->>Lander: mount_Lander_component
Lander->>CapyRailSection: render_CapyRailSection
CapyRailSection->>Browser: useEffect_setupRailObserver
Browser->>IntersectionObserver: create_observer(threshold_0_85)
IntersectionObserver->>DOMBody: observe_element_with_id_more
User->>Browser: scroll_panels
Browser->>IntersectionObserver: intersection_callback(entry_ratio_0_9)
IntersectionObserver->>DOMBody: add_class_rail_active
DOMBody->>CSSEngine: trigger_restyle
CSSEngine->>Browser: apply_body_rail_active_background
User->>Browser: scroll_panels_out_of_view
Browser->>IntersectionObserver: intersection_callback(entry_ratio_0_5)
IntersectionObserver->>DOMBody: remove_class_rail_active
DOMBody->>CSSEngine: trigger_restyle
CSSEngine->>Browser: restore_default_body_background
Class diagram for updated Lander mobile/desktop layout selectionclassDiagram
class Lander {
- HTMLElement scrollerRef
- HTMLElement disabledRef
- boolean isMobile
+ Lander()
+ useHorizontalWheelScroll(targetRef, options)
+ useEffect_setupMediaQuery()
}
class HeroSection {
+ HeroSection()
+ AnimatedPanel
+ img heroLogoMark
}
class FeaturesSection {
+ FeaturesSection()
+ AnimatedPanel
}
class InterfaceSection {
+ InterfaceSection()
+ AnimatedPanel
}
class ContactSection {
+ ContactSection()
+ AnimatedPanel
}
class CapyRailSection {
+ CapyRailSection()
+ useEffect_setupRailObserver()
}
class TopNav {
+ TopNav()
}
class useHorizontalWheelScroll {
+ useHorizontalWheelScroll(targetRef, options)
}
class AnimatedPanel {
+ AnimatedPanel(props)
}
class MotionDiv {
+ motion_div(props)
}
Lander --> HeroSection : renders
Lander --> FeaturesSection : renders
Lander --> InterfaceSection : renders
Lander --> ContactSection : renders
Lander --> CapyRailSection : renders
Lander --> TopNav : renders onDesktop
Lander ..> useHorizontalWheelScroll : uses
HeroSection ..> AnimatedPanel : wrapsContent
FeaturesSection ..> AnimatedPanel : wrapsContent
InterfaceSection ..> AnimatedPanel : wrapsContent
ContactSection ..> AnimatedPanel : wrapsContent
CapyRailSection ..> AnimatedPanel : wrapsContent
Lander ..> MotionDiv : wrapsPanels
class DesktopLayout {
+ horizontalScroller
+ panelTrack
}
class MobileLayout {
+ mobileSnapScroller
}
Lander --> DesktopLayout : when isMobile false
Lander --> MobileLayout : when isMobile true
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The
isMobilestate inLanderdirectly accesseswindow.matchMediaboth in the initializer anduseEffect, which can throw in non-browser/SSR environments—consider guarding allwindowusage behind a runtime check that no-ops whenwindowis undefined. - In
Lander,disabledRefis created only to pass intouseHorizontalWheelScrollon mobile but is never attached to a DOM element, so you can simplify by passingnull(or updating the hook to handle a disabled flag) instead of keeping an unused ref. - The
IntersectionObserverinCapyRailSectiontoggles the globalbodyclass but assumesdocument/bodyis always available; consider early-returning whentypeof document === 'undefined'to keep this component safe in non-DOM environments.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `isMobile` state in `Lander` directly accesses `window.matchMedia` both in the initializer and `useEffect`, which can throw in non-browser/SSR environments—consider guarding all `window` usage behind a runtime check that no-ops when `window` is undefined.
- In `Lander`, `disabledRef` is created only to pass into `useHorizontalWheelScroll` on mobile but is never attached to a DOM element, so you can simplify by passing `null` (or updating the hook to handle a disabled flag) instead of keeping an unused ref.
- The `IntersectionObserver` in `CapyRailSection` toggles the global `body` class but assumes `document`/`body` is always available; consider early-returning when `typeof document === 'undefined'` to keep this component safe in non-DOM environments.
## Individual Comments
### Comment 1
<location path="src/lander/Lander.tsx" line_range="15" />
<code_context>
import { Helmet } from 'react-helmet-async'
import styles from './Lander.module.css'
+const MOBILE_QUERY = '(max-width: 768px)'
+
/**
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the responsive logic and shared sections into reusable hooks/components so Lander stays focused and avoids duplicated JSX and ad‑hoc control flow.
You can keep the new behavior and trim quite a bit of complexity with a few small extra abstractions.
### 1. Extract media-query logic into a hook
Move the `MOBILE_QUERY` + `useState` + `useEffect` bundle out of `Lander`:
```ts
// shared/hooks/useIsMobile.ts
import { useEffect, useState } from 'react'
const MOBILE_QUERY = '(max-width: 768px)'
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(
() => typeof window !== 'undefined' && window.matchMedia(MOBILE_QUERY).matches,
)
useEffect(() => {
const mq = window.matchMedia(MOBILE_QUERY)
const handler = (event: MediaQueryListEvent) => setIsMobile(event.matches)
handler(mq as any) // ensure initial sync if needed
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [])
return isMobile
}
```
Then `Lander` becomes simpler:
```ts
import { useRef } from 'react'
import { useIsMobile } from '@/shared/hooks/useIsMobile'
function Lander() {
const scrollerRef = useRef<HTMLElement | null>(null)!
const isMobile = useIsMobile()
// ...
}
```
### 2. Simplify `useHorizontalWheelScroll` API
Remove the dummy `disabledRef` by letting the hook no-op on `null`:
```ts
// shared/hooks/useHorizontalWheelScroll.ts
export function useHorizontalWheelScroll(
ref: React.RefObject<HTMLElement | null> | null,
options: { endCutoffPx?: number; enabled?: boolean } = {},
) {
const { enabled = true, endCutoffPx } = options
useEffect(() => {
if (!enabled || !ref?.current) return
// existing logic here...
}, [enabled, ref, endCutoffPx])
}
```
Usage in `Lander`:
```ts
const scrollerRef = useRef<HTMLElement | null>(null)!
const isMobile = useIsMobile()
useHorizontalWheelScroll(isMobile ? null : scrollerRef, { endCutoffPx: 300 })
```
No extra ref, clearer intent.
### 3. Deduplicate shared sections
You can keep mobile-only and desktop-only sections while avoiding repeated JSX:
```ts
const sharedSections = (
<>
<HeroSection />
<FeaturesSection />
<CapyRailSection />
</>
)
function Lander() {
const scrollerRef = useRef<HTMLElement | null>(null)!
const isMobile = useIsMobile()
useHorizontalWheelScroll(isMobile ? null : scrollerRef, { endCutoffPx: 300 })
return (
<>
<Helmet>{/* ... */}</Helmet>
<div className={styles.appRoot}>
{isMobile ? (
<main className={styles.mobileSnapScroller}>{sharedSections}</main>
) : (
<>
<TopNav />
<main
className={styles.horizontalScroller}
ref={scrollerRef}
id="scroller"
>
<motion.div
className={styles.panelTrack}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
>
{sharedSections}
<InterfaceSection />
<ContactSection />
</motion.div>
</main>
</>
)}
</div>
<ExitOverlay />
</>
)
}
```
This keeps the mobile/desktop differences (no `TopNav`, `InterfaceSection`, `ContactSection` on mobile) but removes duplication and makes it easier to maintain section ordering.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| import { Helmet } from 'react-helmet-async' | ||
| import styles from './Lander.module.css' | ||
|
|
||
| const MOBILE_QUERY = '(max-width: 768px)' |
There was a problem hiding this comment.
issue (complexity): Consider extracting the responsive logic and shared sections into reusable hooks/components so Lander stays focused and avoids duplicated JSX and ad‑hoc control flow.
You can keep the new behavior and trim quite a bit of complexity with a few small extra abstractions.
1. Extract media-query logic into a hook
Move the MOBILE_QUERY + useState + useEffect bundle out of Lander:
// shared/hooks/useIsMobile.ts
import { useEffect, useState } from 'react'
const MOBILE_QUERY = '(max-width: 768px)'
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(
() => typeof window !== 'undefined' && window.matchMedia(MOBILE_QUERY).matches,
)
useEffect(() => {
const mq = window.matchMedia(MOBILE_QUERY)
const handler = (event: MediaQueryListEvent) => setIsMobile(event.matches)
handler(mq as any) // ensure initial sync if needed
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [])
return isMobile
}Then Lander becomes simpler:
import { useRef } from 'react'
import { useIsMobile } from '@/shared/hooks/useIsMobile'
function Lander() {
const scrollerRef = useRef<HTMLElement | null>(null)!
const isMobile = useIsMobile()
// ...
}2. Simplify useHorizontalWheelScroll API
Remove the dummy disabledRef by letting the hook no-op on null:
// shared/hooks/useHorizontalWheelScroll.ts
export function useHorizontalWheelScroll(
ref: React.RefObject<HTMLElement | null> | null,
options: { endCutoffPx?: number; enabled?: boolean } = {},
) {
const { enabled = true, endCutoffPx } = options
useEffect(() => {
if (!enabled || !ref?.current) return
// existing logic here...
}, [enabled, ref, endCutoffPx])
}Usage in Lander:
const scrollerRef = useRef<HTMLElement | null>(null)!
const isMobile = useIsMobile()
useHorizontalWheelScroll(isMobile ? null : scrollerRef, { endCutoffPx: 300 })No extra ref, clearer intent.
3. Deduplicate shared sections
You can keep mobile-only and desktop-only sections while avoiding repeated JSX:
const sharedSections = (
<>
<HeroSection />
<FeaturesSection />
<CapyRailSection />
</>
)
function Lander() {
const scrollerRef = useRef<HTMLElement | null>(null)!
const isMobile = useIsMobile()
useHorizontalWheelScroll(isMobile ? null : scrollerRef, { endCutoffPx: 300 })
return (
<>
<Helmet>{/* ... */}</Helmet>
<div className={styles.appRoot}>
{isMobile ? (
<main className={styles.mobileSnapScroller}>{sharedSections}</main>
) : (
<>
<TopNav />
<main
className={styles.horizontalScroller}
ref={scrollerRef}
id="scroller"
>
<motion.div
className={styles.panelTrack}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
>
{sharedSections}
<InterfaceSection />
<ContactSection />
</motion.div>
</main>
</>
)}
</div>
<ExitOverlay />
</>
)
}This keeps the mobile/desktop differences (no TopNav, InterfaceSection, ContactSection on mobile) but removes duplication and makes it easier to maintain section ordering.
Summary by Sourcery
Introduce a mobile-optimized variant of the landing experience with responsive styling and dynamic background behavior tied to the CapyRail section.
New Features:
Enhancements:
Build: