@loworbitstudio/visor-theme-engine 0.8.0 → 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.
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +3 -3
- package/dist/{chunk-2O2DPCMJ.js → chunk-BEFH5BTX.js} +58 -27
- package/dist/index.d.ts +25 -3
- package/dist/index.js +97 -4
- package/dist/{types-CV0nmvMz.d.ts → types-0hotGz1u.d.ts} +32 -1
- package/package.json +1 -1
- package/src/visor-theme.schema.json +12 -0
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as GeneratedPrimitives, i as SemanticTokens, R as ResolvedThemeConfig } from '../types-
|
|
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.
|
package/dist/adapters/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseColor,
|
|
14
14
|
resolveThemeFonts,
|
|
15
15
|
sectionComment
|
|
16
|
-
} from "../chunk-
|
|
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");`);
|
|
@@ -129,6 +129,11 @@ var FONT_WEIGHT_ALIASES = {
|
|
|
129
129
|
"PP Model Plastic": {
|
|
130
130
|
400: "Book",
|
|
131
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"
|
|
132
137
|
}
|
|
133
138
|
};
|
|
134
139
|
function lookupFontWeightAlias(family, weight) {
|
|
@@ -174,11 +179,13 @@ var WEIGHT_NAMES = {
|
|
|
174
179
|
800: "ExtraBold",
|
|
175
180
|
900: "Black"
|
|
176
181
|
};
|
|
177
|
-
function buildVisorFontUrl(org, family, weight) {
|
|
182
|
+
function buildVisorFontUrl(org, family, weight, cdnBase) {
|
|
183
|
+
const base = cdnBase ?? VISOR_FONTS_CDN;
|
|
178
184
|
const slug = buildFamilySlug(family);
|
|
179
185
|
const prefix = buildFamilyPrefix(family);
|
|
180
186
|
const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
|
|
181
|
-
|
|
187
|
+
const orgSegment = org ? `/${org}` : "";
|
|
188
|
+
return `${base}${orgSegment}/${slug}/${prefix}-${weightName}.woff2`;
|
|
182
189
|
}
|
|
183
190
|
function buildFontshareCssUrl(family, weights, italic, display) {
|
|
184
191
|
const slug = buildFamilySlug(family);
|
|
@@ -205,7 +212,8 @@ function resolveFont(family, options = {}) {
|
|
|
205
212
|
display,
|
|
206
213
|
category: options.category ?? "sans-serif",
|
|
207
214
|
guidance: null,
|
|
208
|
-
org: options.org ?? null
|
|
215
|
+
org: options.org ?? null,
|
|
216
|
+
cdnBase: options.cdnBase ?? null
|
|
209
217
|
};
|
|
210
218
|
}
|
|
211
219
|
if (explicitSource === "fontshare") {
|
|
@@ -219,7 +227,8 @@ function resolveFont(family, options = {}) {
|
|
|
219
227
|
display,
|
|
220
228
|
category: options.category ?? "sans-serif",
|
|
221
229
|
guidance: null,
|
|
222
|
-
org: null
|
|
230
|
+
org: null,
|
|
231
|
+
cdnBase: null
|
|
223
232
|
};
|
|
224
233
|
}
|
|
225
234
|
if (explicitSource === "local") {
|
|
@@ -235,7 +244,8 @@ function resolveFont(family, options = {}) {
|
|
|
235
244
|
1. Add the font files (.woff2) to your project's public/fonts/ directory
|
|
236
245
|
2. Create @font-face declarations in your theme CSS
|
|
237
246
|
3. Reference the font family in your theme's --font-display or --font-body token`,
|
|
238
|
-
org: null
|
|
247
|
+
org: null,
|
|
248
|
+
cdnBase: null
|
|
239
249
|
};
|
|
240
250
|
}
|
|
241
251
|
const catalogEntry = lookupGoogleFont(family);
|
|
@@ -262,7 +272,8 @@ function resolveFont(family, options = {}) {
|
|
|
262
272
|
display,
|
|
263
273
|
category: catalogEntry.category,
|
|
264
274
|
guidance: null,
|
|
265
|
-
org: null
|
|
275
|
+
org: null,
|
|
276
|
+
cdnBase: null
|
|
266
277
|
};
|
|
267
278
|
}
|
|
268
279
|
return {
|
|
@@ -277,7 +288,8 @@ function resolveFont(family, options = {}) {
|
|
|
277
288
|
1. Add the font files (.woff2) to your project's public/fonts/ directory
|
|
278
289
|
2. Create @font-face declarations in your theme CSS
|
|
279
290
|
3. Reference the font family in your theme's --font-display or --font-body token`,
|
|
280
|
-
org: null
|
|
291
|
+
org: null,
|
|
292
|
+
cdnBase: null
|
|
281
293
|
};
|
|
282
294
|
}
|
|
283
295
|
|
|
@@ -302,19 +314,29 @@ function generatePreloadLinks(resolutions, customFontPaths) {
|
|
|
302
314
|
}
|
|
303
315
|
}
|
|
304
316
|
}
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
for (const resolution of
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
);
|
|
318
340
|
}
|
|
319
341
|
}
|
|
320
342
|
}
|
|
@@ -397,7 +419,7 @@ function generateFontCSS(heading, displayFont, body, mono, typography) {
|
|
|
397
419
|
if (seenVisorFonts.has(font.family)) continue;
|
|
398
420
|
seenVisorFonts.add(font.family);
|
|
399
421
|
for (const weight of font.weights) {
|
|
400
|
-
const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
|
|
422
|
+
const url = buildVisorFontUrl(font.org ?? "", font.family, weight, font.cdnBase);
|
|
401
423
|
lines.push(`@font-face {`);
|
|
402
424
|
lines.push(` font-family: "${font.family}";`);
|
|
403
425
|
lines.push(` src: url("${url}") format("woff2");`);
|
|
@@ -537,6 +559,8 @@ function getSizeAdjust(category) {
|
|
|
537
559
|
function resolveThemeFonts(typography, options) {
|
|
538
560
|
const display = options?.display ?? "swap";
|
|
539
561
|
const warnings = [];
|
|
562
|
+
const cdnOverrides = typography["cdn-overrides"];
|
|
563
|
+
const visorFontsCdnBase = cdnOverrides?.["visor-fonts"];
|
|
540
564
|
let headingResolution = null;
|
|
541
565
|
if (typography.heading?.family) {
|
|
542
566
|
const weights = typography.heading.weights ? [...typography.heading.weights] : typography.heading.weight ? [typography.heading.weight] : [];
|
|
@@ -544,7 +568,8 @@ function resolveThemeFonts(typography, options) {
|
|
|
544
568
|
weights: weights.length > 0 ? weights : void 0,
|
|
545
569
|
display,
|
|
546
570
|
source: typography.heading.source,
|
|
547
|
-
org: typography.heading.org
|
|
571
|
+
org: typography.heading.org,
|
|
572
|
+
cdnBase: visorFontsCdnBase
|
|
548
573
|
});
|
|
549
574
|
if (headingResolution.guidance) {
|
|
550
575
|
warnings.push(headingResolution.guidance);
|
|
@@ -569,7 +594,8 @@ function resolveThemeFonts(typography, options) {
|
|
|
569
594
|
weights: mergedWeights,
|
|
570
595
|
display,
|
|
571
596
|
source: typography.heading.source,
|
|
572
|
-
org: typography.heading.org
|
|
597
|
+
org: typography.heading.org,
|
|
598
|
+
cdnBase: visorFontsCdnBase
|
|
573
599
|
});
|
|
574
600
|
bodyResolution = headingResolution;
|
|
575
601
|
} else {
|
|
@@ -577,7 +603,8 @@ function resolveThemeFonts(typography, options) {
|
|
|
577
603
|
weights: bodyWeights.length > 0 ? bodyWeights : void 0,
|
|
578
604
|
display,
|
|
579
605
|
source: typography.body.source,
|
|
580
|
-
org: typography.body.org
|
|
606
|
+
org: typography.body.org,
|
|
607
|
+
cdnBase: visorFontsCdnBase
|
|
581
608
|
});
|
|
582
609
|
if (bodyResolution.guidance) {
|
|
583
610
|
warnings.push(bodyResolution.guidance);
|
|
@@ -598,7 +625,8 @@ function resolveThemeFonts(typography, options) {
|
|
|
598
625
|
weights: mergedWeights,
|
|
599
626
|
display,
|
|
600
627
|
source: typography.heading.source,
|
|
601
|
-
org: typography.heading.org
|
|
628
|
+
org: typography.heading.org,
|
|
629
|
+
cdnBase: visorFontsCdnBase
|
|
602
630
|
});
|
|
603
631
|
displayResolution = headingResolution;
|
|
604
632
|
}
|
|
@@ -610,7 +638,8 @@ function resolveThemeFonts(typography, options) {
|
|
|
610
638
|
weights: mergedWeights,
|
|
611
639
|
display,
|
|
612
640
|
source: typography.body.source,
|
|
613
|
-
org: typography.body.org
|
|
641
|
+
org: typography.body.org,
|
|
642
|
+
cdnBase: visorFontsCdnBase
|
|
614
643
|
});
|
|
615
644
|
displayResolution = bodyResolution;
|
|
616
645
|
} else {
|
|
@@ -618,7 +647,8 @@ function resolveThemeFonts(typography, options) {
|
|
|
618
647
|
weights: displayWeights.length > 0 ? displayWeights : void 0,
|
|
619
648
|
display,
|
|
620
649
|
source: typography.display.source,
|
|
621
|
-
org: typography.display.org
|
|
650
|
+
org: typography.display.org,
|
|
651
|
+
cdnBase: visorFontsCdnBase
|
|
622
652
|
});
|
|
623
653
|
if (displayResolution.guidance) {
|
|
624
654
|
warnings.push(displayResolution.guidance);
|
|
@@ -650,6 +680,7 @@ function resolveThemeFonts(typography, options) {
|
|
|
650
680
|
display,
|
|
651
681
|
source: monoSource,
|
|
652
682
|
org: monoOrg,
|
|
683
|
+
cdnBase: visorFontsCdnBase,
|
|
653
684
|
category: "monospace"
|
|
654
685
|
});
|
|
655
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-
|
|
2
|
-
export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-
|
|
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
|
-
|
|
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-
|
|
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: {
|
|
@@ -1624,6 +1657,41 @@ var KNOWN_SEMANTIC_TOKENS = /* @__PURE__ */ new Set([
|
|
|
1624
1657
|
...Object.keys(SEMANTIC_BORDER_MAP).map((k) => `border-${k}`),
|
|
1625
1658
|
...Object.keys(SEMANTIC_INTERACTIVE_MAP).map((k) => `interactive-${k}`)
|
|
1626
1659
|
]);
|
|
1660
|
+
var REQUIRED_OVERRIDE_TOKENS = {
|
|
1661
|
+
text: ["primary", "secondary", "tertiary", "disabled"],
|
|
1662
|
+
surface: [
|
|
1663
|
+
"page",
|
|
1664
|
+
"card",
|
|
1665
|
+
"popover",
|
|
1666
|
+
"subtle",
|
|
1667
|
+
"muted",
|
|
1668
|
+
"interactive-default",
|
|
1669
|
+
"interactive-hover",
|
|
1670
|
+
"interactive-active",
|
|
1671
|
+
"interactive-disabled",
|
|
1672
|
+
"selected",
|
|
1673
|
+
"accent-subtle",
|
|
1674
|
+
"success-subtle",
|
|
1675
|
+
"warning-subtle",
|
|
1676
|
+
"error-subtle",
|
|
1677
|
+
"info-subtle",
|
|
1678
|
+
"elev-0",
|
|
1679
|
+
"elev-1",
|
|
1680
|
+
"elev-2",
|
|
1681
|
+
"elev-3",
|
|
1682
|
+
"elev-4"
|
|
1683
|
+
],
|
|
1684
|
+
border: ["default", "muted", "strong", "disabled"],
|
|
1685
|
+
interactive: [
|
|
1686
|
+
"secondary-bg",
|
|
1687
|
+
"secondary-bg-hover",
|
|
1688
|
+
"secondary-bg-active",
|
|
1689
|
+
"secondary-text",
|
|
1690
|
+
"secondary-border",
|
|
1691
|
+
"ghost-bg",
|
|
1692
|
+
"ghost-bg-hover"
|
|
1693
|
+
]
|
|
1694
|
+
};
|
|
1627
1695
|
function issue(severity, code, message, path) {
|
|
1628
1696
|
const result = { severity, code, message };
|
|
1629
1697
|
if (path !== void 0) {
|
|
@@ -1966,6 +2034,30 @@ function checkOverrides(config, issues) {
|
|
|
1966
2034
|
}
|
|
1967
2035
|
}
|
|
1968
2036
|
}
|
|
2037
|
+
function checkOverrideCompleteness(config, issues) {
|
|
2038
|
+
const lightOverrides = config.overrides?.light;
|
|
2039
|
+
if (!lightOverrides) return;
|
|
2040
|
+
const presentKeys = Object.keys(lightOverrides);
|
|
2041
|
+
if (presentKeys.length === 0) return;
|
|
2042
|
+
const hasTriggerKey = "surface-page" in lightOverrides || "surface-card" in lightOverrides;
|
|
2043
|
+
if (!hasTriggerKey) return;
|
|
2044
|
+
const present = new Set(presentKeys);
|
|
2045
|
+
for (const [family, tokens] of Object.entries(REQUIRED_OVERRIDE_TOKENS)) {
|
|
2046
|
+
for (const token of tokens) {
|
|
2047
|
+
const fullKey = `${family}-${token}`;
|
|
2048
|
+
if (!present.has(fullKey)) {
|
|
2049
|
+
issues.push(
|
|
2050
|
+
issue(
|
|
2051
|
+
"warning",
|
|
2052
|
+
"INCOMPLETE_OVERRIDE",
|
|
2053
|
+
`'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).`,
|
|
2054
|
+
`overrides.light.${fullKey}`
|
|
2055
|
+
)
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1969
2061
|
function checkResolvedCompleteness(resolved, issues) {
|
|
1970
2062
|
const requiredColors = [
|
|
1971
2063
|
"primary",
|
|
@@ -2282,6 +2374,7 @@ function validate(config) {
|
|
|
2282
2374
|
for (const iss of overrideIssues) {
|
|
2283
2375
|
(iss.severity === "error" ? errors : warnings).push(iss);
|
|
2284
2376
|
}
|
|
2377
|
+
checkOverrideCompleteness(typedConfig, warnings);
|
|
2285
2378
|
if (errors.length === 0) {
|
|
2286
2379
|
const resolved = resolveConfig(typedConfig);
|
|
2287
2380
|
checkResolvedCompleteness(resolved, errors);
|
|
@@ -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.
|
|
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.",
|