@loworbitstudio/visor-theme-engine 0.5.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/adapters/index.d.ts +11 -1
- package/dist/adapters/index.js +50 -24
- package/dist/{chunk-U5FXQ5EC.js → chunk-2O2DPCMJ.js} +116 -27
- package/dist/index.d.ts +75 -6
- package/dist/index.js +164 -3
- package/dist/{types-ljcTtODU.d.ts → types-CV0nmvMz.d.ts} +11 -2
- package/package.json +1 -1
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).
|
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-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 {
|
package/dist/adapters/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
FULL_SHADE_STEPS,
|
|
3
3
|
MATERIAL_TEXT_SLOTS,
|
|
4
4
|
SELECTIVE_SHADE_STEPS,
|
|
5
|
+
aliasFamily,
|
|
5
6
|
buildVisorFontUrl,
|
|
7
|
+
fontStack,
|
|
6
8
|
generateDarkCss,
|
|
7
9
|
generateLightCss,
|
|
8
10
|
generatePrimitivesCss,
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
parseColor,
|
|
12
14
|
resolveThemeFonts,
|
|
13
15
|
sectionComment
|
|
14
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-2O2DPCMJ.js";
|
|
15
17
|
|
|
16
18
|
// src/adapters/layers.ts
|
|
17
19
|
var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
|
|
@@ -24,18 +26,30 @@ ${trimmed}
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// src/adapters/nextjs.ts
|
|
29
|
+
function toKebabCase(name) {
|
|
30
|
+
return name.toLowerCase().replace(/\s+/g, "-");
|
|
31
|
+
}
|
|
27
32
|
function nextjsAdapter(input, options) {
|
|
28
33
|
const includeFontImports = options?.includeFontImports ?? true;
|
|
29
34
|
const includeFowt = options?.includeFowt ?? true;
|
|
35
|
+
const scopePrefix = options?.scopePrefix;
|
|
30
36
|
const lines = [];
|
|
37
|
+
const slug = toKebabCase(input.config.name);
|
|
38
|
+
const aliasedFamilies = /* @__PURE__ */ new Map();
|
|
31
39
|
lines.push(header("Visor Theme \u2014 NextJS Adapter"));
|
|
32
40
|
if (includeFontImports && input.config.typography) {
|
|
33
41
|
const fontResult = resolveThemeFonts(input.config.typography);
|
|
34
|
-
const
|
|
35
|
-
|
|
42
|
+
const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
|
|
43
|
+
for (const font of fontSlots) {
|
|
44
|
+
if (font && font.source === "visor-fonts" && !aliasedFamilies.has(font.family)) {
|
|
45
|
+
aliasedFamilies.set(font.family, aliasFamily(font.family, slug));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const hostedCssFonts = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono].filter(
|
|
49
|
+
(r) => r !== null && (r.source === "google-fonts" || r.source === "fontshare")
|
|
36
50
|
);
|
|
37
51
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
38
|
-
for (const font of
|
|
52
|
+
for (const font of hostedCssFonts) {
|
|
39
53
|
if (font?.cssUrl && !seenUrls.has(font.cssUrl)) {
|
|
40
54
|
seenUrls.add(font.cssUrl);
|
|
41
55
|
lines.push(`@import url("${font.cssUrl}");`);
|
|
@@ -59,10 +73,11 @@ function nextjsAdapter(input, options) {
|
|
|
59
73
|
for (const font of visorFonts) {
|
|
60
74
|
if (seenVisorFamilies.has(font.family)) continue;
|
|
61
75
|
seenVisorFamilies.add(font.family);
|
|
76
|
+
const aliased = aliasedFamilies.get(font.family);
|
|
62
77
|
for (const weight of font.weights) {
|
|
63
78
|
const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
|
|
64
79
|
lines.push(`@font-face {`);
|
|
65
|
-
lines.push(` font-family: "${
|
|
80
|
+
lines.push(` font-family: "${aliased}";`);
|
|
66
81
|
lines.push(` src: url("${url}") format("woff2");`);
|
|
67
82
|
lines.push(` font-weight: ${weight};`);
|
|
68
83
|
lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
|
|
@@ -75,12 +90,15 @@ function nextjsAdapter(input, options) {
|
|
|
75
90
|
lines.push(LAYER_ORDER);
|
|
76
91
|
lines.push("");
|
|
77
92
|
const primitivesBody = stripHeader(
|
|
78
|
-
generatePrimitivesCss(input.primitives, input.config
|
|
93
|
+
generatePrimitivesCss(input.primitives, input.config, {
|
|
94
|
+
aliasedFamilies,
|
|
95
|
+
scopePrefix
|
|
96
|
+
})
|
|
79
97
|
);
|
|
80
98
|
lines.push(wrapInLayer("visor-primitives", primitivesBody));
|
|
81
99
|
lines.push("");
|
|
82
|
-
const lightBody = stripHeader(generateLightCss(input.tokens));
|
|
83
|
-
const darkBody = stripHeader(generateDarkCss(input.tokens));
|
|
100
|
+
const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
|
|
101
|
+
const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
|
|
84
102
|
lines.push(
|
|
85
103
|
wrapInLayer("visor-adaptive", lightBody + "\n\n" + darkBody)
|
|
86
104
|
);
|
|
@@ -174,7 +192,7 @@ var SELECTIVE_SCALE_ROLES = [
|
|
|
174
192
|
"error",
|
|
175
193
|
"info"
|
|
176
194
|
];
|
|
177
|
-
function
|
|
195
|
+
function toKebabCase2(name) {
|
|
178
196
|
return name.toLowerCase().replace(/\s+/g, "-");
|
|
179
197
|
}
|
|
180
198
|
function generateScopedPrimitives(primitives, config) {
|
|
@@ -226,7 +244,7 @@ function generateSemanticDecls(tokens, mode) {
|
|
|
226
244
|
return decls;
|
|
227
245
|
}
|
|
228
246
|
function deckAdapter(input, options) {
|
|
229
|
-
const scopeClass = options?.scopeClass ?? `.deck--${
|
|
247
|
+
const scopeClass = options?.scopeClass ?? `.deck--${toKebabCase2(input.config.name)}`;
|
|
230
248
|
const lines = [];
|
|
231
249
|
lines.push(header(`Visor Theme \u2014 Deck Adapter (${scopeClass})`));
|
|
232
250
|
const primDecls = generateScopedPrimitives(input.primitives, input.config);
|
|
@@ -260,7 +278,7 @@ function deckAdapter(input, options) {
|
|
|
260
278
|
// src/adapters/docs.ts
|
|
261
279
|
var FULL_SCALE_ROLES2 = ["primary", "accent", "neutral"];
|
|
262
280
|
var SELECTIVE_SCALE_ROLES2 = ["success", "warning", "error", "info"];
|
|
263
|
-
function
|
|
281
|
+
function toKebabCase3(name) {
|
|
264
282
|
return name.toLowerCase().replace(/\s+/g, "-");
|
|
265
283
|
}
|
|
266
284
|
function generateColorDecls(primitives) {
|
|
@@ -298,13 +316,14 @@ function generateRadiusDecls(config) {
|
|
|
298
316
|
`--radius-full: ${config.radius.pill}px;`
|
|
299
317
|
];
|
|
300
318
|
}
|
|
301
|
-
function generateTypographyDecls(config) {
|
|
319
|
+
function generateTypographyDecls(config, aliases) {
|
|
302
320
|
const decls = [];
|
|
303
|
-
|
|
304
|
-
decls.push(`--font-
|
|
305
|
-
decls.push(`--font-
|
|
306
|
-
decls.push(`--font-
|
|
307
|
-
decls.push(`--font-
|
|
321
|
+
const headingFamily = config.typography.heading?.family ?? config.typography.body.family;
|
|
322
|
+
decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
|
|
323
|
+
decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
|
|
324
|
+
decls.push(`--font-heading: ${fontStack(headingFamily, aliases)};`);
|
|
325
|
+
decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
|
|
326
|
+
decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
|
|
308
327
|
const fontSizes = {
|
|
309
328
|
xs: 12,
|
|
310
329
|
sm: 14,
|
|
@@ -415,31 +434,38 @@ function sectionComment2(label) {
|
|
|
415
434
|
/* --- ${label} --- */`;
|
|
416
435
|
}
|
|
417
436
|
function docsAdapter(input, options) {
|
|
418
|
-
const slug =
|
|
437
|
+
const slug = toKebabCase3(input.config.name);
|
|
419
438
|
const scopeClass = `.${slug}-theme`;
|
|
420
439
|
const includeFontImports = options?.includeFontImports ?? true;
|
|
421
440
|
const fontLines = [];
|
|
422
441
|
const lines = [];
|
|
442
|
+
const aliasedFamilies = /* @__PURE__ */ new Map();
|
|
423
443
|
if (includeFontImports && input.config.typography) {
|
|
424
444
|
const fontResult = resolveThemeFonts(input.config.typography);
|
|
425
445
|
const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
|
|
446
|
+
for (const font of fontSlots) {
|
|
447
|
+
if (font && font.source === "visor-fonts" && !aliasedFamilies.has(font.family)) {
|
|
448
|
+
aliasedFamilies.set(font.family, aliasFamily(font.family, slug));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
426
451
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
427
452
|
for (const font of fontSlots) {
|
|
428
|
-
if (font && font.source === "google-fonts" && font.cssUrl && !seenUrls.has(font.cssUrl)) {
|
|
453
|
+
if (font && (font.source === "google-fonts" || font.source === "fontshare") && font.cssUrl && !seenUrls.has(font.cssUrl)) {
|
|
429
454
|
seenUrls.add(font.cssUrl);
|
|
430
455
|
fontLines.push(`@import url("${font.cssUrl}");`);
|
|
431
456
|
fontLines.push("");
|
|
432
457
|
}
|
|
433
458
|
}
|
|
434
459
|
const scale = input.config.typography?.scale ?? 1;
|
|
435
|
-
const
|
|
460
|
+
const emittedFamilies = /* @__PURE__ */ new Set();
|
|
436
461
|
for (const font of fontSlots) {
|
|
437
|
-
if (font && font.source === "visor-fonts" && !
|
|
438
|
-
|
|
462
|
+
if (font && font.source === "visor-fonts" && !emittedFamilies.has(font.family)) {
|
|
463
|
+
emittedFamilies.add(font.family);
|
|
464
|
+
const aliased = aliasedFamilies.get(font.family);
|
|
439
465
|
for (const weight of font.weights) {
|
|
440
466
|
const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
|
|
441
467
|
fontLines.push("@font-face {");
|
|
442
|
-
fontLines.push(` font-family: "${
|
|
468
|
+
fontLines.push(` font-family: "${aliased}";`);
|
|
443
469
|
fontLines.push(` src: url("${url}") format("woff2");`);
|
|
444
470
|
fontLines.push(` font-weight: ${weight};`);
|
|
445
471
|
fontLines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
|
|
@@ -475,7 +501,7 @@ function docsAdapter(input, options) {
|
|
|
475
501
|
lines.push(block(scopeClass, generateRadiusDecls(input.config)));
|
|
476
502
|
lines.push("");
|
|
477
503
|
lines.push(sectionComment2("Primitive: Typography"));
|
|
478
|
-
lines.push(block(scopeClass, generateTypographyDecls(input.config)));
|
|
504
|
+
lines.push(block(scopeClass, generateTypographyDecls(input.config, aliasedFamilies)));
|
|
479
505
|
lines.push("");
|
|
480
506
|
lines.push(sectionComment2("Primitive: Shadows"));
|
|
481
507
|
lines.push(block(scopeClass, generateShadowDecls(input.config)));
|
|
@@ -122,6 +122,10 @@ var FONT_WEIGHT_ALIASES = {
|
|
|
122
122
|
400: "Book",
|
|
123
123
|
800: "Super"
|
|
124
124
|
},
|
|
125
|
+
"PP Model Sans": {
|
|
126
|
+
400: "Book",
|
|
127
|
+
800: "Super"
|
|
128
|
+
},
|
|
125
129
|
"PP Model Plastic": {
|
|
126
130
|
400: "Book",
|
|
127
131
|
800: "Super"
|
|
@@ -151,6 +155,8 @@ function buildGoogleFontsCssUrl(family, weights, italic, display) {
|
|
|
151
155
|
return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${wghtValues}&display=${display}`;
|
|
152
156
|
}
|
|
153
157
|
var VISOR_FONTS_CDN = "https://fonts.visor.design";
|
|
158
|
+
var FONTSHARE_API_ORIGIN = "https://api.fontshare.com";
|
|
159
|
+
var FONTSHARE_CDN_ORIGIN = "https://cdn.fontshare.com";
|
|
154
160
|
function buildFamilySlug(family) {
|
|
155
161
|
return family.toLowerCase().replace(/ /g, "-");
|
|
156
162
|
}
|
|
@@ -174,6 +180,16 @@ function buildVisorFontUrl(org, family, weight) {
|
|
|
174
180
|
const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
|
|
175
181
|
return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
|
|
176
182
|
}
|
|
183
|
+
function buildFontshareCssUrl(family, weights, italic, display) {
|
|
184
|
+
const slug = buildFamilySlug(family);
|
|
185
|
+
const sortedWeights = [...weights].sort((a, b) => a - b);
|
|
186
|
+
const tokens = [];
|
|
187
|
+
if (italic) {
|
|
188
|
+
for (const w of sortedWeights) tokens.push(`${w}i`);
|
|
189
|
+
}
|
|
190
|
+
for (const w of sortedWeights) tokens.push(`${w}`);
|
|
191
|
+
return `${FONTSHARE_API_ORIGIN}/v2/css?f[]=${slug}@${tokens.join(",")}&display=${display}`;
|
|
192
|
+
}
|
|
177
193
|
function resolveFont(family, options = {}) {
|
|
178
194
|
const display = options.display ?? DEFAULT_DISPLAY;
|
|
179
195
|
const requestedWeights = options.weights ?? DEFAULT_WEIGHTS;
|
|
@@ -192,6 +208,20 @@ function resolveFont(family, options = {}) {
|
|
|
192
208
|
org: options.org ?? null
|
|
193
209
|
};
|
|
194
210
|
}
|
|
211
|
+
if (explicitSource === "fontshare") {
|
|
212
|
+
const cssUrl = buildFontshareCssUrl(family, requestedWeights, italic, display);
|
|
213
|
+
return {
|
|
214
|
+
family,
|
|
215
|
+
source: "fontshare",
|
|
216
|
+
cssUrl,
|
|
217
|
+
weights: requestedWeights,
|
|
218
|
+
italic,
|
|
219
|
+
display,
|
|
220
|
+
category: options.category ?? "sans-serif",
|
|
221
|
+
guidance: null,
|
|
222
|
+
org: null
|
|
223
|
+
};
|
|
224
|
+
}
|
|
195
225
|
if (explicitSource === "local") {
|
|
196
226
|
return {
|
|
197
227
|
family,
|
|
@@ -288,6 +318,20 @@ function generatePreloadLinks(resolutions, customFontPaths) {
|
|
|
288
318
|
}
|
|
289
319
|
}
|
|
290
320
|
}
|
|
321
|
+
const hasFontshare = resolutions.some((r) => r.source === "fontshare");
|
|
322
|
+
if (hasFontshare) {
|
|
323
|
+
links.push(`<link rel="preconnect" href="${FONTSHARE_API_ORIGIN}">`);
|
|
324
|
+
links.push(
|
|
325
|
+
`<link rel="preconnect" href="${FONTSHARE_CDN_ORIGIN}" crossorigin>`
|
|
326
|
+
);
|
|
327
|
+
for (const resolution of resolutions) {
|
|
328
|
+
if (resolution.source === "fontshare" && resolution.cssUrl) {
|
|
329
|
+
links.push(
|
|
330
|
+
`<link rel="preload" as="style" href="${resolution.cssUrl}">`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
291
335
|
if (customFontPaths) {
|
|
292
336
|
for (const resolution of resolutions) {
|
|
293
337
|
if (resolution.source === "local") {
|
|
@@ -307,7 +351,7 @@ function generatePreloadLinks(resolutions, customFontPaths) {
|
|
|
307
351
|
function generateStylesheetLinks(resolutions) {
|
|
308
352
|
const links = [];
|
|
309
353
|
for (const resolution of resolutions) {
|
|
310
|
-
if (resolution.source === "google-fonts" && resolution.cssUrl) {
|
|
354
|
+
if ((resolution.source === "google-fonts" || resolution.source === "fontshare") && resolution.cssUrl) {
|
|
311
355
|
links.push(
|
|
312
356
|
`<link rel="stylesheet" href="${resolution.cssUrl}">`
|
|
313
357
|
);
|
|
@@ -330,6 +374,19 @@ function generateFontCSS(heading, displayFont, body, mono, typography) {
|
|
|
330
374
|
}
|
|
331
375
|
lines.push("");
|
|
332
376
|
}
|
|
377
|
+
const fontshareFonts = allSlots.filter(
|
|
378
|
+
(r) => r !== null && r.source === "fontshare"
|
|
379
|
+
);
|
|
380
|
+
const seenFontshareUrls = /* @__PURE__ */ new Set();
|
|
381
|
+
if (fontshareFonts.length > 0) {
|
|
382
|
+
lines.push("/* Fontshare \u2014 load these stylesheets in your HTML <head> */");
|
|
383
|
+
for (const font of fontshareFonts) {
|
|
384
|
+
if (!font.cssUrl || seenFontshareUrls.has(font.cssUrl)) continue;
|
|
385
|
+
seenFontshareUrls.add(font.cssUrl);
|
|
386
|
+
lines.push(`/* ${font.cssUrl} */`);
|
|
387
|
+
}
|
|
388
|
+
lines.push("");
|
|
389
|
+
}
|
|
333
390
|
const visorFonts = allSlots.filter(
|
|
334
391
|
(r) => r !== null && r.source === "visor-fonts"
|
|
335
392
|
);
|
|
@@ -570,13 +627,29 @@ function resolveThemeFonts(typography, options) {
|
|
|
570
627
|
}
|
|
571
628
|
let monoResolution = null;
|
|
572
629
|
if (typography.mono?.family) {
|
|
573
|
-
const monoWeights = [];
|
|
574
|
-
|
|
630
|
+
const monoWeights = typography.mono.weights ? [...typography.mono.weights] : typography.mono.weight ? [typography.mono.weight] : [];
|
|
631
|
+
let monoSource = typography.mono.source;
|
|
632
|
+
let monoOrg = typography.mono.org;
|
|
633
|
+
if (!monoSource) {
|
|
634
|
+
const monoFamilyLower = typography.mono.family.toLowerCase();
|
|
635
|
+
const candidates = [
|
|
636
|
+
{ resolution: headingResolution, configSource: typography.heading?.source, configOrg: typography.heading?.org },
|
|
637
|
+
{ resolution: displayResolution, configSource: typography.display?.source, configOrg: typography.display?.org },
|
|
638
|
+
{ resolution: bodyResolution, configSource: typography.body?.source, configOrg: typography.body?.org }
|
|
639
|
+
];
|
|
640
|
+
for (const candidate of candidates) {
|
|
641
|
+
if (candidate.resolution && candidate.configSource && candidate.resolution.family.toLowerCase() === monoFamilyLower) {
|
|
642
|
+
monoSource = candidate.configSource;
|
|
643
|
+
monoOrg = candidate.configOrg;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
575
648
|
monoResolution = resolveFont(typography.mono.family, {
|
|
576
649
|
weights: monoWeights.length > 0 ? monoWeights : void 0,
|
|
577
650
|
display,
|
|
578
|
-
source:
|
|
579
|
-
org:
|
|
651
|
+
source: monoSource,
|
|
652
|
+
org: monoOrg,
|
|
580
653
|
category: "monospace"
|
|
581
654
|
});
|
|
582
655
|
if (monoResolution.guidance) {
|
|
@@ -1069,6 +1142,17 @@ function generateShadeScale(color, role) {
|
|
|
1069
1142
|
return scale;
|
|
1070
1143
|
}
|
|
1071
1144
|
|
|
1145
|
+
// src/fonts/theme-alias.ts
|
|
1146
|
+
var EMPTY_ALIASES = /* @__PURE__ */ new Map();
|
|
1147
|
+
function aliasFamily(family, themeSlug) {
|
|
1148
|
+
return `${family} [${themeSlug}]`;
|
|
1149
|
+
}
|
|
1150
|
+
function fontStack(bare, aliases) {
|
|
1151
|
+
const aliased = aliases.get(bare);
|
|
1152
|
+
if (!aliased) return bare;
|
|
1153
|
+
return `"${aliased}", "${bare}"`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1072
1156
|
// src/generate-css.ts
|
|
1073
1157
|
function header(label) {
|
|
1074
1158
|
return [
|
|
@@ -1154,15 +1238,15 @@ function generateShadowPrimitives(config) {
|
|
|
1154
1238
|
`--shadow-xl: ${config.shadows.xl};`
|
|
1155
1239
|
];
|
|
1156
1240
|
}
|
|
1157
|
-
function generateTypographyPrimitives(config) {
|
|
1241
|
+
function generateTypographyPrimitives(config, aliases = EMPTY_ALIASES) {
|
|
1158
1242
|
const decls = [];
|
|
1159
1243
|
const scale = config.typography.scale;
|
|
1160
1244
|
decls.push(`font-size: ${scale === 1 ? "1rem" : `${scale}rem`};`);
|
|
1161
|
-
decls.push(`--font-heading: ${config.typography.heading.family};`);
|
|
1162
|
-
decls.push(`--font-display: ${config.typography.display.family};`);
|
|
1163
|
-
decls.push(`--font-sans: ${config.typography.body.family};`);
|
|
1164
|
-
decls.push(`--font-body: ${config.typography.body.family};`);
|
|
1165
|
-
decls.push(`--font-mono: ${config.typography.mono.family};`);
|
|
1245
|
+
decls.push(`--font-heading: ${fontStack(config.typography.heading.family, aliases)};`);
|
|
1246
|
+
decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
|
|
1247
|
+
decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
|
|
1248
|
+
decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
|
|
1249
|
+
decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
|
|
1166
1250
|
const fontSizes = {
|
|
1167
1251
|
xs: 12,
|
|
1168
1252
|
sm: 14,
|
|
@@ -1233,24 +1317,25 @@ function generateMiscPrimitives() {
|
|
|
1233
1317
|
"--focus-ring-offset: 2px;"
|
|
1234
1318
|
];
|
|
1235
1319
|
}
|
|
1236
|
-
function generatePrimitivesCss(primitives, config) {
|
|
1320
|
+
function generatePrimitivesCss(primitives, config, options) {
|
|
1237
1321
|
const lines = [];
|
|
1322
|
+
const host = options?.scopePrefix ?? ":root";
|
|
1238
1323
|
lines.push(sectionComment("Primitive: Colors"));
|
|
1239
1324
|
lines.push(
|
|
1240
|
-
block(
|
|
1325
|
+
block(host, [generateColorPrimitives(primitives)])
|
|
1241
1326
|
);
|
|
1242
1327
|
lines.push(sectionComment("Primitive: Spacing"));
|
|
1243
|
-
lines.push(block(
|
|
1328
|
+
lines.push(block(host, generateSpacingPrimitives(config)));
|
|
1244
1329
|
lines.push(sectionComment("Primitive: Border Radius"));
|
|
1245
|
-
lines.push(block(
|
|
1330
|
+
lines.push(block(host, generateRadiusPrimitives(config)));
|
|
1246
1331
|
lines.push(sectionComment("Primitive: Typography"));
|
|
1247
|
-
lines.push(block(
|
|
1332
|
+
lines.push(block(host, generateTypographyPrimitives(config, options?.aliasedFamilies)));
|
|
1248
1333
|
lines.push(sectionComment("Primitive: Shadows"));
|
|
1249
|
-
lines.push(block(
|
|
1334
|
+
lines.push(block(host, generateShadowPrimitives(config)));
|
|
1250
1335
|
lines.push(sectionComment("Primitive: Motion"));
|
|
1251
|
-
lines.push(block(
|
|
1336
|
+
lines.push(block(host, generateMotionPrimitives(config)));
|
|
1252
1337
|
lines.push(sectionComment("Primitive: Miscellaneous"));
|
|
1253
|
-
lines.push(block(
|
|
1338
|
+
lines.push(block(host, generateMiscPrimitives()));
|
|
1254
1339
|
return header("Visor Theme \u2014 Primitives") + lines.join("\n");
|
|
1255
1340
|
}
|
|
1256
1341
|
function generateSemanticCss(tokens) {
|
|
@@ -1292,25 +1377,27 @@ function buildAdaptiveDecls(tokens, theme) {
|
|
|
1292
1377
|
);
|
|
1293
1378
|
return { textDecls, surfaceDecls, borderDecls, interactiveDecls };
|
|
1294
1379
|
}
|
|
1295
|
-
function generateLightCss(tokens) {
|
|
1380
|
+
function generateLightCss(tokens, options) {
|
|
1296
1381
|
const lines = [];
|
|
1297
1382
|
const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "light");
|
|
1383
|
+
const host = options?.scopePrefix ?? ":root";
|
|
1298
1384
|
lines.push(sectionComment("Adaptive: Text (light)"));
|
|
1299
|
-
lines.push(block(
|
|
1385
|
+
lines.push(block(host, textDecls));
|
|
1300
1386
|
lines.push(sectionComment("Adaptive: Surface (light)"));
|
|
1301
|
-
lines.push(block(
|
|
1387
|
+
lines.push(block(host, surfaceDecls));
|
|
1302
1388
|
lines.push(sectionComment("Adaptive: Border (light)"));
|
|
1303
|
-
lines.push(block(
|
|
1389
|
+
lines.push(block(host, borderDecls));
|
|
1304
1390
|
lines.push(sectionComment("Adaptive: Interactive (light)"));
|
|
1305
|
-
lines.push(block(
|
|
1391
|
+
lines.push(block(host, interactiveDecls));
|
|
1306
1392
|
return header("Visor Theme \u2014 Light") + lines.join("\n");
|
|
1307
1393
|
}
|
|
1308
|
-
function generateDarkCss(tokens) {
|
|
1394
|
+
function generateDarkCss(tokens, options) {
|
|
1309
1395
|
const lines = [];
|
|
1310
1396
|
const { textDecls, surfaceDecls, borderDecls, interactiveDecls } = buildAdaptiveDecls(tokens, "dark");
|
|
1311
|
-
const
|
|
1397
|
+
const prefix = options?.scopePrefix;
|
|
1398
|
+
const darkSelectors = prefix ? [`${prefix}.dark`, `${prefix}.theme-dark`, `${prefix}[data-theme="dark"]`] : [".dark", ".theme-dark", '[data-theme="dark"]'];
|
|
1312
1399
|
const darkSelector = darkSelectors.join(",\n");
|
|
1313
|
-
const prefersSelector = ':root:not(.light):not(.theme-light):not([data-theme="light"])';
|
|
1400
|
+
const prefersSelector = prefix ? `${prefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])';
|
|
1314
1401
|
lines.push(sectionComment("Adaptive: Text (dark) \u2014 manual toggle"));
|
|
1315
1402
|
lines.push(block(darkSelector, textDecls));
|
|
1316
1403
|
lines.push(sectionComment("Adaptive: Surface (dark) \u2014 manual toggle"));
|
|
@@ -1437,6 +1524,8 @@ export {
|
|
|
1437
1524
|
SELECTIVE_SHADE_STEPS,
|
|
1438
1525
|
TAILWIND_GRAY,
|
|
1439
1526
|
generateShadeScale,
|
|
1527
|
+
aliasFamily,
|
|
1528
|
+
fontStack,
|
|
1440
1529
|
header,
|
|
1441
1530
|
sectionComment,
|
|
1442
1531
|
generatePrimitivesCss,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-
|
|
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-CV0nmvMz.js';
|
|
2
|
+
export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-CV0nmvMz.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Font resolver — maps font family names to loadable font resources.
|
|
@@ -102,6 +102,44 @@ declare const googleFontsCatalog: GoogleFontEntry[];
|
|
|
102
102
|
/** Look up a font family in the Google Fonts catalog (case-insensitive) */
|
|
103
103
|
declare function lookupGoogleFont(family: string): GoogleFontEntry | undefined;
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Font coverage validator.
|
|
107
|
+
*
|
|
108
|
+
* Catches the failure mode behind VI-358: emitted theme CSS declares
|
|
109
|
+
* `--font-*: Family, ...` overrides but the same CSS contains no
|
|
110
|
+
* `@font-face` rule for that family, so the browser can never load the
|
|
111
|
+
* declared font and silently falls through to the next stack entry.
|
|
112
|
+
*
|
|
113
|
+
* The validator extracts the primary family from each `--font-*` declaration
|
|
114
|
+
* (the first comma-separated token, unquoted) and checks it against the set
|
|
115
|
+
* of @font-face families in the same emitted CSS. Generic CSS keywords
|
|
116
|
+
* (sans-serif, system-ui, etc.) and well-known platform-installed fonts
|
|
117
|
+
* (SF Mono, Helvetica, etc.) are skipped — those are intentionally part of
|
|
118
|
+
* the fallback stack and never carry their own @font-face.
|
|
119
|
+
*
|
|
120
|
+
* Size-adjusted system-fallback faces (family ends with " Fallback") are
|
|
121
|
+
* also excluded from the @font-face coverage set; they don't load a real
|
|
122
|
+
* font, they only adjust local metrics.
|
|
123
|
+
*/
|
|
124
|
+
interface FontCoverageError {
|
|
125
|
+
family: string;
|
|
126
|
+
declaredAt: string;
|
|
127
|
+
}
|
|
128
|
+
interface FontCoverageResult {
|
|
129
|
+
errors: FontCoverageError[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Format a font coverage error for surfacing through the CLI / private-themes
|
|
133
|
+
* generator. The mono slot gets an additional sentence calling out the engine
|
|
134
|
+
* version requirement and the CLI/engine version coupling (VI-367 / BO-37):
|
|
135
|
+
* bumping the engine alone is silently insufficient because the visor CLI
|
|
136
|
+
* transitively pins its own engine copy.
|
|
137
|
+
*
|
|
138
|
+
* Filename is included so multi-theme runs surface which theme is failing.
|
|
139
|
+
*/
|
|
140
|
+
declare function formatFontCoverageError(filename: string, declaredAt: string, family: string): string;
|
|
141
|
+
declare function validateFontCoverage(css: string): FontCoverageResult;
|
|
142
|
+
|
|
105
143
|
/**
|
|
106
144
|
* Import Pipeline
|
|
107
145
|
*
|
|
@@ -840,6 +878,30 @@ declare const SEMANTIC_MAP: {
|
|
|
840
878
|
interactive: Record<string, SemanticMapping>;
|
|
841
879
|
};
|
|
842
880
|
|
|
881
|
+
/**
|
|
882
|
+
* Per-theme font-family aliasing — substrate fix for VI-354.
|
|
883
|
+
*
|
|
884
|
+
* `@font-face` declarations are global to the document, so co-loaded themes
|
|
885
|
+
* that share a font family with differing per-theme properties (e.g.
|
|
886
|
+
* `size-adjust`) silently overwrite each other. Aliasing each theme's
|
|
887
|
+
* `@font-face` family as `{family} [{slug}]` scopes the declaration to
|
|
888
|
+
* that theme only; the theme's `--font-*` vars then list the alias first
|
|
889
|
+
* with the bare family as a fallback for graceful degradation.
|
|
890
|
+
*
|
|
891
|
+
* Lives in `fonts/` (not `adapters/`) because every adapter that emits
|
|
892
|
+
* visor-fonts `@font-face` blocks needs the same aliasing rules. Sharing
|
|
893
|
+
* the helpers prevents drift between adapters.
|
|
894
|
+
*/
|
|
895
|
+
/**
|
|
896
|
+
* Map of `bare family name → aliased family name` for every family the
|
|
897
|
+
* theme emits as a per-theme `@font-face`. The alias applies to every
|
|
898
|
+
* `--font-*` whose family matches an entry, regardless of which slot the
|
|
899
|
+
* var represents (the bug repro in VI-354 hinges on this for --font-mono,
|
|
900
|
+
* which can resolve to the same family as heading/body but doesn't carry
|
|
901
|
+
* the visor-fonts source through `resolveConfig`).
|
|
902
|
+
*/
|
|
903
|
+
type AliasedFamilies = ReadonlyMap<string, string>;
|
|
904
|
+
|
|
843
905
|
/**
|
|
844
906
|
* CSS Generation (Stage 3 + Output)
|
|
845
907
|
*
|
|
@@ -847,10 +909,17 @@ declare const SEMANTIC_MAP: {
|
|
|
847
909
|
* packages/tokens/src/generate/generate-css.ts output format.
|
|
848
910
|
*/
|
|
849
911
|
|
|
850
|
-
declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig
|
|
912
|
+
declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config: ResolvedThemeConfig, options?: {
|
|
913
|
+
aliasedFamilies?: AliasedFamilies;
|
|
914
|
+
scopePrefix?: string;
|
|
915
|
+
}): string;
|
|
851
916
|
declare function generateSemanticCss(tokens: SemanticTokens): string;
|
|
852
|
-
declare function generateLightCss(tokens: SemanticTokens
|
|
853
|
-
|
|
917
|
+
declare function generateLightCss(tokens: SemanticTokens, options?: {
|
|
918
|
+
scopePrefix?: string;
|
|
919
|
+
}): string;
|
|
920
|
+
declare function generateDarkCss(tokens: SemanticTokens, options?: {
|
|
921
|
+
scopePrefix?: string;
|
|
922
|
+
}): string;
|
|
854
923
|
declare function generateFullBundleCss(primitives: GeneratedPrimitives, tokens: SemanticTokens, config: ResolvedThemeConfig): string;
|
|
855
924
|
|
|
856
925
|
/**
|
|
@@ -928,4 +997,4 @@ declare function cleanFontValue(val: string): string;
|
|
|
928
997
|
*/
|
|
929
998
|
declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
|
|
930
999
|
|
|
931
|
-
export { type CSSFile, ColorRole, type Confidence, type ExtractedToken, type ExtractionResult, FONT_WEIGHT_ALIASES, FontDisplayStrategy, type FontFaceDeclaration, FontResolution, FontResolveOptions, FullShadeScale, GeneratedPrimitives, GoogleFontEntry, OKLCH, ParsedColor, RGB, ResolvedThemeConfig, SEMANTIC_MAP, SelectiveShadeScale, SemanticTokens, ShadeStep, TAILWIND_GRAY, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_FONTS_CDN, type ValidationIssue, type ValidationSeverity, VisorThemeConfig, VisorTypography, applyOverrides, assignSemanticTokens, buildVisorFontUrl, clampToSrgb, cleanFontValue, compositeOverBackground, exportTheme, extractFromCSS, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, googleFontsCatalog, hexToOklch, hexToRgb, isValidColor, isValidHex, isVisorThemeConfig, lookupFontWeightAlias, lookupGoogleFont, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveConfig, resolveFont, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, visorTheme_schema as visorThemeSchema };
|
|
1000
|
+
export { type CSSFile, ColorRole, type Confidence, type ExtractedToken, type ExtractionResult, FONT_WEIGHT_ALIASES, type FontCoverageError, type FontCoverageResult, FontDisplayStrategy, type FontFaceDeclaration, FontResolution, FontResolveOptions, FullShadeScale, GeneratedPrimitives, GoogleFontEntry, OKLCH, ParsedColor, RGB, ResolvedThemeConfig, SEMANTIC_MAP, SelectiveShadeScale, SemanticTokens, ShadeStep, TAILWIND_GRAY, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_FONTS_CDN, type ValidationIssue, type ValidationSeverity, VisorThemeConfig, VisorTypography, applyOverrides, assignSemanticTokens, buildVisorFontUrl, clampToSrgb, cleanFontValue, compositeOverBackground, exportTheme, extractFromCSS, formatFontCoverageError, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, googleFontsCatalog, hexToOklch, hexToRgb, isValidColor, isValidHex, isVisorThemeConfig, lookupFontWeightAlias, lookupGoogleFont, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveConfig, resolveFont, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,161 @@ import {
|
|
|
34
34
|
rgbToHex,
|
|
35
35
|
rgbToOklch,
|
|
36
36
|
serializeColor
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-2O2DPCMJ.js";
|
|
38
|
+
|
|
39
|
+
// src/fonts/validate-coverage.ts
|
|
40
|
+
var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
|
|
41
|
+
var FONT_FACE_RE = /@font-face\s*\{[^}]*\}/g;
|
|
42
|
+
var FONT_FAMILY_DECL_RE = /font-family\s*:\s*([^;]+);/;
|
|
43
|
+
var GOOGLE_FONTS_IMPORT_RE = /@import\s+url\(["']?https:\/\/fonts\.googleapis\.com\/css2?\?family=([^:&"')]+)/g;
|
|
44
|
+
var FONTSHARE_IMPORT_RE = /@import\s+url\(["']?https:\/\/api\.fontshare\.com\/v2\/css\?f(?:\[\]|%5B%5D)=([a-z0-9-]+)/gi;
|
|
45
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
46
|
+
"serif",
|
|
47
|
+
"sans-serif",
|
|
48
|
+
"monospace",
|
|
49
|
+
"cursive",
|
|
50
|
+
"fantasy",
|
|
51
|
+
"system-ui",
|
|
52
|
+
"ui-monospace",
|
|
53
|
+
"ui-sans-serif",
|
|
54
|
+
"ui-serif",
|
|
55
|
+
"ui-rounded",
|
|
56
|
+
"emoji",
|
|
57
|
+
"math",
|
|
58
|
+
"fangsong",
|
|
59
|
+
"inherit",
|
|
60
|
+
"initial",
|
|
61
|
+
"unset",
|
|
62
|
+
"revert",
|
|
63
|
+
"none",
|
|
64
|
+
// Apple / Chromium-on-Mac system keywords. These behave like
|
|
65
|
+
// CSS-engine-level aliases, not real font families — `local()` can
|
|
66
|
+
// never resolve them, so they never need an @font-face.
|
|
67
|
+
"-apple-system",
|
|
68
|
+
"-webkit-system-font",
|
|
69
|
+
"BlinkMacSystemFont"
|
|
70
|
+
]);
|
|
71
|
+
var SYSTEM_FONTS = /* @__PURE__ */ new Set([
|
|
72
|
+
"Apple Color Emoji",
|
|
73
|
+
"Arial",
|
|
74
|
+
"Arial Black",
|
|
75
|
+
"BlinkMacSystemFont",
|
|
76
|
+
"Cambria",
|
|
77
|
+
"Comic Sans MS",
|
|
78
|
+
"Consolas",
|
|
79
|
+
"Courier",
|
|
80
|
+
"Courier New",
|
|
81
|
+
"DejaVu Sans",
|
|
82
|
+
"DejaVu Sans Mono",
|
|
83
|
+
"Fira Code",
|
|
84
|
+
"Fira Mono",
|
|
85
|
+
"Fira Sans",
|
|
86
|
+
"Georgia",
|
|
87
|
+
"Helvetica",
|
|
88
|
+
"Helvetica Neue",
|
|
89
|
+
"Impact",
|
|
90
|
+
"JetBrains Mono",
|
|
91
|
+
"Liberation Mono",
|
|
92
|
+
"Liberation Sans",
|
|
93
|
+
"Lucida Console",
|
|
94
|
+
"Lucida Grande",
|
|
95
|
+
"Menlo",
|
|
96
|
+
"Microsoft YaHei",
|
|
97
|
+
"Monaco",
|
|
98
|
+
"Noto Color Emoji",
|
|
99
|
+
"Open Sans",
|
|
100
|
+
"PingFang SC",
|
|
101
|
+
"PingFang TC",
|
|
102
|
+
"Roboto",
|
|
103
|
+
"Roboto Mono",
|
|
104
|
+
"Roboto Slab",
|
|
105
|
+
"SF Mono",
|
|
106
|
+
"SF Pro Display",
|
|
107
|
+
"SF Pro Text",
|
|
108
|
+
"Segoe UI",
|
|
109
|
+
"Segoe UI Emoji",
|
|
110
|
+
"Segoe UI Symbol",
|
|
111
|
+
"Segoe UI Variable",
|
|
112
|
+
"Source Code Pro",
|
|
113
|
+
"Source Sans Pro",
|
|
114
|
+
"Times",
|
|
115
|
+
"Times New Roman",
|
|
116
|
+
"Trebuchet MS",
|
|
117
|
+
"Verdana"
|
|
118
|
+
]);
|
|
119
|
+
function extractPrimaryFamily(value) {
|
|
120
|
+
const trimmed = value.trim();
|
|
121
|
+
if (trimmed.startsWith("var(")) return null;
|
|
122
|
+
const firstToken = trimmed.split(",")[0].trim();
|
|
123
|
+
if (!firstToken) return null;
|
|
124
|
+
const unquoted = firstToken.replace(/^["']|["']$/g, "");
|
|
125
|
+
if (GENERIC_FAMILIES.has(unquoted)) return null;
|
|
126
|
+
if (SYSTEM_FONTS.has(unquoted)) return null;
|
|
127
|
+
return unquoted;
|
|
128
|
+
}
|
|
129
|
+
function extractFontFaceFamilies(css) {
|
|
130
|
+
const families = /* @__PURE__ */ new Set();
|
|
131
|
+
const blocks = css.match(FONT_FACE_RE) ?? [];
|
|
132
|
+
for (const block of blocks) {
|
|
133
|
+
const decl = FONT_FAMILY_DECL_RE.exec(block);
|
|
134
|
+
if (!decl) continue;
|
|
135
|
+
const family = decl[1].trim().replace(/^["']|["'];?$/g, "");
|
|
136
|
+
if (family.endsWith(" Fallback")) continue;
|
|
137
|
+
families.add(family);
|
|
138
|
+
}
|
|
139
|
+
return families;
|
|
140
|
+
}
|
|
141
|
+
function extractGoogleFontsImports(css) {
|
|
142
|
+
const families = /* @__PURE__ */ new Set();
|
|
143
|
+
for (const match of css.matchAll(GOOGLE_FONTS_IMPORT_RE)) {
|
|
144
|
+
const family = decodeURIComponent(match[1]).replace(/\+/g, " ");
|
|
145
|
+
families.add(family);
|
|
146
|
+
}
|
|
147
|
+
return families;
|
|
148
|
+
}
|
|
149
|
+
function fontshareSlugToFamily(slug) {
|
|
150
|
+
return slug.split("-").filter((p) => p.length > 0).map((p) => p[0].toUpperCase() + p.slice(1)).join(" ");
|
|
151
|
+
}
|
|
152
|
+
function extractFontshareImports(css) {
|
|
153
|
+
const families = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const match of css.matchAll(FONTSHARE_IMPORT_RE)) {
|
|
155
|
+
families.add(fontshareSlugToFamily(match[1]));
|
|
156
|
+
}
|
|
157
|
+
return families;
|
|
158
|
+
}
|
|
159
|
+
function extractFontVarDeclarations(css) {
|
|
160
|
+
const decls = [];
|
|
161
|
+
for (const match of css.matchAll(FONT_VAR_RE)) {
|
|
162
|
+
const slot = `--font-${match[1]}`;
|
|
163
|
+
const family = extractPrimaryFamily(match[2]);
|
|
164
|
+
if (!family) continue;
|
|
165
|
+
decls.push({ slot, family });
|
|
166
|
+
}
|
|
167
|
+
return decls;
|
|
168
|
+
}
|
|
169
|
+
function formatFontCoverageError(filename, declaredAt, family) {
|
|
170
|
+
const base = `${filename}: ${declaredAt} declares "${family}" with no matching @font-face. `;
|
|
171
|
+
if (declaredAt === "--font-mono") {
|
|
172
|
+
return base + `Set typography.mono.source: visor-fonts (with org:), google-fonts, or fontshare; or pick a system mono font. The mono slot's source/org keys require @loworbitstudio/visor-theme-engine \u2265 0.5.0 and @loworbitstudio/visor \u2265 0.10.0 \u2014 bump both, since the CLI bundles its own engine copy.`;
|
|
173
|
+
}
|
|
174
|
+
return base + `Set typography.<slot>.source: visor-fonts (with org:), google-fonts, or fontshare; or pick a system font.`;
|
|
175
|
+
}
|
|
176
|
+
function validateFontCoverage(css) {
|
|
177
|
+
const declaredFamilies = extractFontFaceFamilies(css);
|
|
178
|
+
for (const f of extractGoogleFontsImports(css)) declaredFamilies.add(f);
|
|
179
|
+
for (const f of extractFontshareImports(css)) declaredFamilies.add(f);
|
|
180
|
+
const declarations = extractFontVarDeclarations(css);
|
|
181
|
+
const errors = [];
|
|
182
|
+
const seen = /* @__PURE__ */ new Set();
|
|
183
|
+
for (const decl of declarations) {
|
|
184
|
+
if (declaredFamilies.has(decl.family)) continue;
|
|
185
|
+
const key = `${decl.slot}::${decl.family}`;
|
|
186
|
+
if (seen.has(key)) continue;
|
|
187
|
+
seen.add(key);
|
|
188
|
+
errors.push({ family: decl.family, declaredAt: decl.slot });
|
|
189
|
+
}
|
|
190
|
+
return { errors };
|
|
191
|
+
}
|
|
38
192
|
|
|
39
193
|
// src/pipeline.ts
|
|
40
194
|
import { parse as parseYaml } from "yaml";
|
|
@@ -429,7 +583,7 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
|
|
|
429
583
|
"slots"
|
|
430
584
|
]);
|
|
431
585
|
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
432
|
-
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
|
|
586
|
+
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
433
587
|
var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
|
|
434
588
|
var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
|
|
435
589
|
var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
|
|
@@ -763,6 +917,7 @@ function resolveConfig(config) {
|
|
|
763
917
|
}
|
|
764
918
|
return {
|
|
765
919
|
name: config.name,
|
|
920
|
+
...config.label !== void 0 && { label: config.label },
|
|
766
921
|
version: 1,
|
|
767
922
|
colors: {
|
|
768
923
|
primary: colors.primary,
|
|
@@ -801,7 +956,11 @@ function resolveConfig(config) {
|
|
|
801
956
|
...config.typography?.body?.weights && { weights: config.typography.body.weights }
|
|
802
957
|
},
|
|
803
958
|
mono: {
|
|
804
|
-
family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
|
|
959
|
+
family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family,
|
|
960
|
+
...config.typography?.mono?.weight && { weight: config.typography.mono.weight },
|
|
961
|
+
...config.typography?.mono?.weights && { weights: config.typography.mono.weights },
|
|
962
|
+
...config.typography?.mono?.source && { source: config.typography.mono.source },
|
|
963
|
+
...config.typography?.mono?.org && { org: config.typography.mono.org }
|
|
805
964
|
},
|
|
806
965
|
slots: config.typography?.slots ?? {}
|
|
807
966
|
},
|
|
@@ -2640,6 +2799,7 @@ export {
|
|
|
2640
2799
|
compositeOverBackground,
|
|
2641
2800
|
exportTheme,
|
|
2642
2801
|
extractFromCSS,
|
|
2802
|
+
formatFontCoverageError,
|
|
2643
2803
|
generateDarkCss,
|
|
2644
2804
|
generateFullBundleCss,
|
|
2645
2805
|
generateLightCss,
|
|
@@ -2679,5 +2839,6 @@ export {
|
|
|
2679
2839
|
serializeColor,
|
|
2680
2840
|
validate,
|
|
2681
2841
|
validateConfig,
|
|
2842
|
+
validateFontCoverage,
|
|
2682
2843
|
visor_theme_schema_default as visorThemeSchema
|
|
2683
2844
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Font resolution types for the Visor theme engine.
|
|
3
3
|
*/
|
|
4
4
|
/** Where a font is loaded from */
|
|
5
|
-
type FontSource = "google-fonts" | "visor-fonts" | "local";
|
|
5
|
+
type FontSource = "google-fonts" | "visor-fonts" | "fontshare" | "local";
|
|
6
6
|
/** CSS font-display strategy */
|
|
7
7
|
type FontDisplayStrategy = "swap" | "block" | "fallback" | "optional" | "auto";
|
|
8
8
|
/** A single resolved font */
|
|
@@ -11,7 +11,7 @@ interface FontResolution {
|
|
|
11
11
|
family: string;
|
|
12
12
|
/** Where this font comes from */
|
|
13
13
|
source: FontSource;
|
|
14
|
-
/**
|
|
14
|
+
/** Hosted CSS URL (only for google-fonts or fontshare sources) */
|
|
15
15
|
cssUrl: string | null;
|
|
16
16
|
/** Weights available/requested for this font */
|
|
17
17
|
weights: number[];
|
|
@@ -70,6 +70,8 @@ interface VisorTypography {
|
|
|
70
70
|
mono?: {
|
|
71
71
|
family: string;
|
|
72
72
|
weight?: number;
|
|
73
|
+
/** Explicit list of font weights to load (overrides engine defaults) */
|
|
74
|
+
weights?: number[];
|
|
73
75
|
source?: FontSource;
|
|
74
76
|
org?: string;
|
|
75
77
|
};
|
|
@@ -198,6 +200,7 @@ interface VisorThemeConfig {
|
|
|
198
200
|
mono?: {
|
|
199
201
|
family?: string;
|
|
200
202
|
weight?: number;
|
|
203
|
+
weights?: number[];
|
|
201
204
|
source?: FontSource;
|
|
202
205
|
org?: string;
|
|
203
206
|
};
|
|
@@ -266,6 +269,8 @@ interface VisorThemeConfig {
|
|
|
266
269
|
/** Config with all defaults resolved */
|
|
267
270
|
interface ResolvedThemeConfig {
|
|
268
271
|
name: string;
|
|
272
|
+
/** Optional display label override forwarded from VisorThemeConfig.label. */
|
|
273
|
+
label?: string;
|
|
269
274
|
version: 1;
|
|
270
275
|
colors: {
|
|
271
276
|
primary: string;
|
|
@@ -304,6 +309,10 @@ interface ResolvedThemeConfig {
|
|
|
304
309
|
};
|
|
305
310
|
mono: {
|
|
306
311
|
family: string;
|
|
312
|
+
weight?: number;
|
|
313
|
+
weights?: number[];
|
|
314
|
+
source?: FontSource;
|
|
315
|
+
org?: string;
|
|
307
316
|
};
|
|
308
317
|
/**
|
|
309
318
|
* Per-slot Material `TextTheme` overrides, passed through from the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.
|
|
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",
|