Gjør spillertabellen mer regneark-aktig, fyll full bredde (tillegg til ADR-094)
Bruker sammenlignet med et regneark og påpekte unødvendig sidescroll på bred skjerm -- tettet opp med rutenett-stil og en colgroup som lar rundekolonnene fylle resten av bredden. Samme props-kontrakt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
17589e1707
commit
072638dc37
3 changed files with 160 additions and 104 deletions
|
|
@ -9607,6 +9607,26 @@ typesjekket, gitt øktens allerede betydelige lengde.
|
|||
compose build teecup_api teecup_frontend && up -d` for begge, rene
|
||||
logger, 200 OK (inkl. ny `/hjelp`-rute).
|
||||
|
||||
**Tillegg 2026-08-19 -- visuell revisjon av spillertabellen, rett etter
|
||||
utrulling.** Bruker (skjermdump av den ekte, live tabellen med 51
|
||||
spillere): "skulle ønske tabellen var mer 'regneark-aktig'" og "hvorfor
|
||||
må jeg scrolle horisontalt når jeg har så mye ledig skjermplass?" --
|
||||
gyldig kritikk, bekreftet i skjermdumpen (brede kort-aktige celler med
|
||||
mye padding, faste brede kolonner som sendte rundekolonnene utenfor
|
||||
synlig område selv på en bred skjerm). Bruker tok dette direkte til V0
|
||||
selv (ikke via meg) og lastet opp svaret. Ny versjon av `tournament-
|
||||
players-table.tsx`: rutenett-linjer i stedet for kort-følelse
|
||||
(`border-b border-r` per celle, ingen egen kant per kontroll -- kun
|
||||
hover/fokus lyser opp), lavere radhøyde (`h-9`), mindre tekst
|
||||
(`text-[13px]`), OG et `<colgroup>` som gir faste, smale bredder til
|
||||
de korte kolonnene (HCP/kjønn/alder) mens rundekolonnene IKKE får noen
|
||||
fast bredde -- de fordeler resten av den tilgjengelige bredden seg
|
||||
imellom, så tabellen fyller skjermen i stedet for å la dem presses
|
||||
utenfor. **Props-kontrakten er UENDRET** (samme typer, samme callback-
|
||||
navn) -- ren visuell omskriving, ingen endring i `SetupTab` sin
|
||||
sammenkobling nødvendig. Verifisert i ny lokal scratch-visning (bred
|
||||
skjerm bekreftet fylt uten sidescroll med 3 runde-kolonner, lys+mørk).
|
||||
|
||||
---
|
||||
|
||||
## Utviklingsplan (rekkefølge)
|
||||
|
|
|
|||
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -13020,3 +13020,20 @@ Neste steg:
|
|||
**Rullet ut 2026-08-19** -- bruker bekreftet. Ingen migrasjon.
|
||||
`docker compose build teecup_api teecup_frontend && up -d` for
|
||||
begge, rene logger, 200 OK (inkl. ny `/hjelp`-rute).
|
||||
|
||||
127. **Spillertabellen gjort mer "regneark-aktig" + fyller full bredde --
|
||||
2026-08-19 (tillegg til ADR-094).** Bruker, med skjermdump av den
|
||||
ekte 51-spiller-tabellen: "skulle ønske tabellen var mer 'regneark-
|
||||
aktig'" og "hvorfor må jeg scrolle horisontalt når jeg har så mye
|
||||
ledig skjermplass?" -- gyldig, bekreftet i skjermdumpen. Bruker tok
|
||||
dette direkte til V0 selv og lastet opp svaret. Ny versjon:
|
||||
rutenett-linjer i stedet for kort-følelse, lavere radhøyde, mindre
|
||||
tekst, og et `<colgroup>` som gir faste smale bredder til korte
|
||||
kolonner mens rundekolonnene fordeler resten av bredden seg
|
||||
imellom -- fyller skjermen i stedet for fast sidescroll. Props-
|
||||
kontrakten uendret, ingen endring i `SetupTab` sin sammenkobling.
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Ny scratch-
|
||||
visning bekrefter bred skjerm fylt uten sidescroll, lys+mørk.
|
||||
|
||||
**Rullet ut:** venter på bekreftelse.
|
||||
|
|
|
|||
|
|
@ -64,9 +64,19 @@ const STATUSES: { label: string; value: TournamentPlayerRow["status"] }[] = [
|
|||
{ label: "DNS", value: "dns" },
|
||||
]
|
||||
|
||||
// Shared control styling so every inline field feels like one family.
|
||||
const CONTROL =
|
||||
"h-9 rounded-lg border border-border bg-background px-2.5 text-sm text-foreground transition-all duration-200 ease-in-out hover:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
// Spreadsheet cell control: flat, fills the whole cell, no border of its own.
|
||||
// The grid lines define the structure; controls only light up on hover/focus.
|
||||
const CELL =
|
||||
"h-full w-full border-0 bg-transparent px-2 py-1.5 text-[13px] leading-tight text-foreground outline-none transition-colors hover:bg-accent/40 focus-visible:bg-background focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
|
||||
// Compact header control (round default-tee select).
|
||||
const HEAD_CELL =
|
||||
"h-7 w-full rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground outline-none transition-colors hover:border-primary/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
|
||||
// Shared cell frame: thin grid lines, zero padding (the control supplies it).
|
||||
const TD = "border-b border-r border-border p-0 align-middle"
|
||||
const TH =
|
||||
"border-b border-r border-border bg-muted px-2 py-2 text-left text-[11px] font-bold uppercase tracking-wide text-muted-foreground"
|
||||
|
||||
function computeAge(birthDate: string): number | null {
|
||||
const birth = new Date(birthDate)
|
||||
|
|
@ -133,34 +143,49 @@ export function TournamentPlayersTable({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto rounded-2xl border border-border bg-card shadow-md shadow-black/8">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<div className="overflow-x-auto rounded-xl border border-border bg-card shadow-sm shadow-black/5">
|
||||
{/* Fluid layout: the table fills the available width instead of leaving
|
||||
dead space and pushing round columns off-screen. */}
|
||||
<table className="w-full border-collapse text-left [&_td]:last:border-r-0 [&_th]:last:border-r-0">
|
||||
<colgroup>
|
||||
<col className="w-[13rem]" />
|
||||
<col className="w-[5.5rem]" />
|
||||
{showClassColumn && <col className="w-[9rem]" />}
|
||||
<col className="w-[9rem]" />
|
||||
<col className="w-[7rem]" />
|
||||
<col className="w-[7rem]" />
|
||||
<col className="w-[10rem]" />
|
||||
{rounds.map((r) => (
|
||||
<col key={r.id} />
|
||||
))}
|
||||
<col className="w-[5.5rem]" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="divide-x divide-border border-b border-border">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="sticky left-0 z-20 bg-card px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground"
|
||||
className={`${TH} sticky left-0 top-0 z-30`}
|
||||
>
|
||||
Spiller
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20 text-right`}>
|
||||
Hcp
|
||||
</th>
|
||||
{showClassColumn && (
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
||||
Klasse
|
||||
</th>
|
||||
)}
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
||||
Statistikknivå
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
||||
Kjønn
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
||||
Alder
|
||||
</th>
|
||||
{rounds.map((round) => {
|
||||
|
|
@ -174,23 +199,21 @@ export function TournamentPlayersTable({
|
|||
<th
|
||||
key={round.id}
|
||||
scope="col"
|
||||
className="min-w-52 px-4 py-3 align-top text-xs font-bold uppercase tracking-wide text-muted-foreground"
|
||||
className={`${TH} sticky top-0 z-20 min-w-[9.5rem] align-top`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span>{round.label}</span>
|
||||
{busy && (
|
||||
<span className="flex items-center gap-1 normal-case text-primary">
|
||||
<Loader2 aria-hidden="true" className="size-3.5 animate-spin" />
|
||||
<span className="text-[11px] font-semibold">Oppdaterer</span>
|
||||
<span className="text-[10px] font-semibold">Oppdaterer</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<label className="flex flex-col gap-1 normal-case">
|
||||
<span className="text-[11px] font-semibold text-muted-foreground">Standardutslag</span>
|
||||
<select
|
||||
aria-label={`Standardutslag for ${round.label}`}
|
||||
className={CONTROL}
|
||||
className={HEAD_CELL}
|
||||
value={defaultTees[round.id] ?? ""}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
|
|
@ -203,7 +226,6 @@ export function TournamentPlayersTable({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<BulkCheckbox
|
||||
checked={allAssigned}
|
||||
indeterminate={someAssigned}
|
||||
|
|
@ -217,15 +239,13 @@ export function TournamentPlayersTable({
|
|||
</th>
|
||||
)
|
||||
})}
|
||||
<th scope="col" className="px-4 py-3 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
Spillerpool
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
Fjern
|
||||
<th scope="col" className={`${TH} sticky top-0 z-20 text-center`}>
|
||||
<span className="sr-only">Handlinger</span>
|
||||
<span aria-hidden="true">···</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tbody>
|
||||
{players.map((player) => (
|
||||
<PlayerRow
|
||||
key={player.participantId}
|
||||
|
|
@ -273,16 +293,16 @@ function BulkCheckbox({
|
|||
if (ref.current) ref.current.indeterminate = indeterminate && !checked
|
||||
}, [indeterminate, checked])
|
||||
return (
|
||||
<label className="flex items-center gap-2 normal-case">
|
||||
<label className="flex items-center gap-1.5 normal-case">
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
className="size-5 rounded border-border text-primary transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="size-4 rounded border-border text-primary outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] font-semibold text-muted-foreground">{label}</span>
|
||||
<span className="text-[10px] font-semibold text-muted-foreground">{label}</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
|
@ -352,15 +372,15 @@ function PlayerRow({
|
|||
}
|
||||
|
||||
return (
|
||||
<tr className="divide-x divide-border transition-colors duration-200 ease-in-out hover:bg-accent/40">
|
||||
<tr className="group h-9">
|
||||
<th
|
||||
scope="row"
|
||||
className="sticky left-0 z-10 bg-card px-4 py-3 text-left align-middle text-sm font-bold text-foreground"
|
||||
className={`${TD} sticky left-0 z-10 bg-card px-2 py-1.5 text-left text-[13px] font-semibold text-foreground group-hover:bg-accent/30`}
|
||||
>
|
||||
{player.playerName}
|
||||
</th>
|
||||
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<td className={TD}>
|
||||
<input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
|
|
@ -371,15 +391,15 @@ function PlayerRow({
|
|||
value={hcpDraft}
|
||||
onChange={(e) => setHcpDraft(e.target.value)}
|
||||
onBlur={commitHcp}
|
||||
className={`${CONTROL} w-20 text-right tabular-nums`}
|
||||
className={`${CELL} text-right tabular-nums`}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{showClassColumn && (
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<td className={TD}>
|
||||
<select
|
||||
aria-label={`Klasse for ${player.playerName}`}
|
||||
className={`${CONTROL} w-40`}
|
||||
className={CELL}
|
||||
value={player.classId ?? ""}
|
||||
onChange={(e) => onSetClass(player.participantId, e.target.value === "" ? null : e.target.value)}
|
||||
>
|
||||
|
|
@ -393,10 +413,10 @@ function PlayerRow({
|
|||
</td>
|
||||
)}
|
||||
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<td className={TD}>
|
||||
<select
|
||||
aria-label={`Statistikknivå for ${player.playerName}`}
|
||||
className={`${CONTROL} w-36`}
|
||||
className={CELL}
|
||||
value={player.statLevel}
|
||||
onChange={(e) => onSetStatLevel(player.participantId, e.target.value as TournamentPlayerRow["statLevel"])}
|
||||
>
|
||||
|
|
@ -408,12 +428,10 @@ function PlayerRow({
|
|||
</select>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<td className={TD}>
|
||||
<select
|
||||
aria-label={`Status for ${player.playerName}`}
|
||||
className={`${CONTROL} w-28 ${
|
||||
statusAbnormal ? "border-destructive/40 bg-destructive/10 text-destructive" : ""
|
||||
}`}
|
||||
className={`${CELL} ${statusAbnormal ? "bg-destructive/10 font-semibold text-destructive hover:bg-destructive/15" : ""}`}
|
||||
value={player.status}
|
||||
onChange={(e) => onSetStatus(player.participantId, e.target.value as TournamentPlayerRow["status"])}
|
||||
>
|
||||
|
|
@ -425,10 +443,10 @@ function PlayerRow({
|
|||
</select>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<td className={TD}>
|
||||
<select
|
||||
aria-label={`Kjønn for ${player.playerName}`}
|
||||
className={`${CONTROL} w-28`}
|
||||
className={CELL}
|
||||
value={player.gender ?? ""}
|
||||
onChange={(e) => onChangeGender(player.playerId, e.target.value as "m" | "f" | "x")}
|
||||
>
|
||||
|
|
@ -441,16 +459,16 @@ function PlayerRow({
|
|||
</select>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<div className="flex items-center gap-2">
|
||||
<td className={TD}>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="date"
|
||||
aria-label={`Fødselsdato for ${player.playerName}`}
|
||||
className={`${CONTROL} w-40`}
|
||||
className={`${CELL} min-w-0`}
|
||||
value={player.birthDate ?? ""}
|
||||
onChange={(e) => onChangeBirthDate(player.playerId, e.target.value)}
|
||||
/>
|
||||
<span className="min-w-12 text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
<span className="shrink-0 whitespace-nowrap px-1.5 text-[11px] font-semibold tabular-nums text-muted-foreground">
|
||||
{age !== null ? `${age} år` : "Ukjent"}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -461,14 +479,14 @@ function PlayerRow({
|
|||
const assigned = cell?.assigned ?? false
|
||||
const busy = busyRoundIds.includes(round.id)
|
||||
return (
|
||||
<td key={round.id} className="px-4 py-3 align-middle">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<td key={round.id} className={TD}>
|
||||
<div className="flex items-center gap-1.5 px-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assigned}
|
||||
disabled={busy}
|
||||
aria-label={`${player.playerName} spiller ${round.label}`}
|
||||
title={assigned ? "Spiller runden" : "Spiller ikke runden"}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked
|
||||
const teeId = checked
|
||||
|
|
@ -476,16 +494,12 @@ function PlayerRow({
|
|||
: cell?.teeId ?? round.tees[0]?.id ?? ""
|
||||
onToggleRound(round.id, player.participantId, checked, teeId)
|
||||
}}
|
||||
className="size-5 rounded border-border text-primary transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="size-4 shrink-0 rounded border-border text-primary outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-xs font-semibold text-muted-foreground">
|
||||
{assigned ? "Spiller" : "Ikke satt"}
|
||||
</span>
|
||||
</label>
|
||||
{assigned && (
|
||||
{assigned ? (
|
||||
<select
|
||||
aria-label={`Utslag for ${player.playerName} i ${round.label}`}
|
||||
className={`${CONTROL} w-28`}
|
||||
className="h-7 min-w-0 flex-1 rounded-md border border-border bg-background px-1.5 text-xs text-foreground outline-none transition-colors hover:border-primary/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={cell?.teeId ?? ""}
|
||||
disabled={busy}
|
||||
onChange={(e) => onChangeRoundTee(round.id, player.participantId, e.target.value)}
|
||||
|
|
@ -496,31 +510,36 @@ function PlayerRow({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span aria-hidden="true" className="flex-1 text-xs text-muted-foreground/60">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
|
||||
<td className="px-4 py-3 align-middle">
|
||||
<td className={TD}>
|
||||
<div className="flex items-center justify-center gap-0.5 px-1">
|
||||
<a
|
||||
href={`/organizations/${organizationId}/players?highlight=${player.playerId}`}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-muted-foreground underline-offset-2 transition-colors duration-200 ease-in-out hover:text-foreground hover:underline"
|
||||
aria-label={`Rediger ${player.playerName} i spillerpoolen`}
|
||||
title="Rediger i spillerpoolen"
|
||||
className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<ExternalLink aria-hidden="true" className="size-3.5" />
|
||||
Rediger i spillerpoolen
|
||||
<ExternalLink aria-hidden="true" className="size-4" />
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 text-right align-middle">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Fjern ${player.playerName}`}
|
||||
title="Fjern spiller"
|
||||
onClick={() => onRemoveParticipant(player.participantId)}
|
||||
className="inline-flex size-9 items-center justify-center rounded-lg text-muted-foreground transition-all duration-200 ease-in-out hover:bg-destructive/10 hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring active:scale-95"
|
||||
className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:ring-2 focus-visible:ring-ring active:scale-95"
|
||||
>
|
||||
<Trash2 aria-hidden="true" className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue