Rutenett-stil for spillerpool-tabellen, ingen sidescroll på bred skjerm (tillegg til ADR-094)

EditTable (player-import-view.tsx) får samme grid-stil som
spillertabellen, med individuell kolonnebredde per felt slik at alle
13 spillerpool-felt får plass uten sidescroll. mode.kind==="roster"-
grenen bevart ved manuell fletting siden V0-eksporten var en eldre
snapshot uten den varianten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-19 09:35:37 +02:00
parent 47e3874198
commit 7632f53702
3 changed files with 111 additions and 56 deletions

View file

@ -9627,6 +9627,29 @@ navn) -- ren visuell omskriving, ingen endring i `SetupTab` sin
sammenkobling nødvendig. Verifisert i ny lokal scratch-visning (bred sammenkobling nødvendig. Verifisert i ny lokal scratch-visning (bred
skjerm bekreftet fylt uten sidescroll med 3 runde-kolonner, lys+mørk). skjerm bekreftet fylt uten sidescroll med 3 runde-kolonner, lys+mørk).
**Tillegg 2026-08-19 -- samme rutenett-mønster utvidet til spillerpool-
tabellen (`player-import-view.tsx` sin `EditTable`).** Bruker: "Jeg
ønsker at tabellen skal være så bred at jeg slipper å scrolle
sidelengs" -- presisert etter en første runde der jeg feilaktig hadde
bedt V0 om å BEHOLDE horisontal scroll (misforsto "mye bredere" som
"behold bredden", ikke "fyll bredden slik at scroll blir unødvendig").
Denne tabellen viser opptil 13 datafelt + slett-knapp samtidig i
spillerpool-modus (ingen filtrering på kolonnetilknytning der, ulikt
CSV-import-modus) -- retting: samme `table-fixed`+`<colgroup>`-teknikk
som over, men med EN fast pikselbredde PER felt (`COL_W`, 70-190px
avhengig av innholdstype) i stedet for få brede kolonner, slik at alle
13 feltene faktisk får plass på en bred skjerm. V0-eksporten denne
runden var en eldre prosjekt-snapshot uten `"roster"`-varianten av
`Mode`-typen (kun `team`/`individual`) -- fletting gjort manuelt i
stedet for å lime inn rått, for å bevare `mode.kind === "roster"`-
grenen (annen hjelpetekst, ingen lag/klasse-kolonner) og selve
`Mode`/`FieldKey`-typene uendret. `FIELD_MIN_W`-konstanten (nå erstattet
av `COL_W`) fjernet som dødt kode. Verifisert i lokal scratch (5 rader,
alle 13 spillerpool-felt) -- bekreftet ALLE kolonner synlige uten
sidescroll på 1900px, riktig `mode.kind === "roster"`-hjelpetekst
bevart, redigering og sticky navnekolonne fungerer, lys+mørk, smal
skjerm faller korrekt tilbake til sidescroll.
--- ---
## Utviklingsplan (rekkefølge) ## Utviklingsplan (rekkefølge)

View file

