@loworbitstudio/visor-theme-engine 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,46 @@ Themes are typically managed via the Visor CLI (`visor theme sync`). Direct API
24
24
  import { generateTheme } from '@loworbitstudio/visor-theme-engine'
25
25
  ```
26
26
 
27
+ ## Migration
28
+
29
+ ### Themes pinned to `^0.4.x` with a custom mono font
30
+
31
+ Engine 0.5 expanded `typography.mono` to accept `weight | weights | source | org` (previously only `family`). Engine 0.6 added `validate-coverage`, which errors when any `--font-*` declaration names a family with no matching `@font-face`. The combination created a trap: themes pinned to `^0.4.x` could only write `mono: { family: X }` (the only thing 0.4 allowed) and could not express the source/org fix the 0.6 error message points to.
32
+
33
+ To migrate:
34
+
35
+ 1. **Bump both** `@loworbitstudio/visor` (the CLI) to `≥ 0.10` and `@loworbitstudio/visor-theme-engine` to `≥ 0.6` together. The CLI transitively pins its own engine copy (CLI 0.10 → engine `^0.6.0`), so `visor theme sync` runs against the CLI-bundled engine, not the hoisted one — bumping the engine alone is silently insufficient.
36
+
37
+ 2. **Decide between inheritance and explicit declaration:**
38
+
39
+ - **Inheritance (preferred when applicable).** If your mono slot's family matches another slot (heading, display, or body) with `source`/`org` set, leave `typography.mono.source` and `typography.mono.org` unset. The engine will inherit `source`/`org` from the matching slot. Match precedence: heading → display → body, case-insensitive.
40
+
41
+ ```yaml
42
+ typography:
43
+ body:
44
+ family: PP Model Mono
45
+ weight: 400
46
+ source: visor-fonts
47
+ org: low-orbit-studio
48
+ mono:
49
+ family: PP Model Mono
50
+ weight: 400
51
+ # source/org inherited from body
52
+ ```
53
+
54
+ - **Explicit declaration.** Otherwise, add `source` (and `org` for `visor-fonts`) directly:
55
+
56
+ ```yaml
57
+ typography:
58
+ mono:
59
+ family: PP Model Mono
60
+ weight: 400
61
+ source: visor-fonts # or google-fonts, fontshare, local
62
+ org: low-orbit-studio # required for visor-fonts only
63
+ ```
64
+
65
+ System mono fonts (`SF Mono`, `JetBrains Mono`, `Source Code Pro`, `Menlo`, etc.) are already on the validator's `SYSTEM_FONTS` list and never need `source`/`org`.
66
+
27
67
  ## Documentation
28
68
 
29
69
  Full docs at [visor.loworbit.studio](https://visor.loworbit.studio).
@@ -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-CV0nmvMz.js';
2
2
 
3
3
  /**
4
4
  * Adapter types for the Visor theme engine.
@@ -22,6 +22,16 @@ interface AdapterOptions {
22
22
  interface NextJSAdapterOptions extends AdapterOptions {
23
23
  /** Include Google Fonts @import statements (default: true) */
24
24
  includeFontImports?: boolean;
25
+ /**
26
+ * Optional CSS selector that replaces `:root` in the generated output,
27
+ * enabling the body-class repaint pattern (e.g. `body.blacklight-theme`).
28
+ * When set, the dark-mode block scopes to `<scopePrefix>.dark`,
29
+ * `<scopePrefix>.theme-dark`, and `<scopePrefix>[data-theme="dark"]`;
30
+ * the `prefers-color-scheme: dark` media query composes the prefix with
31
+ * the existing `:not(.light)` guards. When omitted, output is unchanged
32
+ * (`:root`) for backward compatibility. See VI-368.
33
+ */
34
+ scopePrefix?: string;
25
35
  }
26
36
  /** Options specific to the Deck adapter. */
27
37
  interface DeckAdapterOptions extends AdapterOptions {
@@ -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-2O2DPCMJ.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,30 @@ ${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;
35
+ const scopePrefix = options?.scopePrefix;
30
36
  const lines = [];
37
+ const slug = toKebabCase(input.config.name);
38
+ const aliasedFamilies = /* @__PURE__ */ new Map();
31
39
  lines.push(header("Visor Theme \u2014 NextJS Adapter"));
32
40
  if (includeFontImports && input.config.typography) {
33
41
  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"
42
+ const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
43
+ for (const font of fontSlots) {
44
+ if (font && font.source === "visor-fonts" && !aliasedFamilies.has(font.family)) {
45
+ aliasedFamilies.set(font.family, aliasFamily(font.family, slug));
46
+ }
47
+ }
48
+ const hostedCssFonts = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono].filter(
49
+ (r) => r !== null && (r.source === "google-fonts" || r.source === "fontshare")
36
50
  );
37
51
  const seenUrls = /* @__PURE__ */ new Set();
38
- for (const font of googleFonts) {
52
+ for (const font of hostedCssFonts) {
39
53
  if (font?.cssUrl && !seenUrls.has(font.cssUrl)) {
40
54
  seenUrls.add(font.cssUrl);
41
55
  lines.push(`@import url("${font.cssUrl}");`);
@@ -59,10 +73,11 @@ function nextjsAdapter(input, options) {
59
73
  for (const font of visorFonts) {
60
74
  if (seenVisorFamilies.has(font.family)) continue;
61
75
  seenVisorFamilies.add(font.family);
76
+ const aliased = aliasedFamilies.get(font.family);
62
77
  for (const weight of font.weights) {
63
78
  const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
64
79
  lines.push(`@font-face {`);
65
- lines.push(` font-family: "${font.family}";`);
80
+ lines.push(` font-family: "${aliased}";`);
66
81
  lines.push(` src: url("${url}") format("woff2");`);
67
82
  lines.push(` font-weight: ${weight};`);
68
83
  lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
@@ -75,12 +90,15 @@ function nextjsAdapter(input, options) {
75
90
  lines.push(LAYER_ORDER);
76
91
  lines.push("");
77
92
  const primitivesBody = stripHeader(
78
- generatePrimitivesCss(input.primitives, input.config)
93
+ generatePrimitivesCss(input.primitives, input.config, {
94
+ aliasedFamilies,
95
+ scopePrefix
96
+ })
79
97
  );
80
98
  lines.push(wrapInLayer("visor-primitives", primitivesBody));
81
99
  lines.push("");
82
- const lightBody = stripHeader(generateLightCss(input.tokens));
83
- const darkBody = stripHeader(generateDarkCss(input.tokens));
100
+ const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
101
+ const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
84
102
  lines.push(
85
103
  wrapInLayer("visor-adaptive", lightBody + "\n\n" + darkBody)
86
104
  );
@@ -174,7 +192,7 @@ var SELECTIVE_SCALE_ROLES = [
174
192
  "error",
175
193
  "info"
176
194
  ];
177
- function toKebabCase(name) {
195
+ function toKebabCase2(name) {
178
196
  return name.toLowerCase().replace(/\s+/g, "-");
179
197
  }
180
198
  function generateScopedPrimitives(primitives, config) {
@@ -226,7 +244,7 @@ function generateSemanticDecls(tokens, mode) {
226
244
  return decls;
227
245
  }
228
246
  function deckAdapter(input, options) {
229
- const scopeClass = options?.scopeClass ?? `.deck--${toKebabCase(input.config.name)}`;
247
+ const scopeClass = options?.scopeClass ?? `.deck--${toKebabCase2(input.config.name)}`;
230
248
  const lines = [];
231
249
  lines.push(header(`Visor Theme \u2014 Deck Adapter (${scopeClass})`));
232
250
  const primDecls = generateScopedPrimitives(input.primitives, input.config);
@@ -260,7 +278,7 @@ function deckAdapter(input, options) {
260
278
  // src/adapters/docs.ts
261
279
  var FULL_SCALE_ROLES2 = ["primary", "accent", "neutral"];
262
280
  var SELECTIVE_SCALE_ROLES2 = ["success", "warning", "error", "info"];
263
- function toKebabCase2(name) {
281
+ function toKebabCase3(name) {
264
282
  return name.toLowerCase().replace(/\s+/g, "-");
265
283
  }
266
284
  function generateColorDecls(primitives) {
@@ -298,13 +316,14 @@ function generateRadiusDecls(config) {
298
316
  `--radius-full: ${config.radius.pill}px;`
299
317
  ];
300
318
  }
301
- function generateTypographyDecls(config) {
319
+ function generateTypographyDecls(config, aliases) {
302
320
  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};`);
