diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1ca52..a1538ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13584,3 +13584,42 @@ Neste steg: spillerlisten -- knappen dukket korrekt opp igjen. `tsc --noEmit` rent. **Ikke rullet ut ennå** -- venter på bekreftelse fra bruker. + +142. **Nei, ikke helt -- ekte målebasert oppfølging av #141, + 2026-08-20.** Bruker spurte direkte: scroller det nå faktisk langt + nok ned til å vise HELE spillerkort 1? Presis scratch-måling (ikke + skjermbilde-eyeballing) avdekket at svaret fortsatt var nei: det + hardkodede `112px` (lånt fra `scroll-mt-28`, brukt både i CSS-en for + scroll-hoppet og i #141 sin `playerListReached`-terskel) stemte ikke + -- headerens EKTE høyde målte 141,5px for denne runden (bane + utslag + + dato + klokkeslett samtidig), ~30px mer enn antatt. Toppen av + spillerkort 1 lå dermed fortsatt delvis bak headeren. + + Rot-årsak: `RoundHeader` sin høyde er reelt DYNAMISK (rundenavn/ + utslag/dato/klokkeslett kan pakkes til flere linjer avhengig av + runden) -- en NY hardkodet konstant (f.eks. 160px) ville bare flyttet + samme sårbarhet til en annen runde med enda lenger innhold, ikke løst + den. Derfor målt LIVE i stedet: + + - Ny `getHeaderHeight()`-hjelper: `document.querySelector("header") + ?.getBoundingClientRect().height` (112 kun som aller siste + nødfallback). + - `scrollToPlayerList()` erstatter `scrollIntoView` + statisk + `scroll-mt-28` -- regner selv ut nøyaktig mål-scrollposisjon med + `getHeaderHeight()` + en delt `SCROLL_GAP_PX = 16`-konstant. + - `playerListReached`-sjekken (#141) oppdatert til å bruke SAMME + `getHeaderHeight() + SCROLL_GAP_PX`-terskel -- måtte rettes en gang + til underveis i denne rundens scratch-verifisering: brukte først + kun `getHeaderHeight()` uten gap-konstanten, som fikk knappen til å + henge synlig selv etter et korrekt landet hopp (16px avvik mellom + hvor scrollet faktisk landet og hvor "reached" ble regnet ut). + + **Verifisert i scratch, presist (ikke bare skjermbilde):** målte + `getBoundingClientRect()` for spillerkort 1 og headeren direkte via + `evaluate_script` rett etter scroll-hoppet. Bekreftet numerisk: + `cardTop: 157.5`, `headerBottom: 141.5`, `gapBelowHeader: 16`, + `fullyVisible: true` (både over header OG innenfor viewport-bunnen). + Bekreftet at den flytende knappen samtidig er `aria-hidden="true"` + (korrekt skjult) i samme øyeblikk. `tsc --noEmit` rent. + + **Ikke rullet ut ennå** -- venter på bekreftelse fra bruker. diff --git a/frontend/components/round-detail.tsx b/frontend/components/round-detail.tsx index 0f81f68..6157003 100644 --- a/frontend/components/round-detail.tsx +++ b/frontend/components/round-detail.tsx @@ -6634,6 +6634,26 @@ function sumForPlayer(list: ApiHole[] | undefined, numbers: number[]): { strokes // Trykk åpner SAMME eksisterende fullskjerm-veiviser som før (onOpenEntry) // -- selve registreringsvinduet er bevisst IKKE rørt av denne rundens // V0-prompt. +// Målt LIVE, ikke en hardkodet konstant -- den sticky RoundHeader-en sin +// høyde er reelt dynamisk (rundenavn/dato/klokkeslett kan pakkes til flere +// linjer avhengig av runden), så en fast pixel-gjetning viste seg feil for +// en runde med lengre innhold enn den opprinnelige gjetningen (112px) var +// kalibrert mot (scratch-verifisering 2026-08-20: ekte høyde 141,5px for +// en runde med bane+utslag+dato+klokkeslett samtidig). Brukt av BÅDE +// scroll-hoppet til spillerlisten og `playerListReached`-sjekken under. +function getHeaderHeight(): number { + return document.querySelector("header")?.getBoundingClientRect().height ?? 112 +} + +// Pusterom under headeren når spillerlisten scrolles til -- brukt for +// BÅDE selve scroll-målposisjonen og "reached"-terskelen (se begge +// under). Delt konstant med vilje: scroll-hoppet lander bevisst +// `SCROLL_GAP_PX` under headeren (ikke flush), så "reached"-sjekken må +// bruke SAMME avstand, ellers forblir den flytende knappen synlig etter +// at hoppet har landet (funnet i scratch-verifisering 2026-08-20 -- de to +// brukte ulike terskler). +const SCROLL_GAP_PX = 16 + // "Registrer score" -- samme knapp, to plasseringer (se PlayerHoleCards): // i normal side-flyt når banekartet er kollapset, flytende festet nederst // på skjermen når det er utvidet (V0-eksport, 2026-08-20, brukerønske -- @@ -6725,28 +6745,35 @@ function PlayerHoleCards({ // brukeren allerede hadde scrollet forbi/til spillerlisten, der den ikke // lenger gir mening ("Registrer score ↓" når du allerede ser // spillerne). "Reached" = listens toppkant har nådd/passert headerens - // faktiske høyde (samme 112px som scroll-mt-28 andre steder i denne - // filen) -- forblir sant BÅDE mens listen vises OG etter at brukeren - // har scrollet helt forbi den (listen ligger da over synlig område, - // `top` er negativ). Kun scroll TILBAKE OPP forbi listen (top blir - // igjen større enn 112) gjør at knappen skal dukke opp igjen. + // faktiske høyde -- forblir sant BÅDE mens listen vises OG etter at + // brukeren har scrollet helt forbi den (listen ligger da over synlig + // område, `top` er negativ). Kun scroll TILBAKE OPP forbi listen gjør + // at knappen skal dukke opp igjen. // // Første forsøk brukte IntersectionObserver med en krympet rootMargin // -- viste seg FEIL i scratch-verifisering: med standard threshold=[0] // fyrer den kun ved 0%->>0%-kryssinger (element helt inn/ut av roten), // ikke ved at `boundingClientRect.top` passerer en INTERN linje midt i // et allerede-overlappende element. Listen ble dermed hengende på - // "ikke nådd" gjennom hele resten av scrollingen forbi 112px-grensen. - // Vanlig scroll-lytter med direkte `getBoundingClientRect()`-sjekk gir - // korrekt, kontinuerlig oppdatering. + // "ikke nådd" gjennom hele resten av scrollingen forbi grensen. Vanlig + // scroll-lytter med direkte `getBoundingClientRect()`-sjekk gir korrekt, + // kontinuerlig oppdatering. + // + // ANNET funn i scratch, samme runde: en HARDKODET grense (112px, lånt + // fra `scroll-mt-28` andre steder i filen) var selv FEIL -- headeren + // sin faktiske høyde målte 141.5px for denne runden, ikke 112. Headeren + // sitt innhold (rundenavn/dato/klokkeslett) kan pakkes til flere linjer + // avhengig av runden, så høyden er reelt DYNAMISK, ikke en konstant -- + // en ny hardkodet gjetning (f.eks. 160px) ville bare flyttet samme + // sårbarhet til en annen runde med enda lenger innhold. Målt LIVE i + // stedet, se `getHeaderHeight()` under. const [playerListReached, setPlayerListReached] = useState(false) useEffect(() => { if (!mapExpanded) return - const HEADER_HEIGHT_PX = 112 function check() { const el = playerListRef.current if (!el) return - setPlayerListReached(el.getBoundingClientRect().top <= HEADER_HEIGHT_PX) + setPlayerListReached(el.getBoundingClientRect().top <= getHeaderHeight() + SCROLL_GAP_PX) } check() window.addEventListener("scroll", check, { passive: true }) @@ -6756,6 +6783,17 @@ function PlayerHoleCards({ window.removeEventListener("resize", check) } }, [mapExpanded]) + // Manuell scroll (ikke `scrollIntoView` + CSS `scroll-margin-top`) -- + // samme begrunnelse som `getHeaderHeight()` over: en CSS-klasse som + // `scroll-mt-28` kan ikke være dynamisk, og headerens høyde varierer + // reelt med runde-innholdet. Regner ut nøyaktig mål-posisjon selv, med + // et lite pusterom (16px) under headeren. + function scrollToPlayerList() { + const el = playerListRef.current + if (!el) return + const top = el.getBoundingClientRect().top + window.scrollY - getHeaderHeight() - SCROLL_GAP_PX + window.scrollTo({ top, behavior: "smooth" }) + } const holeMeta: Record = {} for (const list of Object.values(holesByParticipant)) { for (const h of list) { @@ -6896,7 +6934,7 @@ function PlayerHoleCards({ kunne dytte knappen helt utenfor synlig skjermbilde). */} {!mapExpanded && ( playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })} + onClick={scrollToPlayerList} /> )} @@ -7169,7 +7207,7 @@ function PlayerHoleCards({ >
playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })} + onClick={scrollToPlayerList} />