@ -13039,3 +13039,25 @@ Neste steg:
**Rullet ut 2026-08-19** -- bruker bekreftet. Ren frontend. **Rullet ut 2026-08-19** -- bruker bekreftet. Ren frontend.
`docker compose build teecup_frontend && up -d`, rene logger, `docker compose build teecup_frontend && up -d`, rene logger,
200 OK. 200 OK.
128. **Samme rutenett-stil utvidet til spillerpool-tabellen -- 2026-08-19
(tillegg til ADR-094).** Bruker: "Jeg ønsker at tabellen skal være
så bred at jeg slipper å scrolle sidelengs" -- presisering etter at
jeg først (feilaktig) hadde bedt V0 om å BEHOLDE sidescroll for
denne tabellen. `player-import-view.tsx` sin `EditTable` (opptil
13 felt + slett-knapp samtidig i spillerpool-modus) fikk samme
`table-fixed`+`<colgroup>`-teknikk som spillertabellen (#127), men
med individuelt tilpasset pikselbredde per felt (70-190px) i stedet
for få brede kolonner -- alle 13 feltene får nå plass uten
sidescroll på en bred skjerm. V0-eksporten manglet `"roster"`-
varianten av `Mode`-typen (eldre snapshot) -- flettet inn manuelt
for å bevare den grenen og selve typene uendret. Fjernet nå-død
`FIELD_MIN_W`-konstant.
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch-
visning (5 rader, alle 13 spillerpool-felt): alle kolonner synlige
uten sidescroll på 1900px, riktig hjelpetekst for spillerpool-modus
bevart, redigering + sticky navnekolonne fungerer, lys+mørk, smal
skjerm faller korrekt tilbake til sidescroll.
**Rullet ut:** venter på bekreftelse.

View file

@ -108,26 +108,6 @@ const FIELD_ORDER: FieldKey[] = [
"comment", "comment",
] ]
// Per-field column width for the edit table. Compact by default so a wide
// screen shows many columns at once; wider for free-text-heavy fields.
const FIELD_MIN_W: Partial<Record<FieldKey, string>> = {
display_name: "min-w-44",
first_name: "min-w-36",
last_name: "min-w-36",
nickname: "min-w-32",
email: "min-w-52",
mobile: "min-w-36",
handicap_index: "min-w-20",
gender: "min-w-28",
birth_date: "min-w-36",
club: "min-w-44",
club_member_number: "min-w-28",
country: "min-w-28",
team_name: "min-w-40",
class_name: "min-w-40",
comment: "min-w-56",
}
const GENDER_OPTIONS: { value: string; label: string }[] = [ const GENDER_OPTIONS: { value: string; label: string }[] = [
{ value: "", label: "—" }, { value: "", label: "—" },
{ value: "female", label: "Kvinne" }, { value: "female", label: "Kvinne" },
@ -429,6 +409,39 @@ function MapStep({
) )
} }
// Realistisk, innholds-tilpasset kolonnebredde (px), ikke en rundhåndet
// min-bredde -- med table-fixed + w-full fordeler nettleseren overskytende
// plass proporsjonalt, så alle ~14 felt (spillerpool-modus) får plass uten
// sidescroll på en bred skjerm (bruker eksplisitt: "jeg ønsker at tabellen
// skal være så bred at jeg slipper å scrolle sidelengs", 2026-08-19).
// overflow-x-auto-wrapperen står igjen som fallback for smal skjerm.
const COL_W: Partial<Record<FieldKey, number>> = {
display_name: 150,
first_name: 110,
last_name: 110,
nickname: 100,
email: 190,
mobile: 120,
handicap_index: 70,
gender: 92,
birth_date: 130,
club: 140,
club_member_number: 104,
country: 90,
team_name: 128,
class_name: 128,
comment: 180,
}
// Rutenett-cellekontroll (2026-08-19): flat, fyller hele cellen, ingen egen
// kant -- rutenett-linjene definerer strukturen, kontrollen lyser kun opp
// ved hover/fokus. Samme mønster som tournament-players-table.tsx.
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"
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 EditTable({ function EditTable({
mode, mode,
rows, rows,
@ -463,19 +476,21 @@ function EditTable({
// narrow screens; on a wide screen every column is visible. `relative` // narrow screens; on a wide screen every column is visible. `relative`
// gives absolutely-positioned descendants (sr-only) a clipped block. // gives absolutely-positioned descendants (sr-only) a clipped block.
<div className="relative min-w-0 overflow-x-auto rounded-2xl border border-border"> <div className="relative min-w-0 overflow-x-auto rounded-2xl border border-border">
<table className="w-full border-collapse text-left"> <table className="w-full table-fixed border-collapse text-left">
<thead className="sticky top-0 z-10"> <colgroup>
<tr className="border-b border-border bg-muted">
{editFields.map((field) => ( {editFields.map((field) => (
<th <col key={field} style={{ width: COL_W[field] ?? 120 }} />
key={field} ))}
scope="col" <col style={{ width: 72 }} />
className="whitespace-nowrap px-3 py-2.5 text-xs font-extrabold uppercase tracking-wide text-muted-foreground" </colgroup>
> <thead className="sticky top-0 z-20">
<tr className="h-9">
{editFields.map((field, i) => (
<th key={field} scope="col" className={cn(TH, "truncate", i === 0 && "sticky left-0 z-30")}>
{FIELD_LABELS[field]} {FIELD_LABELS[field]}
</th> </th>
))} ))}
<th scope="col" className="px-3 py-2.5 text-right"> <th scope="col" className={cn(TH, "border-r-0 text-right")}>
<span className="sr-only">Handlinger</span> <span className="sr-only">Handlinger</span>
</th> </th>
</tr> </tr>
@ -490,19 +505,16 @@ function EditTable({
// scrolle til riktig rad uten å måtte utvide selve // scrolle til riktig rad uten å måtte utvide selve
// props-kontrakten eller endre noe synlig her. // props-kontrakten eller endre noe synlig her.
data-row-key={row.key} data-row-key={row.key}
className={cn( className="group h-9 hover:bg-accent/20"
"border-b border-border last:border-b-0",
rowIndex % 2 === 1 && "bg-muted/30",
)}
> >
{editFields.map((field) => ( {editFields.map((field, i) => (
<td key={field} className="px-2 py-1.5 align-middle"> <td key={field} className={cn(TD, i === 0 && "sticky left-0 z-10 bg-card group-hover:bg-accent/20")}>
{field === "gender" ? ( {field === "gender" ? (
<select <select
aria-label={`${FIELD_LABELS[field]} for rad ${rowIndex + 1}`} aria-label={`${FIELD_LABELS[field]} for rad ${rowIndex + 1}`}
value={row[field] ?? ""} value={row[field] ?? ""}
onChange={(e) => onCellChange(row.key, field, e.target.value)} onChange={(e) => onCellChange(row.key, field, e.target.value)}
className={cn(CONTROL_BASE, FIELD_MIN_W[field], "w-full px-2 text-sm font-semibold")} className={cn(CELL, "cursor-pointer font-medium")}
> >
{GENDER_OPTIONS.map((opt) => ( {GENDER_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}> <option key={opt.value} value={opt.value}>
@ -515,7 +527,7 @@ function EditTable({
aria-label={`Lag for rad ${rowIndex + 1}`} aria-label={`Lag for rad ${rowIndex + 1}`}
value={row[field] ?? ""} value={row[field] ?? ""}
onChange={(e) => onCellChange(row.key, field, e.target.value)} onChange={(e) => onCellChange(row.key, field, e.target.value)}
className={cn(CONTROL_BASE, FIELD_MIN_W[field], "w-full px-2 text-sm font-semibold")} className={cn(CELL, "cursor-pointer font-medium")}
> >
<option value=""></option> <option value=""></option>
{mode.teams.map((t) => ( {mode.teams.map((t) => (
@ -529,7 +541,7 @@ function EditTable({
aria-label={`Klasse for rad ${rowIndex + 1}`} aria-label={`Klasse for rad ${rowIndex + 1}`}
value={row[field] ?? ""} value={row[field] ?? ""}
onChange={(e) => onCellChange(row.key, field, e.target.value)} onChange={(e) => onCellChange(row.key, field, e.target.value)}
className={cn(CONTROL_BASE, FIELD_MIN_W[field], "w-full px-2 text-sm font-semibold")} className={cn(CELL, "cursor-pointer font-medium")}
> >
<option value=""></option> <option value=""></option>
{mode.classes.map((c) => ( {mode.classes.map((c) => (
@ -544,27 +556,25 @@ function EditTable({
value={row[field] ?? ""} value={row[field] ?? ""}
inputMode={field === "email" ? "email" : field === "mobile" ? "tel" : "text"} inputMode={field === "email" ? "email" : field === "mobile" ? "tel" : "text"}
onChange={(e) => onCellChange(row.key, field, e.target.value)} onChange={(e) => onCellChange(row.key, field, e.target.value)}
className={cn(CONTROL_BASE, FIELD_MIN_W[field], "w-full px-2.5 text-sm")} className={cn(CELL, field === "handicap_index" && "text-right tabular-nums")}
/> />
)} )}
</td> </td>
))} ))}
<td className="px-2 py-1.5 align-middle"> <td className={cn(TD, "border-r-0 px-1 text-center")}>
<div className="flex justify-end">
<button <button
type="button" type="button"
onClick={() => onDeleteRow(row.key)} onClick={() => onDeleteRow(row.key)}
aria-label={`Slett rad ${rowIndex + 1}${row.display_name ? ` for ${row.display_name}` : ""}`} aria-label={`Slett rad ${rowIndex + 1}${row.display_name ? ` for ${row.display_name}` : ""}`}
className={cn( className={cn(
"inline-flex h-9 items-center gap-1.5 rounded-lg border border-border px-2.5 text-sm font-bold text-muted-foreground transition-colors", "inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs font-bold text-muted-foreground transition-colors",
"hover:border-destructive/40 hover:bg-destructive/10 hover:text-destructive active:scale-[0.98]", "hover:bg-destructive/10 hover:text-destructive active:scale-[0.98]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
)} )}
> >
<Trash2 aria-hidden="true" className="size-4" /> <Trash2 aria-hidden="true" className="size-3.5" />
Slett Slett
</button> </button>
</div>
</td> </td>
</tr> </tr>
))} ))}