Å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
|
komponentfiler som ville krevd en helt annen testtilnærming enn ren
|
||||||
lib-import — egen, fremtidig vurdering).
|
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:
|
Disse må avklares før eller under de relevante fasene:
|
||||||
|
|
||||||
1. **Sesjons-secret:** TeeOff lar `PUBLIC_SESSION_SECRET` falle tilbake på
|
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`.
|
`corepack enable` + `pnpm install --frozen-lockfile` + `pnpm test`.
|
||||||
|
|
||||||
**Verifisert:** 30/30 grønt lokalt. Ingen produksjonskode endret.
|
**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[] }) {
|
function LiveFriends({ friends }: { friends: ApiFriendOnCourse[] }) {
|
||||||
return (
|
return (
|
||||||
<section aria-label="Venner på banen">
|
<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 ? (
|
{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">
|
<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) => {
|
{friends.map((friend, i) => {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,30 @@ export function Step2Format() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 */}
|
{/* Format grid */}
|
||||||
<section className="flex flex-col gap-2">
|
<section className="flex flex-col gap-2">
|
||||||
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Spilleform</h3>
|
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Spilleform</h3>
|
||||||
|
|
@ -86,25 +110,6 @@ export function Step2Format() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,13 @@ export function RoundCard({ round }: { round: Round }) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</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">
|
<span className="text-xs font-semibold tabular-nums text-muted-foreground">
|
||||||
{played}/{round.holes} hull spilt
|
{played}/{round.holes} hull spilt
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1929,7 +1929,8 @@ function ScoringWizard({
|
||||||
</p>
|
</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">
|
<div className="mx-auto flex max-w-sm flex-col gap-6">
|
||||||
{step === "strokes" && (
|
{step === "strokes" && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -1941,6 +1942,7 @@ function ScoringWizard({
|
||||||
expandLabel="10+"
|
expandLabel="10+"
|
||||||
parValue={hole.par}
|
parValue={hole.par}
|
||||||
showGolfTerms
|
showGolfTerms
|
||||||
|
strokesReceived={strokesReceived}
|
||||||
onChange={(v) => onUpdate({ strokes: v, played: true, pickedUp: false })}
|
onChange={(v) => onUpdate({ strokes: v, played: true, pickedUp: false })}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1981,6 +1983,7 @@ function ScoringWizard({
|
||||||
expandValues={[7, 8, 9, 10]}
|
expandValues={[7, 8, 9, 10]}
|
||||||
expandLabel="7+"
|
expandLabel="7+"
|
||||||
maxValue={stat.strokes ?? undefined}
|
maxValue={stat.strokes ?? undefined}
|
||||||
|
accent="orange"
|
||||||
onChange={(v) => onUpdate({ putts: v })}
|
onChange={(v) => onUpdate({ putts: v })}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1989,12 +1992,18 @@ function ScoringWizard({
|
||||||
{step === "puttDistance" && (
|
{step === "puttDistance" && (
|
||||||
<ChoiceRow
|
<ChoiceRow
|
||||||
label="Avstand første putt"
|
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={[
|
options={[
|
||||||
{ value: "<1m", label: "<1m" },
|
{ value: "<1m", label: "0-1m" },
|
||||||
{ value: "<2m", label: "<2m" },
|
{ value: "<2m", label: "1-2m" },
|
||||||
{ value: "<3m", label: "<3m" },
|
{ value: "<3m", label: "2-3m" },
|
||||||
{ value: "<5m", label: "<5m" },
|
{ value: "<5m", label: "3-5m" },
|
||||||
{ value: "<8m", label: "<8m" },
|
{ value: "<8m", label: "5-8m" },
|
||||||
{ value: "8m+", label: "8m+" },
|
{ value: "8m+", label: "8m+" },
|
||||||
]}
|
]}
|
||||||
value={stat.firstPuttBucket}
|
value={stat.firstPuttBucket}
|
||||||
|
|
@ -2022,6 +2031,12 @@ function ScoringWizard({
|
||||||
variant="inline"
|
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 && (
|
{hole.par !== 3 && (
|
||||||
<DirectionCross
|
<DirectionCross
|
||||||
label="Utslag"
|
label="Utslag"
|
||||||
|
|
@ -2041,34 +2056,40 @@ function ScoringWizard({
|
||||||
onChange={(v) => onUpdate({ approach: v as Approach })}
|
onChange={(v) => onUpdate({ approach: v as Approach })}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
</WizardSection>
|
||||||
|
|
||||||
{/* Alltid stablet, ALDRI et sm:grid-cols-3-sprang: denne wizard-
|
{/* Seksjon 2 -- detaljer per hull (2026-08-11): egen overskrift
|
||||||
seksjonen er fast begrenset til max-w-sm (384px, se
|
+ divider fra "Retning" over. Alle FIRE tellerne (Chip/
|
||||||
containeren over) uansett hvor bred selve VIEWPORTET er --
|
Bunker/Straffeslag/Anywayslag) er konseptuelt samme type
|
||||||
et viewport-basert grid-cols-3-brudd ved 640px trigges
|
data (antall småhendelser per hull, typisk 0-3) og bruker nå
|
||||||
derfor på skjermer godt over 640px bredde mens
|
ETT konsistent +/--stepper-mønster -- Anywayslag var
|
||||||
innholdskolonnen fortsatt kun er 384px, som får de tre
|
tidligere det eneste tall-rutenettet, som brøt mønsteret
|
||||||
stepperne (hver ~190px minimum pga. faste 44px-knapper) til
|
uten grunn. Alltid stablet, ALDRI et sm:grid-cols-3-sprang:
|
||||||
å overlappe hverandre i stedet for å faktisk ha plass. */}
|
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">
|
<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="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="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} />
|
<Stepper label="Straffeslag" value={stat.penalty} onChange={(v) => onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||||
</div>
|
<Stepper
|
||||||
|
|
||||||
<NumberPicker
|
|
||||||
label="Anywayslag"
|
label="Anywayslag"
|
||||||
value={stat.anywayStrokes}
|
value={stat.anywayStrokes ?? 0}
|
||||||
directValues={[0, 1, 2, 3, 4, 5, 6]}
|
|
||||||
expandValues={[7, 8, 9, 10]}
|
|
||||||
expandLabel="7+"
|
|
||||||
maxValue={stat.strokes ?? undefined}
|
|
||||||
onChange={(v) => onUpdate({ anywayStrokes: v })}
|
onChange={(v) => onUpdate({ anywayStrokes: v })}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
max={stat.strokes ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</WizardSection>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollFade>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="grid shrink-0 grid-cols-2 gap-3 border-t border-border p-4">
|
<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({
|
function NumberPicker({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
|
@ -6547,6 +6625,8 @@ function NumberPicker({
|
||||||
parValue,
|
parValue,
|
||||||
showGolfTerms,
|
showGolfTerms,
|
||||||
maxValue,
|
maxValue,
|
||||||
|
strokesReceived,
|
||||||
|
accent,
|
||||||
onChange,
|
onChange,
|
||||||
readOnly,
|
readOnly,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -6565,6 +6645,17 @@ function NumberPicker({
|
||||||
// antall slag registrert på hullet) -- ingen vits i å tilby et tall som
|
// antall slag registrert på hullet) -- ingen vits i å tilby et tall som
|
||||||
// uansett ville vært selvmotsigende.
|
// uansett ville vært selvmotsigende.
|
||||||
maxValue?: number
|
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
|
onChange: (value: number) => void
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -6578,14 +6669,44 @@ function NumberPicker({
|
||||||
|
|
||||||
const visibleValues = showExpanded ? [...cappedDirect, ...cappedExpand] : cappedDirect
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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="text-base font-semibold text-foreground">{label}</span>
|
<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
|
{/* Numpad: tre kolonner, knappene fyller bredden -- store trykkflater
|
||||||
for bruk utendørs, ofte med sol på skjermen (V0-runde 2026-07-24). */}
|
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">
|
<div className="grid max-w-sm grid-cols-3 gap-2.5">
|
||||||
{visibleValues.map((n) => {
|
{visibleValues.map((n) => {
|
||||||
const selected = value === n
|
const selected = value === n
|
||||||
|
const isNetPar = netPar !== null && n === netPar
|
||||||
const term = showGolfTerms && parValue !== undefined ? golfTermForScore(n, parValue) : n === parValue ? "par" : null
|
const term = showGolfTerms && parValue !== undefined ? golfTermForScore(n, parValue) : n === parValue ? "par" : null
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -6593,16 +6714,32 @@ function NumberPicker({
|
||||||
type="button"
|
type="button"
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onClick={() => onChange(n)}
|
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}
|
aria-pressed={selected}
|
||||||
className={cn(
|
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
|
selected
|
||||||
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
||||||
: "border-border bg-card text-foreground hover:bg-accent/50",
|
: "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",
|
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>
|
<span>{n}</span>
|
||||||
{term && (
|
{term && (
|
||||||
<span className={cn("text-[11px] font-bold leading-none", selected ? "text-primary-foreground/80" : "text-muted-foreground")}>
|
<span className={cn("text-[11px] font-bold leading-none", selected ? "text-primary-foreground/80" : "text-muted-foreground")}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue