@inventure71/paddockjs 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add a tiny pre-JS startup placeholder subpath and stylesheet for hosts that want an instant simulator loading surface before importing the full PaddockJS browser runtime. Also tighten the built-in component loading overlay contract across public mounted surfaces.
8
+
3
9
  ## 4.0.0
4
10
 
5
11
  ### Major Changes
@@ -114,12 +114,18 @@ npm run check
114
114
  Install a specific version when the host needs a controlled upgrade:
115
115
 
116
116
  ```bash
117
- npm install @inventure71/paddockjs@4.0.0
117
+ npm install @inventure71/paddockjs@4.1.0
118
118
  npm run check
119
119
  ```
120
120
 
121
121
  After updating, smoke-test the page that mounts the simulator. Browser behavior changes should be checked in the consuming host, because host CSS, container size, and route handling are outside the package.
122
122
 
123
+ ### 4.1.0 Migration Notes
124
+
125
+ - No breaking host migration is required from `4.0.0`.
126
+ - Hosts that want visible simulator startup UI before the full PaddockJS browser bundle downloads can render the new `@inventure71/paddockjs/placeholder` HTML helper with the optional `@inventure71/paddockjs/placeholder.css` stylesheet. This placeholder subpath is intentionally independent of PixiJS, simulator assets, root runtime CSS, and the browser mount.
127
+ - Existing `mountF1Simulator()` and composable `mount*()` integrations keep their normal automatic package loading overlays after PaddockJS JavaScript starts.
128
+
123
129
  ### 4.0.0 Migration Notes
124
130
 
125
131
  - Update Policy Runner HTTP servers for protocol version `2`. Cache `actionSpec`, `observationSpec`, configuration, `previousActionFields`, and `metricFields` from `/policy/reset`, then read model inputs from `body.driverIds[index]` plus aligned `body.vectors[index]`, `body.previousActions[index]`, and `body.metrics[index]` in `/policy/decide-batch`. Rich observation objects, schemas, snapshots, and track metadata are no longer sent on every policy-server decision.
package/README.md CHANGED
@@ -54,6 +54,36 @@ import '@inventure71/paddockjs/styles.css';
54
54
 
55
55
  PaddockJS CSS is scoped under package mount/component roots and uses system font stacks. The package does not load Google Fonts or any other remote font. Hosts that want branded typography should load fonts in the host app.
56
56
 
57
+ ## Startup Loading
58
+
59
+ `mountF1Simulator(root, options)` writes a lightweight package-owned HTML shell into `root` before it awaits PixiJS setup, asset loading, control binding, simulation creation, and the first canvas frame. That shell includes package loading overlays for the race controls, timing tower, race canvas, race-data panel, telemetry stack, and any other startup-capable package surface in the selected template. The overlays are removed only after the runtime has initialized and the initial readouts/frame are ready.
60
+
61
+ Composable hosts get the same behavior per mounted root: every public `simulator.mount*()` surface renders static package markup plus its own loading overlay before `await simulator.start()`.
62
+
63
+ This is a client-side startup shell, not server rendering. If a host wants visible content before the PaddockJS JavaScript bundle has downloaded and executed, use the tiny placeholder subpath and CSS:
64
+
65
+ ```js
66
+ import { createPaddockLoadingPlaceholder } from '@inventure71/paddockjs/placeholder';
67
+ import '@inventure71/paddockjs/placeholder.css';
68
+
69
+ root.innerHTML = createPaddockLoadingPlaceholder({
70
+ label: 'Loading simulator',
71
+ });
72
+
73
+ const loadSimulator = async () => {
74
+ const { mountF1Simulator } = await import('@inventure71/paddockjs');
75
+ await mountF1Simulator(root, { drivers, entries });
76
+ };
77
+
78
+ if ('requestIdleCallback' in window) {
79
+ window.requestIdleCallback(loadSimulator);
80
+ } else {
81
+ setTimeout(loadSimulator, 0);
82
+ }
83
+ ```
84
+
85
+ `@inventure71/paddockjs/placeholder` does not import PixiJS, simulator assets, package runtime CSS, or the root browser mount. It is intended for the pre-JS/network gap. The full simulator mount replaces that placeholder and then uses the built-in package overlays while PixiJS and the race runtime finish booting.
86
+
57
87
  Start here:
58
88
 
59
89
  - [Getting Started](docs/getting-started.md): install, browser mount, CSS, assets, host responsibilities, and container sizing.
@@ -340,7 +340,7 @@ Race completion is owned by `src/simulation/raceSimulation.js`. The simulation r
340
340
 
