@loworbitstudio/visor-theme-engine 0.4.2 → 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-SXT2KY6D.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)));
@@ -116,6 +116,21 @@ function lookupGoogleFont(family) {
116
116
  return catalogMap.get(family.toLowerCase());
117
117
  }
118
118
 
119
+ // src/fonts/font-aliases.ts
120
+ var FONT_WEIGHT_ALIASES = {
121
+ "PP Model Mono": {
122
+ 400: "Book",
123
+ 800: "Super"
124
+ },
125
+ "PP Model Plastic": {
126
+ 400: "Book",
127
+ 800: "Super"
128
+ }
129
+ };
130
+ function lookupFontWeightAlias(family, weight) {
131
+ return FONT_WEIGHT_ALIASES[family]?.[weight] ?? null;
132
+ }
133
+
119
134
  // src/fonts/resolve.ts
120
135
  var DEFAULT_WEIGHTS = [400, 700];
121
136
  var DEFAULT_DISPLAY = "swap";
@@ -136,6 +151,8 @@ function buildGoogleFontsCssUrl(family, weights, italic, display) {
136
151
  return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${wghtValues}&display=${display}`;
137
152
  }
138
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";
139
156
  function buildFamilySlug(family) {
140
157
  return family.toLowerCase().replace(/ /g, "-");
141
158
  }
@@ -156,9 +173,19 @@ var WEIGHT_NAMES = {
156
173
  function buildVisorFontUrl(org, family, weight) {
157
174
  const slug = buildFamilySlug(family);
158
175
  const prefix = buildFamilyPrefix(family);
159
- const weightName = WEIGHT_NAMES[weight] ?? `W${weight}`;
176
+ const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
160
177
  return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
161
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
+ }
162
189
  function resolveFont(family, options = {}) {
163
190
  const display = options.display ?? DEFAULT_DISPLAY;
164
191
  const requestedWeights = options.weights ?? DEFAULT_WEIGHTS;
@@ -177,6 +204,20 @@ function resolveFont(family, options = {}) {
177
204
  org: options.org ?? null
178
205
  };
179
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
+ }
180
221
  if (explicitSource === "local") {
181
222
  return {
182
223
  family,
@@ -273,6 +314,20 @@ function generatePreloadLinks(resolutions, customFontPaths) {
273
314
  }
274
315
  }
275
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
+ }
276
331
  if (customFontPaths) {
277
332
  for (const resolution of resolutions) {
278
333
  if (resolution.source === "local") {
@@ -292,7 +347,7 @@ function generatePreloadLinks(resolutions, customFontPaths) {
292
347
  function generateStylesheetLinks(resolutions) {
293
348
  const links = [];
294
349
  for (const resolution of resolutions) {
295
- if (resolution.source === "google-fonts" && resolution.cssUrl) {
350
+ if ((resolution.source === "google-fonts" || resolution.source === "fontshare") && resolution.cssUrl) {
296
351
  links.push(
297
352
  `<link rel="stylesheet" href="${resolution.cssUrl}">`
298
353
  );
@@ -315,6 +370,19 @@ function generateFontCSS(heading, displayFont, body, mono, typography) {
315
370
  }
316
371
  lines.push("");
317
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
+ }
318
386
  const visorFonts = allSlots.filter(
319
387
  (r) => r !== null && r.source === "visor-fonts"
320
388
  );
@@ -1054,6 +1122,17 @@ function generateShadeScale(color, role) {
1054
1122
  return scale;
1055
1123
  }
1056
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
+
1057
1136
  // src/generate-css.ts
1058
1137
  function header(label) {
1059
1138
  return [
@@ -1139,15 +1218,15 @@ function generateShadowPrimitives(config) {
1139
1218
  `--shadow-xl: ${config.shadows.xl};`
1140
1219
  ];
1141
1220
  }
1142
- function generateTypographyPrimitives(config) {
1221
+ function generateTypographyPrimitives(config, aliases = EMPTY_ALIASES) {
1143
1222
  const decls = [];
1144
1223
  const scale = config.typography.scale;
1145
1224
  decls.push(`font-size: ${scale === 1 ? "1rem" : `${scale}rem`};`);
1146
- decls.push(`--font-heading: ${config.typography.heading.family};`);
1147
- decls.push(`--font-display: ${config.typography.display.family};`);
1148
- decls.push(`--font-sans: ${config.typography.body.family};`);
1149
- decls.push(`--font-body: ${config.typography.body.family};`);
1150
- 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)};`);
1151
1230
  const fontSizes = {
1152
1231
  xs: 12,
1153
1232
  sm: 14,
@@ -1218,7 +1297,7 @@ function generateMiscPrimitives() {
1218
1297
  "--focus-ring-offset: 2px;"
1219
1298
  ];
1220
1299
  }
