@loworbitstudio/visor-theme-engine 0.13.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 +148 -26
- package/dist/{chunk-KFTTL3XP.js → chunk-DQ256PSE.js} +91 -0
- package/dist/index.d.ts +84 -7
- package/dist/index.js +248 -94
- package/dist/{types-CSO2avFQ.d.ts → types-zug1_eLX.d.ts} +54 -0
- package/package.json +1 -1
- package/src/visor-theme.schema.json +25 -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,7 +3,9 @@ import {
|
|
|
3
3
|
MATERIAL_TEXT_SLOTS,
|
|
4
4
|
SELECTIVE_SHADE_STEPS,
|
|
5
5
|
aliasFamily,
|
|
6
|
+
block,
|
|
6
7
|
buildVisorFontUrl,
|
|
8
|
+
collectBrandPassthrough,
|
|
7
9
|
fontStack,
|
|
8
10
|
generateDarkCss,
|
|
9
11
|
generateHairlineDecls,
|
|
@@ -18,7 +20,53 @@ import {
|
|
|
18
20
|
resolveThemeBrand,
|
|
19
21
|
resolveThemeFonts,
|
|
20
22
|
sectionComment
|
|
21
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-DQ256PSE.js";
|
|
24
|
+
|
|
25
|
+
// src/adapters/brand-passthrough.ts
|
|
26
|
+
var SENTINEL_COLOR = "#ff00ff";
|
|
27
|
+
function isDevBuild() {
|
|
28
|
+
return process.env.NODE_ENV !== "production";
|
|
29
|
+
}
|
|
30
|
+
function isUnresolved(value) {
|
|
31
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
32
|
+
}
|
|
33
|
+
function declFor(key, value) {
|
|
34
|
+
if (isUnresolved(value)) {
|
|
35
|
+
if (isDevBuild()) {
|
|
36
|
+
return `--${key}: ${SENTINEL_COLOR}; /* [visor-brand] UNRESOLVED pass-through value */`;
|
|
37
|
+
}
|
|
38
|
+
return `--${key}: ${value};`;
|
|
39
|
+
}
|
|
40
|
+
return `--${key}: ${value};`;
|
|
41
|
+
}
|
|
42
|
+
function indentBlock(selector, decls) {
|
|
43
|
+
if (decls.length === 0) return "";
|
|
44
|
+
return [selector + " {", ...decls.map((d) => ` ${d}`), "}"].join("\n");
|
|
45
|
+
}
|
|
46
|
+
function generateBrandPassthroughCss(passthrough, selectors) {
|
|
47
|
+
const lightKeys = Object.keys(passthrough.light);
|
|
48
|
+
const darkKeys = Object.keys(passthrough.dark);
|
|
49
|
+
if (lightKeys.length === 0 && darkKeys.length === 0) return "";
|
|
50
|
+
const blocks = [];
|
|
51
|
+
if (isDevBuild()) {
|
|
52
|
+
const names = [.../* @__PURE__ */ new Set([...lightKeys, ...darkKeys])].map((k) => `--${k}`).join(", ");
|
|
53
|
+
const count = lightKeys.length + darkKeys.length;
|
|
54
|
+
blocks.push(`/* [visor-brand] ${count} passthrough: ${names} */`);
|
|
55
|
+
}
|
|
56
|
+
if (lightKeys.length > 0) {
|
|
57
|
+
const decls = lightKeys.map((k) => declFor(k, passthrough.light[k]));
|
|
58
|
+
blocks.push(indentBlock(selectors.light, decls));
|
|
59
|
+
}
|
|
60
|
+
if (darkKeys.length > 0) {
|
|
61
|
+
const decls = darkKeys.map((k) => declFor(k, passthrough.dark[k]));
|
|
62
|
+
blocks.push(indentBlock(selectors.dark, decls));
|
|
63
|
+
const prefersInner = indentBlock(selectors.prefers, decls).split("\n").map((l) => ` ${l}`).join("\n");
|
|
64
|
+
blocks.push(`@media (prefers-color-scheme: dark) {
|
|
65
|
+
${prefersInner}
|
|
66
|
+
}`);
|
|
67
|
+
}
|
|
68
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
69
|
+
}
|
|
22
70
|
|
|
23
71
|
// src/adapters/layers.ts
|
|
24
72
|
var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;";
|
|
@@ -102,6 +150,62 @@ function nextjsAdapter(input, options) {
|
|
|
102
150
|
);
|
|
103
151
|
lines.push(wrapInLayer("visor-primitives", primitivesBody));
|
|
104
152
|
lines.push("");
|
|
153
|
+
const passthrough = collectBrandPassthrough(input.tokens, input.config.overrides);
|
|
154
|
+
const darkSelectors = scopePrefix ? [`${scopePrefix}.dark`, `${scopePrefix}.theme-dark`, `${scopePrefix}[data-theme="dark"]`] : [".dark", ".theme-dark", '[data-theme="dark"]'];
|
|
155
|
+
const passthroughCss = generateBrandPassthroughCss(passthrough, {
|
|
156
|
+
light: scopePrefix ?? ":root",
|
|
157
|
+
dark: darkSelectors.join(",\n"),
|
|
158
|
+
prefers: scopePrefix ? `${scopePrefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])'
|
|
159
|
+
});
|
|
160
|
+
if (passthroughCss) {
|
|
161
|
+
lines.push(wrapInLayer("visor-brand", passthroughCss));
|
|
162
|
+
lines.push("");
|
|
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
|
+
}
|
|
105
209
|
const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
|
|
106
210
|
const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
|
|
107
211
|
lines.push(
|
|
@@ -357,7 +461,15 @@ function generateTypographyDecls(config, aliases) {
|
|
|
357
461
|
for (const [name, value] of Object.entries(lineHeights)) {
|
|
358
462
|
decls.push(`--line-height-${name}: ${value};`);
|
|
359
463
|
}
|
|
360
|
-
|
|
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};`);
|
|
361
473
|
return decls;
|
|
362
474
|
}
|
|
363
475
|
function generateShadowDecls(config) {
|
|
@@ -377,6 +489,7 @@ function generateMotionDecls(config) {
|
|
|
377
489
|
"--motion-duration-300: 300ms;",
|
|
378
490
|
`--motion-duration-500: ${config.motion["duration-slow"]};`,
|
|
379
491
|
"--motion-duration-800: 800ms;",
|
|
492
|
+
"--motion-duration-1500: 1500ms;",
|
|
380
493
|
"--motion-easing-linear: linear;",
|
|
381
494
|
"--motion-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);",
|
|
382
495
|
"--motion-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);",
|
|
@@ -431,7 +544,7 @@ function generateFumadocsBridgeDecls(tokens, mode) {
|
|
|
431
544
|
}
|
|
432
545
|
return decls;
|
|
433
546
|
}
|
|
434
|
-
function
|
|
547
|
+
function block2(selector, decls) {
|
|
435
548
|
return [`${selector} {`, ...decls.map((d) => ` ${d}`), "}"].join("\n");
|
|
436
549
|
}
|
|
437
550
|
function sectionComment2(label) {
|
|
@@ -493,29 +606,29 @@ function docsAdapter(input, options) {
|
|
|
493
606
|
"font-family: var(--font-sans);"
|
|
494
607
|
];
|
|
495
608
|
lines.push(sectionComment2("Primitive: Colors"));
|
|
496
|
-
lines.push(
|
|
609
|
+
lines.push(block2(scopeClass, [
|
|
497
610
|
...sharedDecls,
|
|
498
611
|
"",
|
|
499
612
|
...generateColorDecls(input.primitives)
|
|
500
613
|
]));
|
|
501
614
|
lines.push("");
|
|
502
615
|
lines.push(sectionComment2("Primitive: Spacing"));
|
|
503
|
-
lines.push(
|
|
616
|
+
lines.push(block2(scopeClass, generateSpacingDecls(input.config)));
|
|
504
617
|
lines.push("");
|
|
505
618
|
lines.push(sectionComment2("Primitive: Border Radius"));
|
|
506
|
-
lines.push(
|
|
619
|
+
lines.push(block2(scopeClass, generateRadiusDecls(input.config)));
|
|
507
620
|
lines.push("");
|
|
508
621
|
lines.push(sectionComment2("Primitive: Typography"));
|
|
509
|
-
lines.push(
|
|
622
|
+
lines.push(block2(scopeClass, generateTypographyDecls(input.config, aliasedFamilies)));
|
|
510
623
|
lines.push("");
|
|
511
624
|
lines.push(sectionComment2("Primitive: Shadows"));
|
|
512
|
-
lines.push(
|
|
625
|
+
lines.push(block2(scopeClass, generateShadowDecls(input.config)));
|
|
513
626
|
lines.push("");
|
|
514
627
|
lines.push(sectionComment2("Primitive: Motion"));
|
|
515
|
-
lines.push(
|
|
628
|
+
lines.push(block2(scopeClass, generateMotionDecls(input.config)));
|
|
516
629
|
lines.push("");
|
|
517
630
|
lines.push(sectionComment2("Primitive: Miscellaneous"));
|
|
518
|
-
lines.push(
|
|
631
|
+
lines.push(block2(scopeClass, generateMiscDecls()));
|
|
519
632
|
lines.push("");
|
|
520
633
|
lines.push("\n/* \u2500\u2500 Section 2: Dark mode overrides \u2500\u2500 */");
|
|
521
634
|
const darkDecls = generateSemanticDecls2(input.tokens, "dark");
|
|
@@ -539,7 +652,7 @@ function docsAdapter(input, options) {
|
|
|
539
652
|
}
|
|
540
653
|
if (darkPrimitiveOverrides.length > 0) {
|
|
541
654
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 dark brand color anchors at shade 500"));
|
|
542
|
-
lines.push(
|
|
655
|
+
lines.push(block2(`.dark ${scopeClass}`, darkPrimitiveOverrides));
|
|
543
656
|
lines.push("");
|
|
544
657
|
}
|
|
545
658
|
const categories = ["Text", "Surface", "Border", "Interactive"];
|
|
@@ -551,7 +664,7 @@ function docsAdapter(input, options) {
|
|
|
551
664
|
];
|
|
552
665
|
for (let i = 0; i < categories.length; i++) {
|
|
553
666
|
lines.push(sectionComment2(`Adaptive: ${categories[i]} (dark) \u2014 manual toggle`));
|
|
554
|
-
lines.push(
|
|
667
|
+
lines.push(block2(`.dark ${scopeClass}`, categoryDecls[i]));
|
|
555
668
|
lines.push("");
|
|
556
669
|
}
|
|
557
670
|
const pcsCategories = [
|
|
@@ -562,7 +675,7 @@ function docsAdapter(input, options) {
|
|
|
562
675
|
];
|
|
563
676
|
for (const cat of pcsCategories) {
|
|
564
677
|
lines.push(sectionComment2(`Adaptive: ${cat.label} (dark) \u2014 prefers-color-scheme`));
|
|
565
|
-
const inner =
|
|
678
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, cat.entries);
|
|
566
679
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
567
680
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
568
681
|
}`);
|
|
@@ -570,7 +683,7 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
570
683
|
}
|
|
571
684
|
if (darkPrimitiveOverrides.length > 0) {
|
|
572
685
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 prefers-color-scheme"));
|
|
573
|
-
const inner =
|
|
686
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, darkPrimitiveOverrides);
|
|
574
687
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
575
688
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
576
689
|
}`);
|
|
@@ -585,38 +698,38 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
585
698
|
];
|
|
586
699
|
for (const cat of lightCategoryDecls) {
|
|
587
700
|
lines.push(sectionComment2(`Adaptive: ${cat.label} (light)`));
|
|
588
|
-
lines.push(
|
|
701
|
+
lines.push(block2(`html:not(.dark) ${scopeClass}`, cat.entries));
|
|
589
702
|
lines.push("");
|
|
590
703
|
}
|
|
591
704
|
lines.push(sectionComment2("Fumadocs bridge: dark"));
|
|
592
|
-
lines.push(
|
|
705
|
+
lines.push(block2(`.dark ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "dark")));
|
|
593
706
|
lines.push("");
|
|
594
707
|
lines.push(sectionComment2("Fumadocs bridge: light"));
|
|
595
|
-
lines.push(
|
|
708
|
+
lines.push(block2(`html:not(.dark) ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "light")));
|
|
596
709
|
lines.push("");
|
|
597
710
|
const semanticLines = [];
|
|
598
711
|
semanticLines.push("\n/* \u2500\u2500 Layer: Semantic aliases (VI-451) \u2500\u2500 */");
|
|
599
712
|
semanticLines.push(sectionComment2("Discrete: Text size aliases (--text-N)"));
|
|
600
|
-
semanticLines.push(
|
|
713
|
+
semanticLines.push(block2(scopeClass, generateTextScaleAliasDecls()));
|
|
601
714
|
semanticLines.push("");
|
|
602
715
|
semanticLines.push(sectionComment2("Discrete: Space aliases (--space-N)"));
|
|
603
|
-
semanticLines.push(
|
|
716
|
+
semanticLines.push(block2(scopeClass, generateSpaceAliasDecls(input.config)));
|
|
604
717
|
semanticLines.push("");
|
|
605
718
|
semanticLines.push(sectionComment2("Intent aliases (light)"));
|
|
606
|
-
semanticLines.push(
|
|
719
|
+
semanticLines.push(block2(`html:not(.dark) ${scopeClass}`, generateIntentDecls(input.tokens, "light")));
|
|
607
720
|
semanticLines.push("");
|
|
608
721
|
semanticLines.push(sectionComment2("Hairline aliases (light)"));
|
|
609
|
-
semanticLines.push(
|
|
722
|
+
semanticLines.push(block2(`html:not(.dark) ${scopeClass}`, generateHairlineDecls(input.tokens, "light")));
|
|
610
723
|
semanticLines.push("");
|
|
611
724
|
semanticLines.push(sectionComment2("Intent aliases (dark) \u2014 manual toggle"));
|
|
612
|
-
semanticLines.push(
|
|
725
|
+
semanticLines.push(block2(`.dark ${scopeClass}`, generateIntentDecls(input.tokens, "dark")));
|
|
613
726
|
semanticLines.push("");
|
|
614
727
|
semanticLines.push(sectionComment2("Hairline aliases (dark) \u2014 manual toggle"));
|
|
615
|
-
semanticLines.push(
|
|
728
|
+
semanticLines.push(block2(`.dark ${scopeClass}`, generateHairlineDecls(input.tokens, "dark")));
|
|
616
729
|
semanticLines.push("");
|
|
617
730
|
semanticLines.push(sectionComment2("Intent aliases (dark) \u2014 prefers-color-scheme"));
|
|
618
731
|
{
|
|
619
|
-
const inner =
|
|
732
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateIntentDecls(input.tokens, "dark"));
|
|
620
733
|
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
621
734
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
622
735
|
}`);
|
|
@@ -624,16 +737,25 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
624
737
|
semanticLines.push("");
|
|
625
738
|
semanticLines.push(sectionComment2("Hairline aliases (dark) \u2014 prefers-color-scheme"));
|
|
626
739
|
{
|
|
627
|
-
const inner =
|
|
740
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateHairlineDecls(input.tokens, "dark"));
|
|
628
741
|
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
629
742
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
630
743
|
}`);
|
|
631
744
|
}
|
|
632
745
|
semanticLines.push("");
|
|
633
746
|
const brandResult = resolveThemeBrand(input.config.brand, { scope: scopeClass });
|
|
747
|
+
const passthroughCss = generateBrandPassthroughCss(
|
|
748
|
+
collectBrandPassthrough(input.tokens, input.config.overrides),
|
|
749
|
+
{
|
|
750
|
+
light: `html:not(.dark) ${scopeClass}`,
|
|
751
|
+
dark: `.dark ${scopeClass}`,
|
|
752
|
+
prefers: `${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`
|
|
753
|
+
}
|
|
754
|
+
);
|
|
634
755
|
const adaptiveLayer = wrapInLayer("visor-adaptive", lines.join("\n").trim());
|
|
635
756
|
const semanticLayer = wrapInLayer("visor-semantic", semanticLines.join("\n").trim());
|
|
636
|
-
const
|
|
757
|
+
const brandLayerBody = [brandResult.css, passthroughCss].filter(Boolean).join("\n\n");
|
|
758
|
+
const brandLayer = wrapInLayer("visor-brand", brandLayerBody);
|
|
637
759
|
const head = fontLines.length > 0 ? fontLines.join("\n") + "\n" : "";
|
|
638
760
|
const layerBlocks = [semanticLayer, brandLayer, adaptiveLayer].filter(Boolean);
|
|
639
761
|
return head + LAYER_ORDER + "\n\n" + layerBlocks.join("\n\n") + "\n";
|
|
@@ -1347,6 +1347,91 @@ function generateShadeScale(color, role) {
|
|
|
1347
1347
|
return scale;
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
|
+
// src/overrides.ts
|
|
1351
|
+
var TOKEN_CATEGORIES = [
|
|
1352
|
+
{ prefix: "text-", key: "text" },
|
|
1353
|
+
{ prefix: "surface-", key: "surface" },
|
|
1354
|
+
{ prefix: "border-", key: "border" },
|
|
1355
|
+
{ prefix: "interactive-", key: "interactive" },
|
|
1356
|
+
{ prefix: "hairline-", key: "hairline" }
|
|
1357
|
+
];
|
|
1358
|
+
function findToken(key, tokens) {
|
|
1359
|
+
if (key === "hairline" && "default" in tokens.hairline) {
|
|
1360
|
+
return { group: tokens.hairline, name: "default" };
|
|
1361
|
+
}
|
|
1362
|
+
for (const { prefix, key: groupKey } of TOKEN_CATEGORIES) {
|
|
1363
|
+
if (key.startsWith(prefix)) {
|
|
1364
|
+
const name = key.slice(prefix.length);
|
|
1365
|
+
if (name in tokens[groupKey]) {
|
|
1366
|
+
return { group: tokens[groupKey], name };
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (key in tokens.intent) {
|
|
1371
|
+
return { group: tokens.intent, name: key };
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
function isRecognizedOverrideKey(key, tokens) {
|
|
1376
|
+
return findToken(key, tokens) !== null;
|
|
1377
|
+
}
|
|
1378
|
+
function collectBrandPassthrough(tokens, overrides) {
|
|
1379
|
+
const passthrough = { light: {}, dark: {} };
|
|
1380
|
+
if (!overrides) return passthrough;
|
|
1381
|
+
for (const mode of ["light", "dark"]) {
|
|
1382
|
+
const modeOverrides = overrides[mode];
|
|
1383
|
+
if (!modeOverrides) continue;
|
|
1384
|
+
for (const [key, value] of Object.entries(modeOverrides)) {
|
|
1385
|
+
if (!isRecognizedOverrideKey(key, tokens)) {
|
|
1386
|
+
passthrough[mode][key] = value;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return passthrough;
|
|
1391
|
+
}
|
|
1392
|
+
function hasBrandPassthrough(passthrough) {
|
|
1393
|
+
return Object.keys(passthrough.light).length > 0 || Object.keys(passthrough.dark).length > 0;
|
|
1394
|
+
}
|
|
1395
|
+
function applyOverrides(tokens, overrides) {
|
|
1396
|
+
if (!overrides) return tokens;
|
|
1397
|
+
const result = {
|
|
1398
|
+
text: { ...tokens.text },
|
|
1399
|
+
surface: { ...tokens.surface },
|
|
1400
|
+
border: { ...tokens.border },
|
|
1401
|
+
interactive: { ...tokens.interactive },
|
|
1402
|
+
intent: { ...tokens.intent },
|
|
1403
|
+
hairline: { ...tokens.hairline }
|
|
1404
|
+
};
|
|
1405
|
+
for (const group of ["text", "surface", "border", "interactive", "intent", "hairline"]) {
|
|
1406
|
+
for (const [name, value] of Object.entries(result[group])) {
|
|
1407
|
+
result[group][name] = { ...value };
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (overrides.light) {
|
|
1411
|
+
for (const [key, value] of Object.entries(overrides.light)) {
|
|
1412
|
+
const match = findToken(key, result);
|
|
1413
|
+
if (match) {
|
|
1414
|
+
match.group[match.name] = {
|
|
1415
|
+
...match.group[match.name],
|
|
1416
|
+
light: value
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if (overrides.dark) {
|
|
1422
|
+
for (const [key, value] of Object.entries(overrides.dark)) {
|
|
1423
|
+
const match = findToken(key, result);
|
|
1424
|
+
if (match) {
|
|
1425
|
+
match.group[match.name] = {
|
|
1426
|
+
...match.group[match.name],
|
|
1427
|
+
dark: value
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return result;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1350
1435
|
// src/fonts/theme-alias.ts
|
|
1351
1436
|
var EMPTY_ALIASES = /* @__PURE__ */ new Map();
|
|
1352
1437
|
function aliasFamily(family, themeSlug) {
|
|
@@ -1491,6 +1576,7 @@ function generateMotionPrimitives(config) {
|
|
|
1491
1576
|
decls.push("--motion-duration-300: 300ms;");
|
|
1492
1577
|
decls.push(`--motion-duration-500: ${config.motion["duration-slow"]};`);
|
|
1493
1578
|
decls.push("--motion-duration-800: 800ms;");
|
|
1579
|
+
decls.push("--motion-duration-1500: 1500ms;");
|
|
1494
1580
|
decls.push("--motion-easing-linear: linear;");
|
|
1495
1581
|
decls.push("--motion-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);");
|
|
1496
1582
|
decls.push("--motion-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);");
|
|
@@ -1808,6 +1894,7 @@ export {
|
|
|
1808
1894
|
hexToOklch,
|
|
1809
1895
|
clampToSrgb,
|
|
1810
1896
|
oklchToHex,
|
|
1897
|
+
getLuminance,
|
|
1811
1898
|
getContrastRatio,
|
|
1812
1899
|
parseHex,
|
|
1813
1900
|
parseRgba,
|
|
@@ -1822,10 +1909,14 @@ export {
|
|
|
1822
1909
|
SELECTIVE_SHADE_STEPS,
|
|
1823
1910
|
TAILWIND_GRAY,
|
|
1824
1911
|
generateShadeScale,
|
|
1912
|
+
collectBrandPassthrough,
|
|
1913
|
+
hasBrandPassthrough,
|
|
1914
|
+
applyOverrides,
|
|
1825
1915
|
aliasFamily,
|
|
1826
1916
|
fontStack,
|
|
1827
1917
|
header,
|
|
1828
1918
|
sectionComment,
|
|
1919
|
+
block2 as block,
|
|
1829
1920
|
generatePrimitivesCss,
|
|
1830
1921
|
generateSemanticCss,
|
|
1831
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.
|
|
@@ -315,6 +315,18 @@ var properties = {
|
|
|
315
315
|
type: "string",
|
|
316
316
|
description: "Theme group for the docs site theme switcher (e.g. 'Visor', 'Client', 'Low Orbit'). Used by `visor theme sync`. Defaults to folder-based grouping when omitted."
|
|
317
317
|
},
|
|
318
|
+
label: {
|
|
319
|
+
type: "string",
|
|
320
|
+
description: "Human-readable display name for the theme (e.g. 'Blacklight Pro'). Overrides the name-derived label in the docs theme switcher. Optional."
|
|
321
|
+
},
|
|
322
|
+
"default-mode": {
|
|
323
|
+
type: "string",
|
|
324
|
+
"enum": [
|
|
325
|
+
"light",
|
|
326
|
+
"dark"
|
|
327
|
+
],
|
|
328
|
+
description: "Default color mode when activating this theme. When set, the docs site forces this mode on theme switch (unless the user has a stored mode preference). Optional."
|
|
329
|
+
},
|
|
318
330
|
colors: {
|
|
319
331
|
type: "object",
|
|
320
332
|
description: "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
|
|
@@ -502,19 +514,44 @@ var properties = {
|
|
|
502
514
|
type: "number",
|
|
503
515
|
description: "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
504
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
|
+
},
|
|
505
525
|
"letter-spacing": {
|
|
506
526
|
type: "object",
|
|
507
|
-
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).",
|
|
508
528
|
additionalProperties: false,
|
|
509
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
|
+
},
|
|
510
545
|
tight: {
|
|
511
546
|
type: "string"
|
|
512
547
|
},
|
|
513
548
|
normal: {
|
|
514
|
-
type: "string"
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Deprecated legacy alias — maps to md."
|
|
515
551
|
},
|
|
516
552
|
wide: {
|
|
517
|
-
type: "string"
|
|
553
|
+
type: "string",
|
|
554
|
+
description: "Deprecated legacy alias — maps to lg."
|
|
518
555
|
}
|
|
519
556
|
}
|
|
520
557
|
},
|
|
@@ -946,6 +983,21 @@ interface ThemeValidationResult {
|
|
|
946
983
|
errors: ValidationIssue[];
|
|
947
984
|
warnings: ValidationIssue[];
|
|
948
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Options for the validate() function.
|
|
988
|
+
*/
|
|
989
|
+
interface ValidateOptions {
|
|
990
|
+
/**
|
|
991
|
+
* When true, promote DARK_LIGHT_PARITY warnings and the
|
|
992
|
+
* "colors.neutral present without colors-dark.neutral" check
|
|
993
|
+
* from warning to error. Use this in CI to enforce the
|
|
994
|
+
* "always both modes" authoring convention.
|
|
995
|
+
*
|
|
996
|
+
* Opt-in today; flip to the default after all convergent
|
|
997
|
+
* themes add their dark neutral (see VI-495 docs).
|
|
998
|
+
*/
|
|
999
|
+
strictDark?: boolean;
|
|
1000
|
+
}
|
|
949
1001
|
/**
|
|
950
1002
|
* Validate a theme config comprehensively.
|
|
951
1003
|
*
|
|
@@ -953,9 +1005,10 @@ interface ThemeValidationResult {
|
|
|
953
1005
|
* Results are JSON-serializable for CLI `--json` output.
|
|
954
1006
|
*
|
|
955
1007
|
* @param config - A parsed theme config object (from YAML or programmatic)
|
|
1008
|
+
* @param options - Optional validator flags (e.g. strictDark)
|
|
956
1009
|
* @returns ThemeValidationResult with errors[], warnings[], and valid boolean
|
|
957
1010
|
*/
|
|
958
|
-
declare function validate(config: unknown): ThemeValidationResult;
|
|
1011
|
+
declare function validate(config: unknown, options?: ValidateOptions): ThemeValidationResult;
|
|
959
1012
|
|
|
960
1013
|
/**
|
|
961
1014
|
* Shade Scale Generation
|
|
@@ -1062,6 +1115,30 @@ declare function assignSemanticTokens(lightPrimitives: GeneratedPrimitives, dark
|
|
|
1062
1115
|
* replacing derived token values with user-specified values.
|
|
1063
1116
|
*/
|
|
1064
1117
|
|
|
1118
|
+
/** Pass-through brand tokens collected per mode (VI-493). */
|
|
1119
|
+
interface BrandPassthrough {
|
|
1120
|
+
light: Record<string, string>;
|
|
1121
|
+
dark: Record<string, string>;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Collect unrecognized override keys into a brand-passthrough map (VI-493).
|
|
1125
|
+
*
|
|
1126
|
+
* Any `overrides.{light,dark}` key that does NOT map to a recognized semantic,
|
|
1127
|
+
* intent, or hairline token is captured here verbatim (key + value). These were
|
|
1128
|
+
* previously DROPPED silently by `applyOverrides`; the adapters now emit them as
|
|
1129
|
+
* bare `--<key>` custom properties inside `@layer visor-brand`, ending the
|
|
1130
|
+
* dual-source-of-truth between `.visor.yaml` and hand-maintained `:root` blocks.
|
|
1131
|
+
*
|
|
1132
|
+
* Recognized tokens are excluded — they continue to flow through the normal
|
|
1133
|
+
* semantic pipeline. Pass-through tokens are legitimately mode-asymmetric (a key
|
|
1134
|
+
* may appear in `light` only, `dark` only, or both); no both-modes rule applies.
|
|
1135
|
+
*/
|
|
1136
|
+
declare function collectBrandPassthrough(tokens: SemanticTokens, overrides?: {
|
|
1137
|
+
light?: Record<string, string>;
|
|
1138
|
+
dark?: Record<string, string>;
|
|
1139
|
+
}): BrandPassthrough;
|
|
1140
|
+
/** True when the passthrough map carries at least one token in either mode. */
|
|
1141
|
+
declare function hasBrandPassthrough(passthrough: BrandPassthrough): boolean;
|
|
1065
1142
|
/**
|
|
1066
1143
|
* Apply override values to semantic tokens.
|
|
1067
1144
|
* Returns a new SemanticTokens with overrides applied (does not mutate input).
|
|
@@ -1221,4 +1298,4 @@ declare function cleanFontValue(val: string): string;
|
|
|
1221
1298
|
*/
|
|
1222
1299
|
declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
|
|
1223
1300
|
|
|
1224
|
-
export { BrandResolution, BrandSlot, BrandSource, type CSSFile, ColorRole, type Confidence, DEFAULT_VISOR_BRAND, 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, ThemeBrandResult, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_BRANDS_CDN, VISOR_DEFAULT_BRAND_PATH, VISOR_FONTS_CDN, type ValidationIssue, type ValidationSeverity, VisorBrand, VisorThemeConfig, VisorTypography, applyOverrides, assignSemanticTokens, buildVisorBrandUrl, 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, resolveBrandSlot, resolveBrandSource, resolveConfig, resolveFont, resolveThemeBrand, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
|
|
1301
|
+
export { type BrandPassthrough, BrandResolution, BrandSlot, BrandSource, type CSSFile, ColorRole, type Confidence, DEFAULT_VISOR_BRAND, 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, ThemeBrandResult, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_BRANDS_CDN, VISOR_DEFAULT_BRAND_PATH, VISOR_FONTS_CDN, type ValidateOptions, type ValidationIssue, type ValidationSeverity, VisorBrand, VisorThemeConfig, VisorTypography, applyOverrides, assignSemanticTokens, buildVisorBrandUrl, buildVisorFontUrl, clampToSrgb, cleanFontValue, collectBrandPassthrough, compositeOverBackground, exportTheme, extractFromCSS, formatFontCoverageError, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, googleFontsCatalog, hasBrandPassthrough, hexToOklch, hexToRgb, isValidColor, isValidHex, isVisorThemeConfig, lookupFontWeightAlias, lookupGoogleFont, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveBrandSlot, resolveBrandSource, resolveConfig, resolveFont, resolveThemeBrand, resolveThemeFonts, rgbToHex, serializeColor, validate, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
|
package/dist/index.js
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
VISOR_BRANDS_CDN,
|
|
8
8
|
VISOR_DEFAULT_BRAND_PATH,
|
|
9
9
|
VISOR_FONTS_CDN,
|
|
10
|
+
applyOverrides,
|
|
10
11
|
buildVisorBrandUrl,
|
|
11
12
|
buildVisorFontUrl,
|
|
12
13
|
clampToSrgb,
|
|
14
|
+
collectBrandPassthrough,
|
|
13
15
|
compositeOverBackground,
|
|
14
16
|
generateDarkCss,
|
|
15
17
|
generateFullBundleCss,
|
|
@@ -20,7 +22,9 @@ import {
|
|
|
20
22
|
generateShadeScale,
|
|
21
23
|
generateStylesheetLinks,
|
|
22
24
|
getContrastRatio,
|
|
25
|
+
getLuminance,
|
|
23
26
|
googleFontsCatalog,
|
|
27
|
+
hasBrandPassthrough,
|
|
24
28
|
hexToOklch,
|
|
25
29
|
hexToRgb,
|
|
26
30
|
isValidColor,
|
|
@@ -42,7 +46,7 @@ import {
|
|
|
42
46
|
rgbToHex,
|
|
43
47
|
rgbToOklch,
|
|
44
48
|
serializeColor
|
|
45
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-DQ256PSE.js";
|
|
46
50
|
|
|
47
51
|
// src/fonts/validate-coverage.ts
|
|
48
52
|
var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
|
|
@@ -224,6 +228,15 @@ var visor_theme_schema_default = {
|
|
|
224
228
|
type: "string",
|
|
225
229
|
description: "Theme group for the docs site theme switcher (e.g. 'Visor', 'Client', 'Low Orbit'). Used by `visor theme sync`. Defaults to folder-based grouping when omitted."
|
|
226
230
|
},
|
|
231
|
+
label: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Human-readable display name for the theme (e.g. 'Blacklight Pro'). Overrides the name-derived label in the docs theme switcher. Optional."
|
|
234
|
+
},
|
|
235
|
+
"default-mode": {
|
|
236
|
+
type: "string",
|
|
237
|
+
enum: ["light", "dark"],
|
|
238
|
+
description: "Default color mode when activating this theme. When set, the docs site forces this mode on theme switch (unless the user has a stored mode preference). Optional."
|
|
239
|
+
},
|
|
227
240
|
colors: {
|
|
228
241
|
type: "object",
|
|
229
242
|
description: "Color definitions for light mode. Only primary is required \u2014 all others have sensible defaults.",
|
|
@@ -379,14 +392,27 @@ var visor_theme_schema_default = {
|
|
|
379
392
|
type: "number",
|
|
380
393
|
description: "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
381
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
|
+
},
|
|
382
403
|
"letter-spacing": {
|
|
383
404
|
type: "object",
|
|
384
|
-
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).',
|
|
385
406
|
additionalProperties: false,
|
|
386
407
|
properties: {
|
|
408
|
+
xl: { type: "string" },
|
|
409
|
+
lg: { type: "string" },
|
|
410
|
+
md: { type: "string" },
|
|
411
|
+
sm: { type: "string" },
|
|
412
|
+
xs: { type: "string" },
|
|
387
413
|
tight: { type: "string" },
|
|
388
|
-
normal: { type: "string" },
|
|
389
|
-
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." }
|
|
390
416
|
}
|
|
391
417
|
},
|
|
392
418
|
"cdn-overrides": {
|
|
@@ -683,12 +709,25 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
|
|
|
683
709
|
"letter-spacing",
|
|
684
710
|
"scale",
|
|
685
711
|
"slots",
|
|
686
|
-
"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"
|
|
687
717
|
]);
|
|
688
718
|
var KNOWN_CDN_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["visor-fonts"]);
|
|
689
719
|
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
690
720
|
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
691
|
-
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
|
+
]);
|
|
692
731
|
var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
|
|
693
732
|
var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
|
|
694
733
|
var KNOWN_SPACING_KEYS = /* @__PURE__ */ new Set(["base"]);
|
|
@@ -768,6 +807,12 @@ function checkUnknownKeys(obj, errors) {
|
|
|
768
807
|
}
|
|
769
808
|
}
|
|
770
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
|
+
}
|
|
771
816
|
if (typeof typo["cdn-overrides"] === "object" && typo["cdn-overrides"] !== null) {
|
|
772
817
|
for (const key of Object.keys(typo["cdn-overrides"])) {
|
|
773
818
|
if (!KNOWN_CDN_OVERRIDE_KEYS.has(key)) {
|
|
@@ -1091,9 +1136,29 @@ var DEFAULTS = {
|
|
|
1091
1136
|
},
|
|
1092
1137
|
typography: {
|
|
1093
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",
|
|
1094
1148
|
heading: { family: DEFAULT_FONT_SANS, weight: 600 },
|
|
1095
1149
|
body: { family: DEFAULT_FONT_SANS, weight: 400 },
|
|
1096
|
-
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
|
+
}
|
|
1097
1162
|
},
|
|
1098
1163
|
spacing: { base: 4 },
|
|
1099
1164
|
radius: { sm: 2, md: 4, lg: 8, xl: 12, pill: 9999 },
|
|
@@ -1134,6 +1199,17 @@ function resolveBrand(brand) {
|
|
|
1134
1199
|
...brand.custom && { custom: brand.custom }
|
|
1135
1200
|
};
|
|
1136
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
|
+
}
|
|
1137
1213
|
function resolveConfig(config) {
|
|
1138
1214
|
const colors = config.colors;
|
|
1139
1215
|
const originalColors = {};
|
|
@@ -1165,6 +1241,7 @@ function resolveConfig(config) {
|
|
|
1165
1241
|
return {
|
|
1166
1242
|
name: config.name,
|
|
1167
1243
|
...config.label !== void 0 && { label: config.label },
|
|
1244
|
+
...config["default-mode"] !== void 0 && { "default-mode": config["default-mode"] },
|
|
1168
1245
|
version: 1,
|
|
1169
1246
|
colors: {
|
|
1170
1247
|
primary: colors.primary,
|
|
@@ -1181,6 +1258,8 @@ function resolveConfig(config) {
|
|
|
1181
1258
|
"colors-dark": config["colors-dark"],
|
|
1182
1259
|
typography: {
|
|
1183
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"],
|
|
1184
1263
|
heading: {
|
|
1185
1264
|
family: config.typography?.heading?.family ?? DEFAULTS.typography.heading.family,
|
|
1186
1265
|
weight: config.typography?.heading?.weight ?? DEFAULTS.typography.heading.weight,
|
|
@@ -1212,7 +1291,8 @@ function resolveConfig(config) {
|
|
|
1212
1291
|
...config.typography?.["cdn-overrides"] && {
|
|
1213
1292
|
"cdn-overrides": config.typography["cdn-overrides"]
|
|
1214
1293
|
},
|
|
1215
|
-
slots: config.typography?.slots ?? {}
|
|
1294
|
+
slots: config.typography?.slots ?? {},
|
|
1295
|
+
"letter-spacing": resolveLetterSpacing(config.typography?.["letter-spacing"])
|
|
1216
1296
|
},
|
|
1217
1297
|
brand: resolveBrand(config.brand),
|
|
1218
1298
|
spacing: {
|
|
@@ -1261,6 +1341,10 @@ var CONFIG_BACKGROUND = "__CONFIG_BACKGROUND__";
|
|
|
1261
1341
|
var CONFIG_SURFACE = "__CONFIG_SURFACE__";
|
|
1262
1342
|
var CONFIG_DARK_BACKGROUND = "__CONFIG_DARK_BACKGROUND__";
|
|
1263
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
|
+
}
|
|
1264
1348
|
var SEMANTIC_TEXT_MAP = {
|
|
1265
1349
|
primary: {
|
|
1266
1350
|
light: { role: "neutral", shade: 900 },
|
|
@@ -1516,9 +1600,13 @@ var SEMANTIC_INTERACTIVE_MAP = {
|
|
|
1516
1600
|
light: { role: "primary", shade: 800 },
|
|
1517
1601
|
dark: { role: "primary", shade: 300 }
|
|
1518
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.
|
|
1519
1607
|
"primary-text": {
|
|
1520
|
-
light: { constant: "
|
|
1521
|
-
dark: { constant: "
|
|
1608
|
+
light: { constant: deriveOnBg("primary-bg") },
|
|
1609
|
+
dark: { constant: deriveOnBg("primary-bg") }
|
|
1522
1610
|
},
|
|
1523
1611
|
// VI-478: brand-derived alpha-overlay helpers (BL-193). `soft`/`glow` are
|
|
1524
1612
|
// alpha overlays that track the theme's primary via color-mix (distinct from
|
|
@@ -1567,9 +1655,11 @@ var SEMANTIC_INTERACTIVE_MAP = {
|
|
|
1567
1655
|
light: { role: "error", shade: 700 },
|
|
1568
1656
|
dark: { role: "error", shade: 600 }
|
|
1569
1657
|
},
|
|
1658
|
+
// VI-375: derive from the paired `destructive-bg` luminance (was hardcoded
|
|
1659
|
+
// #ffffff). Per-token override still wins.
|
|
1570
1660
|
"destructive-text": {
|
|
1571
|
-
light: { constant: "
|
|
1572
|
-
dark: { constant: "
|
|
1661
|
+
light: { constant: deriveOnBg("destructive-bg") },
|
|
1662
|
+
dark: { constant: deriveOnBg("destructive-bg") }
|
|
1573
1663
|
},
|
|
1574
1664
|
// Ghost action
|
|
1575
1665
|
"ghost-bg": {
|
|
@@ -1586,11 +1676,14 @@ var SEMANTIC_INTENT_MAP = {
|
|
|
1586
1676
|
light: { role: "primary", shade: 500 },
|
|
1587
1677
|
dark: { role: "primary", shade: 500 }
|
|
1588
1678
|
},
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1679
|
+
// Single-source alias of --interactive-primary-text. Default white (same value
|
|
1680
|
+
// as the interactive group); themes that need a different value (e.g. entr)
|
|
1681
|
+
// override via overrides.{light,dark}["primary-text"] which replaces this alias
|
|
1682
|
+
// with the explicit override value. Hand-authored static CSS (blackout-theme.css,
|
|
1683
|
+
// neutral-theme.css) should consume --primary-text via this alias path.
|
|
1591
1684
|
"primary-text": {
|
|
1592
|
-
light: { constant: "
|
|
1593
|
-
dark: { constant: "
|
|
1685
|
+
light: { constant: "var(--interactive-primary-text)" },
|
|
1686
|
+
dark: { constant: "var(--interactive-primary-text)" }
|
|
1594
1687
|
},
|
|
1595
1688
|
accent: {
|
|
1596
1689
|
light: { role: "accent", shade: 500 },
|
|
@@ -1624,6 +1717,16 @@ var SEMANTIC_HAIRLINE_MAP = {
|
|
|
1624
1717
|
dark: { constant: "rgba(255, 255, 255, 0.10)" }
|
|
1625
1718
|
}
|
|
1626
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
|
+
})();
|
|
1627
1730
|
var SEMANTIC_MAP = {
|
|
1628
1731
|
text: SEMANTIC_TEXT_MAP,
|
|
1629
1732
|
surface: SEMANTIC_SURFACE_MAP,
|
|
@@ -1661,6 +1764,66 @@ function resolveRef(ref, primitives, config) {
|
|
|
1661
1764
|
return ref.constant;
|
|
1662
1765
|
}
|
|
1663
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
|
+
}
|
|
1664
1827
|
function resolveMapping(mapping, lightPrimitives, darkPrimitives, config) {
|
|
1665
1828
|
return {
|
|
1666
1829
|
light: resolveRef(mapping.light, lightPrimitives, config),
|
|
@@ -1686,6 +1849,7 @@ function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
|
1686
1849
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.interactive)) {
|
|
1687
1850
|
interactive[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
1688
1851
|
}
|
|
1852
|
+
deriveInteractiveTextColors(interactive, config);
|
|
1689
1853
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.intent)) {
|
|
1690
1854
|
intent[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
1691
1855
|
}
|
|
@@ -1695,71 +1859,6 @@ function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
|
1695
1859
|
return { text, surface, border, interactive, intent, hairline };
|
|
1696
1860
|
}
|
|
1697
1861
|
|
|
1698
|
-
// src/overrides.ts
|
|
1699
|
-
var TOKEN_CATEGORIES = [
|
|
1700
|
-
{ prefix: "text-", key: "text" },
|
|
1701
|
-
{ prefix: "surface-", key: "surface" },
|
|
1702
|
-
{ prefix: "border-", key: "border" },
|
|
1703
|
-
{ prefix: "interactive-", key: "interactive" },
|
|
1704
|
-
{ prefix: "hairline-", key: "hairline" }
|
|
1705
|
-
];
|
|
1706
|
-
function findToken(key, tokens) {
|
|
1707
|
-
if (key === "hairline" && "default" in tokens.hairline) {
|
|
1708
|
-
return { group: tokens.hairline, name: "default" };
|
|
1709
|
-
}
|
|
1710
|
-
for (const { prefix, key: groupKey } of TOKEN_CATEGORIES) {
|
|
1711
|
-
if (key.startsWith(prefix)) {
|
|
1712
|
-
const name = key.slice(prefix.length);
|
|
1713
|
-
if (name in tokens[groupKey]) {
|
|
1714
|
-
return { group: tokens[groupKey], name };
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
if (key in tokens.intent) {
|
|
1719
|
-
return { group: tokens.intent, name: key };
|
|
1720
|
-
}
|
|
1721
|
-
return null;
|
|
1722
|
-
}
|
|
1723
|
-
function applyOverrides(tokens, overrides) {
|
|
1724
|
-
if (!overrides) return tokens;
|
|
1725
|
-
const result = {
|
|
1726
|
-
text: { ...tokens.text },
|
|
1727
|
-
surface: { ...tokens.surface },
|
|
1728
|
-
border: { ...tokens.border },
|
|
1729
|
-
interactive: { ...tokens.interactive },
|
|
1730
|
-
intent: { ...tokens.intent },
|
|
1731
|
-
hairline: { ...tokens.hairline }
|
|
1732
|
-
};
|
|
1733
|
-
for (const group of ["text", "surface", "border", "interactive", "intent", "hairline"]) {
|
|
1734
|
-
for (const [name, value] of Object.entries(result[group])) {
|
|
1735
|
-
result[group][name] = { ...value };
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
if (overrides.light) {
|
|
1739
|
-
for (const [key, value] of Object.entries(overrides.light)) {
|
|
1740
|
-
const match = findToken(key, result);
|
|
1741
|
-
if (match) {
|
|
1742
|
-
match.group[match.name] = {
|
|
1743
|
-
...match.group[match.name],
|
|
1744
|
-
light: value
|
|
1745
|
-
};
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
if (overrides.dark) {
|
|
1750
|
-
for (const [key, value] of Object.entries(overrides.dark)) {
|
|
1751
|
-
const match = findToken(key, result);
|
|
1752
|
-
if (match) {
|
|
1753
|
-
match.group[match.name] = {
|
|
1754
|
-
...match.group[match.name],
|
|
1755
|
-
dark: value
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
return result;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
1862
|
// src/pipeline.ts
|
|
1764
1863
|
function generatePrimitives(config) {
|
|
1765
1864
|
return {
|
|
@@ -1819,6 +1918,7 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
1819
1918
|
const darkPrimitives = generateDarkPrimitives(resolved, primitives);
|
|
1820
1919
|
let tokens = assignSemanticTokens(primitives, darkPrimitives, resolved);
|
|
1821
1920
|
tokens = applyOverrides(tokens, resolved.overrides);
|
|
1921
|
+
tokens = reapplyInteractiveTextDerivation(tokens, resolved, resolved.overrides);
|
|
1822
1922
|
const output = {
|
|
1823
1923
|
primitivesCss: generatePrimitivesCss(primitives, resolved),
|
|
1824
1924
|
semanticCss: generateSemanticCss(tokens),
|
|
@@ -2242,7 +2342,7 @@ function checkTypeScaleCoherence(config, issues) {
|
|
|
2242
2342
|
function checkLetterSpacing(config, issues) {
|
|
2243
2343
|
const ls = config.typography?.["letter-spacing"];
|
|
2244
2344
|
if (!ls) return;
|
|
2245
|
-
for (const key of ["tight", "normal", "wide"]) {
|
|
2345
|
+
for (const key of ["xl", "lg", "md", "sm", "xs", "tight", "normal", "wide"]) {
|
|
2246
2346
|
const value = ls[key];
|
|
2247
2347
|
if (value !== void 0) {
|
|
2248
2348
|
if (typeof value !== "string" || !CSS_LENGTH_RE.test(value.trim())) {
|
|
@@ -2361,7 +2461,7 @@ function checkOverrides(config, issues) {
|
|
|
2361
2461
|
issue(
|
|
2362
2462
|
"warning",
|
|
2363
2463
|
"UNKNOWN_OVERRIDE_KEY",
|
|
2364
|
-
`'overrides.${mode}.${key}' does not match any known semantic token.
|
|
2464
|
+
`'overrides.${mode}.${key}' does not match any known semantic token; it will be emitted as a bare '--${key}' custom property in @layer visor-brand (brand pass-through). If you meant to override a semantic token, valid tokens include: text-primary, surface-page, border-default, interactive-primary-bg, etc.`,
|
|
2365
2465
|
`overrides.${mode}.${key}`
|
|
2366
2466
|
)
|
|
2367
2467
|
);
|
|
@@ -2455,12 +2555,27 @@ function colorToRgb(color) {
|
|
|
2455
2555
|
}
|
|
2456
2556
|
var STANDARD_TEXT_TOKENS = ["primary", "secondary", "tertiary"];
|
|
2457
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
|
+
})();
|
|
2458
2569
|
function checkContrastWarnings(config, issues) {
|
|
2459
2570
|
const resolved = resolveConfig(config);
|
|
2460
2571
|
const lightPrimitives = generatePrimitives(resolved);
|
|
2461
2572
|
const darkPrimitives = generateDarkPrimitives(resolved, lightPrimitives);
|
|
2462
|
-
const tokens =
|
|
2463
|
-
|
|
2573
|
+
const tokens = reapplyInteractiveTextDerivation(
|
|
2574
|
+
applyOverrides(
|
|
2575
|
+
assignSemanticTokens(lightPrimitives, darkPrimitives, resolved),
|
|
2576
|
+
resolved.overrides
|
|
2577
|
+
),
|
|
2578
|
+
resolved,
|
|
2464
2579
|
resolved.overrides
|
|
2465
2580
|
);
|
|
2466
2581
|
const lightBg = resolved.colors.background;
|
|
@@ -2523,7 +2638,10 @@ function checkContrastWarnings(config, issues) {
|
|
|
2523
2638
|
);
|
|
2524
2639
|
}
|
|
2525
2640
|
}
|
|
2526
|
-
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);
|
|
2527
2645
|
if (primaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
2528
2646
|
issues.push(
|
|
2529
2647
|
issue(
|
|
@@ -2534,7 +2652,7 @@ function checkContrastWarnings(config, issues) {
|
|
|
2534
2652
|
)
|
|
2535
2653
|
);
|
|
2536
2654
|
}
|
|
2537
|
-
const primaryOnSurface = getContrastRatio(
|
|
2655
|
+
const primaryOnSurface = getContrastRatio(lightPrimaryBg, lightSurface, lightSurfaceRgb);
|
|
2538
2656
|
if (primaryOnSurface < CONTRAST_INTERACTIVE_AA) {
|
|
2539
2657
|
issues.push(
|
|
2540
2658
|
issue(
|
|
@@ -2545,7 +2663,7 @@ function checkContrastWarnings(config, issues) {
|
|
|
2545
2663
|
)
|
|
2546
2664
|
);
|
|
2547
2665
|
}
|
|
2548
|
-
const darkPrimaryOnBg = getContrastRatio(
|
|
2666
|
+
const darkPrimaryOnBg = getContrastRatio(darkPrimaryBg, darkBg, darkBgRgb);
|
|
2549
2667
|
if (darkPrimaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
2550
2668
|
issues.push(
|
|
2551
2669
|
issue(
|
|
@@ -2556,7 +2674,7 @@ function checkContrastWarnings(config, issues) {
|
|
|
2556
2674
|
)
|
|
2557
2675
|
);
|
|
2558
2676
|
}
|
|
2559
|
-
const darkPrimaryOnSurface = getContrastRatio(
|
|
2677
|
+
const darkPrimaryOnSurface = getContrastRatio(darkPrimaryBg, darkSurface, darkSurfaceRgb);
|
|
2560
2678
|
if (darkPrimaryOnSurface < CONTRAST_INTERACTIVE_AA) {
|
|
2561
2679
|
issues.push(
|
|
2562
2680
|
issue(
|
|
@@ -2567,6 +2685,33 @@ function checkContrastWarnings(config, issues) {
|
|
|
2567
2685
|
)
|
|
2568
2686
|
);
|
|
2569
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
|
+
}
|
|
2570
2715
|
}
|
|
2571
2716
|
function checkColorSimilarity(config, issues) {
|
|
2572
2717
|
const resolved = resolveConfig(config);
|
|
@@ -2642,14 +2787,15 @@ function checkRadiusScale(config, issues) {
|
|
|
2642
2787
|
);
|
|
2643
2788
|
}
|
|
2644
2789
|
}
|
|
2645
|
-
function checkDarkLightParity(config, issues) {
|
|
2790
|
+
function checkDarkLightParity(config, issues, opts) {
|
|
2646
2791
|
if (!config.colors) return;
|
|
2647
2792
|
const colorKeys = Object.keys(config.colors).filter((k) => k !== "primary");
|
|
2648
2793
|
const hasDarkSection = config["colors-dark"] !== void 0;
|
|
2649
2794
|
if (colorKeys.length > 0 && !hasDarkSection) {
|
|
2795
|
+
const severity = opts.strictDark ? "error" : "warning";
|
|
2650
2796
|
issues.push(
|
|
2651
2797
|
issue(
|
|
2652
|
-
|
|
2798
|
+
severity,
|
|
2653
2799
|
"DARK_LIGHT_PARITY",
|
|
2654
2800
|
"Custom colors are set but no colors-dark section exists. Dark mode will use generated defaults which may not match your brand.",
|
|
2655
2801
|
"colors-dark"
|
|
@@ -2663,9 +2809,10 @@ function checkDarkLightParity(config, issues) {
|
|
|
2663
2809
|
for (const key of lightKeys) {
|
|
2664
2810
|
if (key === "primary") continue;
|
|
2665
2811
|
if (!darkKeys.has(key)) {
|
|
2812
|
+
const severity = opts.strictDark ? "error" : "warning";
|
|
2666
2813
|
issues.push(
|
|
2667
2814
|
issue(
|
|
2668
|
-
|
|
2815
|
+
severity,
|
|
2669
2816
|
"DARK_LIGHT_PARITY",
|
|
2670
2817
|
`Color "${key}" is set in colors but missing from colors-dark. Dark mode will use a generated default.`,
|
|
2671
2818
|
"colors-dark"
|
|
@@ -2687,7 +2834,8 @@ function checkDarkLightParity(config, issues) {
|
|
|
2687
2834
|
}
|
|
2688
2835
|
}
|
|
2689
2836
|
}
|
|
2690
|
-
function validate(config) {
|
|
2837
|
+
function validate(config, options) {
|
|
2838
|
+
const opts = options || {};
|
|
2691
2839
|
const errors = [];
|
|
2692
2840
|
const warnings = [];
|
|
2693
2841
|
const structurallyValid = checkStructuralIntegrity(config, errors);
|
|
@@ -2719,7 +2867,11 @@ function validate(config) {
|
|
|
2719
2867
|
checkColorSimilarity(typedConfig, warnings);
|
|
2720
2868
|
checkMissingGlowShadow(typedConfig, warnings);
|
|
2721
2869
|
checkRadiusScale(typedConfig, warnings);
|
|
2722
|
-
|
|
2870
|
+
const parityIssues = [];
|
|
2871
|
+
checkDarkLightParity(typedConfig, parityIssues, opts);
|
|
2872
|
+
for (const iss of parityIssues) {
|
|
2873
|
+
(iss.severity === "error" ? errors : warnings).push(iss);
|
|
2874
|
+
}
|
|
2723
2875
|
}
|
|
2724
2876
|
return {
|
|
2725
2877
|
valid: errors.length === 0,
|
|
@@ -3229,6 +3381,7 @@ export {
|
|
|
3229
3381
|
buildVisorFontUrl,
|
|
3230
3382
|
clampToSrgb,
|
|
3231
3383
|
cleanFontValue,
|
|
3384
|
+
collectBrandPassthrough,
|
|
3232
3385
|
compositeOverBackground,
|
|
3233
3386
|
exportTheme,
|
|
3234
3387
|
extractFromCSS,
|
|
@@ -3248,6 +3401,7 @@ export {
|
|
|
3248
3401
|
generateThemeFromConfig,
|
|
3249
3402
|
getContrastRatio,
|
|
3250
3403
|
googleFontsCatalog,
|
|
3404
|
+
hasBrandPassthrough,
|
|
3251
3405
|
hexToOklch,
|
|
3252
3406
|
hexToRgb,
|
|
3253
3407
|
isValidColor,
|
|
@@ -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
|
/**
|
|
@@ -412,6 +441,8 @@ interface ResolvedThemeConfig {
|
|
|
412
441
|
name: string;
|
|
413
442
|
/** Optional display label override forwarded from VisorThemeConfig.label. */
|
|
414
443
|
label?: string;
|
|
444
|
+
/** Default color mode forwarded from VisorThemeConfig["default-mode"]. */
|
|
445
|
+
"default-mode"?: "dark" | "light";
|
|
415
446
|
version: 1;
|
|
416
447
|
colors: {
|
|
417
448
|
primary: string;
|
|
@@ -427,6 +458,16 @@ interface ResolvedThemeConfig {
|
|
|
427
458
|
"colors-dark"?: VisorThemeConfig["colors-dark"];
|
|
428
459
|
typography: {
|
|
429
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;
|
|
430
471
|
heading: {
|
|
431
472
|
family: string;
|
|
432
473
|
weight: number;
|
|
@@ -469,6 +510,19 @@ interface ResolvedThemeConfig {
|
|
|
469
510
|
* consumes these; other adapters may ignore them.
|
|
470
511
|
*/
|
|
471
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
|
+
};
|
|
472
526
|
};
|
|
473
527
|
/**
|
|
474
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",
|
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
"type": "string",
|
|
21
21
|
"description": "Theme group for the docs site theme switcher (e.g. 'Visor', 'Client', 'Low Orbit'). Used by `visor theme sync`. Defaults to folder-based grouping when omitted."
|
|
22
22
|
},
|
|
23
|
+
"label": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Human-readable display name for the theme (e.g. 'Blacklight Pro'). Overrides the name-derived label in the docs theme switcher. Optional."
|
|
26
|
+
},
|
|
27
|
+
"default-mode": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"enum": ["light", "dark"],
|
|
30
|
+
"description": "Default color mode when activating this theme. When set, the docs site forces this mode on theme switch (unless the user has a stored mode preference). Optional."
|
|
31
|
+
},
|
|
23
32
|
"colors": {
|
|
24
33
|
"type": "object",
|
|
25
34
|
"description": "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
|
|
@@ -175,14 +184,27 @@
|
|
|
175
184
|
"type": "number",
|
|
176
185
|
"description": "Type scale multiplier applied to the font-size ramp. Default: 1."
|
|
177
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
|
+
},
|
|
178
195
|
"letter-spacing": {
|
|
179
196
|
"type": "object",
|
|
180
|
-
"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).",
|
|
181
198
|
"additionalProperties": false,
|
|
182
199
|
"properties": {
|
|
200
|
+
"xl": { "type": "string" },
|
|
201
|
+
"lg": { "type": "string" },
|
|
202
|
+
"md": { "type": "string" },
|
|
203
|
+
"sm": { "type": "string" },
|
|
204
|
+
"xs": { "type": "string" },
|
|
183
205
|
"tight": { "type": "string" },
|
|
184
|
-
"normal": { "type": "string" },
|
|
185
|
-
"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." }
|
|
186
208
|
}
|
|
187
209
|
},
|
|
188
210
|
"cdn-overrides": {
|