-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Bridge 12-tab mobile sweep — every /admin tab hand-composed at 390px #800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -48,6 +48,17 @@ function byStatusRank(a: FeatureHealth, b: FeatureHealth): number { | |||||||||||||||||||||||||||||||||||||||||||||||||
| return STATUS_RANK[a.status] - STATUS_RANK[b.status]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Mirrors the group header's own "N healthy · M no data" phrasing (this | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * file, header rollup below) so the below-`md` disclosure CTA never claims | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * a flat "healthy" count for a set that also folds in neutral (no | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * feature-tagged data) chips. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| function formatCollapsedSummary(counts: Record<FeatureStatus, number>): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const parts: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (counts.green > 0) parts.push(`${counts.green} healthy`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (counts.neutral > 0) parts.push(`${counts.neutral} no data`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return parts.join(' · '); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function FeatureChip({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| feature, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| selected, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -68,19 +79,32 @@ function FeatureChip({ | |||||||||||||||||||||||||||||||||||||||||||||||||
| aria-pressed={selected} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={`${feature.label}: ${STATUS_WORD[feature.status]} — ${feature.reason}`} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'h-auto min-h-0 w-full justify-start gap-2 rounded-xl border px-3 py-2 text-left normal-case', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'h-auto min-h-0 w-full rounded-xl border px-3 py-2 text-left normal-case', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| richGreen | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'border-accent-500/35 bg-accent-50/60 hover:bg-accent-50' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'border-border-subtle bg-surface', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <StatusPill tone={TONE_FOR_STATUS[feature.status]} dot size="sm" className="min-w-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Icon size={12} aria-hidden /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="truncate">{feature.label}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </StatusPill> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="ml-auto flex flex-shrink-0 items-center gap-1 font-fw-mono text-xs tabular-nums text-warm-500" aria-hidden> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{TREND_ARROW[feature.trend]}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{feature.drillIn.warnings24h + feature.topSignatures.reduce((n, s) => n + s.count, 0)}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Button always wraps its `children` in a single inner `<span>` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (src/components/fairway/controls/button.tsx) — passing two | ||||||||||||||||||||||||||||||||||||||||||||||||||
| sibling elements there (as this row used to) mixes an inline | ||||||||||||||||||||||||||||||||||||||||||||||||||
| box (StatusPill) with a block-level one (a `flex` span), which | ||||||||||||||||||||||||||||||||||||||||||||||||||
| the CSS inline-formatting-context rules split onto its own line, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| silently breaking the trailing trend/count out from the label AND | ||||||||||||||||||||||||||||||||||||||||||||||||||
| making `ml-auto` a no-op (no flex ancestor to push against). One | ||||||||||||||||||||||||||||||||||||||||||||||||||
| wrapping row here — passed as Button's ONE child — owns the flex | ||||||||||||||||||||||||||||||||||||||||||||||||||
| context itself instead, so identity (icon + label) and the 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| key stats (trend, incident count) render as one true row, not two | ||||||||||||||||||||||||||||||||||||||||||||||||||
| stacked ones — the card-ify treatment doctrine rule 8 asks for. */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="flex w-full min-w-0 items-center justify-between gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <StatusPill tone={TONE_FOR_STATUS[feature.status]} dot size="sm" className="min-w-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Icon size={12} aria-hidden /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="truncate">{feature.label}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </StatusPill> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="flex flex-shrink-0 items-center gap-1 font-fw-mono text-xs tabular-nums text-warm-500" aria-hidden> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{TREND_ARROW[feature.trend]}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{feature.drillIn.warnings24h + feature.topSignatures.reduce((n, s) => n + s.count, 0)}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -99,16 +123,93 @@ function FeatureGroup({ | |||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const selected = features.find((f) => f.key === selectedKey) ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const sorted = [...features].sort(byStatusRank); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Below `md` only: green AND neutral chips collapse behind one disclosure | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // per group (Mobile Doctrine rule 3 — BaseballHelm alone registers ~48 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // features; a flat single-column list at 390px would run 15+ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // screen-heights). Neutral (no feature-tagged data yet) must fold too: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // feature tagging only began 2026-07-02, so a group can still be mostly | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // neutral this soon after instrumentation — leaving it uncollapsed blows | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // the cap even with green hidden. Toggling shows them at every | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // breakpoint; `md:block` below always wins at `md` and up, so desktop | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // keeps rendering every chip, untouched. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [showHealthy, setShowHealthy] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const counts = sorted.reduce( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (acc, f) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| acc[f.status] += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return acc; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { red: 0, amber: 0, neutral: 0, green: 0 } as Record<FeatureStatus, number>, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const needsEyes = counts.red + counts.amber; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Collapsed-by-default set = green AND neutral (REPAIR: neutral used to | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // stay in the flat list uncollapsed — feature tagging only began | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // 2026-07-02, so a group can be mostly neutral this soon after | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // instrumentation and still blow the rule-3 ~3-screen-height cap even | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // with green folded away). The header rollup above already reports both | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // counts ("N healthy · M no data"), so the CTA mirrors that phrasing. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const collapsedCount = counts.green + counts.neutral; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <section aria-label={heading} className="min-w-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Eyebrow as="h2" tone="secondary"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {heading} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </Eyebrow> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Rollup header: the app label plus an at-a-glance status count, so | ||||||||||||||||||||||||||||||||||||||||||||||||||
| a thumb scanning the Health tab knows whether a lane needs eyes | ||||||||||||||||||||||||||||||||||||||||||||||||||
| before reading a single chip — the whole point of a daily triage | ||||||||||||||||||||||||||||||||||||||||||||||||||
| surface (rule 1's spirit applied to this sub-view). */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 border-b border-accent-600/25 pb-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Eyebrow as="h2" tone="secondary"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {heading} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </Eyebrow> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="font-fw-mono text-xs tabular-nums text-warm-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {needsEyes > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {counts.red > 0 ? <span className="font-semibold text-fw-danger">{counts.red} red</span> : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {counts.red > 0 && counts.amber > 0 ? ' · ' : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {counts.amber > 0 ? <span className="font-semibold text-fw-warning-ink">{counts.amber} amber</span> : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : counts.neutral > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Neutral (no feature-tagged data yet) is never relabeled | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // "healthy" — same honesty rule the page's own copy states. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {counts.green} healthy · {counts.neutral} no data | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="text-accent-700">{counts.green} healthy</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {sorted.map((f) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <FeatureChip key={f.key} feature={f} selected={f.key === selectedKey} onSelect={onSelect} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {sorted.map((f) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const collapsedOnPhone = f.status !== 'red' && f.status !== 'amber' && !showHealthy; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div key={f.key} className={cn('min-w-0', collapsedOnPhone && 'hidden md:block')}> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <FeatureChip feature={f} selected={f.key === selectedKey} onSelect={onSelect} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {collapsedCount > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fullWidth | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setShowHealthy((s) => !s)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-expanded={showHealthy} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'mt-2 justify-start whitespace-normal rounded-xl px-3 py-2 text-left text-xs font-medium md:hidden', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| needsEyes === 0 && !showHealthy | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'bg-fw-success-bg text-accent-700 hover:bg-fw-success-bg/80' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'text-warm-500 hover:text-warm-700', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {showHealthy | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'Hide healthy & no-data features' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : needsEyes === 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? `All ${formatCollapsedSummary(counts)} — show list →` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : `Show ${formatCollapsedSummary(counts)} →`} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+199
to
+210
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not style an all-neutral group as healthy. At [details] - needsEyes === 0 && !showHealthy
+ needsEyes === 0 && counts.green > 0 && counts.neutral === 0 && !showHealthy
...
- needsEyes === 0
+ needsEyes === 0 && counts.green > 0 && counts.neutral === 0[/details] 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {selected ? <FeatureHealthCard feature={selected} /> : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import Link from 'next/link'; | |
| import { StatusPill } from '@/components/fairway'; | ||
| import { cn } from '@/lib/utils'; | ||
| import type { TeamHealth } from '@/lib/admin/data/golf'; | ||
| import { LocalTime } from './LocalTime'; | ||
|
|
||
| const HEALTH_TONE: Record<TeamHealth, 'success' | 'warning' | 'danger'> = { | ||
| active: 'success', | ||
|
|
@@ -24,11 +25,12 @@ export interface TeamHealthEntry { | |
| /** | ||
| * Shared with W9 (baseball) / W10 (users) — sport-agnostic. | ||
| * | ||
| * PHONE-FORMAT RESPONSIVE (owner directive 2026-07-02): `overflow-x-auto` | ||
| * scopes the horizontal scroll to the table itself (never the page), and the | ||
| * first column stays `sticky` so the team's identity is never scrolled out | ||
| * of view on a 375px viewport. Mirrors the cron-board table pattern in | ||
| * `/admin/jobs`. | ||
| * MOBILE (doctrine Rule 8, 2026-07-10): below `md` each team renders as a | ||
| * full-width tap-through card row (identity + roster/last-activity line + | ||
| * health pill + honest error count) — the min-w table would otherwise force | ||
| * a horizontal scroller on a phone-primary reading surface, which Rule 8 | ||
| * bans even when scroll-contained. The table (sticky identity column, | ||
| * `overflow-x-auto` scoped to itself) still owns `md` and up, unchanged. | ||
| * | ||
| * GREEN CONTRACT (Bridge V2, 2026-07-02): a hairline helm-green rule under | ||
| * the header, heavy graphite (never green) numerals for roster/error counts, | ||
|
|
@@ -40,7 +42,46 @@ export interface TeamHealthEntry { | |
| */ | ||
| export function TeamHealthTable({ teams }: { teams: TeamHealthEntry[] }) { | ||
| return ( | ||
| <div className="overflow-x-auto"> | ||
| <> | ||
| {/* Phone: doctrine-8 card rows, whole row is the link. */} | ||
| <div className="divide-y divide-warm-200/60 md:hidden"> | ||
| {teams.map((t) => { | ||
| const isLeader = t.health === 'active' && t.errors7d === 0; | ||
| return ( | ||
| <Link | ||
| key={t.teamId} | ||
| href={t.href ?? `/admin/teams/${t.teamId}`} | ||
| className={cn( | ||
| 'block rounded-fw-md px-2 py-3 transition-colors hover:bg-surface-sunken', | ||
| isLeader && 'bg-accent-50', | ||
| )} | ||
| > | ||
| {/* Dateline rule — replaces the retired border-l-2 leader stripe. */} | ||
| {isLeader && <span aria-hidden className="mb-1 block h-[2px] w-7 rounded-full bg-accent-500" />} | ||
|
Comment on lines
+55
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Use canonical tokens for the mobile leader treatment.
As per coding guidelines, only canonical design-system color families are permitted. 🤖 Prompt for AI AgentsSources: Coding guidelines, Path instructions |
||
| <div className="flex items-start justify-between gap-3"> | ||
| <div className="min-w-0"> | ||
| <p className="truncate text-sm font-medium text-warm-900">{t.name}</p> | ||
| <p className="mt-1 font-fw-mono text-xs tabular-nums text-warm-500"> | ||
| {t.playerCount} players · last{' '} | ||
| {t.lastActivity ? <LocalTime iso={t.lastActivity} variant="date" fallback="never" /> : 'never'} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use a non-semantic hydration fallback for real timestamps.
[details] - <LocalTime iso={t.lastActivity} variant="date" fallback="never" />
+ <LocalTime iso={t.lastActivity} variant="date" fallback="—" />[/details] Also applies to: 112-112 🤖 Prompt for AI Agents |
||
| </p> | ||
| </div> | ||
| <StatusPill tone={HEALTH_TONE[t.health]} dot size="sm" className="shrink-0"> | ||
| {t.health} | ||
| </StatusPill> | ||
| </div> | ||
| {t.errors7d > 0 ? ( | ||
| <p className="mt-2 font-fw-mono text-xs font-semibold tabular-nums text-fw-danger"> | ||
| {t.errors7d} errors this week | ||
| </p> | ||
| ) : null} | ||
| </Link> | ||
| ); | ||
| })} | ||
| </div> | ||
|
|
||
| {/* md+: the original sticky-identity table, byte-for-byte. */} | ||
| <div className="hidden overflow-x-auto md:block"> | ||
| <table className="w-full min-w-[520px] text-sm"> | ||
| <thead> | ||
| <tr className="border-b border-accent-600/25 text-left text-xs uppercase tracking-widest text-warm-500"> | ||
|
|
@@ -68,7 +109,7 @@ export function TeamHealthTable({ teams }: { teams: TeamHealthEntry[] }) { | |
| </td> | ||
| <td className="px-3 font-fw-mono font-semibold tabular-nums text-warm-900">{t.playerCount}</td> | ||
| <td className="px-3 font-fw-mono text-xs tabular-nums text-warm-600"> | ||
| {t.lastActivity ? new Date(t.lastActivity).toLocaleDateString() : 'never'} | ||
| {t.lastActivity ? <LocalTime iso={t.lastActivity} variant="date" fallback="never" /> : 'never'} | ||
| </td> | ||
| <td className="px-3"> | ||
| <StatusPill tone={HEALTH_TONE[t.health]} dot size="sm"> | ||
|
|
@@ -88,6 +129,7 @@ export function TeamHealthTable({ teams }: { teams: TeamHealthEntry[] }) { | |
| })} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use the canonical design-token families.
src/app/admin/_components/FeatureDotGrid.tsx:82-85,159-179,202-203introducesaccent-*classes, while the repository rules requireprimary-*,destructive,warm-*, andcream-*tokens. Replace these with the corresponding canonical tokens.[details]
[/details]
As per coding guidelines, only canonical design-system color families are permitted.
As per path instructions,
src/**/*.{tsx,css}must use canonical design-system tokens.Also applies to: 159-179, 202-203
🤖 Prompt for AI Agents
Sources: Coding guidelines, Path instructions