321
+ const headingFamily = config.typography.heading?.family ?? config.typography.body.family;
322
+ decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
323
+ decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
324
+ decls.push(`--font-heading: ${fontStack(headingFamily, aliases)};`);
325
+ decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
326
+ decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
308
327
  const fontSizes = {
309
328
  xs: 12,
310
329
  sm: 14,
@@ -415,31 +434,38 @@ function sectionComment2(label) {
415
434
  /* --- ${label} --- */`;
416
435
  }
417
436
  function docsAdapter(input, options) {
418
- const slug = toKebabCase2(input.config.name);
437
+ const slug = toKebabCase3(input.config.name);
419
438
  const scopeClass = `.${slug}-theme`;
420
439
  const includeFontImports = options?.includeFontImports ?? true;
421
440
  const fontLines = [];
422
441
  const lines = [];
442
+ const aliasedFamilies = /* @__PURE__ */ new Map();
423
443
  if (includeFontImports && input.config.typography) {
424
444
  const fontResult = resolveThemeFonts(input.config.typography);
425
445
  const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
446
+ for (const font of fontSlots) {
447
+ if (font && font.source === "visor-fonts" && !aliasedFamilies.has(font.family)) {
448
+ aliasedFamilies.set(font.family, aliasFamily(font.family, slug));
449
+ }
450
+ }
426
451
  const seenUrls = /* @__PURE__ */ new Set();
427
452
  for (const font of fontSlots) {
428
- if (font && font.source === "google-fonts" && font.cssUrl && !seenUrls.has(font.cssUrl)) {
453
+ if (font && (font.source === "google-fonts" || font.source === "fontshare") && font.cssUrl && !seenUrls.has(font.cssUrl)) {
429
454
  seenUrls.add(font.cssUrl);
430
455
  fontLines.push(`@import url("${font.cssUrl}");`);
431
456
  fontLines.push("");
432
457
  }
433
458
  }
434
459
  const scale = input.config.typography?.scale ?? 1;
435
- const seenFamilies = /* @__PURE__ */ new Set();
460
+ const emittedFamilies = /* @__PURE__ */ new Set();
436
461
  for (const font of fontSlots) {
437
- if (font && font.source === "visor-fonts" && !seenFamilies.has(font.family)) {
438
- seenFamilies.add(font.family);
462
+ if (font && font.source === "visor-fonts" && !emittedFamilies.has(font.family)) {
463
+ emittedFamilies.add(font.family);
464
+ const aliased = aliasedFamilies.get(font.family);
439
465
  for (const weight of font.weights) {
440
466
  const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
441
467
  fontLines.push("@font-face {");
442
- fontLines.push(` font-family: "${font.family}";`);
468
+ fontLines.push(` font-family: "${aliased}";`);
443
469
  fontLines.push(` src: url("${url}") format("woff2");`);
444
470
  fontLines.push(` font-weight: ${weight};`);
445
471
  fontLines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
@@ -475,7 +501,7 @@ function docsAdapter(input, options) {
475
501
  lines.push(block(scopeClass, generateRadiusDecls(input.config)));
476
502
  lines.push("");
477
503
  lines.push(sectionComment2("Primitive: Typography"));
478
- lines.push(block(scopeClass, generateTypographyDecls(input.config)));
504
+ lines.push(block(scopeClass, generateTypographyDecls(input.config, aliasedFamilies)));
479
505
  lines.push("");
480
506
  lines.push(sectionComment2("Primitive: Shadows"));
481
507
  lines.push(block(scopeClass, generateShadowDecls(input.config)));
@@ -122,6 +122,10 @@ var FONT_WEIGHT_ALIASES = {
122
122
  400: "Book",
123
123
  800: "Super"
124
124
  },
125
+ "PP Model Sans": {
126
+ 400: "Book",
127
+ 800: "Super"
128
+ },
125
129
  "PP Model Plastic": {
126
130
  400: "Book",
127
131
  800: "Super"
@@ -151,6 +155,8 @@ function buildGoogleFontsCssUrl(family, weights, italic, display) {
151
155
  return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${wghtValues}&display=${display}`;
152
156
  }
153
157
  var VISOR_FONTS_CDN = "https://fonts.visor.design";
158
+ var FONTSHARE_API_ORIGIN = "https://api.fontshare.com";
159
+ var FONTSHARE_CDN_ORIGIN = "https://cdn.fontshare.com";
154
160
  function buildFamilySlug(family) {
155
161
  return family.toLowerCase().replace(/ /g, "-");
156
162
  }
@@ -174,6 +180,16 @@ function buildVisorFontUrl(org, family, weight) {
174
180
  const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
175
181
  return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
176
182
  }
183
+ function buildFontshareCssUrl(family, weights, italic, display) {
184
+ const slug = buildFamilySlug(family);
185
+ const sortedWeights = [...weights].sort((a, b) => a - b);
186
+ const tokens = [];
187
+ if (italic) {
188
+ for (const w of sortedWeights) tokens.push(`${w}i`);
189
+ }
190
+ for (const w of sortedWeights) tokens.push(`${w}`);
191
+ return `${FONTSHARE_API_ORIGIN}/v2/css?f[]=${slug}@${tokens.join(",")}&display=${display}`;
192
+ }
177
193
  function resolveFont(family, options = {}) {
178
194
  const display = options.display ?? DEFAULT_DISPLAY;
179
195
  const requestedWeights = options.weights ?? DEFAULT_WEIGHTS;
@@ -192,6 +208,20 @@ function resolveFont(family, options = {}) {
192
208
  org: options.org ?? null
193
209
  };
194
210
  }
211
+ if (explicitSource === "fontshare") {
212
+ const cssUrl = buildFontshareCssUrl(family, requestedWeights, italic, display);
213
+ return {
214
+ family,
215
+ source: "fontshare",
216
+ cssUrl,
217
+ weights: requestedWeights,
218
+ italic,
219
+ display,
220
+ category: options.category ?? "sans-serif",
221
+ guidance: null,
222
+ org: null
223
+ };
224
+ }
195
225
  if (explicitSource === "local") {
196
226
  return {
197
227
  family,
@@ -288,6 +318,20 @@ function generatePreloadLinks(resolutions, customFontPaths) {
288
318
  }
289
319
  }
290
320
  }
321
+ const hasFontshare = resolutions.some((r) => r.source === "fontshare");
322
+ if (hasFontshare) {
323
+ links.push(`<link rel="preconnect" href="${FONTSHARE_API_ORIGIN}">`);
324
+ links.push(
325
+ `<link rel="preconnect" href="${FONTSHARE_CDN_ORIGIN}" crossorigin>`
326
+ );
327
+ for (const resolution of resolutions) {
328
+ if (resolution.source === "fontshare" && resolution.cssUrl) {
329
+ links.push(
330
+ `<link rel="preload" as="style" href="${resolution.cssUrl}">`
331
+ );
332
+ }
333
+ }
334
+ }
291
335
  if (customFontPaths) {
292
336
  for (const resolution of resolutions) {
293
337
  if (resolution.source === "local") {
@@ -307,7 +351,7 @@ function generatePreloadLinks(resolutions, customFontPaths) {
307
351
  function generateStylesheetLinks(resolutions) {
308
352
  const links = [];
309
353
  for (const resolution of resolutions) {
310
- if (resolution.source === "google-fonts" && resolution.cssUrl) {
354
+ if ((resolution.source === "google-fonts" || resolution.source === "fontshare") && resolution.cssUrl) {
311
355
  links.push(
312
356
  `<link rel="stylesheet" href="${resolution.cssUrl}">`
313
357
  );
@@ -330,6 +374,19 @@ function generateFontCSS(heading, displayFont, body, mono, typography) {
330
374
  }
331
375
  lines.push("");
332
376
  }
377
+ const fontshareFonts = allSlots.filter(
378
+ (r) => r !== null && r.source === "fontshare"
379
+ );
380
+ const seenFontshareUrls = /* @__PURE__ */ new Set();
381
+ if (fontshareFonts.length > 0) {
382
+ lines.push("/* Fontshare \u2014 load these stylesheets in your HTML <head> */");
383
+ for (const font of fontshareFonts) {
384
+ if (!font.cssUrl || seenFontshareUrls.has(font.cssUrl)) continue;
385
+ seenFontshareUrls.add(font.cssUrl);
386
+ lines.push(`/* ${font.cssUrl} */`);
387
+ }
388
+ lines.push("");
389
+ }
333
390
  const visorFonts = allSlots.filter(
334
391
  (r) => r !== null && r.source === "visor-fonts"
335
392
  );
@@ -570,13 +627,29 @@ function resolveThemeFonts(typography, options) {
570
627
  }
571
628
  let monoResolution = null;
572
629
  if (typography.mono?.family) {
573
- const monoWeights = [];
574
- if (typography.mono.weight) monoWeights.push(typography.mono.weight);
630
+ const monoWeights = typography.mono.weights ? [...typography.mono.weights] : typography.mono.weight ? [typography.mono.weight] : [];
631
+ let monoSource = typography.mono.source;
632
+ let monoOrg = typography.mono.org;
633
+ if (!monoSource) {
634
+ const monoFamilyLower = typography.mono.family.toLowerCase();
635
+ const candidates = [
636
+ { resolution: headingResolution, configSource: typography.heading?.source, configOrg: typography.heading?.org },
637
+ { resolution: displayResolution, configSource: typography.display?.source, configOrg: typography.display?.org },
638
+ { resolution: bodyResolution, configSource: typography.body?.source, configOrg: typography.body?.org }
639
+ ];
640
+ for (const candidate of candidates) {
641
+ if (candidate.resolution && candidate.configSource && candidate.resolution.family.toLowerCase() === monoFamilyLower) {
642
+ monoSource = candidate.configSource;
643
+ monoOrg = candidate.configOrg;
644
+ break;
645
+ }
646
+ }
647
+ }
575
648
  monoResolution = resolveFont(typography.mono.family, {
576
649
  weights: monoWeights.length > 0 ? monoWeights : void 0,
577
650
  display,
578
- source: typography.mono.source,
579
- org: typography.mono.org,
651
+ source: monoSource,
652
+ org: monoOrg,
580
653
  category: "monospace"
581
654
  });
582
655
  if (monoResolution.guidance) {
@@ -1069,6 +1142,17 @@ function generateShadeScale(color, role) {
1069
1142
  return scale;
1070
1143
  }
1071
1144
 
1145
+ // src/fonts/theme-alias.ts
1146
+ var EMPTY_ALIASES = /* @__PURE__ */ new Map();
1147
+ function aliasFamily(family, themeSlug) {
1148
+ return `${family} [${themeSlug}]`;
1149
+ }
1150
+ function fontStack(bare, aliases) {
1151
+ const aliased = aliases.get(bare);
1152
+ if (!aliased) return bare;
1153
+ return `"${aliased}", "${bare}"`;
1154
+ }
1155
+
1072
1156
  // src/generate-css.ts
1073
1157
  function header(label) {
1074
1158
  return [
@@ -1154,15 +1238,15 @@ function generateShadowPrimitives(config) {
1154
1238
  `--shadow-xl: ${config.shadows.xl};`
1155
1239
  ];
1156
1240
  }
1157
- function generateTypographyPrimitives(config) {
1241
+ function generateTypographyPrimitives(config, aliases = EMPTY_ALIASES) {
1158
1242
  const decls = [];
1159
1243
  const scale = config.typography.scale;
1160
1244
  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};`);
1245
+ decls.push(`--font-heading: ${fontStack(config.typography.heading.family, aliases)};`);
1246
+ decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
1247
+ decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
1248
+ decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
1249
+ decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
1166
1250
  const fontSizes = {
1167
1251
  xs: 12,
1168
1252
  sm: 14,
@@ -1233,24 +1317,25 @@ function generateMiscPrimitives() {
1233
1317
  "--focus-ring-offset: 2px;"
1234
1318
  ];
1235
1319
  }
1236
- function generatePrimitivesCss(primitives, config) {
1320
+ function generatePrimitivesCss(primitives, config, options) {
1237
1321
  const lines = [];
1322
+ const host = options?.scopePrefix ?? ":root";
1238
1323
  lines.push(sectionComment("Primitive: Colors"));
1239
1324
  lines.push(
1240
- block(":root", [generateColorPrimitives(primitives)])
1325
+ block(host, [generateColorPrimitives(primitives)])
1241
1326
  );
1242
1327
  lines.push(sectionComment("Primitive: Spacing"));
1243
- lines.push(block(":root", generateSpacingPrimitives(config)));
1328
+ lines.push(block(host, generateSpacingPrimitives(config)));
1244
1329
  lines.push(sectionComment("Primitive: Border Radius"));
1245
- lines.push(block(":root", generateRadiusPrimitives(config)));
1330
+ lines.push(block(host, generateRadiusPrimitives(config)));
1246
1331
  lines.push(sectionComment("Primitive: Typography"));
1247
- lines.push(block(":root", generateTypographyPrimitives(config)));
1332
+ lines.push(block(host, generateTypographyPrimitives(config, options?.aliasedFamilies)));
1248
1333
  lines.push(sectionComment("Primitive: Shadows"));
1249
- lines.push(block(":root", generateShadowPrimitives(config)));
1334
+ lines.push(block(host, generateShadowPrimitives(config)));
1250
1335
  lines.push(sectionComment("Primitive: Motion"));
1251
- lines.push(block(":root", generateMotionPrimitives(config)));
1336
+ lines.push(block(host, generateMotionPrimitives(config)));
1252
1337
  lines.push(sectionComment("Primitive: Miscellaneous"));
1253
- lines.push(block(":root", generateMiscPrimitives()));
1338
+ lines.push(block(host, generateMiscPrimitives()));
1254
1339
  return header("Visor Theme \u2014 Primitives") + lines.join("\n");
1255
1340
  }
1256
1341
  function generateSemanticCss(tokens) {
@@ -1292,25 +1377,27 @@ function buildAdaptiveDecls(tokens, theme) {
1292
1377
  );
1293
1378
  return { textDecls, surfaceDecls, borderDecls, interactiveDecls };
1294
1379
  }
1295
- function generateLightCss(tokens) {
1380
+ function generateLightCss(tokens, options) {
1296
1381
  const lines = [];
1297
1382
  const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "light");
1383
+ const host = options?.scopePrefix ?? ":root";
1298
1384
  lines.push(sectionComment("Adaptive: Text (light)"));
1299
- lines.push(block(":root", textDecls));
1385
+ lines.push(block(host, textDecls));
1300
1386
  lines.push(sectionComment("Adaptive: Surface (light)"));
1301
- lines.push(block(":root", surfaceDecls));
1387
+ lines.push(block(host, surfaceDecls));
1302
1388
  lines.push(sectionComment("Adaptive: Border (light)"));
1303
- lines.push(block(":root", borderDecls));
1389
+ lines.push(block(host, borderDecls));
1304
1390
  lines.push(sectionComment("Adaptive: Interactive (light)"));
1305
- lines.push(block(":root", interactiveDecls));
1391
+ lines.push(block(host, interactiveDecls));
1306
1392
  return header("Visor Theme \u2014 Light") + lines.join("\n");
1307
1393
  }
1308
- function generateDarkCss(tokens) {
1394
+ function generateDarkCss(tokens, options) {
1309
1395
  const lines = [];
1310
1396
  const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "dark");
1311
- const darkSelectors = [".dark", ".theme-dark", '[data-theme="dark"]'];
1397
+ const prefix = options?.scopePrefix;
1398
+ const darkSelectors = prefix ? [`${prefix}.dark`, `${prefix}.theme-dark`, `${prefix}[data-theme="dark"]`] : [".dark", ".theme-dark", '[data-theme="dark"]'];
1312
1399
  const darkSelector = darkSelectors.join(",\n");
1313
- const prefersSelector = ':root:not(.light):not(.theme-light):not([data-theme="light"])';
1400
+ const prefersSelector = prefix ? `${prefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])';
1314
1401
  lines.push(sectionComment("Adaptive: Text (dark) \u2014 manual toggle"));
1315
1402
  lines.push(block(darkSelector, textDecls));
1316
1403
  lines.push(sectionComment("Adaptive: Surface (dark) \u2014 manual toggle"));
@@ -1437,6 +1524,8 @@ export {
1437
1524
  SELECTIVE_SHADE_STEPS,
1438
1525
  TAILWIND_GRAY,
1439
1526
  generateShadeScale,
1527
+ aliasFamily,
1528
+ fontStack,
1440
1529
  header,
1441
1530
  sectionComment,
1442
1531
  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-CV0nmvMz.js';
2
+ export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-CV0nmvMz.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -102,6 +102,44 @@ 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
+ /**
132
+ * Format a font coverage error for surfacing through the CLI / private-themes
133
+ * generator. The mono slot gets an additional sentence calling out the engine
134
+ * version requirement and the CLI/engine version coupling (VI-367 / BO-37):
135
+ * bumping the engine alone is silently insufficient because the visor CLI
136
+ * transitively pins its own engine copy.
137
+ *
138
+ * Filename is included so multi-theme runs surface which theme is failing.
139
+ */
140
+ declare function formatFontCoverageError(filename: string, declaredAt: string, family: string): string;
141
+ declare function validateFontCoverage(css: string): FontCoverageResult;
142
+
105
143
  /**
106
144
  * Import Pipeline
107
145
  *
@@ -840,6 +878,30 @@ declare const SEMANTIC_MAP: {
840
878
  interactive: Record<string, SemanticMapping>;
841
879
  };
842
880
 
881
+ /**
882
+ * Per-theme font-family aliasing — substrate fix for VI-354.
883
+ *
884
+ * `@font-face` declarations are global to the document, so co-loaded themes
885
+ * that share a font family with differing per-theme properties (e.g.
886
+ * `size-adjust`) silently overwrite each other. Aliasing each theme's
887
+ * `@font-face` family as `{family} [{slug}]` scopes the declaration to
888
+ * that theme only; the theme's `--font-*` vars then list the alias first
889
+ * with the bare family as a fallback for graceful degradation.
890
+ *
891
+ * Lives in `fonts/` (not `adapters/`) because every adapter that emits
892
+ * visor-fonts `@font-face` blocks needs the same aliasing rules. Sharing
893
+ * the helpers prevents drift between adapters.
894
+ */
895
+ /**
896
+ * Map of `bare family name → aliased family name` for every family the
897
+ * theme emits as a per-theme `@font-face`. The alias applies to every
898
+ * `--font-*` whose family matches an entry, regardless of which slot the
899
+ * var represents (the bug repro in VI-354 hinges on this for --font-mono,
900
+ * which can resolve to the same family as heading/body but doesn't carry
901
+ * the visor-fonts source through `resolveConfig`).
902
+ */
903
+ type AliasedFamilies = ReadonlyMap<string, string>;
904
+
843
905
  /**
844
906
  * CSS Generation (Stage 3 + Output)
845
907
  *
@@ -847,10 +909,17 @@ declare const SEMANTIC_MAP: {
847
909
  * packages/tokens/src/generate/generate-css.ts output format.
848
910
  */
849
911
 
850
- declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig): string;
912
+ declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig, options?: {
913
+ aliasedFamilies?: AliasedFamilies;
914
+ scopePrefix?: string;
915
+ }): string;
851
916
  declare function generateSemanticCss(tokens: SemanticTokens): string;
852
- declare function generateLightCss(tokens: SemanticTokens): string;
853
- declare function generateDarkCss(tokens: SemanticTokens): string;
917
+ declare function generateLightCss(tokens: SemanticTokens, options?: {
918
+ scopePrefix?: string;
919
+ }): string;
920
+ declare function generateDarkCss(tokens: SemanticTokens, options?: {
921
+ scopePrefix?: string;
922
+ }): string;
854
923
  declare function generateFullBundleCss(primitives: GeneratedPrimitives, tokens: SemanticTokens, config: ResolvedThemeConfig): string;
855
924
 
856
925
  /**
@@ -928,4 +997,4 @@ declare function cleanFontValue(val: string): string;
928
997
  */
929
998
  declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
930
999
 
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 };
1000
+ 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, formatFontCoverageError, 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,161 @@ import {
34
34
  rgbToHex,
35
35
  rgbToOklch,
36
36
  serializeColor
37
- } from "./chunk-U5FXQ5EC.js";
37
+ } from "./chunk-2O2DPCMJ.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 formatFontCoverageError(filename, declaredAt, family) {
170
+ const base = `${filename}: ${declaredAt} declares "${family}" with no matching @font-face. `;
171
+ if (declaredAt === "--font-mono") {
172
+ return base + `Set typography.mono.source: visor-fonts (with org:), google-fonts, or fontshare; or pick a system mono font. The mono slot's source/org keys require @loworbitstudio/visor-theme-engine \u2265 0.5.0 and @loworbitstudio/visor \u2265 0.10.0 \u2014 bump both, since the CLI bundles its own engine copy.`;
173
+ }
174
+ return base + `Set typography.<slot>.source: visor-fonts (with org:), google-fonts, or fontshare; or pick a system font.`;
175
+ }
176
+ function validateFontCoverage(css) {
177
+ const declaredFamilies = extractFontFaceFamilies(css);
178
+ for (const f of extractGoogleFontsImports(css)) declaredFamilies.add(f);
179
+ for (const f of extractFontshareImports(css)) declaredFamilies.add(f);
180
+ const declarations = extractFontVarDeclarations(css);
181
+ const errors = [];
182
+ const seen = /* @__PURE__ */ new Set();
183
+ for (const decl of declarations) {
184
+ if (declaredFamilies.has(decl.family)) continue;
185
+ const key = `${decl.slot}::${decl.family}`;
186
+ if (seen.has(key)) continue;
187
+ seen.add(key);
188
+ errors.push({ family: decl.family, declaredAt: decl.slot });
189
+ }
190
+ return { errors };
191
+ }
38
192
 
39
193
  // src/pipeline.ts
40
194
  import { parse as parseYaml } from "yaml";
@@ -429,7 +583,7 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
429
583
  "slots"
430
584
  ]);
431
585
  var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
432
- var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
586
+ var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
433
587
  var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
434
588
  var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
435
589
  var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
@@ -763,6 +917,7 @@ function resolveConfig(config) {
763
917
  }
764
918
  return {
765
919
  name: config.name,
920
+ ...config.label !== void 0 && { label: config.label },
766
921
  version: 1,
767
922
  colors: {
768
923
  primary: colors.primary,
@@ -801,7 +956,11 @@ function resolveConfig(config) {
801
956
  ...config.typography?.body?.weights && { weights: config.typography.body.weights }
802
957
  },
803
958
  mono: {
804
- family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
959
+ family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family,
960
+ ...config.typography?.mono?.weight && { weight: config.typography.mono.weight },
961
+ ...config.typography?.mono?.weights && { weights: config.typography.mono.weights },
962
+ ...config.typography?.mono?.source && { source: config.typography.mono.source },
963
+ ...config.typography?.mono?.org && { org: config.typography.mono.org }
805
964
  },
806
965
  slots: config.typography?.slots ?? {}
807
966
  },
@@ -2640,6 +2799,7 @@ export {
2640
2799
  compositeOverBackground,
2641
2800
  exportTheme,
2642
2801
  extractFromCSS,
2802
+ formatFontCoverageError,
2643
2803
  generateDarkCss,
2644
2804
  generateFullBundleCss,
2645
2805
  generateLightCss,
@@ -2679,5 +2839,6 @@ export {
2679
2839
  serializeColor,
2680
2840
  validate,
2681
2841
  validateConfig,
2842
+ validateFontCoverage,
2682
2843
  visor_theme_schema_default as visorThemeSchema
2683
2844
  };
@@ -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[];
@@ -70,6 +70,8 @@ interface VisorTypography {
70
70
  mono?: {
71
71
  family: string;
72
72
  weight?: number;
73
+ /** Explicit list of font weights to load (overrides engine defaults) */
74
+ weights?: number[];
73
75
  source?: FontSource;
74
76
  org?: string;
75
77
  };
@@ -198,6 +200,7 @@ interface VisorThemeConfig {
198
200
  mono?: {
199
201
  family?: string;
200
202
  weight?: number;
203
+ weights?: number[];
201
204
  source?: FontSource;
202
205
  org?: string;
203
206
  };
@@ -266,6 +269,8 @@ interface VisorThemeConfig {
266
269
  /** Config with all defaults resolved */
267
270
  interface ResolvedThemeConfig {
268
271
  name: string;
272
+ /** Optional display label override forwarded from VisorThemeConfig.label. */
273
+ label?: string;
269
274
  version: 1;
270
275
  colors: {
271
276
  primary: string;
@@ -304,6 +309,10 @@ interface ResolvedThemeConfig {
304
309
  };
305
310
  mono: {
306
311
  family: string;
312
+ weight?: number;
313
+ weights?: number[];
314
+ source?: FontSource;
315
+ org?: string;
307
316
  };
308
317
  /**
309
318
  * 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.8.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",