@loworbitstudio/visor-theme-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.d.ts +150 -0
- package/dist/adapters/index.js +537 -0
- package/dist/chunk-ZLXFCNYF.js +1399 -0
- package/dist/fowt.d.ts +37 -0
- package/dist/fowt.js +23 -0
- package/dist/index.d.ts +799 -0
- package/dist/index.js +2414 -0
- package/dist/types-r7ae3WP2.d.ts +314 -0
- package/package.json +62 -0
- package/src/visor-theme.schema.json +282 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2414 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TAILWIND_GRAY,
|
|
3
|
+
VISOR_FONTS_CDN,
|
|
4
|
+
buildVisorFontUrl,
|
|
5
|
+
clampToSrgb,
|
|
6
|
+
compositeOverBackground,
|
|
7
|
+
generateDarkCss,
|
|
8
|
+
generateFullBundleCss,
|
|
9
|
+
generateLightCss,
|
|
10
|
+
generatePreloadLinks,
|
|
11
|
+
generatePrimitivesCss,
|
|
12
|
+
generateSemanticCss,
|
|
13
|
+
generateShadeScale,
|
|
14
|
+
generateStylesheetLinks,
|
|
15
|
+
getContrastRatio,
|
|
16
|
+
googleFontsCatalog,
|
|
17
|
+
hexToOklch,
|
|
18
|
+
hexToRgb,
|
|
19
|
+
isValidColor,
|
|
20
|
+
isValidHex,
|
|
21
|
+
lookupGoogleFont,
|
|
22
|
+
normalizeHex,
|
|
23
|
+
oklchToHex,
|
|
24
|
+
parseColor,
|
|
25
|
+
parseHex,
|
|
26
|
+
parseHsla,
|
|
27
|
+
parseOklch,
|
|
28
|
+
parseRgba,
|
|
29
|
+
resolveFont,
|
|
30
|
+
resolveThemeFonts,
|
|
31
|
+
rgbToHex,
|
|
32
|
+
rgbToOklch,
|
|
33
|
+
serializeColor
|
|
34
|
+
} from "./chunk-ZLXFCNYF.js";
|
|
35
|
+
|
|
36
|
+
// src/pipeline.ts
|
|
37
|
+
import { parse as parseYaml } from "yaml";
|
|
38
|
+
|
|
39
|
+
// src/visor-theme.schema.json
|
|
40
|
+
var visor_theme_schema_default = {
|
|
41
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
42
|
+
$id: "https://visor.loworbit.studio/schemas/visor-theme.schema.json",
|
|
43
|
+
title: "Visor Theme",
|
|
44
|
+
description: "Schema for .visor.yaml theme files \u2014 portable visual identity definitions for the Visor design system.",
|
|
45
|
+
type: "object",
|
|
46
|
+
required: ["name", "version", "colors"],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: {
|
|
49
|
+
name: {
|
|
50
|
+
type: "string",
|
|
51
|
+
minLength: 1,
|
|
52
|
+
description: "Human-readable theme name."
|
|
53
|
+
},
|
|
54
|
+
version: {
|
|
55
|
+
const: 1,
|
|
56
|
+
description: "Schema version. Currently always 1."
|
|
57
|
+
},
|
|
58
|
+
group: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Theme group for the docs site theme switcher (e.g. 'Visor', 'Client', 'Low Orbit'). Used by `visor theme sync`. Defaults to folder-based grouping when omitted."
|
|
61
|
+
},
|
|
62
|
+
colors: {
|
|
63
|
+
type: "object",
|
|
64
|
+
description: "Color definitions for light mode. Only primary is required \u2014 all others have sensible defaults.",
|
|
65
|
+
required: ["primary"],
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
primary: {
|
|
69
|
+
$ref: "#/$defs/cssColor",
|
|
70
|
+
description: "Brand color. Drives interactive-*, accent-*, text-link, border-focus tokens. Anchored at shade 600."
|
|
71
|
+
},
|
|
72
|
+
accent: {
|
|
73
|
+
$ref: "#/$defs/cssColor",
|
|
74
|
+
description: "Secondary brand color. Defaults to primary if omitted."
|
|
75
|
+
},
|
|
76
|
+
neutral: {
|
|
77
|
+
$ref: "#/$defs/cssColor",
|
|
78
|
+
description: "Base neutral color for generating the gray scale (50-950). Defaults to Tailwind Gray if omitted."
|
|
79
|
+
},
|
|
80
|
+
background: {
|
|
81
|
+
$ref: "#/$defs/cssColor",
|
|
82
|
+
description: "Page background for light mode. Default: #FFFFFF."
|
|
83
|
+
},
|
|
84
|
+
surface: {
|
|
85
|
+
$ref: "#/$defs/cssColor",
|
|
86
|
+
description: "Card/panel background for light mode. Default: #FFFFFF."
|
|
87
|
+
},
|
|
88
|
+
success: {
|
|
89
|
+
$ref: "#/$defs/cssColor",
|
|
90
|
+
description: "Success status color. Default: #22C55E (Tailwind green-500)."
|
|
91
|
+
},
|
|
92
|
+
warning: {
|
|
93
|
+
$ref: "#/$defs/cssColor",
|
|
94
|
+
description: "Warning status color. Default: #F59E0B (Tailwind amber-500)."
|
|
95
|
+
},
|
|
96
|
+
error: {
|
|
97
|
+
$ref: "#/$defs/cssColor",
|
|
98
|
+
description: "Error status color. Default: #EF4444 (Tailwind red-500)."
|
|
99
|
+
},
|
|
100
|
+
info: {
|
|
101
|
+
$ref: "#/$defs/cssColor",
|
|
102
|
+
description: "Info status color. Default: #0EA5E9 (Tailwind sky-500)."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"colors-dark": {
|
|
107
|
+
type: "object",
|
|
108
|
+
description: "Dark mode color overrides. Any key from colors can be overridden. Omitted keys inherit from the derived dark mode values (not from light mode).",
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
properties: {
|
|
111
|
+
primary: { $ref: "#/$defs/cssColor" },
|
|
112
|
+
accent: { $ref: "#/$defs/cssColor" },
|
|
113
|
+
neutral: { $ref: "#/$defs/cssColor" },
|
|
114
|
+
background: { $ref: "#/$defs/cssColor" },
|
|
115
|
+
surface: { $ref: "#/$defs/cssColor" },
|
|
116
|
+
success: { $ref: "#/$defs/cssColor" },
|
|
117
|
+
warning: { $ref: "#/$defs/cssColor" },
|
|
118
|
+
error: { $ref: "#/$defs/cssColor" },
|
|
119
|
+
info: { $ref: "#/$defs/cssColor" }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
typography: {
|
|
123
|
+
type: "object",
|
|
124
|
+
description: "Typography configuration. Defaults to system font stacks if omitted.",
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
properties: {
|
|
127
|
+
heading: {
|
|
128
|
+
type: "object",
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
family: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Google Fonts family name or CSS font stack."
|
|
134
|
+
},
|
|
135
|
+
weight: {
|
|
136
|
+
type: "integer",
|
|
137
|
+
minimum: 100,
|
|
138
|
+
maximum: 900,
|
|
139
|
+
description: "Font weight for headings. Default: 600."
|
|
140
|
+
},
|
|
141
|
+
source: {
|
|
142
|
+
type: "string",
|
|
143
|
+
enum: ["google-fonts", "visor-fonts", "local"],
|
|
144
|
+
description: "Font source. Defaults to Google Fonts lookup, then local."
|
|
145
|
+
},
|
|
146
|
+
org: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Organization namespace for visor-fonts CDN (required when source is visor-fonts)."
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
display: {
|
|
153
|
+
type: "object",
|
|
154
|
+
description: "Display/decorative font for hero text and splash screens. Falls back to heading font if omitted.",
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
family: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Google Fonts family name or CSS font stack."
|
|
160
|
+
},
|
|
161
|
+
weight: {
|
|
162
|
+
type: "integer",
|
|
163
|
+
minimum: 100,
|
|
164
|
+
maximum: 900,
|
|
165
|
+
description: "Font weight for display text. Default: 400."
|
|
166
|
+
},
|
|
167
|
+
source: {
|
|
168
|
+
type: "string",
|
|
169
|
+
enum: ["google-fonts", "visor-fonts", "local"],
|
|
170
|
+
description: "Font source. Defaults to Google Fonts lookup, then local."
|
|
171
|
+
},
|
|
172
|
+
org: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "Organization namespace for visor-fonts CDN (required when source is visor-fonts)."
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
body: {
|
|
179
|
+
type: "object",
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
properties: {
|
|
182
|
+
family: {
|
|
183
|
+
type: "string",
|
|
184
|
+
description: "Google Fonts family name or CSS font stack."
|
|
185
|
+
},
|
|
186
|
+
weight: {
|
|
187
|
+
type: "integer",
|
|
188
|
+
minimum: 100,
|
|
189
|
+
maximum: 900,
|
|
190
|
+
description: "Font weight for body text. Default: 400."
|
|
191
|
+
},
|
|
192
|
+
source: {
|
|
193
|
+
type: "string",
|
|
194
|
+
enum: ["google-fonts", "visor-fonts", "local"],
|
|
195
|
+
description: "Font source. Defaults to Google Fonts lookup, then local."
|
|
196
|
+
},
|
|
197
|
+
org: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: "Organization namespace for visor-fonts CDN (required when source is visor-fonts)."
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
mono: {
|
|
204
|
+
type: "object",
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
properties: {
|
|
207
|
+
family: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "Google Fonts family name or CSS font stack for monospace."
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
scale: {
|
|
214
|
+
type: "number",
|
|
215
|
+
description: "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
216
|
+
},
|
|
217
|
+
"letter-spacing": {
|
|
218
|
+
type: "object",
|
|
219
|
+
description: "Letter spacing scale.",
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
properties: {
|
|
222
|
+
tight: { type: "string" },
|
|
223
|
+
normal: { type: "string" },
|
|
224
|
+
wide: { type: "string" }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
spacing: {
|
|
230
|
+
type: "object",
|
|
231
|
+
description: "Spacing configuration.",
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
properties: {
|
|
234
|
+
base: {
|
|
235
|
+
type: "number",
|
|
236
|
+
minimum: 1,
|
|
237
|
+
description: "Base spacing unit in pixels. Default: 4. Generates the full spacing scale."
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
radius: {
|
|
242
|
+
type: "object",
|
|
243
|
+
description: "Border radius scale in pixels.",
|
|
244
|
+
additionalProperties: false,
|
|
245
|
+
properties: {
|
|
246
|
+
sm: { type: "number", minimum: 0 },
|
|
247
|
+
md: { type: "number", minimum: 0 },
|
|
248
|
+
lg: { type: "number", minimum: 0 },
|
|
249
|
+
xl: { type: "number", minimum: 0 },
|
|
250
|
+
pill: { type: "number", minimum: 0, description: "Default: 9999." }
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
shadows: {
|
|
254
|
+
type: "object",
|
|
255
|
+
description: "Named shadow definitions as CSS box-shadow values.",
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
properties: {
|
|
258
|
+
xs: { type: "string" },
|
|
259
|
+
sm: { type: "string" },
|
|
260
|
+
md: { type: "string" },
|
|
261
|
+
lg: { type: "string" },
|
|
262
|
+
xl: { type: "string" }
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
motion: {
|
|
266
|
+
type: "object",
|
|
267
|
+
description: "Motion/animation configuration.",
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
properties: {
|
|
270
|
+
"duration-fast": {
|
|
271
|
+
type: "string",
|
|
272
|
+
pattern: "^\\d+ms$",
|
|
273
|
+
description: "Fast duration for micro-interactions. Default: 100ms."
|
|
274
|
+
},
|
|
275
|
+
"duration-normal": {
|
|
276
|
+
type: "string",
|
|
277
|
+
pattern: "^\\d+ms$",
|
|
278
|
+
description: "Normal duration for standard transitions. Default: 200ms."
|
|
279
|
+
},
|
|
280
|
+
"duration-slow": {
|
|
281
|
+
type: "string",
|
|
282
|
+
pattern: "^\\d+ms$",
|
|
283
|
+
description: "Slow duration for larger animations. Default: 500ms."
|
|
284
|
+
},
|
|
285
|
+
easing: {
|
|
286
|
+
type: "string",
|
|
287
|
+
description: "Default easing curve as a CSS timing function."
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
overrides: {
|
|
292
|
+
type: "object",
|
|
293
|
+
description: "Per-token escape hatch. Values replace derived tokens. Use CSS custom property names without the -- prefix as keys.",
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
properties: {
|
|
296
|
+
light: {
|
|
297
|
+
type: "object",
|
|
298
|
+
description: "Token overrides applied in light mode.",
|
|
299
|
+
additionalProperties: { type: "string" }
|
|
300
|
+
},
|
|
301
|
+
dark: {
|
|
302
|
+
type: "object",
|
|
303
|
+
description: "Token overrides applied in dark mode.",
|
|
304
|
+
additionalProperties: { type: "string" }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
$defs: {
|
|
310
|
+
cssColor: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "CSS color value. Accepts hex (#RGB, #RRGGBB, #RRGGBBAA), rgb()/rgba(), hsl()/hsla(), or oklch() formats.",
|
|
313
|
+
anyOf: [
|
|
314
|
+
{ pattern: "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" },
|
|
315
|
+
{ pattern: "^rgba?\\(" },
|
|
316
|
+
{ pattern: "^hsla?\\(" },
|
|
317
|
+
{ pattern: "^oklch\\(" }
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/schema.ts
|
|
324
|
+
var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
325
|
+
"name",
|
|
326
|
+
"version",
|
|
327
|
+
"group",
|
|
328
|
+
"colors",
|
|
329
|
+
"colors-dark",
|
|
330
|
+
"typography",
|
|
331
|
+
"spacing",
|
|
332
|
+
"radius",
|
|
333
|
+
"shadows",
|
|
334
|
+
"motion",
|
|
335
|
+
"overrides"
|
|
336
|
+
]);
|
|
337
|
+
var KNOWN_COLOR_KEYS = /* @__PURE__ */ new Set([
|
|
338
|
+
"primary",
|
|
339
|
+
"accent",
|
|
340
|
+
"neutral",
|
|
341
|
+
"background",
|
|
342
|
+
"surface",
|
|
343
|
+
"success",
|
|
344
|
+
"warning",
|
|
345
|
+
"error",
|
|
346
|
+
"info"
|
|
347
|
+
]);
|
|
348
|
+
var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
|
|
349
|
+
"heading",
|
|
350
|
+
"display",
|
|
351
|
+
"body",
|
|
352
|
+
"mono",
|
|
353
|
+
"letter-spacing",
|
|
354
|
+
"scale"
|
|
355
|
+
]);
|
|
356
|
+
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "source", "org"]);
|
|
357
|
+
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
|
|
358
|
+
var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
|
|
359
|
+
var KNOWN_SPACING_KEYS = /* @__PURE__ */ new Set(["base"]);
|
|
360
|
+
var KNOWN_RADIUS_KEYS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "pill"]);
|
|
361
|
+
var KNOWN_SHADOW_KEYS = /* @__PURE__ */ new Set(["xs", "sm", "md", "lg", "xl"]);
|
|
362
|
+
var KNOWN_MOTION_KEYS = /* @__PURE__ */ new Set(["duration-fast", "duration-normal", "duration-slow", "easing"]);
|
|
363
|
+
var KNOWN_OVERRIDES_KEYS = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
364
|
+
function checkUnknownKeys(obj, errors) {
|
|
365
|
+
for (const key of Object.keys(obj)) {
|
|
366
|
+
if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
367
|
+
errors.push(`Unknown top-level key '${key}'. Valid keys: ${[...KNOWN_TOP_LEVEL_KEYS].join(", ")}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (typeof obj.colors === "object" && obj.colors !== null) {
|
|
371
|
+
for (const key of Object.keys(obj.colors)) {
|
|
372
|
+
if (!KNOWN_COLOR_KEYS.has(key)) {
|
|
373
|
+
errors.push(`Unknown key 'colors.${key}'. Valid keys: ${[...KNOWN_COLOR_KEYS].join(", ")}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (typeof obj["colors-dark"] === "object" && obj["colors-dark"] !== null) {
|
|
378
|
+
for (const key of Object.keys(obj["colors-dark"])) {
|
|
379
|
+
if (!KNOWN_COLOR_KEYS.has(key)) {
|
|
380
|
+
errors.push(`Unknown key 'colors-dark.${key}'. Valid keys: ${[...KNOWN_COLOR_KEYS].join(", ")}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (typeof obj.typography === "object" && obj.typography !== null) {
|
|
385
|
+
const typo = obj.typography;
|
|
386
|
+
for (const key of Object.keys(typo)) {
|
|
387
|
+
if (!KNOWN_TYPOGRAPHY_KEYS.has(key)) {
|
|
388
|
+
errors.push(`Unknown key 'typography.${key}'. Valid keys: ${[...KNOWN_TYPOGRAPHY_KEYS].join(", ")}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (typeof typo.heading === "object" && typo.heading !== null) {
|
|
392
|
+
for (const key of Object.keys(typo.heading)) {
|
|
393
|
+
if (!KNOWN_TYPOGRAPHY_FONT_KEYS.has(key)) {
|
|
394
|
+
errors.push(`Unknown key 'typography.heading.${key}'. Valid keys: ${[...KNOWN_TYPOGRAPHY_FONT_KEYS].join(", ")}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (typeof typo.display === "object" && typo.display !== null) {
|
|
399
|
+
for (const key of Object.keys(typo.display)) {
|
|
400
|
+
if (!KNOWN_TYPOGRAPHY_FONT_KEYS.has(key)) {
|
|
401
|
+
errors.push(`Unknown key 'typography.display.${key}'. Valid keys: ${[...KNOWN_TYPOGRAPHY_FONT_KEYS].join(", ")}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (typeof typo.body === "object" && typo.body !== null) {
|
|
406
|
+
for (const key of Object.keys(typo.body)) {
|
|
407
|
+
if (!KNOWN_TYPOGRAPHY_FONT_KEYS.has(key)) {
|
|
408
|
+
errors.push(`Unknown key 'typography.body.${key}'. Valid keys: ${[...KNOWN_TYPOGRAPHY_FONT_KEYS].join(", ")}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (typeof typo.mono === "object" && typo.mono !== null) {
|
|
413
|
+
for (const key of Object.keys(typo.mono)) {
|
|
414
|
+
if (!KNOWN_TYPOGRAPHY_MONO_KEYS.has(key)) {
|
|
415
|
+
errors.push(`Unknown key 'typography.mono.${key}'. Valid keys: ${[...KNOWN_TYPOGRAPHY_MONO_KEYS].join(", ")}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (typeof typo["letter-spacing"] === "object" && typo["letter-spacing"] !== null) {
|
|
420
|
+
for (const key of Object.keys(typo["letter-spacing"])) {
|
|
421
|
+
if (!KNOWN_LETTER_SPACING_KEYS.has(key)) {
|
|
422
|
+
errors.push(`Unknown key 'typography.letter-spacing.${key}'. Valid keys: ${[...KNOWN_LETTER_SPACING_KEYS].join(", ")}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (typeof obj.spacing === "object" && obj.spacing !== null) {
|
|
428
|
+
for (const key of Object.keys(obj.spacing)) {
|
|
429
|
+
if (!KNOWN_SPACING_KEYS.has(key)) {
|
|
430
|
+
errors.push(`Unknown key 'spacing.${key}'. Valid keys: ${[...KNOWN_SPACING_KEYS].join(", ")}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (typeof obj.radius === "object" && obj.radius !== null) {
|
|
435
|
+
for (const key of Object.keys(obj.radius)) {
|
|
436
|
+
if (!KNOWN_RADIUS_KEYS.has(key)) {
|
|
437
|
+
errors.push(`Unknown key 'radius.${key}'. Valid keys: ${[...KNOWN_RADIUS_KEYS].join(", ")}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (typeof obj.shadows === "object" && obj.shadows !== null) {
|
|
442
|
+
for (const key of Object.keys(obj.shadows)) {
|
|
443
|
+
if (!KNOWN_SHADOW_KEYS.has(key)) {
|
|
444
|
+
errors.push(`Unknown key 'shadows.${key}'. Valid keys: ${[...KNOWN_SHADOW_KEYS].join(", ")}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (typeof obj.motion === "object" && obj.motion !== null) {
|
|
449
|
+
for (const key of Object.keys(obj.motion)) {
|
|
450
|
+
if (!KNOWN_MOTION_KEYS.has(key)) {
|
|
451
|
+
errors.push(`Unknown key 'motion.${key}'. Valid keys: ${[...KNOWN_MOTION_KEYS].join(", ")}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (typeof obj.overrides === "object" && obj.overrides !== null) {
|
|
456
|
+
for (const key of Object.keys(obj.overrides)) {
|
|
457
|
+
if (!KNOWN_OVERRIDES_KEYS.has(key)) {
|
|
458
|
+
errors.push(`Unknown key 'overrides.${key}'. Valid keys: ${[...KNOWN_OVERRIDES_KEYS].join(", ")}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function validateConfig(config) {
|
|
464
|
+
const errors = [];
|
|
465
|
+
if (typeof config !== "object" || config === null) {
|
|
466
|
+
return { valid: false, errors: ["Config must be an object"] };
|
|
467
|
+
}
|
|
468
|
+
const obj = config;
|
|
469
|
+
checkUnknownKeys(obj, errors);
|
|
470
|
+
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
|
471
|
+
errors.push("'name' is required and must be a non-empty string");
|
|
472
|
+
}
|
|
473
|
+
if (obj.version !== 1) {
|
|
474
|
+
errors.push("'version' must be 1");
|
|
475
|
+
}
|
|
476
|
+
if (typeof obj.colors !== "object" || obj.colors === null) {
|
|
477
|
+
errors.push("'colors' is required and must be an object");
|
|
478
|
+
return { valid: false, errors };
|
|
479
|
+
}
|
|
480
|
+
const colors = obj.colors;
|
|
481
|
+
if (typeof colors.primary !== "string" || !isValidColor(colors.primary)) {
|
|
482
|
+
errors.push("'colors.primary' is required and must be a valid CSS color (hex, rgba, hsla, or oklch)");
|
|
483
|
+
}
|
|
484
|
+
const optionalColorFields = [
|
|
485
|
+
"accent",
|
|
486
|
+
"neutral",
|
|
487
|
+
"background",
|
|
488
|
+
"surface",
|
|
489
|
+
"success",
|
|
490
|
+
"warning",
|
|
491
|
+
"error",
|
|
492
|
+
"info"
|
|
493
|
+
];
|
|
494
|
+
for (const field of optionalColorFields) {
|
|
495
|
+
if (colors[field] !== void 0) {
|
|
496
|
+
if (typeof colors[field] !== "string" || !isValidColor(colors[field])) {
|
|
497
|
+
errors.push(`'colors.${field}' must be a valid CSS color (hex, rgba, hsla, or oklch)`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (obj["colors-dark"] !== void 0) {
|
|
502
|
+
if (typeof obj["colors-dark"] !== "object" || obj["colors-dark"] === null) {
|
|
503
|
+
errors.push("'colors-dark' must be an object");
|
|
504
|
+
} else {
|
|
505
|
+
const darkColors = obj["colors-dark"];
|
|
506
|
+
const allColorFields = ["primary", ...optionalColorFields];
|
|
507
|
+
for (const field of allColorFields) {
|
|
508
|
+
if (darkColors[field] !== void 0) {
|
|
509
|
+
if (typeof darkColors[field] !== "string" || !isValidColor(darkColors[field])) {
|
|
510
|
+
errors.push(`'colors-dark.${field}' must be a valid CSS color (hex, rgba, hsla, or oklch)`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (obj.motion && typeof obj.motion === "object") {
|
|
517
|
+
const motion = obj.motion;
|
|
518
|
+
for (const key of ["duration-fast", "duration-normal", "duration-slow"]) {
|
|
519
|
+
if (motion[key] !== void 0) {
|
|
520
|
+
if (typeof motion[key] !== "string" || !/^\d+ms$/.test(motion[key])) {
|
|
521
|
+
errors.push(`'motion.${key}' must match pattern "Nms" (e.g., "200ms")`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (typeof obj.typography === "object" && obj.typography !== null) {
|
|
527
|
+
const typo = obj.typography;
|
|
528
|
+
for (const slot of ["heading", "display", "body"]) {
|
|
529
|
+
const font = typo[slot];
|
|
530
|
+
if (font && font.source === "visor-fonts" && !font.org) {
|
|
531
|
+
errors.push(`'typography.${slot}.org' is required when source is 'visor-fonts'`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (obj.overrides !== void 0) {
|
|
536
|
+
if (typeof obj.overrides !== "object" || obj.overrides === null) {
|
|
537
|
+
errors.push("'overrides' must be an object");
|
|
538
|
+
} else {
|
|
539
|
+
const overrides = obj.overrides;
|
|
540
|
+
for (const mode of ["light", "dark"]) {
|
|
541
|
+
if (overrides[mode] !== void 0) {
|
|
542
|
+
if (typeof overrides[mode] !== "object" || overrides[mode] === null) {
|
|
543
|
+
errors.push(`'overrides.${mode}' must be an object`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return { valid: errors.length === 0, errors };
|
|
550
|
+
}
|
|
551
|
+
function isVisorThemeConfig(config) {
|
|
552
|
+
return validateConfig(config).valid;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/resolve.ts
|
|
556
|
+
var DEFAULT_FONT_SANS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
557
|
+
var DEFAULT_FONT_MONO = '"SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace';
|
|
558
|
+
var DEFAULTS = {
|
|
559
|
+
colors: {
|
|
560
|
+
background: "#FFFFFF",
|
|
561
|
+
surface: "#FFFFFF",
|
|
562
|
+
success: "#22C55E",
|
|
563
|
+
warning: "#F59E0B",
|
|
564
|
+
error: "#EF4444",
|
|
565
|
+
info: "#0EA5E9"
|
|
566
|
+
},
|
|
567
|
+
typography: {
|
|
568
|
+
scale: 1,
|
|
569
|
+
heading: { family: DEFAULT_FONT_SANS, weight: 600 },
|
|
570
|
+
body: { family: DEFAULT_FONT_SANS, weight: 400 },
|
|
571
|
+
mono: { family: DEFAULT_FONT_MONO }
|
|
572
|
+
},
|
|
573
|
+
spacing: { base: 4 },
|
|
574
|
+
radius: { sm: 2, md: 4, lg: 8, xl: 12, pill: 9999 },
|
|
575
|
+
shadows: {
|
|
576
|
+
xs: "0 1px 1px 0 rgba(0, 0, 0, 0.04)",
|
|
577
|
+
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
|
578
|
+
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
|
|
579
|
+
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
|
580
|
+
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
|
|
581
|
+
},
|
|
582
|
+
motion: {
|
|
583
|
+
"duration-fast": "100ms",
|
|
584
|
+
"duration-normal": "200ms",
|
|
585
|
+
"duration-slow": "500ms",
|
|
586
|
+
easing: "cubic-bezier(0.4, 0, 0.2, 1)"
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function resolveConfig(config) {
|
|
590
|
+
const colors = config.colors;
|
|
591
|
+
const originalColors = {};
|
|
592
|
+
const colorFormats = {};
|
|
593
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
594
|
+
if (value !== void 0) {
|
|
595
|
+
const parsed = parseColor(value);
|
|
596
|
+
if (parsed) {
|
|
597
|
+
originalColors[key] = value;
|
|
598
|
+
if (parsed.format !== "hex") {
|
|
599
|
+
colorFormats[key] = parsed.format;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (config["colors-dark"]) {
|
|
605
|
+
for (const [key, value] of Object.entries(config["colors-dark"])) {
|
|
606
|
+
if (value !== void 0) {
|
|
607
|
+
const parsed = parseColor(value);
|
|
608
|
+
if (parsed) {
|
|
609
|
+
originalColors[`dark.${key}`] = value;
|
|
610
|
+
if (parsed.format !== "hex") {
|
|
611
|
+
colorFormats[`dark.${key}`] = parsed.format;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
name: config.name,
|
|
619
|
+
version: 1,
|
|
620
|
+
colors: {
|
|
621
|
+
primary: colors.primary,
|
|
622
|
+
accent: colors.accent ?? colors.primary,
|
|
623
|
+
neutral: colors.neutral ?? null,
|
|
624
|
+
// null = use Tailwind Gray verbatim
|
|
625
|
+
background: colors.background ?? DEFAULTS.colors.background,
|
|
626
|
+
surface: colors.surface ?? DEFAULTS.colors.surface,
|
|
627
|
+
success: colors.success ?? DEFAULTS.colors.success,
|
|
628
|
+
warning: colors.warning ?? DEFAULTS.colors.warning,
|
|
629
|
+
error: colors.error ?? DEFAULTS.colors.error,
|
|
630
|
+
info: colors.info ?? DEFAULTS.colors.info
|
|
631
|
+
},
|
|
632
|
+
"colors-dark": config["colors-dark"],
|
|
633
|
+
typography: {
|
|
634
|
+
scale: config.typography?.scale ?? DEFAULTS.typography.scale,
|
|
635
|
+
heading: {
|
|
636
|
+
family: config.typography?.heading?.family ?? DEFAULTS.typography.heading.family,
|
|
637
|
+
weight: config.typography?.heading?.weight ?? DEFAULTS.typography.heading.weight,
|
|
638
|
+
...config.typography?.heading?.source && { source: config.typography.heading.source },
|
|
639
|
+
...config.typography?.heading?.org && { org: config.typography.heading.org }
|
|
640
|
+
},
|
|
641
|
+
display: {
|
|
642
|
+
family: config.typography?.display?.family ?? config.typography?.heading?.family ?? DEFAULTS.typography.heading.family,
|
|
643
|
+
weight: config.typography?.display?.weight ?? 400,
|
|
644
|
+
...config.typography?.display?.source && { source: config.typography.display.source },
|
|
645
|
+
...config.typography?.display?.org && { org: config.typography.display.org }
|
|
646
|
+
},
|
|
647
|
+
body: {
|
|
648
|
+
family: config.typography?.body?.family ?? DEFAULTS.typography.body.family,
|
|
649
|
+
weight: config.typography?.body?.weight ?? DEFAULTS.typography.body.weight,
|
|
650
|
+
...config.typography?.body?.source && { source: config.typography.body.source },
|
|
651
|
+
...config.typography?.body?.org && { org: config.typography.body.org }
|
|
652
|
+
},
|
|
653
|
+
mono: {
|
|
654
|
+
family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
spacing: {
|
|
658
|
+
base: config.spacing?.base ?? DEFAULTS.spacing.base
|
|
659
|
+
},
|
|
660
|
+
radius: {
|
|
661
|
+
sm: config.radius?.sm ?? DEFAULTS.radius.sm,
|
|
662
|
+
md: config.radius?.md ?? DEFAULTS.radius.md,
|
|
663
|
+
lg: config.radius?.lg ?? DEFAULTS.radius.lg,
|
|
664
|
+
xl: config.radius?.xl ?? DEFAULTS.radius.xl,
|
|
665
|
+
pill: config.radius?.pill ?? DEFAULTS.radius.pill
|
|
666
|
+
},
|
|
667
|
+
shadows: {
|
|
668
|
+
xs: config.shadows?.xs ?? DEFAULTS.shadows.xs,
|
|
669
|
+
sm: config.shadows?.sm ?? DEFAULTS.shadows.sm,
|
|
670
|
+
md: config.shadows?.md ?? DEFAULTS.shadows.md,
|
|
671
|
+
lg: config.shadows?.lg ?? DEFAULTS.shadows.lg,
|
|
672
|
+
xl: config.shadows?.xl ?? DEFAULTS.shadows.xl
|
|
673
|
+
},
|
|
674
|
+
motion: {
|
|
675
|
+
"duration-fast": config.motion?.["duration-fast"] ?? DEFAULTS.motion["duration-fast"],
|
|
676
|
+
"duration-normal": config.motion?.["duration-normal"] ?? DEFAULTS.motion["duration-normal"],
|
|
677
|
+
"duration-slow": config.motion?.["duration-slow"] ?? DEFAULTS.motion["duration-slow"],
|
|
678
|
+
easing: config.motion?.easing ?? DEFAULTS.motion.easing
|
|
679
|
+
},
|
|
680
|
+
overrides: config.overrides,
|
|
681
|
+
originalColors,
|
|
682
|
+
colorFormats
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/semantic-map.ts
|
|
687
|
+
function isShadeRef(ref) {
|
|
688
|
+
return "role" in ref;
|
|
689
|
+
}
|
|
690
|
+
var CONFIG_BACKGROUND = "__CONFIG_BACKGROUND__";
|
|
691
|
+
var CONFIG_SURFACE = "__CONFIG_SURFACE__";
|
|
692
|
+
var CONFIG_DARK_BACKGROUND = "__CONFIG_DARK_BACKGROUND__";
|
|
693
|
+
var CONFIG_DARK_SURFACE = "__CONFIG_DARK_SURFACE__";
|
|
694
|
+
var SEMANTIC_TEXT_MAP = {
|
|
695
|
+
primary: {
|
|
696
|
+
light: { role: "neutral", shade: 900 },
|
|
697
|
+
dark: { role: "neutral", shade: 50 }
|
|
698
|
+
},
|
|
699
|
+
secondary: {
|
|
700
|
+
light: { role: "neutral", shade: 600 },
|
|
701
|
+
dark: { role: "neutral", shade: 400 }
|
|
702
|
+
},
|
|
703
|
+
tertiary: {
|
|
704
|
+
light: { role: "neutral", shade: 400 },
|
|
705
|
+
dark: { role: "neutral", shade: 500 }
|
|
706
|
+
},
|
|
707
|
+
disabled: {
|
|
708
|
+
light: { role: "neutral", shade: 300 },
|
|
709
|
+
dark: { role: "neutral", shade: 600 }
|
|
710
|
+
},
|
|
711
|
+
inverse: {
|
|
712
|
+
light: { constant: "#ffffff" },
|
|
713
|
+
dark: { role: "neutral", shade: 900 }
|
|
714
|
+
},
|
|
715
|
+
"inverse-secondary": {
|
|
716
|
+
light: { role: "neutral", shade: 200 },
|
|
717
|
+
dark: { role: "neutral", shade: 700 }
|
|
718
|
+
},
|
|
719
|
+
link: {
|
|
720
|
+
light: { role: "primary", shade: 600 },
|
|
721
|
+
dark: { role: "primary", shade: 400 }
|
|
722
|
+
},
|
|
723
|
+
"link-hover": {
|
|
724
|
+
light: { role: "primary", shade: 700 },
|
|
725
|
+
dark: { role: "primary", shade: 300 }
|
|
726
|
+
},
|
|
727
|
+
success: {
|
|
728
|
+
light: { role: "success", shade: 700 },
|
|
729
|
+
dark: { role: "success", shade: 500 }
|
|
730
|
+
},
|
|
731
|
+
warning: {
|
|
732
|
+
light: { role: "warning", shade: 700 },
|
|
733
|
+
dark: { role: "warning", shade: 500 }
|
|
734
|
+
},
|
|
735
|
+
error: {
|
|
736
|
+
light: { role: "error", shade: 700 },
|
|
737
|
+
dark: { role: "error", shade: 500 }
|
|
738
|
+
},
|
|
739
|
+
info: {
|
|
740
|
+
light: { role: "info", shade: 700 },
|
|
741
|
+
dark: { role: "info", shade: 500 }
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var SEMANTIC_SURFACE_MAP = {
|
|
745
|
+
page: {
|
|
746
|
+
light: { constant: CONFIG_BACKGROUND },
|
|
747
|
+
dark: { constant: CONFIG_DARK_BACKGROUND }
|
|
748
|
+
},
|
|
749
|
+
card: {
|
|
750
|
+
light: { constant: CONFIG_SURFACE },
|
|
751
|
+
dark: { constant: CONFIG_DARK_SURFACE }
|
|
752
|
+
},
|
|
753
|
+
subtle: {
|
|
754
|
+
light: { role: "neutral", shade: 50 },
|
|
755
|
+
dark: { role: "neutral", shade: 800 }
|
|
756
|
+
},
|
|
757
|
+
muted: {
|
|
758
|
+
light: { role: "neutral", shade: 100 },
|
|
759
|
+
dark: { role: "neutral", shade: 700 }
|
|
760
|
+
},
|
|
761
|
+
overlay: {
|
|
762
|
+
light: { role: "neutral", shade: 900 },
|
|
763
|
+
dark: { role: "neutral", shade: 950 }
|
|
764
|
+
},
|
|
765
|
+
"interactive-default": {
|
|
766
|
+
light: { constant: "#ffffff" },
|
|
767
|
+
dark: { role: "neutral", shade: 800 }
|
|
768
|
+
},
|
|
769
|
+
"interactive-hover": {
|
|
770
|
+
light: { role: "neutral", shade: 50 },
|
|
771
|
+
dark: { role: "neutral", shade: 700 }
|
|
772
|
+
},
|
|
773
|
+
"interactive-active": {
|
|
774
|
+
light: { role: "neutral", shade: 100 },
|
|
775
|
+
dark: { role: "neutral", shade: 600 }
|
|
776
|
+
},
|
|
777
|
+
"interactive-disabled": {
|
|
778
|
+
light: { role: "neutral", shade: 50 },
|
|
779
|
+
dark: { role: "neutral", shade: 800 }
|
|
780
|
+
},
|
|
781
|
+
"accent-subtle": {
|
|
782
|
+
light: { role: "primary", shade: 50 },
|
|
783
|
+
dark: { role: "primary", shade: 900 }
|
|
784
|
+
},
|
|
785
|
+
"accent-default": {
|
|
786
|
+
light: { role: "primary", shade: 500 },
|
|
787
|
+
dark: { role: "primary", shade: 500 }
|
|
788
|
+
},
|
|
789
|
+
"accent-strong": {
|
|
790
|
+
light: { role: "primary", shade: 600 },
|
|
791
|
+
dark: { role: "primary", shade: 400 }
|
|
792
|
+
},
|
|
793
|
+
"success-subtle": {
|
|
794
|
+
light: { role: "success", shade: 50 },
|
|
795
|
+
dark: { role: "success", shade: 900 }
|
|
796
|
+
},
|
|
797
|
+
"success-default": {
|
|
798
|
+
light: { role: "success", shade: 500 },
|
|
799
|
+
dark: { role: "success", shade: 500 }
|
|
800
|
+
},
|
|
801
|
+
"warning-subtle": {
|
|
802
|
+
light: { role: "warning", shade: 50 },
|
|
803
|
+
dark: { role: "warning", shade: 900 }
|
|
804
|
+
},
|
|
805
|
+
"warning-default": {
|
|
806
|
+
light: { role: "warning", shade: 500 },
|
|
807
|
+
dark: { role: "warning", shade: 500 }
|
|
808
|
+
},
|
|
809
|
+
"error-subtle": {
|
|
810
|
+
light: { role: "error", shade: 50 },
|
|
811
|
+
dark: { role: "error", shade: 900 }
|
|
812
|
+
},
|
|
813
|
+
"error-default": {
|
|
814
|
+
light: { role: "error", shade: 500 },
|
|
815
|
+
dark: { role: "error", shade: 500 }
|
|
816
|
+
},
|
|
817
|
+
"info-subtle": {
|
|
818
|
+
light: { role: "info", shade: 50 },
|
|
819
|
+
dark: { role: "info", shade: 900 }
|
|
820
|
+
},
|
|
821
|
+
"info-default": {
|
|
822
|
+
light: { role: "info", shade: 500 },
|
|
823
|
+
dark: { role: "info", shade: 500 }
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var SEMANTIC_BORDER_MAP = {
|
|
827
|
+
default: {
|
|
828
|
+
light: { role: "neutral", shade: 200 },
|
|
829
|
+
dark: { role: "neutral", shade: 700 }
|
|
830
|
+
},
|
|
831
|
+
muted: {
|
|
832
|
+
light: { role: "neutral", shade: 100 },
|
|
833
|
+
dark: { role: "neutral", shade: 800 }
|
|
834
|
+
},
|
|
835
|
+
strong: {
|
|
836
|
+
light: { role: "neutral", shade: 300 },
|
|
837
|
+
dark: { role: "neutral", shade: 600 }
|
|
838
|
+
},
|
|
839
|
+
focus: {
|
|
840
|
+
light: { role: "primary", shade: 500 },
|
|
841
|
+
dark: { role: "primary", shade: 400 }
|
|
842
|
+
},
|
|
843
|
+
disabled: {
|
|
844
|
+
light: { role: "neutral", shade: 100 },
|
|
845
|
+
dark: { role: "neutral", shade: 800 }
|
|
846
|
+
},
|
|
847
|
+
success: {
|
|
848
|
+
light: { role: "success", shade: 500 },
|
|
849
|
+
dark: { role: "success", shade: 500 }
|
|
850
|
+
},
|
|
851
|
+
warning: {
|
|
852
|
+
light: { role: "warning", shade: 500 },
|
|
853
|
+
dark: { role: "warning", shade: 500 }
|
|
854
|
+
},
|
|
855
|
+
error: {
|
|
856
|
+
light: { role: "error", shade: 500 },
|
|
857
|
+
dark: { role: "error", shade: 500 }
|
|
858
|
+
},
|
|
859
|
+
info: {
|
|
860
|
+
light: { role: "info", shade: 500 },
|
|
861
|
+
dark: { role: "info", shade: 500 }
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
var SEMANTIC_INTERACTIVE_MAP = {
|
|
865
|
+
// Primary action
|
|
866
|
+
"primary-bg": {
|
|
867
|
+
light: { role: "primary", shade: 600 },
|
|
868
|
+
dark: { role: "primary", shade: 500 }
|
|
869
|
+
},
|
|
870
|
+
"primary-bg-hover": {
|
|
871
|
+
light: { role: "primary", shade: 700 },
|
|
872
|
+
dark: { role: "primary", shade: 400 }
|
|
873
|
+
},
|
|
874
|
+
"primary-bg-active": {
|
|
875
|
+
light: { role: "primary", shade: 800 },
|
|
876
|
+
dark: { role: "primary", shade: 300 }
|
|
877
|
+
},
|
|
878
|
+
"primary-text": {
|
|
879
|
+
light: { constant: "#ffffff" },
|
|
880
|
+
dark: { constant: "#ffffff" }
|
|
881
|
+
},
|
|
882
|
+
// Secondary action
|
|
883
|
+
"secondary-bg": {
|
|
884
|
+
light: { constant: "#ffffff" },
|
|
885
|
+
dark: { role: "neutral", shade: 800 }
|
|
886
|
+
},
|
|
887
|
+
"secondary-bg-hover": {
|
|
888
|
+
light: { role: "neutral", shade: 50 },
|
|
889
|
+
dark: { role: "neutral", shade: 700 }
|
|
890
|
+
},
|
|
891
|
+
"secondary-bg-active": {
|
|
892
|
+
light: { role: "neutral", shade: 100 },
|
|
893
|
+
dark: { role: "neutral", shade: 600 }
|
|
894
|
+
},
|
|
895
|
+
"secondary-text": {
|
|
896
|
+
light: { role: "neutral", shade: 900 },
|
|
897
|
+
dark: { role: "neutral", shade: 50 }
|
|
898
|
+
},
|
|
899
|
+
"secondary-border": {
|
|
900
|
+
light: { role: "neutral", shade: 300 },
|
|
901
|
+
dark: { role: "neutral", shade: 600 }
|
|
902
|
+
},
|
|
903
|
+
// Destructive action
|
|
904
|
+
"destructive-bg": {
|
|
905
|
+
light: { role: "error", shade: 600 },
|
|
906
|
+
dark: { role: "error", shade: 500 }
|
|
907
|
+
},
|
|
908
|
+
"destructive-bg-hover": {
|
|
909
|
+
light: { role: "error", shade: 700 },
|
|
910
|
+
dark: { role: "error", shade: 600 }
|
|
911
|
+
},
|
|
912
|
+
"destructive-text": {
|
|
913
|
+
light: { constant: "#ffffff" },
|
|
914
|
+
dark: { constant: "#ffffff" }
|
|
915
|
+
},
|
|
916
|
+
// Ghost action
|
|
917
|
+
"ghost-bg": {
|
|
918
|
+
light: { constant: "#ffffff" },
|
|
919
|
+
dark: { role: "neutral", shade: 800 }
|
|
920
|
+
},
|
|
921
|
+
"ghost-bg-hover": {
|
|
922
|
+
light: { role: "neutral", shade: 100 },
|
|
923
|
+
dark: { role: "neutral", shade: 700 }
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
var SEMANTIC_MAP = {
|
|
927
|
+
text: SEMANTIC_TEXT_MAP,
|
|
928
|
+
surface: SEMANTIC_SURFACE_MAP,
|
|
929
|
+
border: SEMANTIC_BORDER_MAP,
|
|
930
|
+
interactive: SEMANTIC_INTERACTIVE_MAP
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// src/assign.ts
|
|
934
|
+
function lookupShade(primitives, role, shade) {
|
|
935
|
+
const scale = primitives[role];
|
|
936
|
+
const value = scale[shade];
|
|
937
|
+
if (!value) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`Missing shade ${shade} for role '${role}'. Status colors only have shades 50, 100, 500, 600, 700, 900.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return value;
|
|
943
|
+
}
|
|
944
|
+
function resolveRef(ref, primitives, config) {
|
|
945
|
+
if (isShadeRef(ref)) {
|
|
946
|
+
return lookupShade(primitives, ref.role, ref.shade);
|
|
947
|
+
}
|
|
948
|
+
switch (ref.constant) {
|
|
949
|
+
case CONFIG_BACKGROUND:
|
|
950
|
+
return config.colors.background;
|
|
951
|
+
case CONFIG_SURFACE:
|
|
952
|
+
return config.colors.surface;
|
|
953
|
+
case CONFIG_DARK_BACKGROUND:
|
|
954
|
+
return config["colors-dark"]?.background ?? lookupShade(primitives, "neutral", 950);
|
|
955
|
+
case CONFIG_DARK_SURFACE:
|
|
956
|
+
return config["colors-dark"]?.surface ?? lookupShade(primitives, "neutral", 900);
|
|
957
|
+
default:
|
|
958
|
+
return ref.constant;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function resolveMapping(mapping, primitives, config) {
|
|
962
|
+
return {
|
|
963
|
+
light: resolveRef(mapping.light, primitives, config),
|
|
964
|
+
dark: resolveRef(mapping.dark, primitives, config)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function assignSemanticTokens(primitives, config) {
|
|
968
|
+
const text = {};
|
|
969
|
+
const surface = {};
|
|
970
|
+
const border = {};
|
|
971
|
+
const interactive = {};
|
|
972
|
+
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.text)) {
|
|
973
|
+
text[name] = resolveMapping(mapping, primitives, config);
|
|
974
|
+
}
|
|
975
|
+
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.surface)) {
|
|
976
|
+
surface[name] = resolveMapping(mapping, primitives, config);
|
|
977
|
+
}
|
|
978
|
+
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.border)) {
|
|
979
|
+
border[name] = resolveMapping(mapping, primitives, config);
|
|
980
|
+
}
|
|
981
|
+
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.interactive)) {
|
|
982
|
+
interactive[name] = resolveMapping(mapping, primitives, config);
|
|
983
|
+
}
|
|
984
|
+
return { text, surface, border, interactive };
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/overrides.ts
|
|
988
|
+
var TOKEN_CATEGORIES = [
|
|
989
|
+
{ prefix: "text-", key: "text" },
|
|
990
|
+
{ prefix: "surface-", key: "surface" },
|
|
991
|
+
{ prefix: "border-", key: "border" },
|
|
992
|
+
{ prefix: "interactive-", key: "interactive" }
|
|
993
|
+
];
|
|
994
|
+
function findToken(key, tokens) {
|
|
995
|
+
for (const { prefix, key: groupKey } of TOKEN_CATEGORIES) {
|
|
996
|
+
if (key.startsWith(prefix)) {
|
|
997
|
+
const name = key.slice(prefix.length);
|
|
998
|
+
if (name in tokens[groupKey]) {
|
|
999
|
+
return { group: tokens[groupKey], name };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
function applyOverrides(tokens, overrides) {
|
|
1006
|
+
if (!overrides) return tokens;
|
|
1007
|
+
const result = {
|
|
1008
|
+
text: { ...tokens.text },
|
|
1009
|
+
surface: { ...tokens.surface },
|
|
1010
|
+
border: { ...tokens.border },
|
|
1011
|
+
interactive: { ...tokens.interactive }
|
|
1012
|
+
};
|
|
1013
|
+
for (const group of ["text", "surface", "border", "interactive"]) {
|
|
1014
|
+
for (const [name, value] of Object.entries(result[group])) {
|
|
1015
|
+
result[group][name] = { ...value };
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (overrides.light) {
|
|
1019
|
+
for (const [key, value] of Object.entries(overrides.light)) {
|
|
1020
|
+
const match = findToken(key, result);
|
|
1021
|
+
if (match) {
|
|
1022
|
+
match.group[match.name] = {
|
|
1023
|
+
...match.group[match.name],
|
|
1024
|
+
light: value
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (overrides.dark) {
|
|
1030
|
+
for (const [key, value] of Object.entries(overrides.dark)) {
|
|
1031
|
+
const match = findToken(key, result);
|
|
1032
|
+
if (match) {
|
|
1033
|
+
match.group[match.name] = {
|
|
1034
|
+
...match.group[match.name],
|
|
1035
|
+
dark: value
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return result;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/pipeline.ts
|
|
1044
|
+
function generatePrimitives(config) {
|
|
1045
|
+
return {
|
|
1046
|
+
primary: generateShadeScale(config.colors.primary, "primary"),
|
|
1047
|
+
accent: generateShadeScale(config.colors.accent, "accent"),
|
|
1048
|
+
neutral: config.colors.neutral === null ? TAILWIND_GRAY : generateShadeScale(config.colors.neutral, "neutral"),
|
|
1049
|
+
success: generateShadeScale(config.colors.success, "success"),
|
|
1050
|
+
warning: generateShadeScale(config.colors.warning, "warning"),
|
|
1051
|
+
error: generateShadeScale(config.colors.error, "error"),
|
|
1052
|
+
info: generateShadeScale(config.colors.info, "info")
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function parseConfig(yamlString) {
|
|
1056
|
+
const parsed = parseYaml(yamlString);
|
|
1057
|
+
const result = validateConfig(parsed);
|
|
1058
|
+
if (!result.valid) {
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`Invalid .visor.yaml:
|
|
1061
|
+
${result.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
return parsed;
|
|
1065
|
+
}
|
|
1066
|
+
function generateTheme(yamlString) {
|
|
1067
|
+
const config = parseConfig(yamlString);
|
|
1068
|
+
return generateThemeFromConfig(config);
|
|
1069
|
+
}
|
|
1070
|
+
function generateThemeFromConfig(config) {
|
|
1071
|
+
return generateThemeDataFromConfig(config).output;
|
|
1072
|
+
}
|
|
1073
|
+
function generateThemeData(yamlString) {
|
|
1074
|
+
const config = parseConfig(yamlString);
|
|
1075
|
+
return generateThemeDataFromConfig(config);
|
|
1076
|
+
}
|
|
1077
|
+
function generateThemeDataFromConfig(config) {
|
|
1078
|
+
const validation = validateConfig(config);
|
|
1079
|
+
if (!validation.valid) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`Invalid theme config:
|
|
1082
|
+
${validation.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const resolved = resolveConfig(config);
|
|
1086
|
+
const primitives = generatePrimitives(resolved);
|
|
1087
|
+
let tokens = assignSemanticTokens(primitives, resolved);
|
|
1088
|
+
tokens = applyOverrides(tokens, resolved.overrides);
|
|
1089
|
+
const output = {
|
|
1090
|
+
primitivesCss: generatePrimitivesCss(primitives, resolved),
|
|
1091
|
+
semanticCss: generateSemanticCss(tokens),
|
|
1092
|
+
lightCss: generateLightCss(tokens),
|
|
1093
|
+
darkCss: generateDarkCss(tokens),
|
|
1094
|
+
fullBundleCss: generateFullBundleCss(primitives, tokens, resolved)
|
|
1095
|
+
};
|
|
1096
|
+
return { config: resolved, primitives, tokens, output };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/export.ts
|
|
1100
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
1101
|
+
var DEFAULT_COLORS = {
|
|
1102
|
+
background: "#ffffff",
|
|
1103
|
+
surface: "#ffffff",
|
|
1104
|
+
success: "#22c55e",
|
|
1105
|
+
warning: "#f59e0b",
|
|
1106
|
+
error: "#ef4444",
|
|
1107
|
+
info: "#0ea5e9"
|
|
1108
|
+
};
|
|
1109
|
+
var DEFAULT_RADIUS = { sm: 2, md: 4, lg: 8, xl: 12, pill: 9999 };
|
|
1110
|
+
var DEFAULT_SHADOWS = {
|
|
1111
|
+
xs: "0 1px 1px 0 rgba(0, 0, 0, 0.04)",
|
|
1112
|
+
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
|
1113
|
+
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
|
|
1114
|
+
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
|
1115
|
+
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
|
|
1116
|
+
};
|
|
1117
|
+
var DEFAULT_MOTION = {
|
|
1118
|
+
"duration-fast": "100ms",
|
|
1119
|
+
"duration-normal": "200ms",
|
|
1120
|
+
"duration-slow": "500ms",
|
|
1121
|
+
easing: "cubic-bezier(0.4, 0, 0.2, 1)"
|
|
1122
|
+
};
|
|
1123
|
+
var DEFAULT_FONT_SANS2 = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
1124
|
+
var DEFAULT_FONT_MONO2 = '"SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace';
|
|
1125
|
+
function isDefaultColor(key, value) {
|
|
1126
|
+
return key in DEFAULT_COLORS && value.toLowerCase() === DEFAULT_COLORS[key];
|
|
1127
|
+
}
|
|
1128
|
+
function extractAnchorColor(primitives, role) {
|
|
1129
|
+
const anchorShade = role === "primary" || role === "accent" ? 600 : 500;
|
|
1130
|
+
const scale = primitives[role];
|
|
1131
|
+
return scale[anchorShade];
|
|
1132
|
+
}
|
|
1133
|
+
function exportTheme(primitives, config) {
|
|
1134
|
+
const output = {
|
|
1135
|
+
name: config.name,
|
|
1136
|
+
version: 1
|
|
1137
|
+
};
|
|
1138
|
+
const getOriginal = (key) => config.originalColors?.[key];
|
|
1139
|
+
const colors = {
|
|
1140
|
+
primary: getOriginal("primary") ?? extractAnchorColor(primitives, "primary")
|
|
1141
|
+
};
|
|
1142
|
+
const accentOriginal = getOriginal("accent");
|
|
1143
|
+
const accentHex = extractAnchorColor(primitives, "accent");
|
|
1144
|
+
if (accentHex.toLowerCase() !== extractAnchorColor(primitives, "primary").toLowerCase()) {
|
|
1145
|
+
colors.accent = accentOriginal ?? accentHex;
|
|
1146
|
+
}
|
|
1147
|
+
if (config.colors.neutral !== null) {
|
|
1148
|
+
colors.neutral = getOriginal("neutral") ?? extractAnchorColor(primitives, "neutral");
|
|
1149
|
+
}
|
|
1150
|
+
if (config.colors.background.toLowerCase() !== DEFAULT_COLORS.background) {
|
|
1151
|
+
colors.background = getOriginal("background") ?? config.colors.background;
|
|
1152
|
+
}
|
|
1153
|
+
if (config.colors.surface.toLowerCase() !== DEFAULT_COLORS.surface) {
|
|
1154
|
+
colors.surface = getOriginal("surface") ?? config.colors.surface;
|
|
1155
|
+
}
|
|
1156
|
+
for (const role of ["success", "warning", "error", "info"]) {
|
|
1157
|
+
const hex = extractAnchorColor(primitives, role);
|
|
1158
|
+
if (!isDefaultColor(role, hex)) {
|
|
1159
|
+
colors[role] = getOriginal(role) ?? hex;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
output.colors = colors;
|
|
1163
|
+
if (config["colors-dark"]) {
|
|
1164
|
+
const darkColors = {};
|
|
1165
|
+
for (const [key, value] of Object.entries(config["colors-dark"])) {
|
|
1166
|
+
if (value) darkColors[key] = getOriginal(`dark.${key}`) ?? value;
|
|
1167
|
+
}
|
|
1168
|
+
if (Object.keys(darkColors).length > 0) {
|
|
1169
|
+
output["colors-dark"] = darkColors;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const typo = {};
|
|
1173
|
+
if (config.typography.heading.family !== DEFAULT_FONT_SANS2) {
|
|
1174
|
+
typo.heading = { family: config.typography.heading.family };
|
|
1175
|
+
}
|
|
1176
|
+
if (config.typography.heading.weight !== 600) {
|
|
1177
|
+
typo.heading = { ...typo.heading || {}, weight: config.typography.heading.weight };
|
|
1178
|
+
}
|
|
1179
|
+
if (config.typography.body.family !== DEFAULT_FONT_SANS2) {
|
|
1180
|
+
typo.body = { family: config.typography.body.family };
|
|
1181
|
+
}
|
|
1182
|
+
if (config.typography.body.weight !== 400) {
|
|
1183
|
+
typo.body = { ...typo.body || {}, weight: config.typography.body.weight };
|
|
1184
|
+
}
|
|
1185
|
+
if (config.typography.mono.family !== DEFAULT_FONT_MONO2) {
|
|
1186
|
+
typo.mono = { family: config.typography.mono.family };
|
|
1187
|
+
}
|
|
1188
|
+
if (Object.keys(typo).length > 0) {
|
|
1189
|
+
output.typography = typo;
|
|
1190
|
+
}
|
|
1191
|
+
if (config.spacing.base !== 4) {
|
|
1192
|
+
output.spacing = { base: config.spacing.base };
|
|
1193
|
+
}
|
|
1194
|
+
const radius = {};
|
|
1195
|
+
for (const [key, defaultVal] of Object.entries(DEFAULT_RADIUS)) {
|
|
1196
|
+
const val = config.radius[key];
|
|
1197
|
+
if (val !== defaultVal) {
|
|
1198
|
+
radius[key] = val;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (Object.keys(radius).length > 0) {
|
|
1202
|
+
output.radius = radius;
|
|
1203
|
+
}
|
|
1204
|
+
const shadows = {};
|
|
1205
|
+
for (const [key, defaultVal] of Object.entries(DEFAULT_SHADOWS)) {
|
|
1206
|
+
const val = config.shadows[key];
|
|
1207
|
+
if (val !== defaultVal) {
|
|
1208
|
+
shadows[key] = val;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (Object.keys(shadows).length > 0) {
|
|
1212
|
+
output.shadows = shadows;
|
|
1213
|
+
}
|
|
1214
|
+
const motion = {};
|
|
1215
|
+
for (const [key, defaultVal] of Object.entries(DEFAULT_MOTION)) {
|
|
1216
|
+
const val = config.motion[key];
|
|
1217
|
+
if (val !== defaultVal) {
|
|
1218
|
+
motion[key] = val;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (Object.keys(motion).length > 0) {
|
|
1222
|
+
output.motion = motion;
|
|
1223
|
+
}
|
|
1224
|
+
if (config.overrides) {
|
|
1225
|
+
const overrides = {};
|
|
1226
|
+
if (config.overrides.light && Object.keys(config.overrides.light).length > 0) {
|
|
1227
|
+
overrides.light = config.overrides.light;
|
|
1228
|
+
}
|
|
1229
|
+
if (config.overrides.dark && Object.keys(config.overrides.dark).length > 0) {
|
|
1230
|
+
overrides.dark = config.overrides.dark;
|
|
1231
|
+
}
|
|
1232
|
+
if (Object.keys(overrides).length > 0) {
|
|
1233
|
+
output.overrides = overrides;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return stringifyYaml(output, { lineWidth: 0 });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/validate.ts
|
|
1240
|
+
var CONTRAST_TEXT_AA = 4.5;
|
|
1241
|
+
var CONTRAST_INTERACTIVE_AA = 3;
|
|
1242
|
+
var DELTA_E_SIMILAR_THRESHOLD = 10;
|
|
1243
|
+
var CSS_LENGTH_RE = /^-?\d+(\.\d+)?(em|rem|px|%|ex|ch|vw|vh|cm|mm|in|pt|pc)?$/;
|
|
1244
|
+
var NAMED_EASINGS = /* @__PURE__ */ new Set([
|
|
1245
|
+
"linear",
|
|
1246
|
+
"ease",
|
|
1247
|
+
"ease-in",
|
|
1248
|
+
"ease-out",
|
|
1249
|
+
"ease-in-out"
|
|
1250
|
+
]);
|
|
1251
|
+
var CUBIC_BEZIER_RE = /^cubic-bezier\(\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*\)$/;
|
|
1252
|
+
var STEPS_RE = /^steps\(\s*\d+\s*(,\s*(start|end|jump-start|jump-end|jump-none|jump-both)\s*)?\)$/;
|
|
1253
|
+
var KNOWN_SEMANTIC_TOKENS = /* @__PURE__ */ new Set([
|
|
1254
|
+
...Object.keys(SEMANTIC_TEXT_MAP).map((k) => `text-${k}`),
|
|
1255
|
+
...Object.keys(SEMANTIC_SURFACE_MAP).map((k) => `surface-${k}`),
|
|
1256
|
+
...Object.keys(SEMANTIC_BORDER_MAP).map((k) => `border-${k}`),
|
|
1257
|
+
...Object.keys(SEMANTIC_INTERACTIVE_MAP).map((k) => `interactive-${k}`)
|
|
1258
|
+
]);
|
|
1259
|
+
function issue(severity, code, message, path) {
|
|
1260
|
+
const result = { severity, code, message };
|
|
1261
|
+
if (path !== void 0) {
|
|
1262
|
+
result.path = path;
|
|
1263
|
+
}
|
|
1264
|
+
return result;
|
|
1265
|
+
}
|
|
1266
|
+
function deltaEOklch(color1, color2) {
|
|
1267
|
+
const parsed1 = parseColor(color1);
|
|
1268
|
+
const parsed2 = parseColor(color2);
|
|
1269
|
+
if (!parsed1 || !parsed2) return Infinity;
|
|
1270
|
+
const [l1, c1, h1] = rgbToOklch(...parsed1.rgb);
|
|
1271
|
+
const [l2, c2, h2] = rgbToOklch(...parsed2.rgb);
|
|
1272
|
+
const h1Rad = h1 * Math.PI / 180;
|
|
1273
|
+
const h2Rad = h2 * Math.PI / 180;
|
|
1274
|
+
const a1 = c1 * Math.cos(h1Rad);
|
|
1275
|
+
const b1 = c1 * Math.sin(h1Rad);
|
|
1276
|
+
const a2 = c2 * Math.cos(h2Rad);
|
|
1277
|
+
const b2 = c2 * Math.sin(h2Rad);
|
|
1278
|
+
const dL = (l1 - l2) * 100;
|
|
1279
|
+
const da = (a1 - a2) * 100;
|
|
1280
|
+
const db = (b1 - b2) * 100;
|
|
1281
|
+
return Math.sqrt(dL * dL + da * da + db * db);
|
|
1282
|
+
}
|
|
1283
|
+
function checkStructuralIntegrity(config, issues) {
|
|
1284
|
+
const result = validateConfig(config);
|
|
1285
|
+
if (!result.valid) {
|
|
1286
|
+
for (const msg of result.errors) {
|
|
1287
|
+
issues.push(issue("error", "STRUCTURAL", msg));
|
|
1288
|
+
}
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
function checkCompleteness(config, issues) {
|
|
1294
|
+
const colorEntries = Object.entries(config.colors);
|
|
1295
|
+
for (const [key, value] of colorEntries) {
|
|
1296
|
+
if (value !== void 0 && !isValidColor(value)) {
|
|
1297
|
+
issues.push(
|
|
1298
|
+
issue(
|
|
1299
|
+
"error",
|
|
1300
|
+
"INVALID_COLOR",
|
|
1301
|
+
`'colors.${key}' is not a valid CSS color: ${value}`,
|
|
1302
|
+
`colors.${key}`
|
|
1303
|
+
)
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const darkColors = config["colors-dark"];
|
|
1308
|
+
if (darkColors) {
|
|
1309
|
+
const darkEntries = Object.entries(darkColors);
|
|
1310
|
+
for (const [key, value] of darkEntries) {
|
|
1311
|
+
if (value !== void 0 && !isValidColor(value)) {
|
|
1312
|
+
issues.push(
|
|
1313
|
+
issue(
|
|
1314
|
+
"error",
|
|
1315
|
+
"INVALID_COLOR",
|
|
1316
|
+
`'colors-dark.${key}' is not a valid CSS color: ${value}`,
|
|
1317
|
+
`colors-dark.${key}`
|
|
1318
|
+
)
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (config.radius) {
|
|
1324
|
+
const radiusEntries = Object.entries(config.radius);
|
|
1325
|
+
for (const [key, value] of radiusEntries) {
|
|
1326
|
+
if (value !== void 0 && (typeof value !== "number" || value < 0)) {
|
|
1327
|
+
issues.push(
|
|
1328
|
+
issue(
|
|
1329
|
+
"error",
|
|
1330
|
+
"INVALID_RADIUS",
|
|
1331
|
+
`'radius.${key}' must be a non-negative number, got: ${value}`,
|
|
1332
|
+
`radius.${key}`
|
|
1333
|
+
)
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (config.shadows) {
|
|
1339
|
+
const shadowEntries = Object.entries(config.shadows);
|
|
1340
|
+
for (const [key, value] of shadowEntries) {
|
|
1341
|
+
if (value !== void 0) {
|
|
1342
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1343
|
+
issues.push(
|
|
1344
|
+
issue(
|
|
1345
|
+
"error",
|
|
1346
|
+
"INVALID_SHADOW",
|
|
1347
|
+
`'shadows.${key}' must be a non-empty CSS box-shadow string`,
|
|
1348
|
+
`shadows.${key}`
|
|
1349
|
+
)
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (config.typography) {
|
|
1356
|
+
const { heading, display: displayFont, body } = config.typography;
|
|
1357
|
+
if (heading?.weight !== void 0) {
|
|
1358
|
+
if (typeof heading.weight !== "number" || heading.weight < 100 || heading.weight > 900) {
|
|
1359
|
+
issues.push(
|
|
1360
|
+
issue(
|
|
1361
|
+
"error",
|
|
1362
|
+
"INVALID_WEIGHT",
|
|
1363
|
+
`'typography.heading.weight' must be between 100 and 900, got: ${heading.weight}`,
|
|
1364
|
+
"typography.heading.weight"
|
|
1365
|
+
)
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (displayFont?.weight !== void 0) {
|
|
1370
|
+
if (typeof displayFont.weight !== "number" || displayFont.weight < 100 || displayFont.weight > 900) {
|
|
1371
|
+
issues.push(
|
|
1372
|
+
issue(
|
|
1373
|
+
"error",
|
|
1374
|
+
"INVALID_WEIGHT",
|
|
1375
|
+
`'typography.display.weight' must be between 100 and 900, got: ${displayFont.weight}`,
|
|
1376
|
+
"typography.display.weight"
|
|
1377
|
+
)
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (body?.weight !== void 0) {
|
|
1382
|
+
if (typeof body.weight !== "number" || body.weight < 100 || body.weight > 900) {
|
|
1383
|
+
issues.push(
|
|
1384
|
+
issue(
|
|
1385
|
+
"error",
|
|
1386
|
+
"INVALID_WEIGHT",
|
|
1387
|
+
`'typography.body.weight' must be between 100 and 900, got: ${body.weight}`,
|
|
1388
|
+
"typography.body.weight"
|
|
1389
|
+
)
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (heading?.family !== void 0) {
|
|
1394
|
+
if (typeof heading.family !== "string" || heading.family.trim().length === 0) {
|
|
1395
|
+
issues.push(
|
|
1396
|
+
issue(
|
|
1397
|
+
"error",
|
|
1398
|
+
"INVALID_FONT_FAMILY",
|
|
1399
|
+
"'typography.heading.family' must be a non-empty string",
|
|
1400
|
+
"typography.heading.family"
|
|
1401
|
+
)
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (displayFont?.family !== void 0) {
|
|
1406
|
+
if (typeof displayFont.family !== "string" || displayFont.family.trim().length === 0) {
|
|
1407
|
+
issues.push(
|
|
1408
|
+
issue(
|
|
1409
|
+
"error",
|
|
1410
|
+
"INVALID_FONT_FAMILY",
|
|
1411
|
+
"'typography.display.family' must be a non-empty string",
|
|
1412
|
+
"typography.display.family"
|
|
1413
|
+
)
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (body?.family !== void 0) {
|
|
1418
|
+
if (typeof body.family !== "string" || body.family.trim().length === 0) {
|
|
1419
|
+
issues.push(
|
|
1420
|
+
issue(
|
|
1421
|
+
"error",
|
|
1422
|
+
"INVALID_FONT_FAMILY",
|
|
1423
|
+
"'typography.body.family' must be a non-empty string",
|
|
1424
|
+
"typography.body.family"
|
|
1425
|
+
)
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (config.typography.mono?.family !== void 0) {
|
|
1430
|
+
if (typeof config.typography.mono.family !== "string" || config.typography.mono.family.trim().length === 0) {
|
|
1431
|
+
issues.push(
|
|
1432
|
+
issue(
|
|
1433
|
+
"error",
|
|
1434
|
+
"INVALID_FONT_FAMILY",
|
|
1435
|
+
"'typography.mono.family' must be a non-empty string",
|
|
1436
|
+
"typography.mono.family"
|
|
1437
|
+
)
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (config.spacing?.base !== void 0) {
|
|
1443
|
+
if (typeof config.spacing.base !== "number" || config.spacing.base < 1) {
|
|
1444
|
+
issues.push(
|
|
1445
|
+
issue(
|
|
1446
|
+
"error",
|
|
1447
|
+
"INVALID_SPACING",
|
|
1448
|
+
`'spacing.base' must be >= 1, got: ${config.spacing.base}`,
|
|
1449
|
+
"spacing.base"
|
|
1450
|
+
)
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function checkTypeScaleCoherence(config, issues) {
|
|
1456
|
+
const headingWeight = config.typography?.heading?.weight;
|
|
1457
|
+
const bodyWeight = config.typography?.body?.weight;
|
|
1458
|
+
if (headingWeight !== void 0 && bodyWeight !== void 0) {
|
|
1459
|
+
if (headingWeight < bodyWeight) {
|
|
1460
|
+
issues.push(
|
|
1461
|
+
issue(
|
|
1462
|
+
"error",
|
|
1463
|
+
"TYPE_SCALE_INCOHERENT",
|
|
1464
|
+
`Heading weight (${headingWeight}) must be >= body weight (${bodyWeight})`,
|
|
1465
|
+
"typography"
|
|
1466
|
+
)
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function checkLetterSpacing(config, issues) {
|
|
1472
|
+
const ls = config.typography?.["letter-spacing"];
|
|
1473
|
+
if (!ls) return;
|
|
1474
|
+
for (const key of ["tight", "normal", "wide"]) {
|
|
1475
|
+
const value = ls[key];
|
|
1476
|
+
if (value !== void 0) {
|
|
1477
|
+
if (typeof value !== "string" || !CSS_LENGTH_RE.test(value.trim())) {
|
|
1478
|
+
issues.push(
|
|
1479
|
+
issue(
|
|
1480
|
+
"error",
|
|
1481
|
+
"INVALID_LETTER_SPACING",
|
|
1482
|
+
`'typography.letter-spacing.${key}' must be a valid CSS length (e.g., "-0.05em", "0", "0.1rem"), got: ${value}`,
|
|
1483
|
+
`typography.letter-spacing.${key}`
|
|
1484
|
+
)
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function isValidEasing(value) {
|
|
1491
|
+
const trimmed = value.trim();
|
|
1492
|
+
if (NAMED_EASINGS.has(trimmed)) return true;
|
|
1493
|
+
if (CUBIC_BEZIER_RE.test(trimmed)) return true;
|
|
1494
|
+
if (STEPS_RE.test(trimmed)) return true;
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
function checkMotionEasing(config, issues) {
|
|
1498
|
+
const easing = config.motion?.easing;
|
|
1499
|
+
if (easing === void 0) return;
|
|
1500
|
+
if (typeof easing !== "string" || !isValidEasing(easing)) {
|
|
1501
|
+
issues.push(
|
|
1502
|
+
issue(
|
|
1503
|
+
"error",
|
|
1504
|
+
"INVALID_EASING",
|
|
1505
|
+
`'motion.easing' must be a valid CSS timing function (e.g., "ease", "cubic-bezier(0.4, 0, 0.2, 1)", "steps(4, end)"), got: ${easing}`,
|
|
1506
|
+
"motion.easing"
|
|
1507
|
+
)
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
function parseDurationMs(value) {
|
|
1512
|
+
const match = /^(\d+)ms$/.exec(value);
|
|
1513
|
+
return match ? parseInt(match[1], 10) : null;
|
|
1514
|
+
}
|
|
1515
|
+
function checkMotionDurationRuntime(config, issues) {
|
|
1516
|
+
const motion = config.motion;
|
|
1517
|
+
if (!motion) return;
|
|
1518
|
+
const durationKeys = ["duration-fast", "duration-normal", "duration-slow"];
|
|
1519
|
+
const parsed = {};
|
|
1520
|
+
for (const key of durationKeys) {
|
|
1521
|
+
const value = motion[key];
|
|
1522
|
+
if (value === void 0) continue;
|
|
1523
|
+
const ms = parseDurationMs(value);
|
|
1524
|
+
if (ms === null) continue;
|
|
1525
|
+
if (ms <= 0) {
|
|
1526
|
+
issues.push(
|
|
1527
|
+
issue(
|
|
1528
|
+
"error",
|
|
1529
|
+
"INVALID_DURATION",
|
|
1530
|
+
`'motion.${key}' must be > 0ms, got: ${value}`,
|
|
1531
|
+
`motion.${key}`
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
} else if (ms > 1e4) {
|
|
1535
|
+
issues.push(
|
|
1536
|
+
issue(
|
|
1537
|
+
"error",
|
|
1538
|
+
"INVALID_DURATION",
|
|
1539
|
+
`'motion.${key}' must be <= 10000ms, got: ${value}`,
|
|
1540
|
+
`motion.${key}`
|
|
1541
|
+
)
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
parsed[key] = ms;
|
|
1545
|
+
}
|
|
1546
|
+
if (parsed["duration-fast"] !== void 0 && parsed["duration-normal"] !== void 0) {
|
|
1547
|
+
if (parsed["duration-fast"] >= parsed["duration-normal"]) {
|
|
1548
|
+
issues.push(
|
|
1549
|
+
issue(
|
|
1550
|
+
"warning",
|
|
1551
|
+
"DURATION_ORDER",
|
|
1552
|
+
`'motion.duration-fast' (${parsed["duration-fast"]}ms) should be less than 'motion.duration-normal' (${parsed["duration-normal"]}ms)`,
|
|
1553
|
+
"motion"
|
|
1554
|
+
)
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (parsed["duration-normal"] !== void 0 && parsed["duration-slow"] !== void 0) {
|
|
1559
|
+
if (parsed["duration-normal"] >= parsed["duration-slow"]) {
|
|
1560
|
+
issues.push(
|
|
1561
|
+
issue(
|
|
1562
|
+
"warning",
|
|
1563
|
+
"DURATION_ORDER",
|
|
1564
|
+
`'motion.duration-normal' (${parsed["duration-normal"]}ms) should be less than 'motion.duration-slow' (${parsed["duration-slow"]}ms)`,
|
|
1565
|
+
"motion"
|
|
1566
|
+
)
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function checkOverrides(config, issues) {
|
|
1572
|
+
if (!config.overrides) return;
|
|
1573
|
+
for (const mode of ["light", "dark"]) {
|
|
1574
|
+
const modeOverrides = config.overrides[mode];
|
|
1575
|
+
if (!modeOverrides) continue;
|
|
1576
|
+
for (const [key, value] of Object.entries(modeOverrides)) {
|
|
1577
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1578
|
+
issues.push(
|
|
1579
|
+
issue(
|
|
1580
|
+
"error",
|
|
1581
|
+
"INVALID_OVERRIDE",
|
|
1582
|
+
`'overrides.${mode}.${key}' must be a non-empty string`,
|
|
1583
|
+
`overrides.${mode}.${key}`
|
|
1584
|
+
)
|
|
1585
|
+
);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (!KNOWN_SEMANTIC_TOKENS.has(key)) {
|
|
1589
|
+
issues.push(
|
|
1590
|
+
issue(
|
|
1591
|
+
"warning",
|
|
1592
|
+
"UNKNOWN_OVERRIDE_KEY",
|
|
1593
|
+
`'overrides.${mode}.${key}' does not match any known semantic token. Valid tokens include: text-primary, surface-page, border-default, interactive-primary-bg, etc.`,
|
|
1594
|
+
`overrides.${mode}.${key}`
|
|
1595
|
+
)
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function checkResolvedCompleteness(resolved, issues) {
|
|
1602
|
+
const requiredColors = [
|
|
1603
|
+
"primary",
|
|
1604
|
+
"accent",
|
|
1605
|
+
"background",
|
|
1606
|
+
"surface",
|
|
1607
|
+
"success",
|
|
1608
|
+
"warning",
|
|
1609
|
+
"error",
|
|
1610
|
+
"info"
|
|
1611
|
+
];
|
|
1612
|
+
for (const key of requiredColors) {
|
|
1613
|
+
const value = resolved.colors[key];
|
|
1614
|
+
if (value === void 0 || value === null) {
|
|
1615
|
+
if (key === "neutral") continue;
|
|
1616
|
+
issues.push(
|
|
1617
|
+
issue("error", "INCOMPLETE_RESOLVED", `Resolved config missing 'colors.${key}'`, `colors.${key}`)
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (!resolved.typography.heading.family) {
|
|
1622
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", "Resolved config missing 'typography.heading.family'", "typography.heading.family"));
|
|
1623
|
+
}
|
|
1624
|
+
if (!resolved.typography.body.family) {
|
|
1625
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", "Resolved config missing 'typography.body.family'", "typography.body.family"));
|
|
1626
|
+
}
|
|
1627
|
+
if (!resolved.typography.mono.family) {
|
|
1628
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", "Resolved config missing 'typography.mono.family'", "typography.mono.family"));
|
|
1629
|
+
}
|
|
1630
|
+
if (resolved.spacing.base === void 0 || resolved.spacing.base === null) {
|
|
1631
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", "Resolved config missing 'spacing.base'", "spacing.base"));
|
|
1632
|
+
}
|
|
1633
|
+
const requiredRadius = ["sm", "md", "lg", "xl", "pill"];
|
|
1634
|
+
for (const key of requiredRadius) {
|
|
1635
|
+
if (resolved.radius[key] === void 0 || resolved.radius[key] === null) {
|
|
1636
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", `Resolved config missing 'radius.${key}'`, `radius.${key}`));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const requiredShadows = ["xs", "sm", "md", "lg", "xl"];
|
|
1640
|
+
for (const key of requiredShadows) {
|
|
1641
|
+
if (!resolved.shadows[key]) {
|
|
1642
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", `Resolved config missing 'shadows.${key}'`, `shadows.${key}`));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const requiredMotion = [
|
|
1646
|
+
"duration-fast",
|
|
1647
|
+
"duration-normal",
|
|
1648
|
+
"duration-slow",
|
|
1649
|
+
"easing"
|
|
1650
|
+
];
|
|
1651
|
+
for (const key of requiredMotion) {
|
|
1652
|
+
if (!resolved.motion[key]) {
|
|
1653
|
+
issues.push(issue("error", "INCOMPLETE_RESOLVED", `Resolved config missing 'motion.${key}'`, `motion.${key}`));
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function colorToRgb(color) {
|
|
1658
|
+
const parsed = parseColor(color);
|
|
1659
|
+
return parsed ? parsed.rgb : [0, 0, 0];
|
|
1660
|
+
}
|
|
1661
|
+
function checkContrastWarnings(config, issues) {
|
|
1662
|
+
const resolved = resolveConfig(config);
|
|
1663
|
+
const lightBg = resolved.colors.background;
|
|
1664
|
+
const lightSurface = resolved.colors.surface;
|
|
1665
|
+
const primary = resolved.colors.primary;
|
|
1666
|
+
const lightBgRgb = colorToRgb(lightBg);
|
|
1667
|
+
const lightSurfaceRgb = colorToRgb(lightSurface);
|
|
1668
|
+
const textDark = "#111827";
|
|
1669
|
+
const textOnBg = getContrastRatio(textDark, lightBg, lightBgRgb);
|
|
1670
|
+
if (textOnBg < CONTRAST_TEXT_AA) {
|
|
1671
|
+
issues.push(
|
|
1672
|
+
issue(
|
|
1673
|
+
"warning",
|
|
1674
|
+
"WCAG_CONTRAST",
|
|
1675
|
+
`Light mode: text-primary on background has contrast ratio ${textOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1676
|
+
"colors.background"
|
|
1677
|
+
)
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
const textOnSurface = getContrastRatio(textDark, lightSurface, lightSurfaceRgb);
|
|
1681
|
+
if (textOnSurface < CONTRAST_TEXT_AA) {
|
|
1682
|
+
issues.push(
|
|
1683
|
+
issue(
|
|
1684
|
+
"warning",
|
|
1685
|
+
"WCAG_CONTRAST",
|
|
1686
|
+
`Light mode: text-primary on surface has contrast ratio ${textOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1687
|
+
"colors.surface"
|
|
1688
|
+
)
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
const primaryOnBg = getContrastRatio(primary, lightBg, lightBgRgb);
|
|
1692
|
+
if (primaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
1693
|
+
issues.push(
|
|
1694
|
+
issue(
|
|
1695
|
+
"warning",
|
|
1696
|
+
"WCAG_CONTRAST",
|
|
1697
|
+
`Light mode: primary color on background has contrast ratio ${primaryOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_INTERACTIVE_AA}:1)`,
|
|
1698
|
+
"colors.primary"
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
const primaryOnSurface = getContrastRatio(primary, lightSurface, lightSurfaceRgb);
|
|
1703
|
+
if (primaryOnSurface < CONTRAST_INTERACTIVE_AA) {
|
|
1704
|
+
issues.push(
|
|
1705
|
+
issue(
|
|
1706
|
+
"warning",
|
|
1707
|
+
"WCAG_CONTRAST",
|
|
1708
|
+
`Light mode: primary color on surface has contrast ratio ${primaryOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_INTERACTIVE_AA}:1)`,
|
|
1709
|
+
"colors.primary"
|
|
1710
|
+
)
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
|
|
1714
|
+
const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
|
|
1715
|
+
const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
|
|
1716
|
+
const darkBgRgb = colorToRgb(darkBg);
|
|
1717
|
+
const darkSurfaceRgb = colorToRgb(darkSurface);
|
|
1718
|
+
const textLight = "#f9fafb";
|
|
1719
|
+
const textOnDarkBg = getContrastRatio(textLight, darkBg, darkBgRgb);
|
|
1720
|
+
if (textOnDarkBg < CONTRAST_TEXT_AA) {
|
|
1721
|
+
issues.push(
|
|
1722
|
+
issue(
|
|
1723
|
+
"warning",
|
|
1724
|
+
"WCAG_CONTRAST",
|
|
1725
|
+
`Dark mode: text-primary on background has contrast ratio ${textOnDarkBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1726
|
+
"colors-dark.background"
|
|
1727
|
+
)
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
const textOnDarkSurface = getContrastRatio(textLight, darkSurface, darkSurfaceRgb);
|
|
1731
|
+
if (textOnDarkSurface < CONTRAST_TEXT_AA) {
|
|
1732
|
+
issues.push(
|
|
1733
|
+
issue(
|
|
1734
|
+
"warning",
|
|
1735
|
+
"WCAG_CONTRAST",
|
|
1736
|
+
`Dark mode: text-primary on surface has contrast ratio ${textOnDarkSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1737
|
+
"colors-dark.surface"
|
|
1738
|
+
)
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const darkPrimaryOnBg = getContrastRatio(darkPrimary, darkBg, darkBgRgb);
|
|
1742
|
+
if (darkPrimaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
1743
|
+
issues.push(
|
|
1744
|
+
issue(
|
|
1745
|
+
"warning",
|
|
1746
|
+
"WCAG_CONTRAST",
|
|
1747
|
+
`Dark mode: primary color on background has contrast ratio ${darkPrimaryOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_INTERACTIVE_AA}:1)`,
|
|
1748
|
+
"colors-dark.primary"
|
|
1749
|
+
)
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
const darkPrimaryOnSurface = getContrastRatio(darkPrimary, darkSurface, darkSurfaceRgb);
|
|
1753
|
+
if (darkPrimaryOnSurface < CONTRAST_INTERACTIVE_AA) {
|
|
1754
|
+
issues.push(
|
|
1755
|
+
issue(
|
|
1756
|
+
"warning",
|
|
1757
|
+
"WCAG_CONTRAST",
|
|
1758
|
+
`Dark mode: primary color on surface has contrast ratio ${darkPrimaryOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_INTERACTIVE_AA}:1)`,
|
|
1759
|
+
"colors-dark.primary"
|
|
1760
|
+
)
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function checkColorSimilarity(config, issues) {
|
|
1765
|
+
const resolved = resolveConfig(config);
|
|
1766
|
+
const { primary, accent } = resolved.colors;
|
|
1767
|
+
if (config.colors.accent === void 0) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const dE = deltaEOklch(primary, accent);
|
|
1771
|
+
if (dE < DELTA_E_SIMILAR_THRESHOLD) {
|
|
1772
|
+
issues.push(
|
|
1773
|
+
issue(
|
|
1774
|
+
"warning",
|
|
1775
|
+
"COLOR_SIMILARITY",
|
|
1776
|
+
`Primary and accent colors are very similar (deltaE: ${dE.toFixed(1)}). Consider using more distinct colors for better visual hierarchy.`,
|
|
1777
|
+
"colors.accent"
|
|
1778
|
+
)
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function checkMissingGlowShadow(config, issues) {
|
|
1783
|
+
if (config.shadows) {
|
|
1784
|
+
const defined = Object.entries(config.shadows).filter(
|
|
1785
|
+
([, v]) => v !== void 0
|
|
1786
|
+
);
|
|
1787
|
+
if (defined.length > 0 && defined.length < 5) {
|
|
1788
|
+
const allKeys = ["xs", "sm", "md", "lg", "xl"];
|
|
1789
|
+
const missing = allKeys.filter(
|
|
1790
|
+
(k) => config.shadows?.[k] === void 0
|
|
1791
|
+
);
|
|
1792
|
+
issues.push(
|
|
1793
|
+
issue(
|
|
1794
|
+
"warning",
|
|
1795
|
+
"INCOMPLETE_SHADOWS",
|
|
1796
|
+
`Shadow scale is partially defined \u2014 missing: ${missing.join(", ")}. Consider defining the full scale for consistency.`,
|
|
1797
|
+
"shadows"
|
|
1798
|
+
)
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function checkRadiusScale(config, issues) {
|
|
1804
|
+
if (!config.radius) return;
|
|
1805
|
+
const resolved = resolveConfig(config);
|
|
1806
|
+
const { sm, md, lg, xl } = resolved.radius;
|
|
1807
|
+
if (sm > md) {
|
|
1808
|
+
issues.push(
|
|
1809
|
+
issue(
|
|
1810
|
+
"warning",
|
|
1811
|
+
"RADIUS_SCALE",
|
|
1812
|
+
`Radius scale is not monotonically increasing: sm (${sm}) > md (${md})`,
|
|
1813
|
+
"radius"
|
|
1814
|
+
)
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
if (md > lg) {
|
|
1818
|
+
issues.push(
|
|
1819
|
+
issue(
|
|
1820
|
+
"warning",
|
|
1821
|
+
"RADIUS_SCALE",
|
|
1822
|
+
`Radius scale is not monotonically increasing: md (${md}) > lg (${lg})`,
|
|
1823
|
+
"radius"
|
|
1824
|
+
)
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
if (lg > xl) {
|
|
1828
|
+
issues.push(
|
|
1829
|
+
issue(
|
|
1830
|
+
"warning",
|
|
1831
|
+
"RADIUS_SCALE",
|
|
1832
|
+
`Radius scale is not monotonically increasing: lg (${lg}) > xl (${xl})`,
|
|
1833
|
+
"radius"
|
|
1834
|
+
)
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function validate(config) {
|
|
1839
|
+
const errors = [];
|
|
1840
|
+
const warnings = [];
|
|
1841
|
+
const structurallyValid = checkStructuralIntegrity(config, errors);
|
|
1842
|
+
if (!structurallyValid) {
|
|
1843
|
+
return { valid: false, errors, warnings };
|
|
1844
|
+
}
|
|
1845
|
+
const typedConfig = config;
|
|
1846
|
+
checkCompleteness(typedConfig, errors);
|
|
1847
|
+
checkTypeScaleCoherence(typedConfig, errors);
|
|
1848
|
+
checkLetterSpacing(typedConfig, errors);
|
|
1849
|
+
checkMotionEasing(typedConfig, errors);
|
|
1850
|
+
const durationIssues = [];
|
|
1851
|
+
checkMotionDurationRuntime(typedConfig, durationIssues);
|
|
1852
|
+
for (const iss of durationIssues) {
|
|
1853
|
+
(iss.severity === "error" ? errors : warnings).push(iss);
|
|
1854
|
+
}
|
|
1855
|
+
const overrideIssues = [];
|
|
1856
|
+
checkOverrides(typedConfig, overrideIssues);
|
|
1857
|
+
for (const iss of overrideIssues) {
|
|
1858
|
+
(iss.severity === "error" ? errors : warnings).push(iss);
|
|
1859
|
+
}
|
|
1860
|
+
if (errors.length === 0) {
|
|
1861
|
+
const resolved = resolveConfig(typedConfig);
|
|
1862
|
+
checkResolvedCompleteness(resolved, errors);
|
|
1863
|
+
}
|
|
1864
|
+
if (errors.length === 0) {
|
|
1865
|
+
checkContrastWarnings(typedConfig, warnings);
|
|
1866
|
+
checkColorSimilarity(typedConfig, warnings);
|
|
1867
|
+
checkMissingGlowShadow(typedConfig, warnings);
|
|
1868
|
+
checkRadiusScale(typedConfig, warnings);
|
|
1869
|
+
}
|
|
1870
|
+
return {
|
|
1871
|
+
valid: errors.length === 0,
|
|
1872
|
+
errors,
|
|
1873
|
+
warnings
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/extract.ts
|
|
1878
|
+
var ROOT_SELECTOR_RE = /(?::root|html)/;
|
|
1879
|
+
var DARK_SELECTOR_RE = /(?:\.dark|\[data-theme=["']dark["']\]|@custom-variant\s+dark)/;
|
|
1880
|
+
function parseCSSDeclarations(css) {
|
|
1881
|
+
const declarations = [];
|
|
1882
|
+
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1883
|
+
const blocks = extractCSSBlocks(cleaned);
|
|
1884
|
+
for (const block of blocks) {
|
|
1885
|
+
const isDark = DARK_SELECTOR_RE.test(block.selector);
|
|
1886
|
+
const isRoot = ROOT_SELECTOR_RE.test(block.selector) && !isDark;
|
|
1887
|
+
const context = isDark ? "dark" : "light";
|
|
1888
|
+
if (!isRoot && !isDark && !block.selector.includes(":root")) {
|
|
1889
|
+
if (!block.body.includes("--")) continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (block.body.includes("{")) continue;
|
|
1892
|
+
const propRe = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
1893
|
+
let m;
|
|
1894
|
+
while ((m = propRe.exec(block.body)) !== null) {
|
|
1895
|
+
declarations.push({
|
|
1896
|
+
property: m[1],
|
|
1897
|
+
value: m[2].trim(),
|
|
1898
|
+
context
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return declarations;
|
|
1903
|
+
}
|
|
1904
|
+
function extractCSSBlocks(css) {
|
|
1905
|
+
const blocks = [];
|
|
1906
|
+
let i = 0;
|
|
1907
|
+
while (i < css.length) {
|
|
1908
|
+
const braceIdx = css.indexOf("{", i);
|
|
1909
|
+
if (braceIdx === -1) break;
|
|
1910
|
+
const selector = css.slice(i, braceIdx).trim();
|
|
1911
|
+
let depth = 1;
|
|
1912
|
+
let j = braceIdx + 1;
|
|
1913
|
+
while (j < css.length && depth > 0) {
|
|
1914
|
+
if (css[j] === "{") depth++;
|
|
1915
|
+
if (css[j] === "}") depth--;
|
|
1916
|
+
j++;
|
|
1917
|
+
}
|
|
1918
|
+
const body = css.slice(braceIdx + 1, j - 1);
|
|
1919
|
+
if (selector) {
|
|
1920
|
+
blocks.push({ selector, body });
|
|
1921
|
+
if (body.includes("{")) {
|
|
1922
|
+
const innerBlocks = extractCSSBlocks(body);
|
|
1923
|
+
for (const inner of innerBlocks) {
|
|
1924
|
+
const isDarkOuter = DARK_SELECTOR_RE.test(selector);
|
|
1925
|
+
if (isDarkOuter && !DARK_SELECTOR_RE.test(inner.selector)) {
|
|
1926
|
+
blocks.push({
|
|
1927
|
+
selector: `.dark ${inner.selector}`,
|
|
1928
|
+
body: inner.body
|
|
1929
|
+
});
|
|
1930
|
+
} else {
|
|
1931
|
+
blocks.push(inner);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
i = j;
|
|
1937
|
+
}
|
|
1938
|
+
return blocks;
|
|
1939
|
+
}
|
|
1940
|
+
var SEMANTIC_PREFIX_MAP = {
|
|
1941
|
+
"--text-": "text",
|
|
1942
|
+
"--surface-": "surface",
|
|
1943
|
+
"--border-": "border",
|
|
1944
|
+
"--interactive-": "interactive"
|
|
1945
|
+
};
|
|
1946
|
+
function findSemanticMapping(tokenName) {
|
|
1947
|
+
for (const [prefix, category] of Object.entries(SEMANTIC_PREFIX_MAP)) {
|
|
1948
|
+
if (tokenName.startsWith(prefix)) {
|
|
1949
|
+
const key = tokenName.slice(prefix.length);
|
|
1950
|
+
const map = SEMANTIC_MAP[category];
|
|
1951
|
+
if (key in map) {
|
|
1952
|
+
return { category, key, mapping: map[key] };
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
function isColorValue(value) {
|
|
1959
|
+
if (value.startsWith("var(")) return false;
|
|
1960
|
+
if (/^\d+(\.\d+)?(px|rem|em|%|ms|s)?$/.test(value)) return false;
|
|
1961
|
+
if (value.includes(",") && (value.includes("sans") || value.includes("mono") || value.includes("serif"))) return false;
|
|
1962
|
+
if (value.startsWith("cubic-bezier")) return false;
|
|
1963
|
+
if (/^\d+px\s/.test(value)) return false;
|
|
1964
|
+
return parseColor(value) !== null;
|
|
1965
|
+
}
|
|
1966
|
+
var ANCHOR_SHADES = {
|
|
1967
|
+
primary: 600,
|
|
1968
|
+
accent: 600,
|
|
1969
|
+
neutral: 500,
|
|
1970
|
+
success: 500,
|
|
1971
|
+
warning: 500,
|
|
1972
|
+
error: 500,
|
|
1973
|
+
info: 500
|
|
1974
|
+
};
|
|
1975
|
+
var COLOR_NAME_PATTERNS = [
|
|
1976
|
+
{ pattern: /primary/i, role: "primary", confidence: "medium" },
|
|
1977
|
+
{ pattern: /accent/i, role: "accent", confidence: "medium" },
|
|
1978
|
+
{ pattern: /neutral|gray|grey/i, role: "neutral", confidence: "medium" },
|
|
1979
|
+
{ pattern: /success|green/i, role: "success", confidence: "medium" },
|
|
1980
|
+
{ pattern: /warning|amber|yellow|orange/i, role: "warning", confidence: "medium" },
|
|
1981
|
+
{ pattern: /error|danger|red|destructive/i, role: "error", confidence: "medium" },
|
|
1982
|
+
{ pattern: /info|blue/i, role: "info", confidence: "medium" }
|
|
1983
|
+
];
|
|
1984
|
+
function inferColorRoles(declarations) {
|
|
1985
|
+
const candidates = [];
|
|
1986
|
+
const unmapped = [];
|
|
1987
|
+
for (const decl of declarations) {
|
|
1988
|
+
const parsed = parseColor(decl.value);
|
|
1989
|
+
if (!parsed) continue;
|
|
1990
|
+
const semanticMatch = findSemanticMapping(decl.property);
|
|
1991
|
+
if (semanticMatch) {
|
|
1992
|
+
const modeRef = decl.context === "light" ? semanticMatch.mapping.light : semanticMatch.mapping.dark;
|
|
1993
|
+
if (modeRef && isShadeRef(modeRef)) {
|
|
1994
|
+
const ref = modeRef;
|
|
1995
|
+
candidates.push({
|
|
1996
|
+
role: ref.role,
|
|
1997
|
+
value: decl.value,
|
|
1998
|
+
parsed,
|
|
1999
|
+
confidence: "high",
|
|
2000
|
+
reason: `Exact match: ${decl.property} maps to ${ref.role}/${ref.shade}`,
|
|
2001
|
+
shade: ref.shade
|
|
2002
|
+
});
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
let matched = false;
|
|
2007
|
+
for (const { pattern, role, confidence } of COLOR_NAME_PATTERNS) {
|
|
2008
|
+
if (pattern.test(decl.property)) {
|
|
2009
|
+
const shadeMatch = decl.property.match(/(\d{2,3})$/);
|
|
2010
|
+
const shade = shadeMatch ? parseInt(shadeMatch[1]) : void 0;
|
|
2011
|
+
candidates.push({
|
|
2012
|
+
role,
|
|
2013
|
+
value: decl.value,
|
|
2014
|
+
parsed,
|
|
2015
|
+
confidence,
|
|
2016
|
+
reason: `Naming convention: ${decl.property} matches ${role} pattern`,
|
|
2017
|
+
shade
|
|
2018
|
+
});
|
|
2019
|
+
matched = true;
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (!matched) {
|
|
2024
|
+
unmapped.push(decl);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return { candidates, unmapped };
|
|
2028
|
+
}
|
|
2029
|
+
function selectBestColors(candidates) {
|
|
2030
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
2031
|
+
for (const c of candidates) {
|
|
2032
|
+
const existing = byRole.get(c.role) ?? [];
|
|
2033
|
+
existing.push(c);
|
|
2034
|
+
byRole.set(c.role, existing);
|
|
2035
|
+
}
|
|
2036
|
+
const result = /* @__PURE__ */ new Map();
|
|
2037
|
+
for (const [role, roleCandidates] of byRole) {
|
|
2038
|
+
const anchorShade = ANCHOR_SHADES[role];
|
|
2039
|
+
const sorted = [...roleCandidates].sort((a, b) => {
|
|
2040
|
+
const confOrder = { high: 0, medium: 1, low: 2 };
|
|
2041
|
+
const confDiff = confOrder[a.confidence] - confOrder[b.confidence];
|
|
2042
|
+
if (confDiff !== 0) return confDiff;
|
|
2043
|
+
const aIsAnchor = a.shade === anchorShade ? 0 : 1;
|
|
2044
|
+
const bIsAnchor = b.shade === anchorShade ? 0 : 1;
|
|
2045
|
+
return aIsAnchor - bIsAnchor;
|
|
2046
|
+
});
|
|
2047
|
+
const best = sorted[0];
|
|
2048
|
+
result.set(role, {
|
|
2049
|
+
value: best.value,
|
|
2050
|
+
parsed: best.parsed,
|
|
2051
|
+
confidence: best.confidence,
|
|
2052
|
+
reason: best.reason
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
return result;
|
|
2056
|
+
}
|
|
2057
|
+
function parseFontFaceDeclarations(css) {
|
|
2058
|
+
const results = [];
|
|
2059
|
+
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2060
|
+
const fontFaceRe = /@font-face\s*\{([^}]+)\}/g;
|
|
2061
|
+
let match;
|
|
2062
|
+
while ((match = fontFaceRe.exec(cleaned)) !== null) {
|
|
2063
|
+
const block = match[1];
|
|
2064
|
+
const decl = {};
|
|
2065
|
+
const familyMatch = block.match(/font-family\s*:\s*([^;]+);/);
|
|
2066
|
+
if (familyMatch) {
|
|
2067
|
+
decl.family = familyMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2068
|
+
}
|
|
2069
|
+
const srcMatch = block.match(/src\s*:\s*([^;]+);/);
|
|
2070
|
+
if (srcMatch) {
|
|
2071
|
+
decl.src = srcMatch[1].trim();
|
|
2072
|
+
}
|
|
2073
|
+
const weightMatch = block.match(/font-weight\s*:\s*([^;]+);/);
|
|
2074
|
+
if (weightMatch) {
|
|
2075
|
+
decl.weight = weightMatch[1].trim();
|
|
2076
|
+
}
|
|
2077
|
+
const styleMatch = block.match(/font-style\s*:\s*([^;]+);/);
|
|
2078
|
+
if (styleMatch) {
|
|
2079
|
+
decl.style = styleMatch[1].trim();
|
|
2080
|
+
}
|
|
2081
|
+
if (decl.family) {
|
|
2082
|
+
results.push(decl);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2086
|
+
return results.filter((d) => {
|
|
2087
|
+
const key = d.family.toLowerCase();
|
|
2088
|
+
if (seen.has(key)) return false;
|
|
2089
|
+
seen.add(key);
|
|
2090
|
+
return true;
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
function extractTypography(declarations, fontFaces) {
|
|
2094
|
+
const result = {};
|
|
2095
|
+
for (const decl of declarations) {
|
|
2096
|
+
const prop = decl.property;
|
|
2097
|
+
const val = decl.value;
|
|
2098
|
+
if (prop.includes("font-heading") || prop === "--font-family-heading") {
|
|
2099
|
+
result.heading = { ...result.heading, family: cleanFontValue(val) };
|
|
2100
|
+
} else if (prop.includes("font-display") || prop === "--font-family-display") {
|
|
2101
|
+
result.display = { ...result.display, family: cleanFontValue(val) };
|
|
2102
|
+
} else if (prop.includes("font-body") || prop.includes("font-sans") || prop === "--font-family-body") {
|
|
2103
|
+
result.body = { ...result.body, family: cleanFontValue(val) };
|
|
2104
|
+
} else if (prop.includes("font-mono") || prop.includes("font-code") || prop === "--font-family-mono") {
|
|
2105
|
+
result.mono = { family: cleanFontValue(val) };
|
|
2106
|
+
}
|
|
2107
|
+
if (prop.includes("weight-heading") || prop === "--font-weight-heading") {
|
|
2108
|
+
const weight = parseInt(val);
|
|
2109
|
+
if (!isNaN(weight)) result.heading = { ...result.heading, weight };
|
|
2110
|
+
} else if (prop.includes("weight-display") || prop === "--font-weight-display") {
|
|
2111
|
+
const weight = parseInt(val);
|
|
2112
|
+
if (!isNaN(weight)) result.display = { ...result.display, weight };
|
|
2113
|
+
} else if (prop.includes("weight-body") || prop === "--font-weight-body") {
|
|
2114
|
+
const weight = parseInt(val);
|
|
2115
|
+
if (!isNaN(weight)) result.body = { ...result.body, weight };
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
if (fontFaces && fontFaces.length > 0) {
|
|
2119
|
+
const MONO_INDICATORS = /mono|code|console|courier/i;
|
|
2120
|
+
for (const ff of fontFaces) {
|
|
2121
|
+
if (MONO_INDICATORS.test(ff.family)) {
|
|
2122
|
+
if (!result.mono?.family) {
|
|
2123
|
+
result.mono = { family: ff.family };
|
|
2124
|
+
}
|
|
2125
|
+
} else {
|
|
2126
|
+
if (!result.heading?.family) {
|
|
2127
|
+
result.heading = { ...result.heading, family: ff.family };
|
|
2128
|
+
}
|
|
2129
|
+
if (!result.body?.family) {
|
|
2130
|
+
result.body = { ...result.body, family: ff.family };
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return result;
|
|
2136
|
+
}
|
|
2137
|
+
var FALLBACK_FAMILIES = /* @__PURE__ */ new Set([
|
|
2138
|
+
"sans-serif",
|
|
2139
|
+
"serif",
|
|
2140
|
+
"monospace",
|
|
2141
|
+
"cursive",
|
|
2142
|
+
"fantasy",
|
|
2143
|
+
"system-ui",
|
|
2144
|
+
"ui-sans-serif",
|
|
2145
|
+
"ui-serif",
|
|
2146
|
+
"ui-monospace",
|
|
2147
|
+
"ui-rounded"
|
|
2148
|
+
]);
|
|
2149
|
+
function cleanFontValue(val) {
|
|
2150
|
+
if (val.startsWith("var(")) return val;
|
|
2151
|
+
const parts = val.split(",").map((p) => p.trim());
|
|
2152
|
+
for (const part of parts) {
|
|
2153
|
+
const cleaned = part.replace(/["']/g, "").trim();
|
|
2154
|
+
if (FALLBACK_FAMILIES.has(cleaned.toLowerCase())) continue;
|
|
2155
|
+
if (!cleaned) continue;
|
|
2156
|
+
return cleaned;
|
|
2157
|
+
}
|
|
2158
|
+
return val.replace(/["']/g, "").trim();
|
|
2159
|
+
}
|
|
2160
|
+
function extractSpacing(declarations) {
|
|
2161
|
+
for (const decl of declarations) {
|
|
2162
|
+
if (decl.property === "--spacing-base" || decl.property === "--spacing-unit") {
|
|
2163
|
+
const val = parseInt(decl.value);
|
|
2164
|
+
if (!isNaN(val)) return { base: val };
|
|
2165
|
+
}
|
|
2166
|
+
if (decl.property === "--spacing-1") {
|
|
2167
|
+
const val = parseInt(decl.value);
|
|
2168
|
+
if (!isNaN(val)) return { base: val };
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return void 0;
|
|
2172
|
+
}
|
|
2173
|
+
function extractRadius(declarations) {
|
|
2174
|
+
const result = {};
|
|
2175
|
+
const radiusMap = {
|
|
2176
|
+
"--radius-sm": "sm",
|
|
2177
|
+
"--radius-md": "md",
|
|
2178
|
+
"--radius-lg": "lg",
|
|
2179
|
+
"--radius-xl": "xl",
|
|
2180
|
+
"--radius-pill": "pill",
|
|
2181
|
+
"--border-radius-sm": "sm",
|
|
2182
|
+
"--border-radius-md": "md",
|
|
2183
|
+
"--border-radius-lg": "lg",
|
|
2184
|
+
"--border-radius-xl": "xl",
|
|
2185
|
+
"--border-radius-pill": "pill"
|
|
2186
|
+
};
|
|
2187
|
+
for (const decl of declarations) {
|
|
2188
|
+
const key = radiusMap[decl.property];
|
|
2189
|
+
if (key) {
|
|
2190
|
+
const val = parseFloat(decl.value);
|
|
2191
|
+
if (!isNaN(val)) result[key] = val;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
2195
|
+
}
|
|
2196
|
+
function extractShadows(declarations) {
|
|
2197
|
+
const result = {};
|
|
2198
|
+
const shadowMap = {
|
|
2199
|
+
"--shadow-xs": "xs",
|
|
2200
|
+
"--shadow-sm": "sm",
|
|
2201
|
+
"--shadow-md": "md",
|
|
2202
|
+
"--shadow-lg": "lg",
|
|
2203
|
+
"--shadow-xl": "xl"
|
|
2204
|
+
};
|
|
2205
|
+
for (const decl of declarations) {
|
|
2206
|
+
const key = shadowMap[decl.property];
|
|
2207
|
+
if (key) {
|
|
2208
|
+
result[key] = decl.value;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
2212
|
+
}
|
|
2213
|
+
function extractMotion(declarations) {
|
|
2214
|
+
const result = {};
|
|
2215
|
+
const motionMap = {
|
|
2216
|
+
"--motion-duration-fast": "duration-fast",
|
|
2217
|
+
"--motion-duration-normal": "duration-normal",
|
|
2218
|
+
"--motion-duration-slow": "duration-slow",
|
|
2219
|
+
"--motion-easing": "easing",
|
|
2220
|
+
"--duration-fast": "duration-fast",
|
|
2221
|
+
"--duration-normal": "duration-normal",
|
|
2222
|
+
"--duration-slow": "duration-slow",
|
|
2223
|
+
"--easing-default": "easing",
|
|
2224
|
+
"--transition-duration-fast": "duration-fast",
|
|
2225
|
+
"--transition-duration-normal": "duration-normal",
|
|
2226
|
+
"--transition-duration-slow": "duration-slow"
|
|
2227
|
+
};
|
|
2228
|
+
for (const decl of declarations) {
|
|
2229
|
+
const key = motionMap[decl.property];
|
|
2230
|
+
if (key) {
|
|
2231
|
+
result[key] = decl.value;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
2235
|
+
}
|
|
2236
|
+
function extractBackgroundSurface(declarations) {
|
|
2237
|
+
const result = {
|
|
2238
|
+
light: {},
|
|
2239
|
+
dark: {}
|
|
2240
|
+
};
|
|
2241
|
+
for (const decl of declarations) {
|
|
2242
|
+
const parsed = parseColor(decl.value);
|
|
2243
|
+
if (!parsed) continue;
|
|
2244
|
+
if (decl.property === "--surface-page" || decl.property === "--background" || decl.property === "--bg" || decl.property === "--color-background") {
|
|
2245
|
+
if (decl.context === "dark") {
|
|
2246
|
+
result.dark.background = decl.value;
|
|
2247
|
+
} else {
|
|
2248
|
+
result.light.background = decl.value;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (decl.property === "--surface-card" || decl.property === "--surface" || decl.property === "--color-surface") {
|
|
2252
|
+
if (decl.context === "dark") {
|
|
2253
|
+
result.dark.surface = decl.value;
|
|
2254
|
+
} else {
|
|
2255
|
+
result.light.surface = decl.value;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return result;
|
|
2260
|
+
}
|
|
2261
|
+
function extractFromCSS(files, name = "extracted-theme") {
|
|
2262
|
+
const warnings = [];
|
|
2263
|
+
const allDeclarations = [];
|
|
2264
|
+
const allFontFaces = [];
|
|
2265
|
+
for (const file of files) {
|
|
2266
|
+
try {
|
|
2267
|
+
const decls = parseCSSDeclarations(file.content);
|
|
2268
|
+
allDeclarations.push(...decls);
|
|
2269
|
+
const fontFaces = parseFontFaceDeclarations(file.content);
|
|
2270
|
+
allFontFaces.push(...fontFaces);
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
warnings.push(`Failed to parse ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (allDeclarations.length === 0) {
|
|
2276
|
+
warnings.push("No CSS custom properties found in any scanned files.");
|
|
2277
|
+
return {
|
|
2278
|
+
config: { name, version: 1, colors: { primary: "#6366f1" } },
|
|
2279
|
+
tokens: [],
|
|
2280
|
+
unmapped: [],
|
|
2281
|
+
warnings
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
const colorDeclarations = allDeclarations.filter((d) => isColorValue(d.value));
|
|
2285
|
+
const { candidates, unmapped: unmappedDecls } = inferColorRoles(colorDeclarations);
|
|
2286
|
+
const bestColors = selectBestColors(candidates);
|
|
2287
|
+
const typography = extractTypography(allDeclarations, allFontFaces);
|
|
2288
|
+
const spacing = extractSpacing(allDeclarations);
|
|
2289
|
+
const radius = extractRadius(allDeclarations);
|
|
2290
|
+
const shadows = extractShadows(allDeclarations);
|
|
2291
|
+
const motion = extractMotion(allDeclarations);
|
|
2292
|
+
const bgSurface = extractBackgroundSurface(allDeclarations);
|
|
2293
|
+
const tokens = [];
|
|
2294
|
+
for (const candidate of candidates) {
|
|
2295
|
+
tokens.push({
|
|
2296
|
+
name: `colors.${candidate.role}`,
|
|
2297
|
+
value: candidate.value,
|
|
2298
|
+
context: "light",
|
|
2299
|
+
confidence: candidate.confidence,
|
|
2300
|
+
reason: candidate.reason
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
const config = {
|
|
2304
|
+
name,
|
|
2305
|
+
version: 1,
|
|
2306
|
+
colors: {
|
|
2307
|
+
primary: bestColors.get("primary")?.value ?? "#6366f1"
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
const optionalRoles = ["accent", "neutral", "success", "warning", "error", "info"];
|
|
2311
|
+
for (const role of optionalRoles) {
|
|
2312
|
+
const color = bestColors.get(role);
|
|
2313
|
+
if (color) {
|
|
2314
|
+
config.colors[role] = color.value;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
if (bgSurface.light.background) config.colors.background = bgSurface.light.background;
|
|
2318
|
+
if (bgSurface.light.surface) config.colors.surface = bgSurface.light.surface;
|
|
2319
|
+
const darkColors = {};
|
|
2320
|
+
if (bgSurface.dark.background) darkColors.background = bgSurface.dark.background;
|
|
2321
|
+
if (bgSurface.dark.surface) darkColors.surface = bgSurface.dark.surface;
|
|
2322
|
+
for (const decl of colorDeclarations) {
|
|
2323
|
+
if (decl.context !== "dark") continue;
|
|
2324
|
+
for (const { pattern, role } of COLOR_NAME_PATTERNS) {
|
|
2325
|
+
if (pattern.test(decl.property)) {
|
|
2326
|
+
darkColors[role] = decl.value;
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (Object.keys(darkColors).length > 0) {
|
|
2332
|
+
config["colors-dark"] = darkColors;
|
|
2333
|
+
}
|
|
2334
|
+
if (typography.heading || typography.display || typography.body || typography.mono) {
|
|
2335
|
+
config.typography = {};
|
|
2336
|
+
if (typography.heading) config.typography.heading = typography.heading;
|
|
2337
|
+
if (typography.display) config.typography.display = typography.display;
|
|
2338
|
+
if (typography.body) config.typography.body = typography.body;
|
|
2339
|
+
if (typography.mono) config.typography.mono = typography.mono;
|
|
2340
|
+
}
|
|
2341
|
+
if (spacing) config.spacing = spacing;
|
|
2342
|
+
if (radius) config.radius = radius;
|
|
2343
|
+
if (shadows) config.shadows = shadows;
|
|
2344
|
+
if (motion) config.motion = motion;
|
|
2345
|
+
if (!bestColors.has("primary")) {
|
|
2346
|
+
warnings.push(
|
|
2347
|
+
"Could not identify a primary color. Using default (#6366f1). Review the output and set colors.primary manually."
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
const lowConfidenceTokens = tokens.filter((t) => t.confidence === "low");
|
|
2351
|
+
if (lowConfidenceTokens.length > 0) {
|
|
2352
|
+
warnings.push(
|
|
2353
|
+
`${lowConfidenceTokens.length} token(s) extracted with low confidence \u2014 review these values.`
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
const unmapped = unmappedDecls.map((d) => ({
|
|
2357
|
+
name: d.property,
|
|
2358
|
+
value: d.value,
|
|
2359
|
+
context: d.context
|
|
2360
|
+
}));
|
|
2361
|
+
return { config, tokens, unmapped, warnings };
|
|
2362
|
+
}
|
|
2363
|
+
export {
|
|
2364
|
+
SEMANTIC_MAP,
|
|
2365
|
+
TAILWIND_GRAY,
|
|
2366
|
+
VISOR_FONTS_CDN,
|
|
2367
|
+
applyOverrides,
|
|
2368
|
+
assignSemanticTokens,
|
|
2369
|
+
buildVisorFontUrl,
|
|
2370
|
+
clampToSrgb,
|
|
2371
|
+
cleanFontValue,
|
|
2372
|
+
compositeOverBackground,
|
|
2373
|
+
exportTheme,
|
|
2374
|
+
extractFromCSS,
|
|
2375
|
+
generateDarkCss,
|
|
2376
|
+
generateFullBundleCss,
|
|
2377
|
+
generateLightCss,
|
|
2378
|
+
generatePreloadLinks,
|
|
2379
|
+
generatePrimitives,
|
|
2380
|
+
generatePrimitivesCss,
|
|
2381
|
+
generateSemanticCss,
|
|
2382
|
+
generateShadeScale,
|
|
2383
|
+
generateStylesheetLinks,
|
|
2384
|
+
generateTheme,
|
|
2385
|
+
generateThemeData,
|
|
2386
|
+
generateThemeDataFromConfig,
|
|
2387
|
+
generateThemeFromConfig,
|
|
2388
|
+
getContrastRatio,
|
|
2389
|
+
googleFontsCatalog,
|
|
2390
|
+
hexToOklch,
|
|
2391
|
+
hexToRgb,
|
|
2392
|
+
isValidColor,
|
|
2393
|
+
isValidHex,
|
|
2394
|
+
isVisorThemeConfig,
|
|
2395
|
+
lookupGoogleFont,
|
|
2396
|
+
normalizeHex,
|
|
2397
|
+
oklchToHex,
|
|
2398
|
+
parseCSSDeclarations,
|
|
2399
|
+
parseColor,
|
|
2400
|
+
parseConfig,
|
|
2401
|
+
parseFontFaceDeclarations,
|
|
2402
|
+
parseHex,
|
|
2403
|
+
parseHsla,
|
|
2404
|
+
parseOklch,
|
|
2405
|
+
parseRgba,
|
|
2406
|
+
resolveConfig,
|
|
2407
|
+
resolveFont,
|
|
2408
|
+
resolveThemeFonts,
|
|
2409
|
+
rgbToHex,
|
|
2410
|
+
serializeColor,
|
|
2411
|
+
validate,
|
|
2412
|
+
validateConfig,
|
|
2413
|
+
visor_theme_schema_default as visorThemeSchema
|
|
2414
|
+
};
|