@loworbitstudio/visor-theme-engine 0.6.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-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-2O2DPCMJ.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,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"
@@ -623,13 +627,29 @@ function resolveThemeFonts(typography, options) {
623
627
  }
624
628
  let monoResolution = null;
625
629
  if (typography.mono?.family) {
626
- const monoWeights = [];
627
- 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
+ }
628
648
  monoResolution = resolveFont(typography.mono.family, {
629
649
  weights: monoWeights.length > 0 ? monoWeights : void 0,
630
650
  display,
631
- source: typography.mono.source,
632
- org: typography.mono.org,
651
+ source: monoSource,
652
+ org: monoOrg,
633
653
  category: "monospace"
634
654
  });
635
655
  if (monoResolution.guidance) {
@@ -1299,22 +1319,23 @@ function generateMiscPrimitives() {
1299
1319
  }
1300
1320
  function generatePrimitivesCss(primitives, config, options) {
1301
1321
  const lines = [];
1322
+ const host = options?.scopePrefix ?? ":root";
1302
1323
  lines.push(sectionComment("Primitive: Colors"));
1303
1324
  lines.push(
1304
- block(":root", [generateColorPrimitives(primitives)])
1325
+ block(host, [generateColorPrimitives(primitives)])
1305
1326
  );
1306
1327
  lines.push(sectionComment("Primitive: Spacing"));
1307
- lines.push(block(":root", generateSpacingPrimitives(config)));
1328
+ lines.push(block(host, generateSpacingPrimitives(config)));
1308
1329
  lines.push(sectionComment("Primitive: Border Radius"));
1309
- lines.push(block(":root", generateRadiusPrimitives(config)));
1330
+ lines.push(block(host, generateRadiusPrimitives(config)));
1310
1331
  lines.push(sectionComment("Primitive: Typography"));
1311
- lines.push(block(":root", generateTypographyPrimitives(config, options?.aliasedFamilies)));
1332
+ lines.push(block(host, generateTypographyPrimitives(config, options?.aliasedFamilies)));
1312
1333
  lines.push(sectionComment("Primitive: Shadows"));
1313
- lines.push(block(":root", generateShadowPrimitives(config)));
1334
+ lines.push(block(host, generateShadowPrimitives(config)));
1314
1335
  lines.push(sectionComment("Primitive: Motion"));
1315
- lines.push(block(":root", generateMotionPrimitives(config)));
1336
+ lines.push(block(host, generateMotionPrimitives(config)));
1316
1337
  lines.push(sectionComment("Primitive: Miscellaneous"));
1317
- lines.push(block(":root", generateMiscPrimitives()));
1338
+ lines.push(block(host, generateMiscPrimitives()));
1318
1339
  return header("Visor Theme \u2014 Primitives") + lines.join("\n");
1319
1340
  }
1320
1341
  function generateSemanticCss(tokens) {
@@ -1356,25 +1377,27 @@ function buildAdaptiveDecls(tokens, theme) {
1356
1377
  );
1357
1378
  return { textDecls, surfaceDecls, borderDecls, interactiveDecls };
1358
1379
  }
1359
- function generateLightCss(tokens) {
1380
+ function generateLightCss(tokens, options) {
1360
1381
  const lines = [];
1361
1382
  const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "light");
1383
+ const host = options?.scopePrefix ?? ":root";
1362
1384
  lines.push(sectionComment("Adaptive: Text (light)"));
1363
- lines.push(block(":root", textDecls));
1385
+ lines.push(block(host, textDecls));
1364
1386
  lines.push(sectionComment("Adaptive: Surface (light)"));
1365
- lines.push(block(":root", surfaceDecls));
1387
+ lines.push(block(host, surfaceDecls));
1366
1388
  lines.push(sectionComment("Adaptive: Border (light)"));
1367
- lines.push(block(":root", borderDecls));
1389
+ lines.push(block(host, borderDecls));
1368
1390
  lines.push(sectionComment("Adaptive: Interactive (light)"));
1369
- lines.push(block(":root", interactiveDecls));
1391
+ lines.push(block(host, interactiveDecls));
1370
1392
  return header("Visor Theme \u2014 Light") + lines.join("\n");
1371
1393
  }
1372
- function generateDarkCss(tokens) {
1394
+ function generateDarkCss(tokens, options) {
1373
1395
  const lines = [];
1374
1396
  const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "dark");
1375
- 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"]'];
1376
1399
  const darkSelector = darkSelectors.join(",\n");
1377
- 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"])';
1378
1401
  lines.push(sectionComment("Adaptive: Text (dark) \u2014 manual toggle"));
1379
1402
  lines.push(block(darkSelector, textDecls));
1380
1403
  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-2O2DPCMJ.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,
@@ -2791,6 +2799,7 @@ export {
2791
2799
  compositeOverBackground,
2792
2800
  exportTheme,
2793
2801
  extractFromCSS,
2802
+ formatFontCoverageError,
2794
2803
  generateDarkCss,
2795
2804
  generateFullBundleCss,
2796
2805
  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.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",