Deployed and documented. Summary of what changed:
The match/skins identity banner (the "Match" card showing both players/sides + a status like "1 UP" or "AS") now renders as a territory bar in all three places it appears — round-detail.tsx's live Score tab, round-scorecard.tsx's post-round summary, and session-scorecard.tsx's tournament match view. The leading side's colored zone extends proportionally past the midpoint into the trailing side's half (capped so both names stay legible at extreme leads), and both sides are equal/neutral at AS or before any holes are scored — matching your sketch's idea directly. Along the way I found and fixed a real overlap bug during browser testing: a long name could visually collide with the centered status pill on narrow (mobile) screens. Root cause was the identity box sizing itself to its own content instead of stretching to fill its zone — fixed so it always stretches, with alignment now handled via justify-start/end instead. Verified in both light/dark mode, normal and extreme leads, on a real scratch round matching your HCP scenario. Deployed live, teeoff.no unaffected.
This commit is contained in:
parent
30c114a2de
commit
2ad07d09fe
4 changed files with 412 additions and 97 deletions
72
CLAUDE.md
72
CLAUDE.md
|
|
@ -5232,6 +5232,78 @@ Ferdig og verifisert:
|
||||||
Begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
|
Begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
|
||||||
upåvirket.
|
upåvirket.
|
||||||
|
|
||||||
|
- **Match-identitetsbanneret: territorium-bar i stedet for flat 50/50-boks,
|
||||||
|
BYGGET, GRUNDIG BROWSERVERIFISERT (inkl. en reell overlapp-bug funnet OG
|
||||||
|
fikset) OG LIVE (2026-07-29), samme dag:** brukeren lastet opp to bilder
|
||||||
|
av dagens banner (samme identitetskort som ble bygget dagen før -- navn+
|
||||||
|
HCP i hver ende, en sentrert statusboks) og en "VELDIG DÅRLIG, kun
|
||||||
|
illustrerende" skisse av eget forslag: den ledende sidens halvdel bør
|
||||||
|
markeres OVER på motstanderens halvdel (ikke en statisk 50/50-boks), og
|
||||||
|
ved AS skal begge sider markeres likt (nøytralt).
|
||||||
|
**Design:** ny `leadZoneFraction(lead, totalHoles)` -- `0.5 + (lead /
|
||||||
|
totalHoles) * 0.5`, klippet til `[0.18, 0.82]` for å alltid holde begge
|
||||||
|
navn lesbare selv ved en ekstrem ledelse. Dominant side får en solid,
|
||||||
|
mettet farge (`bg-primary`/`bg-brand-orange`, hvit/kontrastfarget tekst
|
||||||
|
via allerede eksisterende `--primary-foreground`/`--brand-orange-
|
||||||
|
foreground`-tokens) og strekker seg proporsjonalt forbi midtlinjen;
|
||||||
|
underlegne side beholder samme fargetone som en svak, lys tint
|
||||||
|
(`/12`-opacity) med vanlig temafarget tekst. Ved AS (eller ingen hull
|
||||||
|
spilt ennå): begge soner nøyaktig 50/50 og nøytralt `bg-muted` -- ingen
|
||||||
|
side ser ut til å lede. Egen lokal kopi i alle tre filer der et match-
|
||||||
|
identitetsbanner finnes (samme "én fil, én kopi"-konvensjon som
|
||||||
|
MatchScorecardCell-arbeidet dagen før): `round-detail.tsx` sin
|
||||||
|
`FormatResultPanel` (primary/brand-orange-tokens), `round-scorecard.tsx`
|
||||||
|
sin nye delte `LeadBar`+`LeadZone` (samme tokens, portert fra den
|
||||||
|
tidligere `IdentitySide`+`CenterBadge`-duoen), `session-scorecard.tsx`
|
||||||
|
sin `IdentityBlock` (FAKTISK lagets `team.color`-hex i stedet for faste
|
||||||
|
tokens -- dominant sone `backgroundColor: color` + hvit tekst, svak sone
|
||||||
|
`color-mix(in srgb, ${color} 15%, transparent)`, samme kontrastvalg som
|
||||||
|
den eksisterende `SegmentedBar` i tournament-leaderboard.tsx).
|
||||||
|
**Reell overlapp-bug funnet OG fikset UNDER selve browserverifiseringen,
|
||||||
|
ikke antatt riktig fra kodegjennomgang alene:** første forsøk brukte en
|
||||||
|
`position:absolute`-sentrert statusboks OPPÅ to `width:X%`-delte soner --
|
||||||
|
et ekte skjermbilde på en smal (390px) mobil-viewport viste at et langt
|
||||||
|
navn ("Erol Haagenrud") ble delvis SKJULT bak statusboksen ("l Haagenrud"
|
||||||
|
vist i stedet, ikke en ellipse-trunkering, et reelt visuelt overlapp).
|
||||||
|
Første fiksforsøk (CSS Grid med `minmax(0,X fr) auto minmax(0,Y fr)` i
|
||||||
|
stedet for absolutt posisjonering) løste IKKE problemet alene -- fortsatt
|
||||||
|
samme overlapp ved re-test. Rot-årsaken var dypere: identitetsboksen
|
||||||
|
brukte `items-start`/`items-end` på sin YTRE flex-kolonne, som sizer
|
||||||
|
boksen etter INNHOLDETS egen bredde (shrink-to-fit) i stedet for sonens
|
||||||
|
faktiske tildelte bredde -- innholdet kunne dermed visuelt strekke seg
|
||||||
|
utover sin egen sone og inn i midt-kolonnen uansett hvor korrekt selve
|
||||||
|
grid-/bredde-fordelingen var. Fikset ved at boksen alltid STREKKER SEG
|
||||||
|
(fjernet items-start/items-end helt), med venstre/høyre-justering i
|
||||||
|
stedet løst via `justify-start`/`justify-end` på navne-raden og
|
||||||
|
`text-left`/`text-right` på HCP-teksten -- INNI en boks som nå alltid har
|
||||||
|
sonens fulle, korrekte bredde, slik at `truncate` faktisk virker presist.
|
||||||
|
**Verifisert grundig i en isolert scratch-nettleserøkt** (fersk
|
||||||
|
`teecup_scratch`-database + isolert scratch-MinIO + engangs API-
|
||||||
|
container, ekte `next dev` mot scratch-backend, Chrome DevTools MCP, 390×
|
||||||
|
844 mobil-viewport): bygget en ekte match-runde fra bunnen via API (egen-
|
||||||
|
definert 18-hulls bane, Erol HCP 10 vs. gjest "Tore Morell" HCP 54,
|
||||||
|
samme HCP-mønster som brukerens skjermbilde) og testet re-render etter
|
||||||
|
HVER kodeendring, ikke bare én gang til slutt -- fanget nettopp DERFOR
|
||||||
|
både at CSS Grid-fiksen alene ikke var nok, og at stretch-fiksen
|
||||||
|
faktisk løste det. Bekreftet: normal ledelse ("1 UP", dominant sone
|
||||||
|
moderat bredere), en EKSTREM ledelse (avgjort "9&7", dominant sone
|
||||||
|
nesten fyller hele baren, underlegne navn korrekt trunkert "T…"/"HCP…"
|
||||||
|
i stedet for å overlappe), AS/ingen-hull-spilt (nøytral 50/50, `bg-
|
||||||
|
muted`), BÅDE lys og mørk modus (kontrast bekreftet i begge), og samme
|
||||||
|
fiks bekreftet å fungere identisk i `round-scorecard.tsx` sin
|
||||||
|
`MatchScorecardGrid`-visning (samme runde, `/my-rounds/{id}/scorecard`).
|
||||||
|
`session-scorecard.tsx` sin tournament-variant IKKE egen nettleser-
|
||||||
|
testet denne runden (identisk kodemønster, kun `team.color`-hex i stedet
|
||||||
|
for faste tokens -- lavere risiko, men flagget ærlig som ikke eget
|
||||||
|
bevist). Ekte typesjekket produksjonsbuild kjørt (to runder -- én etter
|
||||||
|
første, mislykkede CSS Grid-only-forsøk, én etter den faktiske stretch-
|
||||||
|
fiksen), alle 24 ruter listet begge ganger.
|
||||||
|
**Rullet ut live 2026-07-29**, bruker bekreftet eksplisitt: ingen
|
||||||
|
migrasjon (ren frontend), `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:
|
Neste steg:
|
||||||
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
||||||
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen
|
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen
|
||||||
|
|
|
||||||
|
|
@ -2396,6 +2396,74 @@ function IdentitySide({ identity, side }: { identity: { name: string; hcp: numbe
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Territorium-bar (2026-07-29, erstatter det tidligere flate, likt-delte
|
||||||
|
// banneret) -- den ledende sidens sone strekker seg PROPORSJONALT forbi
|
||||||
|
// midtlinjen inn på motstanderens halvdel (brukerens eget forslag: "hele
|
||||||
|
// den halve delen som tilhører den som er en opp er markert over på
|
||||||
|
// motstanderens halvdel"), i stedet for en statisk 50/50-boks med tekst i
|
||||||
|
// midten. Ved AS (eller ingen hull spilt ennå) er begge soner nøyaktig like
|
||||||
|
// store OG likt (nøytralt) farget -- ingen side skal se ut til å lede.
|
||||||
|
// Bruker samme proporsjonale to-fargede-bar-idé som SegmentedBar
|
||||||
|
// (tournament-leaderboard.tsx), tilpasset lead-i-hull i stedet for poeng.
|
||||||
|
function leadZoneFraction(lead: number, totalHoles: number): number {
|
||||||
|
if (totalHoles <= 0) return 0.5
|
||||||
|
const fraction = 0.5 + (lead / totalHoles) * 0.5
|
||||||
|
return Math.min(0.82, Math.max(0.18, fraction))
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeadZone({
|
||||||
|
identity,
|
||||||
|
side,
|
||||||
|
dominant,
|
||||||
|
}: {
|
||||||
|
identity: { name: string; hcp: number | null }
|
||||||
|
side: "a" | "b"
|
||||||
|
dominant: boolean
|
||||||
|
}) {
|
||||||
|
const textTone = dominant ? (side === "a" ? "text-primary-foreground" : "text-brand-orange-foreground") : "text-foreground"
|
||||||
|
const subTone = dominant
|
||||||
|
? side === "a"
|
||||||
|
? "text-primary-foreground/75"
|
||||||
|
: "text-brand-orange-foreground/75"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
// Merk: den ytre boksen STREKKER SEG (ingen items-start/items-end her --
|
||||||
|
// det ville sizet boksen etter innholdets egen bredde, ikke etter sonens
|
||||||
|
// faktiske tildelte bredde, og kunne visuelt lekke inn i midt-kolonnen på
|
||||||
|
// smale skjermer). Venstre/høyre-justering skjer i stedet med
|
||||||
|
// justify-start/justify-end og text-align INNI en boks som alltid har
|
||||||
|
// sonens fulle, korrekte bredde -- truncate virker da presist.
|
||||||
|
return (
|
||||||
|
<div className="flex h-full min-w-0 w-full flex-col justify-center gap-0.5 px-3">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex min-w-0 items-center gap-1.5",
|
||||||
|
side === "a" ? "justify-start" : "justify-end",
|
||||||
|
side === "b" && "flex-row-reverse",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!dominant && (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("size-2 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className={cn("min-w-0 truncate text-sm font-extrabold", textTone)}>{identity.name}</span>
|
||||||
|
</div>
|
||||||
|
{identity.hcp !== null && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"truncate text-xs font-bold tabular-nums",
|
||||||
|
subTone,
|
||||||
|
side === "a" ? "text-left" : "text-right",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
HCP {identity.hcp}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type ApiFlightSummary = {
|
type ApiFlightSummary = {
|
||||||
round_id: string
|
round_id: string
|
||||||
name: string | null
|
name: string | null
|
||||||
|
|
@ -2579,10 +2647,9 @@ function FormatResultPanel({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identitetsbanner (2026-07-29, erstatter den tidligere plainere teksten +
|
// Identitetsbanner (2026-07-29, territorium-bar-versjon -- se leadZoneFraction
|
||||||
// den nå overflødige hull-for-hull-sirkel-listen -- selve scorekortet
|
// over). Navn+HCP i hver sone, stor sentrert løpende status flytende oppå
|
||||||
// under viser dette per hull nå). Navn+HCP enten side, stor sentrert
|
// begge soner, "Ferdig"-merke når avgjort -- samme visuelle språk (nå) som
|
||||||
// løpende status, "Ferdig"-merke når avgjort -- samme visuelle språk som
|
|
||||||
// MatchScorecardGrid (round-scorecard.tsx) og TournamentMatchGrid
|
// MatchScorecardGrid (round-scorecard.tsx) og TournamentMatchGrid
|
||||||
// (session-scorecard.tsx), egen lokal kopi per prosjektets konvensjon.
|
// (session-scorecard.tsx), egen lokal kopi per prosjektets konvensjon.
|
||||||
const identityA = sideIdentity(round, players, "a")
|
const identityA = sideIdentity(round, players, "a")
|
||||||
|
|
@ -2594,39 +2661,61 @@ function FormatResultPanel({
|
||||||
const isDormie = result.match_is_dormie ?? false
|
const isDormie = result.match_is_dormie ?? false
|
||||||
const tone: "a" | "b" | "neutral" = lead > 0 ? "a" : lead < 0 ? "b" : "neutral"
|
const tone: "a" | "b" | "neutral" = lead > 0 ? "a" : lead < 0 ? "b" : "neutral"
|
||||||
const statusLabel = holesPlayed > 0 ? formatMatchStatus(lead, holesRemaining, isClosed, isDormie) : null
|
const statusLabel = holesPlayed > 0 ? formatMatchStatus(lead, holesRemaining, isClosed, isDormie) : null
|
||||||
|
const totalHoles = holesPlayed + holesRemaining
|
||||||
|
const widthA = holesPlayed > 0 ? leadZoneFraction(lead, totalHoles) * 100 : 50
|
||||||
|
const widthB = 100 - widthA
|
||||||
|
const zoneAClass = tone === "a" ? "bg-primary" : tone === "b" ? "bg-primary/12" : "bg-muted"
|
||||||
|
const zoneBClass = tone === "b" ? "bg-brand-orange" : tone === "a" ? "bg-brand-orange/12" : "bg-muted"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-5 flex flex-col gap-2">
|
<div className="mb-5 flex flex-col gap-2">
|
||||||
<span className="text-sm font-bold text-foreground">{FORMAT_LABELS[round.play_format] ?? round.play_format}</span>
|
<span className="text-sm font-bold text-foreground">{FORMAT_LABELS[round.play_format] ?? round.play_format}</span>
|
||||||
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-card px-3 py-3 shadow-md shadow-black/8">
|
{/* CSS Grid, ikke absolutt-posisjonert overlegg (2026-07-29-fiks) --
|
||||||
<IdentitySide identity={identityA} side="a" />
|
en tidligere versjon la statusen som en absolutt sentrert boks OPPÅ
|
||||||
<div className="flex shrink-0 flex-col items-center px-2">
|
navnetekst, som kunne overlappe et langt navn på smale skjermer.
|
||||||
{statusLabel ? (
|
Med grid-kolonner `minmax(0,X fr) auto minmax(0,Y fr)` reserverer
|
||||||
<>
|
nettleseren ALLTID nøyaktig midt-kolonnens egen bredde til status-
|
||||||
<span
|
boksen -- soneandelen forblir proporsjonal (X:Y), men kan aldri
|
||||||
className={cn(
|
overlappe, og lange navn trunkeres i stedet (samme `truncate`-
|
||||||
"text-2xl font-extrabold leading-none tabular-nums",
|
mekanisme som ellers). */}
|
||||||
tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
|
<div
|
||||||
)}
|
className="grid h-[76px] w-full overflow-hidden rounded-2xl shadow-md shadow-black/8 sm:h-20"
|
||||||
>
|
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
|
||||||
{statusLabel}
|
>
|
||||||
</span>
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneAClass)}>
|
||||||
{isClosed ? (
|
<LeadZone identity={identityA} side="a" dominant={tone === "a"} />
|
||||||
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
</div>
|
||||||
Ferdig
|
<div className="flex h-full items-center justify-center px-2">
|
||||||
</span>
|
<div className="flex flex-col items-center rounded-xl border border-border/60 bg-card/95 px-3 py-1.5 shadow-sm shadow-black/10 backdrop-blur-sm">
|
||||||
) : (
|
{statusLabel ? (
|
||||||
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">
|
<>
|
||||||
{holesPlayed} hull spilt
|
<span
|
||||||
{holesRemaining > 0 && isDormie && " · dormie"}
|
className={cn(
|
||||||
</span>
|
"text-xl font-extrabold leading-none tabular-nums sm:text-2xl",
|
||||||
)}
|
tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
|
||||||
</>
|
)}
|
||||||
) : (
|
>
|
||||||
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
{statusLabel}
|
||||||
)}
|
</span>
|
||||||
|
{isClosed ? (
|
||||||
|
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Ferdig
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">
|
||||||
|
{holesPlayed} hull spilt
|
||||||
|
{holesRemaining > 0 && isDormie && " · dormie"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneBClass)}>
|
||||||
|
<LeadZone identity={identityB} side="b" dominant={tone === "b"} />
|
||||||
</div>
|
</div>
|
||||||
<IdentitySide identity={identityB} side="b" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -443,42 +443,121 @@ function computeRunning(results: ("a" | "b" | "halved")[], totalHoles: number):
|
||||||
|
|
||||||
type HeaderIdentity = { name: string; hcp: number | null }
|
type HeaderIdentity = { name: string; hcp: number | null }
|
||||||
|
|
||||||
function IdentitySide({ identity, side }: { identity: HeaderIdentity; side: "a" | "b" }) {
|
// Territorium-bar (2026-07-29, brukerens eget forslag: den ledende sidens
|
||||||
|
// sone strekker seg proporsjonalt forbi midtlinjen inn på motstanderens
|
||||||
|
// halvdel, i stedet for en statisk 50/50-boks med tekst i midten). Ved AS
|
||||||
|
// eller ingen hull avgjort ennå er begge soner like store og nøytralt
|
||||||
|
// farget. Egen lokal kopi av samme idé som round-detail.tsx sin
|
||||||
|
// FormatResultPanel, per prosjektets "én fil, én kopi"-konvensjon.
|
||||||
|
function leadZoneFraction(lead: number, totalHoles: number): number {
|
||||||
|
if (totalHoles <= 0) return 0.5
|
||||||
|
const fraction = 0.5 + (lead / totalHoles) * 0.5
|
||||||
|
return Math.min(0.82, Math.max(0.18, fraction))
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeadZone({ identity, side, dominant }: { identity: HeaderIdentity; side: "a" | "b"; dominant: boolean }) {
|
||||||
|
const textTone = dominant ? (side === "a" ? "text-primary-foreground" : "text-brand-orange-foreground") : "text-foreground"
|
||||||
|
const subTone = dominant
|
||||||
|
? side === "a"
|
||||||
|
? "text-primary-foreground/75"
|
||||||
|
: "text-brand-orange-foreground/75"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
// Boksen STREKKER SEG (ingen items-start/items-end) -- ellers sizes den
|
||||||
|
// etter innholdets egen bredde, ikke sonens faktiske tildelte bredde, og
|
||||||
|
// kan visuelt lekke inn i midt-kolonnen på smale skjermer. Justering skjer
|
||||||
|
// med justify-start/justify-end + text-align inni en boks som alltid har
|
||||||
|
// full, korrekt bredde -- truncate virker da presist.
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex min-w-0 flex-1 flex-col gap-0.5", side === "a" ? "items-start" : "items-end")}>
|
<div className="flex h-full min-w-0 w-full flex-col justify-center gap-0.5 px-3">
|
||||||
<div className={cn("flex min-w-0 items-center gap-1.5", side === "b" && "flex-row-reverse")}>
|
<div
|
||||||
<span
|
className={cn(
|
||||||
aria-hidden="true"
|
"flex min-w-0 items-center gap-1.5",
|
||||||
className={cn("size-2 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")}
|
side === "a" ? "justify-start" : "justify-end",
|
||||||
/>
|
side === "b" && "flex-row-reverse",
|
||||||
<span className="min-w-0 truncate text-sm font-extrabold text-foreground">{identity.name}</span>
|
)}
|
||||||
|
>
|
||||||
|
{!dominant && (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("size-2 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className={cn("min-w-0 truncate text-sm font-extrabold", textTone)}>{identity.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{identity.hcp !== null && (
|
{identity.hcp !== null && (
|
||||||
<span className="text-xs font-bold tabular-nums text-muted-foreground">HCP {identity.hcp}</span>
|
<span
|
||||||
|
className={cn(
|
||||||
|
"truncate text-xs font-bold tabular-nums",
|
||||||
|
subTone,
|
||||||
|
side === "a" ? "text-left" : "text-right",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
HCP {identity.hcp}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CenterBadge({ state, holesPlayed }: { state: RunningState | null; holesPlayed: number }) {
|
function LeadBar({
|
||||||
if (!state) {
|
identityA,
|
||||||
return (
|
identityB,
|
||||||
<div className="flex shrink-0 flex-col items-center px-2">
|
state,
|
||||||
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
holesPlayed,
|
||||||
</div>
|
totalHoles,
|
||||||
)
|
}: {
|
||||||
}
|
identityA: HeaderIdentity
|
||||||
const toneClass = state.tone === "a" ? "text-primary" : state.tone === "b" ? "text-brand-orange" : "text-foreground"
|
identityB: HeaderIdentity
|
||||||
|
state: RunningState | null
|
||||||
|
holesPlayed: number
|
||||||
|
totalHoles: number
|
||||||
|
}) {
|
||||||
|
const lead = state?.lead ?? 0
|
||||||
|
const tone = state?.tone ?? "neutral"
|
||||||
|
const widthA = state ? leadZoneFraction(lead, totalHoles) * 100 : 50
|
||||||
|
const widthB = 100 - widthA
|
||||||
|
const zoneAClass = tone === "a" ? "bg-primary" : tone === "b" ? "bg-primary/12" : "bg-muted"
|
||||||
|
const zoneBClass = tone === "b" ? "bg-brand-orange" : tone === "a" ? "bg-brand-orange/12" : "bg-muted"
|
||||||
return (
|
return (
|
||||||
<div className="flex shrink-0 flex-col items-center px-2">
|
// CSS Grid, ikke absolutt overlegg (2026-07-29-fiks, se round-detail.tsx
|
||||||
<span className={cn("text-2xl font-extrabold leading-none tabular-nums", toneClass)}>{state.label}</span>
|
// sin FormatResultPanel for full begrunnelse) -- midt-kolonnen (status)
|
||||||
{state.isClosed ? (
|
// er `auto`-bredde, de to sonene `minmax(0, X fr)` -- reserverer alltid
|
||||||
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
// nøyaktig plass til statusboksen, kan aldri overlappe et langt navn.
|
||||||
Ferdig
|
<div
|
||||||
</span>
|
className="grid h-[76px] w-full overflow-hidden rounded-2xl sm:h-20"
|
||||||
) : (
|
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
|
||||||
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">{holesPlayed} hull spilt</span>
|
>
|
||||||
)}
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneAClass)}>
|
||||||
|
<LeadZone identity={identityA} side="a" dominant={tone === "a"} />
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full items-center justify-center px-2">
|
||||||
|
<div className="flex flex-col items-center rounded-xl border border-border/60 bg-card/95 px-3 py-1.5 shadow-sm shadow-black/10 backdrop-blur-sm">
|
||||||
|
{state ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-xl font-extrabold leading-none tabular-nums sm:text-2xl",
|
||||||
|
tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{state.label}
|
||||||
|
</span>
|
||||||
|
{state.isClosed ? (
|
||||||
|
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Ferdig
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">{holesPlayed} hull spilt</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneBClass)}>
|
||||||
|
<LeadZone identity={identityB} side="b" dominant={tone === "b"} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -538,11 +617,13 @@ function MatchScorecardGrid({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
|
<div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
|
||||||
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-muted/40 px-3 py-3">
|
<LeadBar
|
||||||
<IdentitySide identity={headerIdentity("a")} side="a" />
|
identityA={headerIdentity("a")}
|
||||||
<CenterBadge state={lastState} holesPlayed={decidedHoles.length} />
|
identityB={headerIdentity("b")}
|
||||||
<IdentitySide identity={headerIdentity("b")} side="b" />
|
state={lastState}
|
||||||
</div>
|
holesPlayed={decidedHoles.length}
|
||||||
|
totalHoles={round.holes_planned}
|
||||||
|
/>
|
||||||
|
|
||||||
{decidedHoles.length === 0 ? (
|
{decidedHoles.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -967,24 +967,62 @@ function unitGross(scorecard: ApiScorecard, unit: Unit, holeNumber: number): num
|
||||||
return entry ? entry.gross_strokes : null
|
return entry ? entry.gross_strokes : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Territorium-bar (2026-07-29, brukerens eget forslag: den ledende sidens
|
||||||
|
// sone strekker seg proporsjonalt forbi midtlinjen inn på motstanderens
|
||||||
|
// halvdel). Ulikt round-detail.tsx/round-scorecard.tsx sin primary/brand-
|
||||||
|
// orange-versjon bruker denne lagets FAKTISKE `team.color` (vilkårlig hex,
|
||||||
|
// samme som SegmentedBar i tournament-leaderboard.tsx) -- dominant sone får
|
||||||
|
// hvit tekst (samme kontrastvalg som SegmentedBar), lys sone en svak
|
||||||
|
// gjennomsiktig tone av samme farge med vanlig tema-tekst.
|
||||||
|
function leadZoneFraction(lead: number, totalHoles: number): number {
|
||||||
|
if (totalHoles <= 0) return 0.5
|
||||||
|
const fraction = 0.5 + (lead / totalHoles) * 0.5
|
||||||
|
return Math.min(0.82, Math.max(0.18, fraction))
|
||||||
|
}
|
||||||
|
|
||||||
function IdentityBlock({
|
function IdentityBlock({
|
||||||
name,
|
name,
|
||||||
hcp,
|
hcp,
|
||||||
color,
|
color,
|
||||||
side,
|
side,
|
||||||
|
dominant,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
hcp: number | null
|
hcp: number | null
|
||||||
color: string
|
color: string
|
||||||
side: "a" | "b"
|
side: "a" | "b"
|
||||||
|
dominant: boolean
|
||||||
}) {
|
}) {
|
||||||
|
// Boksen STREKKER SEG (ingen items-start/items-end) -- ellers sizes den
|
||||||
|
// etter innholdets egen bredde, ikke sonens faktiske tildelte bredde, og
|
||||||
|
// kan visuelt lekke inn i midt-kolonnen på smale skjermer. Justering skjer
|
||||||
|
// med justify-start/justify-end + text-align inni en boks som alltid har
|
||||||
|
// full, korrekt bredde -- truncate virker da presist.
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex min-w-0 flex-1 flex-col gap-0.5", side === "a" ? "items-start" : "items-end")}>
|
<div className="flex h-full min-w-0 w-full flex-col justify-center gap-0.5 px-3">
|
||||||
<div className={cn("flex min-w-0 items-center gap-1.5", side === "b" && "flex-row-reverse")}>
|
<div
|
||||||
<span aria-hidden="true" className="size-2 shrink-0 rounded-full" style={{ backgroundColor: color }} />
|
className={cn(
|
||||||
<span className="min-w-0 truncate text-sm font-extrabold text-foreground">{name}</span>
|
"flex min-w-0 items-center gap-1.5",
|
||||||
|
side === "a" ? "justify-start" : "justify-end",
|
||||||
|
side === "b" && "flex-row-reverse",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!dominant && <span aria-hidden="true" className="size-2 shrink-0 rounded-full" style={{ backgroundColor: color }} />}
|
||||||
|
<span
|
||||||
|
className="min-w-0 truncate text-sm font-extrabold"
|
||||||
|
style={{ color: dominant ? "#fff" : undefined }}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{hcp !== null && <span className="text-xs font-bold tabular-nums text-muted-foreground">HCP {hcp}</span>}
|
{hcp !== null && (
|
||||||
|
<span
|
||||||
|
className={cn("truncate text-xs font-bold tabular-nums text-muted-foreground", side === "a" ? "text-left" : "text-right")}
|
||||||
|
style={dominant ? { color: "rgba(255,255,255,0.75)" } : undefined}
|
||||||
|
>
|
||||||
|
HCP {hcp}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1026,38 +1064,73 @@ function TournamentMatchGrid({
|
||||||
|
|
||||||
const identityA = headerIdentity("a")
|
const identityA = headerIdentity("a")
|
||||||
const identityB = headerIdentity("b")
|
const identityB = headerIdentity("b")
|
||||||
|
const lead = lastState?.lead ?? 0
|
||||||
|
const tone = lastState?.tone ?? "neutral"
|
||||||
|
const widthA = lastState ? leadZoneFraction(lead, totalHoles) * 100 : 50
|
||||||
|
const widthB = 100 - widthA
|
||||||
|
const zoneAStyle =
|
||||||
|
tone === "a"
|
||||||
|
? { backgroundColor: identityA.color }
|
||||||
|
: tone === "b"
|
||||||
|
? { backgroundColor: `color-mix(in srgb, ${identityA.color} 15%, transparent)` }
|
||||||
|
: undefined
|
||||||
|
const zoneBStyle =
|
||||||
|
tone === "b"
|
||||||
|
? { backgroundColor: identityB.color }
|
||||||
|
: tone === "a"
|
||||||
|
? { backgroundColor: `color-mix(in srgb, ${identityB.color} 15%, transparent)` }
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-3 sm:p-4">
|
<div className="flex flex-col gap-3 p-3 sm:p-4">
|
||||||
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-muted/40 px-3 py-3">
|
{/* CSS Grid, ikke absolutt overlegg (2026-07-29-fiks, se round-detail.tsx
|
||||||
<IdentityBlock name={identityA.name} hcp={identityA.hcp} color={identityA.color} side="a" />
|
sin FormatResultPanel for full begrunnelse) -- midt-kolonnen (status)
|
||||||
<div className="flex shrink-0 flex-col items-center px-2">
|
er `auto`-bredde, de to sonene `minmax(0, X fr)` -- reserverer alltid
|
||||||
{lastState ? (
|
nøyaktig plass til statusboksen, kan aldri overlappe et langt navn. */}
|
||||||
<>
|
<div
|
||||||
<span
|
className="grid h-[76px] w-full overflow-hidden rounded-2xl sm:h-20"
|
||||||
className="text-2xl font-extrabold leading-none tabular-nums"
|
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
|
||||||
style={{
|
>
|
||||||
color:
|
<div
|
||||||
lastState.tone === "a" ? identityA.color : lastState.tone === "b" ? identityB.color : undefined,
|
className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", tone === "neutral" && "bg-muted")}
|
||||||
}}
|
style={zoneAStyle}
|
||||||
>
|
>
|
||||||
{lastState.label}
|
<IdentityBlock name={identityA.name} hcp={identityA.hcp} color={identityA.color} side="a" dominant={tone === "a"} />
|
||||||
</span>
|
</div>
|
||||||
{lastState.isClosed ? (
|
<div className="flex h-full items-center justify-center px-2">
|
||||||
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
<div className="flex flex-col items-center rounded-xl border border-border/60 bg-card/95 px-3 py-1.5 shadow-sm shadow-black/10 backdrop-blur-sm">
|
||||||
Ferdig
|
{lastState ? (
|
||||||
</span>
|
<>
|
||||||
) : (
|
<span
|
||||||
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">
|
className="text-xl font-extrabold leading-none tabular-nums sm:text-2xl"
|
||||||
{decidedHoles.length} hull spilt
|
style={{
|
||||||
</span>
|
color:
|
||||||
)}
|
lastState.tone === "a" ? identityA.color : lastState.tone === "b" ? identityB.color : undefined,
|
||||||
</>
|
}}
|
||||||
) : (
|
>
|
||||||
<span className="text-xs font-bold text-muted-foreground">Ingen hull spilt ennå</span>
|
{lastState.label}
|
||||||
)}
|
</span>
|
||||||
|
{lastState.isClosed ? (
|
||||||
|
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Ferdig
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">
|
||||||
|
{decidedHoles.length} hull spilt
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", tone === "neutral" && "bg-muted")}
|
||||||
|
style={zoneBStyle}
|
||||||
|
>
|
||||||
|
<IdentityBlock name={identityB.name} hcp={identityB.hcp} color={identityB.color} side="b" dominant={tone === "b"} />
|
||||||
</div>
|
</div>
|
||||||
<IdentityBlock name={identityB.name} hcp={identityB.hcp} color={identityB.color} side="b" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="-mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">
|
<div className="-mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue