# Component Notes Implementation reference for translating the static mockups into React components. Each entry covers purpose, screens, data shape, and responsive behaviour. --- ## App Navigation — `` **Purpose**: Persistent sticky top bar across all screens. Shows logo, route links, live status badge, and density toggle. **Appears in**: All screens (always mounted). **Props / data needed**: ```ts interface AppNavProps { activePage: 'home' | 'live' | 'race-hub' | 'data-library' | 'standings' | 'drivers' isLive: boolean // shows animated red dot + RACE/QUALI badge sessionLabel?: string // e.g. "Monaco GP — Race" densityMode: 'default' | 'compact' onDensityChange: (mode) => void } ``` **Responsive**: On phone (<768px), collapse nav links to icon-only or a hamburger. Live badge and density toggle remain visible. Session label moves to the session banner. --- ## Density Toggle — `` **Purpose**: Switches between default and compact row heights. Effect is applied as a CSS class on ``, not via React state cascade. **Appears in**: ``. **Implementation note**: Call `document.documentElement.classList.toggle('compact', ...)` directly. Store preference in `localStorage`. React state only tracks the value for rendering the active button — does not gate CSS. **Responsive**: Always visible. Two-button [D][C] works at any width. --- ## Session Status Banner — `` **Purpose**: Real-time session state strip: session name, current lap, session clock, track status, DRS state, fastest lap. Fed by SSE. **Appears in**: `/live` (always visible at top, sticky). On phone, replaces AppNav as the primary context header. **Props / data needed**: ```ts interface SessionBannerProps { sessionName: string // "Monaco GP — Race" lapCurrent: number lapTotal: number sessionClock: string // "1:02:34" trackStatus: 'green' | 'yellow' | 'red' | 'sc' | 'vsc' | 'unknown' drsEnabled: boolean fastestLap?: { driverCode: string; lapTime: number; lap: number } airTemp: number trackTemp: number isConnected: boolean // SSE connection state } ``` **Critical state**: `isConnected: false` must show a visible DISCONNECTED indicator. Do not silently go stale. **Responsive**: Horizontal scroll on narrow viewports. On phone, show session + lap + track status as minimum; other fields scroll off-screen. --- ## Timing Tower — `` **Purpose**: Dense real-time table. One row per driver. Driven by SSE. This is the primary surface of the live screen. **Appears in**: `/live`. **Props / data needed**: ```ts interface TimingEntry { position: number driverCode: string driverNumber: number teamColor: string gap: string // "LEADER" | "+3.456" | "+1 LAP" interval: string // "+3.456" | "—" tyre: 'S' | 'M' | 'H' | 'I' | 'W' tyreAge: number // laps on current tyre lastLap: number // seconds bestLap: number // seconds s1State: 'pb' | 'ob' | 'slow' | 'none' s2State: 'pb' | 'ob' | 'slow' | 'none' s3State: 'pb' | 'ob' | 'slow' | 'none' isPitIn: boolean isPitOut: boolean hasFastestLap: boolean isDNF: boolean } interface TimingTowerProps { entries: TimingEntry[] pinnedDrivers: string[] // driver codes fastestLapDriver: string } ``` **Performance critical**: SSE ticks every 1–2 seconds. Memoize `` by driver number. Diff at the entry level, not the full array. Use `React.memo` + stable references. Avoid full re-renders. **Columns on phone**: P · Driver · Gap · Tyre · Last (hide Int, Age, Best, Sectors). **Columns on iPad**: P · Driver · Gap · Int · Tyre · Age · Last (hide Best, Sectors). **Columns on desktop**: All columns. --- ## Race Control Feed — `` **Purpose**: Scrolling list of race control messages. Auto-scrolls to newest. Color-coded by message type (SC, VSC, DRS, penalty, fastest lap, flag). **Appears in**: `/live` sidebar, `/race-hub` Race Control tab (static version). **Props / data needed**: ```ts interface RCMessage { id: string lap: number type: 'sc' | 'vsc' | 'drs' | 'penalty' | 'fl' | 'flag' | 'info' text: string timestamp: string } interface RaceControlFeedProps { messages: RCMessage[] autoScroll: boolean } ``` **Responsive**: In `/live` sidebar on desktop. On phone, a full-screen panel accessed via bottom tab. In race-hub, inline full-width list. --- ## Battle Row / Battles Panel — `` **Purpose**: Shows detected on-track pairs with gap value and closing/stable/opening trend. **Appears in**: `/live` sidebar. **Props / data needed**: ```ts interface Battle { aheadCode: string aheadTeamColor: string behindCode: string behindTeamColor: string gapSeconds: number trend: 'closing' | 'stable' | 'opening' } interface BattlesListProps { battles: Battle[] } ``` **Responsive**: On phone, shown in the Battles bottom-tab panel alongside Pit Window. --- ## Pinned Driver Strip — `` **Purpose**: Compact horizontal cards for drivers the user has pinned. Shows position, gap, last lap, tyre state at a glance without scrolling the timing tower. **Appears in**: `/live` (below session banner, above timing tower). Hidden on phone to preserve space. **Props / data needed**: ```ts interface PinnedCardData { driverCode: string teamColor: string position: number gap: string lastLap: string tyre: string specialState?: 'pit-out' | 'fastest-lap' | 'dnf' } interface PinnedStripProps { cards: PinnedCardData[] onUnpin: (driverCode: string) => void } ``` **Responsive**: Desktop + iPad only. Hide on phone (<768px) — tower already shows all data. --- ## Pit Window Panel — `` **Purpose**: For each driver with a notable stint age, shows current tyre, age, and pit window status (OPEN / SOON / OVERDUE / DONE). Computed from stint age and expected compound life. **Appears in**: `/live` sidebar (third section after RC and Battles). **Props / data needed**: ```ts interface PitWindowEntry { driverCode: string teamColor: string tyre: 'S' | 'M' | 'H' | 'I' | 'W' tyreAge: number windowStatus: 'open' | 'soon' | 'overdue' | 'done' } ``` **Note**: Window status logic lives in the backend or a pure TS utility — not component logic. **Responsive**: Desktop sidebar. On phone, shown in Battles tab panel below battle list. --- ## Strategy Chart — `` **Purpose**: D3-owned horizontal stint chart. One row per driver (top 10), colored blocks = tyre compound, width = laps. SC/VSC period shading. Lap counter axis. **Appears in**: `/race-hub` Strategy tab. **Props / data needed**: ```ts interface Stint { compound: 'S' | 'M' | 'H' | 'I' | 'W' startLap: number endLap: number } interface StrategyDriver { code: string teamColor: string stints: Stint[] } interface StrategyChartProps { drivers: StrategyDriver[] totalLaps: number scPeriods: Array<{ start: number; end: number }> vscPeriods: Array<{ start: number; end: number }> } ``` **D3 contract**: Component owns its SVG DOM node. Mount/update via `useEffect` with D3. Resize via `ResizeObserver`. No React inside the SVG. **Responsive**: Full-width SVG with `viewBox`, scales with container. Label column width fixed. On narrow screens (< 480px), driver labels may need abbreviating. --- ## Position Evolution Chart — `` **Purpose**: D3-owned line chart. X = lap, Y = position (1 at top). One line per driver (top 6). SC/VSC period bands. End labels per driver. **Appears in**: `/race-hub` Positions tab. **Props / data needed**: ```ts interface PositionPoint { lap: number; position: number } interface PositionDriver { code: string color: string dashed: boolean // true for teammate (same team color) points: PositionPoint[] } interface PositionEvolutionProps { drivers: PositionDriver[] totalLaps: number scPeriods: Array<{ start: number; end: number }> vscPeriods: Array<{ start: number; end: number }> } ``` **D3 contract**: Same as StrategyChart — D3 owns the SVG, React manages data and container sizing. **Responsive**: Full-width `viewBox` SVG. Right-side labels need right-padding. --- ## Dataset Status Indicator — `` **Purpose**: Shows completeness of local data for a session. Used in two modes: full grid (Race Hub Dataset tab, Data Library detail) and mini (Race Hub aside, API response metadata strip). **Appears in**: `/race-hub` Dataset tab, `/race-hub` aside, `/data-library` detail panel. API response metadata. **Props / data needed**: ```ts type DatasetState = 'local' | 'partial' | 'missing' | 'stale' | 'live' interface DatasetEntry { name: string // e.g. "laps", "car_data_samples" state: DatasetState lastIngestedAt?: string error?: string } interface DatasetStatusProps { datasets: DatasetEntry[] variant: 'grid' | 'mini' | 'strip' } ``` **Responsive**: Grid variant reflows to 1-column on phone. Mini variant stays compact at all widths. Strip variant is a horizontal overflow row (source strip in the sub-header). --- ## Ingest Command Block — `` **Purpose**: Displays one or more CLI ingest commands with syntax highlighting (comment lines, command lines). Not interactive — display only. **Appears in**: `/race-hub` Dataset tab, `/data-library` detail panel. **Props / data needed**: ```ts interface IngestLine { type: 'comment' | 'command' text: string } interface IngestCommandBlockProps { lines: IngestLine[] } ``` **Responsive**: Horizontal scroll on overflow. Monospace font required. --- ## Responsive Panel Shell — `` **Purpose**: Layout wrapper that switches between sidebar-on-right (desktop), stacked (tablet), and tab-driven (phone) based on viewport. Used in Live Timing and Race Hub. **Appears in**: Used internally by `/live` and `/race-hub`. **Props / data needed**: ```ts interface PanelShellProps { main: React.ReactNode // always visible aside: React.ReactNode // sidebar on desktop, bottom on tablet phoneTabs?: Array<{ // only on phone — replaces aside with tabs id: string label: string icon: string content: React.ReactNode }> asideWidth?: number // default 300 } ``` **Responsive**: - Desktop (>1024px): `main | aside` grid - iPad (769–1024px): `main | aside` with narrower aside - Phone (<768px): `main` full-width + bottom tab nav switching aside panels **Implementation note**: Density mode class on `` flows through without prop drilling. `PanelShell` does not need to know about density — CSS handles it. --- ## Source/Freshness Strip — `` **Purpose**: Inline metadata bar attached to API responses. Shows data source, last ingest time, staleness state, and list of missing datasets. **Appears in**: Sub-headers of `/race-hub`, `/command-center` (data status for weekend), any screen that reads from the local DB. **Props / data needed**: ```ts interface SourceStripProps { source: 'local' | 'api' | 'cache' | 'live' | 'missing' lastIngestedAt?: string isStale?: boolean missingDatasets?: string[] } ``` **Responsive**: Wraps on narrow viewports. Badges remain readable at any width.