341
341
  Composable hosts may choose where each package-owned component root is placed, but they still receive package-generated markup through the public mount functions. The controller marks each mounted root as an `f1-sim-component` styling scope so standalone pieces receive the same package variables as the all-in-one shell.
342
342
 
343
- `src/styles.css` styles the generated shell, uses package-owned system font stacks, caps timing-tower width for readability, and owns the timing-list scroll behavior. Timing rows are compact content-sized grid rows stacked from the top, with `ui.timingEntryVerticalPadding` setting one shared pixel padding value above and below every entry, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. The compact `Int`/`Gap` header toggle is a timing-tower style contract, not host CSS: the grid reserves a stable gap column, the header labels and toggle share one vertical center, the visible pill stays inside the header, and the clickable target remains `44px` without changing tower width. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/timing/`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
343
+ `src/styles.css` styles the generated shell, uses package-owned system font stacks, caps timing-tower width for readability, and owns the timing-list scroll behavior. Timing rows are compact content-sized grid rows stacked from the top, with `ui.timingEntryVerticalPadding` setting one shared pixel padding value above and below every entry, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. The compact `Int`/`Gap` header toggle is a timing-tower style contract, not host CSS: the grid reserves a stable gap column, the header labels and toggle share one vertical center, the visible pill stays inside the header, and the clickable target remains `44px` without changing tower width. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/timing/`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays that exist after package JavaScript mounts the shell but before PixiJS/runtime startup completes; `F1SimulatorApp` removes them only after the initial frame has rendered. `src/placeholder/index.js` and `src/placeholder.css` own the separate pre-root-import placeholder contract and must remain independent of `src/index.js`, PixiJS, simulator assets, `src/styles.css`, and browser app modules.
344
344
 
345
345
  The host page should only provide:
346
346
 
@@ -18,7 +18,7 @@ This inventory maps package-owned UI surfaces to their shared bases, owner modul
18
18
  | --- | --- | --- | --- | --- | --- |
19
19
  | All-in-one shell | `mountF1Simulator()` | `src/ui/shellTemplate.js` | Composes race controls, optional external camera controls, timing tower, race canvas, telemetry stack, shell unsupported overlay | `F1SimulatorApp` | Owns `data-f1-simulator-shell`, workspace/grid classes, layout preset classes, and the package root styling scope. Not individually mounted by composable hosts. |
20
20
  | Race controls | `mountRaceControls()` and all-in-one shell | `src/ui/raceControlsTemplate.js` | Component surface frame helper, `createSafetyCarControlMarkup({ compact: true })`, loading overlay, unsupported overlay | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Includes title, kicker, safe backlink, safety-car button, and restart. The compact safety button has no standalone component wrapper. |
21
- | Safety-car control | `mountSafetyCarControl()` and drawer/race-controls compact embed | `src/ui/raceControlsTemplate.js` | Component surface frame helper for standalone, same `data-safety-car` button and control-state renderer | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Standalone variant gets `data-paddock-component="safety-car-control"` and unsupported-size overlay. Compact variants are plain subcontrols inside race controls or drawer toolbar. |
21
+ | Safety-car control | `mountSafetyCarControl()` and drawer/race-controls compact embed | `src/ui/raceControlsTemplate.js` | Component surface frame helper for standalone, loading overlay, unsupported overlay, same `data-safety-car` button and control-state renderer | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Standalone variant gets `data-paddock-component="safety-car-control"`. Compact variants are plain subcontrols inside race controls or drawer toolbar. |
22
22
  | Camera controls | `mountCameraControls()`, all-in-one external controls, embedded race-canvas controls, drawer toolbar | `src/ui/cameraControlsTemplate.js` | Component surface frame helper, shared `data-camera-mode`, zoom, banner mute, optional speed button, loading overlay, unsupported overlay | `controlStateRenderer.js`, `CameraController`, `F1SimulatorApp.bindControls()` | External variant adds `camera-controls--external`; embedded variant omits that class. Driver camera and simulation-speed buttons are option gated. |
23
23
  | Timing tower | `mountTimingTower()`, all-in-one shell, embedded race canvas, drawer race canvas | `src/ui/timingTowerTemplate.js` | Component surface frame helper, race-control status banner, loading overlay, unsupported overlay, shared timing gap controls | `timingTowerRenderer.js`, `raceStatusRenderer.js`, `controlStateRenderer.js`, `F1SimulatorApp.observeLayoutResize()` | Embedded variants receive generated instance ids for `aria-controls`. `domBindings.js` exposes plural timing lists/header/status nodes and focused renderers fan out rows, lap counters, status banners, gap controls, and resize observation across every mounted tower owned by the same simulator. Header gap toggle may be hidden while controller APIs remain active. Width/proportions are package-owned and must not be tuned by host CSS. |
