Files
box-box/documentations/refactor/screens/component-notes.md

376 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Component Notes
Implementation reference for translating the static mockups into React components.
Each entry covers purpose, screens, data shape, and responsive behaviour.
---
## App Navigation — `<AppNav>`
**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 — `<DensityToggle>`
**Purpose**: Switches between default and compact row heights. Effect is applied as a CSS class on `<html>`, not via React state cascade.
**Appears in**: `<AppNav>`.
**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 — `<SessionBanner>`
**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 — `<TimingTower>`
**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 12 seconds. Memoize `<TimingRow>` 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 — `<RaceControlFeed>`
**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 — `<BattlesList>`
**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 — `<PinnedStrip>`
**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 — `<PitWindowPanel>`
**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 — `<StrategyChart>`
**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 — `<PositionEvolution>`
**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 — `<DatasetStatus>`
**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 — `<IngestCommandBlock>`
**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 — `<PanelShell>`
**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 (7691024px): `main | aside` with narrower aside
- Phone (<768px): `main` full-width + bottom tab nav switching aside panels
**Implementation note**: Density mode class on `<html>` flows through without prop drilling. `PanelShell` does not need to know about density — CSS handles it.
---
## Source/Freshness Strip — `<SourceStrip>`
**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.