Skip to content

Commit ecde254

Browse files
ryanbarlow97claude
andcommitted
Centre the staff panel and put its title and tabs on one row
Every staff page sat in a 64rem column pinned to the left of a 110rem frame, so on a wide screen the right third was empty. The title and tabs now share one row, centred at 72rem and pinned under the site header on tablets and up. Every page, the map pages included, sits in a centred column of the same width. Accounts puts the staff roster beside the lookup. Players puts search and the view switch on one row and gives the character column 45% of the table. Ranks turns the player search into a slim bar, right-aligns weights and shows Tracks beside Recent changes. Staff panels are a shade lighter, with 6px corners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fc95fac commit ecde254

11 files changed

Lines changed: 162 additions & 142 deletions

File tree

‎frontend/app/admin/layout.tsx‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import AdminNav from "../components/admin/AdminNav";
22

33
/**
4-
* One frame for every staff page: the same width and padding throughout, so the
5-
* title and tabs stay put from tab to tab. The map pages fill it; the rest keep
6-
* to a reading column (AdminColumn) on its left edge.
4+
* One frame for every staff page. The title and tabs share one row, centred at
5+
* the width of AdminColumn, so they stay put from tab to tab.
76
*/
87
export default function AdminLayout({ children }: { children: React.ReactNode }) {
98
return (
10-
<main className="staff-panel relative mx-auto flex min-h-[calc(100dvh-var(--tfmc-header-h))] max-w-[110rem] flex-col px-4 py-10 sm:px-6">
11-
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl text-[var(--tfmc-cream)] sm:text-4xl">
12-
Staff panel
13-
</h1>
14-
<AdminNav />
9+
<main className="staff-panel relative mx-auto flex min-h-[calc(100dvh-var(--tfmc-header-h))] w-full max-w-[110rem] flex-col px-4 pb-10 sm:px-6">
10+
{/* Opaque and pinned under the site header on wider screens, so the tabs stay in reach down a long list. */}
11+
<header className="z-30 -mx-4 bg-[var(--tfmc-forest-deep)] px-4 pt-6 sm:-mx-6 sm:px-6 md:sticky md:top-[var(--tfmc-header-h)]">
12+
<div className="mx-auto flex max-w-[72rem] flex-wrap items-end gap-x-8 gap-y-1 border-b border-[color-mix(in_srgb,var(--tfmc-cream)_14%,transparent)]">
13+
<h1 className="pb-2 font-[family-name:var(--font-fraunces)] text-2xl text-[var(--tfmc-cream)] sm:text-3xl">
14+
Staff panel
15+
</h1>
16+
<AdminNav />
17+
</div>
18+
</header>
1519
{children}
1620
</main>
1721
);
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Suspense } from "react";
2+
import AdminColumn from "../../components/admin/AdminColumn";
23
import EveryoneMovement from "../../components/admin/EveryoneMovement";
34

