@loworbitstudio/visor-theme-engine 0.4.1 → 0.5.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 +10 -6
- package/dist/adapters/index.js +16 -13
- package/dist/{chunk-G4B57FB3.js → chunk-U5FXQ5EC.js} +27 -7
- package/dist/index.d.ts +33 -3
- package/dist/index.js +110 -55
- package/dist/{types-gAlkt__C.d.ts → types-ljcTtODU.d.ts} +15 -0
- 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-ljcTtODU.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Adapter types for the Visor theme engine.
|
|
@@ -139,8 +139,10 @@ declare function deckAdapter(input: AdapterInput, options?: DeckAdapterOptions):
|
|
|
139
139
|
/**
|
|
140
140
|
* Generate docs-site CSS for a theme.
|
|
141
141
|
*
|
|
142
|
-
* Output is class-scoped (.{slug}-theme)
|
|
143
|
-
*
|
|
142
|
+
* Output is class-scoped (.{slug}-theme) and wrapped in @layer visor-adaptive
|
|
143
|
+
* so consumer overrides (unlayered) and visor-core's own visor-adaptive layer
|
|
144
|
+
* cascade correctly. Font @import / @font-face statements stay outside the
|
|
145
|
+
* layer block per CSS spec (VI-312).
|
|
144
146
|
*/
|
|
145
147
|
declare function docsAdapter(input: AdapterInput, options?: DocsAdapterOptions): string;
|
|
146
148
|
|
|
@@ -177,9 +179,11 @@ declare function flutterAdapter(input: AdapterInput, options?: FlutterAdapterOpt
|
|
|
177
179
|
/**
|
|
178
180
|
* CSS @layer utilities for adapter output.
|
|
179
181
|
*
|
|
180
|
-
* Establishes a specificity ordering so theme overrides work
|
|
181
|
-
*
|
|
182
|
-
*
|
|
182
|
+
* Establishes a specificity ordering so theme overrides work without
|
|
183
|
+
* !important. Both adapter output (here) and visor-core's emitted CSS
|
|
184
|
+
* (packages/tokens/src/generate/generate-css.ts) declare this same layer
|
|
185
|
+
* order — defense in depth, so whichever stylesheet loads first establishes
|
|
186
|
+
* the cascade.
|
|
183
187
|
*/
|
|
184
188
|
/** Layer order declaration — must appear before any @layer blocks. */
|
|
185
189
|
declare const LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
|
package/dist/adapters/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
parseColor,
|
|
12
12
|
resolveThemeFonts,
|
|
13
13
|
sectionComment
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-U5FXQ5EC.js";
|
|
15
15
|
|
|
16
16
|
// src/adapters/layers.ts
|
|
17
17
|
var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
|
|
@@ -418,6 +418,7 @@ function docsAdapter(input, options) {
|
|
|
418
418
|
const slug = toKebabCase2(input.config.name);
|
|
419
419
|
const scopeClass = `.${slug}-theme`;
|
|
420
420
|
const includeFontImports = options?.includeFontImports ?? true;
|
|
421
|
+
const fontLines = [];
|
|
421
422
|
const lines = [];
|
|
422
423
|
if (includeFontImports && input.config.typography) {
|
|
423
424
|
const fontResult = resolveThemeFonts(input.config.typography);
|
|
@@ -426,8 +427,8 @@ function docsAdapter(input, options) {
|
|
|
426
427
|
for (const font of fontSlots) {
|
|
427
428
|
if (font && font.source === "google-fonts" && font.cssUrl && !seenUrls.has(font.cssUrl)) {
|
|
428
429
|
seenUrls.add(font.cssUrl);
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
fontLines.push(`@import url("${font.cssUrl}");`);
|
|
431
|
+
fontLines.push("");
|
|
431
432
|
}
|
|
432
433
|
}
|
|
433
434
|
const scale = input.config.typography?.scale ?? 1;
|
|
@@ -437,17 +438,17 @@ function docsAdapter(input, options) {
|
|
|
437
438
|
seenFamilies.add(font.family);
|
|
438
439
|
for (const weight of font.weights) {
|
|
439
440
|
const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
fontLines.push("@font-face {");
|
|
442
|
+
fontLines.push(` font-family: "${font.family}";`);
|
|
443
|
+
fontLines.push(` src: url("${url}") format("woff2");`);
|
|
444
|
+
fontLines.push(` font-weight: ${weight};`);
|
|
445
|
+
fontLines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
|
|
446
|
+
fontLines.push(` font-display: ${font.display};`);
|
|
446
447
|
if (scale !== 1) {
|
|
447
|
-
|
|
448
|
+
fontLines.push(` size-adjust: ${Math.round(scale * 100)}%;`);
|
|
448
449
|
}
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
fontLines.push("}");
|
|
451
|
+
fontLines.push("");
|
|
451
452
|
}
|
|
452
453
|
}
|
|
453
454
|
}
|
|
@@ -562,7 +563,9 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
562
563
|
lines.push(sectionComment2("Fumadocs bridge: light"));
|
|
563
564
|
lines.push(block(`html:not(.dark) ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "light")));
|
|
564
565
|
lines.push("");
|
|
565
|
-
|
|
566
|
+
const layered = wrapInLayer("visor-adaptive", lines.join("\n").trim());
|
|
567
|
+
const head = fontLines.length > 0 ? fontLines.join("\n") + "\n" : "";
|
|
568
|
+
return head + LAYER_ORDER + "\n\n" + layered + "\n";
|
|
566
569
|
}
|
|
567
570
|
|
|
568
571
|
// src/flutter/color-to-dart.ts
|
|
@@ -116,6 +116,21 @@ function lookupGoogleFont(family) {
|
|
|
116
116
|
return catalogMap.get(family.toLowerCase());
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// src/fonts/font-aliases.ts
|
|
120
|
+
var FONT_WEIGHT_ALIASES = {
|
|
121
|
+
"PP Model Mono": {
|
|
122
|
+
400: "Book",
|
|
123
|
+
800: "Super"
|
|
124
|
+
},
|
|
125
|
+
"PP Model Plastic": {
|
|
126
|
+
400: "Book",
|
|
127
|
+
800: "Super"
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function lookupFontWeightAlias(family, weight) {
|
|
131
|
+
return FONT_WEIGHT_ALIASES[family]?.[weight] ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
// src/fonts/resolve.ts
|
|
120
135
|
var DEFAULT_WEIGHTS = [400, 700];
|
|
121
136
|
var DEFAULT_DISPLAY = "swap";
|
|
@@ -156,7 +171,7 @@ var WEIGHT_NAMES = {
|
|
|
156
171
|
function buildVisorFontUrl(org, family, weight) {
|
|
157
172
|
const slug = buildFamilySlug(family);
|
|
158
173
|
const prefix = buildFamilyPrefix(family);
|
|
159
|
-
const weightName = WEIGHT_NAMES[weight] ?? `W${weight}`;
|
|
174
|
+
const weightName = lookupFontWeightAlias(family, weight) ?? WEIGHT_NAMES[weight] ?? `W${weight}`;
|
|
160
175
|
return `${VISOR_FONTS_CDN}/${org}/${slug}/${prefix}-${weightName}.woff2`;
|
|
161
176
|
}
|
|
162
177
|
function resolveFont(family, options = {}) {
|
|
@@ -964,8 +979,11 @@ var LIGHTNESS_TARGETS = {
|
|
|
964
979
|
200: 0.87,
|
|
965
980
|
300: 0.78,
|
|
966
981
|
400: 0.65,
|
|
967
|
-
500
|
|
968
|
-
//
|
|
982
|
+
// 500 is the reference midpoint (Tailwind gray-500's OKLCH L ≈ 0.55).
|
|
983
|
+
// At the anchor shade the input L is used directly; this value seeds the
|
|
984
|
+
// interpolation math in computeLightness() for inputs that don't land at
|
|
985
|
+
// the reference midpoint.
|
|
986
|
+
500: 0.55,
|
|
969
987
|
600: 0.45,
|
|
970
988
|
700: 0.38,
|
|
971
989
|
800: 0.3,
|
|
@@ -1012,20 +1030,20 @@ function computeLightness(step, inputL, anchorShade) {
|
|
|
1012
1030
|
if (step === anchorShade) {
|
|
1013
1031
|
return inputL;
|
|
1014
1032
|
}
|
|
1015
|
-
const anchorTarget =
|
|
1033
|
+
const anchorTarget = LIGHTNESS_TARGETS[anchorShade];
|
|
1016
1034
|
const stepTarget = LIGHTNESS_TARGETS[step];
|
|
1017
1035
|
if (Math.abs(anchorTarget - inputL) < 0.01) {
|
|
1018
1036
|
return stepTarget;
|
|
1019
1037
|
}
|
|
1020
1038
|
if (step < anchorShade) {
|
|
1021
|
-
const anchorDefaultL =
|
|
1039
|
+
const anchorDefaultL = LIGHTNESS_TARGETS[anchorShade];
|
|
1022
1040
|
const rawRange = 0.97 - anchorDefaultL;
|
|
1023
1041
|
const newRange = 0.97 - inputL;
|
|
1024
1042
|
if (rawRange <= 0) return stepTarget;
|
|
1025
1043
|
const t = (stepTarget - anchorDefaultL) / rawRange;
|
|
1026
1044
|
return inputL + t * newRange;
|
|
1027
1045
|
} else {
|
|
1028
|
-
const anchorDefaultL =
|
|
1046
|
+
const anchorDefaultL = LIGHTNESS_TARGETS[anchorShade];
|
|
1029
1047
|
const rawRange = anchorDefaultL - 0.14;
|
|
1030
1048
|
const newRange = inputL - 0.14;
|
|
1031
1049
|
if (rawRange <= 0) return stepTarget;
|
|
@@ -1043,7 +1061,7 @@ function generateShadeScale(color, role) {
|
|
|
1043
1061
|
for (const step of steps) {
|
|
1044
1062
|
const targetL = computeLightness(step, inputL, anchorShade);
|
|
1045
1063
|
let targetC = inputC * CHROMA_MULTIPLIERS[step];
|
|
1046
|
-
if (role === "neutral") {
|
|
1064
|
+
if (role === "neutral" && step !== anchorShade) {
|
|
1047
1065
|
targetC = Math.min(targetC, maxNeutralChroma);
|
|
1048
1066
|
}
|
|
1049
1067
|
scale[step] = oklchToHex(targetL, targetC, inputH);
|
|
@@ -1389,6 +1407,8 @@ var MATERIAL_TEXT_SLOTS = [
|
|
|
1389
1407
|
export {
|
|
1390
1408
|
googleFontsCatalog,
|
|
1391
1409
|
lookupGoogleFont,
|
|
1410
|
+
FONT_WEIGHT_ALIASES,
|
|
1411
|
+
lookupFontWeightAlias,
|
|
1392
1412
|
VISOR_FONTS_CDN,
|
|
1393
1413
|
buildVisorFontUrl,
|
|
1394
1414
|
resolveFont,
|
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-ljcTtODU.js';
|
|
2
|
+
export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-ljcTtODU.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Font resolver — maps font family names to loadable font resources.
|
|
@@ -28,6 +28,22 @@ declare function buildVisorFontUrl(org: string, family: string, weight: number):
|
|
|
28
28
|
*/
|
|
29
29
|
declare function resolveFont(family: string, options?: FontResolveOptions): FontResolution;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Font weight-name alias registry for the Visor Fonts CDN URL builder.
|
|
33
|
+
*
|
|
34
|
+
* Standard PostScript naming (Light/Regular/Medium/Bold/ExtraBold/Black)
|
|
35
|
+
* is handled by the WEIGHT_NAMES table in resolve.ts. Foundries that use
|
|
36
|
+
* non-standard names (e.g. Pangram Pangram's `Book` and `Super`) register
|
|
37
|
+
* per-family overrides here so theme authors can keep writing standard
|
|
38
|
+
* weight numbers in their .visor.yaml files.
|
|
39
|
+
*
|
|
40
|
+
* Family keys are exact-match (case-sensitive); weight keys are the numeric
|
|
41
|
+
* weight (300, 400, 500, …) as in WEIGHT_NAMES. The mapped string is the
|
|
42
|
+
* PostScript-style suffix that follows `{Family}-` in the bucket filename.
|
|
43
|
+
*/
|
|
44
|
+
declare const FONT_WEIGHT_ALIASES: Record<string, Record<number, string>>;
|
|
45
|
+
declare function lookupFontWeightAlias(family: string, weight: number): string | null;
|
|
46
|
+
|
|
31
47
|
/**
|
|
32
48
|
* Preload hint generation for font loading performance.
|
|
33
49
|
*
|
|
@@ -556,6 +572,20 @@ var properties = {
|
|
|
556
572
|
}
|
|
557
573
|
}
|
|
558
574
|
}
|
|
575
|
+
},
|
|
576
|
+
migrate: {
|
|
577
|
+
type: "object",
|
|
578
|
+
description: "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
|
|
579
|
+
additionalProperties: false,
|
|
580
|
+
properties: {
|
|
581
|
+
"token-substitution": {
|
|
582
|
+
type: "object",
|
|
583
|
+
description: "§3.1 V7-primitive → Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
|
|
584
|
+
additionalProperties: {
|
|
585
|
+
type: "string"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
559
589
|
}
|
|
560
590
|
};
|
|
561
591
|
var $defs = {
|
|
@@ -898,4 +928,4 @@ declare function cleanFontValue(val: string): string;
|
|
|
898
928
|
*/
|
|
899
929
|
declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
|
|
900
930
|
|
|
901
|
-
export { type CSSFile, ColorRole, type Confidence, type ExtractedToken, type ExtractionResult, 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, lookupGoogleFont, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveConfig, resolveFont, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, visorTheme_schema as visorThemeSchema };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
FONT_WEIGHT_ALIASES,
|
|
2
3
|
MATERIAL_TEXT_SLOTS,
|
|
3
4
|
TAILWIND_GRAY,
|
|
4
5
|
VISOR_FONTS_CDN,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
hexToRgb,
|
|
20
21
|
isValidColor,
|
|
21
22
|
isValidHex,
|
|
23
|
+
lookupFontWeightAlias,
|
|
22
24
|
lookupGoogleFont,
|
|
23
25
|
normalizeHex,
|
|
24
26
|
oklchToHex,
|
|
@@ -32,7 +34,7 @@ import {
|
|
|
32
34
|
rgbToHex,
|
|
33
35
|
rgbToOklch,
|
|
34
36
|
serializeColor
|
|
35
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-U5FXQ5EC.js";
|
|
36
38
|
|
|
37
39
|
// src/pipeline.ts
|
|
38
40
|
import { parse as parseYaml } from "yaml";
|
|
@@ -339,6 +341,18 @@ var visor_theme_schema_default = {
|
|
|
339
341
|
additionalProperties: { type: "string" }
|
|
340
342
|
}
|
|
341
343
|
}
|
|
344
|
+
},
|
|
345
|
+
migrate: {
|
|
346
|
+
type: "object",
|
|
347
|
+
description: "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
properties: {
|
|
350
|
+
"token-substitution": {
|
|
351
|
+
type: "object",
|
|
352
|
+
description: "\xA73.1 V7-primitive \u2192 Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
|
|
353
|
+
additionalProperties: { type: "string" }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
342
356
|
}
|
|
343
357
|
},
|
|
344
358
|
$defs: {
|
|
@@ -839,13 +853,17 @@ var SEMANTIC_TEXT_MAP = {
|
|
|
839
853
|
light: { role: "neutral", shade: 900 },
|
|
840
854
|
dark: { role: "neutral", shade: 50 }
|
|
841
855
|
},
|
|
856
|
+
// VI-346: rebalanced to fixed-L shades (700/300, 600/400) so AA contrast holds
|
|
857
|
+
// by default for any reasonable input neutral. Shade 500 is brand-anchored
|
|
858
|
+
// (variable L = input itself) and therefore forbidden for any text level that
|
|
859
|
+
// must clear AA. See docs/token-rules.md and SEMANTIC_TEXT_MAP rationale.
|
|
842
860
|
secondary: {
|
|
843
|
-
light: { role: "neutral", shade:
|
|
844
|
-
dark: { role: "neutral", shade:
|
|
861
|
+
light: { role: "neutral", shade: 700 },
|
|
862
|
+
dark: { role: "neutral", shade: 300 }
|
|
845
863
|
},
|
|
846
864
|
tertiary: {
|
|
847
|
-
light: { role: "neutral", shade:
|
|
848
|
-
dark: { role: "neutral", shade:
|
|
865
|
+
light: { role: "neutral", shade: 600 },
|
|
866
|
+
dark: { role: "neutral", shade: 400 }
|
|
849
867
|
},
|
|
850
868
|
disabled: {
|
|
851
869
|
light: { role: "neutral", shade: 300 },
|
|
@@ -976,6 +994,29 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
976
994
|
"info-default": {
|
|
977
995
|
light: { role: "info", shade: 500 },
|
|
978
996
|
dark: { role: "info", shade: 500 }
|
|
997
|
+
},
|
|
998
|
+
// 5-tier ordinal elevation scale — deepest (0) to highest (4)
|
|
999
|
+
// Light mode: BO-10 near-white ramp (white → neutral-300).
|
|
1000
|
+
// Dark mode: deep neutral ramp (neutral-950 → neutral-600).
|
|
1001
|
+
"elev-0": {
|
|
1002
|
+
light: { constant: "#ffffff" },
|
|
1003
|
+
dark: { role: "neutral", shade: 950 }
|
|
1004
|
+
},
|
|
1005
|
+
"elev-1": {
|
|
1006
|
+
light: { role: "neutral", shade: 50 },
|
|
1007
|
+
dark: { role: "neutral", shade: 900 }
|
|
1008
|
+
},
|
|
1009
|
+
"elev-2": {
|
|
1010
|
+
light: { role: "neutral", shade: 100 },
|
|
1011
|
+
dark: { role: "neutral", shade: 800 }
|
|
1012
|
+
},
|
|
1013
|
+
"elev-3": {
|
|
1014
|
+
light: { role: "neutral", shade: 200 },
|
|
1015
|
+
dark: { role: "neutral", shade: 700 }
|
|
1016
|
+
},
|
|
1017
|
+
"elev-4": {
|
|
1018
|
+
light: { role: "neutral", shade: 300 },
|
|
1019
|
+
dark: { role: "neutral", shade: 600 }
|
|
979
1020
|
}
|
|
980
1021
|
};
|
|
981
1022
|
var SEMANTIC_BORDER_MAP = {
|
|
@@ -1826,35 +1867,75 @@ function colorToRgb(color) {
|
|
|
1826
1867
|
const parsed = parseColor(color);
|
|
1827
1868
|
return parsed ? parsed.rgb : [0, 0, 0];
|
|
1828
1869
|
}
|
|
1870
|
+
var STANDARD_TEXT_TOKENS = ["primary", "secondary", "tertiary"];
|
|
1871
|
+
var STATUS_TEXT_TOKENS = ["error", "warning", "success", "info"];
|
|
1829
1872
|
function checkContrastWarnings(config, issues) {
|
|
1830
1873
|
const resolved = resolveConfig(config);
|
|
1874
|
+
const lightPrimitives = generatePrimitives(resolved);
|
|
1875
|
+
const darkPrimitives = generateDarkPrimitives(resolved, lightPrimitives);
|
|
1876
|
+
const tokens = applyOverrides(
|
|
1877
|
+
assignSemanticTokens(lightPrimitives, darkPrimitives, resolved),
|
|
1878
|
+
resolved.overrides
|
|
1879
|
+
);
|
|
1831
1880
|
const lightBg = resolved.colors.background;
|
|
1832
1881
|
const lightSurface = resolved.colors.surface;
|
|
1833
1882
|
const primary = resolved.colors.primary;
|
|
1834
1883
|
const lightBgRgb = colorToRgb(lightBg);
|
|
1835
1884
|
const lightSurfaceRgb = colorToRgb(lightSurface);
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
);
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
);
|
|
1885
|
+
const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
|
|
1886
|
+
const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
|
|
1887
|
+
const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
|
|
1888
|
+
const darkBgRgb = colorToRgb(darkBg);
|
|
1889
|
+
const darkSurfaceRgb = colorToRgb(darkSurface);
|
|
1890
|
+
const textTokensToCheck = [...STANDARD_TEXT_TOKENS, ...STATUS_TEXT_TOKENS];
|
|
1891
|
+
for (const tokenName of textTokensToCheck) {
|
|
1892
|
+
const tokenValue = tokens.text[tokenName];
|
|
1893
|
+
if (!tokenValue) continue;
|
|
1894
|
+
const fullName = `text-${tokenName}`;
|
|
1895
|
+
const lightOnBg = getContrastRatio(tokenValue.light, lightBg, lightBgRgb);
|
|
1896
|
+
if (lightOnBg < CONTRAST_TEXT_AA) {
|
|
1897
|
+
issues.push(
|
|
1898
|
+
issue(
|
|
1899
|
+
"warning",
|
|
1900
|
+
"WCAG_CONTRAST",
|
|
1901
|
+
`Light mode: ${fullName} on background has contrast ratio ${lightOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1902
|
+
"colors.background"
|
|
1903
|
+
)
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
const lightOnSurface = getContrastRatio(tokenValue.light, lightSurface, lightSurfaceRgb);
|
|
1907
|
+
if (lightOnSurface < CONTRAST_TEXT_AA) {
|
|
1908
|
+
issues.push(
|
|
1909
|
+
issue(
|
|
1910
|
+
"warning",
|
|
1911
|
+
"WCAG_CONTRAST",
|
|
1912
|
+
`Light mode: ${fullName} on surface has contrast ratio ${lightOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1913
|
+
"colors.surface"
|
|
1914
|
+
)
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
const darkOnBg = getContrastRatio(tokenValue.dark, darkBg, darkBgRgb);
|
|
1918
|
+
if (darkOnBg < CONTRAST_TEXT_AA) {
|
|
1919
|
+
issues.push(
|
|
1920
|
+
issue(
|
|
1921
|
+
"warning",
|
|
1922
|
+
"WCAG_CONTRAST",
|
|
1923
|
+
`Dark mode: ${fullName} on background has contrast ratio ${darkOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1924
|
+
"colors-dark.background"
|
|
1925
|
+
)
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
const darkOnSurface = getContrastRatio(tokenValue.dark, darkSurface, darkSurfaceRgb);
|
|
1929
|
+
if (darkOnSurface < CONTRAST_TEXT_AA) {
|
|
1930
|
+
issues.push(
|
|
1931
|
+
issue(
|
|
1932
|
+
"warning",
|
|
1933
|
+
"WCAG_CONTRAST",
|
|
1934
|
+
`Dark mode: ${fullName} on surface has contrast ratio ${darkOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1935
|
+
"colors-dark.surface"
|
|
1936
|
+
)
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1858
1939
|
}
|
|
1859
1940
|
const primaryOnBg = getContrastRatio(primary, lightBg, lightBgRgb);
|
|
1860
1941
|
if (primaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
@@ -1878,34 +1959,6 @@ function checkContrastWarnings(config, issues) {
|
|
|
1878
1959
|
)
|
|
1879
1960
|
);
|
|
1880
1961
|
}
|
|
1881
|
-
const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
|
|
1882
|
-
const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
|
|
1883
|
-
const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
|
|
1884
|
-
const darkBgRgb = colorToRgb(darkBg);
|
|
1885
|
-
const darkSurfaceRgb = colorToRgb(darkSurface);
|
|
1886
|
-
const textLight = "#f9fafb";
|
|
1887
|
-
const textOnDarkBg = getContrastRatio(textLight, darkBg, darkBgRgb);
|
|
1888
|
-
if (textOnDarkBg < CONTRAST_TEXT_AA) {
|
|
1889
|
-
issues.push(
|
|
1890
|
-
issue(
|
|
1891
|
-
"warning",
|
|
1892
|
-
"WCAG_CONTRAST",
|
|
1893
|
-
`Dark mode: text-primary on background has contrast ratio ${textOnDarkBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1894
|
-
"colors-dark.background"
|
|
1895
|
-
)
|
|
1896
|
-
);
|
|
1897
|
-
}
|
|
1898
|
-
const textOnDarkSurface = getContrastRatio(textLight, darkSurface, darkSurfaceRgb);
|
|
1899
|
-
if (textOnDarkSurface < CONTRAST_TEXT_AA) {
|
|
1900
|
-
issues.push(
|
|
1901
|
-
issue(
|
|
1902
|
-
"warning",
|
|
1903
|
-
"WCAG_CONTRAST",
|
|
1904
|
-
`Dark mode: text-primary on surface has contrast ratio ${textOnDarkSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1905
|
-
"colors-dark.surface"
|
|
1906
|
-
)
|
|
1907
|
-
);
|
|
1908
|
-
}
|
|
1909
1962
|
const darkPrimaryOnBg = getContrastRatio(darkPrimary, darkBg, darkBgRgb);
|
|
1910
1963
|
if (darkPrimaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
1911
1964
|
issues.push(
|
|
@@ -2575,6 +2628,7 @@ function extractFromCSS(files, name = "extracted-theme") {
|
|
|
2575
2628
|
return { config, tokens, unmapped, warnings };
|
|
2576
2629
|
}
|
|
2577
2630
|
export {
|
|
2631
|
+
FONT_WEIGHT_ALIASES,
|
|
2578
2632
|
SEMANTIC_MAP,
|
|
2579
2633
|
TAILWIND_GRAY,
|
|
2580
2634
|
VISOR_FONTS_CDN,
|
|
@@ -2606,6 +2660,7 @@ export {
|
|
|
2606
2660
|
isValidColor,
|
|
2607
2661
|
isValidHex,
|
|
2608
2662
|
isVisorThemeConfig,
|
|
2663
|
+
lookupFontWeightAlias,
|
|
2609
2664
|
lookupGoogleFont,
|
|
2610
2665
|
normalizeHex,
|
|
2611
2666
|
oklchToHex,
|
|
@@ -247,6 +247,21 @@ interface VisorThemeConfig {
|
|
|
247
247
|
light?: Record<string, string>;
|
|
248
248
|
dark?: Record<string, string>;
|
|
249
249
|
};
|
|
250
|
+
/**
|
|
251
|
+
* Migration metadata — optional, additive, consumed by `visor migrate` commands.
|
|
252
|
+
* Does not affect CSS generation or theme application.
|
|
253
|
+
*/
|
|
254
|
+
migrate?: {
|
|
255
|
+
/**
|
|
256
|
+
* §3.1 V7-primitive → Visor-semantic substitution table.
|
|
257
|
+
* Maps V7 CSS custom property names (without `var()`) to Visor semantic
|
|
258
|
+
* token names. Consumed by `visor migrate token-substitution`.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* { "--panel": "--surface-card", "--text": "--text-primary" }
|
|
262
|
+
*/
|
|
263
|
+
"token-substitution"?: Record<string, string>;
|
|
264
|
+
};
|
|
250
265
|
}
|
|
251
266
|
/** Config with all defaults resolved */
|
|
252
267
|
interface ResolvedThemeConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -299,6 +299,18 @@
|
|
|
299
299
|
"additionalProperties": { "type": "string" }
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
+
},
|
|
303
|
+
"migrate": {
|
|
304
|
+
"type": "object",
|
|
305
|
+
"description": "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
|
|
306
|
+
"additionalProperties": false,
|
|
307
|
+
"properties": {
|
|
308
|
+
"token-substitution": {
|
|
309
|
+
"type": "object",
|
|
310
|
+
"description": "§3.1 V7-primitive → Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
|
|
311
|
+
"additionalProperties": { "type": "string" }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
302
314
|
}
|
|
303
315
|
},
|
|
304
316
|
"$defs": {
|