Redesign the phone layer of the landing
Three layout bugs and a mobile experience that had no wayfinding. The `#proposal` section was clipped, not merely cramped: the submit button carried `whitespace-nowrap` from the shared button base, giving it a min-content width of 358px, which sized the section's implicit `auto` grid track to 406px inside a 343px container. `overflow-hidden` then sliced the copy and every form field off the right edge of a 375px screen. Fixed at the root — `whitespace-nowrap` moves out of `base` and into `headerSize`, where buttons are sized by their label — plus an explicit `grid-cols-[minmax(0,1fr)]` on the section. The market stat cards put their source line in absolute position against a guessed min-height, so the longest label ran underneath it. It is in the flow now, pinned by `mt-auto`. Below 980px the header nav is not rendered, which left seventeen screens of scroll with no way to navigate them. The header gives that width back to the page — logo and call button only — and <ThumbBar /> takes over: scroll progress, a live section indicator that opens a section sheet, and one short CTA, hidden over the hero and over the form. Card rows now say what they are. Rows of unordered peers keep scrolling sideways and gain a measured segment indicator (<Rail />), replacing the two prose "swipe sideways" hints. The launch timeline goes the other way: sideways scrolling hid week 3-5 from someone reading week 1-2, so below 900px it becomes four disclosures on a spine — which also recovers the scroll length the taller hero spent. Below 560px the type scale steps down half a level and the display tracking opens back up, and the hero photo zooms past `cover` onto the wave wall, since fitting by height left only ceiling tiles above the copy. Also: the route card inside the market section split into two 165px columns at 760px, so that split now waits for 980px. Desktop above 980px is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Which section the reader is currently in, plus how far through the page they
|
||||
* are. Driven by scroll position rather than by an IntersectionObserver: every
|
||||
* section here is taller than a phone viewport, so the question is not "is this
|
||||
* visible" but "which one is under the reading line", and a threshold-based
|
||||
* observer cannot answer that.
|
||||
*
|
||||
* `hrefs` must be a stable array — pass a module-level constant.
|
||||
*/
|
||||
export function useReadingPosition(hrefs: readonly string[]) {
|
||||
const [index, setIndex] = useState(0)
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
let frame = 0
|
||||
|
||||
const measure = () => {
|
||||
frame = 0
|
||||
|
||||
/* A third of the way down the screen: the line the eye actually reads
|
||||
from, not the very top edge. */
|
||||
const line = window.scrollY + window.innerHeight * 0.3
|
||||
let current = 0
|
||||
|
||||
hrefs.forEach((href, position) => {
|
||||
const element = document.querySelector(href)
|
||||
if (element && element.getBoundingClientRect().top + window.scrollY <= line) current = position
|
||||
})
|
||||
|
||||
const scrollable = document.documentElement.scrollHeight - window.innerHeight
|
||||
|
||||
setIndex(current)
|
||||
setProgress(scrollable > 0 ? Math.min(1, window.scrollY / scrollable) : 0)
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
if (!frame) frame = requestAnimationFrame(measure)
|
||||
}
|
||||
|
||||
measure()
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
window.addEventListener('resize', onScroll)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
window.removeEventListener('resize', onScroll)
|
||||
if (frame) cancelAnimationFrame(frame)
|
||||
}
|
||||
}, [hrefs])
|
||||
|
||||
return { index, progress }
|
||||
}
|
||||
Reference in New Issue
Block a user