@loworbitstudio/visor-theme-engine 0.6.0 → 0.8.1

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-CtozYHw0.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 {
@@ -13,7 +13,7 @@ import {
13
13
  parseColor,
14
14
  resolveThemeFonts,
15
15
  sectionComment
16
- } from "../chunk-4U5L3AWY.js";
16
+ } from "../chunk-OJVNL7KN.js";
17
17
 
18
18
  // src/adapters/layers.ts
19
19
  var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
@@ -32,6 +32,7 @@ function toKebabCase(name) {
32
32
  function nextjsAdapter(input, options) {
33
33
  const includeFontImports = options?.includeFontImports ?? true;
34
34
  const includeFowt = options?.includeFowt ?? true;
35
+ const scopePrefix = options?.scopePrefix;
35
36
  const lines = [];
36
37
  const slug = toKebabCase(input.config.name);
37
38
  const aliasedFamilies = /* @__PURE__ */ new Map();
@@ -89,12 +90,15 @@ function nextjsAdapter(input, options) {
89
90
  lines.push(LAYER_ORDER);
90
91
  lines.push("");
91
92
  const primitivesBody = stripHeader(
92
- generatePrimitivesCss(input.primitives, input.config, { aliasedFamilies })
93
+ generatePrimitivesCss(input.primitives, input.config, {
94
+ aliasedFamilies,
95
+ scopePrefix
96
+ })
93
97
  );
94
98
  lines.push(wrapInLayer("visor-primitives", primitivesBody));
95
99
  lines.push("");
96
- const lightBody = stripHeader(generateLightCss(input.tokens));
97
- const darkBody = stripHeader(generateDarkCss(input.tokens));
100
+ const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
101
+ const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
98
102
  lines.push(
99
103
  wrapInLayer("visor-adaptive", lightBody + "\n\n" + darkBody)
100
104
  );
@@ -122,9 +122,18 @@ 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"
132
+ },
133
+ // Hoefler's Gotham uses "Book" instead of "Regular" at weight 400.
134
+ // Light (300) and Medium (500) match WEIGHT_NAMES defaults.
135
+ Gotham: {
136
+ 400: "Book"
128
137
  }
129
138
  };
130
139
  function lookupFontWeightAlias(family, weight) {
@@ -623,13 +632,29 @@ function resolveThemeFonts(typography, options) {
623
632
  }
624
633
  let monoResolution = null;
625
634
  if (typography.mono?.family) {
626
- const monoWeights = [];
627
- if (typography.mono.weight) monoWeights.push(typography.mono.weight);
635
+ const monoWeights = typography.mono.weights ? [...typography.mono.weights] : typography.mono.weight ? [typography.mono.weight] : [];
636
+ let monoSource = typography.mono.source;
637
+ let monoOrg = typography.mono.org;
638
+ if (!monoSource) {
639
+ const monoFamilyLower = typography.mono.family.toLowerCase();
640
+ const candidates = [
641
+ { resolution: headingResolution, configSource: typography.heading?.source, configOrg: typography.heading?.org },
642
+ { resolution: displayResolution, configSource: typography.display?.source, configOrg: typography.display?.org },
643
+ { resolution: bodyResolution, configSource: typography.body?.source, configOrg: typography.body?.org }
644
+ ];
645
+ for (const candidate of candidates) {
646
+ if (candidate.resolution && candidate.configSource && candidate.resolution.family.toLowerCase() === monoFamilyLower) {
647
+ monoSource = candidate.configSource;
648
+ monoOrg = candidate.configOrg;
649
+ break;
650
+ }
651
+ }
652
+ }
628
653
  monoResolution = resolveFont(typography.mono.family, {
629
654
  weights: monoWeights.length > 0 ? monoWeights : void 0,
630
655
  display,
631
- source: typography.mono.source,
632
- org: typography.mono.org,
656
+ source: monoSource,
657
+ org: monoOrg,
633
658
  category: "monospace"
634
659
  });
635
660
  if (monoResolution.guidance) {
@@ -1299,22 +1324,23 @@ function generateMiscPrimitives() {
1299
1324
  }
1300
1325
  function generatePrimitivesCss(primitives, config, options) {
1301
1326
  const lines = [];
1327
+ const host = options?.scopePrefix ?? ":root";
1302
1328
  lines.push(sectionComment("Primitive: Colors"));
1303
1329
  lines.push(
1304
- block(":root", [generateColorPrimitives(primitives)])
1330
+ block(host, [generateColorPrimitives(primitives)])
1305
1331
  );
1306
1332
  lines.push(sectionComment("Primitive: Spacing"));
1307
- lines.push(block(":root", generateSpacingPrimitives(config)));
1333
+ lines.push(block(host, generateSpacingPrimitives(config)));
1308
1334
  lines.push(sectionComment("Primitive: Border Radius"));
1309
- lines.push(block(":root", generateRadiusPrimitives(config)));
1335
+ lines.push(block(host, generateRadiusPrimitives(config)));
1310
1336
  lines.push(sectionComment("Primitive: Typography"));
1311
- lines.push(block(":root", generateTypographyPrimitives(config, options?.aliasedFamilies)));
1337
+ lines.push(block(host, generateTypographyPrimitives(config, options?.aliasedFamilies)));
1312
1338
  lines.push(sectionComment("Primitive: Shadows"));
1313
- lines.push(block(":root", generateShadowPrimitives(config)));
1339
+ lines.push(block(host, generateShadowPrimitives(config)));
1314
1340
  lines.push(sectionComment("Primitive: Motion"));
1315
- lines.push(block(":root", generateMotionPrimitives(config)));
1341
+ lines.push(block(host, generateMotionPrimitives(config)));
1316
1342
  lines.push(sectionComment("Primitive: Miscellaneous"));
1317
- lines.push(block(":root", generateMiscPrimitives()));
1343
+ lines.push(block(host, generateMiscPrimitives()));
1318
1344
  return header("Visor Theme \u2014 Primitives") + lines.join("\n");
1319
1345
  }
1320
1346
  function generateSemanticCss(tokens) {
@@ -1356,25 +1382,27 @@ function buildAdaptiveDecls(tokens, theme) {
1356
1382
  );
1357
1383
  return { textDecls, surfaceDecls, borderDecls, interactiveDecls };
1358
1384
  }
1359
- function generateLightCss(tokens) {
1385
+ function generateLightCss(tokens, options) {
1360
1386
  const lines = [];
1361
1387
  const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "light");
1388
+ const host = options?.scopePrefix ?? ":root";
1362
1389
  lines.push(sectionComment("Adaptive: Text (light)"));
1363
- lines.push(block(":root", textDecls));
1390
+ lines.push(block(host, textDecls));
1364
1391
  lines.push(sectionComment("Adaptive: Surface (light)"));
1365
- lines.push(block(":root", surfaceDecls));
1392
+ lines.push(block(host, surfaceDecls));
1366
1393
  lines.push(sectionComment("Adaptive: Border (light)"));
1367
- lines.push(block(":root", borderDecls));
1394
+ lines.push(block(host, borderDecls));
1368
1395
  lines.push(sectionComment("Adaptive: Interactive (light)"));
1369
- lines.push(block(":root", interactiveDecls));
1396
+ lines.push(block(host, interactiveDecls));
1370
1397
  return header("Visor Theme \u2014 Light") + lines.join("\n");
1371
1398
  }
1372
- function generateDarkCss(tokens) {
1399
+ function generateDarkCss(tokens, options) {
1373
1400
  const lines = [];
1374
1401
  const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "dark");
1375
- const darkSelectors = [".dark", ".theme-dark", '[data-theme="dark"]'];
1402
+ const prefix = options?.scopePrefix;
1403
+ const darkSelectors = prefix ? [`${prefix}.dark`, `${prefix}.theme-dark`, `${prefix}[data-theme="dark"]`] : [".dark", ".theme-dark", '[data-theme="dark"]'];
1376
1404
  const darkSelector = darkSelectors.join(",\n");
1377
- const prefersSelector = ':root:not(.light):not(.theme-light):not([data-theme="light"])';
1405
+ const prefersSelector = prefix ? `${prefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])';
1378
1406
  lines.push(sectionComment("Adaptive: Text (dark) \u2014 manual toggle"));
1379
1407
  lines.push(block(darkSelector, textDecls));
1380
1408
  lines.push(sectionComment("Adaptive: Surface (dark) \u2014 manual toggle"));
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-CtozYHw0.js';
2
- export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-CtozYHw0.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.
@@ -128,6 +128,16 @@ interface FontCoverageError {
128
128
  interface FontCoverageResult {
129
129
  errors: FontCoverageError[];
130
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;
131
141
  declare function validateFontCoverage(css: string): FontCoverageResult;
132
142
 
133
143
  /**
@@ -901,10 +911,15 @@ type AliasedFamilies = ReadonlyMap<string, string>;
901
911
 
902
912
  declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig, options?: {
903
913
  aliasedFamilies?: AliasedFamilies;
914
+ scopePrefix?: string;
904
915
  }): string;
905
916
  declare function generateSemanticCss(tokens: SemanticTokens): string;
906
- declare function generateLightCss(tokens: SemanticTokens): string;
907
- 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;
908
923
  declare function generateFullBundleCss(primitives: GeneratedPrimitives, tokens: SemanticTokens, config: ResolvedThemeConfig): string;
909
924
 
910
925
  /**
@@ -982,4 +997,4 @@ declare function cleanFontValue(val: string): string;
982
997
  */
983
998
  declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
984
999
 
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 };
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,7 @@ import {
34
34
  rgbToHex,
35
35
  rgbToOklch,
36
36
  serializeColor
37
- } from "./chunk-4U5L3AWY.js";
37
+ } from "./chunk-OJVNL7KN.js";
38
38
 
39
39
  // src/fonts/validate-coverage.ts
40
40
  var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
@@ -166,6 +166,13 @@ function extractFontVarDeclarations(css) {
166
166
  }
167
167
  return decls;
168
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
+ }
169
176
  function validateFontCoverage(css) {
170
177
  const declaredFamilies = extractFontFaceFamilies(css);
171
178
  for (const f of extractGoogleFontsImports(css)) declaredFamilies.add(f);
@@ -910,6 +917,7 @@ function resolveConfig(config) {
910
917
  }
911
918
  return {
912
919
  name: config.name,
920
+ ...config.label !== void 0 && { label: config.label },
913
921
  version: 1,
914
922
  colors: {
915
923
  primary: colors.primary,
@@ -1616,6 +1624,41 @@ var KNOWN_SEMANTIC_TOKENS = /* @__PURE__ */ new Set([
1616
1624
  ...Object.keys(SEMANTIC_BORDER_MAP).map((k) => `border-${k}`),
1617
1625
  ...Object.keys(SEMANTIC_INTERACTIVE_MAP).map((k) => `interactive-${k}`)
1618
1626
  ]);
1627
+ var REQUIRED_OVERRIDE_TOKENS = {
1628
+ text: ["primary", "secondary", "tertiary", "disabled"],
1629
+ surface: [
1630
+ "page",
1631
+ "card",
1632
+ "popover",
1633
+ "subtle",
1634
+ "muted",
1635
+ "interactive-default",
1636
+ "interactive-hover",
1637
+ "interactive-active",
1638
+ "interactive-disabled",
1639
+ "selected",
1640
+ "accent-subtle",
1641
+ "success-subtle",
1642
+ "warning-subtle",
1643
+ "error-subtle",
1644
+ "info-subtle",
1645
+ "elev-0",
1646
+ "elev-1",
1647
+ "elev-2",
1648
+ "elev-3",
1649
+ "elev-4"
1650
+ ],
1651
+ border: ["default", "muted", "strong", "disabled"],
1652
+ interactive: [
1653
+ "secondary-bg",
1654
+ "secondary-bg-hover",
1655
+ "secondary-bg-active",
1656
+ "secondary-text",
1657
+ "secondary-border",
1658
+ "ghost-bg",
1659
+ "ghost-bg-hover"
1660
+ ]
1661
+ };
1619
1662
  function issue(severity, code, message, path) {
1620
1663
  const result = { severity, code, message };
1621
1664
  if (path !== void 0) {
@@ -1958,6 +2001,30 @@ function checkOverrides(config, issues) {
1958
2001
  }
1959
2002
  }
1960
2003
  }
2004
+ function checkOverrideCompleteness(config, issues) {
2005
+ const lightOverrides = config.overrides?.light;
2006
+ if (!lightOverrides) return;
2007
+ const presentKeys = Object.keys(lightOverrides);
2008
+ if (presentKeys.length === 0) return;
2009
+ const hasTriggerKey = "surface-page" in lightOverrides || "surface-card" in lightOverrides;
2010
+ if (!hasTriggerKey) return;
2011
+ const present = new Set(presentKeys);
2012
+ for (const [family, tokens] of Object.entries(REQUIRED_OVERRIDE_TOKENS)) {
2013
+ for (const token of tokens) {
2014
+ const fullKey = `${family}-${token}`;
2015
+ if (!present.has(fullKey)) {
2016
+ issues.push(
2017
+ issue(
2018
+ "warning",
2019
+ "INCOMPLETE_OVERRIDE",
2020
+ `'overrides.light' inverts the light-mode surface (overrides 'surface-page' or 'surface-card') but is missing '${fullKey}'. The engine's light-mode default for this token may leak (bright/light-natural values on inverted always-dark themes like Blackout).`,
2021
+ `overrides.light.${fullKey}`
2022
+ )
2023
+ );
2024
+ }
2025
+ }
2026
+ }
2027
+ }
1961
2028
  function checkResolvedCompleteness(resolved, issues) {
1962
2029
  const requiredColors = [
1963
2030
  "primary",
@@ -2274,6 +2341,7 @@ function validate(config) {
2274
2341
  for (const iss of overrideIssues) {
2275
2342
  (iss.severity === "error" ? errors : warnings).push(iss);
2276
2343
  }
2344
+ checkOverrideCompleteness(typedConfig, warnings);
2277
2345
  if (errors.length === 0) {
2278
2346
  const resolved = resolveConfig(typedConfig);
2279
2347
  checkResolvedCompleteness(resolved, errors);
@@ -2791,6 +2859,7 @@ export {
2791
2859
  compositeOverBackground,
2792
2860
  exportTheme,
2793
2861
  extractFromCSS,
2862
+ formatFontCoverageError,
2794
2863
  generateDarkCss,
2795
2864
  generateFullBundleCss,
2796
2865
  generateLightCss,
@@ -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
  };
@@ -267,6 +269,8 @@ interface VisorThemeConfig {
267
269
  /** Config with all defaults resolved */
268
270
  interface ResolvedThemeConfig {
269
271
  name: string;
272
+ /** Optional display label override forwarded from VisorThemeConfig.label. */
273
+ label?: string;
270
274
  version: 1;
271
275
  colors: {
272
276
  primary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.6.0",
3
+ "version": "0.8.1",
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",