Erstatt flytende Registrer score-banner med kompakt hjørne-FAB (V0)
Fullbredde-bunnbanneret hadde tre runder med bugs i skjul/vis-ved- scroll-mekanismen (playerListReached), sist når det ble hengende synlig over Kommentarer. Bruker avviste en fjerde punktfiks og ba i stedet om en redesign: en liten, hjørne-forankret FAB som er liten nok til at den ikke trenger å skjule seg igjen for å unngå å dekke innhold. Ny frontend/components/compact-score-fab.tsx (V0-eksport, integrert uendret). round-detail.tsx: playerListReached-mekanismen fjernet i sin helhet (state + effekt + den sårbare mapExpanded-vakten), erstattet med CompactScoreFab drevet av eksisterende inFlowButtonVisible. Netto -75 linjer. Verifisert i full-stack scratch (3 spillere + 6 kommentarer): FAB forblir synlig og ikke-obstruktiv helt ned til og forbi Kommentarer, lys+mørk modus, 48px trykkflate. tsc rent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d44e1cadb
commit
b45aa5bbe4
4 changed files with 120 additions and 111 deletions
35
CHANGELOG.md
35
CHANGELOG.md
|
|
@ -14664,3 +14664,38 @@ Neste steg:
|
|||
kompakt, hjørne-forankret "Score"-FAB som erstatning, jf.
|
||||
stående instruks om at nye UI-overflater skal gjennom V0 fremfor
|
||||
håndkodes direkte. Ingen utrulling av punktfiksen fant sted.
|
||||
|
||||
165. **Kompakt "Score"-FAB integrert, erstatter fullbredde-bunnbanneret
|
||||
helt (V0-eksport, oppfølging av punkt 164), 2026-08-23.** V0-prompt
|
||||
(`Temp-uploads/v0-prompt-compact-score-fab.md`) levert til bruker,
|
||||
eksport mottatt (`components/compact-score-fab.tsx`) og integrert
|
||||
tro mot eksporten -- ny fil `frontend/components/compact-score-fab.tsx`,
|
||||
uendret fra V0 bortsett fra kommentarer.
|
||||
|
||||
**`round-detail.tsx`:** `RegisterScoreButton` (den vanlige, i-flyt-
|
||||
plasserte knappen) er UENDRET og fortsatt eneste sted "Registrer
|
||||
score ↓"-teksten/pil-ikonet brukes. Den gamle flytende
|
||||
fullbredde-banner-JSX-en (`fixed inset-x-0 bottom-0` + gradient-scrim
|
||||
+ `h-20`-spacer) er fjernet i sin helhet, erstattet med
|
||||
`<CompactScoreFab visible={!inFlowButtonVisible}
|
||||
onClick={scrollToPlayerList} />`. Hele `playerListReached`-
|
||||
mekanismen (state + effekt + den sårbare `mapExpanded`-vakten som
|
||||
forårsaket punkt 164) er fjernet -- ikke lenger nødvendig, siden en
|
||||
liten hjørne-FAB ikke trenger å skjule seg igjen for å unngå å dekke
|
||||
over innhold. `inFlowButtonRef`/`inFlowButtonVisible`/
|
||||
`scrollToPlayerList`/`playerListRef`/`SCROLL_GAP_PX`/
|
||||
`getHeaderHeight()` er alle UENDRET og gjenbrukt som før (sistnevnte
|
||||
to også brukt av `scrollToHolePanel()`, urørt av denne runden).
|
||||
|
||||
**Verifisert i full-stack scratch** (samme oppskrift som punkt 164 --
|
||||
3 spillere + 6 kommentarer for å gi nok scroll-lengde): FAB-en
|
||||
bekreftet skjult mens i-flyt-knappen er synlig, dukker opp
|
||||
(fade+skaler, IKKE slide-opp) idet i-flyt-knappen ruller ut av syne,
|
||||
forblir korrekt synlig helt ned til og forbi Kommentarer UTEN å dekke
|
||||
over innholdet (bekreftet med skjermbilder, både lys og mørk modus),
|
||||
og skjules igjen ved scroll tilbake til toppen. Trykkflate målt til
|
||||
48px høyde (over 44px-gulvet), innholds-bredde (~110px, ikke full
|
||||
bredde). `tsc --noEmit` rent, ingen konsollfeil. Alle scratch-
|
||||
ressurser ryddet opp.
|
||||
|
||||
**Ikke rullet ut ennå** -- venter på brukerens bekreftelse.
|
||||
|
|
|
|||
49
frontend/components/compact-score-fab.tsx
Normal file
49
frontend/components/compact-score-fab.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"use client"
|
||||
|
||||
import { PencilLine } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type CompactScoreFabProps = {
|
||||
/** Controls the fade + scale in/out. Driven from outside (the page's
|
||||
* existing `inFlowButtonVisible` state) -- this component owns no
|
||||
* hide/show-on-scroll logic of its own. */
|
||||
visible: boolean
|
||||
/** Scrolls the user back to the player list (same action as the in-flow
|
||||
* button). No navigation, no new page. */
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact, corner-anchored "Score" quick button (FAB).
|
||||
*
|
||||
* Replaces the old full-width bottom banner (round-detail.tsx, removed
|
||||
* 2026-08-23 -- see CHANGELOG). Because it's small and pinned to the
|
||||
* bottom-right corner, it never needs to hide to avoid covering content --
|
||||
* so there is no scrim, no reserved bottom spacer, and no slide-up
|
||||
* animation. It simply fades + scales in place.
|
||||
*/
|
||||
export function CompactScoreFab({ visible, onClick }: CompactScoreFabProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-hidden={!visible}
|
||||
tabIndex={visible ? 0 : -1}
|
||||
className={cn(
|
||||
"fixed bottom-[calc(1.25rem+env(safe-area-inset-bottom))] right-[calc(1.25rem+env(safe-area-inset-right))] z-30",
|
||||
"inline-flex h-12 items-center gap-2 rounded-full px-5",
|
||||
"bg-primary text-primary-foreground font-bold",
|
||||
"shadow-lg shadow-black/20",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"hover:brightness-95 active:scale-[0.98]",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
visible ? "scale-100 opacity-100" : "pointer-events-none scale-90 opacity-0",
|
||||
)}
|
||||
>
|
||||
<PencilLine aria-hidden="true" className="size-5" />
|
||||
Score
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default CompactScoreFab
|
||||
|
|
@ -58,6 +58,7 @@ import {
|
|||
import { ShotMeasurementEntry } from "@/components/shot/shot-measurement-entry"
|
||||
import { HoleTargetDistance } from "@/components/hole-target-distance"
|
||||
import { HoleHistoryDetail } from "@/components/hole-history-detail"
|
||||
import { CompactScoreFab } from "@/components/compact-score-fab"
|
||||
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"
|
||||
|
||||
// --- Types -----------------------------------------------------------------
|
||||
|
|
@ -6315,25 +6316,21 @@ function sumForPlayer(list: ApiHole[] | undefined, numbers: number[]): { strokes
|
|||
// 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.
|
||||
// en runde med bane+utslag+dato+klokkeslett samtidig). Brukt av
|
||||
// scroll-hoppene til spillerlisten/hull-panelet 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).
|
||||
// Pusterom under headeren når det scrolles til spillerlisten/hull-panelet
|
||||
// -- se `scrollToPlayerList()`/`scrollToHolePanel()` under.
|
||||
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 --
|
||||
// det utvidede diagrammet kunne dytte knappen langt utenfor synlig område).
|
||||
// "Registrer score" -- den vanlige, i normal side-flyt-plasserte knappen
|
||||
// (se PlayerHoleCards). Den kompakte, hjørne-forankrede "Score"-FAB-en som
|
||||
// dukker opp når DENNE ruller ut av syne er en egen komponent,
|
||||
// `CompactScoreFab` (redesignet 2026-08-23, se CHANGELOG) -- ikke lenger
|
||||
// samme knapp flyttet til en flytende posisjon.
|
||||
function RegisterScoreButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
|
|
@ -6415,62 +6412,20 @@ function PlayerHoleCards({
|
|||
const [mapExpanded, setMapExpanded] = useState(false)
|
||||
const [distanceAvailable, setDistanceAvailable] = useState(false)
|
||||
const playerListRef = useRef<HTMLUListElement>(null)
|
||||
// Flytende "Registrer score" (2026-08-20) skal kun vises mens
|
||||
// spillerlisten fortsatt er UTENFOR synlig område -- brukerfunn: den
|
||||
// hang igjen synlig (og dekket delvis over spillerkort 1) selv etter at
|
||||
// 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 -- 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 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
|
||||
function check() {
|
||||
const el = playerListRef.current
|
||||
if (!el) return
|
||||
setPlayerListReached(el.getBoundingClientRect().top <= getHeaderHeight() + SCROLL_GAP_PX)
|
||||
}
|
||||
check()
|
||||
window.addEventListener("scroll", check, { passive: true })
|
||||
window.addEventListener("resize", check)
|
||||
return () => {
|
||||
window.removeEventListener("scroll", check)
|
||||
window.removeEventListener("resize", check)
|
||||
}
|
||||
}, [mapExpanded])
|
||||
// Den flytende knappen ble opprinnelig styrt av `mapExpanded` alene --
|
||||
// FEIL, funnet av bruker 2026-08-20: den IKKE-utvidede ("compact")
|
||||
// visningen viser OGSÅ hele hindringslisten (ADR-083, alle hindringer,
|
||||
// ikke bare nærmeste), som på hull med mange hindringer alene kan bli
|
||||
// høy nok til å dytte den vanlige, side-flyt-plasserte knappen utenfor
|
||||
// synlig område -- UTEN at banekartet noensinne ble utvidet. Riktig
|
||||
// signal er derfor ikke "er kartet utvidet", men "er selve
|
||||
// side-flyt-knappen faktisk synlig akkurat nå" -- målt direkte, samme
|
||||
// mønster som `playerListReached` over (ikke IntersectionObserver, av
|
||||
// samme grunn som der: unngår enhver antakelse om når komponenten
|
||||
// regnes som "utvidet" eller ikke -- fungerer likt uansett ÅRSAK til at
|
||||
// knappen er utenfor synlig område).
|
||||
// Flytende "Score"-hurtigknapp (CompactScoreFab, redesignet 2026-08-23
|
||||
// -- se CHANGELOG): en liten, hjørne-forankret FAB nederst til høyre,
|
||||
// IKKE lenger et fullbredde-bunnbanner. Trenger derfor ikke lenger noe
|
||||
// eget "har brukeren nådd spillerlisten"-signal for å skjule seg igjen
|
||||
// (den tidligere `playerListReached`-mekanismen, fjernet) -- en liten
|
||||
// knapp i et hjørne dekker ikke nevneverdig over innhold under, uansett
|
||||
// hvor langt ned siden brukeren har scrollet. Eneste synlighets-signal
|
||||
// som gjenstår er `inFlowButtonVisible` under: er selve den vanlige,
|
||||
// side-flyt-plasserte knappen synlig akkurat nå. Målt direkte med
|
||||
// `getBoundingClientRect()` + scroll/resize-lytter (ikke
|
||||
// IntersectionObserver -- viste seg upålitelig i scratch-verifisering
|
||||
// 2026-08-20: med standard threshold=[0] fyrer den kun ved
|
||||
// 0%->>0%-kryssinger, ikke ved at `top`/`bottom` passerer en INTERN
|
||||
// linje midt i et allerede-overlappende element).
|
||||
const inFlowButtonRef = useRef<HTMLDivElement>(null)
|
||||
const [inFlowButtonVisible, setInFlowButtonVisible] = useState(true)
|
||||
useEffect(() => {
|
||||
|
|
@ -6883,47 +6838,17 @@ function PlayerHoleCards({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Flytende "Registrer score" (V0-eksport, 2026-08-20, utløser-
|
||||
logikk rettet samme dag) -- vises når side-flyt-knappen over
|
||||
IKKE er synlig (uansett ÅRSAK: utvidet banekart ELLER en lang
|
||||
hindringsliste alene, se `inFlowButtonVisible`-kommentaren over)
|
||||
OG spillerlisten fortsatt er utenfor synlig område (se
|
||||
`playerListReached` -- uten denne dekket den delvis over
|
||||
spillerkort 1 selv etter at brukeren allerede hadde scrollet
|
||||
dit). `fixed` posisjonerer mot selve viewporten (ingen ancestor
|
||||
her setter transform/filter, så det er trygt), gradient-scrim +
|
||||
skygge løfter den visuelt over innholdet under,
|
||||
`env(safe-area-inset-bottom)` unngår at den klemmes helt inntil
|
||||
kanten på telefoner med gest-navigasjon. `pointer-events-none`
|
||||
på selve scrim-sonen når skjult/i overgang, slik at den aldri
|
||||
blokkerer klikk på innhold bak. */}
|
||||
<div
|
||||
aria-hidden={inFlowButtonVisible || playerListReached}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-30 px-5 pb-[calc(0.875rem+env(safe-area-inset-bottom))] pt-8 transition-all duration-300 ease-out",
|
||||
"bg-gradient-to-t from-background via-background/90 to-transparent",
|
||||
!inFlowButtonVisible && !playerListReached
|
||||
? "translate-y-0 opacity-100"
|
||||
: "pointer-events-none translate-y-full opacity-0",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto w-full max-w-3xl",
|
||||
!inFlowButtonVisible && !playerListReached ? "pointer-events-auto" : "pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div className="rounded-2xl shadow-xl shadow-black/25">
|
||||
<RegisterScoreButton
|
||||
onClick={scrollToPlayerList}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Gir siste innhold på siden rom til å scrolle klar av den flytende
|
||||
knappen i stedet for å bli permanent skjult bak den -- samme
|
||||
betingelse som selve knappen. */}
|
||||
{!inFlowButtonVisible && !playerListReached && <div aria-hidden="true" className="h-20" />}
|
||||
{/* Kompakt "Score"-hurtigknapp (CompactScoreFab, redesignet
|
||||
2026-08-23 -- se CHANGELOG). Erstatter det tidligere fullbredde-
|
||||
bunnbanneret, som hadde tre runder med skjul/vis-bugs (#141/142,
|
||||
samt en fjerde runde der den ble hengende synlig over
|
||||
Kommentarer-seksjonen). En liten, hjørne-forankret FAB trenger
|
||||
ikke den samme skjul-igjen-mekanismen -- den er liten nok til at
|
||||
den ikke dekker nevneverdig over innhold, uansett hvor langt ned
|
||||
siden brukeren har scrollet. Vises alene når
|
||||
`!inFlowButtonVisible` (den vanlige, side-flyt-plasserte knappen
|
||||
er utenfor synlig område). */}
|
||||
<CompactScoreFab visible={!inFlowButtonVisible} onClick={scrollToPlayerList} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue