Compliance-pass shipped and live. Summary:

"Deg Deg" fixed — the name is now always the real display name; the "Deg" badge alone marks your own row. This also fixed the same problem in the wizard header/context chips/footer, which had no badge at all and were showing "Deg" instead of the roster-context full name the spec requires.
Score button now follows the golfscore language — reused ScoreMark's exact primary/brand-orange fill-vs-tint rules instead of an unconditional green circle.
13 smaller gaps fixed — 2 missing tabular-nums, 11 tap targets brought up to the 44px floor.
Spec's §1 (scorecard-as-grid) deliberately left out — that's a bigger structural call, not something to slip in under a "fix known bugs" pass.
Typechecked build was clean, both containers redeployed and booted cleanly, /health//dashboard → 200, teeoff.no unaffected. Docs updated (CLAUDE.md status + autoritative kilder, FEATURE_BACKLOG.md pointer).

Same honest caveat as the rest of this week's hand-coded work: no real browser interaction test — worth a manual look, especially the score button's shape change at the 56px size.
This commit is contained in:
Erol Haagenrud 2026-07-27 10:32:51 +02:00
parent 6bfd986964
commit a11c638fda
3 changed files with 159 additions and 33 deletions

View file

@ -19,6 +19,16 @@ Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber.
turneringsformatene som ennå ikke er designet (Københavner/High-low-high/
Robbins/Try all, se FEATURE_BACKLOG.md), siden disse dokumentene trolig
dekker akkurat de reglene som trengs der.
- **Design-dokumentasjon, to filer med bevisst motsatt retning (2026-07-27):**
`DESIGN_SYSTEM.md` (innsjekket i git) er DESKRIPTIV — hva som faktisk ER i
koden i dag (farger/typografi/komponentmønstre/golfscore-språket), avledet
direkte fra `globals.css`/`components/ui/*`. `teecup-scorekort-og-entry-
spec.md` (lastet opp av brukeren til prosjektroten, IKKE innsjekket i git)
er PRESKRIPTIV — hva scorekort-/score-entry-skjermene BØR bli, basert på en
sammenligning mot fem etablerte golf-apper. Der de to avviker vinner
kildekoden (DESIGN_SYSTEM.md beskriver den), men spec-dokumentets §3
"Compliance-pass" er en konkret sjekkliste over kjente avvik — les den FØR
videre visuelt arbeid på scorekort-/score-entry-skjermene.
## Sikkerhetsregler (ufravikelige)
- Rør ALDRI `teeoff`-databasen eller den ekte `teecup_db` uten at brukeren
@ -3495,6 +3505,77 @@ Ferdig og verifisert:
ingen migrasjon, `docker compose up -d --build teecup_frontend`
(gjenskapte også `teecup_api` som vanlig bivirkning). Begge containere
boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
- **`DESIGN_SYSTEM.md` opprettet (2026-07-27):** brukeren ba om en ny fil
som viser designsystemet arbeidet tar utgangspunkt i. Skrevet fra bunnen,
DESKRIPTIVT (hva som ER i koden, ikke et mål) — grunnet direkte i
`globals.css`/`components/ui/*.tsx` og etablerte tvers-fil-mønstre:
fargetoken (inkl. OKLCH-opprinnelsen til `primary`/`brand-orange` fra
ADR-009/016), typografi, avstand/hjørner/lag (inkl. "én sammenhengende
liste med `divide-y`, ikke separate kort"-regelen fra 2026-07-26s
leaderboard-fiks), lokale komponentmønstre (NumberPicker/Stepper/
ChoiceRow/DirectionCross — bevisst per-fil, ikke delte importer),
golfscore-språket (`ScoreMark`/`ToParMark`/`PointsMark`/`HoleMark`),
ikonografi, tilbakemelding/tilstander, og navneformat (pekt til CLAUDE.md).
- **`teecup-scorekort-og-entry-spec.md` lest og evaluert, delvis BYGGET SOM
COMPLIANCE-PASS SAMME DAG (2026-07-27):** brukeren lastet opp et
PRESKRIPTIVT motstykke til DESIGN_SYSTEM.md (5-app-sammenligning: Golf
GameBook/Golf Pad/Golfshot/Hole 19/18Birdies) og spurte om det ga mening
og kunne forbedre TeeCup. Vurdert som reelt verdifullt — sylskarpeste nye
innsikt: "grønt-på-grønt"-diagnosen (§0.2) forklarer noe av "fortsatt
rotete"-følelsen fra v1-v3-rundene bedre enn tetthet alene: `primary`
(grønn) var brukt om hverandre for BÅDE "aktiv tilstand" og "generisk
fylt/positiv", uten at begge betydde det samme sted. Foreslo (og
brukeren bekreftet) å fikse spec-dokumentets §3 "Compliance-pass" FØR den
større strukturelle §1-omleggingen (scorekort som grid) vurderes som egen,
senere runde.
**To av sjekklistens fem punkter var KONKRETE, VERIFISERBARE bugs, bekreftet
direkte i koden (ikke antatt fra spec-teksten alene) FØR de ble fikset:**
1. **"Deg Deg"-duplikat:** `playerLabel()` (`round-detail.tsx`) erstattet
tidligere selve navnet med "Deg" for viewer-relativ egen rad, OG en
separat `<Badge>Deg</Badge>` sto ved siden av samme sted (spillerkort,
scoringslisten) — bekreftet duplikat, matchet et tidligere skjermbilde
brukeren delte. **Fikset:** `playerLabel()` returnerer nå alltid det
faktiske `display_name` (aldri "Deg") — Badge-en er nå ENESTE
selv-indikator. Dette retter samtidig et videre, ikke tidligere flagget
avvik: spec-dokumentet krever eksplisitt "roster-kontekst → fullt navn"
for BÅDE scorekort-gridet og score-entry-headeren — veiviserens header/
kontekst-rad/"Neste: {navn}"/fullført-banneret viste tidligere "Deg" i
stedet for et fullt navn der også, uten noen badge til å disambiguere
(reelt forvirrende på en delt telefon som sendes rundt en flight).
Det nå overflødige `rawName`-feltet (var identisk med `name` etter
fiksen) fjernet, tre kallsteder oppdatert.
2. **Score-knappen fulgte ikke `§Golfscore-språket`:** den store runde
knappen i den kompakte scoringslisten (`round-detail.tsx`, bygget i
v2-runden) var `rounded-full`+grønn UANSETT om resultatet var under,
over eller på par — bogey og eagle så identiske ut. **Fikset:** ny
`scoreMarkClasses(diff)`-hjelpefunksjon som gjenbruker EKSAKT samme
`primary`/`brand-orange`-fargespråk og fylt-vs-border+10%-tint-omfangs-
regel som `ScoreMark` i `round-scorecard.tsx` (bekreftet ved å lese
`ScoreMark` sin kildekode direkte, ikke gjettet) — sirkel under par,
nøytral sirkel på par, `rounded-2xl` (bevisst mildere enn scorekortets
`rounded-[4px]`, for å matche denne skjermens øvrige 56px-trykkflate-
avrunding) over par, fylt ved 2+ slag fra par.
**Resten av sjekklisten (44px-trykkgulv, `tabular-nums`) auditert
systematisk mot AKKURAT denne filen** (samme fil brukerens skjermbilder
viste) — 2 manglende `tabular-nums` (HCP/tildelte slag i spillerkortet)
og 11 knapper/lenker under 44px (fane-bryteren `min-h-10`→`min-h-11`,
`EditRoundPanel`s lukk-ikon `size-8`→`size-11`, feilside-tilbakelenken,
og åtte knapper i bane-bytte-/legg-til-medspiller-skjemaene) rettet.
**Bevisst UTENFOR omfang denne runden:** spec-dokumentets §1 (scorekort
som fullt grid, celle åpner veiviseren) — en STØRRE strukturell endring
som fortjener et eget, bevisst ja fra brukeren, ikke bygget stille inn i
en "fiks kjente bugs"-runde.
**Verifisert:** ekte typesjekket produksjonsbuild (samme `Dockerfile` som
deployes) kompilerte rent, alle 24 ruter listet. **Samme ærlige
begrensning som ALLE håndkodede runder denne uken:** ingen ekte
nettleser-interaksjonstest av det faktiske visuelle resultatet (kun kode-
lesing + build-verifisering + bevisst gjenbruk av en allerede lest,
eksisterende komponents fargespråk for å holde risikoen lav).
**Rullet ut live 2026-07-27**, bruker bekreftet eksplisitt: ingen
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
også `teecup_api` som vanlig bivirkning, ingen backend-kode rørt). Begge
containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
upåvirket.
Neste steg:
0a. **Spillerliste-redesign — LIVE, men ikke nettleser-testet

View file

@ -1326,6 +1326,18 @@ ARCHITECTURE_DECISIONS.md (ADR-033-oppdatering 2026-07-26), full
verifiserings-/utrullingsdetalj i CLAUDE.md sin statuslogg — ikke
duplisert her.
**Oppfølging 2026-07-27 — `teecup-scorekort-og-entry-spec.md` (nytt,
brukeropplastet PRESKRIPTIVT motstykke til `DESIGN_SYSTEM.md`, basert på
en 5-app-sammenligning) evaluert, §3 "Compliance-pass" delvis BYGGET SAMME
DAG — ✅ LIVE:** "Deg Deg"-badge-duplikat fjernet (`playerLabel()` viser nå
alltid ekte navn), scoringslistens score-knapp gjort om til å følge
`§Golfscore-språket` (gjenbruker `ScoreMark`s fargespråk fra
`round-scorecard.tsx`), pluss et system­atisk `tabular-nums`/44px-
trykkgulv-avvik-audit fikset (13 steder). Spec-dokumentets §1 (scorekort
som fullt grid, egen skjerm) bevisst IKKE bygget — større strukturell
endring, egen fremtidig runde. Full detalj i CLAUDE.md sin statuslogg
(2026-07-27) — ikke duplisert her.
---
## En tredje (informasjons-)farge til designet — 🧠 DRØFTET 2026-07-25, IKKE besluttet

View file

@ -52,12 +52,10 @@ type PuttBucket = "<1m" | "<2m" | "<3m" | "<5m" | "<8m" | "8m+"
type Player = {
id: string
// Visningsnavn -- "Deg" for spilleren SOM SER PÅ (viewer-relativt, se
// playerLabel()), IKKE nødvendigvis den faktiske display_name.
// Alltid den faktiske display_name (2026-07-27: fjernet "Deg"-erstatning
// her -- se playerLabel() -- brøt "roster-kontekst → fullt navn" og ga en
// "Deg Deg"-duplikat der en egen Badge også viste "Deg" ved siden av).
name: string
// Den faktiske, urørte display_name -- ALDRI "Deg". Brukt til å
// forhåndsutfylle "Rediger"-flytens navnefelt for en gjest (2026-07-26).
rawName: string
userId: string | null
guestEmail: string | null
isOwner: boolean
@ -264,11 +262,17 @@ function statToPatchBody(s: HoleStat) {
}
}
// Viewer-relativt (ADR-036 fase 3-utvidelsen, 2026-07-26): "Deg" skal vises
// for DEN SOM SER PÅ, ikke alltid runde-eieren -- en lenket medspiller som
// åpner runden skal se seg selv som "Deg" og eieren under sitt eget navn.
function playerLabel(p: ApiParticipant, viewerId: string | null): string {
return viewerId !== null && p.user_id === viewerId ? "Deg" : p.display_name
// Alltid det faktiske visningsnavnet -- ALDRI "Deg" (2026-07-27, compliance-
// pass mot teecup-scorekort-og-entry-spec.md). "Deg" var tidligere en
// viewer-relativ erstatning for selve navnet, men det produserte en ekte
// "Deg Deg"-duplikat der navnet OGSÅ sto ved siden av en egen "Deg"-badge
// (spillerkort, scoringslisten) -- og brøt spesifikasjonens eksplisitte
// "roster-kontekst → fullt navn"-regel i skjermer uten noen badge i det
// hele tatt (veiviserens header/kontekst-rad, "Neste: {navn}", fullført-
// banneret). `isSelf` (se players-konstruksjonen under) er fortsatt
// viewer-relativ og brukes alene til å vise "Deg"-badgen der det trengs.
function playerLabel(p: ApiParticipant): string {
return p.display_name
}
// --- Component -------------------------------------------------------------
@ -414,8 +418,7 @@ export function RoundDetail({ roundId }: { roundId: string }) {
const players: Player[] =
round?.participants.map((p) => ({
id: p.id,
name: playerLabel(p, viewerId),
rawName: p.display_name,
name: playerLabel(p),
userId: p.user_id,
guestEmail: p.guest_email,
isOwner: p.is_owner,
@ -651,7 +654,10 @@ export function RoundDetail({ roundId }: { roundId: string }) {
return (
<div className="flex min-h-[100dvh] flex-col items-center justify-center gap-4 bg-background px-5 text-center">
<p className="text-base font-medium text-destructive">{error}</p>
<Link href="/my-rounds" className="text-base font-semibold text-primary underline underline-offset-2">
<Link
href="/my-rounds"
className="flex min-h-11 items-center text-base font-semibold text-primary underline underline-offset-2"
>
Tilbake til egne runder
</Link>
</div>
@ -726,7 +732,7 @@ export function RoundDetail({ roundId }: { roundId: string }) {
onClick={() => setPageTab("score")}
aria-pressed={pageTab === "score"}
className={cn(
"min-h-10 rounded-full px-4 text-sm font-bold transition-colors",
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
pageTab === "score" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
)}
>
@ -737,7 +743,7 @@ export function RoundDetail({ roundId }: { roundId: string }) {
onClick={() => setPageTab("manage")}
aria-pressed={pageTab === "manage"}
className={cn(
"min-h-10 rounded-full px-4 text-sm font-bold transition-colors",
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
pageTab === "manage" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
)}
>
@ -905,6 +911,11 @@ export function RoundDetail({ roundId }: { roundId: string }) {
const playerApiHole = playerHoles?.find((h) => h.hole_number === activeHole) ?? null
const hasScore = Boolean(playerApiHole?.played) && playerApiHole?.score !== null
const running = computeRunningToPar(playerHoles)
// Golfscore-språket (2026-07-27, compliance-pass): sirkel
// under par / rundet firkant over par / nøytral sirkel på
// par -- ALDRI samme grønne fylte sirkel uansett resultat
// (bogey og dobbel bogey skal ikke se ut som birdie).
const diff = hasScore && hole ? (playerApiHole!.score as number) - hole.par : null
return (
<li key={player.id} className="flex items-center gap-3 p-3 sm:p-4">
<span className="flex min-w-0 flex-1 flex-col">
@ -935,14 +946,12 @@ export function RoundDetail({ roundId }: { roundId: string }) {
}}
aria-label={
hasScore
? `Rediger ${player.name} sin registrering på hull ${activeHole}, ${playerApiHole?.score} slag`
? `Rediger ${player.name} sin registrering på hull ${activeHole}, ${playerApiHole?.score} slag${diff !== null && hole ? `, ${golfTermForScore(playerApiHole!.score as number, hole.par) ?? ""}` : ""}`
: `Registrer score for ${player.name} på hull ${activeHole}`
}
className={cn(
"flex size-14 shrink-0 items-center justify-center rounded-full border-2 text-xl font-extrabold tabular-nums transition-colors disabled:opacity-60",
hasScore
? "border-primary bg-primary text-primary-foreground"
: "border-dashed border-border text-muted-foreground hover:border-primary/60 hover:text-primary",
"flex size-14 shrink-0 items-center justify-center border-2 text-xl font-extrabold tabular-nums transition-colors disabled:opacity-60",
scoreMarkClasses(diff),
)}
>
{hasScore ? playerApiHole?.score : ""}
@ -1669,7 +1678,7 @@ function PlayerList({
Utslag: <span className="font-semibold text-foreground">{player.teeName}</span>
{" · "}
HCP:{" "}
<span className="font-semibold text-foreground">
<span className="font-semibold tabular-nums text-foreground">
{player.hcp !== null ? player.hcp.toFixed(1).replace(".", ",") : "ikke satt"}
</span>
{/* Tildelte slag (course handicap) -- KUN vist når HCP
@ -1678,7 +1687,7 @@ function PlayerList({
{player.courseHandicap !== null && (
<>
{" · "}
Tildelte slag: <span className="font-semibold text-foreground">{player.courseHandicap}</span>
Tildelte slag: <span className="font-semibold tabular-nums text-foreground">{player.courseHandicap}</span>
</>
)}
</span>
@ -1862,7 +1871,7 @@ function AddParticipantForm({
<button
type="button"
onClick={() => setMode("guest")}
className="text-sm font-semibold text-foreground underline-offset-2 hover:underline"
className="flex min-h-11 items-center text-sm font-semibold text-foreground underline-offset-2 hover:underline"
>
Legg til uten konto (gjest)
</button>
@ -1904,7 +1913,7 @@ function AddGuestForm({
<button
type="button"
onClick={onBack}
className="self-start text-sm font-semibold text-muted-foreground hover:text-foreground"
className="flex min-h-11 items-center self-start text-sm font-semibold text-muted-foreground hover:text-foreground"
>
Søk i stedet
</button>
@ -2043,7 +2052,7 @@ function EditRoundPanel({
<div className="flex flex-col gap-4 rounded-2xl border border-border bg-card p-4 sm:p-5">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-bold text-foreground">Rediger runde</span>
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-8 rounded-lg text-muted-foreground" aria-label="Lukk">
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-11 rounded-lg text-muted-foreground" aria-label="Lukk">
<X aria-hidden="true" className="size-4" />
</Button>
</div>
@ -2185,7 +2194,7 @@ function EditParticipantPanel({
const [teeName, setTeeName] = useState(player.teeName)
const [hcp, setHcp] = useState(player.hcp !== null ? String(player.hcp).replace(".", ",") : "")
const [statLevel, setStatLevel] = useState<StatLevel>(player.statLevel)
const [guestName, setGuestName] = useState(player.rawName)
const [guestName, setGuestName] = useState(player.name)
const [gender, setGender] = useState<Gender>(player.gender)
const [guestEmail, setGuestEmail] = useState(player.guestEmail ?? "")
const [saving, setSaving] = useState(false)
@ -2223,7 +2232,7 @@ function EditParticipantPanel({
if (statLevel !== player.statLevel) body.stat_level = statLevel
if (isGuest) {
const trimmedName = guestName.trim()
if (trimmedName && trimmedName !== player.rawName) body.guest_name = trimmedName
if (trimmedName && trimmedName !== player.name) body.guest_name = trimmedName
if (gender !== player.gender) body.gender = apiGender
const trimmedEmail = guestEmail.trim()
const normalizedEmail = trimmedEmail === "" ? null : trimmedEmail
@ -2510,7 +2519,7 @@ function ChangeCourseForm({
<button
type="button"
onClick={() => pickFacility(f)}
className="flex w-full flex-col px-3 py-2.5 text-left hover:bg-accent/60"
className="flex min-h-11 w-full flex-col px-3 py-2.5 text-left hover:bg-accent/60"
>
<span className="text-sm font-semibold text-foreground">{f.name}</span>
{(f.city || f.county) && (
@ -2527,7 +2536,11 @@ function ChangeCourseForm({
{source === "teeoff" && selectedFacility && !selectedOfficialCourse && (
<div className="flex flex-col gap-2">
<button type="button" onClick={() => setSelectedFacility(null)} className="self-start text-sm font-semibold text-muted-foreground">
<button
type="button"
onClick={() => setSelectedFacility(null)}
className="flex min-h-11 items-center self-start text-sm font-semibold text-muted-foreground"
>
{selectedFacility.name}
</button>
{officialCourses === null ? (
@ -2539,7 +2552,7 @@ function ChangeCourseForm({
<button
type="button"
onClick={() => setSelectedOfficialCourse(c)}
className="flex w-full items-center justify-between px-3 py-2.5 text-left text-sm font-semibold text-foreground hover:bg-accent/60"
className="flex min-h-11 w-full items-center justify-between px-3 py-2.5 text-left text-sm font-semibold text-foreground hover:bg-accent/60"
>
{c.name}
</button>
@ -2565,7 +2578,7 @@ function ChangeCourseForm({
<button
type="button"
onClick={() => pickCustomCourse(c)}
className="flex w-full px-3 py-2.5 text-left text-sm font-semibold text-foreground hover:bg-accent/60"
className="flex min-h-11 w-full items-center px-3 py-2.5 text-left text-sm font-semibold text-foreground hover:bg-accent/60"
>
{c.name}
</button>
@ -2593,7 +2606,7 @@ function ChangeCourseForm({
onClick={() => setTeeName(t.name)}
aria-pressed={teeName === t.name}
className={cn(
"h-10 rounded-lg border px-3 text-sm font-semibold transition-colors",
"h-11 rounded-lg border px-3 text-sm font-semibold transition-colors",
teeName === t.name
? "border-primary bg-primary text-primary-foreground"
: "border-border bg-background text-foreground hover:bg-accent/50",
@ -2610,7 +2623,7 @@ function ChangeCourseForm({
</div>
)}
<Button type="button" variant="ghost" onClick={onCancel} className="h-10 self-start rounded-xl text-sm font-semibold text-muted-foreground">
<Button type="button" variant="ghost" onClick={onCancel} className="h-11 self-start rounded-xl text-sm font-semibold text-muted-foreground">
Avbryt
</Button>
</div>
@ -2684,6 +2697,26 @@ function golfTermForScore(n: number, par: number): string | null {
return null
}
// Golfscore-språket (§Golfscore-språket i DESIGN_SYSTEM.md, håndhevet
// eksplisitt av teecup-scorekort-og-entry-spec.md §0.3/§3): form encoder
// retning (sirkel = under par, "firkant" = over par) slik at betydningen
// overlever selv uten farge; fylt vs. border+10%-tint encoder omfang (fylt =
// 2 slag eller mer fra par). Samme klassifisering som `ScoreMark` i
// round-scorecard.tsx -- egen kopi her siden denne knappen (size-14,
// trykkbar) trenger andre grunnklasser enn scorekortets små tabell-merker,
// men SAMME fargespråk (2026-07-27, compliance-pass). Bevisst `rounded-2xl`
// for "over par" i stedet for scorekortets `rounded-[4px]` -- fortsatt
// tydelig IKKE en sirkel, men matcher denne skjermens øvrige store
// trykkflate-avrunding (NumberPicker/hull-panel bruker samme radius).
function scoreMarkClasses(diff: number | null): string {
if (diff === null) return "rounded-full border-dashed border-border text-muted-foreground hover:border-primary/60 hover:text-primary"
if (diff <= -2) return "rounded-full border-primary bg-primary text-primary-foreground"
if (diff === -1) return "rounded-full border-primary bg-primary/10 text-primary"
if (diff === 0) return "rounded-full border-border bg-card text-foreground"
if (diff === 1) return "rounded-2xl border-brand-orange bg-brand-orange/10 text-brand-orange"
return "rounded-2xl border-brand-orange bg-brand-orange text-brand-orange-foreground"
}
function NumberPicker({
label,
value,