@loworbitstudio/visor-theme-engine 0.16.0 → 0.17.1

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/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, C as ColorRole, p as SelectiveShadeScale, q as RGB, P as ParsedColor, O as OKLCH, r as SemanticTokens, s as ShadeStep } from './types-BstIS9rL.js';
2
- export { t as BRAND_VARIANTS, u as BRAND_VISIBILITIES, v as BrandArchetype, w as BrandGoverns, x as BrandLexiconEntry, y as BrandPersonalityTrait, z as BrandPillar, A as BrandPositioning, D as BrandStrategyIssueSeverity, E as BrandToneEntry, H as BrandVariant, I as BrandVisibility, J as BrandVoice, K as BrandVoiceTrait, L as ColorFormat, M as DEFAULT_BRAND_STRATEGY_SURFACES, N as DEFAULT_BRAND_STRATEGY_TONE_STATES, Q as FontSource, U as GOVERNS_WILDCARD, W as RGBA, X as SemanticTokenValue } from './types-BstIS9rL.js';
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
  /**
@@ -291,8 +292,11 @@ declare function validateBrandStrategy(block: unknown, ctx?: BrandStrategyContex
291
292
  /**
292
293
  * Serialize a validated brand strategy for the manifest. Returns `null` when
293
294
  * `visibility: private`. Optional fields (`archetype.secondary`,
294
- * `voice.traits[].example`, absent `governs` lists) are emitted only when
295
- * present, so the agent reads a clean, predictable object.
295
+ * `voice.traits[].example`, absent `governs` lists, the Phase 2 wave-1 fields
296
+ * `pillars[].proof` / `messaging` / `taglines` / `boilerplate` / `colorUsage` /
297
+ * `accessibility`) are emitted only when present, so the agent reads a clean,
298
+ * predictable object. The Phase 2 fields are all PUBLIC — a private record
299
+ * never reaches this point, so visibility is enforced once, up top (D4).
296
300
  */
297
301
  declare function serializeBrandStrategy(strategy: BrandStrategy): SerializedBrandStrategy | null;
298
302
 
@@ -387,6 +391,15 @@ var properties = {
387
391
  ],
388
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."
389
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
+ },
390
403
  colors: {
391
404
  type: "object",
392
405
  description: "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
@@ -856,6 +869,27 @@ var properties = {
856
869
  "private"
857
870
  ],
858
871
  description: "Client brands are private and are omitted from the public agent manifest."
872
+ },
873
+ messaging: {
874
+ $ref: "#/$defs/brandMessaging"
875
+ },
876
+ taglines: {
877
+ type: "array",
878
+ items: {
879
+ type: "string",
880
+ minLength: 1
881
+ },
882
+ minItems: 1,
883
+ description: "Phase 2 wave-1 (VI-541). Permanent, brand-level signature line(s) — who the brand is, not what it sells this quarter."
884
+ },
885
+ boilerplate: {
886
+ $ref: "#/$defs/brandBoilerplate"
887
+ },
888
+ colorUsage: {
889
+ $ref: "#/$defs/brandColorUsage"
890
+ },
891
+ accessibility: {
892
+ $ref: "#/$defs/brandAccessibility"
859
893
  }
860
894
  }
861
895
  },
@@ -1208,6 +1242,15 @@ var $defs = {
1208
1242
  },
1209
1243
  governs: {
1210
1244
  $ref: "#/$defs/brandGoverns"
1245
+ },
1246
+ proof: {
1247
+ type: "array",
1248
+ items: {
1249
+ type: "string",
1250
+ minLength: 1
1251
+ },
1252
+ minItems: 1,
1253
+ description: "Phase 2 wave-1 (VI-541). Reasons-to-believe — checkable evidence backing this pillar's claim (the message-house foundation)."
1211
1254
  }
1212
1255
  }
1213
1256
  },
@@ -1298,6 +1341,134 @@ var $defs = {
1298
1341
  minLength: 1
1299
1342
  }
1300
1343
  }
1344
+ },
1345
+ brandMessaging: {
1346
+ type: "object",
1347
+ description: "Phase 2 wave-1 (VI-541). Message-house roof — the single umbrella message above the pillars.",
1348
+ additionalProperties: false,
1349
+ required: [
1350
+ "roof"
1351
+ ],
1352
+ properties: {
1353
+ roof: {
1354
+ type: "string",
1355
+ minLength: 1,
1356
+ description: "One overarching statement the pillars support."
1357
+ }
1358
+ }
1359
+ },
1360
+ brandBoilerplate: {
1361
+ type: "object",
1362
+ description: "Phase 2 wave-1 (VI-541). Reusable \"about us\" copy — short and long forms.",
1363
+ additionalProperties: false,
1364
+ required: [
1365
+ "short",
1366
+ "long"
1367
+ ],
1368
+ properties: {
1369
+ short: {
1370
+ type: "string",
1371
+ minLength: 1
1372
+ },
1373
+ long: {
1374
+ type: "string",
1375
+ minLength: 1
1376
+ }
1377
+ }
1378
+ },
1379
+ brandColorPairing: {
1380
+ type: "object",
1381
+ description: "A color-pairing rule expressed as brand intent (not a computed value).",
1382
+ additionalProperties: false,
1383
+ required: [
1384
+ "use",
1385
+ "with",
1386
+ "rule"
1387
+ ],
1388
+ properties: {
1389
+ use: {
1390
+ type: "string",
1391
+ minLength: 1,
1392
+ description: "The token or role being used (e.g. --primary)."
1393
+ },
1394
+ "with": {
1395
+ type: "string",
1396
+ minLength: 1,
1397
+ description: "What it pairs against (token, role, or surface)."
1398
+ },
1399
+ rule: {
1400
+ type: "string",
1401
+ minLength: 1,
1402
+ description: "The intent — when and how the pairing is allowed."
1403
+ }
1404
+ }
1405
+ },
1406
+ brandColorUsage: {
1407
+ type: "object",
1408
+ description: "Phase 2 wave-1 (VI-541). Color-usage intent — the brand's allowed pairings.",
1409
+ additionalProperties: false,
1410
+ required: [
1411
+ "pairings"
1412
+ ],
1413
+ properties: {
1414
+ pairings: {
1415
+ type: "array",
1416
+ items: {
1417
+ $ref: "#/$defs/brandColorPairing"
1418
+ },
1419
+ minItems: 1
1420
+ }
1421
+ }
1422
+ },
1423
+ brandContrastTarget: {
1424
+ type: "object",
1425
+ description: "A contrast target expressed as brand intent (a WCAG 2.1 AA threshold).",
1426
+ additionalProperties: false,
1427
+ required: [
1428
+ "context",
1429
+ "ratio"
1430
+ ],
1431
+ properties: {
1432
+ context: {
1433
+ type: "string",
1434
+ minLength: 1,
1435
+ description: "The text/UI context the target applies to."
1436
+ },
1437
+ ratio: {
1438
+ type: "string",
1439
+ minLength: 1,
1440
+ description: "The minimum contrast ratio (e.g. \"4.5:1\")."
1441
+ }
1442
+ }
1443
+ },
1444
+ brandAccessibility: {
1445
+ type: "object",
1446
+ description: "Phase 2 wave-1 (VI-541). Accessibility intent — the standard and its contrast targets (authored as intent; live computation is the render surface's concern).",
1447
+ additionalProperties: false,
1448
+ required: [
1449
+ "standard",
1450
+ "contrast",
1451
+ "intent"
1452
+ ],
1453
+ properties: {
1454
+ standard: {
1455
+ type: "string",
1456
+ minLength: 1,
1457
+ description: "The conformance standard. Visor targets \"WCAG 2.1 AA\"."
1458
+ },
1459
+ contrast: {
1460
+ type: "array",
1461
+ items: {
1462
+ $ref: "#/$defs/brandContrastTarget"
1463
+ },
1464
+ minItems: 1
1465
+ },
1466
+ intent: {
1467
+ type: "string",
1468
+ minLength: 1,
1469
+ description: "How the brand applies the standard (intent, not computed results)."
1470
+ }
1471
+ }
1301
1472
  }
