@loworbitstudio/visor-theme-engine 0.5.0 → 0.6.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.
@@ -1,4 +1,4 @@
1
- import { c as GeneratedPrimitives, i as SemanticTokens, R as ResolvedThemeConfig } from '../types-ljcTtODU.js';
1
+ import { c as GeneratedPrimitives, i as SemanticTokens, R as ResolvedThemeConfig } from '../types-CtozYHw0.js';
2
2
 
3
3
  /**
4
4
  * Adapter types for the Visor theme engine.
@@ -2,7 +2,9 @@ import {
2
2
  FULL_SHADE_STEPS,
3
3
  MATERIAL_TEXT_SLOTS,
4
4
  SELECTIVE_SHADE_STEPS,
5
+ aliasFamily,
5
6
  buildVisorFontUrl,
7
+ fontStack,
6
8
  generateDarkCss,
7
9
  generateLightCss,
8
10
  generatePrimitivesCss,
@@ -11,7 +13,7 @@ import {
11
13
  parseColor,
12
14
  resolveThemeFonts,
13
15
  sectionComment
14
- } from "../chunk-U5FXQ5EC.js";
16
+ } from "../chunk-4U5L3AWY.js";
15
17
 
16
18
  // src/adapters/layers.ts
17
19
  var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
@@ -24,18 +26,29 @@ ${trimmed}
24
26
  }
25
27
 
26
28
  // src/adapters/nextjs.ts
29
+ function toKebabCase(name) {
30
+ return name.toLowerCase().replace(/\s+/g, "-");
31
+ }
27
32
  function nextjsAdapter(input, options) {
28
33
  const includeFontImports = options?.includeFontImports ?? true;
29
34
  const includeFowt = options?.includeFowt ?? true;
30
35
  const lines = [];
36
+ const slug = toKebabCase(input.config.name);
37
+ const aliasedFamilies = /* @__PURE__ */ new Map();
31
38
  lines.push(header("Visor Theme \u2014 NextJS Adapter"));
32
39
  if (includeFontImports && input.config.typography) {
33
40
  const fontResult = resolveThemeFonts(input.config.typography);
34
- const googleFonts = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono].filter(
35
- (r) => r !== null && r.source === "google-fonts"
41
+ const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
42
+ for (const font of fontSlots) {
43
+ if (font && font.source === "visor-fonts" && !aliasedFamilies.has(font.family)) {
44
+ aliasedFamilies.set(font.family, aliasFamily(font.family, slug));
45
+ }
46
+ }
47
+ const hostedCssFonts = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono].filter(
48
+ (r) => r !== null && (r.source === "google-fonts" || r.source === "fontshare")
36
49
  );
37
50
  const seenUrls = /* @__PURE__ */ new Set();
