@inventure71/paddockjs 4.2.0 → 4.3.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.
Files changed (249) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INSTALL_AND_UPDATE.md +23 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/grid_walk.md +80 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +36 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +23 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
@@ -0,0 +1,72 @@
1
+ import { DEFAULT_PADDOCK_THEME_INPUT, LEGACY_THEME_CSS_VARIABLES, LEGACY_TOKEN_ALIASES, PADDOCK_THEME_CSS_VARIABLES, PADDOCK_THEME_SCOPED_CSS_VARIABLES } from './themeTokens.js';
2
+ import { normalizePaddockTheme, resolveActiveThemeMode } from './themeResolver.js';
3
+ import { selectThemePackage } from './themeSelectors.js';
4
+ import { isPlainObject, toKebabCase } from './themeUtils.js';
5
+
6
+ export function applyPaddockThemeCssVariables(root, theme = DEFAULT_PADDOCK_THEME_INPUT, context = {}) {
7
+ const resolved = isResolvedTheme(theme) ? theme : normalizePaddockTheme(theme);
8
+ const activeMode = resolveActiveThemeMode(resolved.mode);
9
+ root?.setAttribute?.('data-paddock-theme-mode', activeMode);
10
+ applyResolvedThemeCssVariables(root, resolved.themes?.[resolved.use] ?? resolved, activeMode);
11
+
12
+ getComponentElements(root).forEach((element) => {
13
+ const componentName = element.getAttribute?.('data-paddock-component');
14
+ const themeName = resolved.componentThemes?.[componentName];
15
+ const selectedTheme = selectThemePackage(themeName, resolved, context);
16
+ if (!selectedTheme) {
17
+ if (element !== root) clearPaddockThemeCssVariables(element);
18
+ return;
19
+ }
20
+ applyResolvedThemeCssVariables(element, selectedTheme, activeMode);
21
+ });
22
+ }
23
+
24
+ function applyResolvedThemeCssVariables(target, themePackage, activeMode) {
25
+ clearPaddockThemeCssVariables(target);
26
+ const tokens = themePackage?.tokens?.[activeMode] ?? themePackage?.activeTokens ?? {};
27
+ applyTokenCssVariables(target, tokens);
28
+ applyComponentSlotCssVariables(target, tokens, themePackage?.components);
29
+ }
30
+
31
+ function clearPaddockThemeCssVariables(target) {
32
+ const removeProperty = target?.style?.removeProperty;
33
+ if (typeof removeProperty !== 'function') return;
34
+ PADDOCK_THEME_SCOPED_CSS_VARIABLES.forEach((variable) => {
35
+ removeProperty.call(target.style, variable);
36
+ });
37
+ }
38
+
39
+ function applyTokenCssVariables(target, tokens = {}) {
40
+ Object.entries(PADDOCK_THEME_CSS_VARIABLES).forEach(([key, variable]) => {
41
+ const value = tokens[key];
42
+ if (value == null || value === '') return;
43
+ target?.style?.setProperty?.(variable, String(value));
44
+ });
45
+ Object.entries(LEGACY_THEME_CSS_VARIABLES).forEach(([legacyKey, variable]) => {
46
+ const tokenKey = LEGACY_TOKEN_ALIASES[legacyKey];
47
+ const value = tokens[tokenKey];
48
+ if (value == null || value === '') return;
49
+ target?.style?.setProperty?.(variable, String(value));
50
+ });
51
+ }
52
+
53
+ function applyComponentSlotCssVariables(target, tokens = {}, components = {}) {
54
+ Object.entries(components).forEach(([componentName, slots]) => {
55
+ Object.entries(slots).forEach(([slot, tokenName]) => {
56
+ const value = tokens[tokenName];
57
+ if (value == null || value === '') return;
58
+ target?.style?.setProperty?.(`--paddock-${toKebabCase(componentName)}-${toKebabCase(slot)}`, String(value));
59
+ });
60
+ });
61
+ }
62
+
63
+ function getComponentElements(root) {
64
+ const elements = [];
65
+ if (root?.matches?.('[data-paddock-component]')) elements.push(root);
66
+ root?.querySelectorAll?.('[data-paddock-component]')?.forEach?.((element) => elements.push(element));
67
+ return elements;
68
+ }
69
+
70
+ function isResolvedTheme(theme) {
71
+ return isPlainObject(theme) && isPlainObject(theme.tokens) && isPlainObject(theme.themes) && isPlainObject(theme.componentThemes);
72
+ }
@@ -0,0 +1,155 @@
1
+ import { DEFAULT_COMPONENT_SLOTS, LEGACY_ALIAS_KEYS, LEGACY_TOKEN_ALIASES, THEME_MODES, THEME_TOKEN_KEYS, THEME_TOKEN_SET } from './themeTokens.js';
2
+ import { isPlainObject, warnUnknownThemeKey } from './themeUtils.js';
3
+
4
+ export function mergeThemeInputs(...inputs) {
5
+ return inputs.reduce((merged, input) => mergeThemeInputInto(merged, input), {});
6
+ }
7
+
8
+ export function mergeThemeInputInto(merged, input) {
9
+ if (!isPlainObject(input)) return merged;
10
+ const next = { ...merged };
11
+ if (THEME_MODES.has(input.mode)) next.mode = input.mode;
12
+ if (typeof input.use === 'string') next.use = input.use;
13
+ if (typeof input.extends === 'string') next.extends = input.extends;
14
+
15
+ const tokenInput = extractTokenInput(input);
16
+ if (Object.keys(tokenInput).length > 0) {
17
+ next.tokens = { ...(next.tokens ?? {}), ...tokenInput };
18
+ }
19
+
20
+ if (isPlainObject(input.themes)) {
21
+ next.themes = { ...(next.themes ?? {}) };
22
+ Object.entries(input.themes).forEach(([name, themePackage]) => {
23
+ if (!isPlainObject(themePackage)) return;
24
+ next.themes[name] = mergeThemeInputInto(next.themes[name] ?? {}, themePackage);
25
+ });
26
+ }
27
+ if (isPlainObject(input.components)) {
28
+ const legacyComponentThemes = getLegacyComponentThemeMap(input.components);
29
+ if (legacyComponentThemes) next.componentThemes = { ...(next.componentThemes ?? {}), ...legacyComponentThemes };
30
+ const componentSlots = getComponentSlotOverrides(input.components);
31
+ if (Object.keys(componentSlots).length > 0) {
32
+ next.components = mergeComponentSlots(next.components ?? {}, componentSlots);
33
+ }
34
+ }
35
+ if (isPlainObject(input.componentThemes)) {
36
+ next.componentThemes = { ...(next.componentThemes ?? {}), ...input.componentThemes };
37
+ }
38
+ if (isPlainObject(input.teamThemes)) {
39
+ next.teamThemes = { ...(next.teamThemes ?? {}), ...input.teamThemes };
40
+ }
41
+
42
+ return next;
43
+ }
44
+
45
+ export function pickTopLevelThemeInput(input) {
46
+ if (!isPlainObject(input)) return {};
47
+ const result = {};
48
+ ['tokens', 'components', 'componentThemes', 'teamThemes', 'light', 'dark'].forEach((key) => {
49
+ if (Object.hasOwn(input, key)) result[key] = input[key];
50
+ });
51
+ [...THEME_TOKEN_KEYS, ...LEGACY_ALIAS_KEYS].forEach((key) => {
52
+ if (Object.hasOwn(input, key)) result[key] = input[key];
53
+ });
54
+ return result;
55
+ }
56
+
57
+ export function extractTokenInput(input) {
58
+ if (!isPlainObject(input)) return {};
59
+ const result = {};
60
+ if (isPlainObject(input.light) || isPlainObject(input.dark)) {
61
+ Object.assign(result, extractModeTokenInput(input.light, input.dark));
62
+ }
63
+ const hasResolvedActiveTokens = isPlainObject(input.activeTokens);
64
+ if (!hasResolvedActiveTokens) {
65
+ THEME_TOKEN_KEYS.forEach((key) => {
66
+ if (Object.hasOwn(input, key)) result[key] = input[key];
67
+ });
68
+ LEGACY_ALIAS_KEYS.forEach((key) => {
69
+ if (Object.hasOwn(input, key)) result[LEGACY_TOKEN_ALIASES[key]] = input[key];
70
+ });
71
+ }
72
+ if (isPlainObject(input.tokens)) {
73
+ const modeTokens = extractModeTokenInput(input.tokens.light, input.tokens.dark);
74
+ Object.assign(result, modeTokens);
75
+ Object.entries(input.tokens).forEach(([key, value]) => {
76
+ if (key === 'light' || key === 'dark') return;
77
+ const tokenKey = LEGACY_TOKEN_ALIASES[key] ?? key;
78
+ if (THEME_TOKEN_SET.has(tokenKey)) result[tokenKey] = value;
79
+ else warnUnknownThemeKey(key);
80
+ });
81
+ }
82
+ return result;
83
+ }
84
+
85
+ function extractModeTokenInput(lightInput, darkInput) {
86
+ const result = {};
87
+ addModeTokens(result, 'light', lightInput);
88
+ addModeTokens(result, 'dark', darkInput);
89
+ return result;
90
+ }
91
+
92
+ function addModeTokens(result, mode, input) {
93
+ if (!isPlainObject(input)) return;
94
+ Object.entries(input).forEach(([key, value]) => {
95
+ const tokenKey = LEGACY_TOKEN_ALIASES[key] ?? key;
96
+ if (!THEME_TOKEN_SET.has(tokenKey)) {
97
+ warnUnknownThemeKey(key);
98
+ return;
99
+ }
100
+ const current = isPlainObject(result[tokenKey]) ? result[tokenKey] : {};
101
+ result[tokenKey] = { ...current, [mode]: value };
102
+ });
103
+ }
104
+
105
+ export function getLegacyComponentThemeMap(components) {
106
+ if (!isPlainObject(components)) return null;
107
+ const entries = Object.entries(components)
108
+ .map(([componentName, value]) => [
109
+ componentName,
110
+ typeof value === 'string' ? value : isPlainObject(value) && typeof value.theme === 'string' ? value.theme : null,
111
+ ])
112
+ .filter(([, selector]) => selector);
113
+ return entries.length > 0 ? Object.fromEntries(entries) : null;
114
+ }
115
+
116
+ export function getComponentSlotOverrides(components) {
117
+ if (!isPlainObject(components)) return {};
118
+ const result = {};
119
+ Object.entries(components).forEach(([componentName, slots]) => {
120
+ const canonicalName = normalizeComponentSlotName(componentName);
121
+ const schema = DEFAULT_COMPONENT_SLOTS[canonicalName];
122
+ if (!schema || !isPlainObject(slots)) return;
123
+ Object.entries(slots).forEach(([slot, tokenName]) => {
124
+ if (slot === 'theme') return;
125
+ if (!Object.hasOwn(schema, slot) || !THEME_TOKEN_SET.has(tokenName)) {
126
+ warnUnknownThemeKey(`${componentName}.${slot}`);
127
+ return;
128
+ }
129
+ result[canonicalName] = {
130
+ ...(result[canonicalName] ?? {}),
131
+ [slot]: tokenName,
132
+ };
133
+ });
134
+ });
135
+ return result;
136
+ }
137
+
138
+ export function mergeComponentSlots(parent = {}, overrides = {}) {
139
+ const result = {};
140
+ Object.entries(parent).forEach(([componentName, slots]) => {
141
+ result[componentName] = { ...slots };
142
+ });
143
+ Object.entries(overrides).forEach(([componentName, slots]) => {
144
+ if (!Object.hasOwn(DEFAULT_COMPONENT_SLOTS, componentName)) return;
145
+ result[componentName] = {
146
+ ...(result[componentName] ?? DEFAULT_COMPONENT_SLOTS[componentName]),
147
+ ...slots,
148
+ };
149
+ });
150
+ return result;
151
+ }
152
+
153
+ function normalizeComponentSlotName(name) {
154
+ return String(name).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
155
+ }
@@ -0,0 +1,172 @@
1
+ import { DEFAULT_COMPONENT_THEME_SELECTORS, DEFAULT_THEME_PACKAGE, LEGACY_TOKEN_ALIASES, THEME_COLOR_TOKEN_KEYS, THEME_MODES, THEME_SIZE_TOKEN_KEYS } from './themeTokens.js';
2
+ import { extractTokenInput, getComponentSlotOverrides, getLegacyComponentThemeMap, mergeComponentSlots, mergeThemeInputInto, pickTopLevelThemeInput } from './themeInputs.js';
3
+ import { deriveModeColor, normalizeCssColorToken, normalizeCssSizeToken } from './cssTokens.js';
4
+ import { normalizeComponentThemeSelectors, normalizeThemeSelectors } from './themeSelectors.js';
5
+ import { isPlainObject } from './themeUtils.js';
6
+
7
+ const ACTIVE_THEME_MODES = new Set(['dark', 'light']);
8
+ const RESOLVED_THEME_CACHE = new Map();
9
+
10
+ export function normalizePaddockTheme(input = {}) {
11
+ const mode = THEME_MODES.has(input?.mode) ? input.mode : 'dark';
12
+ const activeMode = resolveActiveThemeMode(mode);
13
+ const cacheKey = stableStringify({ input, activeMode });
14
+ if (RESOLVED_THEME_CACHE.has(cacheKey)) return RESOLVED_THEME_CACHE.get(cacheKey);
15
+
16
+ const themeInputs = isPlainObject(input?.themes) ? input.themes : {};
17
+ const resolving = new Set();
18
+ const resolvedThemes = {};
19
+
20
+ const resolveNamedTheme = (name) => {
21
+ const themeName = typeof name === 'string' && name !== '' ? name : 'default';
22
+ if (resolvedThemes[themeName]) return resolvedThemes[themeName];
23
+ if (resolving.has(themeName)) return resolvedThemes.default ?? DEFAULT_THEME_PACKAGE;
24
+ resolving.add(themeName);
25
+
26
+ const rawPackage = themeName === 'default'
27
+ ? mergeThemeInputInto(themeInputs.default ?? {}, pickTopLevelThemeInput(input))
28
+ : themeInputs[themeName];
29
+ const parentName = themeName === 'default'
30
+ ? null
31
+ : typeof rawPackage?.extends === 'string' && rawPackage.extends !== themeName
32
+ ? rawPackage.extends
33
+ : 'default';
34
+ const parent = parentName ? resolveNamedTheme(parentName) : DEFAULT_THEME_PACKAGE;
35
+ const resolved = resolveThemePackage(rawPackage, parent);
36
+ resolvedThemes[themeName] = resolved;
37
+ resolving.delete(themeName);
38
+ return resolved;
39
+ };
40
+
41
+ resolveNamedTheme('default');
42
+ Object.keys(themeInputs).forEach((name) => {
43
+ if (isPlainObject(themeInputs[name])) resolveNamedTheme(name);
44
+ });
45
+
46
+ const requestedUse = typeof input?.use === 'string' ? input.use : 'default';
47
+ const use = Object.hasOwn(resolvedThemes, requestedUse) ? requestedUse : 'default';
48
+ const activeTheme = resolvedThemes[use] ?? resolvedThemes.default;
49
+ const activeTokens = activeTheme.tokens[activeMode];
50
+ const componentThemes = {
51
+ ...DEFAULT_COMPONENT_THEME_SELECTORS,
52
+ ...normalizeComponentThemeSelectors(
53
+ input?.componentThemes ?? getLegacyComponentThemeMap(input?.components),
54
+ resolvedThemes,
55
+ ),
56
+ };
57
+ const teamThemes = normalizeThemeSelectors(input?.teamThemes, resolvedThemes);
58
+ const result = {
59
+ mode,
60
+ activeMode,
61
+ use,
62
+ tokens: activeTheme.tokens,
63
+ activeTokens,
64
+ components: activeTheme.components,
65
+ themes: resolvedThemes,
66
+ componentThemes,
67
+ teamThemes,
68
+ light: toLegacyThemeTokens(activeTheme.tokens.light),
69
+ dark: toLegacyThemeTokens(activeTheme.tokens.dark),
70
+ ...toLegacyThemeTokens(activeTokens),
71
+ };
72
+
73
+ RESOLVED_THEME_CACHE.set(cacheKey, result);
74
+ return result;
75
+ }
76
+
77
+ function resolveThemePackage(input, parent = DEFAULT_THEME_PACKAGE) {
78
+ const tokenInput = extractTokenInput(input);
79
+ const tokens = {
80
+ light: {},
81
+ dark: {},
82
+ };
83
+
84
+ THEME_COLOR_TOKEN_KEYS.forEach((key) => {
85
+ const fallbackPair = {
86
+ light: parent.tokens.light[key],
87
+ dark: parent.tokens.dark[key],
88
+ };
89
+ const pair = Object.hasOwn(tokenInput, key)
90
+ ? resolveColorModePair(key, tokenInput[key], fallbackPair)
91
+ : fallbackPair;
92
+ tokens.light[key] = pair.light;
93
+ tokens.dark[key] = pair.dark;
94
+ });
95
+
96
+ THEME_SIZE_TOKEN_KEYS.forEach((key) => {
97
+ const fallbackPair = {
98
+ light: parent.tokens.light[key],
99
+ dark: parent.tokens.dark[key],
100
+ };
101
+ const pair = Object.hasOwn(tokenInput, key)
102
+ ? resolveSizeModePair(tokenInput[key], fallbackPair)
103
+ : fallbackPair;
104
+ tokens.light[key] = pair.light;
105
+ tokens.dark[key] = pair.dark;
106
+ });
107
+
108
+ return {
109
+ tokens,
110
+ components: mergeComponentSlots(parent.components, getComponentSlotOverrides(input?.components)),
111
+ };
112
+ }
113
+
114
+ function resolveColorModePair(key, value, fallbackPair) {
115
+ const pair = isPlainObject(value) ? value : { light: value, dark: value };
116
+ const light = normalizeCssColorToken(pair.light, null);
117
+ const dark = normalizeCssColorToken(pair.dark, null);
118
+ if (light && dark) return { light, dark };
119
+ if (light) return {
120
+ light,
121
+ dark: deriveModeColor('dark', key, light) ?? fallbackPair.dark,
122
+ };
123
+ if (dark) return {
124
+ light: deriveModeColor('light', key, dark) ?? fallbackPair.light,
125
+ dark,
126
+ };
127
+ return fallbackPair;
128
+ }
129
+
130
+ function resolveSizeModePair(value, fallbackPair) {
131
+ const pair = isPlainObject(value) ? value : { light: value, dark: value };
132
+ const light = normalizeCssSizeToken(pair.light, null);
133
+ const dark = normalizeCssSizeToken(pair.dark, null);
134
+ if (light && dark) return { light, dark };
135
+ if (light) return { light, dark: light };
136
+ if (dark) return { light: dark, dark };
137
+ return fallbackPair;
138
+ }
139
+
140
+ function toLegacyThemeTokens(tokens = {}) {
141
+ const aliases = {};
142
+ Object.entries(LEGACY_TOKEN_ALIASES).forEach(([legacyKey, tokenKey]) => {
143
+ aliases[legacyKey] = tokens[tokenKey];
144
+ });
145
+ return { ...tokens, ...aliases };
146
+ }
147
+
148
+ export function resolveActiveThemeMode(mode) {
149
+ if (ACTIVE_THEME_MODES.has(mode)) return mode;
150
+ if (globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches) return 'light';
151
+ return 'dark';
152
+ }
153
+
154
+ function stableStringify(value) {
155
+ return JSON.stringify(sortForStableStringify(value, new WeakSet()));
156
+ }
157
+
158
+ function sortForStableStringify(value, seen) {
159
+ if (typeof value === 'bigint') return `bigint:${value.toString()}`;
160
+ if (typeof value === 'function') return `function:${value.name ?? ''}`;
161
+ if (typeof value === 'symbol') return value.toString();
162
+ if (value === undefined) return 'undefined';
163
+ if (Array.isArray(value)) return value.map((item) => sortForStableStringify(item, seen));
164
+ if (!isPlainObject(value)) return value;
165
+ if (seen.has(value)) return '[Circular]';
166
+ seen.add(value);
167
+ return Object.fromEntries(
168
+ Object.keys(value)
169
+ .sort()
170
+ .map((key) => [key, sortForStableStringify(value[key], seen)]),
171
+ );
172
+ }
@@ -0,0 +1,69 @@
1
+ import { COMPONENT_THEME_SELECTOR_ALIASES, PACKAGE_COMPONENT_THEME_KEYS } from './themeTokens.js';
2
+ import { isPlainObject, toKebabCase, warnUnknownThemeKey } from './themeUtils.js';
3
+
4
+ export function normalizeThemeSelectors(selectors, themes) {
5
+ if (!isPlainObject(selectors)) return {};
6
+ return Object.fromEntries(
7
+ Object.entries(selectors)
8
+ .map(([key, selector]) => [
9
+ key,
10
+ typeof selector === 'string' ? selector : isPlainObject(selector) ? selector.theme : null,
11
+ ])
12
+ .filter(([, selector]) => isValidThemeSelector(selector, themes)),
13
+ );
14
+ }
15
+
16
+ export function normalizeComponentThemeSelectors(selectors, themes) {
17
+ if (!isPlainObject(selectors)) return {};
18
+ const result = {};
19
+ Object.entries(selectors).forEach(([key, selectorInput]) => {
20
+ const selector = typeof selectorInput === 'string'
21
+ ? selectorInput
22
+ : isPlainObject(selectorInput)
23
+ ? selectorInput.theme
24
+ : null;
25
+ if (!isValidThemeSelector(selector, themes)) return;
26
+ normalizeComponentThemeSelectorKeys(key).forEach((componentKey) => {
27
+ if (PACKAGE_COMPONENT_THEME_KEYS.has(componentKey)) result[componentKey] = selector;
28
+ else warnUnknownThemeKey(key);
29
+ });
30
+ });
31
+ return result;
32
+ }
33
+
34
+ function normalizeComponentThemeSelectorKeys(key) {
35
+ const raw = String(key);
36
+ if (COMPONENT_THEME_SELECTOR_ALIASES[raw]) return COMPONENT_THEME_SELECTOR_ALIASES[raw];
37
+ return [raw.includes('-') ? raw : toKebabCase(raw)];
38
+ }
39
+
40
+ function isValidThemeSelector(selector, themes) {
41
+ if (typeof selector !== 'string' || selector === '') return false;
42
+ if (selector === 'default' || selector === 'active' || selector === 'selectedTeam' || selector === 'team') return true;
43
+ if (selector.startsWith('team:')) return selector.length > 5;
44
+ return Object.hasOwn(themes, selector);
45
+ }
46
+
47
+ export function selectThemePackage(selector, resolved, context = {}, visitedTeamIds = new Set()) {
48
+ if (!selector || selector === 'active') return null;
49
+ if (selector === 'default') return resolved.themes?.default;
50
+ if (selector === 'team' || selector === 'selectedTeam' || selector.startsWith('team:')) {
51
+ const teamId = selector.startsWith('team:')
52
+ ? selector.slice(5)
53
+ : selector === 'selectedTeam'
54
+ ? context.selectedTeamId
55
+ : context.teamId ?? context.selectedTeamId;
56
+ const teamTheme = selectTeamThemePackage(teamId, resolved, context, visitedTeamIds);
57
+ return teamTheme ?? resolved.themes?.[resolved.use] ?? resolved.themes?.default;
58
+ }
59
+ return resolved.themes?.[selector] ?? null;
60
+ }
61
+
62
+ function selectTeamThemePackage(teamId, resolved, context, visitedTeamIds) {
63
+ if (typeof teamId !== 'string' || teamId === '' || visitedTeamIds.has(teamId)) return null;
64
+ const selector = resolved.teamThemes?.[teamId];
65
+ if (!selector) return null;
66
+ visitedTeamIds.add(teamId);
67
+ if (selector === 'team' || selector === 'selectedTeam') return null;
68
+ return selectThemePackage(selector, resolved, context, visitedTeamIds);
69
+ }