1302
1473
  };
1303
1474
  var visorTheme_schema = {
@@ -1427,6 +1598,7 @@ declare function clampToSrgb(L: number, C: number, H: number): RGB;
1427
1598
  * Convert OKLCH to hex, with gamut clamping.
1428
1599
  */
1429
1600
  declare function oklchToHex(L: number, C: number, H: number): string;
1601
+ declare function getLuminance(r: number, g: number, b: number): number;
1430
1602
  declare function getContrastRatio(color1: string | ParsedColor, color2: string | ParsedColor, compositeBackground?: RGB): number;
1431
1603
  /** Parse a hex color string into a ParsedColor. */
1432
1604
  declare function parseHex(str: string): ParsedColor | null;
@@ -1603,9 +1775,11 @@ declare function generatePrimitivesCss(primitives: GeneratedPrimitives, config:
1603
1775
  declare function generateSemanticCss(tokens: SemanticTokens): string;
1604
1776
  declare function generateLightCss(tokens: SemanticTokens, options?: {
1605
1777
  scopePrefix?: string;
1778
+ colorScheme?: ColorScheme;
1606
1779
  }): string;
1607
1780
  declare function generateDarkCss(tokens: SemanticTokens, options?: {
1608
1781
  scopePrefix?: string;
1782
+ colorScheme?: ColorScheme;
1609
1783
  }): string;
1610
1784
  declare function generateFullBundleCss(primitives: GeneratedPrimitives, tokens: SemanticTokens, config: ResolvedThemeConfig): string;
1611
1785
 
@@ -1684,4 +1858,4 @@ declare function cleanFontValue(val: string): string;
1684
1858
  */
1685
1859
  declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
1686
1860
 
1687
- 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-DQ256PSE.js";
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;
@@ -248,7 +248,13 @@ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
248
248
  "tone",
249
249
  "lexicon",
250
250
  "core",
251
- "visibility"
251
+ "visibility",
252
+ // Phase 2 wave-1 (VI-541) — all optional.
253
+ "messaging",
254
+ "taglines",
255
+ "boilerplate",
256
+ "colorUsage",
257
+ "accessibility"
252
258
  ]);
253
259
  var GOVERNS_KEYS = /* @__PURE__ */ new Set(["tokens", "components", "surfaces"]);
