Skip to content

Feature/mobile#12

Merged
shamikkarkhanis merged 2 commits into
developfrom
feature/mobile
Apr 23, 2026
Merged

Feature/mobile#12
shamikkarkhanis merged 2 commits into
developfrom
feature/mobile

Conversation

@shamikkarkhanis
Copy link
Copy Markdown
Member

@shamikkarkhanis shamikkarkhanis commented Apr 23, 2026

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:

  • Add mobile-specific scroll snapping layout for the landing page panels using a vertical snap scroller.
  • Detect mobile viewport via media queries in the Lander component to switch between horizontal desktop and vertical mobile experiences.
  • Display a hero logo mark and adjust hero typography specifically for mobile viewports.
  • Change the page background color dynamically when the CapyRail section is prominently in view.

Enhancements:

  • Refine responsive styling across hero, features, interface, contact, glass card, CapyRail sections, and top navigation to better support small screens and safe-area insets.
  • Update viewport meta and theme color in index.html for improved mobile browser integration.
  • Adjust global body background and transitions to align with the new rail-active state.

Build:

  • Bump Prettier dev dependency from 3.8.1 to 3.8.3.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 23, 2026

Reviewer's Guide

Implements 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 transition

sequenceDiagram
  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
Loading

Class diagram for updated Lander mobile/desktop layout selection

classDiagram
  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
Loading

File-Level Changes

Change Details Files
Add responsive mobile detection and conditional layout rendering for the Lander page.
  • Introduce a MOBILE_QUERY constant based on max-width: 768px.
  • Track isMobile state using window.matchMedia with a change listener to update on viewport resize.
  • Disable horizontal wheel scrolling on mobile by passing a dummy ref to useHorizontalWheelScroll when isMobile is true.
  • Render a simplified mobile-only main element with vertical snap scrolling instead of the horizontal scroller and TopNav, while preserving the original desktop layout otherwise.
src/lander/Lander.tsx
src/lander/Lander.module.css
Define mobile-specific snap-scrolling behavior and panel styling, including safe-area handling.
  • Add .mobileSnapScroller styles with vertical scroll snapping, hidden scrollbars, and full-viewport height.
  • Override .appRoot and global .panel styles in a mobile media query to remove borders/radius, enforce full-viewport height, and enable scroll-snap alignment.
  • Apply mobile @media rules for each section panel (hero, features, interface, contact, capy rail) to adjust dimensions, padding, typography, layout flow, and hide or reflow certain feature cards.
  • Remove glass-card border and blur on mobile for a flatter look.
src/lander/Lander.module.css
src/lander/sections/HeroSection.module.css
src/lander/sections/FeaturesSection.module.css
src/lander/sections/InterfaceSection.module.css
src/lander/sections/ContactSection.module.css
src/lander/sections/CapyRailSection.module.css
src/shared/components/GlassCard.module.css
Enhance hero and navigation components for mobile presentation and safe-area support.
  • In HeroSection, render a logo mark image positioned absolutely for mobile, and prevent the leading hero title span from wrapping.
  • Add mobile-specific typography and layout tweaks for hero content, including full-height panel, transparent background, and logo placement respecting safe areas.
  • Adjust TopNav padding to include env(safe-area-inset-top) on compact layouts and tighten spacing, logo size, and pill/button dimensions on small screens.
src/lander/sections/HeroSection.tsx
src/lander/sections/HeroSection.module.css
src/shared/components/TopNav.module.css
Drive background color transitions based on CapyRail section visibility.
  • In CapyRailSection, create an IntersectionObserver watching the section with id="more" and toggle a rail-active class on the document body based on intersection ratio threshold 0.85.
  • Ensure cleanup disconnects the observer and removes the rail-active class on unmount.
  • In global styles, set a default body background color and define an alternate background when body.rail-active is present, with a smooth transition.
src/lander/sections/CapyRailSection.tsx
src/index.css
Improve mobile meta configuration and update tooling dependency.
  • Update index.html viewport meta tag to include viewport-fit=cover and add a theme-color meta for better PWA and browser UI theming on mobile.
  • Bump prettier from 3.8.1 to 3.8.3 in package.json (and associated lockfile).
index.html
package.json
package-lock.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@shamikkarkhanis shamikkarkhanis merged commit 6545baf into develop Apr 23, 2026
3 checks passed
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lander/Lander.tsx
import { Helmet } from 'react-helmet-async'
import styles from './Lander.module.css'

const MOBILE_QUERY = '(max-width: 768px)'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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