Fiks scroll-til-spillerliste for godt: mål header-høyden live, ikke hardkodet

Presis scratch-måling avdekket at den hardkodede 112px-antagelsen (arvet
fra scroll-mt-28) var feil -- ekte headerhøyde var 141.5px for en runde
med bane+utslag+dato+klokkeslett samtidig, siden headeren pakkes til
flere linjer avhengig av innhold. Ny getHeaderHeight() måler live i
stedet for å gjette. scrollToPlayerList() erstatter scrollIntoView med
manuell beregning; playerListReached bruker samme terskel (delt
SCROLL_GAP_PX-konstant) -- måtte rettes en gang til underveis da de to
brukte ulike terskler. Se CHANGELOG.md #142. Ikke rullet ut ennå.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-20 08:57:22 +02:00
parent 76b306cb29
commit 116600cd3c
2 changed files with 89 additions and 12 deletions

View file

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

View file

@ -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<number, { par: number; index: number }> = {}
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 && (
<RegisterScoreButton
onClick={() => playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
onClick={scrollToPlayerList}
/>
)}
@ -7169,7 +7207,7 @@ function PlayerHoleCards({
>
<div className="rounded-2xl shadow-xl shadow-black/25">
<RegisterScoreButton
onClick={() => playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
onClick={scrollToPlayerList}
/>
</div>
</div>