@loworbitstudio/visor-theme-engine 0.8.1 → 0.9.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-CV0nmvMz.js';
1
+ import { c as GeneratedPrimitives, i as SemanticTokens, R as ResolvedThemeConfig } from '../types-0hotGz1u.js';
2
2
 
3
3
  /**
4
4
  * Adapter types for the Visor theme engine.
@@ -13,7 +13,7 @@ import {
13
13
  parseColor,
14
14
  resolveThemeFonts,
15
15
  sectionComment
16
- } from "../chunk-OJVNL7KN.js";
16
+ } from "../chunk-BEFH5BTX.js";
17
17
 
18
18
  // src/adapters/layers.ts
19
19
  var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
@@ -75,7 +75,7 @@ function nextjsAdapter(input, options) {
75
75
  seenVisorFamilies.add(font.family);
76
76
  const aliased = aliasedFamilies.get(font.family);
77
77
  for (const weight of font.weights) {
78
- const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
78
+ const url = buildVisorFontUrl(font.org ?? "", font.family, weight, font.cdnBase);
79
79
  lines.push(`@font-face {`);
80
80
  lines.push(` font-family: "${aliased}";`);
81
81
  lines.push(` src: url("${url}") format("woff2");`);
@@ -463,7 +463,7 @@ function docsAdapter(input, options) {
463
463
  emittedFamilies.add(font.family);
464
464
  const aliased = aliasedFamilies.get(font.family);
465
465
  for (const weight of font.weights) {
466
- const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
466
+ const url = buildVisorFontUrl(font.org ?? "", font.family, weight, font.cdnBase);
467
467
  fontLines.push("@font-face {");
468
468
  fontLines.push(` font-family: "${aliased}";`);
469
469
  fontLines.push(` src: url("${url}") format("woff2");`);
@@ -179,11 +179,13 @@ var WEIGHT_NAMES = {
179
179
  800: "ExtraBold",
180
180
  900: "Black"
181
181
  };
182
- function buildVisorFontUrl(org, family, weight) {
182
+ function buildVisorFontUrl(org, family, weight, cdnBase) {
183
+ const base = cdnBase ?? VISOR_FONTS_CDN;
183
184
  const slug = buildFamilySlug(family);
184
185
  const prefix = buildFamilyPrefix(family);
185
186
  const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
186
- return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
187
+ const orgSegment = org ? `/${org}` : "";
188
+ return `${base}${orgSegment}/${slug}/${prefix}-${weightName}.woff2`;
187
189
  }
188
190
  function buildFontshareCssUrl(family, weights, italic, display) {
189
191
  const slug = buildFamilySlug(family);
@@ -210,7 +212,8 @@ function resolveFont(family, options = {}) {
210
212
  display,
211
213
  category: options.category ?? "sans-serif",
212
214
  guidance: null,
213
- org: options.org ?? null
215
+ org: options.org ?? null,
216
+ cdnBase: options.cdnBase ?? null
214
217
  };
215
218
  }
216
219
  if (explicitSource === "fontshare") {
@@ -224,7 +227,8 @@ function resolveFont(family, options = {}) {
224
227
  display,
225
228
  category: options.category ?? "sans-serif",
226
229
  guidance: null,
227
- org: null
230
+ org: null,
231
+ cdnBase: null
228
232
  };
229
233
  }
230
234
  if (explicitSource === "local") {
@@ -240,7 +244,8 @@ function resolveFont(family, options = {}) {
240
244
  1. Add the font files (.woff2) to your project's public/fonts/ directory
241
245
  2. Create @font-face declarations in your theme CSS
242
246
  3. Reference the font family in your theme's --font-display or --font-body token`,
243
- org: null
247
+ org: null,
248
+ cdnBase: null
244
249
  };
245
250
  }
246
251
  const catalogEntry = lookupGoogleFont(family);
@@ -267,7 +272,8 @@ function resolveFont(family, options = {}) {
267
272
  display,
268
273
  category: catalogEntry.category,
269
274
  guidance: null,
270
- org: null
275
+ org: null,
276
+ cdnBase: null
271
277
  };
272
278
  }
273
279
  return {
@@ -282,7 +288,8 @@ function resolveFont(family, options = {}) {
282
288
  1. Add the font files (.woff2) to your project's public/fonts/ directory
283
289
  2. Create @font-face declarations in your theme CSS
284
290
  3. Reference the font family in your theme's --font-display or --font-body token`,
285
- org: null
291
+ org: null,
292
+ cdnBase: null
286
293
  };
287
294
  }
288
295
 
@@ -307,19 +314,29 @@ function generatePreloadLinks(resolutions, customFontPaths) {
307
314
  }
308
315
  }
309
316
  }
310
- const hasVisorFonts = resolutions.some((r) => r.source === "visor-fonts");
311
- if (hasVisorFonts) {
312
- links.push(
313
- `<link rel="preconnect" href="${VISOR_FONTS_CDN}" crossorigin>`
314
- );
315
- for (const resolution of resolutions) {
316
- if (resolution.source === "visor-fonts" && resolution.org) {
317
- for (const weight of resolution.weights) {
318
- const url = buildVisorFontUrl(resolution.org, resolution.family, weight);
319
- links.push(
320
- `<link rel="preload" as="font" type="font/woff2" href="${url}" crossorigin>`
321
- );
322
- }
317
+ const visorFontResolutions = resolutions.filter(
318
+ (r) => r.source === "visor-fonts"
319
+ );
320
+ if (visorFontResolutions.length > 0) {
321
+ const cdnBases = /* @__PURE__ */ new Set();
322
+ for (const resolution of visorFontResolutions) {
323
+ cdnBases.add(resolution.cdnBase ?? VISOR_FONTS_CDN);
324
+ }
325
+ for (const base of cdnBases) {
326
+ links.push(`<link rel="preconnect" href="${base}" crossorigin>`);
327
+ }
328
+ for (const resolution of visorFontResolutions) {
329
+ if (resolution.org === null && resolution.cdnBase === null) continue;
330
+ for (const weight of resolution.weights) {
331
+ const url = buildVisorFontUrl(
332
+ resolution.org ?? "",
333
+ resolution.family,
334
+ weight,
335
+ resolution.cdnBase
336
+ );
337
+ links.push(
338
+ `<link rel="preload" as="font" type="font/woff2" href="${url}" crossorigin>`
339
+ );
323
340
  }
324
341
  }
325
342
  }
@@ -402,7 +419,7 @@ function generateFontCSS(heading, displayFont, body, mono, typography) {
402
419
  if (seenVisorFonts.has(font.family)) continue;
403
420
  seenVisorFonts.add(font.family);
404
421
  for (const weight of font.weights) {
405
- const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
422
+ const url = buildVisorFontUrl(font.org ?? "", font.family, weight, font.cdnBase);
406
423
  lines.push(`@font-face {`);
407
424
  lines.push(` font-family: "${font.family}";`);
408
425
  lines.push(` src: url("${url}") format("woff2");`);
@@ -542,6 +559,8 @@ function getSizeAdjust(category) {
542
559
  function resolveThemeFonts(typography, options) {
543
560
  const display = options?.display ?? "swap";
544
561
  const warnings = [];
562
+ const cdnOverrides = typography["cdn-overrides"];
563
+ const visorFontsCdnBase = cdnOverrides?.["visor-fonts"];
545
564
  let headingResolution = null;
546
565
  if (typography.heading?.family) {
547
566
  const weights = typography.heading.weights ? [...typography.heading.weights] : typography.heading.weight ? [typography.heading.weight] : [];
@@ -549,7 +568,8 @@ function resolveThemeFonts(typography, options) {
549
568
  weights: weights.length > 0 ? weights : void 0,
550
569
  display,
551
570
  source: typography.heading.source,
552
- org: typography.heading.org
571
+ org: typography.heading.org,
572
+ cdnBase: visorFontsCdnBase
553
573
  });
554
574
  if (headingResolution.guidance) {
555
575
  warnings.push(headingResolution.guidance);
@@ -574,7 +594,8 @@ function resolveThemeFonts(typography, options) {
574
594
  weights: mergedWeights,
575
595
  display,
576
596
  source: typography.heading.source,
577
- org: typography.heading.org
597
+ org: typography.heading.org,
598
+ cdnBase: visorFontsCdnBase
578
599
  });
579
600
  bodyResolution = headingResolution;
580
601
  } else {
@@ -582,7 +603,8 @@ function resolveThemeFonts(typography, options) {
582
603
  weights: bodyWeights.length > 0 ? bodyWeights : void 0,
583
604
  display,
584
605
  source: typography.body.source,
585
- org: typography.body.org
606
+ org: typography.body.org,
607
+ cdnBase: visorFontsCdnBase
586
608
  });
587
609
  if (bodyResolution.guidance) {
588
610
  warnings.push(bodyResolution.guidance);
@@ -603,7 +625,8 @@ function resolveThemeFonts(typography, options) {
603
625
  weights: mergedWeights,
604
626
  display,
605
627
  source: typography.heading.source,
606
- org: typography.heading.org
628
+ org: typography.heading.org,
629
+ cdnBase: visorFontsCdnBase
607
630
  });
608
631
  displayResolution = headingResolution;
609
632
  }
@@ -615,7 +638,8 @@ function resolveThemeFonts(typography, options) {
615
638
  weights: mergedWeights,
616
639
  display,
617
640
  source: typography.body.source,
618
- org: typography.body.org
641
+ org: typography.body.org,
642
+ cdnBase: visorFontsCdnBase
619
643
  });
620
644
  displayResolution = bodyResolution;
621
645
  } else {
@@ -623,7 +647,8 @@ function resolveThemeFonts(typography, options) {
623
647
  weights: displayWeights.length > 0 ? displayWeights : void 0,
624
648
  display,
625
649
  source: typography.display.source,
626
- org: typography.display.org
650
+ org: typography.display.org,
651
+ cdnBase: visorFontsCdnBase
627
652
  });
628
653
  if (displayResolution.guidance) {
629
654
  warnings.push(displayResolution.guidance);
@@ -655,6 +680,7 @@ function resolveThemeFonts(typography, options) {
655
680
  display,
656
681
  source: monoSource,
657
682
  org: monoOrg,
683
+ cdnBase: visorFontsCdnBase,
658
684
  category: "monospace"
659
685
  });
660
686
  if (monoResolution.guidance) {
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-CV0nmvMz.js';
2
- export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-CV0nmvMz.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-0hotGz1u.js';
2
+ export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-0hotGz1u.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -16,7 +16,17 @@ export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue }
16
16
  * Filename: original uploaded name (e.g., PPModelPlastic-Regular).
17
17
  */
18
18
  declare const VISOR_FONTS_CDN = "https://fonts.visor.design";
19
- declare function buildVisorFontUrl(org: string, family: string, weight: number): string;
19
+ /**
20
+ * Build a visor-fonts URL.
21
+ *
22
+ * Default pattern: `https://fonts.visor.design/{org}/{slug}/{prefix}-{weight}.woff2`
23
+ *
24
+ * When `cdnBase` is provided, that base replaces `VISOR_FONTS_CDN`. When
25
+ * `org` is empty (theme-level CDN override already encodes the namespace,
26
+ * e.g. `https://fonts.knowmentum.ai`), the org segment is dropped:
27
+ * `{cdnBase}/{slug}/{prefix}-{weight}.woff2`.
28
+ */
29
+ declare function buildVisorFontUrl(org: string, family: string, weight: number, cdnBase?: string | null): string;
20
30
  /**
21
31
  * Resolve a font family name to a FontResolution.
22
32
  *
@@ -422,6 +432,18 @@ var properties = {
422
432
  }
423
433
  }
424
434
  },
435
+ "cdn-overrides": {
436
+ type: "object",
437
+ description: "Per-source CDN base URL overrides. When set, the override CDN replaces the default for every slot whose source matches the key. The per-slot org may be empty when the override CDN already encodes the project namespace (e.g., https://fonts.knowmentum.ai).",
438
+ additionalProperties: false,
439
+ properties: {
440
+ "visor-fonts": {
441
+ type: "string",
442
+ minLength: 1,
443
+ description: "CDN base URL that replaces fonts.visor.design for source: visor-fonts slots (e.g., https://fonts.knowmentum.ai)."
444
+ }
445
+ }
446
+ },
425
447
  slots: {
426
448
  type: "object",
427
449
  description: "Per-slot overrides for the generated Flutter Material TextTheme. Any subset of the 16 slots may be specified; omitted slots fall through to the Material 3 2024 defaults shipped in visor_core.",
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  rgbToHex,
35
35
  rgbToOklch,
36
36
  serializeColor
37
- } from "./chunk-OJVNL7KN.js";
37
+ } from "./chunk-BEFH5BTX.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;
@@ -381,6 +381,18 @@ var visor_theme_schema_default = {
381
381
  wide: { type: "string" }
382
382
  }
383
383
  },
384
+ "cdn-overrides": {
385
+ type: "object",
386
+ description: "Per-source CDN base URL overrides. When set, the override CDN replaces the default for every slot whose source matches the key. The per-slot org may be empty when the override CDN already encodes the project namespace (e.g., https://fonts.knowmentum.ai).",
387
+ additionalProperties: false,
388
+ properties: {
389
+ "visor-fonts": {
390
+ type: "string",
391
+ minLength: 1,
392
+ description: "CDN base URL that replaces fonts.visor.design for source: visor-fonts slots (e.g., https://fonts.knowmentum.ai)."
393
+ }
394
+ }
395
+ },
384
396
  slots: {
385
397
  type: "object",
386
398
  description: "Per-slot overrides for the generated Flutter Material TextTheme. Any subset of the 16 slots may be specified; omitted slots fall through to the Material 3 2024 defaults shipped in visor_core.",
@@ -580,8 +592,10 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
580
592
  "mono",
581
593
  "letter-spacing",
582
594
  "scale",
583
- "slots"
595
+ "slots",
596
+ "cdn-overrides"
584
597
  ]);
598
+ var KNOWN_CDN_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["visor-fonts"]);
585
599
  var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
586
600
  var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
587
601
  var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
@@ -648,6 +662,13 @@ function checkUnknownKeys(obj, errors) {
648
662
  }
649
663
  }
650
664
  }
665
+ if (typeof typo["cdn-overrides"] === "object" && typo["cdn-overrides"] !== null) {
666
+ for (const key of Object.keys(typo["cdn-overrides"])) {
667
+ if (!KNOWN_CDN_OVERRIDE_KEYS.has(key)) {
668
+ errors.push(`Unknown key 'typography.cdn-overrides.${key}'. Valid keys: ${[...KNOWN_CDN_OVERRIDE_KEYS].join(", ")}`);
669
+ }
670
+ }
671
+ }
651
672
  if (typeof typo["letter-spacing"] === "object" && typo["letter-spacing"] !== null) {
652
673
  for (const key of Object.keys(typo["letter-spacing"])) {
653
674
  if (!KNOWN_LETTER_SPACING_KEYS.has(key)) {
@@ -798,10 +819,19 @@ function validateConfig(config) {
798
819
  }
799
820
  if (typeof obj.typography === "object" && obj.typography !== null) {
800
821
  const typo = obj.typography;
822
+ const cdnOverrides = typo["cdn-overrides"];
823
+ const visorFontsOverride = cdnOverrides?.["visor-fonts"];
824
+ if (visorFontsOverride !== void 0 && typeof visorFontsOverride !== "string") {
825
+ errors.push(`'typography.cdn-overrides.visor-fonts' must be a string URL`);
826
+ }
827
+ if (typeof visorFontsOverride === "string" && visorFontsOverride.length === 0) {
828
+ errors.push(`'typography.cdn-overrides.visor-fonts' must not be empty`);
829
+ }
830
+ const orgOptional = typeof visorFontsOverride === "string" && visorFontsOverride.length > 0;
801
831
  for (const slot of ["heading", "display", "body"]) {
802
832
  const font = typo[slot];
803
- if (font && font.source === "visor-fonts" && !font.org) {
804
- errors.push(`'typography.${slot}.org' is required when source is 'visor-fonts'`);
833
+ if (font && font.source === "visor-fonts" && !orgOptional && !font.org) {
834
+ errors.push(`'typography.${slot}.org' is required when source is 'visor-fonts' (unless typography.cdn-overrides.visor-fonts is set)`);
805
835
  }
806
836
  if (font && font.weights !== void 0) {
807
837
  if (!Array.isArray(font.weights) || !font.weights.every((w) => typeof w === "number" && w > 0)) {
@@ -962,6 +992,9 @@ function resolveConfig(config) {
962
992
  ...config.typography?.mono?.source && { source: config.typography.mono.source },
963
993
  ...config.typography?.mono?.org && { org: config.typography.mono.org }
964
994
  },
995
+ ...config.typography?.["cdn-overrides"] && {
996
+ "cdn-overrides": config.typography["cdn-overrides"]
997
+ },
965
998
  slots: config.typography?.slots ?? {}
966
999
  },
967
1000
  spacing: {
@@ -25,6 +25,8 @@ interface FontResolution {
25
25
  guidance: string | null;
26
26
  /** Organization namespace (only for visor-fonts source) */
27
27
  org: string | null;
28
+ /** CDN base URL override (only for visor-fonts source; null = default fonts.visor.design) */
29
+ cdnBase: string | null;
28
30
  }
29
31
  /** Options for resolving a single font */
30
32
  interface FontResolveOptions {
@@ -38,8 +40,10 @@ interface FontResolveOptions {
38
40
  category?: string;
39
41
  /** Font source override — skips Google Fonts lookup when set */
40
42
  source?: FontSource;
41
- /** Organization namespace for visor-fonts CDN (required when source is "visor-fonts") */
43
+ /** Organization namespace for visor-fonts CDN (required when source is "visor-fonts" unless a cdnBase override is provided) */
42
44
  org?: string;
45
+ /** CDN base URL override for visor-fonts (e.g., "https://fonts.knowmentum.ai"); defaults to VISOR_FONTS_CDN */
46
+ cdnBase?: string;
43
47
  }
44
48
  /** Typography section from a .visor.yaml file */
45
49
  interface VisorTypography {
@@ -80,6 +84,15 @@ interface VisorTypography {
80
84
  normal?: string;
81
85
  wide?: string;
82
86
  };
87
+ /**
88
+ * Per-source CDN base URL overrides. When a slot's `source` matches a key,
89
+ * URL resolution uses the override base instead of the default CDN.
90
+ * Currently only `visor-fonts` is supported; Google/Fontshare have no
91
+ * analogous need.
92
+ */
93
+ "cdn-overrides"?: {
94
+ "visor-fonts"?: string;
95
+ };
83
96
  }
84
97
  /** Result of resolving all fonts for a theme */
85
98
  interface ThemeFontResult {
@@ -209,6 +222,16 @@ interface VisorThemeConfig {
209
222
  normal?: string;
210
223
  wide?: string;
211
224
  };
225
+ /**
226
+ * Per-source CDN base URL overrides for font URL resolution. Only
227
+ * `visor-fonts` is currently supported. When set, the override CDN
228
+ * replaces `fonts.visor.design` for every slot whose `source: visor-fonts`.
229
+ * The per-slot `org` may be empty when the override CDN already encodes
230
+ * the project namespace (e.g., `fonts.knowmentum.ai`).
231
+ */
232
+ "cdn-overrides"?: {
233
+ "visor-fonts"?: string;
234
+ };
212
235
  /**
213
236
  * Per-slot overrides for the generated Flutter `TextTheme`. Any subset
214
237
  * of the 16 Material slots may be specified; omitted slots fall
@@ -314,6 +337,14 @@ interface ResolvedThemeConfig {
314
337
  source?: FontSource;
315
338
  org?: string;
316
339
  };
340
+ /**
341
+ * Per-source CDN base URL overrides (e.g., `visor-fonts:
342
+ * https://fonts.knowmentum.ai`). Passed through from raw config so
343
+ * adapters can route font URLs to per-theme buckets.
344
+ */
345
+ "cdn-overrides"?: {
346
+ "visor-fonts"?: string;
347
+ };
317
348
  /**
318
349
  * Per-slot Material `TextTheme` overrides, passed through from the
319
350
  * raw config. Empty object when none supplied. Flutter adapter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.8.1",
3
+ "version": "0.9.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",
@@ -185,6 +185,18 @@
185
185
  "wide": { "type": "string" }
186
186
  }
187
187
  },
188
+ "cdn-overrides": {
189
+ "type": "object",
190
+ "description": "Per-source CDN base URL overrides. When set, the override CDN replaces the default for every slot whose source matches the key. The per-slot org may be empty when the override CDN already encodes the project namespace (e.g., https://fonts.knowmentum.ai).",
191
+ "additionalProperties": false,
192
+ "properties": {
193
+ "visor-fonts": {
194
+ "type": "string",
195
+ "minLength": 1,
196
+ "description": "CDN base URL that replaces fonts.visor.design for source: visor-fonts slots (e.g., https://fonts.knowmentum.ai)."
197
+ }
198
+ }
199
+ },
188
200
  "slots": {
189
201
  "type": "object",
190
202
  "description": "Per-slot overrides for the generated Flutter Material TextTheme. Any subset of the 16 slots may be specified; omitted slots fall through to the Material 3 2024 defaults shipped in visor_core.",