Åtte brukerrapporterte UX-funn: ny-runde og score-registrering
Fire rettet direkte (reelle logikkfeil/tekstvalg): round-card.tsx sin
"Hull"-celle viste alltid planlagt antall, aldri faktisk spilt for
tidlig avsluttede runder; putt-avstand-etiketter gjort konsekvente
("X-Ym" i stedet for blandet "<Xm ... 8m+"); "Se feed" presisert til
"Se venneaktivitet"; @-tagging av medspillere vurdert og bevisst utsatt.
Fire sendt via V0 (reelt interaksjonsdesign), diffet mot live-treet før
integrering: ny-runde steg 2 sin rekkefølge (Antall hull/Avanserte HCP
flyttet før formatrutenettet), "detaljer"-steget delt i seksjoner med
et nytt scroll-hint, Anywayslag unifisert til samme +/--mønster som
Chip/Bunker/Straffeslag, netto par-markør + egen Putter-aksent.
Se ADR-062/CHANGELOG punkt 71 for full detalj. tsc rent, full
scratch-stack browserverifisert (lys+mørk, mobil viewport) før
utrulling.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7c8b352eca
commit
3db00e72db
6 changed files with 342 additions and 71 deletions
|
|
@ -5842,6 +5842,84 @@ og en tilsvarende fullstendighetssjekk PÅ TVERS AV `round-detail.tsx`/
|
|||
komponentfiler som ville krevd en helt annen testtilnærming enn ren
|
||||
lib-import — egen, fremtidig vurdering).
|
||||
|
||||
---
|
||||
|
||||
## ADR-062: Åtte brukerrapporterte UX-funn — "Ny runde" og score-registrering — 2026-08-11
|
||||
|
||||
Bruker sendte åtte konkrete, skjermbilde-dokumenterte problemer fra
|
||||
faktisk bruk (dashbord, ny-runde-veiviseren, score-registrering, feed).
|
||||
Delt i to spor etter samme prinsipp som tidligere runder: reelle
|
||||
logikkfeil rettet direkte, reelt interaksjonsdesign sendt via V0.
|
||||
|
||||
**Direkte rettet, ingen V0 (4 av 8):**
|
||||
1. **`round-card.tsx`: "Hull"-cellen viste alltid PLANLAGT antall hull
|
||||
(`holes_planned`), aldri antall FAKTISK spilt** — en runde markert
|
||||
"Fullført" etter f.eks. 9 av 18 hull viste fortsatt "18". Data fantes
|
||||
allerede (`my_holes_played` sendes for alle runder, uansett status);
|
||||
selve "X/Y hull spilt"-indikatoren var bare feilaktig gatet til
|
||||
`status === "active"`. Rettet til `played < round.holes` (viser
|
||||
avviket uansett status, skjuler seg selv når spilt == planlagt).
|
||||
2. **Putt-avstand-knappenes etiketter** (`round-detail.tsx`) —
|
||||
fem av seks brukte "<Xm"-mønster, den sjette ("8m+") brøt mønsteret
|
||||
med et "+"-suffiks og ble dermed lett oversett. Etikettene endret til
|
||||
konsekvent "X-Ym"-format (kun `label`, ikke `value`/`PuttBucket`-
|
||||
kontrakten — ingen backend-endring). Bekreftet via git-historikk at
|
||||
dette var Claude-forfattet, ikke V0 (brukeren spurte eksplisitt).
|
||||
3. **"Se feed" på dashbordet** — for anonymt/uspesifikt. Endret til "Se
|
||||
venneaktivitet", knyttet direkte til seksjonens egen kontekst
|
||||
("Venner på banen").
|
||||
4. **@-tagging av medspillere i feeden** — vurdert, IKKE bygget denne
|
||||
runden (egen, betydelig funksjon: datamodell + søk + rendering).
|
||||
Anbefalt regel hvis/når den bygges: treff begrenset til faktiske
|
||||
relasjoner (medspillere/venner, ikke hele brukerbasen — personvern),
|
||||
og en ekte søkbar nedtrekksliste ved "@" fremfor fritekst-tolkning
|
||||
(upålitelig med flere like navn).
|
||||
|
||||
**Sendt via V0, mottatt som "tee-cup (9).zip", diffet mot live-treet FØR
|
||||
noe ble tatt inn (samme rutine som alltid — kun `delivery/`-mappen var
|
||||
den faktiske leveransen, resten var V0s egen sandbox-scaffolding, ikke
|
||||
tatt inn):**
|
||||
|
||||
5. **Ny-runde steg 2: "Antall hull" og "Avanserte handicap-
|
||||
innstillinger" flyttet FØR det store 18-knappers formatrutenettet**
|
||||
(var sist, lett oversett bak noe man må skrolle forbi). Ren
|
||||
rekkefølge-endring, ingen ny funksjonalitet.
|
||||
6. **Score-registreringens "detaljer"-steg delt i to synlige seksjoner**
|
||||
("Retning" / "Detaljer per hull") med overskrift+divider, og en ny
|
||||
`ScrollFade`-hjelpekomponent (bunn-fade + nedoverpil, ResizeObserver-
|
||||
drevet, skjuler seg selv ved bunn) som løser at brukere ikke visste
|
||||
de måtte skrolle for å finne Chip/Bunker/Straffeslag/Anywayslag.
|
||||
7. **Anywayslag endret fra tall-rutenett til +/−-stepper** — samme
|
||||
inndatamønster som Chip/Bunker/Straffeslag nå (alle fire er
|
||||
konseptuelt samme type data, brøt tidligere mønster uten grunn).
|
||||
8. **Netto par-markør på Slag-knappene**: en liten oransje prikk (med
|
||||
kontrastring, synlig i BÅDE valgt og uvalgt tilstand) på knappen som
|
||||
tilsvarer spillerens personlige netto par (rå par + `strokesReceived`,
|
||||
allerede tilgjengelig i komponenten), pluss en tekstlig
|
||||
"Ditt netto par (N)"-forklaring over selve rutenettet (tilgjengelighet
|
||||
— ikke avhengig av fargesyn alene). `aria-label` utvidet tilsvarende.
|
||||
9. **Putter-velgeren fikk egen visuell identitet** (oransje venstre-
|
||||
aksentkant + flagg-ikon på tittelraden) for å skille den fra Slag-
|
||||
velgeren, som ellers er nøyaktig samme rutenett-komponent. Selve
|
||||
tallknappenes etablerte grønne "valgt"-stil er UENDRET.
|
||||
|
||||
**Verifisering:** `tsc --noEmit` rent på alle endringer. Full scratch-
|
||||
stack bygget (scratch-DB med alle 63 migrasjoner, egen scratch-MinIO,
|
||||
scratch-API- og scratch-frontend-container, ekte produksjonsbuild av
|
||||
frontend — ikke bare `tsc`). Ekte bruker opprettet og logget inn via
|
||||
magic-link-bypass (`DEV_LOG_MAGIC_LINKS`), egen bane opprettet via ekte
|
||||
API-kall (`POST /personal-courses`) med HCP 24 satt på spilleren for å
|
||||
garantere `strokes_received > 0` på hull 1 — nødvendig for faktisk å se
|
||||
netto-par-markøren i bruk, ikke bare anta at koden er riktig.
|
||||
Browserverifisert (Chrome DevTools MCP, mobil viewport 390×844) i BÅDE
|
||||
lys og mørk modus: ny-runde steg 2 sin nye rekkefølge, seksjonsdeling og
|
||||
scroll-hint (bekreftet `opacity-100`→`opacity-0` ved faktisk scroll til
|
||||
bunn, ikke bare visuell antagelse), netto-par-prikk (både alene og
|
||||
samtidig med "valgt"-tilstand), Putter-aksent, de nye "X-Ym"-putt-
|
||||
avstand-etikettene. Ingen konsollfeil. Scratch-miljøet ryddet opp
|
||||
fullstendig (containere, images, database, rolle), `teecup_db`s
|
||||
rolleoppsett bekreftet uendret.
|
||||
|
||||
Disse må avklares før eller under de relevante fasene:
|
||||
|
||||
1. **Sesjons-secret:** TeeOff lar `PUBLIC_SESSION_SECRET` falle tilbake på
|
||||
|
|
|
|||
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -10386,3 +10386,45 @@ Neste steg:
|
|||
`corepack enable` + `pnpm install --frozen-lockfile` + `pnpm test`.
|
||||
|
||||
**Verifisert:** 30/30 grønt lokalt. Ingen produksjonskode endret.
|
||||
|
||||
71. **Åtte brukerrapporterte UX-funn — "Ny runde" og score-registrering
|
||||
— 2026-08-11, se ADR-062.** Bruker sendte åtte konkrete, skjermbilde-
|
||||
dokumenterte problemer. Delt i to spor: fire rettet direkte (reelle
|
||||
logikkfeil/tekstvalg), fire sendt via V0 (reelt interaksjonsdesign).
|
||||
|
||||
**Direkte:** `round-card.tsx` sin "Hull"-celle viste alltid planlagt
|
||||
antall hull, aldri faktisk spilt, selv for tidlig avsluttede
|
||||
"Fullført"-runder — rettet (`played < round.holes` avgjør nå om
|
||||
avviket vises, uansett status). Putt-avstand-knappenes etiketter
|
||||
endret fra et blandet "<Xm ... 8m+"-mønster til konsekvent
|
||||
"X-Ym" (kun visningstekst, ikke backend-kontrakten) — bekreftet via
|
||||
git-historikk at dette var Claude-forfattet, ikke V0. "Se feed" på
|
||||
dashbordet endret til "Se venneaktivitet". @-tagging av medspillere
|
||||
i feeden vurdert og UTSATT (egen fremtidig funksjon) — anbefalt
|
||||
personvern-begrensning (kun faktiske relasjoner) og ekte søkbar
|
||||
nedtrekksliste (ikke fritekst-tolkning) notert til den runden.
|
||||
|
||||
**Via V0** (`tee-cup (9).zip`, kun `delivery/`-mappen tatt inn, diffet
|
||||
mot live-treet først): ny-runde steg 2 sin "Antall hull"/"Avanserte
|
||||
handicap-innstillinger" flyttet FØR formatrutenettet (var sist, lett
|
||||
oversett). Score-registreringens "detaljer"-steg delt i "Retning"/
|
||||
"Detaljer per hull"-seksjoner + en ny `ScrollFade`-hjelpekomponent
|
||||
(bunn-fade + nedoverpil, ResizeObserver, auto-skjuler ved bunn) --
|
||||
løser at brukere ikke visste de måtte skrolle. Anywayslag endret fra
|
||||
tall-rutenett til +/−-stepper (samme mønster som Chip/Bunker/
|
||||
Straffeslag nå). Netto par-markør (oransje prikk + kontrastring +
|
||||
tekstlig forklaring, tilgjengelig uten fargesyn) på Slag-knappene,
|
||||
kan opptre samtidig med "valgt"-tilstand. Putter-velgeren fikk egen
|
||||
oransje aksent + flagg-ikon for å skille den fra Slag-velgeren.
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent. Full scratch-stack (DB, MinIO,
|
||||
API, frontend — ekte produksjonsbuild). Ekte testbruker, egen bane
|
||||
opprettet via API med HCP 24 for å garantere synlig netto-par-
|
||||
markør. Browserverifisert (mobil viewport, lys+mørk modus): ny
|
||||
rekkefølge, seksjonsdeling, scroll-hint (bekreftet både synlig og
|
||||
auto-skjult), netto-par-prikk alene og kombinert med valgt-tilstand,
|
||||
Putter-aksent, nye putt-avstand-etiketter. Ingen konsollfeil.
|
||||
Scratch-miljøet ryddet opp fullstendig, `teecup_db` bekreftet uendret.
|
||||
|
||||
**Ikke rullet ut ennå** — venter på eksplisitt bekreftelse per
|
||||
CLAUDE.md.
|
||||
|
|
|
|||
|
|
@ -929,7 +929,10 @@ function friendOnCourseInitials(name: string): string {
|
|||
function LiveFriends({ friends }: { friends: ApiFriendOnCourse[] }) {
|
||||
return (
|
||||
<section aria-label="Venner på banen">
|
||||
<SectionHeader title="Venner på banen" action={<SeeAllLink href="/my-feed" label="Se feed" />} />
|
||||
{/* "Se feed" var for anonymt/uspesifikt til å si hva som faktisk
|
||||
venter bak lenken (2026-08-11, brukertilbakemelding) -- knyttet
|
||||
teksten til seksjonens egen kontekst i stedet. */}
|
||||
<SectionHeader title="Venner på banen" action={<SeeAllLink href="/my-feed" label="Se venneaktivitet" />} />
|
||||
{friends.length > 0 ? (
|
||||
<div className="-mx-4 flex snap-x gap-3 overflow-x-auto px-4 pb-1 sm:mx-0 sm:px-0">
|
||||
{friends.map((friend, i) => {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,30 @@ export function Step2Format() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Antall hull -- et grunnleggende oppsettsvalg, derfor FØR det store
|
||||
formatrutenettet, ikke etter (2026-08-11, brukertilbakemelding: ble
|
||||
tidligere lett oversett fordi den lå sist, bak et 18-knappers
|
||||
rutenett man må skrolle forbi først). */}
|
||||
<section className="flex flex-col gap-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Antall hull</h3>
|
||||
<div className="max-w-xs">
|
||||
<Segmented
|
||||
label="Antall hull"
|
||||
value={String(state.numHoles) as "9" | "18"}
|
||||
onChange={(v) => patch({ numHoles: v === "9" ? 9 : 18 })}
|
||||
options={[
|
||||
{ value: "9", label: "9 hull" },
|
||||
{ value: "18", label: "18 hull" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Avanserte HCP-innstillinger (skjult for scramble mot solo) -- fortsatt
|
||||
kollapset som standard, KUN rekkefølgen flyttet opp, samme grunn som
|
||||
Antall hull over. */}
|
||||
{!svs ? <AdvancedHcp /> : null}
|
||||
|
||||
{/* Format grid */}
|
||||
<section className="flex flex-col gap-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Spilleform</h3>
|
||||
|
|
@ -86,25 +110,6 @@ export function Step2Format() {
|
|||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* Advanced HCP (hidden for scramble vs solo) */}
|
||||
{!svs ? <AdvancedHcp /> : null}
|
||||
|
||||
{/* Number of holes */}
|
||||
<section className="flex flex-col gap-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Antall hull</h3>
|
||||
<div className="max-w-xs">
|
||||
<Segmented
|
||||
label="Antall hull"
|
||||
value={String(state.numHoles) as "9" | "18"}
|
||||
onChange={(v) => patch({ numHoles: v === "9" ? 9 : 18 })}
|
||||
options={[
|
||||
{ value: "9", label: "9 hull" },
|
||||
{ value: "18", label: "18 hull" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,13 @@ export function RoundCard({ round }: { round: Round }) {
|
|||
</span>
|
||||
)}
|
||||
</span>
|
||||
{round.status === "active" && (
|
||||
{/* Vis "spilt av planlagt" når det faktisk AVVIKER fra planen --
|
||||
ikke bare mens runden pågår. En runde markert Fullført etter
|
||||
f.eks. 9 av 18 planlagte hull skal fortsatt vise det avviket
|
||||
her, ikke bare stille bytte til status-teksten (2026-08-11,
|
||||
reelt funn: "Hull"-cellen under viser uansett kun PLANLAGT
|
||||
antall, aldri hvor mange som faktisk ble spilt). */}
|
||||
{played < round.holes && (
|
||||
<span className="text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
{played}/{round.holes} hull spilt
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1929,7 +1929,8 @@ function ScoringWizard({
|
|||
</p>
|
||||
)}
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-5 sm:p-6">
|
||||
<main className="flex min-h-0 flex-1 flex-col">
|
||||
<ScrollFade className="flex-1 overflow-y-auto p-5 sm:p-6">
|
||||
<div className="mx-auto flex max-w-sm flex-col gap-6">
|
||||
{step === "strokes" && (
|
||||
<>
|
||||
|
|
@ -1941,6 +1942,7 @@ function ScoringWizard({
|
|||
expandLabel="10+"
|
||||
parValue={hole.par}
|
||||
showGolfTerms
|
||||
strokesReceived={strokesReceived}
|
||||
onChange={(v) => onUpdate({ strokes: v, played: true, pickedUp: false })}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
|
@ -1981,6 +1983,7 @@ function ScoringWizard({
|
|||
expandValues={[7, 8, 9, 10]}
|
||||
expandLabel="7+"
|
||||
maxValue={stat.strokes ?? undefined}
|
||||
accent="orange"
|
||||
onChange={(v) => onUpdate({ putts: v })}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
|
@ -1989,12 +1992,18 @@ function ScoringWizard({
|
|||
{step === "puttDistance" && (
|
||||
<ChoiceRow
|
||||
label="Avstand første putt"
|
||||
// Konsekvent "X-Ym"-mønster på ALLE seks, kun den siste
|
||||
// (uunngåelig ubegrenset) bruker "+" -- 2026-08-11, reelt
|
||||
// UX-funn: den forrige "<Xm ... 8m+"-blandingen brøt
|
||||
// mønsteret på akkurat siste knapp, som da ble lett å
|
||||
// overse. `value` uendret (PuttBucket/backend-kontrakt),
|
||||
// kun visningsteksten er ny.
|
||||
options={[
|
||||
{ value: "<1m", label: "<1m" },
|
||||
{ value: "<2m", label: "<2m" },
|
||||
{ value: "<3m", label: "<3m" },
|
||||
{ value: "<5m", label: "<5m" },
|
||||
{ value: "<8m", label: "<8m" },
|
||||
{ value: "<1m", label: "0-1m" },
|
||||
{ value: "<2m", label: "1-2m" },
|
||||
{ value: "<3m", label: "2-3m" },
|
||||
{ value: "<5m", label: "3-5m" },
|
||||
{ value: "<8m", label: "5-8m" },
|
||||
{ value: "8m+", label: "8m+" },
|
||||
]}
|
||||
value={stat.firstPuttBucket}
|
||||
|
|
@ -2022,6 +2031,12 @@ function ScoringWizard({
|
|||
variant="inline"
|
||||
/>
|
||||
|
||||
{/* Seksjon 1 -- retning (2026-08-11): retningskorsene utgjør nå
|
||||
en tydelig egen gruppe med overskrift, adskilt fra tellerne
|
||||
under, så det er visuelt klart at skjermen består av to
|
||||
ulike typer inndata (brukertilbakemelding: umulig å vite at
|
||||
man måtte skrolle for å finne resten). */}
|
||||
<WizardSection title="Retning">
|
||||
{hole.par !== 3 && (
|
||||
<DirectionCross
|
||||
label="Utslag"
|
||||
|
|
@ -2041,34 +2056,40 @@ function ScoringWizard({
|
|||
onChange={(v) => onUpdate({ approach: v as Approach })}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</WizardSection>
|
||||
|
||||
{/* Alltid stablet, ALDRI et sm:grid-cols-3-sprang: denne wizard-
|
||||
seksjonen er fast begrenset til max-w-sm (384px, se
|
||||
containeren over) uansett hvor bred selve VIEWPORTET er --
|
||||
et viewport-basert grid-cols-3-brudd ved 640px trigges
|
||||
derfor på skjermer godt over 640px bredde mens
|
||||
innholdskolonnen fortsatt kun er 384px, som får de tre
|
||||
stepperne (hver ~190px minimum pga. faste 44px-knapper) til
|
||||
å overlappe hverandre i stedet for å faktisk ha plass. */}
|
||||
{/* Seksjon 2 -- detaljer per hull (2026-08-11): egen overskrift
|
||||
+ divider fra "Retning" over. Alle FIRE tellerne (Chip/
|
||||
Bunker/Straffeslag/Anywayslag) er konseptuelt samme type
|
||||
data (antall småhendelser per hull, typisk 0-3) og bruker nå
|
||||
ETT konsistent +/--stepper-mønster -- Anywayslag var
|
||||
tidligere det eneste tall-rutenettet, som brøt mønsteret
|
||||
uten grunn. Alltid stablet, ALDRI et sm:grid-cols-3-sprang:
|
||||
denne wizard-seksjonen er fast begrenset til max-w-sm
|
||||
(384px, se containeren over) uansett hvor bred selve
|
||||
VIEWPORTET er -- et viewport-basert grid-cols-3-brudd ved
|
||||
640px trigges derfor på skjermer godt over 640px bredde mens
|
||||
innholdskolonnen fortsatt kun er 384px, som får stepperne
|
||||
(hver ~190px minimum pga. faste 44px-knapper) til å
|
||||
overlappe hverandre i stedet for å faktisk ha plass. */}
|
||||
<WizardSection title="Detaljer per hull">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<Stepper label="Chip" value={stat.chip} onChange={(v) => onUpdate({ chip: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||
<Stepper label="Bunker" value={stat.bunker} onChange={(v) => onUpdate({ bunker: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||
<Stepper label="Straffeslag" value={stat.penalty} onChange={(v) => onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||
</div>
|
||||
|
||||
<NumberPicker
|
||||
<Stepper
|
||||
label="Anywayslag"
|
||||
value={stat.anywayStrokes}
|
||||
directValues={[0, 1, 2, 3, 4, 5, 6]}
|
||||
expandValues={[7, 8, 9, 10]}
|
||||
expandLabel="7+"
|
||||
maxValue={stat.strokes ?? undefined}
|
||||
value={stat.anywayStrokes ?? 0}
|
||||
onChange={(v) => onUpdate({ anywayStrokes: v })}
|
||||
readOnly={readOnly}
|
||||
max={stat.strokes ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</WizardSection>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScrollFade>
|
||||
</main>
|
||||
|
||||
<footer className="grid shrink-0 grid-cols-2 gap-3 border-t border-border p-4">
|
||||
|
|
@ -6538,6 +6559,63 @@ function SideScoreWizard({
|
|||
)
|
||||
}
|
||||
|
||||
// --- Wizard-seksjon (2026-08-11) -------------------------------------------
|
||||
// Liten seksjonsoverskrift + hårfin divider, brukt til å dele detalj-steget
|
||||
// i tydelige grupper ("Retning" vs. "Detaljer per hull"). Rent presentasjon
|
||||
// -- ingen egen state, ingen ny farge (gjenbruker border/muted-tokens).
|
||||
function WizardSection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 border-t border-border pt-5 first:border-t-0 first:pt-0">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{title}</h3>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// --- "Mer innhold under"-hint (2026-08-11) ---------------------------------
|
||||
// Legger en myk bunn-fade + nedoverpil over et scrollbart område SÅ LENGE det
|
||||
// finnes mer innhold under fold-linjen -- og skjuler seg av seg selv idet man
|
||||
// har scrollet helt ned. Fanger både resize og innholdsendringer (ulikt
|
||||
// stat_level/par gir ulik høyde) via en ResizeObserver på selve scrollboksen.
|
||||
function ScrollFade({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const update = () => {
|
||||
// 4px slingring: unngår at en sub-piksel-rest holder hintet i live
|
||||
// når man i praksis ER nederst.
|
||||
setHasMore(el.scrollTop + el.clientHeight < el.scrollHeight - 4)
|
||||
}
|
||||
update()
|
||||
el.addEventListener("scroll", update, { passive: true })
|
||||
const ro = new ResizeObserver(update)
|
||||
ro.observe(el)
|
||||
return () => {
|
||||
el.removeEventListener("scroll", update)
|
||||
ro.disconnect()
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div ref={ref} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
{/* Rent dekorativt -- selve scrollingen er fullt mulig uten dette. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 bottom-0 flex h-16 items-end justify-center bg-gradient-to-t from-background to-transparent pb-1.5 transition-opacity duration-200",
|
||||
hasMore ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="size-5 animate-bounce text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NumberPicker({
|
||||
label,
|
||||
value,
|
||||
|
|
@ -6547,6 +6625,8 @@ function NumberPicker({
|
|||
parValue,
|
||||
showGolfTerms,
|
||||
maxValue,
|
||||
strokesReceived,
|
||||
accent,
|
||||
onChange,
|
||||
readOnly,
|
||||
}: {
|
||||
|
|
@ -6565,6 +6645,17 @@ function NumberPicker({
|
|||
// antall slag registrert på hullet) -- ingen vits i å tilby et tall som
|
||||
// uansett ville vært selvmotsigende.
|
||||
maxValue?: number
|
||||
// Spillerens tildelte handicap-slag PÅ DETTE hullet (2026-08-11). Når satt
|
||||
// (og showGolfTerms er på, dvs. Slag-velgeren), markeres knappen som
|
||||
// tilsvarer spillerens PERSONLIGE netto par (rå par + mottatte slag) med en
|
||||
// liten, diskret prikk -- tydelig forskjellig fra "valgt"-tilstanden, og de
|
||||
// to kan opptre samtidig på samme knapp.
|
||||
strokesReceived?: number | null
|
||||
// Valgfri egen aksent for seksjonen (2026-08-11, brukes til å gi Putter-
|
||||
// velgeren en distinkt visuell identitet så den ikke forveksles med Slag-
|
||||
// velgeren). Rører IKKE selve tallknappene -- kun tittelrad/ikon. Gjenbruker
|
||||
// eksisterende tokens.
|
||||
accent?: "primary" | "orange"
|
||||
onChange: (value: number) => void
|
||||
readOnly: boolean
|
||||
}) {
|
||||
|
|
@ -6578,14 +6669,44 @@ function NumberPicker({
|
|||
|
||||
const visibleValues = showExpanded ? [...cappedDirect, ...cappedExpand] : cappedDirect
|
||||
|
||||
// Spillerens netto par for hullet = rå par + tildelte slag. Kun beregnet
|
||||
// når vi faktisk har begge (Slag-velgeren med kjent strokesReceived).
|
||||
const netPar =
|
||||
parValue !== undefined && strokesReceived !== null && strokesReceived !== undefined
|
||||
? parValue + strokesReceived
|
||||
: null
|
||||
|
||||
// Putter-velgeren får en egen aksent (oransje) og et eget ikon på
|
||||
// tittelraden, så den ikke er til å forveksle med Slag-velgeren -- som
|
||||
// ellers er nøyaktig samme rutenett. Kun tittelraden/venstre aksentbånd
|
||||
// endres; selve tallknappene beholder den etablerte grønne "valgt"-stilen.
|
||||
const isOrange = accent === "orange"
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-base font-semibold text-foreground">{label}</span>
|
||||
<div className={cn("flex flex-col gap-2", isOrange && "rounded-2xl border-l-4 border-brand-orange bg-brand-orange/5 py-3 pl-3 pr-1")}>
|
||||
<span className="flex items-center gap-2 text-base font-semibold text-foreground">
|
||||
{isOrange && (
|
||||
<span aria-hidden="true" className="flex size-6 items-center justify-center rounded-lg bg-brand-orange text-brand-orange-foreground">
|
||||
<Flag className="size-4" strokeWidth={2.5} />
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</span>
|
||||
{/* Netto par-forklaring: gjør prikken selvforklarende uten å stole på
|
||||
fargesyn alene (tilgjengelighet -- sol/utendørs). Vises kun når det
|
||||
faktisk finnes en netto par-markør å forklare. */}
|
||||
{netPar !== null && (
|
||||
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-brand-orange ring-2 ring-brand-orange/25" />
|
||||
Ditt netto par ({netPar})
|
||||
</span>
|
||||
)}
|
||||
{/* Numpad: tre kolonner, knappene fyller bredden -- store trykkflater
|
||||
for bruk utendørs, ofte med sol på skjermen (V0-runde 2026-07-24). */}
|
||||
<div className="grid max-w-sm grid-cols-3 gap-2.5">
|
||||
{visibleValues.map((n) => {
|
||||
const selected = value === n
|
||||
const isNetPar = netPar !== null && n === netPar
|
||||
const term = showGolfTerms && parValue !== undefined ? golfTermForScore(n, parValue) : n === parValue ? "par" : null
|
||||
return (
|
||||
<button
|
||||
|
|
@ -6593,16 +6714,32 @@ function NumberPicker({
|
|||
type="button"
|
||||
disabled={readOnly}
|
||||
onClick={() => onChange(n)}
|
||||
aria-label={term ? `${label} ${n}, ${term}` : `${label} ${n}`}
|
||||
aria-label={term ? `${label} ${n}, ${term}${isNetPar ? ", ditt netto par" : ""}` : `${label} ${n}${isNetPar ? ", ditt netto par" : ""}`}
|
||||
aria-pressed={selected}
|
||||
className={cn(
|
||||
"flex h-16 flex-col items-center justify-center gap-0 rounded-2xl border text-2xl font-extrabold leading-none tabular-nums transition-colors disabled:opacity-100",
|
||||
"relative flex h-16 flex-col items-center justify-center gap-0 rounded-2xl border text-2xl font-extrabold leading-none tabular-nums transition-colors disabled:opacity-100",
|
||||
selected
|
||||
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
||||
: "border-border bg-card text-foreground hover:bg-accent/50",
|
||||
// Netto par-knapp som IKKE er valgt: en subtil oransje ring +
|
||||
// hint-bakgrunn, tydelig annerledes enn den fylte grønne
|
||||
// "valgt"-tilstanden. Prikken øverst til høyre er signalet som
|
||||
// holder også når knappen ER valgt (grønn fyll under).
|
||||
isNetPar && !selected && "border-brand-orange/60 bg-brand-orange/5",
|
||||
readOnly && !selected && "opacity-40",
|
||||
)}
|
||||
>
|
||||
{isNetPar && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"absolute right-1.5 top-1.5 size-2.5 rounded-full bg-brand-orange",
|
||||
// Kontrast-ring så prikken leses uansett om knappen er
|
||||
// grønn (valgt) eller lys (uvalgt).
|
||||
selected ? "ring-2 ring-primary" : "ring-2 ring-card",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span>{n}</span>
|
||||
{term && (
|
||||
<span className={cn("text-[11px] font-bold leading-none", selected ? "text-primary-foreground/80" : "text-muted-foreground")}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue