@loworbitstudio/visor-theme-engine 0.14.0 → 0.15.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 +80 -25
- package/dist/{chunk-YDRQQIOB.js → chunk-DQ256PSE.js} +3 -0
- package/dist/index.d.ts +30 -5
- package/dist/index.js +213 -19
- package/dist/{types-BDRXkldG.d.ts → types-zug1_eLX.d.ts} +52 -0
- package/package.json +1 -1
- package/src/visor-theme.schema.json +16 -3
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as GeneratedPrimitives, m as SemanticTokens, R as ResolvedThemeConfig } from '../types-
|
|
1
|
+
import { g as GeneratedPrimitives, m as SemanticTokens, R as ResolvedThemeConfig } from '../types-zug1_eLX.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Adapter types for the Visor theme engine.
|
package/dist/adapters/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
MATERIAL_TEXT_SLOTS,
|
|
4
4
|
SELECTIVE_SHADE_STEPS,
|
|
5
5
|
aliasFamily,
|
|
6
|
+
block,
|
|
6
7
|
buildVisorFontUrl,
|
|
7
8
|
collectBrandPassthrough,
|
|
8
9
|
fontStack,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
resolveThemeBrand,
|
|
20
21
|
resolveThemeFonts,
|
|
21
22
|
sectionComment
|
|
22
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-DQ256PSE.js";
|
|
23
24
|
|
|
24
25
|
// src/adapters/brand-passthrough.ts
|
|
25
26
|
var SENTINEL_COLOR = "#ff00ff";
|
|
@@ -160,6 +161,51 @@ function nextjsAdapter(input, options) {
|
|
|
160
161
|
lines.push(wrapInLayer("visor-brand", passthroughCss));
|
|
161
162
|
lines.push("");
|
|
162
163
|
}
|
|
164
|
+
const hostSelector = scopePrefix ?? ":root";
|
|
165
|
+
const lightModeSelector = `html:not(.dark) ${scopePrefix ?? ""}`.trim();
|
|
166
|
+
const darkModeSelector = `html.dark ${scopePrefix ?? ""}`.trim();
|
|
167
|
+
const prefersSelector = scopePrefix ? `${scopePrefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])';
|
|
168
|
+
const semanticLines = [];
|
|
169
|
+
semanticLines.push("/* \u2500\u2500 Layer: Semantic aliases (VI-453) \u2500\u2500 */");
|
|
170
|
+
semanticLines.push(sectionComment("Discrete: Text size aliases (--text-N)"));
|
|
171
|
+
semanticLines.push(block(hostSelector, generateTextScaleAliasDecls()));
|
|
172
|
+
semanticLines.push("");
|
|
173
|
+
semanticLines.push(sectionComment("Discrete: Space aliases (--space-N)"));
|
|
174
|
+
semanticLines.push(block(hostSelector, generateSpaceAliasDecls(input.config)));
|
|
175
|
+
semanticLines.push("");
|
|
176
|
+
semanticLines.push(sectionComment("Intent aliases (light)"));
|
|
177
|
+
semanticLines.push(block(lightModeSelector, generateIntentDecls(input.tokens, "light")));
|
|
178
|
+
semanticLines.push("");
|
|
179
|
+
semanticLines.push(sectionComment("Hairline aliases (light)"));
|
|
180
|
+
semanticLines.push(block(lightModeSelector, generateHairlineDecls(input.tokens, "light")));
|
|
181
|
+
semanticLines.push("");
|
|
182
|
+
semanticLines.push(sectionComment("Intent aliases (dark) \u2014 manual toggle"));
|
|
183
|
+
semanticLines.push(block(darkModeSelector, generateIntentDecls(input.tokens, "dark")));
|
|
184
|
+
semanticLines.push("");
|
|
185
|
+
semanticLines.push(sectionComment("Hairline aliases (dark) \u2014 manual toggle"));
|
|
186
|
+
semanticLines.push(block(darkModeSelector, generateHairlineDecls(input.tokens, "dark")));
|
|
187
|
+
semanticLines.push("");
|
|
188
|
+
semanticLines.push(sectionComment("Intent aliases (dark) \u2014 prefers-color-scheme"));
|
|
189
|
+
{
|
|
190
|
+
const inner = block(prefersSelector, generateIntentDecls(input.tokens, "dark"));
|
|
191
|
+
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
192
|
+
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
193
|
+
}`);
|
|
194
|
+
}
|
|
195
|
+
semanticLines.push("");
|
|
196
|
+
semanticLines.push(sectionComment("Hairline aliases (dark) \u2014 prefers-color-scheme"));
|
|
197
|
+
{
|
|
198
|
+
const inner = block(prefersSelector, generateHairlineDecls(input.tokens, "dark"));
|
|
199
|
+
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
200
|
+
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
201
|
+
}`);
|
|
202
|
+
}
|
|
203
|
+
semanticLines.push("");
|
|
204
|
+
const semanticLayer = wrapInLayer("visor-semantic", semanticLines.join("\n").trim());
|
|
205
|
+
if (semanticLayer) {
|
|
206
|
+
lines.push(semanticLayer);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
163
209
|
const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
|
|
164
210
|
const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
|
|
165
211
|
lines.push(
|
|
@@ -415,7 +461,15 @@ function generateTypographyDecls(config, aliases) {
|
|
|
415
461
|
for (const [name, value] of Object.entries(lineHeights)) {
|
|
416
462
|
decls.push(`--line-height-${name}: ${value};`);
|
|
417
463
|
}
|
|
418
|
-
|
|
464
|
+
const ls = config.typography["letter-spacing"];
|
|
465
|
+
decls.push(`--letter-spacing-xl: ${ls.xl};`);
|
|
466
|
+
decls.push(`--letter-spacing-lg: ${ls.lg};`);
|
|
467
|
+
decls.push(`--letter-spacing-md: ${ls.md};`);
|
|
468
|
+
decls.push(`--letter-spacing-sm: ${ls.sm};`);
|
|
469
|
+
decls.push(`--letter-spacing-xs: ${ls.xs};`);
|
|
470
|
+
decls.push(`--letter-spacing-tight: ${ls.tight};`);
|
|
471
|
+
decls.push(`--letter-spacing-normal: ${ls.md};`);
|
|
472
|
+
decls.push(`--letter-spacing-wide: ${ls.lg};`);
|
|
419
473
|
return decls;
|
|
420
474
|
}
|
|
421
475
|
function generateShadowDecls(config) {
|
|
@@ -435,6 +489,7 @@ function generateMotionDecls(config) {
|
|
|
435
489
|
"--motion-duration-300: 300ms;",
|
|
436
490
|
`--motion-duration-500: ${config.motion["duration-slow"]};`,
|
|
437
491
|
"--motion-duration-800: 800ms;",
|
|
492
|
+
"--motion-duration-1500: 1500ms;",
|
|
438
493
|
"--motion-easing-linear: linear;",
|
|
439
494
|
"--motion-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);",
|
|
440
495
|
"--motion-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);",
|
|
@@ -489,7 +544,7 @@ function generateFumadocsBridgeDecls(tokens, mode) {
|
|
|
489
544
|
}
|
|
490
545
|
return decls;
|
|
491
546
|
}
|
|
492
|
-
function
|
|
547
|
+
function block2(selector, decls) {
|
|
493
548
|
return [`${selector} {`, ...decls.map((d) => ` ${d}`), "}"].join("\n");
|
|
494
549
|
}
|
|
495
550
|
function sectionComment2(label) {
|
|
@@ -551,29 +606,29 @@ function docsAdapter(input, options) {
|
|
|
551
606
|
"font-family: var(--font-sans);"
|
|
552
607
|
];
|
|
553
608
|
lines.push(sectionComment2("Primitive: Colors"));
|
|
554
|
-
lines.push(
|
|
609
|
+
lines.push(block2(scopeClass, [
|
|
555
610
|
...sharedDecls,
|
|
556
611
|
"",
|
|
557
612
|
...generateColorDecls(input.primitives)
|
|
558
613
|
]));
|
|
559
614
|
lines.push("");
|
|
560
615
|
lines.push(sectionComment2("Primitive: Spacing"));
|
|
561
|
-
lines.push(
|
|
616
|
+
lines.push(block2(scopeClass, generateSpacingDecls(input.config)));
|
|
562
617
|
lines.push("");
|
|
563
618
|
lines.push(sectionComment2("Primitive: Border Radius"));
|
|
564
|
-
lines.push(
|
|
619
|
+
lines.push(block2(scopeClass, generateRadiusDecls(input.config)));
|
|
565
620
|
lines.push("");
|
|
566
621
|
lines.push(sectionComment2("Primitive: Typography"));
|
|
567
|
-
lines.push(
|
|
622
|
+
lines.push(block2(scopeClass, generateTypographyDecls(input.config, aliasedFamilies)));
|
|
568
623
|
lines.push("");
|
|
569
624
|
lines.push(sectionComment2("Primitive: Shadows"));
|
|
570
|
-
lines.push(
|
|
625
|
+
lines.push(block2(scopeClass, generateShadowDecls(input.config)));
|
|
571
626
|
lines.push("");
|
|
572
627
|
lines.push(sectionComment2("Primitive: Motion"));
|
|
573
|
-
lines.push(
|
|
628
|
+
lines.push(block2(scopeClass, generateMotionDecls(input.config)));
|
|
574
629
|
lines.push("");
|
|
575
630
|
lines.push(sectionComment2("Primitive: Miscellaneous"));
|
|
576
|
-
lines.push(
|
|
631
|
+
lines.push(block2(scopeClass, generateMiscDecls()));
|
|
577
632
|
lines.push("");
|
|
578
633
|
lines.push("\n/* \u2500\u2500 Section 2: Dark mode overrides \u2500\u2500 */");
|
|
579
634
|
const darkDecls = generateSemanticDecls2(input.tokens, "dark");
|
|
@@ -597,7 +652,7 @@ function docsAdapter(input, options) {
|
|
|
597
652
|
}
|
|
598
653
|
if (darkPrimitiveOverrides.length > 0) {
|
|
599
654
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 dark brand color anchors at shade 500"));
|
|
600
|
-
lines.push(
|
|
655
|
+
lines.push(block2(`.dark ${scopeClass}`, darkPrimitiveOverrides));
|
|
601
656
|
lines.push("");
|
|
602
657
|
}
|
|
603
658
|
const categories = ["Text", "Surface", "Border", "Interactive"];
|
|
@@ -609,7 +664,7 @@ function docsAdapter(input, options) {
|
|
|
609
664
|
];
|
|
610
665
|
for (let i = 0; i < categories.length; i++) {
|
|
611
666
|
lines.push(sectionComment2(`Adaptive: ${categories[i]} (dark) \u2014 manual toggle`));
|
|
612
|
-
lines.push(
|
|
667
|
+
lines.push(block2(`.dark ${scopeClass}`, categoryDecls[i]));
|
|
613
668
|
lines.push("");
|
|
614
669
|
}
|
|
615
670
|
const pcsCategories = [
|
|
@@ -620,7 +675,7 @@ function docsAdapter(input, options) {
|
|
|
620
675
|
];
|
|
621
676
|
for (const cat of pcsCategories) {
|
|
622
677
|
lines.push(sectionComment2(`Adaptive: ${cat.label} (dark) \u2014 prefers-color-scheme`));
|
|
623
|
-
const inner =
|
|
678
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, cat.entries);
|
|
624
679
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
625
680
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
626
681
|
}`);
|
|
@@ -628,7 +683,7 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
628
683
|
}
|
|
629
684
|
if (darkPrimitiveOverrides.length > 0) {
|
|
630
685
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 prefers-color-scheme"));
|
|
631
|
-
const inner =
|
|
686
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, darkPrimitiveOverrides);
|
|
632
687
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
633
688
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
634
689
|
}`);
|
|
@@ -643,38 +698,38 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
643
698
|
];
|
|
644
699
|
for (const cat of lightCategoryDecls) {
|
|
645
700
|
lines.push(sectionComment2(`Adaptive: ${cat.label} (light)`));
|
|
646
|
-
lines.push(
|
|
701
|
+
lines.push(block2(`html:not(.dark) ${scopeClass}`, cat.entries));
|
|
647
702
|
lines.push("");
|
|
648
703
|
}
|
|
649
704
|
lines.push(sectionComment2("Fumadocs bridge: dark"));
|
|
650
|
-
lines.push(
|
|
705
|
+
lines.push(block2(`.dark ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "dark")));
|
|
651
706
|
lines.push("");
|
|
652
707
|
lines.push(sectionComment2("Fumadocs bridge: light"));
|
|
653
|
-
lines.push(
|
|
708
|
+
lines.push(block2(`html:not(.dark) ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "light")));
|
|
654
709
|
lines.push("");
|
|
655
710
|
const semanticLines = [];
|
|
656
711
|
semanticLines.push("\n/* \u2500\u2500 Layer: Semantic aliases (VI-451) \u2500\u2500 */");
|
|
657
712
|
semanticLines.push(sectionComment2("Discrete: Text size aliases (--text-N)"));
|
|
658
|
-
semanticLines.push(
|
|
713
|
+
semanticLines.push(block2(scopeClass, generateTextScaleAliasDecls()));
|
|
659
714
|
semanticLines.push("");
|
|
660
715
|
semanticLines.push(sectionComment2("Discrete: Space aliases (--space-N)"));
|
|
661
|
-
semanticLines.push(
|
|
716
|
+
semanticLines.push(block2(scopeClass, generateSpaceAliasDecls(input.config)));
|
|
662
717
|
semanticLines.push("");
|
|
663
718
|
semanticLines.push(sectionComment2("Intent aliases (light)"));
|
|
664
|
-
semanticLines.push(
|
|
719
|
+
semanticLines.push(block2(`html:not(.dark) ${scopeClass}`, generateIntentDecls(input.tokens, "light")));
|
|
665
720
|
semanticLines.push("");
|
|
666
721
|
semanticLines.push(sectionComment2("Hairline aliases (light)"));
|
|
667
|
-
semanticLines.push(
|
|
722
|
+
semanticLines.push(block2(`html:not(.dark) ${scopeClass}`, generateHairlineDecls(input.tokens, "light")));
|
|
668
723
|
semanticLines.push("");
|
|
669
724
|
semanticLines.push(sectionComment2("Intent aliases (dark) \u2014 manual toggle"));
|
|
670
|
-
semanticLines.push(
|
|
725
|
+
semanticLines.push(block2(`.dark ${scopeClass}`, generateIntentDecls(input.tokens, "dark")));
|
|
671
726
|
semanticLines.push("");
|
|
672
727
|
semanticLines.push(sectionComment2("Hairline aliases (dark) \u2014 manual toggle"));
|
|
673
|
-
semanticLines.push(
|
|
728
|
+
semanticLines.push(block2(`.dark ${scopeClass}`, generateHairlineDecls(input.tokens, "dark")));
|
|
674
729
|
semanticLines.push("");
|
|
675
730
|
semanticLines.push(sectionComment2("Intent aliases (dark) \u2014 prefers-color-scheme"));
|
|
676
731
|
{
|
|
677
|
-
const inner =
|
|
732
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateIntentDecls(input.tokens, "dark"));
|
|
678
733
|
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
679
734
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
680
735
|
}`);
|
|
@@ -682,7 +737,7 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
682
737
|
semanticLines.push("");
|
|
683
738
|
semanticLines.push(sectionComment2("Hairline aliases (dark) \u2014 prefers-color-scheme"));
|
|
684
739
|
{
|
|
685
|
-
const inner =
|
|
740
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateHairlineDecls(input.tokens, "dark"));
|
|
686
741
|
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
687
742
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
688
743
|
}`);
|
|
@@ -1576,6 +1576,7 @@ function generateMotionPrimitives(config) {
|
|
|
1576
1576
|
decls.push("--motion-duration-300: 300ms;");
|
|
1577
1577
|
decls.push(`--motion-duration-500: ${config.motion["duration-slow"]};`);
|
|
1578
1578
|
decls.push("--motion-duration-800: 800ms;");
|
|
1579
|
+
decls.push("--motion-duration-1500: 1500ms;");
|
|
1579
1580
|
decls.push("--motion-easing-linear: linear;");
|
|
1580
1581
|
decls.push("--motion-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);");
|
|
1581
1582
|
decls.push("--motion-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);");
|
|
@@ -1893,6 +1894,7 @@ export {
|
|
|
1893
1894
|
hexToOklch,
|
|
1894
1895
|
clampToSrgb,
|
|
1895
1896
|
oklchToHex,
|
|
1897
|
+
getLuminance,
|
|
1896
1898
|
getContrastRatio,
|
|
1897
1899
|
parseHex,
|
|
1898
1900
|
parseRgba,
|
|
@@ -1914,6 +1916,7 @@ export {
|
|
|
1914
1916
|
fontStack,
|
|
1915
1917
|
header,
|
|
1916
1918
|
sectionComment,
|
|
1919
|
+
block2 as block,
|
|
1917
1920
|
generatePrimitivesCss,
|
|
1918
1921
|
generateSemanticCss,
|
|
1919
1922
|
generateTextScaleAliasDecls,
|
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, c as VisorBrand, B as BrandSlot, d as BrandSource, e as BrandResolution, f as ThemeBrandResult, R as ResolvedThemeConfig, g as GeneratedPrimitives, h as ThemeOutput, i as ThemeData, j as VisorThemeConfig, k as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, l as RGB, P as ParsedColor, O as OKLCH, m as SemanticTokens, n as ShadeStep } from './types-
|
|
2
|
-
export { o as BRAND_VARIANTS, p as BrandVariant, q as ColorFormat, r as FontSource, s as RGBA, t as SemanticTokenValue } from './types-
|
|
1
|
+
import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, c as VisorBrand, B as BrandSlot, d as BrandSource, e as BrandResolution, f as ThemeBrandResult, R as ResolvedThemeConfig, g as GeneratedPrimitives, h as ThemeOutput, i as ThemeData, j as VisorThemeConfig, k as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, l as RGB, P as ParsedColor, O as OKLCH, m as SemanticTokens, n as ShadeStep } from './types-zug1_eLX.js';
|
|
2
|
+
export { o as BRAND_VARIANTS, p as BrandVariant, q as ColorFormat, r as FontSource, s as RGBA, t as SemanticTokenValue } from './types-zug1_eLX.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Font resolver — maps font family names to loadable font resources.
|
|
@@ -514,19 +514,44 @@ var properties = {
|
|
|
514
514
|
type: "number",
|
|
515
515
|
description: "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
516
516
|
},
|
|
517
|
+
"text-on-light": {
|
|
518
|
+
type: "string",
|
|
519
|
+
description: "VI-375: text color placed on a LIGHT interactive background. Auto-picked for interactive *-text tokens whose paired *-bg has relative luminance > 0.5. Defaults to #FFFFFF (which deliberately still fails AA on a bright bg so the contrast validator flags it); set a readable near-black (e.g. #1E1F21) to clear the warning. Per-token overrides (overrides.{mode}.interactive-*-text) still win."
|
|
520
|
+
},
|
|
521
|
+
"text-on-dark": {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "VI-375: text color placed on a DARK interactive background. Auto-picked for interactive *-text tokens whose paired *-bg has relative luminance <= 0.5. Defaults to #FFFFFF. Per-token overrides still win."
|
|
524
|
+
},
|
|
517
525
|
"letter-spacing": {
|
|
518
526
|
type: "object",
|
|
519
|
-
description: "
|
|
527
|
+
description: "6-tier letter-spacing ramp (xl | lg | md | sm | xs | tight). Each value is a CSS length (e.g. \"0.1em\", \"0.44rem\", \"-0.01em\"). The legacy triad keys normal and wide remain valid for back-compat and map onto the ramp (normal->md, wide->lg, tight->tight).",
|
|
520
528
|
additionalProperties: false,
|
|
521
529
|
properties: {
|
|
530
|
+
xl: {
|
|
531
|
+
type: "string"
|
|
532
|
+
},
|
|
533
|
+
lg: {
|
|
534
|
+
type: "string"
|
|
535
|
+
},
|
|
536
|
+
md: {
|
|
537
|
+
type: "string"
|
|
538
|
+
},
|
|
539
|
+
sm: {
|
|
540
|
+
type: "string"
|
|
541
|
+
},
|
|
542
|
+
xs: {
|
|
543
|
+
type: "string"
|
|
544
|
+
},
|
|
522
545
|
tight: {
|
|
523
546
|
type: "string"
|
|
524
547
|
},
|
|
525
548
|
normal: {
|
|
526
|
-
type: "string"
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Deprecated legacy alias — maps to md."
|
|
527
551
|
},
|
|
528
552
|
wide: {
|
|
529
|
-
type: "string"
|
|
553
|
+
type: "string",
|
|
554
|
+
description: "Deprecated legacy alias — maps to lg."
|
|
530
555
|
}
|
|
531
556
|
}
|
|
532
557
|
},
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
generateShadeScale,
|
|
23
23
|
generateStylesheetLinks,
|
|
24
24
|
getContrastRatio,
|
|
25
|
+
getLuminance,
|
|
25
26
|
googleFontsCatalog,
|
|
26
27
|
hasBrandPassthrough,
|
|
27
28
|
hexToOklch,
|
|
@@ -45,7 +46,7 @@ import {
|
|
|
45
46
|
rgbToHex,
|
|
46
47
|
rgbToOklch,
|
|
47
48
|
serializeColor
|
|
48
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-DQ256PSE.js";
|
|
49
50
|
|
|
50
51
|
// src/fonts/validate-coverage.ts
|
|
51
52
|
var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
|
|
@@ -391,14 +392,27 @@ var visor_theme_schema_default = {
|
|
|
391
392
|
type: "number",
|
|
392
393
|
description: "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
393
394
|
},
|
|
395
|
+
"text-on-light": {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "VI-375: text color placed on a LIGHT interactive background. Auto-picked for interactive *-text tokens whose paired *-bg has relative luminance > 0.5. Defaults to #FFFFFF (which deliberately still fails AA on a bright bg so the contrast validator flags it); set a readable near-black (e.g. #1E1F21) to clear the warning. Per-token overrides (overrides.{mode}.interactive-*-text) still win."
|
|
398
|
+
},
|
|
399
|
+
"text-on-dark": {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "VI-375: text color placed on a DARK interactive background. Auto-picked for interactive *-text tokens whose paired *-bg has relative luminance <= 0.5. Defaults to #FFFFFF. Per-token overrides still win."
|
|
402
|
+
},
|
|
394
403
|
"letter-spacing": {
|
|
395
404
|
type: "object",
|
|
396
|
-
description:
|
|
405
|
+
description: '6-tier letter-spacing ramp (xl | lg | md | sm | xs | tight). Each value is a CSS length (e.g. "0.1em", "0.44rem", "-0.01em"). The legacy triad keys normal and wide remain valid for back-compat and map onto the ramp (normal->md, wide->lg, tight->tight).',
|
|
397
406
|
additionalProperties: false,
|
|
398
407
|
properties: {
|
|
408
|
+
xl: { type: "string" },
|
|
409
|
+
lg: { type: "string" },
|
|
410
|
+
md: { type: "string" },
|
|
411
|
+
sm: { type: "string" },
|
|
412
|
+
xs: { type: "string" },
|
|
399
413
|
tight: { type: "string" },
|
|
400
|
-
normal: { type: "string" },
|
|
401
|
-
wide: { type: "string" }
|
|
414
|
+
normal: { type: "string", description: "Deprecated legacy alias \u2014 maps to md." },
|
|
415
|
+
wide: { type: "string", description: "Deprecated legacy alias \u2014 maps to lg." }
|
|
402
416
|
}
|
|
403
417
|
},
|
|
404
418
|
"cdn-overrides": {
|
|
@@ -695,12 +709,25 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
|
|
|
695
709
|
"letter-spacing",
|
|
696
710
|
"scale",
|
|
697
711
|
"slots",
|
|
698
|
-
"cdn-overrides"
|
|
712
|
+
"cdn-overrides",
|
|
713
|
+
// VI-375: default text colors for interactive backgrounds (auto-picked by
|
|
714
|
+
// paired `*-bg` luminance; theme-overridable).
|
|
715
|
+
"text-on-light",
|
|
716
|
+
"text-on-dark"
|
|
699
717
|
]);
|
|
700
718
|
var KNOWN_CDN_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["visor-fonts"]);
|
|
701
719
|
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
702
720
|
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
703
|
-
var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set([
|
|
721
|
+
var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set([
|
|
722
|
+
"xl",
|
|
723
|
+
"lg",
|
|
724
|
+
"md",
|
|
725
|
+
"sm",
|
|
726
|
+
"xs",
|
|
727
|
+
"tight",
|
|
728
|
+
"normal",
|
|
729
|
+
"wide"
|
|
730
|
+
]);
|
|
704
731
|
var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
|
|
705
732
|
var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
|
|
706
733
|
var KNOWN_SPACING_KEYS = /* @__PURE__ */ new Set(["base"]);
|
|
@@ -780,6 +807,12 @@ function checkUnknownKeys(obj, errors) {
|
|
|
780
807
|
}
|
|
781
808
|
}
|
|
782
809
|
}
|
|
810
|
+
for (const key of ["text-on-light", "text-on-dark"]) {
|
|
811
|
+
const value = typo[key];
|
|
812
|
+
if (value !== void 0 && (typeof value !== "string" || !isValidColor(value))) {
|
|
813
|
+
errors.push(`'typography.${key}' must be a valid CSS color (hex, rgba, hsla, or oklch)`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
783
816
|
if (typeof typo["cdn-overrides"] === "object" && typo["cdn-overrides"] !== null) {
|
|
784
817
|
for (const key of Object.keys(typo["cdn-overrides"])) {
|
|
785
818
|
if (!KNOWN_CDN_OVERRIDE_KEYS.has(key)) {
|
|
@@ -1103,9 +1136,29 @@ var DEFAULTS = {
|
|
|
1103
1136
|
},
|
|
1104
1137
|
typography: {
|
|
1105
1138
|
scale: 1,
|
|
1139
|
+
// VI-375: default text colors for interactive backgrounds. Auto-picked for
|
|
1140
|
+
// `*-text` tokens by the paired `*-bg` luminance. The defaults preserve the
|
|
1141
|
+
// historical white-on-button behavior (both default to white), so a theme
|
|
1142
|
+
// that only overrides a bright button bg STILL fails AA and raises a
|
|
1143
|
+
// WCAG_CONTRAST warning — the operator must opt into a readable on-light
|
|
1144
|
+
// color (e.g. ENTR's `text-on-light: "#1E1F21"`) to clear it. This keeps the
|
|
1145
|
+
// fix loud rather than silently flipping every theme's button text.
|
|
1146
|
+
"text-on-light": "#FFFFFF",
|
|
1147
|
+
"text-on-dark": "#FFFFFF",
|
|
1106
1148
|
heading: { family: DEFAULT_FONT_SANS, weight: 600 },
|
|
1107
1149
|
body: { family: DEFAULT_FONT_SANS, weight: 400 },
|
|
1108
|
-
mono: { family: DEFAULT_FONT_MONO }
|
|
1150
|
+
mono: { family: DEFAULT_FONT_MONO },
|
|
1151
|
+
// VI-447: default 6-tier letter-spacing ramp. em-based so it scales with
|
|
1152
|
+
// type size; `md` is anchored at the historical `--letter-spacing-normal`
|
|
1153
|
+
// value (0.05em) so existing themes emit byte-identical output.
|
|
1154
|
+
"letter-spacing": {
|
|
1155
|
+
xl: "0.16em",
|
|
1156
|
+
lg: "0.1em",
|
|
1157
|
+
md: "0.05em",
|
|
1158
|
+
sm: "0.025em",
|
|
1159
|
+
xs: "0.01em",
|
|
1160
|
+
tight: "-0.01em"
|
|
1161
|
+
}
|
|
1109
1162
|
},
|
|
1110
1163
|
spacing: { base: 4 },
|
|
1111
1164
|
radius: { sm: 2, md: 4, lg: 8, xl: 12, pill: 9999 },
|
|
@@ -1146,6 +1199,17 @@ function resolveBrand(brand) {
|
|
|
1146
1199
|
...brand.custom && { custom: brand.custom }
|
|
1147
1200
|
};
|
|
1148
1201
|
}
|
|
1202
|
+
function resolveLetterSpacing(ls) {
|
|
1203
|
+
const d = DEFAULTS.typography["letter-spacing"];
|
|
1204
|
+
return {
|
|
1205
|
+
xl: ls?.xl ?? d.xl,
|
|
1206
|
+
lg: ls?.lg ?? ls?.wide ?? d.lg,
|
|
1207
|
+
md: ls?.md ?? ls?.normal ?? d.md,
|
|
1208
|
+
sm: ls?.sm ?? d.sm,
|
|
1209
|
+
xs: ls?.xs ?? d.xs,
|
|
1210
|
+
tight: ls?.tight ?? d.tight
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1149
1213
|
function resolveConfig(config) {
|
|
1150
1214
|
const colors = config.colors;
|
|
1151
1215
|
const originalColors = {};
|
|
@@ -1194,6 +1258,8 @@ function resolveConfig(config) {
|
|
|
1194
1258
|
"colors-dark": config["colors-dark"],
|
|
1195
1259
|
typography: {
|
|
1196
1260
|
scale: config.typography?.scale ?? DEFAULTS.typography.scale,
|
|
1261
|
+
"text-on-light": config.typography?.["text-on-light"] ?? DEFAULTS.typography["text-on-light"],
|
|
1262
|
+
"text-on-dark": config.typography?.["text-on-dark"] ?? DEFAULTS.typography["text-on-dark"],
|
|
1197
1263
|
heading: {
|
|
1198
1264
|
family: config.typography?.heading?.family ?? DEFAULTS.typography.heading.family,
|
|
1199
1265
|
weight: config.typography?.heading?.weight ?? DEFAULTS.typography.heading.weight,
|
|
@@ -1225,7 +1291,8 @@ function resolveConfig(config) {
|
|
|
1225
1291
|
...config.typography?.["cdn-overrides"] && {
|
|
1226
1292
|
"cdn-overrides": config.typography["cdn-overrides"]
|
|
1227
1293
|
},
|
|
1228
|
-
slots: config.typography?.slots ?? {}
|
|
1294
|
+
slots: config.typography?.slots ?? {},
|
|
1295
|
+
"letter-spacing": resolveLetterSpacing(config.typography?.["letter-spacing"])
|
|
1229
1296
|
},
|
|
1230
1297
|
brand: resolveBrand(config.brand),
|
|
1231
1298
|
spacing: {
|
|
@@ -1274,6 +1341,10 @@ var CONFIG_BACKGROUND = "__CONFIG_BACKGROUND__";
|
|
|
1274
1341
|
var CONFIG_SURFACE = "__CONFIG_SURFACE__";
|
|
1275
1342
|
var CONFIG_DARK_BACKGROUND = "__CONFIG_DARK_BACKGROUND__";
|
|
1276
1343
|
var CONFIG_DARK_SURFACE = "__CONFIG_DARK_SURFACE__";
|
|
1344
|
+
var DERIVE_ON_BG_PREFIX = "__DERIVE_ON_BG__";
|
|
1345
|
+
function deriveOnBg(pairedBgToken) {
|
|
1346
|
+
return `${DERIVE_ON_BG_PREFIX}${pairedBgToken}`;
|
|
1347
|
+
}
|
|
1277
1348
|
var SEMANTIC_TEXT_MAP = {
|
|
1278
1349
|
primary: {
|
|
1279
1350
|
light: { role: "neutral", shade: 900 },
|
|
@@ -1529,9 +1600,13 @@ var SEMANTIC_INTERACTIVE_MAP = {
|
|
|
1529
1600
|
light: { role: "primary", shade: 800 },
|
|
1530
1601
|
dark: { role: "primary", shade: 300 }
|
|
1531
1602
|
},
|
|
1603
|
+
// VI-375: was a hardcoded #ffffff constant in both modes — a blind spot that
|
|
1604
|
+
// let a bright brand bg (ENTR dark mint) render white text at ~1.6:1. Now
|
|
1605
|
+
// derives from the paired `primary-bg` luminance: light bg → text-on-light,
|
|
1606
|
+
// dark bg → text-on-dark. Per-token override still wins.
|
|
1532
1607
|
"primary-text": {
|
|
1533
|
-
light: { constant: "
|
|
1534
|
-
dark: { constant: "
|
|
1608
|
+
light: { constant: deriveOnBg("primary-bg") },
|
|
1609
|
+
dark: { constant: deriveOnBg("primary-bg") }
|
|
1535
1610
|
},
|
|
1536
1611
|
// VI-478: brand-derived alpha-overlay helpers (BL-193). `soft`/`glow` are
|
|
1537
1612
|
// alpha overlays that track the theme's primary via color-mix (distinct from
|
|
@@ -1580,9 +1655,11 @@ var SEMANTIC_INTERACTIVE_MAP = {
|
|
|
1580
1655
|
light: { role: "error", shade: 700 },
|
|
1581
1656
|
dark: { role: "error", shade: 600 }
|
|
1582
1657
|
},
|
|
1658
|
+
// VI-375: derive from the paired `destructive-bg` luminance (was hardcoded
|
|
1659
|
+
// #ffffff). Per-token override still wins.
|
|
1583
1660
|
"destructive-text": {
|
|
1584
|
-
light: { constant: "
|
|
1585
|
-
dark: { constant: "
|
|
1661
|
+
light: { constant: deriveOnBg("destructive-bg") },
|
|
1662
|
+
dark: { constant: deriveOnBg("destructive-bg") }
|
|
1586
1663
|
},
|
|
1587
1664
|
// Ghost action
|
|
1588
1665
|
"ghost-bg": {
|
|
@@ -1640,6 +1717,16 @@ var SEMANTIC_HAIRLINE_MAP = {
|
|
|
1640
1717
|
dark: { constant: "rgba(255, 255, 255, 0.10)" }
|
|
1641
1718
|
}
|
|
1642
1719
|
};
|
|
1720
|
+
var INTERACTIVE_TEXT_BG_PAIRS = (() => {
|
|
1721
|
+
const pairs = {};
|
|
1722
|
+
for (const [name, mapping] of Object.entries(SEMANTIC_INTERACTIVE_MAP)) {
|
|
1723
|
+
const light = mapping.light;
|
|
1724
|
+
if ("constant" in light && light.constant.startsWith(DERIVE_ON_BG_PREFIX)) {
|
|
1725
|
+
pairs[name] = light.constant.slice(DERIVE_ON_BG_PREFIX.length);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return pairs;
|
|
1729
|
+
})();
|
|
1643
1730
|
var SEMANTIC_MAP = {
|
|
1644
1731
|
text: SEMANTIC_TEXT_MAP,
|
|
1645
1732
|
surface: SEMANTIC_SURFACE_MAP,
|
|
@@ -1677,6 +1764,66 @@ function resolveRef(ref, primitives, config) {
|
|
|
1677
1764
|
return ref.constant;
|
|
1678
1765
|
}
|
|
1679
1766
|
}
|
|
1767
|
+
function pickTextForBg(bgValue, config) {
|
|
1768
|
+
const parsed = parseColor(bgValue);
|
|
1769
|
+
const luminance = parsed ? getLuminance(...parsed.rgb) : 0;
|
|
1770
|
+
return luminance > 0.5 ? config.typography["text-on-light"] : config.typography["text-on-dark"];
|
|
1771
|
+
}
|
|
1772
|
+
function deriveInteractiveTextColors(interactive, config, opts = {}) {
|
|
1773
|
+
const skip = opts.skipTokens ?? /* @__PURE__ */ new Set();
|
|
1774
|
+
const modes = opts.modes ?? ["light", "dark"];
|
|
1775
|
+
for (const [textToken, bgToken] of Object.entries(INTERACTIVE_TEXT_BG_PAIRS)) {
|
|
1776
|
+
if (skip.has(textToken)) continue;
|
|
1777
|
+
const textValue = interactive[textToken];
|
|
1778
|
+
const bgValue = interactive[bgToken];
|
|
1779
|
+
if (!textValue || !bgValue) continue;
|
|
1780
|
+
const next = { ...textValue };
|
|
1781
|
+
for (const mode of modes) {
|
|
1782
|
+
next[mode] = pickTextForBg(bgValue[mode], config);
|
|
1783
|
+
}
|
|
1784
|
+
interactive[textToken] = next;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function reapplyInteractiveTextDerivation(tokens, config, overrides) {
|
|
1788
|
+
if (!overrides) return tokens;
|
|
1789
|
+
const skip = /* @__PURE__ */ new Set();
|
|
1790
|
+
const bgOverriddenModes = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const mode of ["light", "dark"]) {
|
|
1792
|
+
const modeOverrides = overrides[mode];
|
|
1793
|
+
if (!modeOverrides) continue;
|
|
1794
|
+
for (const key of Object.keys(modeOverrides)) {
|
|
1795
|
+
if (!key.startsWith("interactive-")) continue;
|
|
1796
|
+
const tokenName = key.slice("interactive-".length);
|
|
1797
|
+
if (tokenName in INTERACTIVE_TEXT_BG_PAIRS) {
|
|
1798
|
+
skip.add(tokenName);
|
|
1799
|
+
}
|
|
1800
|
+
for (const [textToken, bgToken] of Object.entries(INTERACTIVE_TEXT_BG_PAIRS)) {
|
|
1801
|
+
if (tokenName === bgToken) {
|
|
1802
|
+
if (!bgOverriddenModes.has(textToken)) {
|
|
1803
|
+
bgOverriddenModes.set(textToken, /* @__PURE__ */ new Set());
|
|
1804
|
+
}
|
|
1805
|
+
bgOverriddenModes.get(textToken).add(mode);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (bgOverriddenModes.size === 0) return tokens;
|
|
1811
|
+
const interactive = {};
|
|
1812
|
+
for (const [name, value] of Object.entries(tokens.interactive)) {
|
|
1813
|
+
interactive[name] = { ...value };
|
|
1814
|
+
}
|
|
1815
|
+
for (const [textToken, modes] of bgOverriddenModes) {
|
|
1816
|
+
if (skip.has(textToken)) continue;
|
|
1817
|
+
deriveInteractiveTextColors(interactive, config, {
|
|
1818
|
+
// Re-derive only this token by skipping the others.
|
|
1819
|
+
skipTokens: new Set(
|
|
1820
|
+
Object.keys(INTERACTIVE_TEXT_BG_PAIRS).filter((t) => t !== textToken)
|
|
1821
|
+
),
|
|
1822
|
+
modes: [...modes]
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
return { ...tokens, interactive };
|
|
1826
|
+
}
|
|
1680
1827
|
function resolveMapping(mapping, lightPrimitives, darkPrimitives, config) {
|
|
1681
1828
|
return {
|
|
1682
1829
|
light: resolveRef(mapping.light, lightPrimitives, config),
|
|
@@ -1702,6 +1849,7 @@ function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
|
1702
1849
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.interactive)) {
|
|
1703
1850
|
interactive[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
1704
1851
|
}
|
|
1852
|
+
deriveInteractiveTextColors(interactive, config);
|
|
1705
1853
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.intent)) {
|
|
1706
1854
|
intent[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
1707
1855
|
}
|
|
@@ -1770,6 +1918,7 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
1770
1918
|
const darkPrimitives = generateDarkPrimitives(resolved, primitives);
|
|
1771
1919
|
let tokens = assignSemanticTokens(primitives, darkPrimitives, resolved);
|
|
1772
1920
|
tokens = applyOverrides(tokens, resolved.overrides);
|
|
1921
|
+
tokens = reapplyInteractiveTextDerivation(tokens, resolved, resolved.overrides);
|
|
1773
1922
|
const output = {
|
|
1774
1923
|
primitivesCss: generatePrimitivesCss(primitives, resolved),
|
|
1775
1924
|
semanticCss: generateSemanticCss(tokens),
|
|
@@ -2193,7 +2342,7 @@ function checkTypeScaleCoherence(config, issues) {
|
|
|
2193
2342
|
function checkLetterSpacing(config, issues) {
|
|
2194
2343
|
const ls = config.typography?.["letter-spacing"];
|
|
2195
2344
|
if (!ls) return;
|
|
2196
|
-
for (const key of ["tight", "normal", "wide"]) {
|
|
2345
|
+
for (const key of ["xl", "lg", "md", "sm", "xs", "tight", "normal", "wide"]) {
|
|
2197
2346
|
const value = ls[key];
|
|
2198
2347
|
if (value !== void 0) {
|
|
2199
2348
|
if (typeof value !== "string" || !CSS_LENGTH_RE.test(value.trim())) {
|
|
@@ -2406,12 +2555,27 @@ function colorToRgb(color) {
|
|
|
2406
2555
|
}
|
|
2407
2556
|
var STANDARD_TEXT_TOKENS = ["primary", "secondary", "tertiary"];
|
|
2408
2557
|
var STATUS_TEXT_TOKENS = ["error", "warning", "success", "info"];
|
|
2558
|
+
var INTERACTIVE_PAIRS_TO_VALIDATE = (() => {
|
|
2559
|
+
const pairs = {};
|
|
2560
|
+
for (const name of Object.keys(SEMANTIC_INTERACTIVE_MAP)) {
|
|
2561
|
+
if (!name.endsWith("-text")) continue;
|
|
2562
|
+
const bgToken = `${name.slice(0, -"-text".length)}-bg`;
|
|
2563
|
+
if (bgToken in SEMANTIC_INTERACTIVE_MAP) {
|
|
2564
|
+
pairs[name] = bgToken;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return pairs;
|
|
2568
|
+
})();
|
|
2409
2569
|
function checkContrastWarnings(config, issues) {
|
|
2410
2570
|
const resolved = resolveConfig(config);
|
|
2411
2571
|
const lightPrimitives = generatePrimitives(resolved);
|
|
2412
2572
|
const darkPrimitives = generateDarkPrimitives(resolved, lightPrimitives);
|
|
2413
|
-
const tokens =
|
|
2414
|
-
|
|
2573
|
+
const tokens = reapplyInteractiveTextDerivation(
|
|
2574
|
+
applyOverrides(
|
|
2575
|
+
assignSemanticTokens(lightPrimitives, darkPrimitives, resolved),
|
|
2576
|
+
resolved.overrides
|
|
2577
|
+
),
|
|
2578
|
+
resolved,
|
|
2415
2579
|
resolved.overrides
|
|
2416
2580
|
);
|
|
2417
2581
|
const lightBg = resolved.colors.background;
|
|
@@ -2474,7 +2638,10 @@ function checkContrastWarnings(config, issues) {
|
|
|
2474
2638
|
);
|
|
2475
2639
|
}
|
|
2476
2640
|
}
|
|
2477
|
-
const
|
|
2641
|
+
const primaryBgToken = tokens.interactive["primary-bg"];
|
|
2642
|
+
const lightPrimaryBg = primaryBgToken?.light ?? primary;
|
|
2643
|
+
const darkPrimaryBg = primaryBgToken?.dark ?? darkPrimary;
|
|
2644
|
+
const primaryOnBg = getContrastRatio(lightPrimaryBg, lightBg, lightBgRgb);
|
|
2478
2645
|
if (primaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
2479
2646
|
issues.push(
|
|
2480
2647
|
issue(
|
|
@@ -2485,7 +2652,7 @@ function checkContrastWarnings(config, issues) {
|
|
|
2485
2652
|
)
|
|
2486
2653
|
);
|
|
2487
2654
|
}
|
|
2488
|
-
const primaryOnSurface = getContrastRatio(
|
|
2655
|
+
const primaryOnSurface = getContrastRatio(lightPrimaryBg, lightSurface, lightSurfaceRgb);
|
|
2489
2656
|
if (primaryOnSurface < CONTRAST_INTERACTIVE_AA) {
|
|
2490
2657
|
issues.push(
|
|
2491
2658
|
issue(
|
|
@@ -2496,7 +2663,7 @@ function checkContrastWarnings(config, issues) {
|
|
|
2496
2663
|
)
|
|
2497
2664
|
);
|
|
2498
2665
|
}
|
|
2499
|
-
const darkPrimaryOnBg = getContrastRatio(
|
|
2666
|
+
const darkPrimaryOnBg = getContrastRatio(darkPrimaryBg, darkBg, darkBgRgb);
|
|
2500
2667
|
if (darkPrimaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
2501
2668
|
issues.push(
|
|
2502
2669
|
issue(
|
|
@@ -2507,7 +2674,7 @@ function checkContrastWarnings(config, issues) {
|
|
|
2507
2674
|
)
|
|
2508
2675
|
);
|
|
2509
2676
|
}
|
|
2510
|
-
const darkPrimaryOnSurface = getContrastRatio(
|
|
2677
|
+
const darkPrimaryOnSurface = getContrastRatio(darkPrimaryBg, darkSurface, darkSurfaceRgb);
|
|
2511
2678
|
if (darkPrimaryOnSurface < CONTRAST_INTERACTIVE_AA) {
|
|
2512
2679
|
issues.push(
|
|
2513
2680
|
issue(
|
|
@@ -2518,6 +2685,33 @@ function checkContrastWarnings(config, issues) {
|
|
|
2518
2685
|
)
|
|
2519
2686
|
);
|
|
2520
2687
|
}
|
|
2688
|
+
for (const [textToken, bgToken] of Object.entries(INTERACTIVE_PAIRS_TO_VALIDATE)) {
|
|
2689
|
+
const text = tokens.interactive[textToken];
|
|
2690
|
+
const bg = tokens.interactive[bgToken];
|
|
2691
|
+
if (!text || !bg) continue;
|
|
2692
|
+
const lightRatio = getContrastRatio(text.light, bg.light, colorToRgb(bg.light));
|
|
2693
|
+
if (lightRatio < CONTRAST_TEXT_AA) {
|
|
2694
|
+
issues.push(
|
|
2695
|
+
issue(
|
|
2696
|
+
"warning",
|
|
2697
|
+
"WCAG_CONTRAST",
|
|
2698
|
+
`Light mode: interactive-${textToken} on interactive-${bgToken} has contrast ratio ${lightRatio.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
2699
|
+
`interactive.${textToken}`
|
|
2700
|
+
)
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
const darkRatio = getContrastRatio(text.dark, bg.dark, colorToRgb(bg.dark));
|
|
2704
|
+
if (darkRatio < CONTRAST_TEXT_AA) {
|
|
2705
|
+
issues.push(
|
|
2706
|
+
issue(
|
|
2707
|
+
"warning",
|
|
2708
|
+
"WCAG_CONTRAST",
|
|
2709
|
+
`Dark mode: interactive-${textToken} on interactive-${bgToken} has contrast ratio ${darkRatio.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
2710
|
+
`interactive.${textToken}`
|
|
2711
|
+
)
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2521
2715
|
}
|
|
2522
2716
|
function checkColorSimilarity(config, issues) {
|
|
2523
2717
|
const resolved = resolveConfig(config);
|
|
@@ -294,6 +294,22 @@ interface VisorThemeConfig {
|
|
|
294
294
|
};
|
|
295
295
|
typography?: {
|
|
296
296
|
scale?: number;
|
|
297
|
+
/**
|
|
298
|
+
* VI-375: text color placed on a LIGHT interactive background. Auto-picked
|
|
299
|
+
* for interactive `*-text` tokens whose paired `*-bg` resolves to a light
|
|
300
|
+
* color (relative luminance > 0.5). Defaults to `#FFFFFF` (preserving the
|
|
301
|
+
* historical white-on-button default, which deliberately still fails AA on
|
|
302
|
+
* a bright bg so the contrast validator flags it). Set a readable near-black
|
|
303
|
+
* (e.g. ENTR's `#1E1F21`) to clear the warning.
|
|
304
|
+
*/
|
|
305
|
+
"text-on-light"?: string;
|
|
306
|
+
/**
|
|
307
|
+
* VI-375: text color placed on a DARK interactive background. Auto-picked
|
|
308
|
+
* for interactive `*-text` tokens whose paired `*-bg` resolves to a dark
|
|
309
|
+
* color (relative luminance <= 0.5). Theme-overridable. Defaults to
|
|
310
|
+
* `#FFFFFF`.
|
|
311
|
+
*/
|
|
312
|
+
"text-on-dark"?: string;
|
|
297
313
|
heading?: {
|
|
298
314
|
family?: string;
|
|
299
315
|
weight?: number;
|
|
@@ -322,9 +338,22 @@ interface VisorThemeConfig {
|
|
|
322
338
|
source?: FontSource;
|
|
323
339
|
org?: string;
|
|
324
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* VI-447: 6-tier letter-spacing ramp (`xl | lg | md | sm | xs | tight`).
|
|
343
|
+
* Each value is a CSS length (e.g. `"0.1em"`, `"0.44rem"`, `"-0.01em"`).
|
|
344
|
+
* The legacy triad keys (`normal`, `wide`) stay valid for back-compat and
|
|
345
|
+
* map onto the ramp at resolve time (`normal`→md, `wide`→lg, `tight`→tight).
|
|
346
|
+
*/
|
|
325
347
|
"letter-spacing"?: {
|
|
348
|
+
xl?: string;
|
|
349
|
+
lg?: string;
|
|
350
|
+
md?: string;
|
|
351
|
+
sm?: string;
|
|
352
|
+
xs?: string;
|
|
326
353
|
tight?: string;
|
|
354
|
+
/** @deprecated legacy alias — maps to `md`. */
|
|
327
355
|
normal?: string;
|
|
356
|
+
/** @deprecated legacy alias — maps to `lg`. */
|
|
328
357
|
wide?: string;
|
|
329
358
|
};
|
|
330
359
|
/**
|
|
@@ -429,6 +458,16 @@ interface ResolvedThemeConfig {
|
|
|
429
458
|
"colors-dark"?: VisorThemeConfig["colors-dark"];
|
|
430
459
|
typography: {
|
|
431
460
|
scale: number;
|
|
461
|
+
/**
|
|
462
|
+
* VI-375: resolved default text color for LIGHT interactive backgrounds.
|
|
463
|
+
* Always present (defaults to `#1E1F21`); theme-overridable.
|
|
464
|
+
*/
|
|
465
|
+
"text-on-light": string;
|
|
466
|
+
/**
|
|
467
|
+
* VI-375: resolved default text color for DARK interactive backgrounds.
|
|
468
|
+
* Always present (defaults to `#FFFFFF`); theme-overridable.
|
|
469
|
+
*/
|
|
470
|
+
"text-on-dark": string;
|
|
432
471
|
heading: {
|
|
433
472
|
family: string;
|
|
434
473
|
weight: number;
|
|
@@ -471,6 +510,19 @@ interface ResolvedThemeConfig {
|
|
|
471
510
|
* consumes these; other adapters may ignore them.
|
|
472
511
|
*/
|
|
473
512
|
slots: Partial<Record<MaterialTextSlot, TextSlotOverride>>;
|
|
513
|
+
/**
|
|
514
|
+
* VI-447: resolved 6-tier letter-spacing ramp. Always present — legacy
|
|
515
|
+
* triad input is folded onto the ramp and missing tiers fall back to the
|
|
516
|
+
* Visor default ramp, so every tier carries a CSS length.
|
|
517
|
+
*/
|
|
518
|
+
"letter-spacing": {
|
|
519
|
+
xl: string;
|
|
520
|
+
lg: string;
|
|
521
|
+
md: string;
|
|
522
|
+
sm: string;
|
|
523
|
+
xs: string;
|
|
524
|
+
tight: string;
|
|
525
|
+
};
|
|
474
526
|
};
|
|
475
527
|
/**
|
|
476
528
|
* Resolved brand block (VI-470). Always present — falls back to the Visor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -184,14 +184,27 @@
|
|
|
184
184
|
"type": "number",
|
|
185
185
|
"description": "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
186
186
|
},
|
|
187
|
+
"text-on-light": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "VI-375: text color placed on a LIGHT interactive background. Auto-picked for interactive *-text tokens whose paired *-bg has relative luminance > 0.5. Defaults to #FFFFFF (which deliberately still fails AA on a bright bg so the contrast validator flags it); set a readable near-black (e.g. #1E1F21) to clear the warning. Per-token overrides (overrides.{mode}.interactive-*-text) still win."
|
|
190
|
+
},
|
|
191
|
+
"text-on-dark": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "VI-375: text color placed on a DARK interactive background. Auto-picked for interactive *-text tokens whose paired *-bg has relative luminance <= 0.5. Defaults to #FFFFFF. Per-token overrides still win."
|
|
194
|
+
},
|
|
187
195
|
"letter-spacing": {
|
|
188
196
|
"type": "object",
|
|
189
|
-
"description": "
|
|
197
|
+
"description": "6-tier letter-spacing ramp (xl | lg | md | sm | xs | tight). Each value is a CSS length (e.g. \"0.1em\", \"0.44rem\", \"-0.01em\"). The legacy triad keys normal and wide remain valid for back-compat and map onto the ramp (normal->md, wide->lg, tight->tight).",
|
|
190
198
|
"additionalProperties": false,
|
|
191
199
|
"properties": {
|
|
200
|
+
"xl": { "type": "string" },
|
|
201
|
+
"lg": { "type": "string" },
|
|
202
|
+
"md": { "type": "string" },
|
|
203
|
+
"sm": { "type": "string" },
|
|
204
|
+
"xs": { "type": "string" },
|
|
192
205
|
"tight": { "type": "string" },
|
|
193
|
-
"normal": { "type": "string" },
|
|
194
|
-
"wide": { "type": "string" }
|
|
206
|
+
"normal": { "type": "string", "description": "Deprecated legacy alias — maps to md." },
|
|
207
|
+
"wide": { "type": "string", "description": "Deprecated legacy alias — maps to lg." }
|
|
195
208
|
}
|
|
196
209
|
},
|
|
197
210
|
"cdn-overrides": {
|