@loworbitstudio/visor-theme-engine 0.12.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-B56A5DE6.js → chunk-YDRQQIOB.js} +90 -1
- package/dist/index.d.ts +59 -4
- package/dist/index.js +58 -79
- package/dist/{types-BKEkyelS.d.ts → types-BDRXkldG.d.ts} +10 -1
- package/package.json +1 -1
- package/src/visor-theme.schema.json +10 -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";
|
|
@@ -802,7 +802,8 @@ var BRAND_VARIANTS = [
|
|
|
802
802
|
"brandmark",
|
|
803
803
|
"wordmark",
|
|
804
804
|
"monochrome",
|
|
805
|
-
"favicon"
|
|
805
|
+
"favicon",
|
|
806
|
+
"animated"
|
|
806
807
|
];
|
|
807
808
|
|
|
808
809
|
// src/brand/pipeline.ts
|
|
@@ -1346,6 +1347,91 @@ function generateShadeScale(color, role) {
|
|
|
1346
1347
|
return scale;
|
|
1347
1348
|
}
|
|
1348
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
|
+
|
|
1349
1435
|
// src/fonts/theme-alias.ts
|
|
1350
1436
|
var EMPTY_ALIASES = /* @__PURE__ */ new Map();
|
|
1351
1437
|
function aliasFamily(family, themeSlug) {
|
|
@@ -1821,6 +1907,9 @@ export {
|
|
|
1821
1907
|
SELECTIVE_SHADE_STEPS,
|
|
1822
1908
|
TAILWIND_GRAY,
|
|
1823
1909
|
generateShadeScale,
|
|
1910
|
+
collectBrandPassthrough,
|
|
1911
|
+
hasBrandPassthrough,
|
|
1912
|
+
applyOverrides,
|
|
1824
1913
|
aliasFamily,
|
|
1825
1914
|
fontStack,
|
|
1826
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.",
|
|
@@ -631,6 +643,9 @@ var properties = {
|
|
|
631
643
|
favicon: {
|
|
632
644
|
$ref: "#/$defs/brandSlot"
|
|
633
645
|
},
|
|
646
|
+
animated: {
|
|
647
|
+
$ref: "#/$defs/brandSlot"
|
|
648
|
+
},
|
|
634
649
|
custom: {
|
|
635
650
|
type: "object",
|
|
636
651
|
description: "Operator-defined slots, addressed by key.",
|
|
@@ -943,6 +958,21 @@ interface ThemeValidationResult {
|
|
|
943
958
|
errors: ValidationIssue[];
|
|
944
959
|
warnings: ValidationIssue[];
|
|
945
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
|
+
}
|
|
946
976
|
/**
|
|
947
977
|
* Validate a theme config comprehensively.
|
|
948
978
|
*
|
|
@@ -950,9 +980,10 @@ interface ThemeValidationResult {
|
|
|
950
980
|
* Results are JSON-serializable for CLI `--json` output.
|
|
951
981
|
*
|
|
952
982
|
* @param config - A parsed theme config object (from YAML or programmatic)
|
|
983
|
+
* @param options - Optional validator flags (e.g. strictDark)
|
|
953
984
|
* @returns ThemeValidationResult with errors[], warnings[], and valid boolean
|
|
954
985
|
*/
|
|
955
|
-
declare function validate(config: unknown): ThemeValidationResult;
|
|
986
|
+
declare function validate(config: unknown, options?: ValidateOptions): ThemeValidationResult;
|
|
956
987
|
|
|
957
988
|
/**
|
|
958
989
|
* Shade Scale Generation
|
|
@@ -1059,6 +1090,30 @@ declare function assignSemanticTokens(lightPrimitives: GeneratedPrimitives, dark
|
|
|
1059
1090
|
* replacing derived token values with user-specified values.
|
|
1060
1091
|
*/
|
|
1061
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;
|
|
1062
1117
|
/**
|
|
1063
1118
|
* Apply override values to semantic tokens.
|
|
1064
1119
|
* Returns a new SemanticTokens with overrides applied (does not mutate input).
|
|
@@ -1218,4 +1273,4 @@ declare function cleanFontValue(val: string): string;
|
|
|
1218
1273
|
*/
|
|
1219
1274
|
declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
|
|
1220
1275
|
|
|
1221
|
-
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.",
|
|
@@ -457,6 +469,7 @@ var visor_theme_schema_default = {
|
|
|
457
469
|
wordmark: { $ref: "#/$defs/brandSlot" },
|
|
458
470
|
monochrome: { $ref: "#/$defs/brandSlot" },
|
|
459
471
|
favicon: { $ref: "#/$defs/brandSlot" },
|
|
472
|
+
animated: { $ref: "#/$defs/brandSlot" },
|
|
460
473
|
custom: {
|
|
461
474
|
type: "object",
|
|
462
475
|
description: "Operator-defined slots, addressed by key.",
|
|
@@ -705,9 +718,10 @@ var KNOWN_BRAND_KEYS = /* @__PURE__ */ new Set([
|
|
|
705
718
|
"wordmark",
|
|
706
719
|
"monochrome",
|
|
707
720
|
"favicon",
|
|
721
|
+
"animated",
|
|
708
722
|
"custom"
|
|
709
723
|
]);
|
|
710
|
-
var KNOWN_BRAND_STANDARD_SLOTS = ["logo", "brandmark", "wordmark", "monochrome", "favicon"];
|
|
724
|
+
var KNOWN_BRAND_STANDARD_SLOTS = ["logo", "brandmark", "wordmark", "monochrome", "favicon", "animated"];
|
|
711
725
|
var KNOWN_BRAND_SLOT_KEYS = /* @__PURE__ */ new Set(["slug", "formats", "light", "dark", "clearSpace", "aspectRatio"]);
|
|
712
726
|
var KNOWN_BRAND_SOURCES = /* @__PURE__ */ new Set(["visor-brands", "local"]);
|
|
713
727
|
var KNOWN_BRAND_CDN_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["visor-brands"]);
|
|
@@ -1039,6 +1053,20 @@ function validateConfig(config) {
|
|
|
1039
1053
|
}
|
|
1040
1054
|
}
|
|
1041
1055
|
}
|
|
1056
|
+
if (typeof brand.animated === "object" && brand.animated !== null) {
|
|
1057
|
+
const animated = brand.animated;
|
|
1058
|
+
if (Array.isArray(animated.formats) && !animated.formats.every(
|
|
1059
|
+
(f) => typeof f === "string" && f.toLowerCase() === "svg"
|
|
1060
|
+
)) {
|
|
1061
|
+
errors.push("'brand.animated.formats' must be SVG-only (the animated slot accepts self-contained animated SVGs only)");
|
|
1062
|
+
}
|
|
1063
|
+
for (const mode of ["light", "dark"]) {
|
|
1064
|
+
const p = animated[mode];
|
|
1065
|
+
if (typeof p === "string" && !p.toLowerCase().endsWith(".svg")) {
|
|
1066
|
+
errors.push(`'brand.animated.${mode}' must be an .svg path (the animated slot is SVG-only)`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1042
1070
|
}
|
|
1043
1071
|
}
|
|
1044
1072
|
if (obj.overrides !== void 0) {
|
|
@@ -1112,6 +1140,9 @@ function resolveBrand(brand) {
|
|
|
1112
1140
|
wordmark: brand.wordmark ?? DEFAULT_VISOR_BRAND.wordmark,
|
|
1113
1141
|
monochrome: brand.monochrome ?? DEFAULT_VISOR_BRAND.monochrome,
|
|
1114
1142
|
favicon: brand.favicon ?? DEFAULT_VISOR_BRAND.favicon,
|
|
1143
|
+
// animated is optional with no Visor default (D2): pass through only when a
|
|
1144
|
+
// theme declares it, so undeclared themes emit no --brand-animated.
|
|
1145
|
+
...brand.animated && { animated: brand.animated },
|
|
1115
1146
|
...brand.custom && { custom: brand.custom }
|
|
1116
1147
|
};
|
|
1117
1148
|
}
|
|
@@ -1146,6 +1177,7 @@ function resolveConfig(config) {
|
|
|
1146
1177
|
return {
|
|
1147
1178
|
name: config.name,
|
|
1148
1179
|
...config.label !== void 0 && { label: config.label },
|
|
1180
|
+
...config["default-mode"] !== void 0 && { "default-mode": config["default-mode"] },
|
|
1149
1181
|
version: 1,
|
|
1150
1182
|
colors: {
|
|
1151
1183
|
primary: colors.primary,
|
|
@@ -1407,7 +1439,7 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
1407
1439
|
// VI-478: status soft tints (BL-193) — alpha overlays, semantically distinct
|
|
1408
1440
|
// from the OPAQUE `surface-{status}-subtle` above (do NOT alias them together).
|
|
1409
1441
|
// Default to a color-mix of the status color so they track the theme; themes
|
|
1410
|
-
// pin exact values via overrides (blacklight-
|
|
1442
|
+
// pin exact values via overrides (blacklight-pro: success @10%,
|
|
1411
1443
|
// warning/error @12%).
|
|
1412
1444
|
"success-soft": {
|
|
1413
1445
|
light: { constant: "color-mix(in srgb, var(--color-success-500) 10%, transparent)" },
|
|
@@ -1504,7 +1536,7 @@ var SEMANTIC_INTERACTIVE_MAP = {
|
|
|
1504
1536
|
// VI-478: brand-derived alpha-overlay helpers (BL-193). `soft`/`glow` are
|
|
1505
1537
|
// alpha overlays that track the theme's primary via color-mix (distinct from
|
|
1506
1538
|
// any opaque surface); `strong` is a solid lightened-brand emphasis color.
|
|
1507
|
-
// Themes pin exact values via overrides — e.g. blacklight-
|
|
1539
|
+
// Themes pin exact values via overrides — e.g. blacklight-pro sets
|
|
1508
1540
|
// soft @12% / glow @32% / strong #FFD050.
|
|
1509
1541
|
"primary-soft": {
|
|
1510
1542
|
light: { constant: "color-mix(in srgb, var(--color-primary-500) 12%, transparent)" },
|
|
@@ -1567,11 +1599,14 @@ var SEMANTIC_INTENT_MAP = {
|
|
|
1567
1599
|
light: { role: "primary", shade: 500 },
|
|
1568
1600
|
dark: { role: "primary", shade: 500 }
|
|
1569
1601
|
},
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
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.
|
|
1572
1607
|
"primary-text": {
|
|
1573
|
-
light: { constant: "
|
|
1574
|
-
dark: { constant: "
|
|
1608
|
+
light: { constant: "var(--interactive-primary-text)" },
|
|
1609
|
+
dark: { constant: "var(--interactive-primary-text)" }
|
|
1575
1610
|
},
|
|
1576
1611
|
accent: {
|
|
1577
1612
|
light: { role: "accent", shade: 500 },
|
|
@@ -1676,71 +1711,6 @@ function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
|
1676
1711
|
return { text, surface, border, interactive, intent, hairline };
|
|
1677
1712
|
}
|
|
1678
1713
|
|
|
1679
|
-
// src/overrides.ts
|
|
1680
|
-
var TOKEN_CATEGORIES = [
|
|
1681
|
-
{ prefix: "text-", key: "text" },
|
|
1682
|
-
{ prefix: "surface-", key: "surface" },
|
|
1683
|
-
{ prefix: "border-", key: "border" },
|
|
1684
|
-
{ prefix: "interactive-", key: "interactive" },
|
|
1685
|
-
{ prefix: "hairline-", key: "hairline" }
|
|
1686
|
-
];
|
|
1687
|
-
function findToken(key, tokens) {
|
|
1688
|
-
if (key === "hairline" && "default" in tokens.hairline) {
|
|
1689
|
-
return { group: tokens.hairline, name: "default" };
|
|
1690
|
-
}
|
|
1691
|
-
for (const { prefix, key: groupKey } of TOKEN_CATEGORIES) {
|
|
1692
|
-
if (key.startsWith(prefix)) {
|
|
1693
|
-
const name = key.slice(prefix.length);
|
|
1694
|
-
if (name in tokens[groupKey]) {
|
|
1695
|
-
return { group: tokens[groupKey], name };
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
if (key in tokens.intent) {
|
|
1700
|
-
return { group: tokens.intent, name: key };
|
|
1701
|
-
}
|
|
1702
|
-
return null;
|
|
1703
|
-
}
|
|
1704
|
-
function applyOverrides(tokens, overrides) {
|
|
1705
|
-
if (!overrides) return tokens;
|
|
1706
|
-
const result = {
|
|
1707
|
-
text: { ...tokens.text },
|
|
1708
|
-
surface: { ...tokens.surface },
|
|
1709
|
-
border: { ...tokens.border },
|
|
1710
|
-
interactive: { ...tokens.interactive },
|
|
1711
|
-
intent: { ...tokens.intent },
|
|
1712
|
-
hairline: { ...tokens.hairline }
|
|
1713
|
-
};
|
|
1714
|
-
for (const group of ["text", "surface", "border", "interactive", "intent", "hairline"]) {
|
|
1715
|
-
for (const [name, value] of Object.entries(result[group])) {
|
|
1716
|
-
result[group][name] = { ...value };
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
if (overrides.light) {
|
|
1720
|
-
for (const [key, value] of Object.entries(overrides.light)) {
|
|
1721
|
-
const match = findToken(key, result);
|
|
1722
|
-
if (match) {
|
|
1723
|
-
match.group[match.name] = {
|
|
1724
|
-
...match.group[match.name],
|
|
1725
|
-
light: value
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
if (overrides.dark) {
|
|
1731
|
-
for (const [key, value] of Object.entries(overrides.dark)) {
|
|
1732
|
-
const match = findToken(key, result);
|
|
1733
|
-
if (match) {
|
|
1734
|
-
match.group[match.name] = {
|
|
1735
|
-
...match.group[match.name],
|
|
1736
|
-
dark: value
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
return result;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
1714
|
// src/pipeline.ts
|
|
1745
1715
|
function generatePrimitives(config) {
|
|
1746
1716
|
return {
|
|
@@ -2342,7 +2312,7 @@ function checkOverrides(config, issues) {
|
|
|
2342
2312
|
issue(
|
|
2343
2313
|
"warning",
|
|
2344
2314
|
"UNKNOWN_OVERRIDE_KEY",
|
|
2345
|
-
`'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.`,
|
|
2346
2316
|
`overrides.${mode}.${key}`
|
|
2347
2317
|
)
|
|
2348
2318
|
);
|
|
@@ -2623,14 +2593,15 @@ function checkRadiusScale(config, issues) {
|
|
|
2623
2593
|
);
|
|
2624
2594
|
}
|
|
2625
2595
|
}
|
|
2626
|
-
function checkDarkLightParity(config, issues) {
|
|
2596
|
+
function checkDarkLightParity(config, issues, opts) {
|
|
2627
2597
|
if (!config.colors) return;
|
|
2628
2598
|
const colorKeys = Object.keys(config.colors).filter((k) => k !== "primary");
|
|
2629
2599
|
const hasDarkSection = config["colors-dark"] !== void 0;
|
|
2630
2600
|
if (colorKeys.length > 0 && !hasDarkSection) {
|
|
2601
|
+
const severity = opts.strictDark ? "error" : "warning";
|
|
2631
2602
|
issues.push(
|
|
2632
2603
|
issue(
|
|
2633
|
-
|
|
2604
|
+
severity,
|
|
2634
2605
|
"DARK_LIGHT_PARITY",
|
|
2635
2606
|
"Custom colors are set but no colors-dark section exists. Dark mode will use generated defaults which may not match your brand.",
|
|
2636
2607
|
"colors-dark"
|
|
@@ -2644,9 +2615,10 @@ function checkDarkLightParity(config, issues) {
|
|
|
2644
2615
|
for (const key of lightKeys) {
|
|
2645
2616
|
if (key === "primary") continue;
|
|
2646
2617
|
if (!darkKeys.has(key)) {
|
|
2618
|
+
const severity = opts.strictDark ? "error" : "warning";
|
|
2647
2619
|
issues.push(
|
|
2648
2620
|
issue(
|
|
2649
|
-
|
|
2621
|
+
severity,
|
|
2650
2622
|
"DARK_LIGHT_PARITY",
|
|
2651
2623
|
`Color "${key}" is set in colors but missing from colors-dark. Dark mode will use a generated default.`,
|
|
2652
2624
|
"colors-dark"
|
|
@@ -2668,7 +2640,8 @@ function checkDarkLightParity(config, issues) {
|
|
|
2668
2640
|
}
|
|
2669
2641
|
}
|
|
2670
2642
|
}
|
|
2671
|
-
function validate(config) {
|
|
2643
|
+
function validate(config, options) {
|
|
2644
|
+
const opts = options || {};
|
|
2672
2645
|
const errors = [];
|
|
2673
2646
|
const warnings = [];
|
|
2674
2647
|
const structurallyValid = checkStructuralIntegrity(config, errors);
|
|
@@ -2700,7 +2673,11 @@ function validate(config) {
|
|
|
2700
2673
|
checkColorSimilarity(typedConfig, warnings);
|
|
2701
2674
|
checkMissingGlowShadow(typedConfig, warnings);
|
|
2702
2675
|
checkRadiusScale(typedConfig, warnings);
|
|
2703
|
-
|
|
2676
|
+
const parityIssues = [];
|
|
2677
|
+
checkDarkLightParity(typedConfig, parityIssues, opts);
|
|
2678
|
+
for (const iss of parityIssues) {
|
|
2679
|
+
(iss.severity === "error" ? errors : warnings).push(iss);
|
|
2680
|
+
}
|
|
2704
2681
|
}
|
|
2705
2682
|
return {
|
|
2706
2683
|
valid: errors.length === 0,
|
|
@@ -3210,6 +3187,7 @@ export {
|
|
|
3210
3187
|
buildVisorFontUrl,
|
|
3211
3188
|
clampToSrgb,
|
|
3212
3189
|
cleanFontValue,
|
|
3190
|
+
collectBrandPassthrough,
|
|
3213
3191
|
compositeOverBackground,
|
|
3214
3192
|
exportTheme,
|
|
3215
3193
|
extractFromCSS,
|
|
@@ -3229,6 +3207,7 @@ export {
|
|
|
3229
3207
|
generateThemeFromConfig,
|
|
3230
3208
|
getContrastRatio,
|
|
3231
3209
|
googleFontsCatalog,
|
|
3210
|
+
hasBrandPassthrough,
|
|
3232
3211
|
hexToOklch,
|
|
3233
3212
|
hexToRgb,
|
|
3234
3213
|
isValidColor,
|
|
@@ -138,7 +138,7 @@ type BrandSource = "visor-brands" | "local";
|
|
|
138
138
|
* Standard brand variant slots. A fixed set covers the common lockups; custom
|
|
139
139
|
* operator-defined slots are addressed by key through the `custom` map.
|
|
140
140
|
*/
|
|
141
|
-
type BrandVariant = "logo" | "brandmark" | "wordmark" | "monochrome" | "favicon";
|
|
141
|
+
type BrandVariant = "logo" | "brandmark" | "wordmark" | "monochrome" | "favicon" | "animated";
|
|
142
142
|
/** Ordered list of the standard brand variant slots. */
|
|
143
143
|
declare const BRAND_VARIANTS: readonly BrandVariant[];
|
|
144
144
|
/**
|
|
@@ -185,6 +185,13 @@ interface VisorBrand {
|
|
|
185
185
|
monochrome?: BrandSlot;
|
|
186
186
|
/** Favicon source. */
|
|
187
187
|
favicon?: BrandSlot;
|
|
188
|
+
/**
|
|
189
|
+
* Animated lockup. Optional and SVG-only (D2/D3): the asset must be a
|
|
190
|
+
* self-contained animated SVG (inlined `<style>`/@keyframes or SMIL) so it
|
|
191
|
+
* animates inside `<img>`. Stock themes omit this — absent → no
|
|
192
|
+
* `--brand-animated` emitted. Reduced-motion consumers fall back to `logo`.
|
|
193
|
+
*/
|
|
194
|
+
animated?: BrandSlot;
|
|
188
195
|
/** Operator-defined slots, addressed by key. */
|
|
189
196
|
custom?: Record<string, BrandSlot>;
|
|
190
197
|
}
|
|
@@ -405,6 +412,8 @@ interface ResolvedThemeConfig {
|
|
|
405
412
|
name: string;
|
|
406
413
|
/** Optional display label override forwarded from VisorThemeConfig.label. */
|
|
407
414
|
label?: string;
|
|
415
|
+
/** Default color mode forwarded from VisorThemeConfig["default-mode"]. */
|
|
416
|
+
"default-mode"?: "dark" | "light";
|
|
408
417
|
version: 1;
|
|
409
418
|
colors: {
|
|
410
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.",
|
|
@@ -253,6 +262,7 @@
|
|
|
253
262
|
"wordmark": { "$ref": "#/$defs/brandSlot" },
|
|
254
263
|
"monochrome": { "$ref": "#/$defs/brandSlot" },
|
|
255
264
|
"favicon": { "$ref": "#/$defs/brandSlot" },
|
|
265
|
+
"animated": { "$ref": "#/$defs/brandSlot" },
|
|
256
266
|
"custom": {
|
|
257
267
|
"type": "object",
|
|
258
268
|
"description": "Operator-defined slots, addressed by key.",
|