1221
- function generatePrimitivesCss(primitives, config) {
1300
+ function generatePrimitivesCss(primitives, config, options) {
1222
1301
  const lines = [];
1223
1302
  lines.push(sectionComment("Primitive: Colors"));
1224
1303
  lines.push(
@@ -1229,7 +1308,7 @@ function generatePrimitivesCss(primitives, config) {
1229
1308
  lines.push(sectionComment("Primitive: Border Radius"));
1230
1309
  lines.push(block(":root", generateRadiusPrimitives(config)));
1231
1310
  lines.push(sectionComment("Primitive: Typography"));
1232
- lines.push(block(":root", generateTypographyPrimitives(config)));
1311
+ lines.push(block(":root", generateTypographyPrimitives(config, options?.aliasedFamilies)));
1233
1312
  lines.push(sectionComment("Primitive: Shadows"));
1234
1313
  lines.push(block(":root", generateShadowPrimitives(config)));
1235
1314
  lines.push(sectionComment("Primitive: Motion"));
@@ -1392,6 +1471,8 @@ var MATERIAL_TEXT_SLOTS = [
1392
1471
  export {
1393
1472
  googleFontsCatalog,
1394
1473
  lookupGoogleFont,
1474
+ FONT_WEIGHT_ALIASES,
1475
+ lookupFontWeightAlias,
1395
1476
  VISOR_FONTS_CDN,
1396
1477
  buildVisorFontUrl,
1397
1478
  resolveFont,
@@ -1420,6 +1501,8 @@ export {
1420
1501
  SELECTIVE_SHADE_STEPS,
1421
1502
  TAILWIND_GRAY,
1422
1503
  generateShadeScale,
1504
+ aliasFamily,
1505
+ fontStack,
1423
1506
  header,
1424
1507
  sectionComment,
1425
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.
@@ -28,6 +28,22 @@ declare function buildVisorFontUrl(org: string, family: string, weight: number):
28
28
  */
29
29
  declare function resolveFont(family: string, options?: FontResolveOptions): FontResolution;
30
30
 
31
+ /**
32
+ * Font weight-name alias registry for the Visor Fonts CDN URL builder.
33
+ *
34
+ * Standard PostScript naming (Light/Regular/Medium/Bold/ExtraBold/Black)
35
+ * is handled by the WEIGHT_NAMES table in resolve.ts. Foundries that use
36
+ * non-standard names (e.g. Pangram Pangram's `Book` and `Super`) register
37
+ * per-family overrides here so theme authors can keep writing standard
38
+ * weight numbers in their .visor.yaml files.
39
+ *
40
+ * Family keys are exact-match (case-sensitive); weight keys are the numeric
41
+ * weight (300, 400, 500, …) as in WEIGHT_NAMES. The mapped string is the
42
+ * PostScript-style suffix that follows `{Family}-` in the bucket filename.
43
+ */
44
+ declare const FONT_WEIGHT_ALIASES: Record<string, Record<number, string>>;
45
+ declare function lookupFontWeightAlias(family: string, weight: number): string | null;
46
+
31
47
  /**
32
48
  * Preload hint generation for font loading performance.
33
49
  *
@@ -86,6 +102,34 @@ declare const googleFontsCatalog: GoogleFontEntry[];
86
102
  /** Look up a font family in the Google Fonts catalog (case-insensitive) */
87
103
  declare function lookupGoogleFont(family: string): GoogleFontEntry | undefined;
88
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
+
89
133
  /**
90
134
  * Import Pipeline
91
135
  *
@@ -824,6 +868,30 @@ declare const SEMANTIC_MAP: {
824
868
  interactive: Record<string, SemanticMapping>;
825
869
  };
826
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
+
827
895
  /**
828
896
  * CSS Generation (Stage 3 + Output)
829
897
  *
@@ -831,7 +899,9 @@ declare const SEMANTIC_MAP: {
831
899
  * packages/tokens/src/generate/generate-css.ts output format.
832
900
  */
833
901
 
834
- declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig): string;
902
+ declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig, options?: {
903
+ aliasedFamilies?: AliasedFamilies;
904
+ }): string;
835
905
  declare function generateSemanticCss(tokens: SemanticTokens): string;
836
906
  declare function generateLightCss(tokens: SemanticTokens): string;
837
907
  declare function generateDarkCss(tokens: SemanticTokens): string;
@@ -912,4 +982,4 @@ declare function cleanFontValue(val: string): string;
912
982
  */
913
983
  declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
914
984
 
915
- export { type CSSFile, ColorRole, type Confidence, type ExtractedToken, type ExtractionResult, 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, 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
@@ -1,4 +1,5 @@
1
1
  import {
2
+ FONT_WEIGHT_ALIASES,
2
3
  MATERIAL_TEXT_SLOTS,
3
4
  TAILWIND_GRAY,
4
5
  VISOR_FONTS_CDN,
@@ -19,6 +20,7 @@ import {
19
20
  hexToRgb,
20
21
  isValidColor,
21
22
  isValidHex,
23
+ lookupFontWeightAlias,
22
24
  lookupGoogleFont,
23
25
  normalizeHex,
24
26
  oklchToHex,
@@ -32,7 +34,154 @@ import {
32
34
  rgbToHex,
33
35
  rgbToOklch,
34
36
  serializeColor
35
- } from "./chunk-SXT2KY6D.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
+ }
36
185
 
37
186
  // src/pipeline.ts
38
187
  import { parse as parseYaml } from "yaml";
@@ -427,7 +576,7 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
427
576
  "slots"
428
577
  ]);
429
578
  var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
430
- var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
579
+ var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
431
580
  var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
432
581
  var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
433
582
  var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
@@ -799,7 +948,11 @@ function resolveConfig(config) {
799
948
  ...config.typography?.body?.weights && { weights: config.typography.body.weights }
800
949
  },
801
950
  mono: {
802
- 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 }
803
956
  },
804
957
  slots: config.typography?.slots ?? {}
805
958
  },
@@ -2626,6 +2779,7 @@ function extractFromCSS(files, name = "extracted-theme") {
2626
2779
  return { config, tokens, unmapped, warnings };
2627
2780
  }
2628
2781
  export {
2782
+ FONT_WEIGHT_ALIASES,
2629
2783
  SEMANTIC_MAP,
2630
2784
  TAILWIND_GRAY,
2631
2785
  VISOR_FONTS_CDN,
@@ -2657,6 +2811,7 @@ export {
2657
2811
  isValidColor,
2658
2812
  isValidHex,
2659
2813
  isVisorThemeConfig,
2814
+ lookupFontWeightAlias,
2660
2815
  lookupGoogleFont,
2661
2816
  normalizeHex,
2662
2817
  oklchToHex,
@@ -2675,5 +2830,6 @@ export {
2675
2830
  serializeColor,
2676
2831
  validate,
2677
2832
  validateConfig,
2833
+ validateFontCoverage,
2678
2834
  visor_theme_schema_default as visorThemeSchema
2679
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.4.2",
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",