docs: import TRM design handoff + defer adoption to phase 3.8
A design handoff bundle generated by Claude Design (claude.ai/design) on 2026-05-02. Defines the Bloomberg/F1-pit-wall aesthetic for TRM: - ink-on-paper base + race-flag red accent (#E8412B) - square-edged everything, sharp printed offset shadows - mono numerics (JetBrains Mono) for any changing value - Goldplay (real licensed font, three weights in bundle fonts/) - four surfaces designed: dashboard / leaderboard / mobile / marketing (SPA scope is the first two) The bundle is committed in-tree at TRM_Design_System-handoff/ so 3.8 has the full source material when it picks the work up. Includes: - Top-level + project READMEs (the design spec) - chats/chat1.md (intent + iteration history) - colors_and_type.css (token set, drop-in for Tailwind 4 @theme) - fonts/ (Goldplay regular/semibold/bold) - ui_kits/ (HTML prototypes per surface) - preview/ (per-token visual reference cards) Updated phase-3-dogfood-readiness/README.md task 3.8 row to point at the bundle and document the recommended approach (retheme shadcn via CSS variable overrides + Tailwind 4 @theme, not replace). Why deferred: foundational tokens are non-blocking for Phase 1 (login + placeholder home) and Phase 2 (live map without chrome). Applying them now would either delay dogfood-blocking work or land partial styling that gets reworked when 3.8 lands the full pass.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
function AlertFeed({ alerts }) {
|
||||
return (
|
||||
<div className="card" style={{ height: '100%' }}>
|
||||
<div className="card-head">
|
||||
<h3>Race control feed</h3>
|
||||
<span className="pill live" style={{ marginLeft: 'auto' }}><span className="dot"></span>LIVE</span>
|
||||
</div>
|
||||
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
|
||||
{alerts.map((a, i) => (
|
||||
<div key={i} style={{ display: 'grid', gridTemplateColumns: '4px 1fr auto', gap: 12, alignItems: 'flex-start', padding: '10px 14px', borderBottom: '1px solid var(--hairline)' }}>
|
||||
<div style={{ width: 4, alignSelf: 'stretch', background: a.color }}></div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 500 }}>{a.msg}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{a.detail}</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-4)' }}>{a.time}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.AlertFeed = AlertFeed;
|
||||
@@ -0,0 +1,42 @@
|
||||
function EventsTable({ events, onSelect }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Events</h3>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
<button className="btn ghost"><i data-lucide="filter"></i>Filter</button>
|
||||
<button className="btn primary"><i data-lucide="plus"></i>New event</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="lb">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 30 }}></th>
|
||||
<th>EVENT</th>
|
||||
<th>DATE</th>
|
||||
<th>SPORT</th>
|
||||
<th className="num">STARTERS</th>
|
||||
<th className="num">WAVES</th>
|
||||
<th>STATUS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map(e => (
|
||||
<tr key={e.id} onClick={() => onSelect && onSelect(e)} style={{ cursor: 'pointer' }}>
|
||||
<td><i data-lucide={e.icon} style={{ width: 14, height: 14, color: 'var(--ink-3)' }}></i></td>
|
||||
<td className="name">{e.name}</td>
|
||||
<td>{e.date}</td>
|
||||
<td>{e.sport}</td>
|
||||
<td className="num">{e.starters.toLocaleString()}</td>
|
||||
<td className="num">{e.waves}</td>
|
||||
<td><span className={'pill ' + e.statusClass}><span className="dot"></span>{e.status}</span></td>
|
||||
<td><i data-lucide="chevron-right" style={{ width: 14, height: 14, color: 'var(--ink-4)' }}></i></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.EventsTable = EventsTable;
|
||||
@@ -0,0 +1,16 @@
|
||||
function KpiStrip({ items }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${items.length}, 1fr)`, gap: 0, border: '1px solid var(--ink)', background: 'var(--paper)' }}>
|
||||
{items.map((it, i) => (
|
||||
<div key={i} className="kpi" style={{ borderRight: i < items.length - 1 ? '1px solid var(--hairline)' : 'none' }}>
|
||||
<div className="lbl">{it.label}</div>
|
||||
<div className="val">{it.value}</div>
|
||||
<div className={'delta ' + (it.dir || '')} style={{ color: it.dir === 'up' ? 'var(--green)' : it.dir === 'dn' ? 'var(--flag)' : 'var(--ink-4)' }}>
|
||||
{it.delta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.KpiStrip = KpiStrip;
|
||||
@@ -0,0 +1,17 @@
|
||||
# Dashboard UI Kit — TRM
|
||||
|
||||
Race director / timing official dashboard. Primary surface for organizing events, managing waves and bibs, and running race control on race day.
|
||||
|
||||
## Screens covered
|
||||
- **Events overview** — list of upcoming/live/past events with KPI strip
|
||||
- **Race control** — live event view with leaderboard, mat status, and alert feed
|
||||
|
||||
## Components
|
||||
- `TopBar.jsx` — sticky header with brand, search, clock, user
|
||||
- `SideNav.jsx` — primary navigation
|
||||
- `EventsTable.jsx` — events list with status pills
|
||||
- `KpiStrip.jsx` — 4-up KPI cards
|
||||
- `RaceControl.jsx` — leaderboard + alert feed + mat status
|
||||
- `AlertFeed.jsx` — race-control alerts
|
||||
|
||||
Open `index.html` to interact (click between screens).
|
||||
@@ -0,0 +1,58 @@
|
||||
function RaceControl({ leaders, alerts, mats }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Live leaderboard · Coastline 10K</h3>
|
||||
<span className="pill live" style={{ marginLeft: 'auto' }}><span className="dot"></span>LIVE</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontSize: 14, fontWeight: 600 }}>00:42:18.4</span>
|
||||
</div>
|
||||
<table className="lb">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 36 }}>POS</th>
|
||||
<th style={{ width: 50 }}>BIB</th>
|
||||
<th>NAME</th>
|
||||
<th>WAVE</th>
|
||||
<th className="num">SPLIT 3</th>
|
||||
<th className="num">GAP</th>
|
||||
<th className="num">PACE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{leaders.map(l => (
|
||||
<tr key={l.pos}>
|
||||
<td style={{ fontWeight: 700, color: l.pos === 1 ? 'var(--flag)' : 'var(--ink)' }}>{l.pos}</td>
|
||||
<td>{l.bib}</td>
|
||||
<td className="name">{l.name}</td>
|
||||
<td>{l.wave}</td>
|
||||
<td className="num">{l.split}</td>
|
||||
<td className={'num ' + (l.gap === '—' ? '' : 'loss')}>{l.gap}</td>
|
||||
<td className="num">{l.pace}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div className="card">
|
||||
<div className="card-head"><h3>Timing mats</h3></div>
|
||||
<div style={{ padding: 14, display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
|
||||
{mats.map(m => (
|
||||
<div key={m.id} style={{ border: '1px solid var(--hairline)', padding: '8px 10px', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span className="dot" style={{ width: 8, height: 8, borderRadius: 999, background: m.online ? 'var(--green)' : 'var(--flag)', display: 'inline-block' }}></span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 600 }}>{m.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)' }}>{m.id} · {m.km} km</div>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)' }}>{m.reads}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AlertFeed alerts={alerts} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.RaceControl = RaceControl;
|
||||
@@ -0,0 +1,39 @@
|
||||
function SideNav({ active, onNav }) {
|
||||
const items = [
|
||||
{ id: 'events', icon: 'calendar', label: 'Events', badge: '12' },
|
||||
{ id: 'live', icon: 'radio', label: 'Race control', badge: '1' },
|
||||
{ id: 'results', icon: 'trophy', label: 'Results' },
|
||||
{ id: 'participants', icon: 'users', label: 'Participants' },
|
||||
{ id: 'mats', icon: 'route', label: 'Timing mats' },
|
||||
{ id: 'reports', icon: 'bar-chart-3', label: 'Reports' },
|
||||
];
|
||||
const settings = [
|
||||
{ id: 'team', icon: 'user-cog', label: 'Team' },
|
||||
{ id: 'billing', icon: 'credit-card', label: 'Billing' },
|
||||
{ id: 'settings', icon: 'settings', label: 'Settings' },
|
||||
];
|
||||
return (
|
||||
<aside className="kit-side">
|
||||
<div className="kit-side-section">Operations</div>
|
||||
{items.map(it => (
|
||||
<div key={it.id} className={'kit-nav-item' + (active === it.id ? ' active' : '')} onClick={() => onNav(it.id)}>
|
||||
<i data-lucide={it.icon}></i>
|
||||
<span>{it.label}</span>
|
||||
{it.badge && <span className="badge">{it.badge}</span>}
|
||||
</div>
|
||||
))}
|
||||
<div className="kit-side-section">Account</div>
|
||||
{settings.map(it => (
|
||||
<div key={it.id} className={'kit-nav-item' + (active === it.id ? ' active' : '')} onClick={() => onNav(it.id)}>
|
||||
<i data-lucide={it.icon}></i>
|
||||
<span>{it.label}</span>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<div style={{ padding: 14, borderTop: '1px solid var(--hairline)', fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
v3.18.0 · build 2741
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
window.SideNav = SideNav;
|
||||
@@ -0,0 +1,22 @@
|
||||
// TopBar — sticky header for the dashboard.
|
||||
function TopBar({ clock }) {
|
||||
return (
|
||||
<header className="kit-top">
|
||||
<div className="kit-brand">
|
||||
<img src="../../assets/logo-trm.png" alt="TRM" />
|
||||
<span className="b-meta">Dashboard</span>
|
||||
</div>
|
||||
<div className="kit-search">
|
||||
<i data-lucide="search" style={{ width: 14, height: 14, color: 'var(--ink-3)' }}></i>
|
||||
<input placeholder="Search events, bibs, athletes…" />
|
||||
</div>
|
||||
<div className="kit-top-actions">
|
||||
<span className="pill live"><span className="dot"></span>LIVE · 1 EVENT</span>
|
||||
<span className="kit-clk">{clock}</span>
|
||||
<i data-lucide="bell" style={{ width: 18, height: 18 }}></i>
|
||||
<div style={{ width: 28, height: 28, border: '1px solid var(--ink)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600 }}>JR</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
window.TopBar = TopBar;
|
||||
@@ -0,0 +1,128 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>TRM Dashboard · UI Kit</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="../kit.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="TopBar.jsx"></script>
|
||||
<script type="text/babel" src="SideNav.jsx"></script>
|
||||
<script type="text/babel" src="KpiStrip.jsx"></script>
|
||||
<script type="text/babel" src="EventsTable.jsx"></script>
|
||||
<script type="text/babel" src="AlertFeed.jsx"></script>
|
||||
<script type="text/babel" src="RaceControl.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const EVENTS = [
|
||||
{ id: 1, name: 'Coastline 10K · Spring 2026', date: 'Apr 27, 09:00', sport: 'Run', starters: 1247, waves: 4, status: 'LIVE', statusClass: 'live', icon: 'radio' },
|
||||
{ id: 2, name: 'Granite Crit Series · Round 3', date: 'May 03, 18:30', sport: 'Cycling', starters: 184, waves: 6, status: 'SCHEDULED', statusClass: 'info', icon: 'calendar' },
|
||||
{ id: 3, name: 'Twin Peaks Triathlon', date: 'May 11, 06:30', sport: 'Triathlon', starters: 612, waves: 8, status: 'SCHEDULED', statusClass: 'info', icon: 'calendar' },
|
||||
{ id: 4, name: 'Harbor Sprint Regatta', date: 'May 18, 11:00', sport: 'Sailing', starters: 88, waves: 3, status: 'DRAFT', statusClass: 'neutral', icon: 'edit' },
|
||||
{ id: 5, name: 'Northshore Marathon', date: 'Apr 06, 07:30', sport: 'Run', starters: 3402, waves: 5, status: 'FINISHED', statusClass: 'ok', icon: 'check' },
|
||||
{ id: 6, name: 'Iron Sands Ultra 100', date: 'Mar 22, 05:00', sport: 'Ultra', starters: 287, waves: 1, status: 'FINISHED', statusClass: 'ok', icon: 'check' },
|
||||
];
|
||||
|
||||
const LEADERS = [
|
||||
{ pos: 1, bib: 247, name: 'Maya Chen', wave: 'Elite', split: '00:42:18.4', gap: '—', pace: '04:13' },
|
||||
{ pos: 2, bib: 188, name: 'Ravi Park', wave: 'Elite', split: '00:42:22.6', gap: '+0:04.2', pace: '04:14' },
|
||||
{ pos: 3, bib: 44, name: 'Noemi Vega', wave: 'Elite', split: '00:42:30.1', gap: '+0:11.7', pace: '04:15' },
|
||||
{ pos: 4, bib: 302, name: 'Tomás Riera', wave: 'Elite', split: '00:42:55.0', gap: '+0:36.6', pace: '04:18' },
|
||||
{ pos: 5, bib: 119, name: 'Anika Joshi', wave: 'Elite', split: '00:43:01.2', gap: '+0:42.8', pace: '04:19' },
|
||||
{ pos: 6, bib: 401, name: 'Felix Okafor', wave: 'Open', split: '00:43:08.7', gap: '+0:50.3', pace: '04:19' },
|
||||
{ pos: 7, bib: 77, name: 'Sara Lindqvist',wave:'Open', split: '00:43:14.0', gap: '+0:55.6', pace: '04:20' },
|
||||
{ pos: 8, bib: 215, name: 'David Müller', wave: 'Open', split: '00:43:22.4', gap: '+1:04.0', pace: '04:21' },
|
||||
];
|
||||
|
||||
const ALERTS = [
|
||||
{ msg: 'Bib 247 missed split 3', detail: 'Flagging for manual review', time: '14:02:11', color: 'var(--flag)' },
|
||||
{ msg: 'Yellow flag · sector 2', detail: 'Debris cleared in 90s', time: '14:00:42', color: 'var(--amber)' },
|
||||
{ msg: 'Wave 4 cleared start mat', detail: '412 / 412 starters', time: '13:45:00', color: 'var(--green)' },
|
||||
{ msg: 'Tracking active', detail: '14 of 14 mats online', time: '13:30:00', color: 'var(--blue)' },
|
||||
{ msg: 'Bib 088 retired', detail: 'Marked DNF · medical', time: '13:24:18', color: 'var(--ink-3)' },
|
||||
];
|
||||
|
||||
const MATS = [
|
||||
{ id: 'M-01', name: 'Start', km: 0, online: true, reads: '1,247' },
|
||||
{ id: 'M-02', name: 'Split 1', km: 2.5, online: true, reads: '1,201' },
|
||||
{ id: 'M-03', name: 'Split 2', km: 5.0, online: true, reads: '1,184' },
|
||||
{ id: 'M-04', name: 'Split 3', km: 7.5, online: true, reads: '892' },
|
||||
{ id: 'M-05', name: 'Finish', km: 10, online: true, reads: '0' },
|
||||
{ id: 'M-06', name: 'Backup', km: 10, online: false, reads: '—' },
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [active, setActive] = useState('events');
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
const [clock, setClock] = useState('14:02:11');
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
const d = new Date();
|
||||
setClock(d.toTimeString().slice(0,8));
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { lucide.createIcons(); });
|
||||
|
||||
const showRaceControl = active === 'live' || (selectedEvent && selectedEvent.statusClass === 'live');
|
||||
|
||||
return (
|
||||
<div className="kit-app">
|
||||
<TopBar clock={clock} />
|
||||
<div className="kit-main">
|
||||
<SideNav active={showRaceControl ? 'live' : active} onNav={(id) => { setActive(id); setSelectedEvent(null); }} />
|
||||
<main className="kit-content">
|
||||
{showRaceControl ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 18 }}>
|
||||
<button className="btn ghost" onClick={() => { setActive('events'); setSelectedEvent(null); }} style={{ padding: 0, height: 'auto', boxShadow: 'none', border: 0, color: 'var(--ink-3)', fontSize: 12 }}>← All events</button>
|
||||
<h1 style={{ fontSize: 28, margin: 0 }}>Coastline 10K · Spring 2026</h1>
|
||||
<span className="pill live"><span className="dot"></span>LIVE</span>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
<button className="btn"><i data-lucide="download"></i>Export</button>
|
||||
<button className="btn flag"><i data-lucide="flag"></i>Issue red flag</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<KpiStrip items={[
|
||||
{ label: 'Active bibs', value: '1,247', delta: '↑ 12 in last 5 min', dir: 'up' },
|
||||
{ label: 'Lead time', value: '42:18.4', delta: 'Bib 247 · split 3' },
|
||||
{ label: 'Avg pace', value: '4:23/km', delta: '↓ 0:06 vs target', dir: 'dn' },
|
||||
{ label: 'Mats online', value: '14 / 14', delta: '100% uptime', dir: 'up' },
|
||||
]} />
|
||||
</div>
|
||||
<RaceControl leaders={LEADERS} alerts={ALERTS} mats={MATS} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 18 }}>
|
||||
<h1 style={{ fontSize: 32, margin: 0 }}>Events</h1>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--ink-4)' }}>12 total · 1 live · 3 scheduled</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<KpiStrip items={[
|
||||
{ label: 'Events this season', value: '12', delta: '↑ 3 vs last season', dir: 'up' },
|
||||
{ label: 'Total starters', value: '5,820', delta: '↑ 18% YoY', dir: 'up' },
|
||||
{ label: 'Avg finish rate', value: '94.2%', delta: '↑ 1.4 pts', dir: 'up' },
|
||||
{ label: 'Outstanding reviews',value: '7', delta: '3 high priority' },
|
||||
]} />
|
||||
</div>
|
||||
<EventsTable events={EVENTS} onSelect={setSelectedEvent} />
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user