24
24
  | Race-control status banner | Timing tower internal component | `src/ui/raceControlStatusBanner.js` | Timing tower status slot | `raceStatusRenderer.js` | Internal only. It has no public mount because it is a timing-tower status affordance, not a standalone component. |
@@ -27,14 +27,15 @@ This inventory maps package-owned UI surfaces to their shared bases, owner modul
27
27
  | Steward message | Race canvas internal component | `src/ui/bannerTemplates.js` | Shared banner markup conventions | `stewardMessageController.js` | Internal top race-control message for warnings/penalties. No public mount because it depends on race-canvas layering. |
28
28
  | Race-data panel | `mountRaceDataPanel()`, race canvas lower-third, drawer lower-third | `src/ui/bannerTemplates.js` | Component surface frame helper, selected driver readouts, optional race-data telemetry detail, loading overlay, unsupported overlay | `domBindings.js` grouped panel bindings, `raceDataBannerController.js`, `telemetryRenderer.js` for sector detail | Standalone variant adds `race-data-panel--standalone`. Embedded variants are clipped/layered by race canvas. Project and radio modes share the same panel; runtime swaps content/classes across every mounted panel owned by the simulator. |
29
29
  | Telemetry sector banner | `mountTelemetrySectorBanner()` and optional race-canvas embed | `src/ui/bannerTemplates.js` | Component surface frame helper, sector-bar markup, loading overlay, unsupported overlay | `telemetryRenderer.js` | Independent broadcast lower-third style sector surface. It is separate from race-data telemetry detail and should only render when explicitly mounted or embedded. |
30
- | Telemetry stack | `mountTelemetryPanel()` and drawer content | `src/ui/telemetryTemplates.js` | Component surface frame helper, telemetry core, sector progress, lap times, sector times, optional car/driver overview, unsupported overlay | `telemetryRenderer.js`, `carOverviewRenderer.js` when overview is included | The stack owns scrolling when constrained. It may include overview by default, but drawer content intentionally disables overview. |
30
+ | Telemetry stack | `mountTelemetryPanel()` and drawer content | `src/ui/telemetryTemplates.js` | Component surface frame helper, loading overlay, telemetry core, sector progress, lap times, sector times, optional car/driver overview, unsupported overlay | `telemetryRenderer.js`, `carOverviewRenderer.js` when overview is included | The stack owns scrolling when constrained. It may include overview by default, but drawer content intentionally disables overview. |
31
31
  | Telemetry core | `mountTelemetryCore()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Scalar selected-car telemetry only. It shares selected-code/name readouts with sector banner. |
32
32
  | Telemetry sectors | `mountTelemetrySectors()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, sector-bar markup, loading overlay, unsupported overlay | `telemetryRenderer.js` | Sector progress bars only. It shares sector bar selectors with race-data detail and sector banner. |
33
33
  | Telemetry lap times | `mountTelemetryLapTimes()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Current, last, and best lap table only. |
34
34
  | Telemetry sector times | `mountTelemetrySectorTimes()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Last/best sector table only. |
35
35
  | Car/driver overview | `mountCarDriverOverview()` and optional telemetry stack embed | `src/ui/carOverviewTemplate.js` | Component surface frame helper, overview field slots, loading overlay, unsupported overlay | `domBindings.js` cached field bindings, `carOverviewRenderer.js` | Standalone and stack-embedded variants share identical markup. Runtime toggles vehicle vs driver fields without remounting and updates cached label/value bindings instead of querying each field during render. |
