@inventure71/paddockjs 2.0.1 → 3.0.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 +31 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +65 -15
- package/docs/architecture.md +17 -13
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +96 -21
- package/docs/index.md +2 -1
- package/docs/learnings.md +6 -6
- package/docs/rules.md +7 -7
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +25 -18
- package/docs/training.md +13 -12
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +74 -4
- package/src/app/F1SimulatorApp.js +191 -28
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/index.d.ts +1 -2
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +3 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/surfaceRays.js +41 -33
- package/src/environment/sensors/trackRays.js +44 -49
- package/src/index.d.ts +110 -9
- package/src/index.js +25 -1
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitKinematics.js +2 -2
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryIndex.js +180 -135
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +3 -3
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +3 -3
- package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
- package/src/simulation/vehicle/vehicleState.js +1 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
const THEME_MODES = new Set(['dark', 'light', 'system']);
|
|
2
|
+
const ACTIVE_THEME_MODES = new Set(['dark', 'light']);
|
|
3
|
+
const GENERATED_COLOR_CACHE = new Map();
|
|
4
|
+
const RESOLVED_THEME_CACHE = new Map();
|
|
5
|
+
|
|
6
|
+
export const THEME_COLOR_TOKEN_KEYS = [
|
|
7
|
+
'primary',
|
|
8
|
+
'primaryText',
|
|
9
|
+
'secondary',
|
|
10
|
+
'secondaryText',
|
|
11
|
+
'surface',
|
|
12
|
+
'surfaceRaised',
|
|
13
|
+
'surfacePanel',
|
|
14
|
+
'text',
|
|
15
|
+
'mutedText',
|
|
16
|
+
'border',
|
|
17
|
+
'success',
|
|
18
|
+
'warning',
|
|
19
|
+
'danger',
|
|
20
|
+
'info',
|
|
21
|
+
'yellowFlag',
|
|
22
|
+
'greenFlag',
|
|
23
|
+
'redFlag',
|
|
24
|
+
'safetyCar',
|
|
25
|
+
'drsActive',
|
|
26
|
+
'pitLane',
|
|
27
|
+
'track',
|
|
28
|
+
'trackEdge',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const THEME_SIZE_TOKEN_KEYS = [
|
|
32
|
+
'timingTowerMaxWidth',
|
|
33
|
+
'raceViewMinHeight',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const THEME_TOKEN_KEYS = [...THEME_COLOR_TOKEN_KEYS, ...THEME_SIZE_TOKEN_KEYS];
|
|
37
|
+
const THEME_TOKEN_SET = new Set(THEME_TOKEN_KEYS);
|
|
38
|
+
const COLOR_TOKEN_SET = new Set(THEME_COLOR_TOKEN_KEYS);
|
|
39
|
+
const SIZE_TOKEN_SET = new Set(THEME_SIZE_TOKEN_KEYS);
|
|
40
|
+
|
|
41
|
+
const LEGACY_TOKEN_ALIASES = {
|
|
42
|
+
accentColor: 'primary',
|
|
43
|
+
greenColor: 'greenFlag',
|
|
44
|
+
yellowColor: 'yellowFlag',
|
|
45
|
+
blueColor: 'info',
|
|
46
|
+
raceControlRedColor: 'redFlag',
|
|
47
|
+
surfaceColor: 'surface',
|
|
48
|
+
surfaceRaisedColor: 'surfaceRaised',
|
|
49
|
+
surfacePanelColor: 'surfacePanel',
|
|
50
|
+
lineColor: 'border',
|
|
51
|
+
lineStrongColor: 'border',
|
|
52
|
+
textColor: 'text',
|
|
53
|
+
mutedTextColor: 'mutedText',
|
|
54
|
+
trackColor: 'track',
|
|
55
|
+
trackEdgeColor: 'trackEdge',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const LEGACY_ALIAS_KEYS = Object.keys(LEGACY_TOKEN_ALIASES);
|
|
59
|
+
|
|
60
|
+
export const PADDOCK_THEME_CSS_VARIABLES = {
|
|
61
|
+
primary: '--paddock-color-primary',
|
|
62
|
+
primaryText: '--paddock-color-primary-text',
|
|
63
|
+
secondary: '--paddock-color-secondary',
|
|
64
|
+
secondaryText: '--paddock-color-secondary-text',
|
|
65
|
+
surface: '--paddock-color-surface',
|
|
66
|
+
surfaceRaised: '--paddock-color-surface-raised',
|
|
67
|
+
surfacePanel: '--paddock-color-surface-panel',
|
|
68
|
+
text: '--paddock-color-text',
|
|
69
|
+
mutedText: '--paddock-color-muted-text',
|
|
70
|
+
border: '--paddock-color-border',
|
|
71
|
+
success: '--paddock-color-success',
|
|
72
|
+
warning: '--paddock-color-warning',
|
|
73
|
+
danger: '--paddock-color-danger',
|
|
74
|
+
info: '--paddock-color-info',
|
|
75
|
+
yellowFlag: '--paddock-color-yellow-flag',
|
|
76
|
+
greenFlag: '--paddock-color-green-flag',
|
|
77
|
+
redFlag: '--paddock-color-red-flag',
|
|
78
|
+
safetyCar: '--paddock-color-safety-car',
|
|
79
|
+
drsActive: '--paddock-color-drs-active',
|
|
80
|
+
pitLane: '--paddock-color-pit-lane',
|
|
81
|
+
track: '--paddock-color-track',
|
|
82
|
+
trackEdge: '--paddock-color-track-edge',
|
|
83
|
+
timingTowerMaxWidth: '--paddock-timing-tower-max-width',
|
|
84
|
+
raceViewMinHeight: '--paddock-race-view-min-height',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const LEGACY_THEME_CSS_VARIABLES = {
|
|
88
|
+
accentColor: '--paddock-accent-color',
|
|
89
|
+
greenColor: '--paddock-green-color',
|
|
90
|
+
yellowColor: '--paddock-yellow-color',
|
|
91
|
+
blueColor: '--paddock-blue-color',
|
|
92
|
+
raceControlRedColor: '--paddock-race-control-red-color',
|
|
93
|
+
surfaceColor: '--paddock-surface-color',
|
|
94
|
+
surfaceRaisedColor: '--paddock-surface-raised-color',
|
|
95
|
+
surfacePanelColor: '--paddock-surface-panel-color',
|
|
96
|
+
lineColor: '--paddock-line-color',
|
|
97
|
+
lineStrongColor: '--paddock-line-strong-color',
|
|
98
|
+
textColor: '--paddock-text-color',
|
|
99
|
+
mutedTextColor: '--paddock-muted-text-color',
|
|
100
|
+
trackColor: '--paddock-track-color',
|
|
101
|
+
trackEdgeColor: '--paddock-track-edge-color',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const DEFAULT_COMPONENT_SLOTS = {
|
|
105
|
+
button: {
|
|
106
|
+
background: 'primary',
|
|
107
|
+
text: 'primaryText',
|
|
108
|
+
border: 'primary',
|
|
109
|
+
},
|
|
110
|
+
raceControls: {
|
|
111
|
+
background: 'surfacePanel',
|
|
112
|
+
text: 'text',
|
|
113
|
+
border: 'border',
|
|
114
|
+
accent: 'primary',
|
|
115
|
+
},
|
|
116
|
+
cameraControls: {
|
|
117
|
+
background: 'surfacePanel',
|
|
118
|
+
text: 'text',
|
|
119
|
+
border: 'border',
|
|
120
|
+
accent: 'primary',
|
|
121
|
+
},
|
|
122
|
+
timingTower: {
|
|
123
|
+
shellBackground: 'surfacePanel',
|
|
124
|
+
rowBackground: 'surfaceRaised',
|
|
125
|
+
driverText: 'text',
|
|
126
|
+
gapText: 'mutedText',
|
|
127
|
+
warningText: 'yellowFlag',
|
|
128
|
+
},
|
|
129
|
+
raceCanvas: {
|
|
130
|
+
track: 'track',
|
|
131
|
+
trackEdge: 'trackEdge',
|
|
132
|
+
pitLane: 'pitLane',
|
|
133
|
+
},
|
|
134
|
+
raceDataPanel: {
|
|
135
|
+
background: 'surfacePanel',
|
|
136
|
+
text: 'text',
|
|
137
|
+
accent: 'primary',
|
|
138
|
+
},
|
|
139
|
+
selectedDriverPanel: {
|
|
140
|
+
background: 'surfacePanel',
|
|
141
|
+
text: 'text',
|
|
142
|
+
accent: 'primary',
|
|
143
|
+
},
|
|
144
|
+
driverRows: {
|
|
145
|
+
background: 'surfaceRaised',
|
|
146
|
+
text: 'text',
|
|
147
|
+
accent: 'primary',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const DEFAULT_COMPONENT_THEME_SELECTORS = {
|
|
152
|
+
'car-driver-overview': 'selectedTeam',
|
|
153
|
+
'race-data-panel': 'selectedTeam',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const PADDOCK_COMPONENT_SLOT_CSS_VARIABLES = Object.entries(DEFAULT_COMPONENT_SLOTS).flatMap(([componentName, slots]) => (
|
|
157
|
+
Object.keys(slots).map((slot) => `--paddock-${toKebabCase(componentName)}-${toKebabCase(slot)}`)
|
|
158
|
+
));
|
|
159
|
+
|
|
160
|
+
const PADDOCK_THEME_SCOPED_CSS_VARIABLES = [
|
|
161
|
+
...new Set([
|
|
162
|
+
...Object.values(PADDOCK_THEME_CSS_VARIABLES),
|
|
163
|
+
...Object.values(LEGACY_THEME_CSS_VARIABLES),
|
|
164
|
+
...PADDOCK_COMPONENT_SLOT_CSS_VARIABLES,
|
|
165
|
+
]),
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const PACKAGE_COMPONENT_THEME_KEYS = new Set([
|
|
169
|
+
'button',
|
|
170
|
+
'race-controls',
|
|
171
|
+
'camera-controls',
|
|
172
|
+
'safety-car-control',
|
|
173
|
+
'timing-tower',
|
|
174
|
+
'race-canvas',
|
|
175
|
+
'race-data-panel',
|
|
176
|
+
'car-driver-overview',
|
|
177
|
+
'race-telemetry-drawer',
|
|
178
|
+
'telemetry-stack',
|
|
179
|
+
'telemetry-core',
|
|
180
|
+
'telemetry-sectors',
|
|
181
|
+
'telemetry-lap-times',
|
|
182
|
+
'telemetry-sector-times',
|
|
183
|
+
'telemetry-sector-banner',
|
|
184
|
+
'steward-message',
|
|
185
|
+
'driver-rows',
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const COMPONENT_THEME_SELECTOR_ALIASES = {
|
|
189
|
+
selectedDriverPanel: ['car-driver-overview', 'race-data-panel'],
|
|
190
|
+
carDriverOverview: ['car-driver-overview'],
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const DEFAULT_DARK_THEME_TOKENS = {
|
|
194
|
+
primary: '#e10600',
|
|
195
|
+
primaryText: '#ffffff',
|
|
196
|
+
secondary: '#151923',
|
|
197
|
+
secondaryText: '#f4f7fb',
|
|
198
|
+
surface: '#08090b',
|
|
199
|
+
surfaceRaised: '#111318',
|
|
200
|
+
surfacePanel: '#171a20',
|
|
201
|
+
text: '#f4f7fb',
|
|
202
|
+
mutedText: '#8d97a7',
|
|
203
|
+
border: 'rgba(255, 255, 255, 0.13)',
|
|
204
|
+
success: '#14c784',
|
|
205
|
+
warning: '#ffd166',
|
|
206
|
+
danger: '#e10600',
|
|
207
|
+
info: '#39a7ff',
|
|
208
|
+
yellowFlag: '#ffd166',
|
|
209
|
+
greenFlag: '#14c784',
|
|
210
|
+
redFlag: '#e10600',
|
|
211
|
+
safetyCar: '#f5c542',
|
|
212
|
+
drsActive: '#8b5cf6',
|
|
213
|
+
pitLane: '#2f3540',
|
|
214
|
+
track: '#262a31',
|
|
215
|
+
trackEdge: '#f4f7fb',
|
|
216
|
+
timingTowerMaxWidth: '390px',
|
|
217
|
+
raceViewMinHeight: '620px',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const DEFAULT_LIGHT_THEME_TOKENS = {
|
|
221
|
+
primary: '#c90400',
|
|
222
|
+
primaryText: '#ffffff',
|
|
223
|
+
secondary: '#eef2f7',
|
|
224
|
+
secondaryText: '#111827',
|
|
225
|
+
surface: '#f6f8fb',
|
|
226
|
+
surfaceRaised: '#ffffff',
|
|
227
|
+
surfacePanel: '#eef2f7',
|
|
228
|
+
text: '#111827',
|
|
229
|
+
mutedText: '#5d6675',
|
|
230
|
+
border: 'rgba(17, 24, 39, 0.16)',
|
|
231
|
+
success: '#0f9f6a',
|
|
232
|
+
warning: '#b97800',
|
|
233
|
+
danger: '#c90400',
|
|
234
|
+
info: '#1e74c9',
|
|
235
|
+
yellowFlag: '#b97800',
|
|
236
|
+
greenFlag: '#0f9f6a',
|
|
237
|
+
redFlag: '#c90400',
|
|
238
|
+
safetyCar: '#b97800',
|
|
239
|
+
drsActive: '#6d28d9',
|
|
240
|
+
pitLane: '#d8dee8',
|
|
241
|
+
track: '#d7dde6',
|
|
242
|
+
trackEdge: '#111827',
|
|
243
|
+
timingTowerMaxWidth: '390px',
|
|
244
|
+
raceViewMinHeight: '620px',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const DEFAULT_PADDOCK_THEME_INPUT = {
|
|
248
|
+
mode: 'dark',
|
|
249
|
+
use: 'default',
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const DEFAULT_THEME_PACKAGE = {
|
|
253
|
+
tokens: {
|
|
254
|
+
light: DEFAULT_LIGHT_THEME_TOKENS,
|
|
255
|
+
dark: DEFAULT_DARK_THEME_TOKENS,
|
|
256
|
+
},
|
|
257
|
+
components: DEFAULT_COMPONENT_SLOTS,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export function mergeThemeInputs(...inputs) {
|
|
261
|
+
return inputs.reduce((merged, input) => mergeThemeInputInto(merged, input), {});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function normalizePaddockTheme(input = {}) {
|
|
265
|
+
const mode = THEME_MODES.has(input?.mode) ? input.mode : 'dark';
|
|
266
|
+
const activeMode = resolveActiveThemeMode(mode);
|
|
267
|
+
const cacheKey = stableStringify({ input, activeMode });
|
|
268
|
+
if (RESOLVED_THEME_CACHE.has(cacheKey)) return RESOLVED_THEME_CACHE.get(cacheKey);
|
|
269
|
+
|
|
270
|
+
const themeInputs = isPlainObject(input?.themes) ? input.themes : {};
|
|
271
|
+
const resolving = new Set();
|
|
272
|
+
const resolvedThemes = {};
|
|
273
|
+
|
|
274
|
+
const resolveNamedTheme = (name) => {
|
|
275
|
+
const themeName = typeof name === 'string' && name !== '' ? name : 'default';
|
|
276
|
+
if (resolvedThemes[themeName]) return resolvedThemes[themeName];
|
|
277
|
+
if (resolving.has(themeName)) return resolvedThemes.default ?? DEFAULT_THEME_PACKAGE;
|
|
278
|
+
resolving.add(themeName);
|
|
279
|
+
|
|
280
|
+
const rawPackage = themeName === 'default'
|
|
281
|
+
? mergeThemeInputInto(themeInputs.default ?? {}, pickTopLevelThemeInput(input))
|
|
282
|
+
: themeInputs[themeName];
|
|
283
|
+
const parentName = themeName === 'default'
|
|
284
|
+
? null
|
|
285
|
+
: typeof rawPackage?.extends === 'string' && rawPackage.extends !== themeName
|
|
286
|
+
? rawPackage.extends
|
|
287
|
+
: 'default';
|
|
288
|
+
const parent = parentName ? resolveNamedTheme(parentName) : DEFAULT_THEME_PACKAGE;
|
|
289
|
+
const resolved = resolveThemePackage(rawPackage, parent);
|
|
290
|
+
resolvedThemes[themeName] = resolved;
|
|
291
|
+
resolving.delete(themeName);
|
|
292
|
+
return resolved;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
resolveNamedTheme('default');
|
|
296
|
+
Object.keys(themeInputs).forEach((name) => {
|
|
297
|
+
if (isPlainObject(themeInputs[name])) resolveNamedTheme(name);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const requestedUse = typeof input?.use === 'string' ? input.use : 'default';
|
|
301
|
+
const use = Object.hasOwn(resolvedThemes, requestedUse) ? requestedUse : 'default';
|
|
302
|
+
const activeTheme = resolvedThemes[use] ?? resolvedThemes.default;
|
|
303
|
+
const activeTokens = activeTheme.tokens[activeMode];
|
|
304
|
+
const componentThemes = {
|
|
305
|
+
...DEFAULT_COMPONENT_THEME_SELECTORS,
|
|
306
|
+
...normalizeComponentThemeSelectors(
|
|
307
|
+
input?.componentThemes ?? getLegacyComponentThemeMap(input?.components),
|
|
308
|
+
resolvedThemes,
|
|
309
|
+
),
|
|
310
|
+
};
|
|
311
|
+
const teamThemes = normalizeThemeSelectors(input?.teamThemes, resolvedThemes);
|
|
312
|
+
const result = {
|
|
313
|
+
mode,
|
|
314
|
+
activeMode,
|
|
315
|
+
use,
|
|
316
|
+
tokens: activeTheme.tokens,
|
|
317
|
+
activeTokens,
|
|
318
|
+
components: activeTheme.components,
|
|
319
|
+
themes: resolvedThemes,
|
|
320
|
+
componentThemes,
|
|
321
|
+
teamThemes,
|
|
322
|
+
light: toLegacyThemeTokens(activeTheme.tokens.light),
|
|
323
|
+
dark: toLegacyThemeTokens(activeTheme.tokens.dark),
|
|
324
|
+
...toLegacyThemeTokens(activeTokens),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
RESOLVED_THEME_CACHE.set(cacheKey, result);
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function applyPaddockThemeCssVariables(root, theme = DEFAULT_PADDOCK_THEME_INPUT, context = {}) {
|
|
332
|
+
const resolved = isResolvedTheme(theme) ? theme : normalizePaddockTheme(theme);
|
|
333
|
+
const activeMode = resolveActiveThemeMode(resolved.mode);
|
|
334
|
+
root?.setAttribute?.('data-paddock-theme-mode', activeMode);
|
|
335
|
+
applyResolvedThemeCssVariables(root, resolved.themes?.[resolved.use] ?? resolved, activeMode);
|
|
336
|
+
|
|
337
|
+
getComponentElements(root).forEach((element) => {
|
|
338
|
+
const componentName = element.getAttribute?.('data-paddock-component');
|
|
339
|
+
const themeName = resolved.componentThemes?.[componentName];
|
|
340
|
+
const selectedTheme = selectThemePackage(themeName, resolved, context);
|
|
341
|
+
if (!selectedTheme) {
|
|
342
|
+
if (element !== root) clearPaddockThemeCssVariables(element);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
applyResolvedThemeCssVariables(element, selectedTheme, activeMode);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function normalizeCssColorToken(value, fallback = null) {
|
|
350
|
+
if (typeof value !== 'string') return fallback;
|
|
351
|
+
const candidate = value.trim();
|
|
352
|
+
if (
|
|
353
|
+
candidate === '' ||
|
|
354
|
+
/[\u0000-\u001F\u007F;]/.test(candidate) ||
|
|
355
|
+
/[{};]/.test(candidate) ||
|
|
356
|
+
/\b(?:url|expression)\s*\(/i.test(candidate)
|
|
357
|
+
) return fallback;
|
|
358
|
+
|
|
359
|
+
if (/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(candidate)) return normalizeHexColor(candidate);
|
|
360
|
+
if (/^(?:rgb|rgba|hsl|hsla)\([0-9\s.,%/+*-]+\)$/i.test(candidate)) return candidate;
|
|
361
|
+
if (/^(?:transparent|currentcolor|black|white|red|green|blue|yellow|orange|purple|pink|gray|grey)$/i.test(candidate)) {
|
|
362
|
+
return candidate.toLowerCase();
|
|
363
|
+
}
|
|
364
|
+
return fallback;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function deriveDarkColor(lightColor) {
|
|
368
|
+
return deriveModeColor('dark', 'primary', lightColor);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function deriveLightColor(darkColor) {
|
|
372
|
+
return deriveModeColor('light', 'primary', darkColor);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function resolveModePair(value, fallbackPair = {
|
|
376
|
+
light: DEFAULT_LIGHT_THEME_TOKENS.primary,
|
|
377
|
+
dark: DEFAULT_DARK_THEME_TOKENS.primary,
|
|
378
|
+
}) {
|
|
379
|
+
return resolveColorModePair('primary', value, fallbackPair);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function mergeThemeInputInto(merged, input) {
|
|
383
|
+
if (!isPlainObject(input)) return merged;
|
|
384
|
+
const next = { ...merged };
|
|
385
|
+
if (THEME_MODES.has(input.mode)) next.mode = input.mode;
|
|
386
|
+
if (typeof input.use === 'string') next.use = input.use;
|
|
387
|
+
if (typeof input.extends === 'string') next.extends = input.extends;
|
|
388
|
+
|
|
389
|
+
const tokenInput = extractTokenInput(input);
|
|
390
|
+
if (Object.keys(tokenInput).length > 0) {
|
|
391
|
+
next.tokens = { ...(next.tokens ?? {}), ...tokenInput };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (isPlainObject(input.themes)) {
|
|
395
|
+
next.themes = { ...(next.themes ?? {}) };
|
|
396
|
+
Object.entries(input.themes).forEach(([name, themePackage]) => {
|
|
397
|
+
if (!isPlainObject(themePackage)) return;
|
|
398
|
+
next.themes[name] = mergeThemeInputInto(next.themes[name] ?? {}, themePackage);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (isPlainObject(input.components)) {
|
|
402
|
+
const legacyComponentThemes = getLegacyComponentThemeMap(input.components);
|
|
403
|
+
if (legacyComponentThemes) next.componentThemes = { ...(next.componentThemes ?? {}), ...legacyComponentThemes };
|
|
404
|
+
const componentSlots = getComponentSlotOverrides(input.components);
|
|
405
|
+
if (Object.keys(componentSlots).length > 0) {
|
|
406
|
+
next.components = mergeComponentSlots(next.components ?? {}, componentSlots);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (isPlainObject(input.componentThemes)) {
|
|
410
|
+
next.componentThemes = { ...(next.componentThemes ?? {}), ...input.componentThemes };
|
|
411
|
+
}
|
|
412
|
+
if (isPlainObject(input.teamThemes)) {
|
|
413
|
+
next.teamThemes = { ...(next.teamThemes ?? {}), ...input.teamThemes };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return next;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function pickTopLevelThemeInput(input) {
|
|
420
|
+
if (!isPlainObject(input)) return {};
|
|
421
|
+
const result = {};
|
|
422
|
+
['tokens', 'components', 'componentThemes', 'teamThemes', 'light', 'dark'].forEach((key) => {
|
|
423
|
+
if (Object.hasOwn(input, key)) result[key] = input[key];
|
|
424
|
+
});
|
|
425
|
+
[...THEME_TOKEN_KEYS, ...LEGACY_ALIAS_KEYS].forEach((key) => {
|
|
426
|
+
if (Object.hasOwn(input, key)) result[key] = input[key];
|
|
427
|
+
});
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function resolveThemePackage(input, parent = DEFAULT_THEME_PACKAGE) {
|
|
432
|
+
const tokenInput = extractTokenInput(input);
|
|
433
|
+
const tokens = {
|
|
434
|
+
light: {},
|
|
435
|
+
dark: {},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
THEME_COLOR_TOKEN_KEYS.forEach((key) => {
|
|
439
|
+
const fallbackPair = {
|
|
440
|
+
light: parent.tokens.light[key],
|
|
441
|
+
dark: parent.tokens.dark[key],
|
|
442
|
+
};
|
|
443
|
+
const pair = Object.hasOwn(tokenInput, key)
|
|
444
|
+
? resolveColorModePair(key, tokenInput[key], fallbackPair)
|
|
445
|
+
: fallbackPair;
|
|
446
|
+
tokens.light[key] = pair.light;
|
|
447
|
+
tokens.dark[key] = pair.dark;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
THEME_SIZE_TOKEN_KEYS.forEach((key) => {
|
|
451
|
+
const fallbackPair = {
|
|
452
|
+
light: parent.tokens.light[key],
|
|
453
|
+
dark: parent.tokens.dark[key],
|
|
454
|
+
};
|
|
455
|
+
const pair = Object.hasOwn(tokenInput, key)
|
|
456
|
+
? resolveSizeModePair(tokenInput[key], fallbackPair)
|
|
457
|
+
: fallbackPair;
|
|
458
|
+
tokens.light[key] = pair.light;
|
|
459
|
+
tokens.dark[key] = pair.dark;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
tokens,
|
|
464
|
+
components: mergeComponentSlots(parent.components, getComponentSlotOverrides(input?.components)),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function extractTokenInput(input) {
|
|
469
|
+
if (!isPlainObject(input)) return {};
|
|
470
|
+
const result = {};
|
|
471
|
+
if (isPlainObject(input.light) || isPlainObject(input.dark)) {
|
|
472
|
+
Object.assign(result, extractModeTokenInput(input.light, input.dark));
|
|
473
|
+
}
|
|
474
|
+
const hasResolvedActiveTokens = isPlainObject(input.activeTokens);
|
|
475
|
+
if (!hasResolvedActiveTokens) {
|
|
476
|
+
THEME_TOKEN_KEYS.forEach((key) => {
|
|
477
|
+
if (Object.hasOwn(input, key)) result[key] = input[key];
|
|
478
|
+
});
|
|
479
|
+
LEGACY_ALIAS_KEYS.forEach((key) => {
|
|
480
|
+
if (Object.hasOwn(input, key)) result[LEGACY_TOKEN_ALIASES[key]] = input[key];
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (isPlainObject(input.tokens)) {
|
|
484
|
+
const modeTokens = extractModeTokenInput(input.tokens.light, input.tokens.dark);
|
|
485
|
+
Object.assign(result, modeTokens);
|
|
486
|
+
Object.entries(input.tokens).forEach(([key, value]) => {
|
|
487
|
+
if (key === 'light' || key === 'dark') return;
|
|
488
|
+
const tokenKey = LEGACY_TOKEN_ALIASES[key] ?? key;
|
|
489
|
+
if (THEME_TOKEN_SET.has(tokenKey)) result[tokenKey] = value;
|
|
490
|
+
else warnUnknownThemeKey(key);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function extractModeTokenInput(lightInput, darkInput) {
|
|
497
|
+
const result = {};
|
|
498
|
+
addModeTokens(result, 'light', lightInput);
|
|
499
|
+
addModeTokens(result, 'dark', darkInput);
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function addModeTokens(result, mode, input) {
|
|
504
|
+
if (!isPlainObject(input)) return;
|
|
505
|
+
Object.entries(input).forEach(([key, value]) => {
|
|
506
|
+
const tokenKey = LEGACY_TOKEN_ALIASES[key] ?? key;
|
|
507
|
+
if (!THEME_TOKEN_SET.has(tokenKey)) {
|
|
508
|
+
warnUnknownThemeKey(key);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const current = isPlainObject(result[tokenKey]) ? result[tokenKey] : {};
|
|
512
|
+
result[tokenKey] = { ...current, [mode]: value };
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function resolveColorModePair(key, value, fallbackPair) {
|
|
517
|
+
const pair = isPlainObject(value) ? value : { light: value, dark: value };
|
|
518
|
+
const light = normalizeCssColorToken(pair.light, null);
|
|
519
|
+
const dark = normalizeCssColorToken(pair.dark, null);
|
|
520
|
+
if (light && dark) return { light, dark };
|
|
521
|
+
if (light) return {
|
|
522
|
+
light,
|
|
523
|
+
dark: deriveModeColor('dark', key, light) ?? fallbackPair.dark,
|
|
524
|
+
};
|
|
525
|
+
if (dark) return {
|
|
526
|
+
light: deriveModeColor('light', key, dark) ?? fallbackPair.light,
|
|
527
|
+
dark,
|
|
528
|
+
};
|
|
529
|
+
return fallbackPair;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function resolveSizeModePair(value, fallbackPair) {
|
|
533
|
+
const pair = isPlainObject(value) ? value : { light: value, dark: value };
|
|
534
|
+
const light = normalizeCssSizeToken(pair.light, null);
|
|
535
|
+
const dark = normalizeCssSizeToken(pair.dark, null);
|
|
536
|
+
if (light && dark) return { light, dark };
|
|
537
|
+
if (light) return { light, dark: light };
|
|
538
|
+
if (dark) return { light: dark, dark };
|
|
539
|
+
return fallbackPair;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function getLegacyComponentThemeMap(components) {
|
|
543
|
+
if (!isPlainObject(components)) return null;
|
|
544
|
+
const entries = Object.entries(components)
|
|
545
|
+
.map(([componentName, value]) => [
|
|
546
|
+
componentName,
|
|
547
|
+
typeof value === 'string' ? value : isPlainObject(value) && typeof value.theme === 'string' ? value.theme : null,
|
|
548
|
+
])
|
|
549
|
+
.filter(([, selector]) => selector);
|
|
550
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function getComponentSlotOverrides(components) {
|
|
554
|
+
if (!isPlainObject(components)) return {};
|
|
555
|
+
const result = {};
|
|
556
|
+
Object.entries(components).forEach(([componentName, slots]) => {
|
|
557
|
+
const canonicalName = normalizeComponentSlotName(componentName);
|
|
558
|
+
const schema = DEFAULT_COMPONENT_SLOTS[canonicalName];
|
|
559
|
+
if (!schema || !isPlainObject(slots)) return;
|
|
560
|
+
Object.entries(slots).forEach(([slot, tokenName]) => {
|
|
561
|
+
if (slot === 'theme') return;
|
|
562
|
+
if (!Object.hasOwn(schema, slot) || !THEME_TOKEN_SET.has(tokenName)) {
|
|
563
|
+
warnUnknownThemeKey(`${componentName}.${slot}`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
result[canonicalName] = {
|
|
567
|
+
...(result[canonicalName] ?? {}),
|
|
568
|
+
[slot]: tokenName,
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function mergeComponentSlots(parent = {}, overrides = {}) {
|
|
576
|
+
const result = {};
|
|
577
|
+
Object.entries(parent).forEach(([componentName, slots]) => {
|
|
578
|
+
result[componentName] = { ...slots };
|
|
579
|
+
});
|
|
580
|
+
Object.entries(overrides).forEach(([componentName, slots]) => {
|
|
581
|
+
if (!Object.hasOwn(DEFAULT_COMPONENT_SLOTS, componentName)) return;
|
|
582
|
+
result[componentName] = {
|
|
583
|
+
...(result[componentName] ?? DEFAULT_COMPONENT_SLOTS[componentName]),
|
|
584
|
+
...slots,
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function normalizeThemeSelectors(selectors, themes) {
|
|
591
|
+
if (!isPlainObject(selectors)) return {};
|
|
592
|
+
return Object.fromEntries(
|
|
593
|
+
Object.entries(selectors)
|
|
594
|
+
.map(([key, selector]) => [
|
|
595
|
+
key,
|
|
596
|
+
typeof selector === 'string' ? selector : isPlainObject(selector) ? selector.theme : null,
|
|
597
|
+
])
|
|
598
|
+
.filter(([, selector]) => isValidThemeSelector(selector, themes)),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function normalizeComponentThemeSelectors(selectors, themes) {
|
|
603
|
+
if (!isPlainObject(selectors)) return {};
|
|
604
|
+
const result = {};
|
|
605
|
+
Object.entries(selectors).forEach(([key, selectorInput]) => {
|
|
606
|
+
const selector = typeof selectorInput === 'string'
|
|
607
|
+
? selectorInput
|
|
608
|
+
: isPlainObject(selectorInput)
|
|
609
|
+
? selectorInput.theme
|
|
610
|
+
: null;
|
|
611
|
+
if (!isValidThemeSelector(selector, themes)) return;
|
|
612
|
+
normalizeComponentThemeSelectorKeys(key).forEach((componentKey) => {
|
|
613
|
+
if (PACKAGE_COMPONENT_THEME_KEYS.has(componentKey)) result[componentKey] = selector;
|
|
614
|
+
else warnUnknownThemeKey(key);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function normalizeComponentThemeSelectorKeys(key) {
|
|
621
|
+
const raw = String(key);
|
|
622
|
+
if (COMPONENT_THEME_SELECTOR_ALIASES[raw]) return COMPONENT_THEME_SELECTOR_ALIASES[raw];
|
|
623
|
+
return [raw.includes('-') ? raw : toKebabCase(raw)];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isValidThemeSelector(selector, themes) {
|
|
627
|
+
if (typeof selector !== 'string' || selector === '') return false;
|
|
628
|
+
if (selector === 'default' || selector === 'active' || selector === 'selectedTeam' || selector === 'team') return true;
|
|
629
|
+
if (selector.startsWith('team:')) return selector.length > 5;
|
|
630
|
+
return Object.hasOwn(themes, selector);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function selectThemePackage(selector, resolved, context = {}, visitedTeamIds = new Set()) {
|
|
634
|
+
if (!selector || selector === 'active') return null;
|
|
635
|
+
if (selector === 'default') return resolved.themes?.default;
|
|
636
|
+
if (selector === 'team' || selector === 'selectedTeam' || selector.startsWith('team:')) {
|
|
637
|
+
const teamId = selector.startsWith('team:')
|
|
638
|
+
? selector.slice(5)
|
|
639
|
+
: selector === 'selectedTeam'
|
|
640
|
+
? context.selectedTeamId
|
|
641
|
+
: context.teamId ?? context.selectedTeamId;
|
|
642
|
+
const teamTheme = selectTeamThemePackage(teamId, resolved, context, visitedTeamIds);
|
|
643
|
+
return teamTheme ?? resolved.themes?.[resolved.use] ?? resolved.themes?.default;
|
|
644
|
+
}
|
|
645
|
+
return resolved.themes?.[selector] ?? null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function selectTeamThemePackage(teamId, resolved, context, visitedTeamIds) {
|
|
649
|
+
if (typeof teamId !== 'string' || teamId === '' || visitedTeamIds.has(teamId)) return null;
|
|
650
|
+
const selector = resolved.teamThemes?.[teamId];
|
|
651
|
+
if (!selector) return null;
|
|
652
|
+
visitedTeamIds.add(teamId);
|
|
653
|
+
if (selector === 'team' || selector === 'selectedTeam') return null;
|
|
654
|
+
return selectThemePackage(selector, resolved, context, visitedTeamIds);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function applyResolvedThemeCssVariables(target, themePackage, activeMode) {
|
|
658
|
+
clearPaddockThemeCssVariables(target);
|
|
659
|
+
const tokens = themePackage?.tokens?.[activeMode] ?? themePackage?.activeTokens ?? {};
|
|
660
|
+
applyTokenCssVariables(target, tokens);
|
|
661
|
+
applyComponentSlotCssVariables(target, tokens, themePackage?.components);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function clearPaddockThemeCssVariables(target) {
|
|
665
|
+
const removeProperty = target?.style?.removeProperty;
|
|
666
|
+
if (typeof removeProperty !== 'function') return;
|
|
667
|
+
PADDOCK_THEME_SCOPED_CSS_VARIABLES.forEach((variable) => {
|
|
668
|
+
removeProperty.call(target.style, variable);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function applyTokenCssVariables(target, tokens = {}) {
|
|
673
|
+
Object.entries(PADDOCK_THEME_CSS_VARIABLES).forEach(([key, variable]) => {
|
|
674
|
+
const value = tokens[key];
|
|
675
|
+
if (value == null || value === '') return;
|
|
676
|
+
target?.style?.setProperty?.(variable, String(value));
|
|
677
|
+
});
|
|
678
|
+
Object.entries(LEGACY_THEME_CSS_VARIABLES).forEach(([legacyKey, variable]) => {
|
|
679
|
+
const tokenKey = LEGACY_TOKEN_ALIASES[legacyKey];
|
|
680
|
+
const value = tokens[tokenKey];
|
|
681
|
+
if (value == null || value === '') return;
|
|
682
|
+
target?.style?.setProperty?.(variable, String(value));
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function applyComponentSlotCssVariables(target, tokens = {}, components = {}) {
|
|
687
|
+
Object.entries(components).forEach(([componentName, slots]) => {
|
|
688
|
+
Object.entries(slots).forEach(([slot, tokenName]) => {
|
|
689
|
+
const value = tokens[tokenName];
|
|
690
|
+
if (value == null || value === '') return;
|
|
691
|
+
target?.style?.setProperty?.(`--paddock-${toKebabCase(componentName)}-${toKebabCase(slot)}`, String(value));
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function toLegacyThemeTokens(tokens = {}) {
|
|
697
|
+
const aliases = {};
|
|
698
|
+
Object.entries(LEGACY_TOKEN_ALIASES).forEach(([legacyKey, tokenKey]) => {
|
|
699
|
+
aliases[legacyKey] = tokens[tokenKey];
|
|
700
|
+
});
|
|
701
|
+
return { ...tokens, ...aliases };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function getComponentElements(root) {
|
|
705
|
+
const elements = [];
|
|
706
|
+
if (root?.matches?.('[data-paddock-component]')) elements.push(root);
|
|
707
|
+
root?.querySelectorAll?.('[data-paddock-component]')?.forEach?.((element) => elements.push(element));
|
|
708
|
+
return elements;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function normalizeComponentSlotName(name) {
|
|
712
|
+
return String(name).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function normalizeCssSizeToken(value, fallback = null) {
|
|
716
|
+
if (typeof value !== 'string' && typeof value !== 'number') return fallback;
|
|
717
|
+
const candidate = String(value).trim();
|
|
718
|
+
if (
|
|
719
|
+
candidate === '' ||
|
|
720
|
+
/[\u0000-\u001F\u007F;]/.test(candidate) ||
|
|
721
|
+
/[{};]/.test(candidate) ||
|
|
722
|
+
/\b(?:url|expression)\s*\(/i.test(candidate)
|
|
723
|
+
) return fallback;
|
|
724
|
+
return candidate;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function deriveModeColor(mode, key, value) {
|
|
728
|
+
const cacheKey = `${mode}:${key}:${value}`;
|
|
729
|
+
if (GENERATED_COLOR_CACHE.has(cacheKey)) return GENERATED_COLOR_CACHE.get(cacheKey);
|
|
730
|
+
|
|
731
|
+
const rgb = parseHexColor(value);
|
|
732
|
+
const generated = rgb ? rgbToHex(adjustRgbForMode(rgb, mode)) : value;
|
|
733
|
+
GENERATED_COLOR_CACHE.set(cacheKey, generated);
|
|
734
|
+
return generated;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function parseHexColor(value) {
|
|
738
|
+
const normalized = normalizeCssColorToken(value, null);
|
|
739
|
+
if (!normalized?.startsWith?.('#')) return null;
|
|
740
|
+
const hex = normalized.slice(1);
|
|
741
|
+
const expanded = hex.length === 3
|
|
742
|
+
? hex.split('').map((char) => `${char}${char}`).join('')
|
|
743
|
+
: hex;
|
|
744
|
+
return {
|
|
745
|
+
r: Number.parseInt(expanded.slice(0, 2), 16),
|
|
746
|
+
g: Number.parseInt(expanded.slice(2, 4), 16),
|
|
747
|
+
b: Number.parseInt(expanded.slice(4, 6), 16),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function normalizeHexColor(value) {
|
|
752
|
+
const hex = value.slice(1);
|
|
753
|
+
if (hex.length === 3) {
|
|
754
|
+
return `#${hex.split('').map((char) => `${char}${char}`).join('')}`.toLowerCase();
|
|
755
|
+
}
|
|
756
|
+
return value.toLowerCase();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function adjustRgbForMode(rgb, mode) {
|
|
760
|
+
const amount = mode === 'light' ? 0.18 : -0.18;
|
|
761
|
+
return {
|
|
762
|
+
r: adjustChannel(rgb.r, amount),
|
|
763
|
+
g: adjustChannel(rgb.g, amount),
|
|
764
|
+
b: adjustChannel(rgb.b, amount),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function adjustChannel(channel, amount) {
|
|
769
|
+
const target = amount > 0 ? 255 : 0;
|
|
770
|
+
return Math.round(channel + (target - channel) * Math.abs(amount));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function rgbToHex({ r, g, b }) {
|
|
774
|
+
return `#${[r, g, b].map((channel) => clampColorChannel(channel).toString(16).padStart(2, '0')).join('')}`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function clampColorChannel(value) {
|
|
778
|
+
return Math.max(0, Math.min(255, Math.round(value)));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function resolveActiveThemeMode(mode) {
|
|
782
|
+
if (ACTIVE_THEME_MODES.has(mode)) return mode;
|
|
783
|
+
if (globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches) return 'light';
|
|
784
|
+
return 'dark';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function isResolvedTheme(theme) {
|
|
788
|
+
return isPlainObject(theme) && isPlainObject(theme.tokens) && isPlainObject(theme.themes) && isPlainObject(theme.componentThemes);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function stableStringify(value) {
|
|
792
|
+
return JSON.stringify(sortForStableStringify(value, new WeakSet()));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function sortForStableStringify(value, seen) {
|
|
796
|
+
if (typeof value === 'bigint') return `bigint:${value.toString()}`;
|
|
797
|
+
if (typeof value === 'function') return `function:${value.name ?? ''}`;
|
|
798
|
+
if (typeof value === 'symbol') return value.toString();
|
|
799
|
+
if (value === undefined) return 'undefined';
|
|
800
|
+
if (Array.isArray(value)) return value.map((item) => sortForStableStringify(item, seen));
|
|
801
|
+
if (!isPlainObject(value)) return value;
|
|
802
|
+
if (seen.has(value)) return '[Circular]';
|
|
803
|
+
seen.add(value);
|
|
804
|
+
return Object.fromEntries(
|
|
805
|
+
Object.keys(value)
|
|
806
|
+
.sort()
|
|
807
|
+
.map((key) => [key, sortForStableStringify(value[key], seen)]),
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function toKebabCase(value) {
|
|
812
|
+
return String(value)
|
|
813
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
814
|
+
.replace(/_/g, '-')
|
|
815
|
+
.toLowerCase();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function warnUnknownThemeKey(key) {
|
|
819
|
+
if (globalThis.process?.env?.NODE_ENV === 'production') return;
|
|
820
|
+
globalThis.console?.warn?.(`[PaddockJS] Ignoring unknown theme token or slot "${key}".`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function isPlainObject(value) {
|
|
824
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
825
|
+
}
|