45
export default function AdminMovementPage() {
56
// The view lives in the URL's query, which needs a Suspense boundary.
67
return (
7-
<Suspense fallback={<p className="mt-6 text-[var(--tfmc-mist)]">Loading…</p>}>
8-
<EveryoneMovement />
9-
</Suspense>
8+
<AdminColumn>
9+
<Suspense fallback={<p className="mt-6 text-[var(--tfmc-mist)]">Loading…</p>}>
10+
<EveryoneMovement />
11+
</Suspense>
12+
</AdminColumn>
1013
);
1114
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import Link from "next/link";
22
import { Suspense } from "react";
3+
import AdminColumn from "../../../../components/admin/AdminColumn";
34
import PlayerMovementPage from "../../../../components/admin/PlayerMovementPage";
45
import PlayerTabs from "../../../../components/admin/PlayerTabs";
56

67
export default async function AdminPlayerMovementPage({ params }: { params: Promise<{ uuid: string }> }) {
78
const { uuid } = await params;
89
const id = decodeURIComponent(uuid);
910
return (
10-
<>
11+
<AdminColumn>
1112
<Link href="/admin/players" className="mt-4 text-sm text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]">
1213
← All players
1314
</Link>
@@ -17,6 +18,6 @@ export default async function AdminPlayerMovementPage({ params }: { params: Prom
1718
<Suspense fallback={<p className="mt-6 text-[var(--tfmc-mist)]">Loading…</p>}>
1819
<PlayerMovementPage key={id} uuid={id} />
1920
</Suspense>
20-
</>
21+
</AdminColumn>
2122
);
2223
}

‎frontend/app/admin/rail/page.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import AdminColumn from "../../components/admin/AdminColumn";
12
import RailOverview from "../../components/admin/RailOverview";
23

34
export default function AdminRailPage() {
4-
return <RailOverview />;
5+
return (
6+
<AdminColumn>
7+
<RailOverview />
8+
</AdminColumn>
9+
);
510
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** The reading column for staff pages without a map: one width, on the frame's left edge. */
1+
/** The column every staff page sits in: centred, at the same width as the title and tabs. */
22
export default function AdminColumn({ children }: { children: React.ReactNode }) {
3-
return <div className="flex max-w-5xl flex-col">{children}</div>;
3+
return <div className="mx-auto flex w-full max-w-[72rem] flex-col">{children}</div>;
44
}

‎frontend/app/components/admin/AdminNav.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function adminSection(pathname: string): AdminSection | null {
2727
export default function AdminNav() {
2828
const current = adminSection(usePathname() ?? "");
2929
return (
30-
<nav aria-label="Staff panel" className="mt-4 flex gap-4 border-b sm:gap-5 border-[color-mix(in_srgb,var(--tfmc-cream)_14%,transparent)]">
30+
<nav aria-label="Staff panel" className="flex gap-4 sm:gap-5">
3131
{LINKS.map((link) => (
3232
<Link
3333
key={link.key}

‎frontend/app/components/admin/AdminPanel.tsx‎

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import AccountActions from "./AccountActions";
1616
import { StaffGateMessage, gateKind, type GateKind } from "./StaffGate";
1717

1818
const panelClass =
19-
"mt-6 rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_18%,transparent)] bg-[color-mix(in_srgb,var(--tfmc-forest)_28%,transparent)] p-5";
19+
"mt-6 rounded-md border border-[color-mix(in_srgb,var(--tfmc-cream)_14%,transparent)] bg-[color-mix(in_srgb,var(--tfmc-forest)_55%,transparent)] p-5 sm:p-6";
2020
const headingClass = "font-[family-name:var(--font-fraunces)] text-xl text-[var(--tfmc-cream)]";
2121
const buttonClass =
2222
"inline-flex items-center justify-center rounded-sm bg-[var(--tfmc-accent)] px-4 py-2 text-sm font-semibold text-[var(--tfmc-forest-deep)] transition-opacity hover:opacity-90 disabled:opacity-50";
@@ -87,42 +87,45 @@ export default function AdminPanel() {
8787
change here is recorded with your reason.
8888
</p>
8989

90-
<section className={panelClass} aria-label="Staff">
91-
<h2 className={headingClass}>Staff</h2>
92-
<AccountTable accounts={staff} me={me} onChanged={afterChange} empty="No staff yet." />
93-
</section>
90+
{/* The roster beside the lookup on wide screens; stacked below that. */}
91+
<div className="grid gap-x-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)] lg:items-start">
92+
<section className={panelClass} aria-label="Staff">
93+
<h2 className={headingClass}>Staff</h2>
94+
<AccountTable accounts={staff} me={me} onChanged={afterChange} empty="No staff yet." />
95+
</section>
9496

95-
<section className={panelClass} aria-label="Find an account">
96-
<h2 className={headingClass}>Find an account</h2>
97-
<form onSubmit={onSearch} className="mt-3 flex flex-wrap gap-3">
98-
<label className="sr-only" htmlFor="admin-lookup">
99-
Discord username, display name or ID
100-
</label>
101-
<input
102-
id="admin-lookup"
103-
value={query}
104-
onChange={(e) => setQuery(e.target.value)}
105-
placeholder="Discord username, display name or ID"
106-
autoComplete="off"
107-
spellCheck={false}
108-
className={`${inputClass} min-w-0 flex-1`}
109-
/>
110-
<button type="submit" disabled={searching} className={buttonClass}>
111-
{searching ? "Searching…" : "Find"}
112-
</button>
113-
</form>
114-
<p className="mt-2 text-xs text-[var(--tfmc-stone)]">
115-
Exact matches only. People appear once they have signed in to the website.
116-
</p>
117-
{searchError ? (
118-
<p className="mt-3 text-sm text-[#e8a0a0]" role="alert">
119-
{searchError}
97+
<section className={panelClass} aria-label="Find an account">
98+
<h2 className={headingClass}>Find an account</h2>
99+
<form onSubmit={onSearch} className="mt-3 flex flex-wrap gap-3">
100+
<label className="sr-only" htmlFor="admin-lookup">
101+
Discord username, display name or ID
102+
</label>
103+
<input
104+
id="admin-lookup"
105+
value={query}
106+
onChange={(e) => setQuery(e.target.value)}
107+
placeholder="Discord username, display name or ID"
108+
autoComplete="off"
109+
spellCheck={false}
110+
className={`${inputClass} min-w-0 flex-1`}
111+
/>
112+
<button type="submit" disabled={searching} className={buttonClass}>
113+
{searching ? "Searching…" : "Find"}
114+
</button>
115+
</form>
116+
<p className="mt-2 text-xs text-[var(--tfmc-stone)]">
117+
Exact matches only. People appear once they have signed in to the website.
120118
</p>
121-
) : null}
122-
{results ? (
123-
<AccountTable accounts={results} me={me} onChanged={afterChange} empty="No account matches that." />
124-
) : null}
125-
</section>
119+
{searchError ? (
120+
<p className="mt-3 text-sm text-[#e8a0a0]" role="alert">
121+
{searchError}
122+
</p>
123+
) : null}
124+
{results ? (
125+
<AccountTable accounts={results} me={me} onChanged={afterChange} empty="No account matches that." />
126+
) : null}
127+
</section>
128+
</div>
126129
</>
127130
);
128131
}

‎frontend/app/components/admin/PlayersDirectory.tsx‎

Lines changed: 74 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ function CharacterTable({ rows, known, sorting }: { rows: CharacterRow[]; known:
241241
<Table
242242
head={
243243
<>
244-
<SortHeader column="name" label="Character" sorting={sorting} className={headClass} />
244+
<SortHeader column="name" label="Character" sorting={sorting} className={`sm:w-[45%] ${headClass}`} />
245245
<th scope="col" className={`hidden sm:table-cell ${headClass}`}>Player</th>
246246
<SortHeader column="last_seen" label="Player last seen" sorting={sorting} className={lastSeenHeadClass} />
247247
</>
@@ -373,79 +373,81 @@ export default function PlayersDirectory({ initialQuery, initialListing, initial
373373

374374
return (
375375
<section aria-label="Players" className="mt-6">
376-
{/* Search gets a row of its own, so the view tabs never squeeze it. */}
377-
<label className="sr-only" htmlFor="player-search">
378-
Search players
379-
</label>
380-
<div className="relative">
381-
<svg
382-
aria-hidden
383-
viewBox="0 0 20 20"
384-
className="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-[var(--tfmc-stone)]"
385-
fill="none"
386-
stroke="currentColor"
387-
strokeWidth="2"
388-
>
389-
<circle cx="8.5" cy="8.5" r="5.5" />
390-
<path d="M13 13l4 4" strokeLinecap="round" />
391-
</svg>
392-
<input
393-
id="player-search"
394-
type="search"
395-
value={query}
396-
onChange={(e) => {
397-
setQuery(e.target.value);
398-
setPage(1);
399-
}}
400-
placeholder="Search by Minecraft, Discord (handle or nickname) or character name, or UUID"
401-
autoComplete="off"
402-
spellCheck={false}
403-
className={inputClass}
404-
/>
405-
{query ? (
406-
<button
407-
type="button"
408-
aria-label="Clear search"
409-
onClick={() => {
410-
setQuery("");
411-
setSearch("");
412-
setPage(1);
413-
}}
414-
className="absolute right-0 top-0 flex h-full w-11 items-center justify-center text-xl text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]"
376+
{/* Search and the view tabs share a row on wide screens; the tabs move under it before they squeeze it. */}
377+
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
378+
<label className="sr-only" htmlFor="player-search">
379+
Search players
380+
</label>
381+
<div className="relative min-w-0 flex-1">
382+
<svg
383+
aria-hidden
384+
viewBox="0 0 20 20"
385+
className="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-[var(--tfmc-stone)]"
386+
fill="none"
387+
stroke="currentColor"
388+
strokeWidth="2"
415389
>
416-
×
417-
</button>
418-
) : null}
419-
</div>
420-
421-
<div
422-
role="tablist"
423-
aria-label="Player views"
424-
className="mt-3 grid grid-cols-3 rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_20%,transparent)] p-0.5 sm:inline-grid"
425-
>
426-
{VIEWS.map((option, index) => (
427-
<button
428-
key={option.key}
429-
ref={(el) => {
430-
tabs.current[index] = el;
390+
<circle cx="8.5" cy="8.5" r="5.5" />
391+
<path d="M13 13l4 4" strokeLinecap="round" />
392+
</svg>
393+
<input
394+
id="player-search"
395+
type="search"
396+
value={query}
397+
onChange={(e) => {
398+
setQuery(e.target.value);
399+
setPage(1);
431400
}}
432-
id={`players-tab-${option.key}`}
433-
type="button"
434-
role="tab"
435-
aria-selected={view === option.key}
436-
aria-controls={PANEL_ID}
437-
tabIndex={view === option.key ? 0 : -1}
438-
onClick={() => choose(option.key)}
439-
onKeyDown={(event) => onTabKey(event, index)}
440-
className={`min-h-11 rounded-sm px-2 text-sm transition-colors sm:min-h-9 sm:px-4 ${
441-
view === option.key
442-
? "bg-[var(--tfmc-accent)] font-semibold text-[var(--tfmc-forest-deep)]"
443-
: "text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]"
444-
}`}
445-
>
446-
{option.label}
447-
</button>
448-
))}
401+
placeholder="Search by Minecraft, Discord (handle or nickname) or character name, or UUID"
402+
autoComplete="off"
403+
spellCheck={false}
404+
className={inputClass}
405+
/>
406+
{query ? (
407+
<button
408+
type="button"
409+
aria-label="Clear search"
410+
onClick={() => {
411+
setQuery("");
412+
setSearch("");
413+
setPage(1);
414+
}}
415+
className="absolute right-0 top-0 flex h-full w-11 items-center justify-center text-xl text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]"
416+
>
417+
×
418+
</button>
419+
) : null}
420+
</div>
421+
422+
<div
423+
role="tablist"
424+
aria-label="Player views"
425+
className="grid shrink-0 grid-cols-3 rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_20%,transparent)] p-0.5 sm:self-start lg:self-auto"
426+
>
427+
{VIEWS.map((option, index) => (
428+
<button
429+
key={option.key}
430+
ref={(el) => {
431+
tabs.current[index] = el;
432+
}}
433+
id={`players-tab-${option.key}`}
434+
type="button"
435+
role="tab"
436+
aria-selected={view === option.key}
437+
aria-controls={PANEL_ID}
438+
tabIndex={view === option.key ? 0 : -1}
439+
onClick={() => choose(option.key)}
440+
onKeyDown={(event) => onTabKey(event, index)}
441+
className={`min-h-11 rounded-sm px-2 text-sm transition-colors sm:min-h-9 sm:px-4 ${
442+
view === option.key
443+
? "bg-[var(--tfmc-accent)] font-semibold text-[var(--tfmc-forest-deep)]"
444+
: "text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]"
445+
}`}
446+
>
447+
{option.label}
448+
</button>
449+
))}
450+
</div>
449451
</div>
450452

451453
<div id={PANEL_ID} role="tabpanel" aria-labelledby={`players-tab-${view}`}>

‎frontend/app/components/admin/profileParts.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** Styles and pieces shared by the player profile's sections. */
22

33
export const panelClass =
4-
"mt-6 rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_18%,transparent)] bg-[color-mix(in_srgb,var(--tfmc-forest)_28%,transparent)] p-5";
4+
"mt-6 rounded-md border border-[color-mix(in_srgb,var(--tfmc-cream)_14%,transparent)] bg-[color-mix(in_srgb,var(--tfmc-forest)_55%,transparent)] p-5 sm:p-6";
55
export const headingClass = "font-[family-name:var(--font-fraunces)] text-xl text-[var(--tfmc-cream)]";
66
export const moreClass =
77
"mt-3 rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_20%,transparent)] px-3 py-1.5 text-sm text-[var(--tfmc-cream)] hover:border-[var(--tfmc-accent)] disabled:opacity-40";

0 commit comments

Comments
 (0)