Upgrade live timing page for race weekends

- Track status flag banner (green/yellow/SC/VSC/red, defensive mapping)
- Weather strip: air/track temp, humidity, wind with compass, rain badge
- Gap trend sparklines from a per-driver interval ring buffer
- Battle detection: consecutive cars within 1.0s chained and highlighted
- Stint history column with compound sequence per driver
- Pinnable drivers (max 3) with focus cards, persisted to localStorage

Pure logic lives in lib/gapHistory.ts and lib/battles.ts with unit
tests; 137 frontend tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 00:21:23 -04:00
parent 7a5e0a323d
commit c708b6697d
17 changed files with 1342 additions and 31 deletions

View File

@@ -0,0 +1,63 @@
import { teamColor } from '../../utils'
import type { LiveTimingRow } from '../../lib/live'
import { driverCode, tyreClass, tyreLabel } from '../../lib/live'
import type { GapHistoryMap } from '../../lib/gapHistory'
import { GapSparkline } from './GapSparkline'
interface Props {
rows: LiveTimingRow[]
history: GapHistoryMap
pinned: string[]
onToggle: (racingNumber: string) => void
}
export function PinnedDrivers({ rows, history, pinned, onToggle }: Props) {
if (pinned.length === 0) return null
const rowsByNumber = new Map(rows.map((row) => [row.RacingNumber, row]))
return (
<div className="pinned-strip" data-testid="pinned-strip">
{pinned.map((number) => {
const row = rowsByNumber.get(number)
if (!row) {
return (
<button
type="button"
className="pinned-card pinned-card-missing"
key={number}
onClick={() => onToggle(number)}
title="Unpin driver"
>
<span className="drv-code">#{number}</span>
<span className="pinned-nodata">no data</span>
<span className="pinned-unpin" aria-hidden="true">×</span>
</button>
)
}
const driver = row.Driver
const gap = driver.Interval || driver.GapToLeader || '-'
return (
<button
type="button"
className="pinned-card"
key={number}
onClick={() => onToggle(number)}
title="Unpin driver"
>
<span className="pinned-pos mono">P{row.Position}</span>
<span className="drv-bar" style={{ background: teamColor(row.Info?.TeamColour) }} />
<span className="drv-code">{driverCode(row)}</span>
<span className={`tyre-badge ${tyreClass(row.Tyre)}`}>{tyreLabel(row.Tyre)}</span>
<span className="pinned-gap mono">{gap}</span>
<GapSparkline samples={history[number]} />
{driver.InPit && <span className="badge badge-pit">PIT</span>}
{driver.Retired && <span className="badge badge-out">RET</span>}
<span className="pinned-unpin" aria-hidden="true">×</span>
</button>
)
})}
</div>
)
}