254
260
  function checkBrandStrategyStructure(block) {
@@ -347,6 +353,64 @@ function checkBrandStrategyStructure(block) {
347
353
  if (!isNonEmptyString(block.visibility) || !BRAND_VISIBILITIES.includes(block.visibility)) {
348
354
  errors.push(error("BRAND_STRATEGY_VISIBILITY", `'brand-strategy.visibility' is required and must be one of: ${BRAND_VISIBILITIES.join(", ")}`, "brand-strategy.visibility"));
349
355
  }
356
+ errors.push(...checkPhase2Structure(block));
357
+ return errors;
358
+ }
359
+ function checkPhase2Structure(block) {
360
+ const errors = [];
361
+ if (block.messaging !== void 0) {
362
+ const messaging = block.messaging;
363
+ if (!isObject(messaging) || !isNonEmptyString(messaging.roof)) {
364
+ errors.push(error("BRAND_STRATEGY_MESSAGING", "'brand-strategy.messaging' must be an object with a non-empty 'roof' string", "brand-strategy.messaging"));
365
+ }
366
+ }
367
+ if (block.taglines !== void 0) {
368
+ if (!isStringArray(block.taglines) || block.taglines.length === 0) {
369
+ errors.push(error("BRAND_STRATEGY_TAGLINES", "'brand-strategy.taglines' must be a non-empty array of strings when present", "brand-strategy.taglines"));
370
+ } else if (!block.taglines.every(isNonEmptyString)) {
371
+ errors.push(error("BRAND_STRATEGY_TAGLINES", "'brand-strategy.taglines' entries must be non-empty strings", "brand-strategy.taglines"));
372
+ }
373
+ }
374
+ if (block.boilerplate !== void 0) {
375
+ const boilerplate = block.boilerplate;
376
+ if (!isObject(boilerplate) || !isNonEmptyString(boilerplate.short) || !isNonEmptyString(boilerplate.long)) {
377
+ errors.push(error("BRAND_STRATEGY_BOILERPLATE", "'brand-strategy.boilerplate' must be an object with non-empty 'short' and 'long' strings", "brand-strategy.boilerplate"));
378
+ }
379
+ }
380
+ if (block.colorUsage !== void 0) {
381
+ const colorUsage = block.colorUsage;
382
+ if (!isObject(colorUsage) || !Array.isArray(colorUsage.pairings) || colorUsage.pairings.length === 0) {
383
+ errors.push(error("BRAND_STRATEGY_COLOR_USAGE", "'brand-strategy.colorUsage' must be an object with a non-empty 'pairings' array", "brand-strategy.colorUsage"));
384
+ } else {
385
+ colorUsage.pairings.forEach((pairing, i) => {
386
+ if (!isObject(pairing) || !isNonEmptyString(pairing.use) || !isNonEmptyString(pairing.with) || !isNonEmptyString(pairing.rule)) {
387
+ errors.push(error("BRAND_STRATEGY_COLOR_USAGE", `'brand-strategy.colorUsage.pairings[${i}]' must have non-empty 'use', 'with', and 'rule' strings`, `brand-strategy.colorUsage.pairings[${i}]`));
388
+ }
389
+ });
390
+ }
391
+ }
392
+ if (block.accessibility !== void 0) {
393
+ const accessibility = block.accessibility;
394
+ if (!isObject(accessibility)) {
395
+ errors.push(error("BRAND_STRATEGY_ACCESSIBILITY", "'brand-strategy.accessibility' must be an object", "brand-strategy.accessibility"));
396
+ } else {
397
+ if (!isNonEmptyString(accessibility.standard)) {
398
+ errors.push(error("BRAND_STRATEGY_ACCESSIBILITY", "'brand-strategy.accessibility.standard' is required and must be a non-empty string", "brand-strategy.accessibility.standard"));
399
+ }
400
+ if (!isNonEmptyString(accessibility.intent)) {
401
+ errors.push(error("BRAND_STRATEGY_ACCESSIBILITY", "'brand-strategy.accessibility.intent' is required and must be a non-empty string", "brand-strategy.accessibility.intent"));
402
+ }
403
+ if (!Array.isArray(accessibility.contrast) || accessibility.contrast.length === 0) {
404
+ errors.push(error("BRAND_STRATEGY_ACCESSIBILITY", "'brand-strategy.accessibility.contrast' must be a non-empty array", "brand-strategy.accessibility.contrast"));
405
+ } else {
406
+ accessibility.contrast.forEach((target, i) => {
407
+ if (!isObject(target) || !isNonEmptyString(target.context) || !isNonEmptyString(target.ratio)) {
408
+ errors.push(error("BRAND_STRATEGY_ACCESSIBILITY", `'brand-strategy.accessibility.contrast[${i}]' must have non-empty 'context' and 'ratio' strings`, `brand-strategy.accessibility.contrast[${i}]`));
409
+ }
410
+ });
411
+ }
412
+ }
413
+ }
350
414
  return errors;
351
415
  }
352
416
  function checkPillarStructure(pillar, index) {
@@ -383,6 +447,13 @@ function checkPillarStructure(pillar, index) {
383
447
  errors.push(error("BRAND_STRATEGY_GOVERNS", `'${path}.governs' must declare at least one of: ${[...GOVERNS_KEYS].join(", ")}`, `${path}.governs`));
384
448
  }
385
449
  }
450
+ if (pillar.proof !== void 0) {
451
+ if (!isStringArray(pillar.proof) || pillar.proof.length === 0) {
452
+ errors.push(error("BRAND_STRATEGY_PROOF", `'${path}.proof' must be a non-empty array of strings when present`, `${path}.proof`));
453
+ } else if (!pillar.proof.every(isNonEmptyString)) {
454
+ errors.push(error("BRAND_STRATEGY_PROOF", `'${path}.proof' entries must be non-empty strings`, `${path}.proof`));
455
+ }
456
+ }
386
457
  return errors;
387
458
  }