36
- | Race telemetry drawer | `mountRaceTelemetryDrawer()` and complete preview template | `src/ui/telemetryDrawerTemplate.js` | Component surface frame helper, camera controls, compact safety-car control, race canvas with timing/data panel, telemetry stack, unsupported overlay | `F1SimulatorApp.setTelemetryDrawerOpen()`, `controlStateRenderer.js`, normal race/telemetry renderers | Higher-level mixed template. It intentionally owns toolbar placement, drawer ids, open/closed inert state, and responsive narrow drawer behavior. |
37
- | Loading overlay | All startup-capable public surfaces except internal-only steward/status | `src/ui/templateUtils.js` | `createLoadingMarkup(label)` | `F1SimulatorApp.completeComponentLoading()` | Removed after startup. Component labels remain surface-specific for accessibility. |
36
+ | Race telemetry drawer | `mountRaceTelemetryDrawer()` and complete preview template | `src/ui/telemetryDrawerTemplate.js` | Component surface frame helper, loading overlay, camera controls, compact safety-car control, race canvas with timing/data panel, telemetry stack, unsupported overlay | `F1SimulatorApp.setTelemetryDrawerOpen()`, `controlStateRenderer.js`, normal race/telemetry renderers | Higher-level mixed template. It intentionally owns toolbar placement, drawer ids, open/closed inert state, and responsive narrow drawer behavior. |
37
+ | Loading overlay | All public startup-capable mounted surfaces, including all-in-one shell children and composable roots | `src/ui/templateUtils.js` | `createLoadingMarkup(label)` | `F1SimulatorApp.completeComponentLoading()` | Removed after startup after the initial frame. Component labels remain surface-specific for accessibility. Pre-JS placeholders are host-owned, not package-owned. |
38
+ | Pre-JS startup placeholder | `@inventure71/paddockjs/placeholder` and `@inventure71/paddockjs/placeholder.css` | `src/placeholder/index.js`, `src/placeholder.css` | Escaped inert HTML helper, optional tiny CSS start-light animation | Host page before root simulator import | Covers the network/import gap before the root package can render. It must stay independent of PixiJS, simulator assets, root runtime CSS, and app modules. |
38
39
  | Unsupported-size overlay | Public mount roots, shell, drawer, race/data/telemetry surfaces | `src/ui/templateUtils.js` | `createUnsupportedSizeMarkup(label)` | `installLayoutSupport()` | Display is driven by measured support state. Minimum size thresholds remain centralized in `layoutSupport.js`. |
39
40
 
40
41
  ## Local Preview Variants
@@ -34,6 +34,8 @@ await simulator.start();
34
34
 
35
35
  Controller methods are the canonical composable style. The standalone mount helper exports exist for compatibility, but new host code should prefer `simulator.mountRaceCanvas(root)` and the matching controller methods.
36
36
 
37
+ Each `mount*()` call writes lightweight static package markup into that root immediately and includes a package-owned loading overlay for the mounted surface. `await simulator.start()` then initializes the shared PixiJS/simulation runtime and removes those overlays after assets, controls, initial DOM readouts, and the first frame are ready. Mount all required surfaces before `start()` so the runtime can bind one coherent set of package-owned controls and readouts.
38
+
37
39
  ## Available Surfaces
38
40
 
39
41
  - `mountRaceControls(root)`
@@ -1203,4 +1203,8 @@ Composable controllers additionally expose the canonical composable mounting API
1203
1203
  - `querySelector(selector)` / `querySelectorAll(selector)`: search across the mounted package-owned composable roots. These exist for integration tests and advanced host glue; ordinary hosts should prefer explicit controller methods and mounted component roots.
