@loworbitstudio/visor-theme-engine 0.17.0 → 0.18.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 +16 -2
- package/dist/adapters/index.js +163 -78
- package/dist/{chunk-DQ256PSE.js → chunk-BUWBBUFG.js} +64 -2
- package/dist/index.d.ts +16 -3
- package/dist/index.js +50 -3
- package/dist/{types-DqsWwFVZ.d.ts → types-Cvm7vFwe.d.ts} +19 -1
- package/package.json +1 -1
- package/src/visor-theme.schema.json +5 -0
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { k as GeneratedPrimitives,
|
|
1
|
+
import { k as GeneratedPrimitives, s as SemanticTokens, R as ResolvedThemeConfig } from '../types-Cvm7vFwe.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Adapter types for the Visor theme engine.
|
|
@@ -32,6 +32,13 @@ interface NextJSAdapterOptions extends AdapterOptions {
|
|
|
32
32
|
* (`:root`) for backward compatibility. See VI-368.
|
|
33
33
|
*/
|
|
34
34
|
scopePrefix?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Emit the `@import "@loworbitstudio/visor-core/reset";` line and the
|
|
37
|
+
* `@layer visor-base` origination block that binds theme tokens to the page
|
|
38
|
+
* root (default: true). Set false for consumers that ship Tailwind preflight
|
|
39
|
+
* or their own reset. See VI-616.
|
|
40
|
+
*/
|
|
41
|
+
includeBaseLayer?: boolean;
|
|
35
42
|
}
|
|
36
43
|
/** Options specific to the Deck adapter. */
|
|
37
44
|
interface DeckAdapterOptions extends AdapterOptions {
|
|
@@ -198,12 +205,19 @@ declare function flutterAdapter(input: AdapterInput, options?: FlutterAdapterOpt
|
|
|
198
205
|
/**
|
|
199
206
|
* Layer order declaration — must appear before any @layer blocks.
|
|
200
207
|
*
|
|
208
|
+
* `visor-base` (VI-616) is FIRST — i.e. lowest priority. It carries the
|
|
209
|
+
* element-level baseline (token-to-page binding + UA reset). Lowest placement
|
|
210
|
+
* means a consumer's own unlayered `body {}` always wins, and component
|
|
211
|
+
* `.module.css` (which uses no `@layer` at all) beats the base layer
|
|
212
|
+
* unconditionally. Author-origin still beats the UA stylesheet regardless of
|
|
213
|
+
* layer, so the reset does its job.
|
|
214
|
+
*
|
|
201
215
|
* `visor-brand` (VI-470) is ordered immediately after `visor-semantic`: brand
|
|
202
216
|
* asset vars (`--brand-*`) sit above semantic tokens so brand overrides stay
|
|
203
217
|
* cleanly separable, while still below `visor-adaptive` chrome and the
|
|
204
218
|
* `visor-bridge` framework layer.
|
|
205
219
|
*/
|
|
206
|
-
declare const LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;";
|
|
220
|
+
declare const LAYER_ORDER = "@layer visor-base, visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;";
|
|
207
221
|
/**
|
|
208
222
|
* Wrap CSS content in a named @layer block.
|
|
209
223
|
*/
|
package/dist/adapters/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resolveThemeBrand,
|
|
21
21
|
resolveThemeFonts,
|
|
22
22
|
sectionComment
|
|
23
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-BUWBBUFG.js";
|
|
24
24
|
|
|
25
25
|
// src/adapters/brand-passthrough.ts
|
|
26
26
|
var SENTINEL_COLOR = "#ff00ff";
|
|
@@ -43,7 +43,7 @@ function indentBlock(selector, decls) {
|
|
|
43
43
|
if (decls.length === 0) return "";
|
|
44
44
|
return [selector + " {", ...decls.map((d) => ` ${d}`), "}"].join("\n");
|
|
45
45
|
}
|
|
46
|
-
function generateBrandPassthroughCss(passthrough, selectors) {
|
|
46
|
+
function generateBrandPassthroughCss(passthrough, selectors, colorScheme = "adaptive") {
|
|
47
47
|
const lightKeys = Object.keys(passthrough.light);
|
|
48
48
|
const darkKeys = Object.keys(passthrough.dark);
|
|
49
49
|
if (lightKeys.length === 0 && darkKeys.length === 0) return "";
|
|
@@ -53,6 +53,20 @@ function generateBrandPassthroughCss(passthrough, selectors) {
|
|
|
53
53
|
const count = lightKeys.length + darkKeys.length;
|
|
54
54
|
blocks.push(`/* [visor-brand] ${count} passthrough: ${names} */`);
|
|
55
55
|
}
|
|
56
|
+
if (colorScheme === "dark-only") {
|
|
57
|
+
if (darkKeys.length > 0) {
|
|
58
|
+
const decls = darkKeys.map((k) => declFor(k, passthrough.dark[k]));
|
|
59
|
+
blocks.push(indentBlock(selectors.light, decls));
|
|
60
|
+
}
|
|
61
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
if (colorScheme === "light-only") {
|
|
64
|
+
if (lightKeys.length > 0) {
|
|
65
|
+
const decls = lightKeys.map((k) => declFor(k, passthrough.light[k]));
|
|
66
|
+
blocks.push(indentBlock(selectors.light, decls));
|
|
67
|
+
}
|
|
68
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
69
|
+
}
|
|
56
70
|
if (lightKeys.length > 0) {
|
|
57
71
|
const decls = lightKeys.map((k) => declFor(k, passthrough.light[k]));
|
|
58
72
|
blocks.push(indentBlock(selectors.light, decls));
|
|
@@ -69,7 +83,7 @@ ${prefersInner}
|
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
// src/adapters/layers.ts
|
|
72
|
-
var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;";
|
|
86
|
+
var LAYER_ORDER = "@layer visor-base, visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;";
|
|
73
87
|
function wrapInLayer(layerName, css) {
|
|
74
88
|
const trimmed = css.trim();
|
|
75
89
|
if (!trimmed) return "";
|
|
@@ -85,11 +99,17 @@ function toKebabCase(name) {
|
|
|
85
99
|
function nextjsAdapter(input, options) {
|
|
86
100
|
const includeFontImports = options?.includeFontImports ?? true;
|
|
87
101
|
const includeFowt = options?.includeFowt ?? true;
|
|
102
|
+
const includeBaseLayer = options?.includeBaseLayer ?? true;
|
|
88
103
|
const scopePrefix = options?.scopePrefix;
|
|
104
|
+
const colorScheme = input.config["color-scheme"] ?? "adaptive";
|
|
89
105
|
const lines = [];
|
|
90
106
|
const slug = toKebabCase(input.config.name);
|
|
91
107
|
const aliasedFamilies = /* @__PURE__ */ new Map();
|
|
92
108
|
lines.push(header("Visor Theme \u2014 NextJS Adapter"));
|
|
109
|
+
if (includeBaseLayer) {
|
|
110
|
+
lines.push('@import "@loworbitstudio/visor-core/reset";');
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
93
113
|
if (includeFontImports && input.config.typography) {
|
|
94
114
|
const fontResult = resolveThemeFonts(input.config.typography);
|
|
95
115
|
const fontSlots = [fontResult.heading, fontResult.display, fontResult.body, fontResult.mono];
|
|
@@ -142,6 +162,27 @@ function nextjsAdapter(input, options) {
|
|
|
142
162
|
}
|
|
143
163
|
lines.push(LAYER_ORDER);
|
|
144
164
|
lines.push("");
|
|
165
|
+
if (includeBaseLayer) {
|
|
166
|
+
const baseLines = [];
|
|
167
|
+
if (colorScheme === "dark-only" || colorScheme === "light-only") {
|
|
168
|
+
baseLines.push(sectionComment("Base: UA color-scheme"));
|
|
169
|
+
baseLines.push(
|
|
170
|
+
block("html", [`color-scheme: ${colorScheme === "dark-only" ? "dark" : "light"};`])
|
|
171
|
+
);
|
|
172
|
+
baseLines.push("");
|
|
173
|
+
}
|
|
174
|
+
baseLines.push(sectionComment("Base: token-to-page binding"));
|
|
175
|
+
baseLines.push(
|
|
176
|
+
block(scopePrefix ?? "body", [
|
|
177
|
+
"font-family: var(--font-body);",
|
|
178
|
+
"font-size: 1rem;",
|
|
179
|
+
"color: var(--text-primary);",
|
|
180
|
+
"background: var(--surface-page, var(--surface-background));"
|
|
181
|
+
])
|
|
182
|
+
);
|
|
183
|
+
lines.push(wrapInLayer("visor-base", baseLines.join("\n").trim()));
|
|
184
|
+
lines.push("");
|
|
185
|
+
}
|
|
145
186
|
const primitivesBody = stripHeader(
|
|
146
187
|
generatePrimitivesCss(input.primitives, input.config, {
|
|
147
188
|
aliasedFamilies,
|
|
@@ -156,7 +197,7 @@ function nextjsAdapter(input, options) {
|
|
|
156
197
|
light: scopePrefix ?? ":root",
|
|
157
198
|
dark: darkSelectors.join(",\n"),
|
|
158
199
|
prefers: scopePrefix ? `${scopePrefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])'
|
|
159
|
-
});
|
|
200
|
+
}, colorScheme);
|
|
160
201
|
if (passthroughCss) {
|
|
161
202
|
lines.push(wrapInLayer("visor-brand", passthroughCss));
|
|
162
203
|
lines.push("");
|
|
@@ -173,43 +214,60 @@ function nextjsAdapter(input, options) {
|
|
|
173
214
|
semanticLines.push(sectionComment("Discrete: Space aliases (--space-N)"));
|
|
174
215
|
semanticLines.push(block(hostSelector, generateSpaceAliasDecls(input.config)));
|
|
175
216
|
semanticLines.push("");
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
semanticLines.push(
|
|
217
|
+
if (colorScheme === "dark-only") {
|
|
218
|
+
semanticLines.push(sectionComment("Intent aliases (dark) \u2014 host"));
|
|
219
|
+
semanticLines.push(block(hostSelector, generateIntentDecls(input.tokens, "dark")));
|
|
220
|
+
semanticLines.push("");
|
|
221
|
+
semanticLines.push(sectionComment("Hairline aliases (dark) \u2014 host"));
|
|
222
|
+
semanticLines.push(block(hostSelector, generateHairlineDecls(input.tokens, "dark")));
|
|
223
|
+
semanticLines.push("");
|
|
224
|
+
} else if (colorScheme === "light-only") {
|
|
225
|
+
semanticLines.push(sectionComment("Intent aliases (light) \u2014 host"));
|
|
226
|
+
semanticLines.push(block(hostSelector, generateIntentDecls(input.tokens, "light")));
|
|
227
|
+
semanticLines.push("");
|
|
228
|
+
semanticLines.push(sectionComment("Hairline aliases (light) \u2014 host"));
|
|
229
|
+
semanticLines.push(block(hostSelector, generateHairlineDecls(input.tokens, "light")));
|
|
230
|
+
semanticLines.push("");
|
|
231
|
+
} else {
|
|
232
|
+
semanticLines.push(sectionComment("Intent aliases (light)"));
|
|
233
|
+
semanticLines.push(block(lightModeSelector, generateIntentDecls(input.tokens, "light")));
|
|
234
|
+
semanticLines.push("");
|
|
235
|
+
semanticLines.push(sectionComment("Hairline aliases (light)"));
|
|
236
|
+
semanticLines.push(block(lightModeSelector, generateHairlineDecls(input.tokens, "light")));
|
|
237
|
+
semanticLines.push("");
|
|
238
|
+
semanticLines.push(sectionComment("Intent aliases (dark) \u2014 manual toggle"));
|
|
239
|
+
semanticLines.push(block(darkModeSelector, generateIntentDecls(input.tokens, "dark")));
|
|
240
|
+
semanticLines.push("");
|
|
241
|
+
semanticLines.push(sectionComment("Hairline aliases (dark) \u2014 manual toggle"));
|
|
242
|
+
semanticLines.push(block(darkModeSelector, generateHairlineDecls(input.tokens, "dark")));
|
|
243
|
+
semanticLines.push("");
|
|
244
|
+
semanticLines.push(sectionComment("Intent aliases (dark) \u2014 prefers-color-scheme"));
|
|
245
|
+
{
|
|
246
|
+
const inner = block(prefersSelector, generateIntentDecls(input.tokens, "dark"));
|
|
247
|
+
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
192
248
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
193
249
|
}`);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
250
|
+
}
|
|
251
|
+
semanticLines.push("");
|
|
252
|
+
semanticLines.push(sectionComment("Hairline aliases (dark) \u2014 prefers-color-scheme"));
|
|
253
|
+
{
|
|
254
|
+
const inner = block(prefersSelector, generateHairlineDecls(input.tokens, "dark"));
|
|
255
|
+
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
200
256
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
201
257
|
}`);
|
|
258
|
+
}
|
|
259
|
+
semanticLines.push("");
|
|
202
260
|
}
|
|
203
|
-
semanticLines.push("");
|
|
204
261
|
const semanticLayer = wrapInLayer("visor-semantic", semanticLines.join("\n").trim());
|
|
205
262
|
if (semanticLayer) {
|
|
206
263
|
lines.push(semanticLayer);
|
|
207
264
|
lines.push("");
|
|
208
265
|
}
|
|
209
|
-
const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
|
|
210
|
-
const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
|
|
266
|
+
const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix, colorScheme }));
|
|
267
|
+
const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix, colorScheme }));
|
|
268
|
+
const adaptiveBody = [lightBody, darkBody].filter(Boolean).join("\n\n");
|
|
211
269
|
lines.push(
|
|
212
|
-
wrapInLayer("visor-adaptive",
|
|
270
|
+
wrapInLayer("visor-adaptive", adaptiveBody)
|
|
213
271
|
);
|
|
214
272
|
if (includeFowt) {
|
|
215
273
|
lines.push("");
|
|
@@ -554,6 +612,14 @@ function sectionComment2(label) {
|
|
|
554
612
|
function docsAdapter(input, options) {
|
|
555
613
|
const slug = toKebabCase3(input.config.name);
|
|
556
614
|
const scopeClass = `.${slug}-theme`;
|
|
615
|
+
const colorScheme = input.config["color-scheme"] ?? "adaptive";
|
|
616
|
+
const singleMode = colorScheme === "dark-only" ? "dark" : colorScheme === "light-only" ? "light" : null;
|
|
617
|
+
const emitDark = colorScheme !== "light-only";
|
|
618
|
+
const emitLight = colorScheme !== "dark-only";
|
|
619
|
+
const emitPrefers = colorScheme === "adaptive";
|
|
620
|
+
const darkSel = singleMode ? scopeClass : `.dark ${scopeClass}`;
|
|
621
|
+
const lightSel = singleMode ? scopeClass : `html:not(.dark) ${scopeClass}`;
|
|
622
|
+
const darkSuffix = singleMode ? " \u2014 host" : " \u2014 manual toggle";
|
|
557
623
|
const includeFontImports = options?.includeFontImports ?? true;
|
|
558
624
|
const fontLines = [];
|
|
559
625
|
const lines = [];
|
|
@@ -601,6 +667,8 @@ function docsAdapter(input, options) {
|
|
|
601
667
|
const sharedDecls = [
|
|
602
668
|
"min-height: 100vh;",
|
|
603
669
|
"font-size: 1rem;",
|
|
670
|
+
// BO-56: pin UA chrome to the brand's single mode (adaptive emits nothing).
|
|
671
|
+
...singleMode ? [`color-scheme: ${singleMode};`] : [],
|
|
604
672
|
`background: var(--surface-page, var(--surface-background));`,
|
|
605
673
|
"color: var(--text-primary);",
|
|
606
674
|
"font-family: var(--font-sans);"
|
|
@@ -650,9 +718,9 @@ function docsAdapter(input, options) {
|
|
|
650
718
|
}
|
|
651
719
|
}
|
|
652
720
|
}
|
|
653
|
-
if (darkPrimitiveOverrides.length > 0) {
|
|
721
|
+
if (darkPrimitiveOverrides.length > 0 && emitDark) {
|
|
654
722
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 dark brand color anchors at shade 500"));
|
|
655
|
-
lines.push(block2(
|
|
723
|
+
lines.push(block2(darkSel, darkPrimitiveOverrides));
|
|
656
724
|
lines.push("");
|
|
657
725
|
}
|
|
658
726
|
const categories = ["Text", "Surface", "Border", "Interactive"];
|
|
@@ -662,10 +730,12 @@ function docsAdapter(input, options) {
|
|
|
662
730
|
Object.entries(input.tokens.border).map(([n, v]) => `--border-${n}: ${v.dark};`),
|
|
663
731
|
Object.entries(input.tokens.interactive).map(([n, v]) => `--interactive-${n}: ${v.dark};`)
|
|
664
732
|
];
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
733
|
+
if (emitDark) {
|
|
734
|
+
for (let i = 0; i < categories.length; i++) {
|
|
735
|
+
lines.push(sectionComment2(`Adaptive: ${categories[i]} (dark)${darkSuffix}`));
|
|
736
|
+
lines.push(block2(darkSel, categoryDecls[i]));
|
|
737
|
+
lines.push("");
|
|
738
|
+
}
|
|
669
739
|
}
|
|
670
740
|
const pcsCategories = [
|
|
671
741
|
{ label: "Text", entries: Object.entries(input.tokens.text).map(([n, v]) => `--text-${n}: ${v.dark};`) },
|
|
@@ -673,15 +743,17 @@ function docsAdapter(input, options) {
|
|
|
673
743
|
{ label: "Border", entries: Object.entries(input.tokens.border).map(([n, v]) => `--border-${n}: ${v.dark};`) },
|
|
674
744
|
{ label: "Interactive", entries: Object.entries(input.tokens.interactive).map(([n, v]) => `--interactive-${n}: ${v.dark};`) }
|
|
675
745
|
];
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
746
|
+
if (emitPrefers) {
|
|
747
|
+
for (const cat of pcsCategories) {
|
|
748
|
+
lines.push(sectionComment2(`Adaptive: ${cat.label} (dark) \u2014 prefers-color-scheme`));
|
|
749
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, cat.entries);
|
|
750
|
+
lines.push(`@media (prefers-color-scheme: dark) {
|
|
680
751
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
681
752
|
}`);
|
|
682
|
-
|
|
753
|
+
lines.push("");
|
|
754
|
+
}
|
|
683
755
|
}
|
|
684
|
-
if (darkPrimitiveOverrides.length > 0) {
|
|
756
|
+
if (darkPrimitiveOverrides.length > 0 && emitPrefers) {
|
|
685
757
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 prefers-color-scheme"));
|
|
686
758
|
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, darkPrimitiveOverrides);
|
|
687
759
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
@@ -696,17 +768,23 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
696
768
|
{ label: "Border", entries: Object.entries(input.tokens.border).map(([n, v]) => `--border-${n}: ${v.light};`) },
|
|
697
769
|
{ label: "Interactive", entries: Object.entries(input.tokens.interactive).map(([n, v]) => `--interactive-${n}: ${v.light};`) }
|
|
698
770
|
];
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
771
|
+
if (emitLight) {
|
|
772
|
+
for (const cat of lightCategoryDecls) {
|
|
773
|
+
lines.push(sectionComment2(`Adaptive: ${cat.label} (light)`));
|
|
774
|
+
lines.push(block2(lightSel, cat.entries));
|
|
775
|
+
lines.push("");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (emitDark) {
|
|
779
|
+
lines.push(sectionComment2("Fumadocs bridge: dark"));
|
|
780
|
+
lines.push(block2(darkSel, generateFumadocsBridgeDecls(input.tokens, "dark")));
|
|
781
|
+
lines.push("");
|
|
782
|
+
}
|
|
783
|
+
if (emitLight) {
|
|
784
|
+
lines.push(sectionComment2("Fumadocs bridge: light"));
|
|
785
|
+
lines.push(block2(lightSel, generateFumadocsBridgeDecls(input.tokens, "light")));
|
|
702
786
|
lines.push("");
|
|
703
787
|
}
|
|
704
|
-
lines.push(sectionComment2("Fumadocs bridge: dark"));
|
|
705
|
-
lines.push(block2(`.dark ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "dark")));
|
|
706
|
-
lines.push("");
|
|
707
|
-
lines.push(sectionComment2("Fumadocs bridge: light"));
|
|
708
|
-
lines.push(block2(`html:not(.dark) ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "light")));
|
|
709
|
-
lines.push("");
|
|
710
788
|
const semanticLines = [];
|
|
711
789
|
semanticLines.push("\n/* \u2500\u2500 Layer: Semantic aliases (VI-451) \u2500\u2500 */");
|
|
712
790
|
semanticLines.push(sectionComment2("Discrete: Text size aliases (--text-N)"));
|
|
@@ -715,42 +793,49 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
715
793
|
semanticLines.push(sectionComment2("Discrete: Space aliases (--space-N)"));
|
|
716
794
|
semanticLines.push(block2(scopeClass, generateSpaceAliasDecls(input.config)));
|
|
717
795
|
semanticLines.push("");
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
796
|
+
if (emitLight) {
|
|
797
|
+
semanticLines.push(sectionComment2("Intent aliases (light)"));
|
|
798
|
+
semanticLines.push(block2(lightSel, generateIntentDecls(input.tokens, "light")));
|
|
799
|
+
semanticLines.push("");
|
|
800
|
+
semanticLines.push(sectionComment2("Hairline aliases (light)"));
|
|
801
|
+
semanticLines.push(block2(lightSel, generateHairlineDecls(input.tokens, "light")));
|
|
802
|
+
semanticLines.push("");
|
|
803
|
+
}
|
|
804
|
+
if (emitDark) {
|
|
805
|
+
semanticLines.push(sectionComment2(`Intent aliases (dark)${darkSuffix}`));
|
|
806
|
+
semanticLines.push(block2(darkSel, generateIntentDecls(input.tokens, "dark")));
|
|
807
|
+
semanticLines.push("");
|
|
808
|
+
semanticLines.push(sectionComment2(`Hairline aliases (dark)${darkSuffix}`));
|
|
809
|
+
semanticLines.push(block2(darkSel, generateHairlineDecls(input.tokens, "dark")));
|
|
810
|
+
semanticLines.push("");
|
|
811
|
+
}
|
|
812
|
+
if (emitPrefers) {
|
|
813
|
+
semanticLines.push(sectionComment2("Intent aliases (dark) \u2014 prefers-color-scheme"));
|
|
814
|
+
{
|
|
815
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateIntentDecls(input.tokens, "dark"));
|
|
816
|
+
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
734
817
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
735
818
|
}`);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
819
|
+
}
|
|
820
|
+
semanticLines.push("");
|
|
821
|
+
semanticLines.push(sectionComment2("Hairline aliases (dark) \u2014 prefers-color-scheme"));
|
|
822
|
+
{
|
|
823
|
+
const inner = block2(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateHairlineDecls(input.tokens, "dark"));
|
|
824
|
+
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
742
825
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
743
826
|
}`);
|
|
827
|
+
}
|
|
828
|
+
semanticLines.push("");
|
|
744
829
|
}
|
|
745
|
-
|
|
746
|
-
const brandResult = resolveThemeBrand(input.config.brand, { scope: scopeClass });
|
|
830
|
+
const brandResult = resolveThemeBrand(input.config.brand, { scope: scopeClass, colorScheme });
|
|
747
831
|
const passthroughCss = generateBrandPassthroughCss(
|
|
748
832
|
collectBrandPassthrough(input.tokens, input.config.overrides),
|
|
749
833
|
{
|
|
750
|
-
light:
|
|
751
|
-
dark:
|
|
834
|
+
light: lightSel,
|
|
835
|
+
dark: darkSel,
|
|
752
836
|
prefers: `${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`
|
|
753
|
-
}
|
|
837
|
+
},
|
|
838
|
+
colorScheme
|
|
754
839
|
);
|
|
755
840
|
const adaptiveLayer = wrapInLayer("visor-adaptive", lines.join("\n").trim());
|
|
756
841
|
const semanticLayer = wrapInLayer("visor-semantic", semanticLines.join("\n").trim());
|
|
@@ -829,7 +829,7 @@ function block(selector, decls) {
|
|
|
829
829
|
if (decls.length === 0) return "";
|
|
830
830
|
return [`${selector} {`, ...decls.map((d) => ` ${d}`), "}"].join("\n");
|
|
831
831
|
}
|
|
832
|
-
function generateBrandCSS(resolutions, scope) {
|
|
832
|
+
function generateBrandCSS(resolutions, scope, colorScheme = "adaptive") {
|
|
833
833
|
if (resolutions.length === 0) return "";
|
|
834
834
|
const baseSelector = scope ? scope : ":root";
|
|
835
835
|
const lightSelector = scope ? `html:not(.dark) ${scope}` : ":root";
|
|
@@ -840,6 +840,18 @@ function generateBrandCSS(resolutions, scope) {
|
|
|
840
840
|
lines.push("/* --- Brand: forced-mode aliases + tokens --- */");
|
|
841
841
|
lines.push(block(baseSelector, staticDecls));
|
|
842
842
|
lines.push("");
|
|
843
|
+
if (colorScheme === "dark-only") {
|
|
844
|
+
const darkDecls2 = resolutions.map((r) => modeDecl(r, "dark"));
|
|
845
|
+
lines.push("/* --- Brand: variants (dark) \u2014 host --- */");
|
|
846
|
+
lines.push(block(baseSelector, darkDecls2));
|
|
847
|
+
return lines.join("\n").trim();
|
|
848
|
+
}
|
|
849
|
+
if (colorScheme === "light-only") {
|
|
850
|
+
const lightDecls2 = resolutions.map((r) => modeDecl(r, "light"));
|
|
851
|
+
lines.push("/* --- Brand: variants (light) \u2014 host --- */");
|
|
852
|
+
lines.push(block(baseSelector, lightDecls2));
|
|
853
|
+
return lines.join("\n").trim();
|
|
854
|
+
}
|
|
843
855
|
const lightDecls = resolutions.map((r) => modeDecl(r, "light"));
|
|
844
856
|
lines.push("/* --- Brand: variants (light) --- */");
|
|
845
857
|
lines.push(block(lightSelector, lightDecls));
|
|
@@ -860,6 +872,7 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
860
872
|
function resolveThemeBrand(brand, options) {
|
|
861
873
|
const effective = brand ?? DEFAULT_VISOR_BRAND;
|
|
862
874
|
const scope = options?.scope ?? "";
|
|
875
|
+
const colorScheme = options?.colorScheme ?? "adaptive";
|
|
863
876
|
const source = resolveBrandSource(effective);
|
|
864
877
|
const org = effective.org ?? null;
|
|
865
878
|
const cdnBase = effective["cdn-overrides"]?.["visor-brands"] ?? null;
|
|
@@ -881,7 +894,7 @@ function resolveThemeBrand(brand, options) {
|
|
|
881
894
|
if (resolution.guidance) warnings.push(resolution.guidance);
|
|
882
895
|
}
|
|
883
896
|
}
|
|
884
|
-
const css = generateBrandCSS([...variants, ...custom], scope);
|
|
897
|
+
const css = generateBrandCSS([...variants, ...custom], scope, colorScheme);
|
|
885
898
|
return { variants, custom, css, warnings };
|
|
886
899
|
}
|
|
887
900
|
|
|
@@ -1730,9 +1743,15 @@ function buildAdaptiveDecls(tokens, theme) {
|
|
|
1730
1743
|
return { textDecls, surfaceDecls, borderDecls, interactiveDecls, intentDecls, hairlineDecls };
|
|
1731
1744
|
}
|
|
1732
1745
|
function generateLightCss(tokens, options) {
|
|
1746
|
+
const colorScheme = options?.colorScheme ?? "adaptive";
|
|
1747
|
+
if (colorScheme === "dark-only") return "";
|
|
1733
1748
|
const lines = [];
|
|
1734
1749
|
const { textDecls, surfaceDecls, borderDecls, interactiveDecls, intentDecls, hairlineDecls } = buildAdaptiveDecls(tokens, "light");
|
|
1735
1750
|
const host = options?.scopePrefix ?? ":root";
|
|
1751
|
+
if (colorScheme === "light-only") {
|
|
1752
|
+
lines.push(sectionComment("Adaptive: color-scheme (light)"));
|
|
1753
|
+
lines.push(block2(host, ["color-scheme: light;"]));
|
|
1754
|
+
}
|
|
1736
1755
|
lines.push(sectionComment("Adaptive: Text (light)"));
|
|
1737
1756
|
lines.push(block2(host, textDecls));
|
|
1738
1757
|
lines.push(sectionComment("Adaptive: Surface (light)"));
|
|
@@ -1748,8 +1767,28 @@ function generateLightCss(tokens, options) {
|
|
|
1748
1767
|
return header("Visor Theme \u2014 Light") + lines.join("\n");
|
|
1749
1768
|
}
|
|
1750
1769
|
function generateDarkCss(tokens, options) {
|
|
1770
|
+
const colorScheme = options?.colorScheme ?? "adaptive";
|
|
1771
|
+
if (colorScheme === "light-only") return "";
|
|
1751
1772
|
const lines = [];
|
|
1752
1773
|
const { textDecls, surfaceDecls, borderDecls, interactiveDecls, intentDecls, hairlineDecls } = buildAdaptiveDecls(tokens, "dark");
|
|
1774
|
+
if (colorScheme === "dark-only") {
|
|
1775
|
+
const host = options?.scopePrefix ?? ":root";
|
|
1776
|
+
lines.push(sectionComment("Adaptive: color-scheme (dark)"));
|
|
1777
|
+
lines.push(block2(host, ["color-scheme: dark;"]));
|
|
1778
|
+
lines.push(sectionComment("Adaptive: Text (dark) \u2014 host"));
|
|
1779
|
+
lines.push(block2(host, textDecls));
|
|
1780
|
+
lines.push(sectionComment("Adaptive: Surface (dark) \u2014 host"));
|
|
1781
|
+
lines.push(block2(host, surfaceDecls));
|
|
1782
|
+
lines.push(sectionComment("Adaptive: Border (dark) \u2014 host"));
|
|
1783
|
+
lines.push(block2(host, borderDecls));
|
|
1784
|
+
lines.push(sectionComment("Adaptive: Interactive (dark) \u2014 host"));
|
|
1785
|
+
lines.push(block2(host, interactiveDecls));
|
|
1786
|
+
lines.push(sectionComment("Adaptive: Intent aliases (dark) \u2014 host"));
|
|
1787
|
+
lines.push(block2(host, intentDecls));
|
|
1788
|
+
lines.push(sectionComment("Adaptive: Hairline aliases (dark) \u2014 host"));
|
|
1789
|
+
lines.push(block2(host, hairlineDecls));
|
|
1790
|
+
return header("Visor Theme \u2014 Dark") + lines.join("\n");
|
|
1791
|
+
}
|
|
1753
1792
|
const prefix = options?.scopePrefix;
|
|
1754
1793
|
const darkSelectors = prefix ? [`${prefix}.dark`, `${prefix}.theme-dark`, `${prefix}[data-theme="dark"]`] : [".dark", ".theme-dark", '[data-theme="dark"]'];
|
|
1755
1794
|
const darkSelector = darkSelectors.join(",\n");
|
|
@@ -1829,6 +1868,29 @@ function generateFullBundleCss(primitives, tokens, config) {
|
|
|
1829
1868
|
);
|
|
1830
1869
|
const primitivesBody = generatePrimitivesCss(primitives, config).split("\n").slice(6).join("\n");
|
|
1831
1870
|
lines.push(primitivesBody);
|
|
1871
|
+
const colorScheme = config["color-scheme"] ?? "adaptive";
|
|
1872
|
+
if (colorScheme === "dark-only") {
|
|
1873
|
+
lines.push(
|
|
1874
|
+
"/* ============================================",
|
|
1875
|
+
" Tier 3: Adaptive \u2014 Dark Theme (:root host) + color-scheme: dark",
|
|
1876
|
+
" ============================================ */"
|
|
1877
|
+
);
|
|
1878
|
+
lines.push(
|
|
1879
|
+
generateDarkCss(tokens, { colorScheme }).split("\n").slice(6).join("\n")
|
|
1880
|
+
);
|
|
1881
|
+
return lines.join("\n");
|
|
1882
|
+
}
|
|
1883
|
+
if (colorScheme === "light-only") {
|
|
1884
|
+
lines.push(
|
|
1885
|
+
"/* ============================================",
|
|
1886
|
+
" Tier 3: Adaptive \u2014 Light Theme (:root host) + color-scheme: light",
|
|
1887
|
+
" ============================================ */"
|
|
1888
|
+
);
|
|
1889
|
+
lines.push(
|
|
1890
|
+
generateLightCss(tokens, { colorScheme }).split("\n").slice(6).join("\n")
|
|
1891
|
+
);
|
|
1892
|
+
return lines.join("\n");
|
|
1893
|
+
}
|
|
1832
1894
|
lines.push(
|
|
1833
1895
|
"/* ============================================",
|
|
1834
1896
|
" Tier 3: Adaptive \u2014 Light Theme (:root)",
|
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, g as BrandStrategy, h as BrandStrategyContext, i as BrandStrategyIssue, j as BrandStrategyValidationResult, S as SerializedBrandStrategy, R as ResolvedThemeConfig, k as GeneratedPrimitives, l as ThemeOutput, m as ThemeData, n as VisorThemeConfig, o as FullShadeScale,
|
|
2
|
-
export {
|
|
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, C as ColorScheme, f as ThemeBrandResult, g as BrandStrategy, h as BrandStrategyContext, i as BrandStrategyIssue, j as BrandStrategyValidationResult, S as SerializedBrandStrategy, R as ResolvedThemeConfig, k as GeneratedPrimitives, l as ThemeOutput, m as ThemeData, n as VisorThemeConfig, o as FullShadeScale, p as ColorRole, q as SelectiveShadeScale, r as RGB, P as ParsedColor, O as OKLCH, s as SemanticTokens, t as ShadeStep } from './types-Cvm7vFwe.js';
|
|
2
|
+
export { u as BRAND_VARIANTS, v as BRAND_VISIBILITIES, w as BrandAccessibility, x as BrandArchetype, y as BrandBoilerplate, z as BrandColorPairing, A as BrandColorUsage, D as BrandContrastTarget, E as BrandGoverns, H as BrandLexiconEntry, I as BrandMessaging, J as BrandPersonalityTrait, K as BrandPillar, L as BrandPositioning, M as BrandStrategyIssueSeverity, N as BrandToneEntry, Q as BrandVariant, U as BrandVisibility, W as BrandVoice, X as BrandVoiceTrait, Y as ColorFormat, Z as DEFAULT_BRAND_STRATEGY_SURFACES, _ as DEFAULT_BRAND_STRATEGY_TONE_STATES, $ as FontSource, a0 as GOVERNS_WILDCARD, a1 as RGBA, a2 as SemanticTokenValue } from './types-Cvm7vFwe.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Font resolver — maps font family names to loadable font resources.
|
|
@@ -234,6 +234,7 @@ declare function resolveBrandSource(brand: VisorBrand): BrandSource;
|
|
|
234
234
|
*/
|
|
235
235
|
declare function resolveThemeBrand(brand: VisorBrand | undefined, options?: {
|
|
236
236
|
scope?: string;
|
|
237
|
+
colorScheme?: ColorScheme;
|
|
237
238
|
}): ThemeBrandResult;
|
|
238
239
|
|
|
239
240
|
/**
|
|
@@ -390,6 +391,15 @@ var properties = {
|
|
|
390
391
|
],
|
|
391
392
|
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."
|
|
392
393
|
},
|
|
394
|
+
"color-scheme": {
|
|
395
|
+
type: "string",
|
|
396
|
+
"enum": [
|
|
397
|
+
"dark-only",
|
|
398
|
+
"light-only",
|
|
399
|
+
"adaptive"
|
|
400
|
+
],
|
|
401
|
+
description: "Brand constraint declaring which color modes the theme supports. 'dark-only'/'light-only' lock the brand to a single mode; 'adaptive' supports both. Complements 'default-mode' (the runtime default) — 'color-scheme' is authoritative for the brand-lock. Defaults to 'adaptive' when omitted. Optional."
|
|
402
|
+
},
|
|
393
403
|
colors: {
|
|
394
404
|
type: "object",
|
|
395
405
|
description: "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
|
|
@@ -1588,6 +1598,7 @@ declare function clampToSrgb(L: number, C: number, H: number): RGB;
|
|
|
1588
1598
|
* Convert OKLCH to hex, with gamut clamping.
|
|
1589
1599
|
*/
|
|
1590
1600
|
declare function oklchToHex(L: number, C: number, H: number): string;
|
|
1601
|
+
declare function getLuminance(r: number, g: number, b: number): number;
|
|
1591
1602
|
declare function getContrastRatio(color1: string | ParsedColor, color2: string | ParsedColor, compositeBackground?: RGB): number;
|
|
1592
1603
|
/** Parse a hex color string into a ParsedColor. */
|
|
1593
1604
|
declare function parseHex(str: string): ParsedColor | null;
|
|
@@ -1764,9 +1775,11 @@ declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config:
|
|
|
1764
1775
|
declare function generateSemanticCss(tokens: SemanticTokens): string;
|
|
1765
1776
|
declare function generateLightCss(tokens: SemanticTokens, options?: {
|
|
1766
1777
|
scopePrefix?: string;
|
|
1778
|
+
colorScheme?: ColorScheme;
|
|
1767
1779
|
}): string;
|
|
1768
1780
|
declare function generateDarkCss(tokens: SemanticTokens, options?: {
|
|
1769
1781
|
scopePrefix?: string;
|
|
1782
|
+
colorScheme?: ColorScheme;
|
|
1770
1783
|
}): string;
|
|
1771
1784
|
declare function generateFullBundleCss(primitives: GeneratedPrimitives, tokens: SemanticTokens, config: ResolvedThemeConfig): string;
|
|
1772
1785
|
|
|
@@ -1845,4 +1858,4 @@ declare function cleanFontValue(val: string): string;
|
|
|
1845
1858
|
*/
|
|
1846
1859
|
declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
|
|
1847
1860
|
|
|
1848
|
-
export { type BrandPassthrough, BrandResolution, BrandSlot, BrandSource, BrandStrategy, BrandStrategyContext, BrandStrategyIssue, BrandStrategyValidationResult, 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, SerializedBrandStrategy, 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, checkBrandStrategyCoherence, checkBrandStrategyStructure, clampToSrgb, cleanFontValue, collectBrandPassthrough, compositeOverBackground, exportTheme, extractFromCSS, formatFontCoverageError, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, getKnownTokenRefs, 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, serializeBrandStrategy, serializeColor, validate, validateBrandStrategy, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
|
|
1861
|
+
export { type BrandPassthrough, BrandResolution, BrandSlot, BrandSource, BrandStrategy, BrandStrategyContext, BrandStrategyIssue, BrandStrategyValidationResult, 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, SerializedBrandStrategy, 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, checkBrandStrategyCoherence, checkBrandStrategyStructure, clampToSrgb, cleanFontValue, collectBrandPassthrough, compositeOverBackground, exportTheme, extractFromCSS, formatFontCoverageError, generateDarkCss, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, getKnownTokenRefs, getLuminance, 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, serializeBrandStrategy, serializeColor, validate, validateBrandStrategy, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
rgbToHex,
|
|
47
47
|
rgbToOklch,
|
|
48
48
|
serializeColor
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-BUWBBUFG.js";
|
|
50
50
|
|
|
51
51
|
// src/fonts/validate-coverage.ts
|
|
52
52
|
var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
|
|
@@ -628,6 +628,11 @@ var visor_theme_schema_default = {
|
|
|
628
628
|
enum: ["light", "dark"],
|
|
629
629
|
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."
|
|
630
630
|
},
|
|
631
|
+
"color-scheme": {
|
|
632
|
+
type: "string",
|
|
633
|
+
enum: ["dark-only", "light-only", "adaptive"],
|
|
634
|
+
description: "Brand constraint declaring which color modes the theme supports. 'dark-only'/'light-only' lock the brand to a single mode; 'adaptive' supports both. Complements 'default-mode' (the runtime default) \u2014 'color-scheme' is authoritative for the brand-lock. Defaults to 'adaptive' when omitted. Optional."
|
|
635
|
+
},
|
|
631
636
|
colors: {
|
|
632
637
|
type: "object",
|
|
633
638
|
description: "Color definitions for light mode. Only primary is required \u2014 all others have sensible defaults.",
|
|
@@ -1294,6 +1299,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
1294
1299
|
"group",
|
|
1295
1300
|
"label",
|
|
1296
1301
|
"default-mode",
|
|
1302
|
+
"color-scheme",
|
|
1297
1303
|
"colors",
|
|
1298
1304
|
"colors-dark",
|
|
1299
1305
|
"typography",
|
|
@@ -1574,6 +1580,12 @@ function validateConfig(config) {
|
|
|
1574
1580
|
errors.push("'default-mode' must be either 'dark' or 'light'");
|
|
1575
1581
|
}
|
|
1576
1582
|
}
|
|
1583
|
+
if (obj["color-scheme"] !== void 0) {
|
|
1584
|
+
const scheme = obj["color-scheme"];
|
|
1585
|
+
if (scheme !== "dark-only" && scheme !== "light-only" && scheme !== "adaptive") {
|
|
1586
|
+
errors.push("'color-scheme' must be one of: dark-only, light-only, adaptive");
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1577
1589
|
if (typeof obj.colors !== "object" || obj.colors === null) {
|
|
1578
1590
|
errors.push("'colors' is required and must be an object");
|
|
1579
1591
|
return { valid: false, errors };
|
|
@@ -1863,6 +1875,9 @@ function resolveConfig(config) {
|
|
|
1863
1875
|
name: config.name,
|
|
1864
1876
|
...config.label !== void 0 && { label: config.label },
|
|
1865
1877
|
...config["default-mode"] !== void 0 && { "default-mode": config["default-mode"] },
|
|
1878
|
+
// BO-55: always-resolved brand constraint — existing themes (no field) resolve
|
|
1879
|
+
// to 'adaptive', preserving back-compat.
|
|
1880
|
+
"color-scheme": config["color-scheme"] ?? "adaptive",
|
|
1866
1881
|
version: 1,
|
|
1867
1882
|
colors: {
|
|
1868
1883
|
primary: colors.primary,
|
|
@@ -2555,8 +2570,8 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
2555
2570
|
const output = {
|
|
2556
2571
|
primitivesCss: generatePrimitivesCss(primitives, resolved),
|
|
2557
2572
|
semanticCss: generateSemanticCss(tokens),
|
|
2558
|
-
lightCss: generateLightCss(tokens),
|
|
2559
|
-
darkCss: generateDarkCss(tokens),
|
|
2573
|
+
lightCss: generateLightCss(tokens, { colorScheme: resolved["color-scheme"] }),
|
|
2574
|
+
darkCss: generateDarkCss(tokens, { colorScheme: resolved["color-scheme"] }),
|
|
2560
2575
|
fullBundleCss: generateFullBundleCss(primitives, tokens, resolved)
|
|
2561
2576
|
};
|
|
2562
2577
|
return { config: resolved, primitives, tokens, output };
|
|
@@ -3903,6 +3918,36 @@ function extractBackgroundSurface(declarations) {
|
|
|
3903
3918
|
}
|
|
3904
3919
|
return result;
|
|
3905
3920
|
}
|
|
3921
|
+
function detectExplicitColorScheme(files) {
|
|
3922
|
+
for (const file of files) {
|
|
3923
|
+
const content = file.content.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3924
|
+
const re = /(?<![\w-])color-scheme\s*:\s*([^;}]+)/gi;
|
|
3925
|
+
let m;
|
|
3926
|
+
while ((m = re.exec(content)) !== null) {
|
|
3927
|
+
const keywords = m[1].trim().toLowerCase().split(/\s+/).filter((k) => k.length > 0 && k !== "only");
|
|
3928
|
+
const hasDark = keywords.includes("dark");
|
|
3929
|
+
const hasLight = keywords.includes("light");
|
|
3930
|
+
if (hasDark && hasLight) return "adaptive";
|
|
3931
|
+
if (hasDark) return "dark-only";
|
|
3932
|
+
if (hasLight) return "light-only";
|
|
3933
|
+
if (keywords.includes("normal")) return "adaptive";
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return null;
|
|
3937
|
+
}
|
|
3938
|
+
function detectColorScheme(files, bgSurface, warnings) {
|
|
3939
|
+
const explicit = detectExplicitColorScheme(files);
|
|
3940
|
+
const heuristic = bgSurface.dark.background && !bgSurface.light.background ? "dark-only" : "adaptive";
|
|
3941
|
+
if (explicit) {
|
|
3942
|
+
if (heuristic !== "adaptive" && heuristic !== explicit) {
|
|
3943
|
+
warnings.push(
|
|
3944
|
+
`color-scheme ambiguity: source CSS explicitly declares 'color-scheme: ${explicit}' but the background heuristic suggests '${heuristic}'. Using the explicit declaration \u2014 review the source if this is unexpected.`
|
|
3945
|
+
);
|
|
3946
|
+
}
|
|
3947
|
+
return explicit;
|
|
3948
|
+
}
|
|
3949
|
+
return heuristic;
|
|
3950
|
+
}
|
|
3906
3951
|
function extractFromCSS(files, name = "extracted-theme") {
|
|
3907
3952
|
const warnings = [];
|
|
3908
3953
|
const allDeclarations = [];
|
|
@@ -3976,6 +4021,7 @@ function extractFromCSS(files, name = "extracted-theme") {
|
|
|
3976
4021
|
if (Object.keys(darkColors).length > 0) {
|
|
3977
4022
|
config["colors-dark"] = darkColors;
|
|
3978
4023
|
}
|
|
4024
|
+
config["color-scheme"] = detectColorScheme(files, bgSurface, warnings);
|
|
3979
4025
|
if (typography.heading || typography.display || typography.body || typography.mono) {
|
|
3980
4026
|
config.typography = {};
|
|
3981
4027
|
if (typography.heading) config.typography.heading = typography.heading;
|
|
@@ -4046,6 +4092,7 @@ export {
|
|
|
4046
4092
|
generateThemeFromConfig,
|
|
4047
4093
|
getContrastRatio,
|
|
4048
4094
|
getKnownTokenRefs,
|
|
4095
|
+
getLuminance,
|
|
4049
4096
|
googleFontsCatalog,
|
|
4050
4097
|
hasBrandPassthrough,
|
|
4051
4098
|
hexToOklch,
|
|
@@ -483,6 +483,11 @@ interface TextSlotOverride {
|
|
|
483
483
|
/** Letter spacing in logical pixels (Flutter `TextStyle.letterSpacing`). */
|
|
484
484
|
"letter-spacing"?: number;
|
|
485
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* BO-55/BO-56: brand color-mode constraint. `dark-only`/`light-only` lock the
|
|
488
|
+
* brand to a single mode; `adaptive` supports both (the historical default).
|
|
489
|
+
*/
|
|
490
|
+
type ColorScheme = "dark-only" | "light-only" | "adaptive";
|
|
486
491
|
interface VisorThemeConfig {
|
|
487
492
|
name: string;
|
|
488
493
|
version: 1;
|
|
@@ -492,6 +497,13 @@ interface VisorThemeConfig {
|
|
|
492
497
|
label?: string;
|
|
493
498
|
/** Default color mode to force when the theme is activated ('dark' or 'light'). If unset, user/system preference applies. */
|
|
494
499
|
"default-mode"?: "dark" | "light";
|
|
500
|
+
/**
|
|
501
|
+
* BO-55: Brand constraint declaring which color modes the theme supports.
|
|
502
|
+
* 'dark-only'/'light-only' lock the brand to a single mode; 'adaptive' supports both.
|
|
503
|
+
* Complements `default-mode` (the runtime default) — `color-scheme` is authoritative
|
|
504
|
+
* for the brand-lock. Defaults to 'adaptive' when omitted.
|
|
505
|
+
*/
|
|
506
|
+
"color-scheme"?: ColorScheme;
|
|
495
507
|
colors: {
|
|
496
508
|
primary: string;
|
|
497
509
|
accent?: string;
|
|
@@ -674,6 +686,12 @@ interface ResolvedThemeConfig {
|
|
|
674
686
|
label?: string;
|
|
675
687
|
/** Default color mode forwarded from VisorThemeConfig["default-mode"]. */
|
|
676
688
|
"default-mode"?: "dark" | "light";
|
|
689
|
+
/**
|
|
690
|
+
* BO-55: Brand constraint for supported color modes. Always resolved (defaults
|
|
691
|
+
* to 'adaptive' when the theme omits it) so downstream readers (engine/extractor/gate)
|
|
692
|
+
* get a guaranteed value.
|
|
693
|
+
*/
|
|
694
|
+
"color-scheme": ColorScheme;
|
|
677
695
|
version: 1;
|
|
678
696
|
colors: {
|
|
679
697
|
primary: string;
|
|
@@ -836,4 +854,4 @@ interface ThemeData {
|
|
|
836
854
|
output: ThemeOutput;
|
|
837
855
|
}
|
|
838
856
|
|
|
839
|
-
export {
|
|
857
|
+
export { type FontSource as $, type BrandColorUsage as A, type BrandSlot as B, type ColorScheme as C, type BrandContrastTarget as D, type BrandGoverns as E, type FontResolveOptions as F, type GoogleFontEntry as G, type BrandLexiconEntry as H, type BrandMessaging as I, type BrandPersonalityTrait as J, type BrandPillar as K, type BrandPositioning as L, type BrandStrategyIssueSeverity as M, type BrandToneEntry as N, type OKLCH as O, type ParsedColor as P, type BrandVariant as Q, type ResolvedThemeConfig as R, type SerializedBrandStrategy as S, type ThemeFontResult as T, type BrandVisibility as U, type VisorTypography as V, type BrandVoice as W, type BrandVoiceTrait as X, type ColorFormat as Y, DEFAULT_BRAND_STRATEGY_SURFACES as Z, DEFAULT_BRAND_STRATEGY_TONE_STATES as _, type FontResolution as a, GOVERNS_WILDCARD as a0, type RGBA as a1, type SemanticTokenValue as a2, type FontDisplayStrategy as b, type VisorBrand as c, type BrandSource as d, type BrandResolution as e, type ThemeBrandResult as f, type BrandStrategy as g, type BrandStrategyContext as h, type BrandStrategyIssue as i, type BrandStrategyValidationResult as j, type GeneratedPrimitives as k, type ThemeOutput as l, type ThemeData as m, type VisorThemeConfig as n, type FullShadeScale as o, type ColorRole as p, type SelectiveShadeScale as q, type RGB as r, type SemanticTokens as s, type ShadeStep as t, BRAND_VARIANTS as u, BRAND_VISIBILITIES as v, type BrandAccessibility as w, type BrandArchetype as x, type BrandBoilerplate as y, type BrandColorPairing as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"enum": ["light", "dark"],
|
|
30
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
31
|
},
|
|
32
|
+
"color-scheme": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["dark-only", "light-only", "adaptive"],
|
|
35
|
+
"description": "Brand constraint declaring which color modes the theme supports. 'dark-only'/'light-only' lock the brand to a single mode; 'adaptive' supports both. Complements 'default-mode' (the runtime default) — 'color-scheme' is authoritative for the brand-lock. Defaults to 'adaptive' when omitted. Optional."
|
|
36
|
+
},
|
|
32
37
|
"colors": {
|
|
33
38
|
"type": "object",
|
|
34
39
|
"description": "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
|