@loworbitstudio/visor-theme-engine 0.13.0 → 0.14.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 +73 -6
- package/dist/{chunk-KFTTL3XP.js → chunk-YDRQQIOB.js} +88 -0
- package/dist/index.d.ts +56 -4
- package/dist/index.js +36 -76
- package/dist/{types-CSO2avFQ.d.ts → types-BDRXkldG.d.ts} +2 -0
- package/package.json +1 -1
- package/src/visor-theme.schema.json +9 -0
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-BDRXkldG.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Adapter types for the Visor theme engine.
|
package/dist/adapters/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SELECTIVE_SHADE_STEPS,
|
|
5
5
|
aliasFamily,
|
|
6
6
|
buildVisorFontUrl,
|
|
7
|
+
collectBrandPassthrough,
|
|
7
8
|
fontStack,
|
|
8
9
|
generateDarkCss,
|
|
9
10
|
generateHairlineDecls,
|
|
@@ -18,7 +19,53 @@ import {
|
|
|
18
19
|
resolveThemeBrand,
|
|
19
20
|
resolveThemeFonts,
|
|
20
21
|
sectionComment
|
|
21
|
-
} from "../chunk-
|
|
22
|
+
} from "../chunk-YDRQQIOB.js";
|
|
23
|
+
|
|
24
|
+
// src/adapters/brand-passthrough.ts
|
|
25
|
+
var SENTINEL_COLOR = "#ff00ff";
|
|
26
|
+
function isDevBuild() {
|
|
27
|
+
return process.env.NODE_ENV !== "production";
|
|
28
|
+
}
|
|
29
|
+
function isUnresolved(value) {
|
|
30
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
31
|
+
}
|
|
32
|
+
function declFor(key, value) {
|
|
33
|
+
if (isUnresolved(value)) {
|
|
34
|
+
if (isDevBuild()) {
|
|
35
|
+
return `--${key}: ${SENTINEL_COLOR}; /* [visor-brand] UNRESOLVED pass-through value */`;
|
|
36
|
+
}
|
|
37
|
+
return `--${key}: ${value};`;
|
|
38
|
+
}
|
|
39
|
+
return `--${key}: ${value};`;
|
|
40
|
+
}
|
|
41
|
+
function indentBlock(selector, decls) {
|
|
42
|
+
if (decls.length === 0) return "";
|
|
43
|
+
return [selector + " {", ...decls.map((d) => ` ${d}`), "}"].join("\n");
|
|
44
|
+
}
|
|
45
|
+
function generateBrandPassthroughCss(passthrough, selectors) {
|
|
46
|
+
const lightKeys = Object.keys(passthrough.light);
|
|
47
|
+
const darkKeys = Object.keys(passthrough.dark);
|
|
48
|
+
if (lightKeys.length === 0 && darkKeys.length === 0) return "";
|
|
49
|
+
const blocks = [];
|
|
50
|
+
if (isDevBuild()) {
|
|
51
|
+
const names = [.../* @__PURE__ */ new Set([...lightKeys, ...darkKeys])].map((k) => `--${k}`).join(", ");
|
|
52
|
+
const count = lightKeys.length + darkKeys.length;
|
|
53
|
+
blocks.push(`/* [visor-brand] ${count} passthrough: ${names} */`);
|
|
54
|
+
}
|
|
55
|
+
if (lightKeys.length > 0) {
|
|
56
|
+
const decls = lightKeys.map((k) => declFor(k, passthrough.light[k]));
|
|
57
|
+
blocks.push(indentBlock(selectors.light, decls));
|
|
58
|
+
}
|
|
59
|
+
if (darkKeys.length > 0) {
|
|
60
|
+
const decls = darkKeys.map((k) => declFor(k, passthrough.dark[k]));
|
|
61
|
+
blocks.push(indentBlock(selectors.dark, decls));
|
|
62
|
+
const prefersInner = indentBlock(selectors.prefers, decls).split("\n").map((l) => ` ${l}`).join("\n");
|
|
63
|
+
blocks.push(`@media (prefers-color-scheme: dark) {
|
|
64
|
+
${prefersInner}
|
|
65
|
+
}`);
|
|
66
|
+
}
|
|
67
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
68
|
+
}
|
|
22
69
|
|
|
23
70
|
// src/adapters/layers.ts
|
|
24
71
|
var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;";
|
|
@@ -102,6 +149,17 @@ function nextjsAdapter(input, options) {
|
|
|
102
149
|
);
|
|
103
150
|
lines.push(wrapInLayer("visor-primitives", primitivesBody));
|
|
104
151
|
lines.push("");
|
|
152
|
+
const passthrough = collectBrandPassthrough(input.tokens, input.config.overrides);
|
|
153
|
+
const darkSelectors = scopePrefix ? [`${scopePrefix}.dark`, `${scopePrefix}.theme-dark`, `${scopePrefix}[data-theme="dark"]`] : [".dark", ".theme-dark", '[data-theme="dark"]'];
|
|
154
|
+
const passthroughCss = generateBrandPassthroughCss(passthrough, {
|
|
155
|
+
light: scopePrefix ?? ":root",
|
|
156
|
+
dark: darkSelectors.join(",\n"),
|
|
157
|
+
prefers: scopePrefix ? `${scopePrefix}:not(.light):not(.theme-light):not([data-theme="light"])` : ':root:not(.light):not(.theme-light):not([data-theme="light"])'
|
|
158
|
+
});
|
|
159
|
+
if (passthroughCss) {
|
|
160
|
+
lines.push(wrapInLayer("visor-brand", passthroughCss));
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
105
163
|
const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix }));
|
|
106
164
|
const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix }));
|
|
107
165
|
lines.push(
|
|
@@ -562,7 +620,7 @@ function docsAdapter(input, options) {
|
|
|
562
620
|
];
|
|
563
621
|
for (const cat of pcsCategories) {
|
|
564
622
|
lines.push(sectionComment2(`Adaptive: ${cat.label} (dark) \u2014 prefers-color-scheme`));
|
|
565
|
-
const inner = block(`${scopeClass}:not(.light)`, cat.entries);
|
|
623
|
+
const inner = block(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, cat.entries);
|
|
566
624
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
567
625
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
568
626
|
}`);
|
|
@@ -570,7 +628,7 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
570
628
|
}
|
|
571
629
|
if (darkPrimitiveOverrides.length > 0) {
|
|
572
630
|
lines.push(sectionComment2("Primitive overrides (dark) \u2014 prefers-color-scheme"));
|
|
573
|
-
const inner = block(`${scopeClass}:not(.light)`, darkPrimitiveOverrides);
|
|
631
|
+
const inner = block(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, darkPrimitiveOverrides);
|
|
574
632
|
lines.push(`@media (prefers-color-scheme: dark) {
|
|
575
633
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
576
634
|
}`);
|
|
@@ -616,7 +674,7 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
616
674
|
semanticLines.push("");
|
|
617
675
|
semanticLines.push(sectionComment2("Intent aliases (dark) \u2014 prefers-color-scheme"));
|
|
618
676
|
{
|
|
619
|
-
const inner = block(`${scopeClass}:not(.light)`, generateIntentDecls(input.tokens, "dark"));
|
|
677
|
+
const inner = block(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateIntentDecls(input.tokens, "dark"));
|
|
620
678
|
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
621
679
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
622
680
|
}`);
|
|
@@ -624,16 +682,25 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
|
624
682
|
semanticLines.push("");
|
|
625
683
|
semanticLines.push(sectionComment2("Hairline aliases (dark) \u2014 prefers-color-scheme"));
|
|
626
684
|
{
|
|
627
|
-
const inner = block(`${scopeClass}:not(.light)`, generateHairlineDecls(input.tokens, "dark"));
|
|
685
|
+
const inner = block(`${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`, generateHairlineDecls(input.tokens, "dark"));
|
|
628
686
|
semanticLines.push(`@media (prefers-color-scheme: dark) {
|
|
629
687
|
${inner.split("\n").map((l) => ` ${l}`).join("\n")}
|
|
630
688
|
}`);
|
|
631
689
|
}
|
|
632
690
|
semanticLines.push("");
|
|
633
691
|
const brandResult = resolveThemeBrand(input.config.brand, { scope: scopeClass });
|
|
692
|
+
const passthroughCss = generateBrandPassthroughCss(
|
|
693
|
+
collectBrandPassthrough(input.tokens, input.config.overrides),
|
|
694
|
+
{
|
|
695
|
+
light: `html:not(.dark) ${scopeClass}`,
|
|
696
|
+
dark: `.dark ${scopeClass}`,
|
|
697
|
+
prefers: `${scopeClass}:not(.light):not(.theme-light):not([data-theme="light"])`
|
|
698
|
+
}
|
|
699
|
+
);
|
|
634
700
|
const adaptiveLayer = wrapInLayer("visor-adaptive", lines.join("\n").trim());
|
|
635
701
|
const semanticLayer = wrapInLayer("visor-semantic", semanticLines.join("\n").trim());
|
|
636
|
-
const
|
|
702
|
+
const brandLayerBody = [brandResult.css, passthroughCss].filter(Boolean).join("\n\n");
|
|
703
|
+
const brandLayer = wrapInLayer("visor-brand", brandLayerBody);
|
|
637
704
|
const head = fontLines.length > 0 ? fontLines.join("\n") + "\n" : "";
|
|
638
705
|
const layerBlocks = [semanticLayer, brandLayer, adaptiveLayer].filter(Boolean);
|
|
639
706
|
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) {
|
|
@@ -1822,6 +1907,9 @@ export {
|
|
|
1822
1907
|
SELECTIVE_SHADE_STEPS,
|
|
1823
1908
|
TAILWIND_GRAY,
|
|
1824
1909
|
generateShadeScale,
|
|
1910
|
+
collectBrandPassthrough,
|
|
1911
|
+
hasBrandPassthrough,
|
|
1912
|
+
applyOverrides,
|
|
1825
1913
|
aliasFamily,
|
|
1826
1914
|
fontStack,
|
|
1827
1915
|
header,
|
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-BDRXkldG.js';
|
|
2
|
+
export { o as BRAND_VARIANTS, p as BrandVariant, q as ColorFormat, r as FontSource, s as RGBA, t as SemanticTokenValue } from './types-BDRXkldG.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.",
|
|
@@ -946,6 +958,21 @@ interface ThemeValidationResult {
|
|
|
946
958
|
errors: ValidationIssue[];
|
|
947
959
|
warnings: ValidationIssue[];
|
|
948
960
|
}
|
|
961
|
+
/**
|
|
962
|
+
* Options for the validate() function.
|
|
963
|
+
*/
|
|
964
|
+
interface ValidateOptions {
|
|
965
|
+
/**
|
|
966
|
+
* When true, promote DARK_LIGHT_PARITY warnings and the
|
|
967
|
+
* "colors.neutral present without colors-dark.neutral" check
|
|
968
|
+
* from warning to error. Use this in CI to enforce the
|
|
969
|
+
* "always both modes" authoring convention.
|
|
970
|
+
*
|
|
971
|
+
* Opt-in today; flip to the default after all convergent
|
|
972
|
+
* themes add their dark neutral (see VI-495 docs).
|
|
973
|
+
*/
|
|
974
|
+
strictDark?: boolean;
|
|
975
|
+
}
|
|
949
976
|
/**
|
|
950
977
|
* Validate a theme config comprehensively.
|
|
951
978
|
*
|
|
@@ -953,9 +980,10 @@ interface ThemeValidationResult {
|
|
|
953
980
|
* Results are JSON-serializable for CLI `--json` output.
|
|
954
981
|
*
|
|
955
982
|
* @param config - A parsed theme config object (from YAML or programmatic)
|
|
983
|
+
* @param options - Optional validator flags (e.g. strictDark)
|
|
956
984
|
* @returns ThemeValidationResult with errors[], warnings[], and valid boolean
|
|
957
985
|
*/
|
|
958
|
-
declare function validate(config: unknown): ThemeValidationResult;
|
|
986
|
+
declare function validate(config: unknown, options?: ValidateOptions): ThemeValidationResult;
|
|
959
987
|
|
|
960
988
|
/**
|
|
961
989
|
* Shade Scale Generation
|
|
@@ -1062,6 +1090,30 @@ declare function assignSemanticTokens(lightPrimitives: GeneratedPrimitives, dark
|
|
|
1062
1090
|
* replacing derived token values with user-specified values.
|
|
1063
1091
|
*/
|
|
1064
1092
|
|
|
1093
|
+
/** Pass-through brand tokens collected per mode (VI-493). */
|
|
1094
|
+
interface BrandPassthrough {
|
|
1095
|
+
light: Record<string, string>;
|
|
1096
|
+
dark: Record<string, string>;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Collect unrecognized override keys into a brand-passthrough map (VI-493).
|
|
1100
|
+
*
|
|
1101
|
+
* Any `overrides.{light,dark}` key that does NOT map to a recognized semantic,
|
|
1102
|
+
* intent, or hairline token is captured here verbatim (key + value). These were
|
|
1103
|
+
* previously DROPPED silently by `applyOverrides`; the adapters now emit them as
|
|
1104
|
+
* bare `--<key>` custom properties inside `@layer visor-brand`, ending the
|
|
1105
|
+
* dual-source-of-truth between `.visor.yaml` and hand-maintained `:root` blocks.
|
|
1106
|
+
*
|
|
1107
|
+
* Recognized tokens are excluded — they continue to flow through the normal
|
|
1108
|
+
* semantic pipeline. Pass-through tokens are legitimately mode-asymmetric (a key
|
|
1109
|
+
* may appear in `light` only, `dark` only, or both); no both-modes rule applies.
|
|
1110
|
+
*/
|
|
1111
|
+
declare function collectBrandPassthrough(tokens: SemanticTokens, overrides?: {
|
|
1112
|
+
light?: Record<string, string>;
|
|
1113
|
+
dark?: Record<string, string>;
|
|
1114
|
+
}): BrandPassthrough;
|
|
1115
|
+
/** True when the passthrough map carries at least one token in either mode. */
|
|
1116
|
+
declare function hasBrandPassthrough(passthrough: BrandPassthrough): boolean;
|
|
1065
1117
|
/**
|
|
1066
1118
|
* Apply override values to semantic tokens.
|
|
1067
1119
|
* Returns a new SemanticTokens with overrides applied (does not mutate input).
|
|
@@ -1221,4 +1273,4 @@ declare function cleanFontValue(val: string): string;
|
|
|
1221
1273
|
*/
|
|
1222
1274
|
declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
|
|
1223
1275
|
|
|
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 };
|
|
1276
|
+
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,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
generateStylesheetLinks,
|
|
22
24
|
getContrastRatio,
|
|
23
25
|
googleFontsCatalog,
|
|
26
|
+
hasBrandPassthrough,
|
|
24
27
|
hexToOklch,
|
|
25
28
|
hexToRgb,
|
|
26
29
|
isValidColor,
|
|
@@ -42,7 +45,7 @@ import {
|
|
|
42
45
|
rgbToHex,
|
|
43
46
|
rgbToOklch,
|
|
44
47
|
serializeColor
|
|
45
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-YDRQQIOB.js";
|
|
46
49
|
|
|
47
50
|
// src/fonts/validate-coverage.ts
|
|
48
51
|
var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
|
|
@@ -224,6 +227,15 @@ var visor_theme_schema_default = {
|
|
|
224
227
|
type: "string",
|
|
225
228
|
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
229
|
},
|
|
230
|
+
label: {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: "Human-readable display name for the theme (e.g. 'Blacklight Pro'). Overrides the name-derived label in the docs theme switcher. Optional."
|
|
233
|
+
},
|
|
234
|
+
"default-mode": {
|
|
235
|
+
type: "string",
|
|
236
|
+
enum: ["light", "dark"],
|
|
237
|
+
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."
|
|
238
|
+
},
|
|
227
239
|
colors: {
|
|
228
240
|
type: "object",
|
|
229
241
|
description: "Color definitions for light mode. Only primary is required \u2014 all others have sensible defaults.",
|
|
@@ -1165,6 +1177,7 @@ function resolveConfig(config) {
|
|
|
1165
1177
|
return {
|
|
1166
1178
|
name: config.name,
|
|
1167
1179
|
...config.label !== void 0 && { label: config.label },
|
|
1180
|
+
...config["default-mode"] !== void 0 && { "default-mode": config["default-mode"] },
|
|
1168
1181
|
version: 1,
|
|
1169
1182
|
colors: {
|
|
1170
1183
|
primary: colors.primary,
|
|
@@ -1586,11 +1599,14 @@ var SEMANTIC_INTENT_MAP = {
|
|
|
1586
1599
|
light: { role: "primary", shade: 500 },
|
|
1587
1600
|
dark: { role: "primary", shade: 500 }
|
|
1588
1601
|
},
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1602
|
+
// Single-source alias of --interactive-primary-text. Default white (same value
|
|
1603
|
+
// as the interactive group); themes that need a different value (e.g. entr)
|
|
1604
|
+
// override via overrides.{light,dark}["primary-text"] which replaces this alias
|
|
1605
|
+
// with the explicit override value. Hand-authored static CSS (blackout-theme.css,
|
|
1606
|
+
// neutral-theme.css) should consume --primary-text via this alias path.
|
|
1591
1607
|
"primary-text": {
|
|
1592
|
-
light: { constant: "
|
|
1593
|
-
dark: { constant: "
|
|
1608
|
+
light: { constant: "var(--interactive-primary-text)" },
|
|
1609
|
+
dark: { constant: "var(--interactive-primary-text)" }
|
|
1594
1610
|
},
|
|
1595
1611
|
accent: {
|
|
1596
1612
|
light: { role: "accent", shade: 500 },
|
|
@@ -1695,71 +1711,6 @@ function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
|
1695
1711
|
return { text, surface, border, interactive, intent, hairline };
|
|
1696
1712
|
}
|
|
1697
1713
|
|
|
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
1714
|
// src/pipeline.ts
|
|
1764
1715
|
function generatePrimitives(config) {
|
|
1765
1716
|
return {
|
|
@@ -2361,7 +2312,7 @@ function checkOverrides(config, issues) {
|
|
|
2361
2312
|
issue(
|
|
2362
2313
|
"warning",
|
|
2363
2314
|
"UNKNOWN_OVERRIDE_KEY",
|
|
2364
|
-
`'overrides.${mode}.${key}' does not match any known semantic token.
|
|
2315
|
+
`'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
2316
|
`overrides.${mode}.${key}`
|
|
2366
2317
|
)
|
|
2367
2318
|
);
|
|
@@ -2642,14 +2593,15 @@ function checkRadiusScale(config, issues) {
|
|
|
2642
2593
|
);
|
|
2643
2594
|
}
|
|
2644
2595
|
}
|
|
2645
|
-
function checkDarkLightParity(config, issues) {
|
|
2596
|
+
function checkDarkLightParity(config, issues, opts) {
|
|
2646
2597
|
if (!config.colors) return;
|
|
2647
2598
|
const colorKeys = Object.keys(config.colors).filter((k) => k !== "primary");
|
|
2648
2599
|
const hasDarkSection = config["colors-dark"] !== void 0;
|
|
2649
2600
|
if (colorKeys.length > 0 && !hasDarkSection) {
|
|
2601
|
+
const severity = opts.strictDark ? "error" : "warning";
|
|
2650
2602
|
issues.push(
|
|
2651
2603
|
issue(
|
|
2652
|
-
|
|
2604
|
+
severity,
|
|
2653
2605
|
"DARK_LIGHT_PARITY",
|
|
2654
2606
|
"Custom colors are set but no colors-dark section exists. Dark mode will use generated defaults which may not match your brand.",
|
|
2655
2607
|
"colors-dark"
|
|
@@ -2663,9 +2615,10 @@ function checkDarkLightParity(config, issues) {
|
|
|
2663
2615
|
for (const key of lightKeys) {
|
|
2664
2616
|
if (key === "primary") continue;
|
|
2665
2617
|
if (!darkKeys.has(key)) {
|
|
2618
|
+
const severity = opts.strictDark ? "error" : "warning";
|
|
2666
2619
|
issues.push(
|
|
2667
2620
|
issue(
|
|
2668
|
-
|
|
2621
|
+
severity,
|
|
2669
2622
|
"DARK_LIGHT_PARITY",
|
|
2670
2623
|
`Color "${key}" is set in colors but missing from colors-dark. Dark mode will use a generated default.`,
|
|
2671
2624
|
"colors-dark"
|
|
@@ -2687,7 +2640,8 @@ function checkDarkLightParity(config, issues) {
|
|
|
2687
2640
|
}
|
|
2688
2641
|
}
|
|
2689
2642
|
}
|
|
2690
|
-
function validate(config) {
|
|
2643
|
+
function validate(config, options) {
|
|
2644
|
+
const opts = options || {};
|
|
2691
2645
|
const errors = [];
|
|
2692
2646
|
const warnings = [];
|
|
2693
2647
|
const structurallyValid = checkStructuralIntegrity(config, errors);
|
|
@@ -2719,7 +2673,11 @@ function validate(config) {
|
|
|
2719
2673
|
checkColorSimilarity(typedConfig, warnings);
|
|
2720
2674
|
checkMissingGlowShadow(typedConfig, warnings);
|
|
2721
2675
|
checkRadiusScale(typedConfig, warnings);
|
|
2722
|
-
|
|
2676
|
+
const parityIssues = [];
|
|
2677
|
+
checkDarkLightParity(typedConfig, parityIssues, opts);
|
|
2678
|
+
for (const iss of parityIssues) {
|
|
2679
|
+
(iss.severity === "error" ? errors : warnings).push(iss);
|
|
2680
|
+
}
|
|
2723
2681
|
}
|
|
2724
2682
|
return {
|
|
2725
2683
|
valid: errors.length === 0,
|
|
@@ -3229,6 +3187,7 @@ export {
|
|
|
3229
3187
|
buildVisorFontUrl,
|
|
3230
3188
|
clampToSrgb,
|
|
3231
3189
|
cleanFontValue,
|
|
3190
|
+
collectBrandPassthrough,
|
|
3232
3191
|
compositeOverBackground,
|
|
3233
3192
|
exportTheme,
|
|
3234
3193
|
extractFromCSS,
|
|
@@ -3248,6 +3207,7 @@ export {
|
|
|
3248
3207
|
generateThemeFromConfig,
|
|
3249
3208
|
getContrastRatio,
|
|
3250
3209
|
googleFontsCatalog,
|
|
3210
|
+
hasBrandPassthrough,
|
|
3251
3211
|
hexToOklch,
|
|
3252
3212
|
hexToRgb,
|
|
3253
3213
|
isValidColor,
|
|
@@ -412,6 +412,8 @@ interface ResolvedThemeConfig {
|
|
|
412
412
|
name: string;
|
|
413
413
|
/** Optional display label override forwarded from VisorThemeConfig.label. */
|
|
414
414
|
label?: string;
|
|
415
|
+
/** Default color mode forwarded from VisorThemeConfig["default-mode"]. */
|
|
416
|
+
"default-mode"?: "dark" | "light";
|
|
415
417
|
version: 1;
|
|
416
418
|
colors: {
|
|
417
419
|
primary: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.",
|