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:
Erol Haagenrud 2026-07-29 16:26:31 +02:00
parent 30c114a2de
commit 2ad07d09fe
4 changed files with 412 additions and 97 deletions

View file

@ -5232,6 +5232,78 @@ Ferdig og verifisert:
Begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
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:
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen

View file

@ -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 = {
round_id: string
name: string | null
@ -2579,10 +2647,9 @@ function FormatResultPanel({
)
}
// Identitetsbanner (2026-07-29, erstatter den tidligere plainere teksten +
// den nå overflødige hull-for-hull-sirkel-listen -- selve scorekortet
// under viser dette per hull nå). Navn+HCP enten side, stor sentrert
// løpende status, "Ferdig"-merke når avgjort -- samme visuelle språk som
// Identitetsbanner (2026-07-29, territorium-bar-versjon -- se leadZoneFraction
// over). Navn+HCP i hver sone, stor sentrert løpende status flytende oppå
// begge soner, "Ferdig"-merke når avgjort -- samme visuelle språk (nå) som
// MatchScorecardGrid (round-scorecard.tsx) og TournamentMatchGrid
// (session-scorecard.tsx), egen lokal kopi per prosjektets konvensjon.
const identityA = sideIdentity(round, players, "a")
@ -2594,18 +2661,37 @@ function FormatResultPanel({
const isDormie = result.match_is_dormie ?? false
const tone: "a" | "b" | "neutral" = lead > 0 ? "a" : lead < 0 ? "b" : "neutral"
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 (
<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>
<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">
<IdentitySide identity={identityA} side="a" />
<div className="flex shrink-0 flex-col items-center px-2">
{/* CSS Grid, ikke absolutt-posisjonert overlegg (2026-07-29-fiks) --
en tidligere versjon la statusen som en absolutt sentrert boks OPPÅ
navnetekst, som kunne overlappe et langt navn smale skjermer.
Med grid-kolonner `minmax(0,X fr) auto minmax(0,Y fr)` reserverer
nettleseren ALLTID nøyaktig midt-kolonnens egen bredde til status-
boksen -- soneandelen forblir proporsjonal (X:Y), men kan aldri
overlappe, og lange navn trunkeres i stedet (samme `truncate`-
mekanisme som ellers). */}
<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)` }}
>
<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">
{statusLabel ? (
<>
<span
className={cn(
"text-2xl font-extrabold leading-none tabular-nums",
"text-xl font-extrabold leading-none tabular-nums sm:text-2xl",
tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
)}
>
@ -2626,7 +2712,10 @@ function FormatResultPanel({
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
)}
</div>
<IdentitySide identity={identityB} side="b" />
</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>
)

View file

@ -443,35 +443,105 @@ function computeRunning(results: ("a" | "b" | "halved")[], totalHoles: number):
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 (
<div className={cn("flex min-w-0 flex-1 flex-col gap-0.5", side === "a" ? "items-start" : "items-end")}>
<div className={cn("flex min-w-0 items-center gap-1.5", side === "b" && "flex-row-reverse")}>
<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="min-w-0 truncate text-sm font-extrabold text-foreground">{identity.name}</span>
)}
<span className={cn("min-w-0 truncate text-sm font-extrabold", textTone)}>{identity.name}</span>
</div>
{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>
)
}
function CenterBadge({ state, holesPlayed }: { state: RunningState | null; holesPlayed: number }) {
if (!state) {
function LeadBar({
identityA,
identityB,
state,
holesPlayed,
totalHoles,
}: {
identityA: HeaderIdentity
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 (
<div className="flex shrink-0 flex-col items-center px-2">
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
// CSS Grid, ikke absolutt overlegg (2026-07-29-fiks, se round-detail.tsx
// sin FormatResultPanel for full begrunnelse) -- midt-kolonnen (status)
// er `auto`-bredde, de to sonene `minmax(0, X fr)` -- reserverer alltid
// nøyaktig plass til statusboksen, kan aldri overlappe et langt navn.
<div
className="grid h-[76px] w-full overflow-hidden rounded-2xl sm:h-20"
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
>
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneAClass)}>
<LeadZone identity={identityA} side="a" dominant={tone === "a"} />
</div>
)
}
const toneClass = state.tone === "a" ? "text-primary" : state.tone === "b" ? "text-brand-orange" : "text-foreground"
return (
<div className="flex shrink-0 flex-col items-center px-2">
<span className={cn("text-2xl font-extrabold leading-none tabular-nums", toneClass)}>{state.label}</span>
<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
@ -479,6 +549,15 @@ function CenterBadge({ state, holesPlayed }: { state: RunningState | null; holes
) : (
<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>
)
}
@ -538,11 +617,13 @@ function MatchScorecardGrid({
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 items-center justify-between gap-2 rounded-2xl border border-border bg-muted/40 px-3 py-3">
<IdentitySide identity={headerIdentity("a")} side="a" />
<CenterBadge state={lastState} holesPlayed={decidedHoles.length} />
<IdentitySide identity={headerIdentity("b")} side="b" />
</div>
<LeadBar
identityA={headerIdentity("a")}
identityB={headerIdentity("b")}
state={lastState}
holesPlayed={decidedHoles.length}
totalHoles={round.holes_planned}
/>
{decidedHoles.length === 0 ? (
<p className="text-sm text-muted-foreground">

View file

@ -967,24 +967,62 @@ function unitGross(scorecard: ApiScorecard, unit: Unit, holeNumber: number): num
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({
name,
hcp,
color,
side,
dominant,
}: {
name: string
hcp: number | null
color: string
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 (
<div className={cn("flex min-w-0 flex-1 flex-col gap-0.5", side === "a" ? "items-start" : "items-end")}>
<div className={cn("flex min-w-0 items-center gap-1.5", side === "b" && "flex-row-reverse")}>
<span aria-hidden="true" className="size-2 shrink-0 rounded-full" style={{ backgroundColor: color }} />
<span className="min-w-0 truncate text-sm font-extrabold text-foreground">{name}</span>
<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="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>
{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>
)
}
@ -1026,16 +1064,45 @@ function TournamentMatchGrid({
const identityA = headerIdentity("a")
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 (
<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">
<IdentityBlock name={identityA.name} hcp={identityA.hcp} color={identityA.color} side="a" />
<div className="flex shrink-0 flex-col items-center px-2">
{/* CSS Grid, ikke absolutt overlegg (2026-07-29-fiks, se round-detail.tsx
sin FormatResultPanel for full begrunnelse) -- midt-kolonnen (status)
er `auto`-bredde, de to sonene `minmax(0, X fr)` -- reserverer alltid
nøyaktig plass til statusboksen, kan aldri overlappe et langt navn. */}
<div
className="grid h-[76px] w-full overflow-hidden rounded-2xl sm:h-20"
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
>
<div
className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", tone === "neutral" && "bg-muted")}
style={zoneAStyle}
>
<IdentityBlock name={identityA.name} hcp={identityA.hcp} color={identityA.color} 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">
{lastState ? (
<>
<span
className="text-2xl font-extrabold leading-none tabular-nums"
className="text-xl font-extrabold leading-none tabular-nums sm:text-2xl"
style={{
color:
lastState.tone === "a" ? identityA.color : lastState.tone === "b" ? identityB.color : undefined,
@ -1054,10 +1121,16 @@ function TournamentMatchGrid({
)}
</>
) : (
<span className="text-xs font-bold text-muted-foreground">Ingen hull spilt ennå</span>
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
)}
</div>
<IdentityBlock name={identityB.name} hcp={identityB.hcp} color={identityB.color} side="b" />
</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>
<div className="-mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">