38
- for (const font of googleFonts) {
51
+ for (const font of hostedCssFonts) {
39
52
  if (font?.cssUrl && !seenUrls.has(font.cssUrl)) {
40
53
  seenUrls.add(font.cssUrl);
41
54
  lines.push(`@import url("${font.cssUrl}");`);
@@ -59,10 +72,11 @@ function nextjsAdapter(input, options) {
59
72
  for (const font of visorFonts) {
60
73
  if (seenVisorFamilies.has(font.family)) continue;
61
74
  seenVisorFamilies.add(font.family);
75
+ const aliased = aliasedFamilies.get(font.family);
62
76
  for (const weight of font.weights) {
63
77
  const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
64
78
  lines.push(`@font-face {`);
65
- lines.push(` font-family: "${font.family}";`);
79
+ lines.push(` font-family: "${aliased}";`);
66
80
  lines.push(` src: url("${url}") format("woff2");`);
67
81
  lines.push(` font-weight: ${weight};`);
68
82
  lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
@@ -75,7 +89,7 @@ function nextjsAdapter(input, options) {
75
89
  lines.push(LAYER_ORDER);
76
90
  lines.push("");
77
91
  const primitivesBody = stripHeader(
78
- generatePrimitivesCss(input.primitives, input.config)
92
+ generatePrimitivesCss(input.primitives, input.config, { aliasedFamilies })
79
93
  );
80
94
  lines.push(wrapInLayer("visor-primitives", primitivesBody));
81
95
  lines.push("");
@@ -174,7 +188,7 @@ var SELECTIVE_SCALE_ROLES = [
174
188
  "error",
175
189
  "info"
176
190
  ];
177
- function toKebabCase(name) {
191
+ function toKebabCase2(name) {
178
192
  return name.toLowerCase().replace(/\s+/g, "-");
179
193
  }
180
194
  function generateScopedPrimitives(primitives, config) {
@@ -226,7 +240,7 @@ function generateSemanticDecls(tokens, mode) {
226
240
  return decls;
227
241
  }
228
242
  function deckAdapter(input, options) {
229
- const scopeClass = options?.scopeClass ?? `.deck--${toKebabCase(input.config.name)}`;
243
+ const scopeClass = options?.scopeClass ?? `.deck--${toKebabCase2(input.config.name)}`;
230
244
  const lines = [];
231
245
  lines.push(header(`Visor Theme \u2014 Deck Adapter (${scopeClass})`));
232
246
  const primDecls = generateScopedPrimitives(input.primitives, input.config);
@@ -260,7 +274,7 @@ function deckAdapter(input, options) {
260
274
  // src/adapters/docs.ts
261
275
  var FULL_SCALE_ROLES2 = ["primary", "accent", "neutral"];
262
276
  var SELECTIVE_SCALE_ROLES2 = ["success", "warning", "error", "info"];
263
- function toKebabCase2(name) {
277
+ function toKebabCase3(name) {
264
278
  return name.toLowerCase().replace(/\s+/g, "-");
265
279
  }
266
280
  function generateColorDecls(primitives) {
@@ -298,13 +312,14 @@ function generateRadiusDecls(config) {
298
312
  `--radius-full: ${config.radius.pill}px;`
299
313
  ];
300
314
  }
301
- function generateTypographyDecls(config) {
315
+ function generateTypographyDecls(config, aliases) {
302
316
  const decls = [];
303
- decls.push(`--font-display: ${config.typography.display.family};`);
304
- decls.push(`--font-sans: ${config.typography.body.family};`);
305
- decls.push(`--font-heading: var(--font-sans);`);
306
- decls.push(`--font-body: ${config.typography.body.family};`);
307
- decls.push(`--font-mono: ${config.typography.mono.family};`);
317
+ const headingFamily = config.typography.heading?.family ?? config.typography.body.family;
318
+ decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
319
+ decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
320
+ decls.push(`--font-heading: ${fontStack(headingFamily, aliases)};`);
321
+ decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
322
+ decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
308
323
  const fontSizes = {
309
324
  xs: 12,
310
325
  sm: 14,
@@ -415,31 +430,38 @@ function sectionComment2(label) {
415
430
  /* --- ${label} --- */`;
416
431
  }
417
432
  function docsAdapter(input, options) {
418
- const slug = toKebabCase2(input.config.name);
433
+ const slug = toKebabCase3(input.config.name);
419
434
  const scopeClass = `.${slug}-theme`;
420
435
  const includeFontImports = options?.includeFontImports ?? true;
421
436
  const fontLines = [];
422
437
  const lines = [];
438
+ const aliasedFamilies = /* @__PURE__ */ new Map();
423
439
  if (includeFontImports && input.config.typography) {
424
440
  const fontResult = resolveThemeFonts(input.config.typography);
425
441
  const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
442
+ for (const font of fontSlots) {
443
+ if (font && font.source === "visor-fonts" && !aliasedFamilies.has(font.family)) {
444
+ aliasedFamilies.set(font.family, aliasFamily(font.family, slug));
445
+ }
446
+ }
426
447
  const seenUrls = /* @__PURE__ */ new Set();
427
448
  for (const font of fontSlots) {
428
- if (font && font.source === "google-fonts" && font.cssUrl && !seenUrls.has(font.cssUrl)) {
449
+ if (font && (font.source === "google-fonts" || font.source === "fontshare") && font.cssUrl && !seenUrls.has(font.cssUrl)) {
429
450
  seenUrls.add(font.cssUrl);
430
451
  fontLines.push(`@import url("${font.cssUrl}");`);
431
452
  fontLines.push("");
432
453
  }
433
454
  }
434
455
  const scale = input.config.typography?.scale ?? 1;
435
- const seenFamilies = /* @__PURE__ */ new Set();
456
+ const emittedFamilies = /* @__PURE__ */ new Set();
436
457
  for (const font of fontSlots) {
437
- if (font && font.source === "visor-fonts" && !seenFamilies.has(font.family)) {
438
- seenFamilies.add(font.family);
458
+ if (font && font.source === "visor-fonts" && !emittedFamilies.has(font.family)) {
459
+ emittedFamilies.add(font.family);
460
+ const aliased = aliasedFamilies.get(font.family);
439
461
  for (const weight of font.weights) {
440
462
  const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
441
463
  fontLines.push("@font-face {");
442
- fontLines.push(` font-family: "${font.family}";`);
464
+ fontLines.push(` font-family: "${aliased}";`);
443
465
  fontLines.push(` src: url("${url}") format("woff2");`);
444
466
  fontLines.push(` font-weight: ${weight};`);
445
467
  fontLines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
@@ -475,7 +497,7 @@ function docsAdapter(input, options) {
475
497
  lines.push(block(scopeClass, generateRadiusDecls(input.config)));
476
498
  lines.push("");
477
499
  lines.push(sectionComment2("Primitive: Typography"));
478
- lines.push(block(scopeClass, generateTypographyDecls(input.config)));
500
+ lines.push(block(scopeClass, generateTypographyDecls(input.config, aliasedFamilies)));
479
501
  lines.push("");
480
502
  lines.push(sectionComment2("Primitive: Shadows"));
481
503
  lines.push(block(scopeClass, generateShadowDecls(input.config)));
@@ -151,6 +151,8 @@ function buildGoogleFontsCssUrl(family, weights, italic, display) {
151
151
  return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${wghtValues}&display=${display}`;
152
152
  }
153
153
  var VISOR_FONTS_CDN = "https://fonts.visor.design";
154
+ var FONTSHARE_API_ORIGIN = "https://api.fontshare.com";
155
+ var FONTSHARE_CDN_ORIGIN = "https://cdn.fontshare.com";
154
156
  function buildFamilySlug(family) {
155
157
  return family.toLowerCase().replace(/ /g, "-");
156
158
  }
@@ -174,6 +176,16 @@ function buildVisorFontUrl(org, family, weight) {
174
176
  const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
175
177
  return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
176
178
  }
179
+ function buildFontshareCssUrl(family, weights, italic, display) {
180
+ const slug = buildFamilySlug(family);
181
+ const sortedWeights = [...weights].sort((a, b) => a - b);
182
+ const tokens = [];
183
+ if (italic) {
184
+ for (const w of sortedWeights) tokens.push(`${w}i`);
185
+ }
186
+ for (const w of sortedWeights) tokens.push(`${w}`);
187
+ return `${FONTSHARE_API_ORIGIN}/v2/css?f[]=${slug}@${tokens.join(",")}&display=${display}`;
188
+ }
177
189
  function resolveFont(family, options = {}) {
178
190
  const display = options.display ?? DEFAULT_DISPLAY;
179
191
  const requestedWeights = options.weights ?? DEFAULT_WEIGHTS;
@@ -192,6 +204,20 @@ function resolveFont(family, options = {}) {
192
204
  org: options.org ?? null
193
205
  };
194
206
  }
207
+ if (explicitSource === "fontshare") {
208
+ const cssUrl = buildFontshareCssUrl(family, requestedWeights, italic, display);
209
+ return {
210
+ family,
211
+ source: "fontshare",
212
+ cssUrl,
213
+ weights: requestedWeights,
214
+ italic,
215
+ display,
216
+ category: options.category ?? "sans-serif",
217
+ guidance: null,
218
+ org: null
219
+ };
220
+ }
195
221
  if (explicitSource === "local") {
196
222
  return {
197
223
  family,
@@ -288,6 +314,20 @@ function generatePreloadLinks(resolutions, customFontPaths) {
288
314
  }
289
315
  }
290
316
  }
317
+ const hasFontshare = resolutions.some((r) => r.source === "fontshare");
318
+ if (hasFontshare) {
319
+ links.push(`<link rel="preconnect" href="${FONTSHARE_API_ORIGIN}">`);
320
+ links.push(
321
+ `<link rel="preconnect" href="${FONTSHARE_CDN_ORIGIN}" crossorigin>`
322
+ );
323
+ for (const resolution of resolutions) {
324
+ if (resolution.source === "fontshare" && resolution.cssUrl) {
325
+ links.push(
326
+ `<link rel="preload" as="style" href="${resolution.cssUrl}">`
327
+ );
328
+ }
329
+ }
330
+ }
291
331
  if (customFontPaths) {
292
332
  for (const resolution of resolutions) {
293
333
  if (resolution.source === "local") {
@@ -307,7 +347,7 @@ function generatePreloadLinks(resolutions, customFontPaths) {
307
347
  function generateStylesheetLinks(resolutions) {
308
348
  const links = [];
309
349
  for (const resolution of resolutions) {
310
- if (resolution.source === "google-fonts" && resolution.cssUrl) {
350
+ if ((resolution.source === "google-fonts" || resolution.source === "fontshare") && resolution.cssUrl) {
311
351
  links.push(
312
352
  `<link rel="stylesheet" href="${resolution.cssUrl}">`
313
353
  );
@@ -330,6 +370,19 @@ function generateFontCSS(heading, displayFont, body, mono, typography) {
330
370
  }
331
371
  lines.push("");
332
372
  }
373
+ const fontshareFonts = allSlots.filter(
374
+ (r) => r !== null && r.source === "fontshare"
375
+ );
376
+ const seenFontshareUrls = /* @__PURE__ */ new Set();
377
+ if (fontshareFonts.length > 0) {
378
+ lines.push("/* Fontshare \u2014 load these stylesheets in your HTML <head> */");
379
+ for (const font of fontshareFonts) {
380
+ if (!font.cssUrl || seenFontshareUrls.has(font.cssUrl)) continue;
381
+ seenFontshareUrls.add(font.cssUrl);
382
+ lines.push(`/* ${font.cssUrl} */`);
383
+ }
384
+ lines.push("");
385
+ }
333
386
  const visorFonts = allSlots.filter(
334
387
  (r) => r !== null && r.source === "visor-fonts"
335
388
  );
@@ -1069,6 +1122,17 @@ function generateShadeScale(color, role) {
1069
1122
  return scale;
1070
1123
  }
1071
1124
 
1125
+ // src/fonts/theme-alias.ts
1126
+ var EMPTY_ALIASES = /* @__PURE__ */ new Map();
1127
+ function aliasFamily(family, themeSlug) {
1128
+ return `${family} [${themeSlug}]`;
1129
+ }
1130
+ function fontStack(bare, aliases) {
1131
+ const aliased = aliases.get(bare);
1132
+ if (!aliased) return bare;
1133
+ return `"${aliased}", "${bare}"`;
1134
+ }
1135
+
1072
1136
  // src/generate-css.ts
1073
1137
  function header(label) {
1074
1138
  return [
@@ -1154,15 +1218,15 @@ function generateShadowPrimitives(config) {
1154
1218
  `--shadow-xl: ${config.shadows.xl};`
1155
1219
  ];
1156
1220
  }
1157
- function generateTypographyPrimitives(config) {
1221
+ function generateTypographyPrimitives(config, aliases = EMPTY_ALIASES) {
1158
1222
  const decls = [];
1159
1223
  const scale = config.typography.scale;
1160
1224
  decls.push(`font-size: ${scale === 1 ? "1rem" : `${scale}rem`};`);
1161
- decls.push(`--font-heading: ${config.typography.heading.family};`);
1162
- decls.push(`--font-display: ${config.typography.display.family};`);
1163
- decls.push(`--font-sans: ${config.typography.body.family};`);
1164
- decls.push(`--font-body: ${config.typography.body.family};`);
1165
- decls.push(`--font-mono: ${config.typography.mono.family};`);
1225
+ decls.push(`--font-heading: ${fontStack(config.typography.heading.family, aliases)};`);
1226
+ decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
1227
+ decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
1228
+ decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
1229
+ decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
1166
1230
  const fontSizes = {
1167
1231
  xs: 12,
1168
1232
  sm: 14,
@@ -1233,7 +1297,7 @@ function generateMiscPrimitives() {
1233
1297
  "--focus-ring-offset: 2px;"
1234
1298
  ];
1235
1299
  }
1236
- function generatePrimitivesCss(primitives, config) {
1300
+ function generatePrimitivesCss(primitives, config, options) {
1237
1301
  const lines = [];
1238
1302
  lines.push(sectionComment("Primitive: Colors"));
1239
1303
  lines.push(
@@ -1244,7 +1308,7 @@ function generatePrimitivesCss(primitives, config) {
1244
1308
  lines.push(sectionComment("Primitive: Border Radius"));
1245
1309
  lines.push(block(":root", generateRadiusPrimitives(config)));
1246
1310
  lines.push(sectionComment("Primitive: Typography"));
1247
- lines.push(block(":root", generateTypographyPrimitives(config)));
1311
+ lines.push(block(":root", generateTypographyPrimitives(config, options?.aliasedFamilies)));
1248
1312
  lines.push(sectionComment("Primitive: Shadows"));
1249
1313
  lines.push(block(":root", generateShadowPrimitives(config)));
1250
1314
  lines.push(sectionComment("Primitive: Motion"));
@@ -1437,6 +1501,8 @@ export {
1437
1501
  SELECTIVE_SHADE_STEPS,
1438
1502
  TAILWIND_GRAY,
1439
1503
  generateShadeScale,
1504
+ aliasFamily,
1505
+ fontStack,
1440
1506
  header,
1441
1507
  sectionComment,
1442
1508
  generatePrimitivesCss,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-ljcTtODU.js';
2
- export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-ljcTtODU.js';
1
+ import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-CtozYHw0.js';
2
+ export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-CtozYHw0.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -102,6 +102,34 @@ declare const googleFontsCatalog: GoogleFontEntry[];
102
102
  /** Look up a font family in the Google Fonts catalog (case-insensitive) */
103
103
  declare function lookupGoogleFont(family: string): GoogleFontEntry | undefined;
104
104
 
105
+ /**
106
+ * Font coverage validator.
107
+ *
108
+ * Catches the failure mode behind VI-358: emitted theme CSS declares
109
+ * `--font-*: Family, ...` overrides but the same CSS contains no
110
+ * `@font-face` rule for that family, so the browser can never load the
111
+ * declared font and silently falls through to the next stack entry.
112
+ *
113
+ * The validator extracts the primary family from each `--font-*` declaration
114
+ * (the first comma-separated token, unquoted) and checks it against the set
115
+ * of @font-face families in the same emitted CSS. Generic CSS keywords
116
+ * (sans-serif, system-ui, etc.) and well-known platform-installed fonts
117
+ * (SF Mono, Helvetica, etc.) are skipped — those are intentionally part of
118
+ * the fallback stack and never carry their own @font-face.
119
+ *
120
+ * Size-adjusted system-fallback faces (family ends with " Fallback") are
121
+ * also excluded from the @font-face coverage set; they don't load a real
122
+ * font, they only adjust local metrics.
123
+ */
124
+ interface FontCoverageError {
125
+ family: string;
126
+ declaredAt: string;
127
+ }
128
+ interface FontCoverageResult {
129
+ errors: FontCoverageError[];
130
+ }
131
+ declare function validateFontCoverage(css: string): FontCoverageResult;
132
+
105
133
  /**
106
134
  * Import Pipeline
107
135
  *
@@ -840,6 +868,30 @@ declare const SEMANTIC_MAP: {
840
868
  interactive: Record<string, SemanticMapping>;
841
869
  };
842
870
 
871
+ /**
872
+ * Per-theme font-family aliasing — substrate fix for VI-354.
873
+ *
874
+ * `@font-face` declarations are global to the document, so co-loaded themes
875
+ * that share a font family with differing per-theme properties (e.g.
876
+ * `size-adjust`) silently overwrite each other. Aliasing each theme's
877
+ * `@font-face` family as `{family} [{slug}]` scopes the declaration to
878
+ * that theme only; the theme's `--font-*` vars then list the alias first
879
+ * with the bare family as a fallback for graceful degradation.
880
+ *
881
+ * Lives in `fonts/` (not `adapters/`) because every adapter that emits
882
+ * visor-fonts `@font-face` blocks needs the same aliasing rules. Sharing
883
+ * the helpers prevents drift between adapters.
884
+ */
885
+ /**
886
+ * Map of `bare family name → aliased family name` for every family the
887
+ * theme emits as a per-theme `@font-face`. The alias applies to every
888
+ * `--font-*` whose family matches an entry, regardless of which slot the
889
+ * var represents (the bug repro in VI-354 hinges on this for --font-mono,
890
+ * which can resolve to the same family as heading/body but doesn't carry
891
+ * the visor-fonts source through `resolveConfig`).
892
+ */
893
+ type AliasedFamilies = ReadonlyMap<string, string>;
894
+
843
895
  /**
844
896
  * CSS Generation (Stage 3 + Output)
845
897
  *
@@ -847,7 +899,9 @@ declare const SEMANTIC_MAP: {
847
899
  * packages/tokens/src/generate/generate-css.ts output format.
848
900
  */
849
901
 
850
- declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig): string;
902
+ declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig, options?: {
903
+ aliasedFamilies?: AliasedFamilies;
904
+ }): string;
851
905
  declare function generateSemanticCss(tokens: SemanticTokens): string;
852
906
  declare function generateLightCss(tokens: SemanticTokens): string;
853
907
  declare function generateDarkCss(tokens: SemanticTokens): string;
@@ -928,4 +982,4 @@ declare function cleanFontValue(val: string): string;
928
982
  */
929
983
  declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
930
984
 
931
- export { type CSSFile, ColorRole, type Confidence, type ExtractedToken, type ExtractionResult, FONT_WEIGHT_ALIASES, FontDisplayStrategy, type FontFaceDeclaration, FontResolution, FontResolveOptions, FullShadeScale, GeneratedPrimitives, GoogleFontEntry, OKLCH, ParsedColor, RGB, ResolvedThemeConfig, SEMANTIC_MAP, SelectiveShadeScale, SemanticTokens, ShadeStep, TAILWIND_GRAY, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_FONTS_CDN, type ValidationIssue, type ValidationSeverity, VisorThemeConfig, VisorTypography, applyOverrides, assignSemanticTokens, buildVisorFontUrl, clampToSrgb, cleanFontValue, compositeOverBackground, exportTheme, extractFromCSS, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, googleFontsCatalog, hexToOklch, hexToRgb, isValidColor, isValidHex, isVisorThemeConfig, lookupFontWeightAlias, lookupGoogleFont, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveConfig, resolveFont, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, visorTheme_schema as visorThemeSchema };
985
+ export { type CSSFile, ColorRole, type Confidence, type ExtractedToken, type ExtractionResult, FONT_WEIGHT_ALIASES, type FontCoverageError, type FontCoverageResult, FontDisplayStrategy, type FontFaceDeclaration, FontResolution, FontResolveOptions, FullShadeScale, GeneratedPrimitives, GoogleFontEntry, OKLCH, ParsedColor, RGB, ResolvedThemeConfig, SEMANTIC_MAP, SelectiveShadeScale, SemanticTokens, ShadeStep, TAILWIND_GRAY, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_FONTS_CDN, type ValidationIssue, type ValidationSeverity, VisorThemeConfig, VisorTypography, applyOverrides, assignSemanticTokens, buildVisorFontUrl, clampToSrgb, cleanFontValue, compositeOverBackground, exportTheme, extractFromCSS, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, googleFontsCatalog, hexToOklch, hexToRgb, isValidColor, isValidHex, isVisorThemeConfig, lookupFontWeightAlias, lookupGoogleFont, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveConfig, resolveFont, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
package/dist/index.js CHANGED
@@ -34,7 +34,154 @@ import {
34
34
  rgbToHex,
35
35
  rgbToOklch,
36
36
  serializeColor
37
- } from "./chunk-U5FXQ5EC.js";
37
+ } from "./chunk-4U5L3AWY.js";
38
+
39
+ // src/fonts/validate-coverage.ts
40
+ var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
41
+ var FONT_FACE_RE = /@font-face\s*\{[^}]*\}/g;
42
+ var FONT_FAMILY_DECL_RE = /font-family\s*:\s*([^;]+);/;
43
+ var GOOGLE_FONTS_IMPORT_RE = /@import\s+url\(["']?https:\/\/fonts\.googleapis\.com\/css2?\?family=([^:&"')]+)/g;
44
+ var FONTSHARE_IMPORT_RE = /@import\s+url\(["']?https:\/\/api\.fontshare\.com\/v2\/css\?f(?:\[\]|%5B%5D)=([a-z0-9-]+)/gi;
45
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
46
+ "serif",
47
+ "sans-serif",
48
+ "monospace",
49
+ "cursive",
50
+ "fantasy",
51
+ "system-ui",
52
+ "ui-monospace",
53
+ "ui-sans-serif",
54
+ "ui-serif",
55
+ "ui-rounded",
56
+ "emoji",
57
+ "math",
58
+ "fangsong",
59
+ "inherit",
60
+ "initial",
61
+ "unset",
62
+ "revert",
63
+ "none",
64
+ // Apple / Chromium-on-Mac system keywords. These behave like
65
+ // CSS-engine-level aliases, not real font families — `local()` can
66
+ // never resolve them, so they never need an @font-face.
67
+ "-apple-system",
68
+ "-webkit-system-font",
69
+ "BlinkMacSystemFont"
70
+ ]);
71
+ var SYSTEM_FONTS = /* @__PURE__ */ new Set([
72
+ "Apple Color Emoji",
73
+ "Arial",
74
+ "Arial Black",
75
+ "BlinkMacSystemFont",
76
+ "Cambria",
77
+ "Comic Sans MS",
78
+ "Consolas",
79
+ "Courier",
80
+ "Courier New",
81
+ "DejaVu Sans",
82
+ "DejaVu Sans Mono",
83
+ "Fira Code",
84
+ "Fira Mono",
85
+ "Fira Sans",
86
+ "Georgia",
87
+ "Helvetica",
88
+ "Helvetica Neue",
89
+ "Impact",
90
+ "JetBrains Mono",
91
+ "Liberation Mono",
92
+ "Liberation Sans",
93
+ "Lucida Console",
94
+ "Lucida Grande",
95
+ "Menlo",
96
+ "Microsoft YaHei",
97
+ "Monaco",
98
+ "Noto Color Emoji",
99
+ "Open Sans",
100
+ "PingFang SC",
101
+ "PingFang TC",
102
+ "Roboto",
103
+ "Roboto Mono",
104
+ "Roboto Slab",
105
+ "SF Mono",
106
+ "SF Pro Display",
107
+ "SF Pro Text",
108
+ "Segoe UI",
109
+ "Segoe UI Emoji",
110
+ "Segoe UI Symbol",
111
+ "Segoe UI Variable",
112
+ "Source Code Pro",
113
+ "Source Sans Pro",
114
+ "Times",
115
+ "Times New Roman",
116
+ "Trebuchet MS",
117
+ "Verdana"
118
+ ]);
119
+ function extractPrimaryFamily(value) {
120
+ const trimmed = value.trim();
121
+ if (trimmed.startsWith("var(")) return null;
122
+ const firstToken = trimmed.split(",")[0].trim();
123
+ if (!firstToken) return null;
124
+ const unquoted = firstToken.replace(/^["']|["']$/g, "");
125
+ if (GENERIC_FAMILIES.has(unquoted)) return null;
126
+ if (SYSTEM_FONTS.has(unquoted)) return null;
127
+ return unquoted;
128
+ }
129
+ function extractFontFaceFamilies(css) {
130
+ const families = /* @__PURE__ */ new Set();
131
+ const blocks = css.match(FONT_FACE_RE) ?? [];
132
+ for (const block of blocks) {
133
+ const decl = FONT_FAMILY_DECL_RE.exec(block);
134
+ if (!decl) continue;
135
+ const family = decl[1].trim().replace(/^["']|["'];?$/g, "");
136
+ if (family.endsWith(" Fallback")) continue;
137
+ families.add(family);
138
+ }
139
+ return families;
140
+ }
141
+ function extractGoogleFontsImports(css) {
142
+ const families = /* @__PURE__ */ new Set();
143
+ for (const match of css.matchAll(GOOGLE_FONTS_IMPORT_RE)) {
144
+ const family = decodeURIComponent(match[1]).replace(/\+/g, " ");
145
+ families.add(family);
146
+ }
147
+ return families;
148
+ }
149
+ function fontshareSlugToFamily(slug) {
150
+ return slug.split("-").filter((p) => p.length > 0).map((p) => p[0].toUpperCase() + p.slice(1)).join(" ");
151
+ }
152
+ function extractFontshareImports(css) {
153
+ const families = /* @__PURE__ */ new Set();
154
+ for (const match of css.matchAll(FONTSHARE_IMPORT_RE)) {
155
+ families.add(fontshareSlugToFamily(match[1]));
156
+ }
157
+ return families;
158
+ }
159
+ function extractFontVarDeclarations(css) {
160
+ const decls = [];
161
+ for (const match of css.matchAll(FONT_VAR_RE)) {
162
+ const slot = `--font-${match[1]}`;
163
+ const family = extractPrimaryFamily(match[2]);
164
+ if (!family) continue;
165
+ decls.push({ slot, family });
166
+ }
167
+ return decls;
168
+ }
169
+ function validateFontCoverage(css) {
170
+ const declaredFamilies = extractFontFaceFamilies(css);
171
+ for (const f of extractGoogleFontsImports(css)) declaredFamilies.add(f);
172
+ for (const f of extractFontshareImports(css)) declaredFamilies.add(f);
173
+ const declarations = extractFontVarDeclarations(css);
174
+ const errors = [];
175
+ const seen = /* @__PURE__ */ new Set();
176
+ for (const decl of declarations) {
177
+ if (declaredFamilies.has(decl.family)) continue;
178
+ const key = `${decl.slot}::${decl.family}`;
179
+ if (seen.has(key)) continue;
180
+ seen.add(key);
181
+ errors.push({ family: decl.family, declaredAt: decl.slot });
182
+ }
183
+ return { errors };
184
+ }
38
185
 
39
186
  // src/pipeline.ts
40
187
  import { parse as parseYaml } from "yaml";
@@ -429,7 +576,7 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
429
576
  "slots"
430
577
  ]);
431
578
  var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
432
- var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
579
+ var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
433
580
  var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
434
581
  var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
435
582
  var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
@@ -801,7 +948,11 @@ function resolveConfig(config) {
801
948
  ...config.typography?.body?.weights && { weights: config.typography.body.weights }
802
949
  },
803
950
  mono: {
804
- family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
951
+ family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family,
952
+ ...config.typography?.mono?.weight && { weight: config.typography.mono.weight },
953
+ ...config.typography?.mono?.weights && { weights: config.typography.mono.weights },
954
+ ...config.typography?.mono?.source && { source: config.typography.mono.source },
955
+ ...config.typography?.mono?.org && { org: config.typography.mono.org }
805
956
  },
806
957
  slots: config.typography?.slots ?? {}
807
958
  },
@@ -2679,5 +2830,6 @@ export {
2679
2830
  serializeColor,
2680
2831
  validate,
2681
2832
  validateConfig,
2833
+ validateFontCoverage,
2682
2834
  visor_theme_schema_default as visorThemeSchema
2683
2835
  };
@@ -2,7 +2,7 @@
2
2
  * Font resolution types for the Visor theme engine.
3
3
  */
4
4
  /** Where a font is loaded from */
5
- type FontSource = "google-fonts" | "visor-fonts" | "local";
5
+ type FontSource = "google-fonts" | "visor-fonts" | "fontshare" | "local";
6
6
  /** CSS font-display strategy */
7
7
  type FontDisplayStrategy = "swap" | "block" | "fallback" | "optional" | "auto";
8
8
  /** A single resolved font */
@@ -11,7 +11,7 @@ interface FontResolution {
11
11
  family: string;
12
12
  /** Where this font comes from */
13
13
  source: FontSource;
14
- /** Google Fonts CSS URL (only for google-fonts source) */
14
+ /** Hosted CSS URL (only for google-fonts or fontshare sources) */
15
15
  cssUrl: string | null;
16
16
  /** Weights available/requested for this font */
17
17
  weights: number[];
@@ -198,6 +198,7 @@ interface VisorThemeConfig {
198
198
  mono?: {
199
199
  family?: string;
200
200
  weight?: number;
201
+ weights?: number[];
201
202
  source?: FontSource;
202
203
  org?: string;
203
204
  };
@@ -304,6 +305,10 @@ interface ResolvedThemeConfig {
304
305
  };
305
306
  mono: {
306
307
  family: string;
308
+ weight?: number;
309
+ weights?: number[];
310
+ source?: FontSource;
311
+ org?: string;
307
312
  };
308
313
  /**
309
314
  * Per-slot Material `TextTheme` overrides, passed through from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Theme engine for the Visor design system — shade generation, token mapping, font resolution, and import/export for .visor.yaml themes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",