388
459
  function checkBrandStrategyCoherence(strategy, ctx = {}) {
@@ -442,6 +513,15 @@ function compactGoverns(governs) {
442
513
  if (governs.surfaces) out.surfaces = [...governs.surfaces];
443
514
  return out;
444
515
  }
516
+ function serializePillar(p) {
517
+ const pillar = {
518
+ id: p.id,
519
+ statement: p.statement,
520
+ governs: compactGoverns(p.governs)
521
+ };
522
+ if (p.proof !== void 0) pillar.proof = [...p.proof];
523
+ return pillar;
524
+ }
445
525
  function serializeBrandStrategy(strategy) {
446
526
  if (strategy.visibility === "private") {
447
527
  return null;
@@ -453,7 +533,7 @@ function serializeBrandStrategy(strategy) {
453
533
  for (const [state, entry] of Object.entries(strategy.tone)) {
454
534
  tone[state] = { feeling: entry.feeling, example: entry.example };
455
535
  }
456
- return {
536
+ const serialized = {
457
537
  positioning: {
458
538
  onliness: strategy.positioning.onliness,
459
539
  category: strategy.positioning.category,
@@ -462,11 +542,7 @@ function serializeBrandStrategy(strategy) {
462
542
  essence: [...strategy.essence],
463
543
  personality: strategy.personality.map((p) => ({ trait: p.trait, not: p.not })),
464
544
  archetype,
465
- pillars: strategy.pillars.map((p) => ({
466
- id: p.id,
467
- statement: p.statement,
468
- governs: compactGoverns(p.governs)
469
- })),
545
+ pillars: strategy.pillars.map(serializePillar),
470
546
  voice: {
471
547
  traits: strategy.voice.traits.map((t) => {
472
548
  const trait = {
@@ -483,6 +559,38 @@ function serializeBrandStrategy(strategy) {
483
559
  core: [...strategy.core],
484
560
  visibility: strategy.visibility
485
561
  };
562
+ if (strategy.messaging !== void 0) {
563
+ serialized.messaging = { roof: strategy.messaging.roof };
564
+ }
565
+ if (strategy.taglines !== void 0) {
566
+ serialized.taglines = [...strategy.taglines];
567
+ }
568
+ if (strategy.boilerplate !== void 0) {
569
+ serialized.boilerplate = {
570
+ short: strategy.boilerplate.short,
571
+ long: strategy.boilerplate.long
572
+ };
573
+ }
574
+ if (strategy.colorUsage !== void 0) {
575
+ serialized.colorUsage = {
576
+ pairings: strategy.colorUsage.pairings.map((p) => ({
577
+ use: p.use,
578
+ with: p.with,
579
+ rule: p.rule
580
+ }))
581
+ };
582
+ }
583
+ if (strategy.accessibility !== void 0) {
584
+ serialized.accessibility = {
585
+ standard: strategy.accessibility.standard,
586
+ contrast: strategy.accessibility.contrast.map((c) => ({
587
+ context: c.context,
588
+ ratio: c.ratio
589
+ })),
590
+ intent: strategy.accessibility.intent
591
+ };
592
+ }
593
+ return serialized;
486
594
  }
487
595
 
488
596
  // src/pipeline.ts
@@ -520,6 +628,11 @@ var visor_theme_schema_default = {
520
628
  enum: ["light", "dark"],
521
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."
522
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
+ },
523
636
  colors: {
524
637
  type: "object",
525
638
  description: "Color definitions for light mode. Only primary is required \u2014 all others have sensible defaults.",
@@ -834,7 +947,17 @@ var visor_theme_schema_default = {
834
947
  type: "string",
835
948
  enum: ["public", "private"],
836
949
  description: "Client brands are private and are omitted from the public agent manifest."
837
- }
950
+ },
951
+ messaging: { $ref: "#/$defs/brandMessaging" },
952
+ taglines: {
953
+ type: "array",
954
+ items: { type: "string", minLength: 1 },
955
+ minItems: 1,
956
+ description: "Phase 2 wave-1 (VI-541). Permanent, brand-level signature line(s) \u2014 who the brand is, not what it sells this quarter."
957
+ },
958
+ boilerplate: { $ref: "#/$defs/brandBoilerplate" },
959
+ colorUsage: { $ref: "#/$defs/brandColorUsage" },
960
+ accessibility: { $ref: "#/$defs/brandAccessibility" }
838
961
  }
839
962
  },
840
963
  spacing: {
@@ -1056,7 +1179,13 @@ var visor_theme_schema_default = {
1056
1179
  properties: {
1057
1180
  id: { type: "string", minLength: 1 },
1058
1181
  statement: { type: "string", minLength: 1, description: "The pillar's claim in one line." },
1059
- governs: { $ref: "#/$defs/brandGoverns" }
1182
+ governs: { $ref: "#/$defs/brandGoverns" },
1183
+ proof: {
1184
+ type: "array",
1185
+ items: { type: "string", minLength: 1 },
1186
+ minItems: 1,
1187
+ description: "Phase 2 wave-1 (VI-541). Reasons-to-believe \u2014 checkable evidence backing this pillar's claim (the message-house foundation)."
1188
+ }
1060
1189
  }
1061
1190
  },
1062
1191
  brandVoice: {
@@ -1099,6 +1228,66 @@ var visor_theme_schema_default = {
1099
1228
  use: { type: "string", minLength: 1 },
1100
1229
  avoid: { type: "string", minLength: 1 }
1101
1230
  }
1231
+ },
1232
+ brandMessaging: {
1233
+ type: "object",
1234
+ description: "Phase 2 wave-1 (VI-541). Message-house roof \u2014 the single umbrella message above the pillars.",
1235
+ additionalProperties: false,
1236
+ required: ["roof"],
1237
+ properties: {
1238
+ roof: { type: "string", minLength: 1, description: "One overarching statement the pillars support." }
1239
+ }
1240
+ },
1241
+ brandBoilerplate: {
1242
+ type: "object",
1243
+ description: 'Phase 2 wave-1 (VI-541). Reusable "about us" copy \u2014 short and long forms.',
1244
+ additionalProperties: false,
1245
+ required: ["short", "long"],
1246
+ properties: {
1247
+ short: { type: "string", minLength: 1 },
1248
+ long: { type: "string", minLength: 1 }
1249
+ }
1250
+ },
1251
+ brandColorPairing: {
1252
+ type: "object",
1253
+ description: "A color-pairing rule expressed as brand intent (not a computed value).",
1254
+ additionalProperties: false,
1255
+ required: ["use", "with", "rule"],
1256
+ properties: {
1257
+ use: { type: "string", minLength: 1, description: "The token or role being used (e.g. --primary)." },
1258
+ with: { type: "string", minLength: 1, description: "What it pairs against (token, role, or surface)." },
1259
+ rule: { type: "string", minLength: 1, description: "The intent \u2014 when and how the pairing is allowed." }
1260
+ }
1261
+ },
1262
+ brandColorUsage: {
1263
+ type: "object",
1264
+ description: "Phase 2 wave-1 (VI-541). Color-usage intent \u2014 the brand's allowed pairings.",
1265
+ additionalProperties: false,
1266
+ required: ["pairings"],
1267
+ properties: {
1268
+ pairings: { type: "array", items: { $ref: "#/$defs/brandColorPairing" }, minItems: 1 }
1269
+ }
1270
+ },
1271
+ brandContrastTarget: {
1272
+ type: "object",
1273
+ description: "A contrast target expressed as brand intent (a WCAG 2.1 AA threshold).",
1274
+ additionalProperties: false,
1275
+ required: ["context", "ratio"],
1276
+ properties: {
1277
+ context: { type: "string", minLength: 1, description: "The text/UI context the target applies to." },
1278
+ ratio: { type: "string", minLength: 1, description: 'The minimum contrast ratio (e.g. "4.5:1").' }
1279
+ }
1280
+ },
1281
+ brandAccessibility: {
1282
+ type: "object",
1283
+ description: "Phase 2 wave-1 (VI-541). Accessibility intent \u2014 the standard and its contrast targets (authored as intent; live computation is the render surface's concern).",
1284
+ additionalProperties: false,
1285
+ required: ["standard", "contrast", "intent"],
1286
+ properties: {
1287
+ standard: { type: "string", minLength: 1, description: 'The conformance standard. Visor targets "WCAG 2.1 AA".' },
1288
+ contrast: { type: "array", items: { $ref: "#/$defs/brandContrastTarget" }, minItems: 1 },
1289
+ intent: { type: "string", minLength: 1, description: "How the brand applies the standard (intent, not computed results)." }
1290
+ }
1102
1291
  }
1103
1292
  }
1104
1293
  };
@@ -1110,6 +1299,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1110
1299
  "group",
1111
1300
  "label",
1112
1301
  "default-mode",
1302
+ "color-scheme",
1113
1303
  "colors",
1114
1304
  "colors-dark",
1115
1305
  "typography",
@@ -1390,6 +1580,12 @@ function validateConfig(config) {
1390
1580
  errors.push("'default-mode' must be either 'dark' or 'light'");
1391
1581
  }
1392
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
+ }
1393
1589
  if (typeof obj.colors !== "object" || obj.colors === null) {
1394
1590
  errors.push("'colors' is required and must be an object");
1395
1591
  return { valid: false, errors };
@@ -1679,6 +1875,9 @@ function resolveConfig(config) {
1679
1875
  name: config.name,
1680
1876
  ...config.label !== void 0 && { label: config.label },
1681
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",
1682
1881
  version: 1,
1683
1882
  colors: {
1684
1883
  primary: colors.primary,
@@ -2371,8 +2570,8 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
2371
2570
  const output = {
2372
2571
  primitivesCss: generatePrimitivesCss(primitives, resolved),
2373
2572
  semanticCss: generateSemanticCss(tokens),
2374
- lightCss: generateLightCss(tokens),
2375
- darkCss: generateDarkCss(tokens),
2573
+ lightCss: generateLightCss(tokens, { colorScheme: resolved["color-scheme"] }),
2574
+ darkCss: generateDarkCss(tokens, { colorScheme: resolved["color-scheme"] }),
2376
2575
  fullBundleCss: generateFullBundleCss(primitives, tokens, resolved)
2377
2576
  };
2378
2577
  return { config: resolved, primitives, tokens, output };
@@ -3719,6 +3918,36 @@ function extractBackgroundSurface(declarations) {
3719
3918
  }
3720
3919
  return result;
3721
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
+ }
3722
3951
  function extractFromCSS(files, name = "extracted-theme") {
3723
3952
  const warnings = [];
3724
3953
  const allDeclarations = [];
@@ -3792,6 +4021,7 @@ function extractFromCSS(files, name = "extracted-theme") {
3792
4021
  if (Object.keys(darkColors).length > 0) {
3793
4022
  config["colors-dark"] = darkColors;
3794
4023
  }
4024
+ config["color-scheme"] = detectColorScheme(files, bgSurface, warnings);
3795
4025
  if (typography.heading || typography.display || typography.body || typography.mono) {
3796
4026
  config.typography = {};
3797
4027
  if (typography.heading) config.typography.heading = typography.heading;
@@ -3862,6 +4092,7 @@ export {
3862
4092
  generateThemeFromConfig,
3863
4093
  getContrastRatio,
3864
4094
  getKnownTokenRefs,
4095
+ getLuminance,
3865
4096
  googleFontsCatalog,
3866
4097
  hasBrandPassthrough,
3867
4098
  hexToOklch,