@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.
@@ -0,0 +1,1399 @@
1
+ // src/fonts/google-fonts-catalog.ts
2
+ var googleFontsCatalog = [
3
+ { family: "Roboto", weights: [100, 300, 400, 500, 700, 900], styles: ["italic", "normal"], category: "sans-serif" },
4
+ { family: "Open Sans", weights: [300, 400, 500, 600, 700, 800], styles: ["italic", "normal"], category: "sans-serif" },
5
+ { family: "Noto Sans", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
6
+ { family: "Lato", weights: [100, 300, 400, 700, 900], styles: ["italic", "normal"], category: "sans-serif" },
7
+ { family: "Montserrat", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
8
+ { family: "Poppins", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
9
+ { family: "Roboto Condensed", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
10
+ { family: "Source Sans 3", weights: [200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
11
+ { family: "Inter", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
12
+ { family: "Oswald", weights: [200, 300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
13
+ { family: "Roboto Mono", weights: [100, 200, 300, 400, 500, 600, 700], styles: ["italic", "normal"], category: "monospace" },
14
+ { family: "Raleway", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
15
+ { family: "Nunito", weights: [200, 300, 400, 500, 600, 700, 800, 900, 1e3], styles: ["italic", "normal"], category: "sans-serif" },
16
+ { family: "Nunito Sans", weights: [200, 300, 400, 500, 600, 700, 800, 900, 1e3], styles: ["italic", "normal"], category: "sans-serif" },
17
+ { family: "Ubuntu", weights: [300, 400, 500, 700], styles: ["italic", "normal"], category: "sans-serif" },
18
+ { family: "Rubik", weights: [300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
19
+ { family: "Playfair Display", weights: [400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "serif" },
20
+ { family: "Noto Sans JP", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
21
+ { family: "PT Sans", weights: [400, 700], styles: ["italic", "normal"], category: "sans-serif" },
22
+ { family: "Merriweather", weights: [300, 400, 700, 900], styles: ["italic", "normal"], category: "serif" },
23
+ { family: "Roboto Slab", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "serif" },
24
+ { family: "Noto Sans KR", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
25
+ { family: "Kanit", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
26
+ { family: "Work Sans", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
27
+ { family: "Lora", weights: [400, 500, 600, 700], styles: ["italic", "normal"], category: "serif" },
28
+ { family: "Fira Sans", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
29
+ { family: "Noto Serif", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "serif" },
30
+ { family: "Quicksand", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
31
+ { family: "Barlow", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
32
+ { family: "Mulish", weights: [200, 300, 400, 500, 600, 700, 800, 900, 1e3], styles: ["italic", "normal"], category: "sans-serif" },
33
+ { family: "IBM Plex Sans", weights: [100, 200, 300, 400, 500, 600, 700], styles: ["italic", "normal"], category: "sans-serif" },
34
+ { family: "Manrope", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["normal"], category: "sans-serif" },
35
+ { family: "PT Serif", weights: [400, 700], styles: ["italic", "normal"], category: "serif" },
36
+ { family: "Karla", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["italic", "normal"], category: "sans-serif" },
37
+ { family: "Heebo", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
38
+ { family: "Noto Sans TC", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
39
+ { family: "Libre Franklin", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
40
+ { family: "Josefin Sans", weights: [100, 200, 300, 400, 500, 600, 700], styles: ["italic", "normal"], category: "sans-serif" },
41
+ { family: "Hind", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
42
+ { family: "Inconsolata", weights: [200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "monospace" },
43
+ { family: "DM Sans", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900, 1e3], styles: ["italic", "normal"], category: "sans-serif" },
44
+ { family: "Libre Baskerville", weights: [400, 700], styles: ["italic", "normal"], category: "serif" },
45
+ { family: "Source Code Pro", weights: [200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "monospace" },
46
+ { family: "Mukta", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["normal"], category: "sans-serif" },
47
+ { family: "Oxygen", weights: [300, 400, 700], styles: ["normal"], category: "sans-serif" },
48
+ { family: "Cabin", weights: [400, 500, 600, 700], styles: ["italic", "normal"], category: "sans-serif" },
49
+ { family: "Source Serif 4", weights: [200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "serif" },
50
+ { family: "Bitter", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "serif" },
51
+ { family: "Abel", weights: [400], styles: ["normal"], category: "sans-serif" },
52
+ { family: "Exo 2", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
53
+ { family: "Titillium Web", weights: [200, 300, 400, 600, 700, 900], styles: ["italic", "normal"], category: "sans-serif" },
54
+ { family: "EB Garamond", weights: [400, 500, 600, 700, 800], styles: ["italic", "normal"], category: "serif" },
55
+ { family: "Anton", weights: [400], styles: ["normal"], category: "sans-serif" },
56
+ { family: "Archivo", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
57
+ { family: "Cairo", weights: [200, 300, 400, 500, 600, 700, 800, 900, 1e3], styles: ["normal"], category: "sans-serif" },
58
+ { family: "Overpass", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
59
+ { family: "Varela Round", weights: [400], styles: ["normal"], category: "sans-serif" },
60
+ { family: "Cormorant Garamond", weights: [300, 400, 500, 600, 700], styles: ["italic", "normal"], category: "serif" },
61
+ { family: "Crimson Text", weights: [400, 600, 700], styles: ["italic", "normal"], category: "serif" },
62
+ { family: "Outfit", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
63
+ { family: "Dosis", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["normal"], category: "sans-serif" },
64
+ { family: "Comfortaa", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "display" },
65
+ { family: "Assistant", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["normal"], category: "sans-serif" },
66
+ { family: "IBM Plex Mono", weights: [100, 200, 300, 400, 500, 600, 700], styles: ["italic", "normal"], category: "monospace" },
67
+ { family: "Signika", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
68
+ { family: "Space Grotesk", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
69
+ { family: "Space Mono", weights: [400, 700], styles: ["italic", "normal"], category: "monospace" },
70
+ { family: "Catamaran", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
71
+ { family: "Arimo", weights: [400, 500, 600, 700], styles: ["italic", "normal"], category: "sans-serif" },
72
+ { family: "Figtree", weights: [300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
73
+ { family: "Teko", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
74
+ { family: "Nanum Gothic", weights: [400, 700, 800], styles: ["normal"], category: "sans-serif" },
75
+ { family: "Lexend", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
76
+ { family: "Prompt", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
77
+ { family: "Fira Code", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "monospace" },
78
+ { family: "Plus Jakarta Sans", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["italic", "normal"], category: "sans-serif" },
79
+ { family: "Barlow Condensed", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
80
+ { family: "Crimson Pro", weights: [200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "serif" },
81
+ { family: "Fjalla One", weights: [400], styles: ["normal"], category: "sans-serif" },
82
+ { family: "Maven Pro", weights: [400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
83
+ { family: "Sora", weights: [100, 200, 300, 400, 500, 600, 700, 800], styles: ["normal"], category: "sans-serif" },
84
+ { family: "Asap", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
85
+ { family: "Red Hat Display", weights: [300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
86
+ { family: "Yanone Kaffeesatz", weights: [200, 300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
87
+ { family: "Bebas Neue", weights: [400], styles: ["normal"], category: "sans-serif" },
88
+ { family: "Philosopher", weights: [400, 700], styles: ["italic", "normal"], category: "sans-serif" },
89
+ { family: "Edu Australia VIC WA NT Hand", weights: [400, 500, 600, 700], styles: ["normal"], category: "handwriting" },
90
+ { family: "Jost", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
91
+ { family: "Albert Sans", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
92
+ { family: "Spectral", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["italic", "normal"], category: "serif" },
93
+ { family: "Urbanist", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
94
+ { family: "Vollkorn", weights: [400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "serif" },
95
+ { family: "Gelasio", weights: [400, 500, 600, 700], styles: ["italic", "normal"], category: "serif" },
96
+ { family: "JetBrains Mono", weights: [100, 200, 300, 400, 500, 600, 700, 800], styles: ["italic", "normal"], category: "monospace" },
97
+ { family: "Arvo", weights: [400, 700], styles: ["italic", "normal"], category: "serif" },
98
+ { family: "Rajdhani", weights: [300, 400, 500, 600, 700], styles: ["normal"], category: "sans-serif" },
99
+ { family: "Archivo Narrow", weights: [400, 500, 600, 700], styles: ["italic", "normal"], category: "sans-serif" },
100
+ { family: "Public Sans", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
101
+ { family: "Noto Sans SC", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
102
+ { family: "Prata", weights: [400], styles: ["normal"], category: "serif" },
103
+ { family: "Questrial", weights: [400], styles: ["normal"], category: "sans-serif" },
104
+ { family: "Saira", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["italic", "normal"], category: "sans-serif" },
105
+ { family: "Red Hat Text", weights: [300, 400, 500, 600, 700], styles: ["italic", "normal"], category: "sans-serif" },
106
+ { family: "Atkinson Hyperlegible", weights: [400, 700], styles: ["italic", "normal"], category: "sans-serif" },
107
+ { family: "Bricolage Grotesque", weights: [200, 300, 400, 500, 600, 700, 800], styles: ["normal"], category: "sans-serif" },
108
+ { family: "Geist", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "sans-serif" },
109
+ { family: "Geist Mono", weights: [100, 200, 300, 400, 500, 600, 700, 800, 900], styles: ["normal"], category: "monospace" }
110
+ ];
111
+ var catalogMap = /* @__PURE__ */ new Map();
112
+ for (const entry of googleFontsCatalog) {
113
+ catalogMap.set(entry.family.toLowerCase(), entry);
114
+ }
115
+ function lookupGoogleFont(family) {
116
+ return catalogMap.get(family.toLowerCase());
117
+ }
118
+
119
+ // src/fonts/resolve.ts
120
+ var DEFAULT_WEIGHTS = [400, 700];
121
+ var DEFAULT_DISPLAY = "swap";
122
+ function buildGoogleFontsCssUrl(family, weights, italic, display) {
123
+ const encodedFamily = family.replace(/ /g, "+");
124
+ const tuples = [];
125
+ const sortedWeights = [...weights].sort((a, b) => a - b);
126
+ if (italic) {
127
+ for (const w of sortedWeights) {
128
+ tuples.push(`0,${w}`);
129
+ }
130
+ for (const w of sortedWeights) {
131
+ tuples.push(`1,${w}`);
132
+ }
133
+ return `https://fonts.googleapis.com/css2?family=${encodedFamily}:ital,wght@${tuples.join(";")}&display=${display}`;
134
+ }
135
+ const wghtValues = sortedWeights.join(";");
136
+ return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${wghtValues}&display=${display}`;
137
+ }
138
+ var VISOR_FONTS_CDN = "https://fonts.visor.design";
139
+ function buildFamilySlug(family) {
140
+ return family.toLowerCase().replace(/ /g, "-");
141
+ }
142
+ function buildFamilyPrefix(family) {
143
+ return family.replace(/ /g, "");
144
+ }
145
+ var WEIGHT_NAMES = {
146
+ 100: "Thin",
147
+ 200: "ExtraLight",
148
+ 300: "Light",
149
+ 400: "Regular",
150
+ 500: "Medium",
151
+ 600: "SemiBold",
152
+ 700: "Bold",
153
+ 800: "ExtraBold",
154
+ 900: "Black"
155
+ };
156
+ function buildVisorFontUrl(org, family, weight) {
157
+ const slug = buildFamilySlug(family);
158
+ const prefix = buildFamilyPrefix(family);
159
+ const weightName = WEIGHT_NAMES[weight] ?? `W${weight}`;
160
+ return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
161
+ }
162
+ function resolveFont(family, options = {}) {
163
+ const display = options.display ?? DEFAULT_DISPLAY;
164
+ const requestedWeights = options.weights ?? DEFAULT_WEIGHTS;
165
+ const italic = options.italic ?? false;
166
+ const explicitSource = options.source;
167
+ if (explicitSource === "visor-fonts") {
168
+ return {
169
+ family,
170
+ source: "visor-fonts",
171
+ cssUrl: null,
172
+ weights: requestedWeights,
173
+ italic,
174
+ display,
175
+ category: options.category ?? "sans-serif",
176
+ guidance: null,
177
+ org: options.org ?? null
178
+ };
179
+ }
180
+ if (explicitSource === "local") {
181
+ return {
182
+ family,
183
+ source: "local",
184
+ cssUrl: null,
185
+ weights: requestedWeights,
186
+ italic,
187
+ display,
188
+ category: options.category ?? "sans-serif",
189
+ guidance: `"${family}" is a local font. To use this font:
190
+ 1. Add the font files (.woff2) to your project's public/fonts/ directory
191
+ 2. Create @font-face declarations in your theme CSS
192
+ 3. Reference the font family in your theme's --font-display or --font-body token`,
193
+ org: null
194
+ };
195
+ }
196
+ const catalogEntry = lookupGoogleFont(family);
197
+ if (catalogEntry) {
198
+ const availableWeights = requestedWeights.filter(
199
+ (w) => catalogEntry.weights.includes(w)
200
+ );
201
+ const weights = availableWeights.length > 0 ? availableWeights : catalogEntry.weights;
202
+ const hasItalic = catalogEntry.styles.includes("italic");
203
+ const resolvedItalic = italic && hasItalic;
204
+ const cssUrl = buildGoogleFontsCssUrl(
205
+ catalogEntry.family,
206
+ weights,
207
+ resolvedItalic,
208
+ display
209
+ );
210
+ return {
211
+ family: catalogEntry.family,
212
+ // Use canonical casing from catalog
213
+ source: "google-fonts",
214
+ cssUrl,
215
+ weights,
216
+ italic: resolvedItalic,
217
+ display,
218
+ category: catalogEntry.category,
219
+ guidance: null,
220
+ org: null
221
+ };
222
+ }
223
+ return {
224
+ family,
225
+ source: "local",
226
+ cssUrl: null,
227
+ weights: requestedWeights,
228
+ italic,
229
+ display,
230
+ category: options.category ?? "sans-serif",
231
+ guidance: `"${family}" is not available on Google Fonts. To use this font:
232
+ 1. Add the font files (.woff2) to your project's public/fonts/ directory
233
+ 2. Create @font-face declarations in your theme CSS
234
+ 3. Reference the font family in your theme's --font-display or --font-body token`,
235
+ org: null
236
+ };
237
+ }
238
+
239
+ // src/fonts/preload.ts
240
+ var GOOGLE_FONTS_ORIGIN = "https://fonts.googleapis.com";
241
+ var GOOGLE_FONTS_STATIC_ORIGIN = "https://fonts.gstatic.com";
242
+ function generatePreloadLinks(resolutions, customFontPaths) {
243
+ const links = [];
244
+ const hasGoogleFonts = resolutions.some((r) => r.source === "google-fonts");
245
+ if (hasGoogleFonts) {
246
+ links.push(
247
+ `<link rel="preconnect" href="${GOOGLE_FONTS_ORIGIN}">`
248
+ );
249
+ links.push(
250
+ `<link rel="preconnect" href="${GOOGLE_FONTS_STATIC_ORIGIN}" crossorigin>`
251
+ );
252
+ for (const resolution of resolutions) {
253
+ if (resolution.source === "google-fonts" && resolution.cssUrl) {
254
+ links.push(
255
+ `<link rel="preload" as="style" href="${resolution.cssUrl}">`
256
+ );
257
+ }
258
+ }
259
+ }
260
+ const hasVisorFonts = resolutions.some((r) => r.source === "visor-fonts");
261
+ if (hasVisorFonts) {
262
+ links.push(
263
+ `<link rel="preconnect" href="${VISOR_FONTS_CDN}" crossorigin>`
264
+ );
265
+ for (const resolution of resolutions) {
266
+ if (resolution.source === "visor-fonts" && resolution.org) {
267
+ for (const weight of resolution.weights) {
268
+ const url = buildVisorFontUrl(resolution.org, resolution.family, weight);
269
+ links.push(
270
+ `<link rel="preload" as="font" type="font/woff2" href="${url}" crossorigin>`
271
+ );
272
+ }
273
+ }
274
+ }
275
+ }
276
+ if (customFontPaths) {
277
+ for (const resolution of resolutions) {
278
+ if (resolution.source === "local") {
279
+ const paths = customFontPaths.get(resolution.family);
280
+ if (paths) {
281
+ for (const path of paths) {
282
+ links.push(
283
+ `<link rel="preload" as="font" type="font/woff2" href="${path}" crossorigin>`
284
+ );
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ return links;
291
+ }
292
+ function generateStylesheetLinks(resolutions) {
293
+ const links = [];
294
+ for (const resolution of resolutions) {
295
+ if (resolution.source === "google-fonts" && resolution.cssUrl) {
296
+ links.push(
297
+ `<link rel="stylesheet" href="${resolution.cssUrl}">`
298
+ );
299
+ }
300
+ }
301
+ return links;
302
+ }
303
+
304
+ // src/fonts/pipeline.ts
305
+ function generateFontCSS(heading, displayFont, body, mono, typography) {
306
+ const lines = [];
307
+ const allSlots = [heading, displayFont, body, mono];
308
+ const googleFonts = allSlots.filter(
309
+ (r) => r !== null && r.source === "google-fonts"
310
+ );
311
+ if (googleFonts.length > 0) {
312
+ lines.push("/* Google Fonts \u2014 load these stylesheets in your HTML <head> */");
313
+ for (const font of googleFonts) {
314
+ lines.push(`/* ${font.cssUrl} */`);
315
+ }
316
+ lines.push("");
317
+ }
318
+ const visorFonts = allSlots.filter(
319
+ (r) => r !== null && r.source === "visor-fonts"
320
+ );
321
+ const seenVisorFonts = /* @__PURE__ */ new Set();
322
+ if (visorFonts.length > 0) {
323
+ lines.push("/* Visor Fonts \u2014 CDN-hosted @font-face declarations */");
324
+ for (const font of visorFonts) {
325
+ if (seenVisorFonts.has(font.family)) continue;
326
+ seenVisorFonts.add(font.family);
327
+ for (const weight of font.weights) {
328
+ const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
329
+ lines.push(`@font-face {`);
330
+ lines.push(` font-family: "${font.family}";`);
331
+ lines.push(` src: url("${url}") format("woff2");`);
332
+ lines.push(` font-weight: ${weight};`);
333
+ lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
334
+ lines.push(` font-display: ${font.display};`);
335
+ lines.push(`}`);
336
+ lines.push("");
337
+ }
338
+ }
339
+ }
340
+ const localFonts = allSlots.filter(
341
+ (r) => r !== null && r.source === "local"
342
+ );
343
+ if (localFonts.length > 0) {
344
+ lines.push(
345
+ "/* Local fonts \u2014 add your @font-face declarations below */"
346
+ );
347
+ for (const font of localFonts) {
348
+ lines.push(`/* @font-face {`);
349
+ lines.push(` font-family: "${font.family}";`);
350
+ lines.push(` src: url("/fonts/${font.family.replace(/ /g, "-").toLowerCase()}.woff2") format("woff2");`);
351
+ lines.push(` font-weight: ${font.weights.length === 1 ? font.weights[0] : `${font.weights[0]} ${font.weights[font.weights.length - 1]}`};`);
352
+ lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
353
+ lines.push(` font-display: ${font.display};`);
354
+ lines.push(` } */`);
355
+ lines.push("");
356
+ }
357
+ }
358
+ const allFonts = allSlots.filter(
359
+ (r) => r !== null
360
+ );
361
+ const seenFamilies = /* @__PURE__ */ new Set();
362
+ for (const font of allFonts) {
363
+ if (!seenFamilies.has(font.family)) {
364
+ seenFamilies.add(font.family);
365
+ lines.push(generateFallbackFontFace(font));
366
+ lines.push("");
367
+ }
368
+ }
369
+ const overrides = [];
370
+ if (heading) {
371
+ const fallbackName = `${heading.family} Fallback`;
372
+ const fallback = getFallbackStack(heading);
373
+ overrides.push(
374
+ ` --font-heading: "${heading.family}", "${fallbackName}", ${fallback};`
375
+ );
376
+ }
377
+ if (displayFont) {
378
+ const fallbackName = `${displayFont.family} Fallback`;
379
+ const fallback = getFallbackStack(displayFont);
380
+ overrides.push(
381
+ ` --font-display: "${displayFont.family}", "${fallbackName}", ${fallback};`
382
+ );
383
+ }
384
+ if (body) {
385
+ const fallbackName = `${body.family} Fallback`;
386
+ const fallback = getFallbackStack(body);
387
+ overrides.push(` --font-body: "${body.family}", "${fallbackName}", ${fallback};`);
388
+ overrides.push(` --font-sans: "${body.family}", "${fallbackName}", ${fallback};`);
389
+ }
390
+ if (mono) {
391
+ const fallbackName = `${mono.family} Fallback`;
392
+ const fallback = getFallbackStack(mono);
393
+ overrides.push(` --font-mono: "${mono.family}", "${fallbackName}", ${fallback};`);
394
+ }
395
+ if (typography.heading?.weight) {
396
+ overrides.push(
397
+ ` --weight-heading: ${typography.heading.weight};`
398
+ );
399
+ }
400
+ if (typography.display?.weight) {
401
+ overrides.push(
402
+ ` --weight-display: ${typography.display.weight};`
403
+ );
404
+ }
405
+ if (typography.body?.weight) {
406
+ overrides.push(
407
+ ` --weight-body: ${typography.body.weight};`
408
+ );
409
+ }
410
+ if (overrides.length > 0) {
411
+ lines.push(":root {");
412
+ lines.push(...overrides);
413
+ lines.push("}");
414
+ }
415
+ return lines.join("\n");
416
+ }
417
+ function getFallbackStack(resolution) {
418
+ switch (resolution.category) {
419
+ case "serif":
420
+ return 'Georgia, "Times New Roman", Times, serif';
421
+ case "monospace":
422
+ return '"SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace';
423
+ case "display":
424
+ case "handwriting":
425
+ case "sans-serif":
426
+ default:
427
+ return '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
428
+ }
429
+ }
430
+ function generateFallbackFontFace(resolution) {
431
+ const fallbackName = `${resolution.family} Fallback`;
432
+ const systemFont = getSystemFallbackFont(resolution.category);
433
+ const sizeAdjust = getSizeAdjust(resolution.category);
434
+ return [
435
+ `@font-face {`,
436
+ ` font-family: "${fallbackName}";`,
437
+ ` src: local("${systemFont}");`,
438
+ ` size-adjust: ${sizeAdjust};`,
439
+ ` ascent-override: 100%;`,
440
+ ` descent-override: 20%;`,
441
+ ` line-gap-override: 0%;`,
442
+ `}`
443
+ ].join("\n");
444
+ }
445
+ function getSystemFallbackFont(category) {
446
+ switch (category) {
447
+ case "serif":
448
+ return "Georgia";
449
+ case "monospace":
450
+ return "Courier New";
451
+ default:
452
+ return "Arial";
453
+ }
454
+ }
455
+ function getSizeAdjust(category) {
456
+ switch (category) {
457
+ case "serif":
458
+ return "105%";
459
+ case "monospace":
460
+ return "100%";
461
+ default:
462
+ return "107%";
463
+ }
464
+ }
465
+ function resolveThemeFonts(typography, options) {
466
+ const display = options?.display ?? "swap";
467
+ const warnings = [];
468
+ let headingResolution = null;
469
+ if (typography.heading?.family) {
470
+ const weights = [];
471
+ if (typography.heading.weight) weights.push(typography.heading.weight);
472
+ headingResolution = resolveFont(typography.heading.family, {
473
+ weights: weights.length > 0 ? weights : void 0,
474
+ display,
475
+ source: typography.heading.source,
476
+ org: typography.heading.org
477
+ });
478
+ if (headingResolution.guidance) {
479
+ warnings.push(headingResolution.guidance);
480
+ }
481
+ }
482
+ let bodyResolution = null;
483
+ if (typography.body?.family) {
484
+ const bodyWeights = [];
485
+ if (typography.body.weight) bodyWeights.push(typography.body.weight);
486
+ if (!bodyWeights.includes(400)) bodyWeights.push(400);
487
+ if (!bodyWeights.includes(700)) bodyWeights.push(700);
488
+ if (headingResolution && typography.body.family.toLowerCase() === headingResolution.family.toLowerCase()) {
489
+ const mergedWeights = Array.from(
490
+ /* @__PURE__ */ new Set([...headingResolution.weights, ...bodyWeights])
491
+ ).sort((a, b) => a - b);
492
+ headingResolution = resolveFont(typography.heading.family, {
493
+ weights: mergedWeights,
494
+ display,
495
+ source: typography.heading.source,
496
+ org: typography.heading.org
497
+ });
498
+ bodyResolution = headingResolution;
499
+ } else {
500
+ bodyResolution = resolveFont(typography.body.family, {
501
+ weights: bodyWeights.length > 0 ? bodyWeights : void 0,
502
+ display,
503
+ source: typography.body.source,
504
+ org: typography.body.org
505
+ });
506
+ if (bodyResolution.guidance) {
507
+ warnings.push(bodyResolution.guidance);
508
+ }
509
+ }
510
+ }
511
+ let displayResolution = null;
512
+ if (typography.display?.family) {
513
+ const displayWeights = [];
514
+ if (typography.display.weight) displayWeights.push(typography.display.weight);
515
+ if (headingResolution && typography.display.family.toLowerCase() === headingResolution.family.toLowerCase()) {
516
+ const mergedWeights = Array.from(
517
+ /* @__PURE__ */ new Set([...headingResolution.weights, ...displayWeights])
518
+ ).sort((a, b) => a - b);
519
+ headingResolution = resolveFont(typography.heading.family, {
520
+ weights: mergedWeights,
521
+ display,
522
+ source: typography.heading.source,
523
+ org: typography.heading.org
524
+ });
525
+ displayResolution = headingResolution;
526
+ } else if (bodyResolution && typography.display.family.toLowerCase() === bodyResolution.family.toLowerCase()) {
527
+ const mergedWeights = Array.from(
528
+ /* @__PURE__ */ new Set([...bodyResolution.weights, ...displayWeights])
529
+ ).sort((a, b) => a - b);
530
+ bodyResolution = resolveFont(typography.body.family, {
531
+ weights: mergedWeights,
532
+ display,
533
+ source: typography.body.source,
534
+ org: typography.body.org
535
+ });
536
+ displayResolution = bodyResolution;
537
+ } else {
538
+ displayResolution = resolveFont(typography.display.family, {
539
+ weights: displayWeights.length > 0 ? displayWeights : void 0,
540
+ display,
541
+ source: typography.display.source,
542
+ org: typography.display.org
543
+ });
544
+ if (displayResolution.guidance) {
545
+ warnings.push(displayResolution.guidance);
546
+ }
547
+ }
548
+ }
549
+ let monoResolution = null;
550
+ if (typography.mono?.family) {
551
+ const monoWeights = [];
552
+ if (typography.mono.weight) monoWeights.push(typography.mono.weight);
553
+ monoResolution = resolveFont(typography.mono.family, {
554
+ weights: monoWeights.length > 0 ? monoWeights : void 0,
555
+ display,
556
+ source: typography.mono.source,
557
+ org: typography.mono.org,
558
+ category: "monospace"
559
+ });
560
+ if (monoResolution.guidance) {
561
+ warnings.push(monoResolution.guidance);
562
+ }
563
+ }
564
+ const allResolutions = [];
565
+ if (headingResolution) allResolutions.push(headingResolution);
566
+ if (displayResolution && displayResolution !== headingResolution) {
567
+ allResolutions.push(displayResolution);
568
+ }
569
+ if (bodyResolution && bodyResolution !== headingResolution && bodyResolution !== displayResolution) {
570
+ allResolutions.push(bodyResolution);
571
+ }
572
+ if (monoResolution) allResolutions.push(monoResolution);
573
+ const preloadLinks = generatePreloadLinks(allResolutions);
574
+ const stylesheetLinks = generateStylesheetLinks(allResolutions);
575
+ const allLinks = [...preloadLinks, ...stylesheetLinks];
576
+ const css = generateFontCSS(headingResolution, displayResolution, bodyResolution, monoResolution, typography);
577
+ return {
578
+ heading: headingResolution,
579
+ display: displayResolution,
580
+ body: bodyResolution,
581
+ mono: monoResolution,
582
+ preloadLinks: allLinks,
583
+ css,
584
+ warnings
585
+ };
586
+ }
587
+
588
+ // src/color.ts
589
+ function normalizeHex(hex) {
590
+ let color = hex.replace(/^#/, "");
591
+ if (color.length === 3) {
592
+ color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
593
+ }
594
+ if (color.length === 8) {
595
+ color = color.slice(0, 6);
596
+ }
597
+ if (!/^[0-9a-fA-F]{6}$/.test(color)) {
598
+ return null;
599
+ }
600
+ return `#${color.toLowerCase()}`;
601
+ }
602
+ function isValidHex(hex) {
603
+ return normalizeHex(hex) !== null;
604
+ }
605
+ function hexToRgb(hex) {
606
+ const normalized = normalizeHex(hex);
607
+ if (!normalized) {
608
+ throw new Error(`Invalid hex color: ${hex}`);
609
+ }
610
+ return [
611
+ parseInt(normalized.slice(1, 3), 16),
612
+ parseInt(normalized.slice(3, 5), 16),
613
+ parseInt(normalized.slice(5, 7), 16)
614
+ ];
615
+ }
616
+ function rgbToHex(rgb) {
617
+ const [r, g, b] = rgb.map((c) => Math.round(Math.max(0, Math.min(255, c))));
618
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
619
+ }
620
+ function toLinear(c) {
621
+ c /= 255;
622
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
623
+ }
624
+ function fromLinear(c) {
625
+ return c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
626
+ }
627
+ function rgbToOklch(r, g, b) {
628
+ const lr = toLinear(r);
629
+ const lg = toLinear(g);
630
+ const lb = toLinear(b);
631
+ const x = 0.4124564 * lr + 0.3575761 * lg + 0.1804375 * lb;
632
+ const y = 0.2126729 * lr + 0.7151522 * lg + 0.072175 * lb;
633
+ const z = 0.0193339 * lr + 0.119192 * lg + 0.9503041 * lb;
634
+ const l_ = 0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z;
635
+ const m_ = 0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z;
636
+ const s_ = 0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z;
637
+ const lCbrt = Math.cbrt(l_);
638
+ const mCbrt = Math.cbrt(m_);
639
+ const sCbrt = Math.cbrt(s_);
640
+ const L = 0.2104542553 * lCbrt + 0.793617785 * mCbrt - 0.0040720468 * sCbrt;
641
+ const a = 1.9779984951 * lCbrt - 2.428592205 * mCbrt + 0.4505937099 * sCbrt;
642
+ const okb = 0.0259040371 * lCbrt + 0.7827717662 * mCbrt - 0.808675766 * sCbrt;
643
+ const C = Math.sqrt(a * a + okb * okb);
644
+ let H = Math.atan2(okb, a) * (180 / Math.PI);
645
+ if (H < 0) H += 360;
646
+ return [L, C, H];
647
+ }
648
+ function hexToOklch(hex) {
649
+ return rgbToOklch(...hexToRgb(hex));
650
+ }
651
+ function oklchToLinearRgb(L, C, H) {
652
+ const hRad = H * (Math.PI / 180);
653
+ const a = C * Math.cos(hRad);
654
+ const okb = C * Math.sin(hRad);
655
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * okb;
656
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * okb;
657
+ const s_ = L - 0.0894841775 * a - 1.291485548 * okb;
658
+ const l = l_ * l_ * l_;
659
+ const m = m_ * m_ * m_;
660
+ const s = s_ * s_ * s_;
661
+ const lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
662
+ const lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
663
+ const lb = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
664
+ return [lr, lg, lb];
665
+ }
666
+ function isInGamut(lr, lg, lb) {
667
+ const eps = 1e-4;
668
+ return lr >= -eps && lr <= 1 + eps && lg >= -eps && lg <= 1 + eps && lb >= -eps && lb <= 1 + eps;
669
+ }
670
+ function clampToSrgb(L, C, H) {
671
+ const [lr, lg, lb] = oklchToLinearRgb(L, C, H);
672
+ if (isInGamut(lr, lg, lb)) {
673
+ return [
674
+ Math.round(fromLinear(Math.max(0, Math.min(1, lr))) * 255),
675
+ Math.round(fromLinear(Math.max(0, Math.min(1, lg))) * 255),
676
+ Math.round(fromLinear(Math.max(0, Math.min(1, lb))) * 255)
677
+ ];
678
+ }
679
+ let lo = 0;
680
+ let hi = C;
681
+ const tolerance = 1e-3;
682
+ while (hi - lo > tolerance) {
683
+ const mid = (lo + hi) / 2;
684
+ const [r2, g2, b2] = oklchToLinearRgb(L, mid, H);
685
+ if (isInGamut(r2, g2, b2)) {
686
+ lo = mid;
687
+ } else {
688
+ hi = mid;
689
+ }
690
+ }
691
+ const [r, g, b] = oklchToLinearRgb(L, lo, H);
692
+ return [
693
+ Math.round(fromLinear(Math.max(0, Math.min(1, r))) * 255),
694
+ Math.round(fromLinear(Math.max(0, Math.min(1, g))) * 255),
695
+ Math.round(fromLinear(Math.max(0, Math.min(1, b))) * 255)
696
+ ];
697
+ }
698
+ function oklchToHex(L, C, H) {
699
+ return rgbToHex(clampToSrgb(L, C, H));
700
+ }
701
+ function getLuminance(r, g, b) {
702
+ const [rs, gs, bs] = [r, g, b].map((c) => {
703
+ c = c / 255;
704
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
705
+ });
706
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
707
+ }
708
+ function getContrastRatio(color1, color2, compositeBackground) {
709
+ const resolved1 = resolveContrastColor(color1, compositeBackground);
710
+ const resolved2 = resolveContrastColor(color2, compositeBackground);
711
+ const l1 = getLuminance(...resolved1);
712
+ const l2 = getLuminance(...resolved2);
713
+ const lighter = Math.max(l1, l2);
714
+ const darker = Math.min(l1, l2);
715
+ return (lighter + 0.05) / (darker + 0.05);
716
+ }
717
+ function resolveContrastColor(color, compositeBackground) {
718
+ if (typeof color === "string") {
719
+ const parsed = parseColor(color);
720
+ if (!parsed) {
721
+ throw new Error(`Invalid color: ${color}`);
722
+ }
723
+ if (parsed.alpha !== void 0 && parsed.alpha < 1 && compositeBackground) {
724
+ return compositeOverBackground(parsed, compositeBackground);
725
+ }
726
+ return parsed.rgb;
727
+ }
728
+ if (color.alpha !== void 0 && color.alpha < 1 && compositeBackground) {
729
+ return compositeOverBackground(color, compositeBackground);
730
+ }
731
+ return color.rgb;
732
+ }
733
+ function parseHex(str) {
734
+ const trimmed = str.trim();
735
+ const stripped = trimmed.replace(/^#/, "");
736
+ let alpha;
737
+ if (stripped.length === 8) {
738
+ alpha = parseInt(stripped.slice(6, 8), 16) / 255;
739
+ }
740
+ const normalized = normalizeHex(trimmed);
741
+ if (!normalized) return null;
742
+ return {
743
+ rgb: hexToRgb(normalized),
744
+ alpha,
745
+ format: "hex",
746
+ original: trimmed
747
+ };
748
+ }
749
+ var RGBA_RE = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01]?\.?\d*))?\s*\)$/;
750
+ function parseRgba(str) {
751
+ const trimmed = str.trim();
752
+ const m = RGBA_RE.exec(trimmed);
753
+ if (!m) return null;
754
+ const r = parseInt(m[1], 10);
755
+ const g = parseInt(m[2], 10);
756
+ const b = parseInt(m[3], 10);
757
+ if (r > 255 || g > 255 || b > 255) return null;
758
+ let alpha;
759
+ if (m[4] !== void 0) {
760
+ alpha = parseFloat(m[4]);
761
+ if (isNaN(alpha) || alpha < 0 || alpha > 1) return null;
762
+ }
763
+ return {
764
+ rgb: [r, g, b],
765
+ alpha,
766
+ format: "rgba",
767
+ original: trimmed
768
+ };
769
+ }
770
+ var HSLA_RE = /^hsla?\(\s*(\d{1,3}(?:\.\d+)?)\s*,\s*(\d{1,3}(?:\.\d+)?)%\s*,\s*(\d{1,3}(?:\.\d+)?)%\s*(?:,\s*([01]?\.?\d*))?\s*\)$/;
771
+ function hslToRgb(h, s, l) {
772
+ const sNorm = s / 100;
773
+ const lNorm = l / 100;
774
+ const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
775
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
776
+ const m = lNorm - c / 2;
777
+ let r1, g1, b1;
778
+ if (h < 60) {
779
+ [r1, g1, b1] = [c, x, 0];
780
+ } else if (h < 120) {
781
+ [r1, g1, b1] = [x, c, 0];
782
+ } else if (h < 180) {
783
+ [r1, g1, b1] = [0, c, x];
784
+ } else if (h < 240) {
785
+ [r1, g1, b1] = [0, x, c];
786
+ } else if (h < 300) {
787
+ [r1, g1, b1] = [x, 0, c];
788
+ } else {
789
+ [r1, g1, b1] = [c, 0, x];
790
+ }
791
+ return [
792
+ Math.round((r1 + m) * 255),
793
+ Math.round((g1 + m) * 255),
794
+ Math.round((b1 + m) * 255)
795
+ ];
796
+ }
797
+ function parseHsla(str) {
798
+ const trimmed = str.trim();
799
+ const m = HSLA_RE.exec(trimmed);
800
+ if (!m) return null;
801
+ const h = parseFloat(m[1]);
802
+ const s = parseFloat(m[2]);
803
+ const l = parseFloat(m[3]);
804
+ if (h > 360 || s > 100 || l > 100) return null;
805
+ let alpha;
806
+ if (m[4] !== void 0) {
807
+ alpha = parseFloat(m[4]);
808
+ if (isNaN(alpha) || alpha < 0 || alpha > 1) return null;
809
+ }
810
+ return {
811
+ rgb: hslToRgb(h, s, l),
812
+ alpha,
813
+ format: "hsla",
814
+ original: trimmed
815
+ };
816
+ }
817
+ var OKLCH_RE = /^oklch\(\s*([01]?\.?\d+)\s+([0-9]*\.?\d+)\s+([0-9]*\.?\d+)\s*(?:\/\s*([01]?\.?\d*))?\s*\)$/;
818
+ function parseOklch(str) {
819
+ const trimmed = str.trim();
820
+ const m = OKLCH_RE.exec(trimmed);
821
+ if (!m) return null;
822
+ const L = parseFloat(m[1]);
823
+ const C = parseFloat(m[2]);
824
+ const H = parseFloat(m[3]);
825
+ if (isNaN(L) || isNaN(C) || isNaN(H)) return null;
826
+ if (L < 0 || L > 1) return null;
827
+ let alpha;
828
+ if (m[4] !== void 0) {
829
+ alpha = parseFloat(m[4]);
830
+ if (isNaN(alpha) || alpha < 0 || alpha > 1) return null;
831
+ }
832
+ return {
833
+ rgb: clampToSrgb(L, C, H),
834
+ alpha,
835
+ format: "oklch",
836
+ original: trimmed
837
+ };
838
+ }
839
+ function parseColor(str) {
840
+ if (!str || typeof str !== "string") return null;
841
+ const trimmed = str.trim();
842
+ if (trimmed.startsWith("#")) {
843
+ return parseHex(trimmed);
844
+ }
845
+ if (trimmed.startsWith("rgb")) {
846
+ return parseRgba(trimmed);
847
+ }
848
+ if (trimmed.startsWith("hsl")) {
849
+ return parseHsla(trimmed);
850
+ }
851
+ if (trimmed.startsWith("oklch")) {
852
+ return parseOklch(trimmed);
853
+ }
854
+ return null;
855
+ }
856
+ function isValidColor(str) {
857
+ return parseColor(str) !== null;
858
+ }
859
+ function compositeOverBackground(color, background) {
860
+ const a = color.alpha ?? 1;
861
+ return [
862
+ Math.round(a * color.rgb[0] + (1 - a) * background[0]),
863
+ Math.round(a * color.rgb[1] + (1 - a) * background[1]),
864
+ Math.round(a * color.rgb[2] + (1 - a) * background[2])
865
+ ];
866
+ }
867
+ function rgbToRgbaString(rgb, alpha) {
868
+ if (alpha !== void 0) {
869
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
870
+ }
871
+ return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
872
+ }
873
+ function rgbToHslaString(rgb, alpha) {
874
+ const r = rgb[0] / 255;
875
+ const g = rgb[1] / 255;
876
+ const b = rgb[2] / 255;
877
+ const max = Math.max(r, g, b);
878
+ const min = Math.min(r, g, b);
879
+ const l = (max + min) / 2;
880
+ const d = max - min;
881
+ let h = 0;
882
+ let s = 0;
883
+ if (d !== 0) {
884
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
885
+ if (max === r) {
886
+ h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
887
+ } else if (max === g) {
888
+ h = ((b - r) / d + 2) * 60;
889
+ } else {
890
+ h = ((r - g) / d + 4) * 60;
891
+ }
892
+ }
893
+ const hRound = Math.round(h);
894
+ const sRound = Math.round(s * 100);
895
+ const lRound = Math.round(l * 100);
896
+ if (alpha !== void 0) {
897
+ return `hsla(${hRound}, ${sRound}%, ${lRound}%, ${alpha})`;
898
+ }
899
+ return `hsl(${hRound}, ${sRound}%, ${lRound}%)`;
900
+ }
901
+ function rgbToOklchString(rgb, alpha) {
902
+ const [L, C, H] = rgbToOklch(...rgb);
903
+ const lStr = L.toFixed(4);
904
+ const cStr = C.toFixed(4);
905
+ const hStr = H.toFixed(2);
906
+ if (alpha !== void 0) {
907
+ return `oklch(${lStr} ${cStr} ${hStr} / ${alpha})`;
908
+ }
909
+ return `oklch(${lStr} ${cStr} ${hStr})`;
910
+ }
911
+ function serializeColor(parsed) {
912
+ if (parsed.format !== "hex" && parsed.original) {
913
+ return parsed.original;
914
+ }
915
+ switch (parsed.format) {
916
+ case "hex": {
917
+ if (parsed.alpha !== void 0) {
918
+ const alphaHex = Math.round(parsed.alpha * 255).toString(16).padStart(2, "0");
919
+ return `${rgbToHex(parsed.rgb)}${alphaHex}`;
920
+ }
921
+ return rgbToHex(parsed.rgb);
922
+ }
923
+ case "rgba":
924
+ return rgbToRgbaString(parsed.rgb, parsed.alpha);
925
+ case "hsla":
926
+ return rgbToHslaString(parsed.rgb, parsed.alpha);
927
+ case "oklch":
928
+ return rgbToOklchString(parsed.rgb, parsed.alpha);
929
+ }
930
+ }
931
+
932
+ // src/shades.ts
933
+ var FULL_SHADE_STEPS = [
934
+ 50,
935
+ 100,
936
+ 200,
937
+ 300,
938
+ 400,
939
+ 500,
940
+ 600,
941
+ 700,
942
+ 800,
943
+ 900,
944
+ 950
945
+ ];
946
+ var SELECTIVE_SHADE_STEPS = [
947
+ 50,
948
+ 100,
949
+ 500,
950
+ 600,
951
+ 700,
952
+ 900
953
+ ];
954
+ var LIGHTNESS_TARGETS = {
955
+ 50: 0.97,
956
+ 100: 0.93,
957
+ 200: 0.87,
958
+ 300: 0.78,
959
+ 400: 0.65,
960
+ 500: 0.55,
961
+ 600: -1,
962
+ // placeholder — replaced by input L at anchor
963
+ 700: 0.38,
964
+ 800: 0.3,
965
+ 900: 0.22,
966
+ 950: 0.14
967
+ };
968
+ var CHROMA_MULTIPLIERS = {
969
+ 50: 0.15,
970
+ 100: 0.25,
971
+ 200: 0.45,
972
+ 300: 0.7,
973
+ 400: 0.9,
974
+ 500: 1,
975
+ 600: 1,
976
+ 700: 1,
977
+ 800: 0.85,
978
+ 900: 0.7,
979
+ 950: 0.5
980
+ };
981
+ var ANCHOR_SHADE = {
982
+ primary: 600,
983
+ accent: 600,
984
+ neutral: 500,
985
+ success: 500,
986
+ warning: 500,
987
+ error: 500,
988
+ info: 500
989
+ };
990
+ var FULL_SCALE_ROLES = ["primary", "accent", "neutral"];
991
+ var TAILWIND_GRAY = {
992
+ 50: "#f9fafb",
993
+ 100: "#f3f4f6",
994
+ 200: "#e5e7eb",
995
+ 300: "#d1d5db",
996
+ 400: "#9ca3af",
997
+ 500: "#6b7280",
998
+ 600: "#4b5563",
999
+ 700: "#374151",
1000
+ 800: "#1f2937",
1001
+ 900: "#111827",
1002
+ 950: "#030712"
1003
+ };
1004
+ function computeLightness(step, inputL, anchorShade) {
1005
+ if (step === anchorShade) {
1006
+ return inputL;
1007
+ }
1008
+ const anchorTarget = anchorShade === 600 ? inputL : LIGHTNESS_TARGETS[anchorShade];
1009
+ const stepTarget = LIGHTNESS_TARGETS[step];
1010
+ if (Math.abs(anchorTarget - inputL) < 0.01) {
1011
+ return stepTarget;
1012
+ }
1013
+ if (step < anchorShade) {
1014
+ const anchorDefaultL = anchorShade === 600 ? 0.45 : LIGHTNESS_TARGETS[anchorShade];
1015
+ const rawRange = 0.97 - anchorDefaultL;
1016
+ const newRange = 0.97 - inputL;
1017
+ if (rawRange <= 0) return stepTarget;
1018
+ const t = (stepTarget - anchorDefaultL) / rawRange;
1019
+ return inputL + t * newRange;
1020
+ } else {
1021
+ const anchorDefaultL = anchorShade === 600 ? 0.45 : LIGHTNESS_TARGETS[anchorShade];
1022
+ const rawRange = anchorDefaultL - 0.14;
1023
+ const newRange = inputL - 0.14;
1024
+ if (rawRange <= 0) return stepTarget;
1025
+ const t = (anchorDefaultL - stepTarget) / rawRange;
1026
+ return inputL - t * newRange;
1027
+ }
1028
+ }
1029
+ function generateShadeScale(color, role) {
1030
+ const parsed = parseColor(color);
1031
+ const [inputL, inputC, inputH] = parsed ? rgbToOklch(...parsed.rgb) : hexToOklch(color);
1032
+ const anchorShade = ANCHOR_SHADE[role];
1033
+ const steps = FULL_SCALE_ROLES.includes(role) ? FULL_SHADE_STEPS : SELECTIVE_SHADE_STEPS;
1034
+ const maxNeutralChroma = 0.02;
1035
+ const scale = {};
1036
+ for (const step of steps) {
1037
+ const targetL = computeLightness(step, inputL, anchorShade);
1038
+ let targetC = inputC * CHROMA_MULTIPLIERS[step];
1039
+ if (role === "neutral") {
1040
+ targetC = Math.min(targetC, maxNeutralChroma);
1041
+ }
1042
+ scale[step] = oklchToHex(targetL, targetC, inputH);
1043
+ }
1044
+ return scale;
1045
+ }
1046
+
1047
+ // src/generate-css.ts
1048
+ function header(label) {
1049
+ return [
1050
+ "/* ============================================",
1051
+ ` ${label}`,
1052
+ " Generated by @loworbitstudio/visor-theme-engine",
1053
+ " DO NOT EDIT \u2014 regenerate from .visor.yaml",
1054
+ " ============================================ */",
1055
+ ""
1056
+ ].join("\n");
1057
+ }
1058
+ function sectionComment(label) {
1059
+ return `
1060
+ /* --- ${label} --- */`;
1061
+ }
1062
+ function block(selector, declarations) {
1063
+ if (declarations.length === 0) return "";
1064
+ return [selector + " {", ...declarations.map((d) => ` ${d}`), "}", ""].join(
1065
+ "\n"
1066
+ );
1067
+ }
1068
+ var FULL_SCALE_ROLES2 = ["primary", "accent", "neutral"];
1069
+ var SELECTIVE_SCALE_ROLES = [
1070
+ "success",
1071
+ "warning",
1072
+ "error",
1073
+ "info"
1074
+ ];
1075
+ function generateColorPrimitives(primitives) {
1076
+ const decls = [];
1077
+ decls.push("--color-white: #ffffff;");
1078
+ decls.push("--color-black: #000000;");
1079
+ const allRoles = [...FULL_SCALE_ROLES2, ...SELECTIVE_SCALE_ROLES];
1080
+ for (const role of allRoles) {
1081
+ const scale = primitives[role];
1082
+ const steps = FULL_SCALE_ROLES2.includes(role) ? FULL_SHADE_STEPS : SELECTIVE_SHADE_STEPS;
1083
+ for (const step of steps) {
1084
+ const value = scale[step];
1085
+ decls.push(`--color-${role}-${step}: ${value};`);
1086
+ }
1087
+ }
1088
+ return decls.join("\n ");
1089
+ }
1090
+ function generateSpacingPrimitives(config) {
1091
+ const base = config.spacing.base;
1092
+ const multipliers = [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24];
1093
+ return multipliers.map((m) => {
1094
+ const px = base * m;
1095
+ const rem = px === 0 ? "0" : `${px / 16}rem`;
1096
+ return `--spacing-${m}: ${rem}; /* ${px}px */`;
1097
+ });
1098
+ }
1099
+ function generateRadiusPrimitives(config) {
1100
+ const decls = [];
1101
+ decls.push("--radius-none: 0;");
1102
+ decls.push(
1103
+ `--radius-sm: ${config.radius.sm / 16}rem; /* ${config.radius.sm}px */`
1104
+ );
1105
+ decls.push(
1106
+ `--radius-md: ${config.radius.md / 16}rem; /* ${config.radius.md}px */`
1107
+ );
1108
+ decls.push(
1109
+ `--radius-lg: ${config.radius.lg / 16}rem; /* ${config.radius.lg}px */`
1110
+ );
1111
+ decls.push(
1112
+ `--radius-xl: ${config.radius.xl / 16}rem; /* ${config.radius.xl}px */`
1113
+ );
1114
+ decls.push(
1115
+ `--radius-2xl: ${config.radius.xl * 1.333 / 16}rem; /* ${Math.round(config.radius.xl * 1.333)}px */`
1116
+ );
1117
+ decls.push(
1118
+ `--radius-3xl: ${config.radius.xl * 2 / 16}rem; /* ${config.radius.xl * 2}px */`
1119
+ );
1120
+ decls.push(`--radius-full: ${config.radius.pill}px;`);
1121
+ return decls;
1122
+ }
1123
+ function generateShadowPrimitives(config) {
1124
+ return [
1125
+ `--shadow-xs: ${config.shadows.xs};`,
1126
+ `--shadow-sm: ${config.shadows.sm};`,
1127
+ `--shadow-md: ${config.shadows.md};`,
1128
+ `--shadow-lg: ${config.shadows.lg};`,
1129
+ `--shadow-xl: ${config.shadows.xl};`
1130
+ ];
1131
+ }
1132
+ function generateTypographyPrimitives(config) {
1133
+ const decls = [];
1134
+ const scale = config.typography.scale;
1135
+ decls.push(`font-size: ${scale === 1 ? "1rem" : `${scale}rem`};`);
1136
+ decls.push(`--font-heading: ${config.typography.heading.family};`);
1137
+ decls.push(`--font-display: ${config.typography.display.family};`);
1138
+ decls.push(`--font-sans: ${config.typography.body.family};`);
1139
+ decls.push(`--font-body: ${config.typography.body.family};`);
1140
+ decls.push(`--font-mono: ${config.typography.mono.family};`);
1141
+ const fontSizes = {
1142
+ xs: 12,
1143
+ sm: 14,
1144
+ base: 16,
1145
+ lg: 18,
1146
+ xl: 20,
1147
+ "2xl": 24,
1148
+ "3xl": 30,
1149
+ "4xl": 36
1150
+ };
1151
+ for (const [name, px] of Object.entries(fontSizes)) {
1152
+ decls.push(`--font-size-${name}: ${px / 16}rem; /* ${px}px */`);
1153
+ }
1154
+ decls.push(`--font-weight-normal: ${config.typography.body.weight};`);
1155
+ decls.push("--font-weight-medium: 500;");
1156
+ decls.push(`--font-weight-semibold: ${config.typography.heading.weight};`);
1157
+ decls.push("--font-weight-bold: 700;");
1158
+ decls.push(`--weight-display: ${config.typography.display.weight};`);
1159
+ const lineHeights = {
1160
+ none: 1,
1161
+ tight: 1.25,
1162
+ snug: 1.375,
1163
+ normal: 1.5,
1164
+ relaxed: 1.625,
1165
+ loose: 2
1166
+ };
1167
+ for (const [name, value] of Object.entries(lineHeights)) {
1168
+ decls.push(`--line-height-${name}: ${value};`);
1169
+ }
1170
+ return decls;
1171
+ }
1172
+ function generateMotionPrimitives(config) {
1173
+ const decls = [];
1174
+ decls.push(`--motion-duration-100: ${config.motion["duration-fast"]};`);
1175
+ decls.push("--motion-duration-150: 150ms;");
1176
+ decls.push(`--motion-duration-200: ${config.motion["duration-normal"]};`);
1177
+ decls.push("--motion-duration-300: 300ms;");
1178
+ decls.push(`--motion-duration-500: ${config.motion["duration-slow"]};`);
1179
+ decls.push("--motion-duration-800: 800ms;");
1180
+ decls.push("--motion-easing-linear: linear;");
1181
+ decls.push("--motion-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);");
1182
+ decls.push("--motion-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);");
1183
+ decls.push(`--motion-easing-ease-in-out: ${config.motion.easing};`);
1184
+ decls.push(
1185
+ "--motion-easing-spring: cubic-bezier(0.34, 1.56, 0.64, 1);"
1186
+ );
1187
+ return decls;
1188
+ }
1189
+ function generateMiscPrimitives() {
1190
+ return [
1191
+ // Border widths
1192
+ "--border-width-1: 1px;",
1193
+ "--border-width-2: 2px;",
1194
+ "--border-width-3: 3px;",
1195
+ "--border-width-4: 4px;",
1196
+ // Z-index
1197
+ "--z-base: 0;",
1198
+ "--z-raised: 1;",
1199
+ "--z-dropdown: 1000;",
1200
+ "--z-sticky: 1100;",
1201
+ "--z-modal: 1300;",
1202
+ "--z-popover: 1400;",
1203
+ "--z-toast: 1500;",
1204
+ // Overlay
1205
+ "--overlay-bg: rgba(0, 0, 0, 0.5);",
1206
+ // Focus ring
1207
+ "--focus-ring-width: 2px;",
1208
+ "--focus-ring-offset: 2px;"
1209
+ ];
1210
+ }
1211
+ function generatePrimitivesCss(primitives, config) {
1212
+ const lines = [];
1213
+ lines.push(sectionComment("Primitive: Colors"));
1214
+ lines.push(
1215
+ block(":root", [generateColorPrimitives(primitives)])
1216
+ );
1217
+ lines.push(sectionComment("Primitive: Spacing"));
1218
+ lines.push(block(":root", generateSpacingPrimitives(config)));
1219
+ lines.push(sectionComment("Primitive: Border Radius"));
1220
+ lines.push(block(":root", generateRadiusPrimitives(config)));
1221
+ lines.push(sectionComment("Primitive: Typography"));
1222
+ lines.push(block(":root", generateTypographyPrimitives(config)));
1223
+ lines.push(sectionComment("Primitive: Shadows"));
1224
+ lines.push(block(":root", generateShadowPrimitives(config)));
1225
+ lines.push(sectionComment("Primitive: Motion"));
1226
+ lines.push(block(":root", generateMotionPrimitives(config)));
1227
+ lines.push(sectionComment("Primitive: Miscellaneous"));
1228
+ lines.push(block(":root", generateMiscPrimitives()));
1229
+ return header("Visor Theme \u2014 Primitives") + lines.join("\n");
1230
+ }
1231
+ function generateSemanticCss(tokens) {
1232
+ const lines = [];
1233
+ lines.push(sectionComment("Semantic: Text"));
1234
+ const textDecls = Object.entries(tokens.text).map(
1235
+ ([name, { light }]) => `--text-${name}: ${light};`
1236
+ );
1237
+ lines.push(block(":root", textDecls));
1238
+ lines.push(sectionComment("Semantic: Surface"));
1239
+ const surfaceDecls = Object.entries(tokens.surface).map(
1240
+ ([name, { light }]) => `--surface-${name}: ${light};`
1241
+ );
1242
+ lines.push(block(":root", surfaceDecls));
1243
+ lines.push(sectionComment("Semantic: Border"));
1244
+ const borderDecls = Object.entries(tokens.border).map(
1245
+ ([name, { light }]) => `--border-${name}: ${light};`
1246
+ );
1247
+ lines.push(block(":root", borderDecls));
1248
+ lines.push(sectionComment("Semantic: Interactive"));
1249
+ const interactiveDecls = Object.entries(tokens.interactive).map(
1250
+ ([name, { light }]) => `--interactive-${name}: ${light};`
1251
+ );
1252
+ lines.push(block(":root", interactiveDecls));
1253
+ return header("Visor Theme \u2014 Semantic") + lines.join("\n");
1254
+ }
1255
+ function buildAdaptiveDecls(tokens, theme) {
1256
+ const textDecls = Object.entries(tokens.text).map(
1257
+ ([name, values]) => `--text-${name}: ${values[theme]};`
1258
+ );
1259
+ const surfaceDecls = Object.entries(tokens.surface).map(
1260
+ ([name, values]) => `--surface-${name}: ${values[theme]};`
1261
+ );
1262
+ const borderDecls = Object.entries(tokens.border).map(
1263
+ ([name, values]) => `--border-${name}: ${values[theme]};`
1264
+ );
1265
+ const interactiveDecls = Object.entries(tokens.interactive).map(
1266
+ ([name, values]) => `--interactive-${name}: ${values[theme]};`
1267
+ );
1268
+ return { textDecls, surfaceDecls, borderDecls, interactiveDecls };
1269
+ }
1270
+ function generateLightCss(tokens) {
1271
+ const lines = [];
1272
+ const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "light");
1273
+ lines.push(sectionComment("Adaptive: Text (light)"));
1274
+ lines.push(block(":root", textDecls));
1275
+ lines.push(sectionComment("Adaptive: Surface (light)"));
1276
+ lines.push(block(":root", surfaceDecls));
1277
+ lines.push(sectionComment("Adaptive: Border (light)"));
1278
+ lines.push(block(":root", borderDecls));
1279
+ lines.push(sectionComment("Adaptive: Interactive (light)"));
1280
+ lines.push(block(":root", interactiveDecls));
1281
+ return header("Visor Theme \u2014 Light") + lines.join("\n");
1282
+ }
1283
+ function generateDarkCss(tokens) {
1284
+ const lines = [];
1285
+ const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "dark");
1286
+ const darkSelectors = [".dark", ".theme-dark", '[data-theme="dark"]'];
1287
+ const darkSelector = darkSelectors.join(",\n");
1288
+ const prefersSelector = ':root:not(.light):not(.theme-light):not([data-theme="light"])';
1289
+ lines.push(sectionComment("Adaptive: Text (dark) \u2014 manual toggle"));
1290
+ lines.push(block(darkSelector, textDecls));
1291
+ lines.push(sectionComment("Adaptive: Surface (dark) \u2014 manual toggle"));
1292
+ lines.push(block(darkSelector, surfaceDecls));
1293
+ lines.push(sectionComment("Adaptive: Border (dark) \u2014 manual toggle"));
1294
+ lines.push(block(darkSelector, borderDecls));
1295
+ lines.push(sectionComment("Adaptive: Interactive (dark) \u2014 manual toggle"));
1296
+ lines.push(block(darkSelector, interactiveDecls));
1297
+ lines.push(
1298
+ sectionComment("Adaptive: Text (dark) \u2014 prefers-color-scheme")
1299
+ );
1300
+ lines.push(
1301
+ `@media (prefers-color-scheme: dark) {
1302
+ ${block(prefersSelector, textDecls)}}`
1303
+ );
1304
+ lines.push("");
1305
+ lines.push(
1306
+ sectionComment("Adaptive: Surface (dark) \u2014 prefers-color-scheme")
1307
+ );
1308
+ lines.push(
1309
+ `@media (prefers-color-scheme: dark) {
1310
+ ${block(prefersSelector, surfaceDecls)}}`
1311
+ );
1312
+ lines.push("");
1313
+ lines.push(
1314
+ sectionComment("Adaptive: Border (dark) \u2014 prefers-color-scheme")
1315
+ );
1316
+ lines.push(
1317
+ `@media (prefers-color-scheme: dark) {
1318
+ ${block(prefersSelector, borderDecls)}}`
1319
+ );
1320
+ lines.push("");
1321
+ lines.push(
1322
+ sectionComment("Adaptive: Interactive (dark) \u2014 prefers-color-scheme")
1323
+ );
1324
+ lines.push(
1325
+ `@media (prefers-color-scheme: dark) {
1326
+ ${block(prefersSelector, interactiveDecls)}}`
1327
+ );
1328
+ lines.push("");
1329
+ return header("Visor Theme \u2014 Dark") + lines.join("\n");
1330
+ }
1331
+ function generateFullBundleCss(primitives, tokens, config) {
1332
+ const lines = [
1333
+ header("Visor Theme \u2014 Full Bundle"),
1334
+ "/* Import order: primitives \u2192 adaptive (light) \u2192 adaptive (dark) */",
1335
+ ""
1336
+ ];
1337
+ lines.push(
1338
+ "/* ============================================",
1339
+ " Tier 1: Primitives",
1340
+ " ============================================ */"
1341
+ );
1342
+ const primitivesBody = generatePrimitivesCss(primitives, config).split("\n").slice(6).join("\n");
1343
+ lines.push(primitivesBody);
1344
+ lines.push(
1345
+ "/* ============================================",
1346
+ " Tier 3: Adaptive \u2014 Light Theme (:root)",
1347
+ " ============================================ */"
1348
+ );
1349
+ const lightBody = generateLightCss(tokens).split("\n").slice(6).join("\n");
1350
+ lines.push(lightBody);
1351
+ lines.push(
1352
+ "/* ============================================",
1353
+ ' Tier 3: Adaptive \u2014 Dark Theme (.dark, .theme-dark, [data-theme="dark"])',
1354
+ " and @media (prefers-color-scheme: dark)",
1355
+ " ============================================ */"
1356
+ );
1357
+ const darkBody = generateDarkCss(tokens).split("\n").slice(6).join("\n");
1358
+ lines.push(darkBody);
1359
+ return lines.join("\n");
1360
+ }
1361
+
1362
+ export {
1363
+ googleFontsCatalog,
1364
+ lookupGoogleFont,
1365
+ VISOR_FONTS_CDN,
1366
+ buildVisorFontUrl,
1367
+ resolveFont,
1368
+ generatePreloadLinks,
1369
+ generateStylesheetLinks,
1370
+ resolveThemeFonts,
1371
+ normalizeHex,
1372
+ isValidHex,
1373
+ hexToRgb,
1374
+ rgbToHex,
1375
+ rgbToOklch,
1376
+ hexToOklch,
1377
+ clampToSrgb,
1378
+ oklchToHex,
1379
+ getContrastRatio,
1380
+ parseHex,
1381
+ parseRgba,
1382
+ parseHsla,
1383
+ parseOklch,
1384
+ parseColor,
1385
+ isValidColor,
1386
+ compositeOverBackground,
1387
+ serializeColor,
1388
+ FULL_SHADE_STEPS,
1389
+ SELECTIVE_SHADE_STEPS,
1390
+ TAILWIND_GRAY,
1391
+ generateShadeScale,
1392
+ header,
1393
+ sectionComment,
1394
+ generatePrimitivesCss,
1395
+ generateSemanticCss,
1396
+ generateLightCss,
1397
+ generateDarkCss,
1398
+ generateFullBundleCss
1399
+ };