1204
1204
  - `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
1205
1205
 
1206
- Mount component roots before calling `start()`. If a component is not mounted, the runtime skips that UI surface instead of requiring hidden placeholder DOM. Mounted surfaces render a package-owned loading overlay immediately; `start()` removes those overlays after PixiJS, assets, controls, and initial readouts have initialized. If `restart(nextOptions)` is called before `start()`, mounted component markup is refreshed from the new resolved options so copy, component visibility, driver data, and theme changes do not leave stale pre-start HTML behind.
1206
+ Mount component roots before calling `start()`. If a component is not mounted, the runtime skips that UI surface instead of requiring hidden placeholder DOM. Every public mounted surface renders lightweight static package markup and a surface-specific `data-paddock-loading` overlay immediately. `start()` removes those overlays only after PixiJS, assets, controls, initial DOM readouts, and the first frame have initialized. If `restart(nextOptions)` is called before `start()`, mounted component markup is refreshed from the new resolved options so copy, component visibility, driver data, and theme changes do not leave stale pre-start HTML behind.
1207
+
1208
+ This loading overlay is the package's client-side startup state. It begins after the PaddockJS JavaScript bundle has executed and a mount function has run. Hosts that need an instant placeholder before package JavaScript loads should render host-owned fallback HTML/CSS around or inside the mount root, then let `mountF1Simulator()` or the composable `mount*()` calls replace that root content when the package starts.
1209
+
1210
+ For a package-owned pre-JS placeholder, import `createPaddockLoadingPlaceholder()` from `@inventure71/paddockjs/placeholder` and the optional tiny stylesheet from `@inventure71/paddockjs/placeholder.css`. That subpath is intentionally separate from the root browser mount and does not import PixiJS, simulator assets, package runtime CSS, or `F1SimulatorApp`.
@@ -38,6 +38,36 @@ if (root) {
38
38
 
39
39
  Use the root package only for browser mounts and browser helper exports. It imports package CSS and bundled simulator assets.
40
40
 
41
+ ## Startup Loading
42
+
43
+ `mountF1Simulator()` replaces the host root with a lightweight PaddockJS shell immediately after the package JavaScript starts running. The shell is normal HTML/CSS with package loading overlays; the PixiJS renderer, simulator runtime, textures, controls, and initial readouts are initialized afterward. The mount promise resolves only after that runtime initialization is complete.
44
+
45
+ For the fastest visible first paint, render the tiny placeholder before loading the full simulator bundle:
46
+
47
+ ```html
48
+ <section class="simulator-section">
49
+ <div id="f1-simulator-root"></div>
50
+ </section>
51
+ ```
52
+
53
+ ```js
54
+ import { createPaddockLoadingPlaceholder } from '@inventure71/paddockjs/placeholder';
55
+ import '@inventure71/paddockjs/placeholder.css';
56
+
57
+ const root = document.getElementById('f1-simulator-root');
58
+ root.innerHTML = createPaddockLoadingPlaceholder({
59
+ label: 'Loading simulator',
60
+ detail: 'Preparing race control',
61
+ });
62
+
63
+ const { mountF1Simulator } = await import('@inventure71/paddockjs');
64
+ await mountF1Simulator(root, { drivers, entries });
65
+ ```
66
+
67
+ `@inventure71/paddockjs/placeholder` is CSS-free JavaScript and `@inventure71/paddockjs/placeholder.css` is a standalone tiny stylesheet. They do not import PixiJS, the simulator runtime, bundled assets, or the full package stylesheet. The default placeholder uses the same start-light idea as the simulator loading overlay. Pass `variant: 'custom'`, `className`, `detail`, or safe `attributes` when the host wants to layer its own overlay while keeping the same minimal helper contract.
68
+
69
+ That placeholder covers the time before the full PaddockJS bundle executes. Once the bundle runs, PaddockJS owns the root markup and shows its package loading overlays until the first real simulator frame is ready.
70
+
41
71
  ## CSS
42
72
 
43
73
  Most browser bundlers consume the root package stylesheet automatically. If your host build does not, import the stylesheet explicitly from a browser entry:
@@ -52,6 +52,7 @@ Root package exports:
52
52
  | --- | --- | --- |
53
53
  | Browser mounts | `mountF1Simulator`, `createPaddockSimulator` | all-in-one shell or composable package-owned UI surfaces |
54
54
  | Composable helper mounts | `mountRaceControls`, `mountCameraControls`, `mountSafetyCarControl`, `mountTimingTower`, `mountRaceCanvas`, `mountTelemetryPanel`, `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetrySectorBanner`, `mountTelemetryLapTimes`, `mountTelemetrySectorTimes`, `mountRaceTelemetryDrawer`, `mountCarDriverOverview`, `mountRaceDataPanel` | function-call wrappers around a composable simulator controller |
55
+ | Startup placeholder | `createPaddockLoadingPlaceholder` from `@inventure71/paddockjs/placeholder`, plus `@inventure71/paddockjs/placeholder.css` | optional pre-JS loading placeholder that does not import PixiJS, simulator assets, root runtime CSS, or the browser mount |
55
56
  | Data helpers | `DriverData`, `VehicleData`, `CHAMPIONSHIP_ENTRY_BLUEPRINTS`, `DEMO_PROJECT_DRIVERS`, `buildChampionshipDriverGrid`, `formatDriverNumber`, `normalizeSimulatorDrivers` | demo data, host entry normalization, and rating helper construction |
56
57
  | Theme helpers | `DEFAULT_PADDOCK_THEME`, `PADDOCK_THEME_CSS_VARIABLES`, `PADDOCK_THEME_TOKEN_KEYS`, `resolvePaddockTheme`, `applyPaddockTheme` | host-side theme preview/sync code without copying package token names |
57
58
  | Package constants | `DEFAULT_F1_SIMULATOR_ASSETS`, `PADDOCK_SIMULATOR_PRESETS` | inspect bundled asset mapping and UI preset defaults |
@@ -66,6 +67,13 @@ Data subpath exports:
66
67
  | `createProceduralTrack` | CSS-free procedural track generation helper |
67
68
  | `kphToSimSpeed`, `simSpeedToKph` | CSS-free speed conversion helpers |
68
69
 
70
+ Placeholder subpath exports:
71
+
72
+ | Export | Use |
73
+ | --- | --- |
74
+ | `createPaddockLoadingPlaceholder(options)` | Generate inert, escaped startup placeholder HTML for the network/import gap before the root simulator bundle runs. |
75
+ | `PaddockLoadingPlaceholderOptions`, `PaddockLoadingPlaceholderVariant` | TypeScript shapes for host placeholder configuration. |
76
+
69
77
  Environment subpath exports:
70
78
 
71
79
  | Export | Use |
@@ -263,13 +271,15 @@ Returned controller:
263
271
  - Composable mounting can embed the timing tower directly inside the race canvas with `mountRaceCanvas(root, { includeTimingTower: true })`.
264
272
  - The race canvas is required before `start()` because PixiJS needs a canvas host.
265
273
  - Mount component roots before calling `start()`. A pre-start `restart(nextOptions)` re-resolves options and refreshes already mounted component markup, so non-asset changes such as title, kicker, UI visibility, theme, drivers, entries, seed, or track seed are reflected before `start()` binds the runtime.
274
+ - `mountF1Simulator()` and each composable `mount*()` call must synchronously write lightweight package HTML before awaiting runtime initialization. Those startup templates may contain controls/readout placeholders, but they must not require PixiJS, texture loading, race stepping, or host-provided hidden DOM to be visible.
266
275
  - Timing tower, telemetry, controls, and race-data panels are optional from a runtime safety perspective; omitted panels simply do not render their readouts.
267
276
  - Timing tower entries are compact content-sized rows stacked from the top of the timing list. Row vertical position must be based on rank/index, never distributed by available height or total entry count. `ui.timingEntryVerticalPadding` defaults to `5` and applies that pixel value above and below every row entry.
268
277
  - Timing tower entries display team icons. `ui.timingGapMode` sets the initial display to interval-to-car-ahead timing or direct gap-to-leader timing, and the controller can change it at runtime with `setTimingGapMode()`, `getTimingGapMode()`, or `toggleTimingGapMode()`. The tower shows one compact `Int`/`Gap` header toggle by default. That toggle must stay aligned with the other timing header labels, preserve a practical `44px` target, draw active/focus treatment inside the timing header rather than outside the column, and never change the timing board width. `ui.timingGapModeToggle: false` hides only the manual header affordance; it must not disable the controller API. Seconds gaps are calculated from hidden timing-line crossings, and whole-lap deficits display as `+N`.
269
278
  - Snapshots expose calibrated display units through `speedKph`, `distanceMeters`, and `gapMeters`; internal physics remains in simulator units. Public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds.
270
279
  - Snapshots expose automatic three-sector track metadata through `track.sectors` and per-car lap/sector timing through `car.lapTelemetry`, including completed current-lap sector splits before the active sector, live active-sector elapsed time, per-sector progress, and sector performance classes for overall-best, personal-best, and slower completed sector times. Future-sector timing/progress entries are cleared so banner and sidebar telemetry cannot display stale split values from another lap or state.
271
280
  - Entries can include optional `team` metadata. Team color defaults to car color when omitted.
272
- - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, and initial readout initialization.
281
+ - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, initial readout, and first-frame initialization. `F1SimulatorApp.completeComponentLoading()` marks the owning `data-paddock-component` as loaded and removes `data-paddock-loading` nodes after the initial frame is rendered.
282
+ - Package loading overlays are a post-JavaScript client startup state, not a server-rendered skeleton. Before the root PaddockJS bundle has downloaded, parsed, and called a mount function, only pre-existing HTML/CSS can paint. The optional `@inventure71/paddockjs/placeholder` subpath and `@inventure71/paddockjs/placeholder.css` stylesheet are the package-owned phase-1 contract for that gap. They must stay independent of PixiJS, simulator assets, root package CSS, app runtime modules, and browser mount side effects.
273
283
  - `preset` is a preset-first API. Presets are resolved before explicit host overrides so hosts can use `dashboard`, `timing-overlay`, `compact-race`, or `full-dashboard` as a starting point and still override specific `ui` or `theme` fields.
274
284
  - `theme` is the public sizing/color contract. It resolves semantic package-owned tokens into complete light/dark themes, supports named derivative theme packages through `theme.themes`, selects the active package with `theme.use`, applies component-specific packages through `theme.componentThemes`, and accepts team selectors through `theme.teamThemes` or `entries[*].team.theme`. Component theme keys accept package component ids such as `timing-tower` and camelCase aliases such as `timingTower`; `selectedDriverPanel` targets both selected-driver package surfaces. Selected-driver surfaces use the selected team's theme by default when one is available while structural controls keep the active global theme unless explicitly overridden. Unknown theme tokens or component slots are ignored, one-sided light/dark token overrides generate and cache the opposite mode, and theme plus driver/team colors are validated before they are written to CSS variables. Legacy aliases such as `accentColor`, `greenColor`, and `yellowColor` remain migration aliases for the new semantic tokens.
275
285
  - `setTheme(theme)`, `setThemeMode(mode)`, and `syncThemeFrom(element, options)` are runtime presentation APIs. They must resolve through the same theme normalizer used at mount time, reapply CSS variables to every package-owned mounted root, and must not call `restart()` or replace race state. Existing simulation state, timing order, selected driver, active penalties, camera state, speed setting, and pit intent state remain owned by the running app. `syncThemeFrom()` applies immediately, watches the configured attribute with `MutationObserver` when available, maps host attribute values through `options.map`, and returns a cleanup function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -24,7 +24,8 @@
24
24
  "ui-component"
25
25
  ],
26
26
  "sideEffects": [
27
- "./src/styles.css"
27
+ "./src/styles.css",
28
+ "./src/placeholder.css"
28
29
  ],
29
30
  "publishConfig": {
30
31
  "access": "public"
@@ -50,7 +51,13 @@
50
51
  "import": "./src/data/index.js",
51
52
  "default": "./src/data/index.js"
52
53
  },
53
- "./styles.css": "./src/styles.css"
54
+ "./placeholder": {
55
+ "types": "./src/placeholder/index.d.ts",
56
+ "import": "./src/placeholder/index.js",
57
+ "default": "./src/placeholder/index.js"
58
+ },
59
+ "./styles.css": "./src/styles.css",
60
+ "./placeholder.css": "./src/placeholder.css"
54
61
  },
55
62
  "files": [
56
63
  "CHANGELOG.md",
@@ -68,12 +75,14 @@
68
75
  "src/config",
69
76
  "src/data",
70
77
  "src/environment",
78
+ "src/placeholder",
71
79
  "src/rendering",
72
80
  "src/simulation",
73
81
  "src/ui",
74
82
  "src/index.d.ts",
75
83
  "src/index.js",
76
84
  "src/styles.css",
85
+ "src/placeholder.css",
77
86
  "README.md"
78
87
  ],
79
88
  "scripts": {
@@ -0,0 +1,11 @@
1
+ export type PaddockLoadingPlaceholderVariant = 'lights' | 'custom';
2
+
3
+ export interface PaddockLoadingPlaceholderOptions {
4
+ label?: string;
5
+ detail?: string;
6
+ className?: string;
7
+ variant?: PaddockLoadingPlaceholderVariant;
8
+ attributes?: Record<string, string | number | boolean | null | undefined>;
9
+ }
10
+
11
+ export function createPaddockLoadingPlaceholder(options?: PaddockLoadingPlaceholderOptions): string;
@@ -0,0 +1,61 @@
1
+ function escapeHtml(value) {
2
+ return String(value ?? '')
3
+ .replaceAll('&', '&amp;')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;')
6
+ .replaceAll('"', '&quot;')
7
+ .replaceAll("'", '&#39;');
8
+ }
9
+
10
+ function normalizeVariant(value) {
11
+ return value === 'custom' ? 'custom' : 'lights';
12
+ }
13
+
14
+ function renderAttributes(attributes = {}) {
15
+ if (!attributes || typeof attributes !== 'object') return '';
16
+ return Object.entries(attributes)
17
+ .filter(([name, value]) => (
18
+ value != null &&
19
+ value !== false &&
20
+ /^[a-zA-Z_][\w:.-]*$/.test(name) &&
21
+ !/^on/i.test(name)
22
+ ))
23
+ .map(([name, value]) => (value === true
24
+ ? ` ${escapeHtml(name)}`
25
+ : ` ${escapeHtml(name)}="${escapeHtml(value)}"`))
26
+ .join('');
27
+ }
28
+
29
+ export function createPaddockLoadingPlaceholder({
30
+ label = 'Loading simulator',
31
+ detail = '',
32
+ className = '',
33
+ variant = 'lights',
34
+ attributes,
35
+ } = {}) {
36
+ const normalizedVariant = normalizeVariant(variant);
37
+ const extraClassName = className ? ` ${escapeHtml(className)}` : '';
38
+ const detailMarkup = detail
39
+ ? `<span class="paddock-placeholder__detail">${escapeHtml(detail)}</span>`
40
+ : '';
41
+ const lightsMarkup = normalizedVariant === 'lights'
42
+ ? `
43
+ <div class="paddock-placeholder__lights" aria-hidden="true">
44
+ <span></span>
45
+ <span></span>
46
+ <span></span>
47
+ <span></span>
48
+ <span></span>
49
+ </div>`
50
+ : '';
51
+
52
+ return `
53
+ <div class="paddock-placeholder${extraClassName}" data-paddock-placeholder data-paddock-placeholder-variant="${normalizedVariant}" role="status" aria-live="polite" aria-busy="true"${renderAttributes(attributes)}>
54
+ <div class="paddock-placeholder__content">
55
+ ${lightsMarkup}
56
+ <span class="paddock-placeholder__label">${escapeHtml(label)}</span>
57
+ ${detailMarkup}
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
@@ -0,0 +1,72 @@
1
+ .paddock-placeholder {
2
+ --paddock-placeholder-background: #000;
3
+ --paddock-placeholder-color: rgba(244, 241, 234, 0.78);
4
+ --paddock-placeholder-light: #ff1f18;
5
+ --paddock-placeholder-light-dim: #8f0804;
6
+ --paddock-placeholder-min-height: 420px;
7
+ position: relative;
8
+ display: grid;
9
+ min-height: var(--paddock-placeholder-min-height);
10
+ place-items: center;
11
+ overflow: hidden;
12
+ color: var(--paddock-placeholder-color);
13
+ background: var(--paddock-placeholder-background);
14
+ }
15
+
16
+ .paddock-placeholder__content {
17
+ display: grid;
18
+ justify-items: center;
19
+ gap: 0.8rem;
20
+ padding: 1.25rem;
21
+ text-align: center;
22
+ }
23
+
24
+ .paddock-placeholder__label {
25
+ font: 800 0.72rem/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ letter-spacing: 0.14em;
27
+ text-transform: uppercase;
28
+ }
29
+
30
+ .paddock-placeholder__detail {
31
+ max-width: 32rem;
32
+ font: 500 0.82rem/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
33
+ opacity: 0.72;
34
+ }
35
+
36
+ .paddock-placeholder__lights {
37
+ display: grid;
38
+ grid-template-columns: repeat(5, 0.78rem);
39
+ gap: 0.42rem;
40
+ padding: 0.32rem 0.46rem;
41
+ border-radius: 999px;
42
+ background: rgba(255, 255, 255, 0.08);
43
+ }
44
+
45
+ .paddock-placeholder__lights span {
46
+ width: 0.78rem;
47
+ height: 0.78rem;
48
+ border-radius: 50%;
49
+ background: var(--paddock-placeholder-light-dim);
50
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
51
+ animation: paddock-placeholder-lights 1200ms cubic-bezier(0.45, 0, 0.2, 1) infinite;
52
+ }
53
+
54
+ .paddock-placeholder__lights span:nth-child(2) { animation-delay: 120ms; }
55
+ .paddock-placeholder__lights span:nth-child(3) { animation-delay: 240ms; }
56
+ .paddock-placeholder__lights span:nth-child(4) { animation-delay: 360ms; }
57
+ .paddock-placeholder__lights span:nth-child(5) { animation-delay: 480ms; }
58
+
59
+ .paddock-placeholder[data-paddock-placeholder-variant="custom"] .paddock-placeholder__content {
60
+ min-height: inherit;
61
+ }
62
+
63
+ @keyframes paddock-placeholder-lights {
64
+ 0%, 100% {
65
+ background: var(--paddock-placeholder-light-dim);
66
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
67
+ }
68
+ 45%, 70% {
69
+ background: var(--paddock-placeholder-light);
70
+ box-shadow: 0 0 14px rgba(225, 6, 0, 0.56);
71
+ }
72
+ }
@@ -49,5 +49,6 @@ export function createSafetyCarControlMarkup({ compact = false } = {}) {
49
49
  ariaLabel: 'Safety car control',
50
50
  body,
51
51
  unsupportedLabel: 'Safety car control',
52
+ loadingLabel: 'Safety car control',
52
53
  });
53
54
  }
@@ -64,5 +64,6 @@ export function createRaceTelemetryDrawerMarkup(options, {
64
64
  attributes: 'data-race-telemetry-drawer',
65
65
  body,
66
66
  unsupportedLabel: 'Race telemetry drawer',
67
+ loadingLabel: 'Race telemetry drawer',
67
68
  });
68
69
  }
@@ -158,5 +158,6 @@ export function createTelemetryPanelMarkup(options, { includeOverview = options.
158
158
  ariaLabel: 'Selected car telemetry stack',
159
159
  body,
160
160
  unsupportedLabel: 'Telemetry stack',
161
+ loadingLabel: 'Telemetry stack',
161
162
  });
162
163
  }