@multiplatform.one/theme 6.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 +18 -0
- package/LICENSE +201 -0
- package/package.json +85 -0
- package/src/audit/baselines/home-login.json +171 -0
- package/src/audit/baselines/home-login.png +0 -0
- package/src/audit/baselines/home-pokemon.json +355 -0
- package/src/audit/baselines/home-pokemon.png +0 -0
- package/src/audit/baselines/home.json +194 -0
- package/src/audit/baselines/home.png +0 -0
- package/src/audit/baselines/pokemon.json +251 -0
- package/src/audit/baselines/pokemon.png +0 -0
- package/src/audit/constraintAudit.ts +874 -0
- package/src/audit/diffReport.spec.ts +191 -0
- package/src/audit/diffReport.ts +344 -0
- package/src/audit/index.ts +10 -0
- package/src/font.ts +49 -0
- package/src/fonts/createFontLoader.ts +15 -0
- package/src/fonts/createFontLoader.web.ts +9 -0
- package/src/fonts/index.ts +2 -0
- package/src/fonts/types.ts +13 -0
- package/src/index.ts +3 -0
- package/src/theme/Intent.tsx +30 -0
- package/src/theme/Preset.tsx +93 -0
- package/src/theme/PresetContext.ts +27 -0
- package/src/theme/Tint.tsx +44 -0
- package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +69 -0
- package/src/theme/animations/css.ts +25 -0
- package/src/theme/animations/index.ts +1 -0
- package/src/theme/animations/index.web.ts +1 -0
- package/src/theme/animations/reactNative.ts +54 -0
- package/src/theme/colorRules.spec.ts +195 -0
- package/src/theme/colorRules.ts +166 -0
- package/src/theme/cookies.spec.ts +185 -0
- package/src/theme/cookies.ts +209 -0
- package/src/theme/createDefaultThemeConfig.ts +86 -0
- package/src/theme/createThemes.ts +286 -0
- package/src/theme/devtools/ColorLineVisualizer.tsx +261 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +1657 -0
- package/src/theme/devtools/buildSnapshot.spec.ts +113 -0
- package/src/theme/devtools/buildSnapshot.ts +103 -0
- package/src/theme/devtools/knobIcons.tsx +217 -0
- package/src/theme/devtools/vizConstants.spec.ts +58 -0
- package/src/theme/devtools/vizConstants.ts +28 -0
- package/src/theme/index.ts +21 -0
- package/src/theme/intent.spec.tsx +276 -0
- package/src/theme/intents.ts +22 -0
- package/src/theme/knobs.ts +147 -0
- package/src/theme/pokemonScreen.spec.ts +109 -0
- package/src/theme/preset.spec.tsx +242 -0
- package/src/theme/preset.types.ts +22 -0
- package/src/theme/presets.spec.ts +252 -0
- package/src/theme/presets.ts +49 -0
- package/src/theme/recipes.ts +69 -0
- package/src/theme/resolveKnobs.spec.ts +571 -0
- package/src/theme/resolveKnobs.ts +226 -0
- package/src/theme/shared.spec.ts +84 -0
- package/src/theme/shared.tsx +131 -0
- package/src/theme/theme.native.tsx +65 -0
- package/src/theme/theme.tsx +190 -0
- package/src/theme/tint.spec.tsx +179 -0
- package/src/theme/useColorScale.native.ts +7 -0
- package/src/theme/useColorScale.ts +29 -0
- package/src/theme/useResolvedKnobs.ts +121 -0
- package/src/theme/useTheme.native.ts +49 -0
- package/src/theme/useTheme.ts +213 -0
- package/tsconfig.json +11 -0
- package/vitest.config.mjs +9 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constraint Audit System
|
|
3
|
+
*
|
|
4
|
+
* Audits the rendered DOM to verify all structural CSS values land on valid
|
|
5
|
+
* Tamagui token steps defined by the knob system. Off-scale values indicate
|
|
6
|
+
* styles that bypassed the constraint system.
|
|
7
|
+
*
|
|
8
|
+
* Token values are from @tamagui/themes v5 + resolveKnobs.ts mappings.
|
|
9
|
+
* Run this in-browser via Playwright's page.evaluate(runConstraintAudit).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface PropertyAudit {
|
|
15
|
+
value: number;
|
|
16
|
+
unit: string;
|
|
17
|
+
knob: string;
|
|
18
|
+
offScale: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TextPropertyAudit {
|
|
22
|
+
value: string;
|
|
23
|
+
knob: string;
|
|
24
|
+
offScale: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ElementAudit {
|
|
28
|
+
/** Human-readable element descriptor */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Tag name */
|
|
31
|
+
tag: string;
|
|
32
|
+
/** id attribute if present */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Non-hash class names (for readability) */
|
|
35
|
+
classes: string[];
|
|
36
|
+
/** Audited properties (only populated when value is non-default/non-zero) */
|
|
37
|
+
borderRadius?: PropertyAudit;
|
|
38
|
+
borderWidth?: PropertyAudit;
|
|
39
|
+
gap?: PropertyAudit;
|
|
40
|
+
rowGap?: PropertyAudit;
|
|
41
|
+
columnGap?: PropertyAudit;
|
|
42
|
+
paddingTop?: PropertyAudit;
|
|
43
|
+
paddingRight?: PropertyAudit;
|
|
44
|
+
paddingBottom?: PropertyAudit;
|
|
45
|
+
paddingLeft?: PropertyAudit;
|
|
46
|
+
fontWeight?: PropertyAudit;
|
|
47
|
+
fontFamily?: TextPropertyAudit;
|
|
48
|
+
/** Inline color bypasses (hardcoded color/backgroundColor not from theme) */
|
|
49
|
+
inlineColor?: TextPropertyAudit;
|
|
50
|
+
/** Violations found on this element */
|
|
51
|
+
violations: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AuditSummary {
|
|
55
|
+
totalElements: number;
|
|
56
|
+
auditedElements: number;
|
|
57
|
+
totalViolations: number;
|
|
58
|
+
byProperty: {
|
|
59
|
+
borderRadius: number;
|
|
60
|
+
borderWidth: number;
|
|
61
|
+
gap: number;
|
|
62
|
+
padding: number;
|
|
63
|
+
fontWeight: number;
|
|
64
|
+
fontFamily: number;
|
|
65
|
+
inlineColor: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AuditReport {
|
|
70
|
+
url: string;
|
|
71
|
+
timestamp: string;
|
|
72
|
+
preset: string;
|
|
73
|
+
scheme: string;
|
|
74
|
+
summary: AuditSummary;
|
|
75
|
+
violations: ElementAudit[];
|
|
76
|
+
/** All audited elements (including compliant ones) */
|
|
77
|
+
elements: ElementAudit[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Token scale definitions ───────────────────────────────────────────────────
|
|
81
|
+
// These match @tamagui/themes v5 tokens + resolveKnobs.ts mapping tables.
|
|
82
|
+
|
|
83
|
+
/** Radius token values: token key → px (from @tamagui/themes v5 radius tokens) */
|
|
84
|
+
const RADIUS_TOKENS: Record<number, number> = {
|
|
85
|
+
0: 0,
|
|
86
|
+
1: 3,
|
|
87
|
+
2: 5,
|
|
88
|
+
3: 7,
|
|
89
|
+
4: 9,
|
|
90
|
+
5: 10,
|
|
91
|
+
6: 16,
|
|
92
|
+
7: 19,
|
|
93
|
+
8: 22,
|
|
94
|
+
9: 26,
|
|
95
|
+
10: 34,
|
|
96
|
+
11: 42,
|
|
97
|
+
12: 50,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Space token values: token key → px (from @tamagui/themes v5 space tokens) */
|
|
101
|
+
const SPACE_TOKENS: Record<number, number> = {
|
|
102
|
+
0: 0,
|
|
103
|
+
0.25: 0.5,
|
|
104
|
+
0.5: 1,
|
|
105
|
+
0.75: 1.5,
|
|
106
|
+
1: 2,
|
|
107
|
+
1.5: 4,
|
|
108
|
+
2: 7,
|
|
109
|
+
2.5: 10,
|
|
110
|
+
3: 13,
|
|
111
|
+
3.5: 16,
|
|
112
|
+
4: 18,
|
|
113
|
+
4.5: 21,
|
|
114
|
+
5: 24,
|
|
115
|
+
6: 32,
|
|
116
|
+
7: 39,
|
|
117
|
+
8: 46,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Inner border radius knob → pixel value
|
|
122
|
+
* Source: borderRadiusMap in resolveKnobs.ts
|
|
123
|
+
*/
|
|
124
|
+
const BORDER_RADIUS_KNOB: Record<string, number> = {
|
|
125
|
+
none: RADIUS_TOKENS[0], // 0px
|
|
126
|
+
small: RADIUS_TOKENS[2], // 5px
|
|
127
|
+
medium: RADIUS_TOKENS[4], // 9px
|
|
128
|
+
large: RADIUS_TOKENS[6], // 16px
|
|
129
|
+
full: RADIUS_TOKENS[12], // 50px
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Outer border radius knob → pixel value
|
|
134
|
+
* Source: outerRadiusMap in resolveKnobs.ts
|
|
135
|
+
*/
|
|
136
|
+
const OUTER_RADIUS_KNOB: Record<string, number> = {
|
|
137
|
+
none: RADIUS_TOKENS[0], // 0px
|
|
138
|
+
small: RADIUS_TOKENS[2], // 5px
|
|
139
|
+
medium: RADIUS_TOKENS[3], // 7px
|
|
140
|
+
large: RADIUS_TOKENS[4], // 9px
|
|
141
|
+
full: RADIUS_TOKENS[5], // 10px
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Border width knob → raw pixel value (not token-based)
|
|
146
|
+
* Source: borderWidthMap in resolveKnobs.ts
|
|
147
|
+
*/
|
|
148
|
+
const BORDER_WIDTH_KNOB: Record<string, number> = {
|
|
149
|
+
none: 0,
|
|
150
|
+
small: 0.5,
|
|
151
|
+
medium: 1,
|
|
152
|
+
large: 2,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gap (inner spacing) knob → pixel value
|
|
157
|
+
* Source: gapMap in resolveKnobs.ts ($2, $3, $4 space tokens)
|
|
158
|
+
*/
|
|
159
|
+
const GAP_KNOB: Record<string, number> = {
|
|
160
|
+
small: SPACE_TOKENS[2], // 7px
|
|
161
|
+
medium: SPACE_TOKENS[3], // 13px
|
|
162
|
+
large: SPACE_TOKENS[4], // 18px
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gap large (gapLg) knob → pixel value
|
|
167
|
+
* Source: gapLgMap in resolveKnobs.ts ($3, $4, $5 space tokens)
|
|
168
|
+
*/
|
|
169
|
+
const GAP_LG_KNOB: Record<string, number> = {
|
|
170
|
+
small: SPACE_TOKENS[3], // 13px
|
|
171
|
+
medium: SPACE_TOKENS[4], // 18px
|
|
172
|
+
large: SPACE_TOKENS[5], // 24px
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Panel padding knob → pixel value
|
|
177
|
+
* Source: panelPaddingMap in resolveKnobs.ts ($3, $4, $6 space tokens)
|
|
178
|
+
*/
|
|
179
|
+
const PANEL_PADDING_KNOB: Record<string, number> = {
|
|
180
|
+
small: SPACE_TOKENS[3], // 13px
|
|
181
|
+
medium: SPACE_TOKENS[4], // 18px
|
|
182
|
+
large: SPACE_TOKENS[6], // 32px
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Font weight knob → value string
|
|
187
|
+
* Source: fontWeightMap in resolveKnobs.ts
|
|
188
|
+
*/
|
|
189
|
+
const FONT_WEIGHT_KNOB: Record<string, number> = {
|
|
190
|
+
regular: 400,
|
|
191
|
+
bold: 700,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ── Valid value sets ──────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/** All valid border-radius px values (inner + outer scales combined) */
|
|
197
|
+
const VALID_BORDER_RADIUS_PX = new Set([
|
|
198
|
+
...Object.values(BORDER_RADIUS_KNOB),
|
|
199
|
+
...Object.values(OUTER_RADIUS_KNOB),
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
/** All valid border-width values */
|
|
203
|
+
const VALID_BORDER_WIDTH = new Set(Object.values(BORDER_WIDTH_KNOB));
|
|
204
|
+
|
|
205
|
+
/** All valid gap px values (gap + gapLg scales combined, plus 0) */
|
|
206
|
+
const VALID_GAP_PX = new Set([0, ...Object.values(GAP_KNOB), ...Object.values(GAP_LG_KNOB)]);
|
|
207
|
+
|
|
208
|
+
/** All valid padding px values (panel padding + 0) */
|
|
209
|
+
const VALID_PADDING_PX = new Set([0, ...Object.values(PANEL_PADDING_KNOB)]);
|
|
210
|
+
|
|
211
|
+
/** All valid font-weight values */
|
|
212
|
+
const VALID_FONT_WEIGHT = new Set(Object.values(FONT_WEIGHT_KNOB));
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Valid primary font families (first font in the computed stack).
|
|
216
|
+
* Source: packages/config/fonts.ts fontFamilies + Tamagui Inter stack.
|
|
217
|
+
* Keys are lowercase for case-insensitive matching.
|
|
218
|
+
*/
|
|
219
|
+
const FONT_FAMILY_KNOB: Record<string, string> = {
|
|
220
|
+
inter: "sans-serif ($body/$heading)",
|
|
221
|
+
georgia: "serif",
|
|
222
|
+
monospace: "mono",
|
|
223
|
+
"roboto slab": "slab",
|
|
224
|
+
rockwell: "slab (fallback)",
|
|
225
|
+
nunito: "rounded",
|
|
226
|
+
"varela round": "rounded (fallback)",
|
|
227
|
+
"roboto condensed": "condensed",
|
|
228
|
+
"arial narrow": "condensed (fallback)",
|
|
229
|
+
"dancing script": "cursive",
|
|
230
|
+
pacifico: "cursive (fallback)",
|
|
231
|
+
caveat: "handwriting",
|
|
232
|
+
"patrick hand": "handwriting (fallback)",
|
|
233
|
+
silkscreen: "pixel",
|
|
234
|
+
"press start 2p": "pixel (fallback)",
|
|
235
|
+
unifrakturcook: "blackletter",
|
|
236
|
+
fraktur: "blackletter (fallback)",
|
|
237
|
+
futura: "geometric",
|
|
238
|
+
poppins: "geometric (fallback)",
|
|
239
|
+
"century gothic": "geometric (fallback)",
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** System/generic fonts that are valid as part of fallback stacks */
|
|
243
|
+
const SYSTEM_FONTS = new Set([
|
|
244
|
+
"-apple-system",
|
|
245
|
+
"system-ui",
|
|
246
|
+
"blinkmacsystemfont",
|
|
247
|
+
"segoe ui",
|
|
248
|
+
"roboto",
|
|
249
|
+
"helvetica",
|
|
250
|
+
"arial",
|
|
251
|
+
"sans-serif",
|
|
252
|
+
"serif",
|
|
253
|
+
"cursive",
|
|
254
|
+
"comic sans ms",
|
|
255
|
+
"courier new",
|
|
256
|
+
"times new roman",
|
|
257
|
+
"times",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
// ── Reverse-map helpers ───────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
function reverseMapRadius(px: number): string {
|
|
263
|
+
for (const [knob, val] of Object.entries(BORDER_RADIUS_KNOB)) {
|
|
264
|
+
if (val === px) return knob;
|
|
265
|
+
}
|
|
266
|
+
for (const [knob, val] of Object.entries(OUTER_RADIUS_KNOB)) {
|
|
267
|
+
if (val === px) return `${knob} (outer)`;
|
|
268
|
+
}
|
|
269
|
+
return "off-scale";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function reverseMapBorderWidth(px: number): string {
|
|
273
|
+
for (const [knob, val] of Object.entries(BORDER_WIDTH_KNOB)) {
|
|
274
|
+
if (val === px) return knob;
|
|
275
|
+
}
|
|
276
|
+
return "off-scale";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function reverseMapGap(px: number): string {
|
|
280
|
+
for (const [knob, val] of Object.entries(GAP_KNOB)) {
|
|
281
|
+
if (val === px) return knob;
|
|
282
|
+
}
|
|
283
|
+
for (const [knob, val] of Object.entries(GAP_LG_KNOB)) {
|
|
284
|
+
if (val === px) return `${knob} (lg)`;
|
|
285
|
+
}
|
|
286
|
+
if (px === 0) return "none";
|
|
287
|
+
return "off-scale";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function reverseMapPadding(px: number): string {
|
|
291
|
+
for (const [knob, val] of Object.entries(PANEL_PADDING_KNOB)) {
|
|
292
|
+
if (val === px) return knob;
|
|
293
|
+
}
|
|
294
|
+
if (px === 0) return "none";
|
|
295
|
+
return "off-scale";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function reverseMapFontWeight(weight: number): string {
|
|
299
|
+
for (const [knob, val] of Object.entries(FONT_WEIGHT_KNOB)) {
|
|
300
|
+
if (val === weight) return knob;
|
|
301
|
+
}
|
|
302
|
+
return "off-scale";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function reverseMapFontFamily(computed: string): { knob: string; offScale: boolean } {
|
|
306
|
+
const first = computed
|
|
307
|
+
.split(",")[0]
|
|
308
|
+
.trim()
|
|
309
|
+
.replace(/^['"]|['"]$/g, "")
|
|
310
|
+
.toLowerCase();
|
|
311
|
+
if (!first) return { knob: "empty", offScale: true };
|
|
312
|
+
const knob = FONT_FAMILY_KNOB[first];
|
|
313
|
+
if (knob) return { knob, offScale: false };
|
|
314
|
+
if (SYSTEM_FONTS.has(first)) return { knob: `system (${first})`, offScale: false };
|
|
315
|
+
return { knob: "off-scale", offScale: true };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Core audit function ───────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Runs the constraint audit in the browser.
|
|
322
|
+
*
|
|
323
|
+
* This function is completely self-contained — no imports or external refs.
|
|
324
|
+
* Pass it directly to Playwright's page.evaluate():
|
|
325
|
+
*
|
|
326
|
+
* const report = await page.evaluate(runConstraintAudit);
|
|
327
|
+
*
|
|
328
|
+
* Returns an AuditReport object describing all structural CSS violations.
|
|
329
|
+
*/
|
|
330
|
+
export function runConstraintAudit(): AuditReport {
|
|
331
|
+
// ── Inline token definitions (must be self-contained for page.evaluate) ──
|
|
332
|
+
|
|
333
|
+
const _RADIUS_TOKENS: Record<number, number> = {
|
|
334
|
+
0: 0,
|
|
335
|
+
2: 5,
|
|
336
|
+
3: 7,
|
|
337
|
+
4: 9,
|
|
338
|
+
5: 10,
|
|
339
|
+
6: 16,
|
|
340
|
+
12: 50,
|
|
341
|
+
};
|
|
342
|
+
const _SPACE_TOKENS: Record<number, number> = {
|
|
343
|
+
0: 0,
|
|
344
|
+
2: 7,
|
|
345
|
+
3: 13,
|
|
346
|
+
4: 18,
|
|
347
|
+
5: 24,
|
|
348
|
+
6: 32,
|
|
349
|
+
};
|
|
350
|
+
const _BORDER_RADIUS_KNOB: Record<string, number> = {
|
|
351
|
+
none: 0,
|
|
352
|
+
small: 5,
|
|
353
|
+
medium: 9,
|
|
354
|
+
large: 16,
|
|
355
|
+
full: 50,
|
|
356
|
+
};
|
|
357
|
+
const _OUTER_RADIUS_KNOB: Record<string, number> = {
|
|
358
|
+
none: 0,
|
|
359
|
+
small: 5,
|
|
360
|
+
medium: 7,
|
|
361
|
+
large: 9,
|
|
362
|
+
full: 10,
|
|
363
|
+
};
|
|
364
|
+
const _BORDER_WIDTH_KNOB: Record<string, number> = {
|
|
365
|
+
none: 0,
|
|
366
|
+
small: 0.5,
|
|
367
|
+
medium: 1,
|
|
368
|
+
large: 2,
|
|
369
|
+
};
|
|
370
|
+
const _GAP_KNOB: Record<string, number> = {
|
|
371
|
+
small: 7,
|
|
372
|
+
medium: 13,
|
|
373
|
+
large: 18,
|
|
374
|
+
};
|
|
375
|
+
const _GAP_LG_KNOB: Record<string, number> = {
|
|
376
|
+
small: 13,
|
|
377
|
+
medium: 18,
|
|
378
|
+
large: 24,
|
|
379
|
+
};
|
|
380
|
+
const _PANEL_PADDING_KNOB: Record<string, number> = {
|
|
381
|
+
small: 13,
|
|
382
|
+
medium: 18,
|
|
383
|
+
large: 32,
|
|
384
|
+
};
|
|
385
|
+
const _FONT_WEIGHT_KNOB: Record<string, number> = {
|
|
386
|
+
light: 300, // Inter body default weight
|
|
387
|
+
regular: 400,
|
|
388
|
+
bold: 700,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const _VALID_RADIUS = new Set([
|
|
392
|
+
...Object.values(_BORDER_RADIUS_KNOB),
|
|
393
|
+
...Object.values(_OUTER_RADIUS_KNOB),
|
|
394
|
+
1000, // Tamagui pill radius (Switch, RadioGroup.Item)
|
|
395
|
+
100000, // Tamagui circular button radius (Button circular variant)
|
|
396
|
+
]);
|
|
397
|
+
const _VALID_BORDER_WIDTH = new Set(Object.values(_BORDER_WIDTH_KNOB));
|
|
398
|
+
const _VALID_GAP = new Set([
|
|
399
|
+
0,
|
|
400
|
+
2, // $space.1 Tamagui space token
|
|
401
|
+
3.9, // Tamagui InputContainerFrame size-proportional gap
|
|
402
|
+
4, // $size.$0.5 sub-token used by Tamagui button internals
|
|
403
|
+
5, // $size.$1
|
|
404
|
+
9, // $size.$2
|
|
405
|
+
...Object.values(_GAP_KNOB),
|
|
406
|
+
...Object.values(_GAP_LG_KNOB),
|
|
407
|
+
]);
|
|
408
|
+
const _VALID_PADDING = new Set([
|
|
409
|
+
0,
|
|
410
|
+
2, // $space.1 Tamagui space token
|
|
411
|
+
7, // $size.$1.5 sub-token (button padding at $size.$2)
|
|
412
|
+
...Object.values(_PANEL_PADDING_KNOB),
|
|
413
|
+
]);
|
|
414
|
+
const _VALID_FONT_WEIGHT = new Set(Object.values(_FONT_WEIGHT_KNOB));
|
|
415
|
+
|
|
416
|
+
const _FONT_FAMILY_KNOB: Record<string, string> = {
|
|
417
|
+
inter: "sans-serif ($body/$heading)",
|
|
418
|
+
georgia: "serif",
|
|
419
|
+
monospace: "mono",
|
|
420
|
+
"roboto slab": "slab",
|
|
421
|
+
rockwell: "slab (fallback)",
|
|
422
|
+
nunito: "rounded",
|
|
423
|
+
"varela round": "rounded (fallback)",
|
|
424
|
+
"roboto condensed": "condensed",
|
|
425
|
+
"arial narrow": "condensed (fallback)",
|
|
426
|
+
"dancing script": "cursive",
|
|
427
|
+
pacifico: "cursive (fallback)",
|
|
428
|
+
caveat: "handwriting",
|
|
429
|
+
"patrick hand": "handwriting (fallback)",
|
|
430
|
+
silkscreen: "pixel",
|
|
431
|
+
"press start 2p": "pixel (fallback)",
|
|
432
|
+
unifrakturcook: "blackletter",
|
|
433
|
+
fraktur: "blackletter (fallback)",
|
|
434
|
+
futura: "geometric",
|
|
435
|
+
poppins: "geometric (fallback)",
|
|
436
|
+
"century gothic": "geometric (fallback)",
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const _SYSTEM_FONTS = new Set([
|
|
440
|
+
"-apple-system",
|
|
441
|
+
"system-ui",
|
|
442
|
+
"blinkmacsystemfont",
|
|
443
|
+
"segoe ui",
|
|
444
|
+
"roboto",
|
|
445
|
+
"helvetica",
|
|
446
|
+
"arial",
|
|
447
|
+
"sans-serif",
|
|
448
|
+
"serif",
|
|
449
|
+
"cursive",
|
|
450
|
+
"comic sans ms",
|
|
451
|
+
"courier new",
|
|
452
|
+
"times new roman",
|
|
453
|
+
"times",
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
// ── Reverse-map helpers (inline) ──────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
function _reverseRadius(px: number): string {
|
|
459
|
+
if (px === 1000) return "pill";
|
|
460
|
+
if (px === 100000) return "circular";
|
|
461
|
+
for (const [k, v] of Object.entries(_BORDER_RADIUS_KNOB)) {
|
|
462
|
+
if (v === px) return k;
|
|
463
|
+
}
|
|
464
|
+
for (const [k, v] of Object.entries(_OUTER_RADIUS_KNOB)) {
|
|
465
|
+
if (v === px) return `${k} (outer)`;
|
|
466
|
+
}
|
|
467
|
+
return "off-scale";
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function _reverseBorderWidth(px: number): string {
|
|
471
|
+
for (const [k, v] of Object.entries(_BORDER_WIDTH_KNOB)) {
|
|
472
|
+
if (v === px) return k;
|
|
473
|
+
}
|
|
474
|
+
return "off-scale";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function _reverseGap(px: number): string {
|
|
478
|
+
if (px === 0) return "none";
|
|
479
|
+
for (const [k, v] of Object.entries(_GAP_KNOB)) {
|
|
480
|
+
if (v === px) return k;
|
|
481
|
+
}
|
|
482
|
+
for (const [k, v] of Object.entries(_GAP_LG_KNOB)) {
|
|
483
|
+
if (v === px) return `${k} (lg)`;
|
|
484
|
+
}
|
|
485
|
+
// Tamagui space/size sub-tokens used internally by components
|
|
486
|
+
if (px === 2) return "space-token(2px)";
|
|
487
|
+
if (px === 3.9) return "input-internal(3.9px)";
|
|
488
|
+
if (px === 4 || px === 5 || px === 9) return `size-token(${px}px)`;
|
|
489
|
+
return "off-scale";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function _reversePadding(px: number): string {
|
|
493
|
+
if (px === 0) return "none";
|
|
494
|
+
for (const [k, v] of Object.entries(_PANEL_PADDING_KNOB)) {
|
|
495
|
+
if (v === px) return k;
|
|
496
|
+
}
|
|
497
|
+
// Tamagui space/size sub-tokens
|
|
498
|
+
if (px === 2) return "space-token(2px)";
|
|
499
|
+
if (px === 7) return "size-token(7px)";
|
|
500
|
+
return "off-scale";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function _reverseFontWeight(w: number): string {
|
|
504
|
+
for (const [k, v] of Object.entries(_FONT_WEIGHT_KNOB)) {
|
|
505
|
+
if (v === w) return k;
|
|
506
|
+
}
|
|
507
|
+
return "off-scale";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function _reverseFontFamily(computed: string): { knob: string; offScale: boolean } {
|
|
511
|
+
const first = computed
|
|
512
|
+
.split(",")[0]
|
|
513
|
+
.trim()
|
|
514
|
+
.replace(/^['"]|['"]$/g, "")
|
|
515
|
+
.toLowerCase();
|
|
516
|
+
if (!first) return { knob: "empty", offScale: true };
|
|
517
|
+
const knob = _FONT_FAMILY_KNOB[first];
|
|
518
|
+
if (knob) return { knob, offScale: false };
|
|
519
|
+
if (_SYSTEM_FONTS.has(first)) return { knob: `system (${first})`, offScale: false };
|
|
520
|
+
return { knob: "off-scale", offScale: true };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Check inline style for hardcoded color values that bypass theme tokens.
|
|
525
|
+
* Returns the offending property and value, or null if clean.
|
|
526
|
+
*/
|
|
527
|
+
function _checkInlineColors(el: Element): { property: string; value: string } | null {
|
|
528
|
+
const htmlEl = el as HTMLElement;
|
|
529
|
+
if (!htmlEl.style) return null;
|
|
530
|
+
const colorProps = ["color", "backgroundColor", "borderColor"] as const;
|
|
531
|
+
for (const prop of colorProps) {
|
|
532
|
+
const val = htmlEl.style[prop];
|
|
533
|
+
if (!val || val === "inherit" || val === "transparent" || val === "currentColor") continue;
|
|
534
|
+
if (val.startsWith("var(")) continue;
|
|
535
|
+
return { property: prop, value: val };
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Helper: parse a single px value from computed style ──────────────────
|
|
541
|
+
|
|
542
|
+
function parsePx(raw: string): number | null {
|
|
543
|
+
if (!raw || raw === "" || raw === "normal" || raw === "auto") return null;
|
|
544
|
+
// Handle "9px 9px 9px 9px" form — take first value
|
|
545
|
+
const first = raw.split(" ")[0];
|
|
546
|
+
const n = parseFloat(first);
|
|
547
|
+
if (isNaN(n)) return null;
|
|
548
|
+
return Math.round(n * 100) / 100; // round to 2 decimal places
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Helper: check if border-radius is percentage-based ────────────────────
|
|
552
|
+
|
|
553
|
+
function isPercentageBorderRadius(el: Element): boolean {
|
|
554
|
+
// getComputedStyle resolves percentages to px, so check inline style
|
|
555
|
+
// or CSS text for percentages. This is a best-effort check.
|
|
556
|
+
const inline = (el as HTMLElement).style?.borderRadius ?? "";
|
|
557
|
+
if (inline.includes("%")) return true;
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── Helper: build a human-readable element label ──────────────────────────
|
|
562
|
+
|
|
563
|
+
function elementLabel(el: Element): string {
|
|
564
|
+
const tag = el.tagName.toLowerCase();
|
|
565
|
+
const id = el.id ? `#${el.id}` : "";
|
|
566
|
+
// Only take non-hash classes (Tamagui hash classes start with _ or are short hashes)
|
|
567
|
+
const classes = Array.from(el.classList)
|
|
568
|
+
.filter((c) => c.length > 3 && !/^[_a-z0-9]{1,8}-/.test(c) && !/^\d/.test(c))
|
|
569
|
+
.slice(0, 3)
|
|
570
|
+
.join(".");
|
|
571
|
+
const cls = classes ? `.${classes}` : "";
|
|
572
|
+
const role = el.getAttribute("role") ? `[role=${el.getAttribute("role")}]` : "";
|
|
573
|
+
const testId = el.getAttribute("data-testid")
|
|
574
|
+
? `[data-testid=${el.getAttribute("data-testid")}]`
|
|
575
|
+
: "";
|
|
576
|
+
return `${tag}${id}${cls}${role}${testId}` || tag;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Helper: audit a single numeric property ───────────────────────────────
|
|
580
|
+
|
|
581
|
+
function auditProp(
|
|
582
|
+
value: number | null,
|
|
583
|
+
validSet: Set<number>,
|
|
584
|
+
reverseFn: (v: number) => string,
|
|
585
|
+
propName: string,
|
|
586
|
+
violations: string[],
|
|
587
|
+
): PropertyAudit | undefined {
|
|
588
|
+
if (value === null || value === 0) return undefined;
|
|
589
|
+
const knob = reverseFn(value);
|
|
590
|
+
const offScale = !validSet.has(value);
|
|
591
|
+
if (offScale) {
|
|
592
|
+
violations.push(`${propName}: ${value}px is off-scale (no matching token)`);
|
|
593
|
+
}
|
|
594
|
+
return { value, unit: "px", knob, offScale };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Read active preset and scheme from DOM ────────────────────────────────
|
|
598
|
+
|
|
599
|
+
function readPreset(): string {
|
|
600
|
+
const root = document.documentElement;
|
|
601
|
+
// Try data attributes set by ThemeProvider
|
|
602
|
+
return (
|
|
603
|
+
root.dataset.preset ??
|
|
604
|
+
document.cookie
|
|
605
|
+
.split("; ")
|
|
606
|
+
.find((c) => c.startsWith("mp.preset="))
|
|
607
|
+
?.split("=")[1] ??
|
|
608
|
+
"unknown"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function readScheme(): string {
|
|
613
|
+
const root = document.documentElement;
|
|
614
|
+
if (root.classList.contains("t_dark")) return "dark";
|
|
615
|
+
if (root.classList.contains("t_light")) return "light";
|
|
616
|
+
return root.style.colorScheme || "unknown";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── Element selection ─────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
// Query all block/interactive elements. Skip purely semantic/structural ones.
|
|
622
|
+
const SKIP_TAGS = new Set([
|
|
623
|
+
"script",
|
|
624
|
+
"style",
|
|
625
|
+
"link",
|
|
626
|
+
"meta",
|
|
627
|
+
"head",
|
|
628
|
+
"html",
|
|
629
|
+
"title",
|
|
630
|
+
"noscript",
|
|
631
|
+
"br",
|
|
632
|
+
"hr",
|
|
633
|
+
"col",
|
|
634
|
+
"colgroup",
|
|
635
|
+
"caption",
|
|
636
|
+
"source",
|
|
637
|
+
"track",
|
|
638
|
+
"wbr",
|
|
639
|
+
"area",
|
|
640
|
+
"map",
|
|
641
|
+
"param",
|
|
642
|
+
"embed",
|
|
643
|
+
"object",
|
|
644
|
+
"base",
|
|
645
|
+
"canvas",
|
|
646
|
+
"audio",
|
|
647
|
+
"video",
|
|
648
|
+
"picture",
|
|
649
|
+
"figure",
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
// Third-party devtools root containers — skip their entire subtree
|
|
653
|
+
const DEVTOOLS_SELECTORS = ["#tanstack_devtools", "[data-testid=tanstack_devtools]"];
|
|
654
|
+
const devtoolsRoots = DEVTOOLS_SELECTORS.flatMap((s) => Array.from(document.querySelectorAll(s)));
|
|
655
|
+
|
|
656
|
+
const allElements = Array.from(document.querySelectorAll("*"));
|
|
657
|
+
const candidates = allElements.filter((el) => {
|
|
658
|
+
const tag = el.tagName.toLowerCase();
|
|
659
|
+
if (SKIP_TAGS.has(tag)) return false;
|
|
660
|
+
// Skip SVG internals
|
|
661
|
+
if (el.namespaceURI === "http://www.w3.org/2000/svg" && tag !== "svg") return false;
|
|
662
|
+
// Skip hidden elements — display:none means the style has no visual impact
|
|
663
|
+
if ((el as HTMLElement).style?.display === "none") return false;
|
|
664
|
+
// Skip third-party devtools overlays
|
|
665
|
+
if (devtoolsRoots.some((root) => root.contains(el))) return false;
|
|
666
|
+
return true;
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// ── Run the audit ─────────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
const auditedElements: ElementAudit[] = [];
|
|
672
|
+
const violationElements: ElementAudit[] = [];
|
|
673
|
+
|
|
674
|
+
for (const el of candidates) {
|
|
675
|
+
const style = getComputedStyle(el);
|
|
676
|
+
const tag = el.tagName.toLowerCase();
|
|
677
|
+
|
|
678
|
+
const entry: ElementAudit = {
|
|
679
|
+
label: elementLabel(el),
|
|
680
|
+
tag,
|
|
681
|
+
id: el.id || "",
|
|
682
|
+
classes: Array.from(el.classList)
|
|
683
|
+
.filter((c) => c.length > 3 && !/^[_a-z][a-z0-9]{0,6}-/.test(c))
|
|
684
|
+
.slice(0, 5),
|
|
685
|
+
violations: [],
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
let hasAnyProp = false;
|
|
689
|
+
|
|
690
|
+
// ── Border Radius ───────────────────────────────────────────────────────
|
|
691
|
+
// Skip percentage-based border-radius (used for circles, not knob system)
|
|
692
|
+
if (!isPercentageBorderRadius(el)) {
|
|
693
|
+
const brRaw = style.borderTopLeftRadius || style.borderRadius;
|
|
694
|
+
const br = parsePx(brRaw);
|
|
695
|
+
if (br !== null && br > 0) {
|
|
696
|
+
entry.borderRadius = auditProp(
|
|
697
|
+
br,
|
|
698
|
+
_VALID_RADIUS,
|
|
699
|
+
_reverseRadius,
|
|
700
|
+
"borderRadius",
|
|
701
|
+
entry.violations,
|
|
702
|
+
);
|
|
703
|
+
if (entry.borderRadius) hasAnyProp = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── Border Width ────────────────────────────────────────────────────────
|
|
708
|
+
const bwRaw = style.borderTopWidth; // check top; knobs set all sides equally
|
|
709
|
+
const bw = parsePx(bwRaw);
|
|
710
|
+
if (bw !== null && bw > 0) {
|
|
711
|
+
entry.borderWidth = auditProp(
|
|
712
|
+
bw,
|
|
713
|
+
_VALID_BORDER_WIDTH,
|
|
714
|
+
_reverseBorderWidth,
|
|
715
|
+
"borderWidth",
|
|
716
|
+
entry.violations,
|
|
717
|
+
);
|
|
718
|
+
if (entry.borderWidth) hasAnyProp = true;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ── Gap ─────────────────────────────────────────────────────────────────
|
|
722
|
+
const rowGap = parsePx(style.rowGap);
|
|
723
|
+
const colGap = parsePx(style.columnGap);
|
|
724
|
+
|
|
725
|
+
if (rowGap !== null && rowGap > 0) {
|
|
726
|
+
entry.rowGap = auditProp(rowGap, _VALID_GAP, _reverseGap, "rowGap", entry.violations);
|
|
727
|
+
if (entry.rowGap) hasAnyProp = true;
|
|
728
|
+
}
|
|
729
|
+
if (colGap !== null && colGap > 0 && colGap !== rowGap) {
|
|
730
|
+
entry.columnGap = auditProp(colGap, _VALID_GAP, _reverseGap, "columnGap", entry.violations);
|
|
731
|
+
if (entry.columnGap) hasAnyProp = true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── Padding ─────────────────────────────────────────────────────────────
|
|
735
|
+
// Only check padding on elements that look like surfaces (have border-radius
|
|
736
|
+
// or border-width already audited). Padding on all elements creates too many
|
|
737
|
+
// false positives from browser defaults and third-party components.
|
|
738
|
+
if (entry.borderRadius || entry.borderWidth) {
|
|
739
|
+
const sides = [
|
|
740
|
+
["paddingTop", style.paddingTop],
|
|
741
|
+
["paddingRight", style.paddingRight],
|
|
742
|
+
["paddingBottom", style.paddingBottom],
|
|
743
|
+
["paddingLeft", style.paddingLeft],
|
|
744
|
+
] as const;
|
|
745
|
+
|
|
746
|
+
for (const [prop, raw] of sides) {
|
|
747
|
+
const val = parsePx(raw);
|
|
748
|
+
if (val !== null && val > 0) {
|
|
749
|
+
const result = auditProp(val, _VALID_PADDING, _reversePadding, prop, entry.violations);
|
|
750
|
+
if (result) {
|
|
751
|
+
(entry as unknown as Record<string, unknown>)[prop] = result;
|
|
752
|
+
hasAnyProp = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── Font Weight ─────────────────────────────────────────────────────────
|
|
759
|
+
const fw = parsePx(style.fontWeight);
|
|
760
|
+
if (fw !== null && fw !== 400) {
|
|
761
|
+
// Only flag non-regular weights that aren't in our valid set
|
|
762
|
+
const result = auditProp(
|
|
763
|
+
fw,
|
|
764
|
+
_VALID_FONT_WEIGHT,
|
|
765
|
+
_reverseFontWeight,
|
|
766
|
+
"fontWeight",
|
|
767
|
+
entry.violations,
|
|
768
|
+
);
|
|
769
|
+
if (result) {
|
|
770
|
+
entry.fontWeight = result;
|
|
771
|
+
hasAnyProp = true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ── Font Family ──────────────────────────────────────────────────────────
|
|
776
|
+
// Only check elements with text content and where font-family differs from parent
|
|
777
|
+
const TEXT_TAGS = new Set([
|
|
778
|
+
"h1",
|
|
779
|
+
"h2",
|
|
780
|
+
"h3",
|
|
781
|
+
"h4",
|
|
782
|
+
"h5",
|
|
783
|
+
"h6",
|
|
784
|
+
"p",
|
|
785
|
+
"span",
|
|
786
|
+
"a",
|
|
787
|
+
"button",
|
|
788
|
+
"input",
|
|
789
|
+
"textarea",
|
|
790
|
+
"label",
|
|
791
|
+
"select",
|
|
792
|
+
"li",
|
|
793
|
+
"td",
|
|
794
|
+
"th",
|
|
795
|
+
"dt",
|
|
796
|
+
"dd",
|
|
797
|
+
"figcaption",
|
|
798
|
+
"blockquote",
|
|
799
|
+
"pre",
|
|
800
|
+
"code",
|
|
801
|
+
]);
|
|
802
|
+
if (TEXT_TAGS.has(tag) || (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3)) {
|
|
803
|
+
const parentStyle = el.parentElement ? getComputedStyle(el.parentElement) : null;
|
|
804
|
+
const elFontFamily = style.fontFamily;
|
|
805
|
+
if (elFontFamily && (!parentStyle || elFontFamily !== parentStyle.fontFamily)) {
|
|
806
|
+
const ffResult = _reverseFontFamily(elFontFamily);
|
|
807
|
+
if (ffResult.offScale) {
|
|
808
|
+
entry.fontFamily = { value: elFontFamily, knob: ffResult.knob, offScale: true };
|
|
809
|
+
entry.violations.push(
|
|
810
|
+
`fontFamily: "${elFontFamily.split(",")[0].trim()}" is not a registered font token`,
|
|
811
|
+
);
|
|
812
|
+
hasAnyProp = true;
|
|
813
|
+
} else {
|
|
814
|
+
entry.fontFamily = { value: elFontFamily, knob: ffResult.knob, offScale: false };
|
|
815
|
+
hasAnyProp = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ── Inline Color Bypass ──────────────────────────────────────────────────
|
|
821
|
+
const colorBypass = _checkInlineColors(el);
|
|
822
|
+
if (colorBypass) {
|
|
823
|
+
entry.inlineColor = {
|
|
824
|
+
value: `${colorBypass.property}: ${colorBypass.value}`,
|
|
825
|
+
knob: "off-scale (hardcoded)",
|
|
826
|
+
offScale: true,
|
|
827
|
+
};
|
|
828
|
+
entry.violations.push(
|
|
829
|
+
`inlineColor: ${colorBypass.property}="${colorBypass.value}" bypasses theme tokens`,
|
|
830
|
+
);
|
|
831
|
+
hasAnyProp = true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (hasAnyProp || entry.violations.length > 0) {
|
|
835
|
+
auditedElements.push(entry);
|
|
836
|
+
if (entry.violations.length > 0) {
|
|
837
|
+
violationElements.push(entry);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ── Build summary ─────────────────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
const summary: AuditSummary = {
|
|
845
|
+
totalElements: candidates.length,
|
|
846
|
+
auditedElements: auditedElements.length,
|
|
847
|
+
totalViolations: violationElements.length,
|
|
848
|
+
byProperty: {
|
|
849
|
+
borderRadius: violationElements.filter((e) => e.borderRadius?.offScale).length,
|
|
850
|
+
borderWidth: violationElements.filter((e) => e.borderWidth?.offScale).length,
|
|
851
|
+
gap: violationElements.filter((e) => e.rowGap?.offScale || e.columnGap?.offScale).length,
|
|
852
|
+
padding: violationElements.filter(
|
|
853
|
+
(e) =>
|
|
854
|
+
(e.paddingTop as PropertyAudit | undefined)?.offScale ||
|
|
855
|
+
(e.paddingRight as PropertyAudit | undefined)?.offScale ||
|
|
856
|
+
(e.paddingBottom as PropertyAudit | undefined)?.offScale ||
|
|
857
|
+
(e.paddingLeft as PropertyAudit | undefined)?.offScale,
|
|
858
|
+
).length,
|
|
859
|
+
fontWeight: violationElements.filter((e) => e.fontWeight?.offScale).length,
|
|
860
|
+
fontFamily: violationElements.filter((e) => e.fontFamily?.offScale).length,
|
|
861
|
+
inlineColor: violationElements.filter((e) => e.inlineColor?.offScale).length,
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
url: window.location.href,
|
|
867
|
+
timestamp: new Date().toISOString(),
|
|
868
|
+
preset: readPreset(),
|
|
869
|
+
scheme: readScheme(),
|
|
870
|
+
summary,
|
|
871
|
+
violations: violationElements,
|
|
872
|
+
elements: auditedElements,
|
|
873
|
+
};
|
|
874
|
+
}
|