@loworbitstudio/visor-theme-engine 0.15.1 → 0.16.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/index.js CHANGED
@@ -202,6 +202,289 @@ function validateFontCoverage(css) {
202
202
  return { errors };
203
203
  }
204
204
 
205
+ // src/brand-strategy/types.ts
206
+ var DEFAULT_BRAND_STRATEGY_TONE_STATES = [
207
+ "error",
208
+ "success",
209
+ "warning",
210
+ "info",
211
+ "empty",
212
+ "loading",
213
+ "validation-warning"
214
+ ];
215
+ var DEFAULT_BRAND_STRATEGY_SURFACES = [
216
+ "manifest",
217
+ "cli",
218
+ "component-metadata"
219
+ ];
220
+ var BRAND_VISIBILITIES = ["public", "private"];
221
+ var GOVERNS_WILDCARD = "*";
222
+
223
+ // src/brand-strategy/validate.ts
224
+ function error(code, message, path) {
225
+ const issue2 = { severity: "error", code, message };
226
+ if (path !== void 0) issue2.path = path;
227
+ return issue2;
228
+ }
229
+ function isObject(value) {
230
+ return typeof value === "object" && value !== null && !Array.isArray(value);
231
+ }
232
+ function isNonEmptyString(value) {
233
+ return typeof value === "string" && value.length > 0;
234
+ }
235
+ function isStringArray(value) {
236
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
237
+ }
238
+ function normalizeTokenRef(ref) {
239
+ return ref.startsWith("--") ? ref.slice(2) : ref;
240
+ }
241
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
242
+ "positioning",
243
+ "essence",
244
+ "personality",
245
+ "archetype",
246
+ "pillars",
247
+ "voice",
248
+ "tone",
249
+ "lexicon",
250
+ "core",
251
+ "visibility"
252
+ ]);
253
+ var GOVERNS_KEYS = /* @__PURE__ */ new Set(["tokens", "components", "surfaces"]);
254
+ function checkBrandStrategyStructure(block) {
255
+ const errors = [];
256
+ if (!isObject(block)) {
257
+ errors.push(error("BRAND_STRATEGY_STRUCTURAL", "'brand-strategy' must be an object", "brand-strategy"));
258
+ return errors;
259
+ }
260
+ for (const key of Object.keys(block)) {
261
+ if (!TOP_LEVEL_KEYS.has(key)) {
262
+ errors.push(
263
+ error(
264
+ "BRAND_STRATEGY_UNKNOWN_KEY",
265
+ `Unknown key 'brand-strategy.${key}'. Valid keys: ${[...TOP_LEVEL_KEYS].join(", ")}`,
266
+ `brand-strategy.${key}`
267
+ )
268
+ );
269
+ }
270
+ }
271
+ const positioning = block.positioning;
272
+ if (!isObject(positioning)) {
273
+ errors.push(error("BRAND_STRATEGY_POSITIONING", "'brand-strategy.positioning' is required and must be an object", "brand-strategy.positioning"));
274
+ } else {
275
+ for (const field of ["onliness", "category", "differentiation"]) {
276
+ if (!isNonEmptyString(positioning[field])) {
277
+ errors.push(error("BRAND_STRATEGY_POSITIONING", `'brand-strategy.positioning.${field}' is required and must be a non-empty string`, `brand-strategy.positioning.${field}`));
278
+ }
279
+ }
280
+ }
281
+ if (!isStringArray(block.essence) || block.essence.length === 0) {
282
+ errors.push(error("BRAND_STRATEGY_ESSENCE", "'brand-strategy.essence' is required and must be a non-empty array of strings", "brand-strategy.essence"));
283
+ }
284
+ if (!Array.isArray(block.personality) || block.personality.length === 0) {
285
+ errors.push(error("BRAND_STRATEGY_PERSONALITY", "'brand-strategy.personality' is required and must be a non-empty array", "brand-strategy.personality"));
286
+ } else {
287
+ block.personality.forEach((trait, i) => {
288
+ if (!isObject(trait) || !isNonEmptyString(trait.trait) || !isNonEmptyString(trait.not)) {
289
+ errors.push(error("BRAND_STRATEGY_PERSONALITY", `'brand-strategy.personality[${i}]' must have non-empty 'trait' and 'not' strings`, `brand-strategy.personality[${i}]`));
290
+ }
291
+ });
292
+ }
293
+ const archetype = block.archetype;
294
+ if (!isObject(archetype)) {
295
+ errors.push(error("BRAND_STRATEGY_ARCHETYPE", "'brand-strategy.archetype' is required and must be an object", "brand-strategy.archetype"));
296
+ } else {
297
+ if (!isNonEmptyString(archetype.primary)) {
298
+ errors.push(error("BRAND_STRATEGY_ARCHETYPE", "'brand-strategy.archetype.primary' is required and must be a non-empty string", "brand-strategy.archetype.primary"));
299
+ }
300
+ for (const field of ["secondary", "tertiary"]) {
301
+ if (archetype[field] !== void 0 && !isNonEmptyString(archetype[field])) {
302
+ errors.push(error("BRAND_STRATEGY_ARCHETYPE", `'brand-strategy.archetype.${field}' must be a non-empty string when present`, `brand-strategy.archetype.${field}`));
303
+ }
304
+ }
305
+ }
306
+ if (!Array.isArray(block.pillars) || block.pillars.length === 0) {
307
+ errors.push(error("BRAND_STRATEGY_PILLARS", "'brand-strategy.pillars' is required and must be a non-empty array", "brand-strategy.pillars"));
308
+ } else {
309
+ block.pillars.forEach((pillar, i) => {
310
+ errors.push(...checkPillarStructure(pillar, i));
311
+ });
312
+ }
313
+ const voice = block.voice;
314
+ if (!isObject(voice) || !Array.isArray(voice.traits) || voice.traits.length === 0) {
315
+ errors.push(error("BRAND_STRATEGY_VOICE", "'brand-strategy.voice' is required and must have a non-empty 'traits' array", "brand-strategy.voice"));
316
+ } else {
317
+ voice.traits.forEach((trait, i) => {
318
+ if (!isObject(trait) || !isNonEmptyString(trait.name) || !isNonEmptyString(trait.do) || !isNonEmptyString(trait.dont)) {
319
+ errors.push(error("BRAND_STRATEGY_VOICE", `'brand-strategy.voice.traits[${i}]' must have non-empty 'name', 'do', and 'dont' strings`, `brand-strategy.voice.traits[${i}]`));
320
+ } else if (trait.example !== void 0 && !isNonEmptyString(trait.example)) {
321
+ errors.push(error("BRAND_STRATEGY_VOICE", `'brand-strategy.voice.traits[${i}].example' must be a non-empty string when present`, `brand-strategy.voice.traits[${i}].example`));
322
+ }
323
+ });
324
+ }
325
+ const tone = block.tone;
326
+ if (!isObject(tone) || Object.keys(tone).length === 0) {
327
+ errors.push(error("BRAND_STRATEGY_TONE", "'brand-strategy.tone' is required and must be a non-empty object keyed by UI state", "brand-strategy.tone"));
328
+ } else {
329
+ for (const [state, entry] of Object.entries(tone)) {
330
+ if (!isObject(entry) || !isNonEmptyString(entry.feeling) || !isNonEmptyString(entry.example)) {
331
+ errors.push(error("BRAND_STRATEGY_TONE", `'brand-strategy.tone.${state}' must have non-empty 'feeling' and 'example' strings`, `brand-strategy.tone.${state}`));
332
+ }
333
+ }
334
+ }
335
+ if (!Array.isArray(block.lexicon) || block.lexicon.length === 0) {
336
+ errors.push(error("BRAND_STRATEGY_LEXICON", "'brand-strategy.lexicon' is required and must be a non-empty array", "brand-strategy.lexicon"));
337
+ } else {
338
+ block.lexicon.forEach((entry, i) => {
339
+ if (!isObject(entry) || !isNonEmptyString(entry.use) || !isNonEmptyString(entry.avoid)) {
340
+ errors.push(error("BRAND_STRATEGY_LEXICON", `'brand-strategy.lexicon[${i}]' must have non-empty 'use' and 'avoid' strings`, `brand-strategy.lexicon[${i}]`));
341
+ }
342
+ });
343
+ }
344
+ if (!isStringArray(block.core) || block.core.length === 0) {
345
+ errors.push(error("BRAND_STRATEGY_CORE", "'brand-strategy.core' is required and must be a non-empty array of strings", "brand-strategy.core"));
346
+ }
347
+ if (!isNonEmptyString(block.visibility) || !BRAND_VISIBILITIES.includes(block.visibility)) {
348
+ errors.push(error("BRAND_STRATEGY_VISIBILITY", `'brand-strategy.visibility' is required and must be one of: ${BRAND_VISIBILITIES.join(", ")}`, "brand-strategy.visibility"));
349
+ }
350
+ return errors;
351
+ }
352
+ function checkPillarStructure(pillar, index) {
353
+ const path = `brand-strategy.pillars[${index}]`;
354
+ if (!isObject(pillar)) {
355
+ return [error("BRAND_STRATEGY_PILLARS", `'${path}' must be an object`, path)];
356
+ }
357
+ const errors = [];
358
+ if (!isNonEmptyString(pillar.id)) {
359
+ errors.push(error("BRAND_STRATEGY_PILLARS", `'${path}.id' is required and must be a non-empty string`, `${path}.id`));
360
+ }
361
+ if (!isNonEmptyString(pillar.statement)) {
362
+ errors.push(error("BRAND_STRATEGY_PILLARS", `'${path}.statement' is required and must be a non-empty string`, `${path}.statement`));
363
+ }
364
+ const governs = pillar.governs;
365
+ if (!isObject(governs)) {
366
+ errors.push(error("BRAND_STRATEGY_PILLARS", `'${path}.governs' is required and must be an object`, `${path}.governs`));
367
+ } else {
368
+ for (const key of Object.keys(governs)) {
369
+ if (!GOVERNS_KEYS.has(key)) {
370
+ errors.push(error("BRAND_STRATEGY_GOVERNS", `Unknown key '${path}.governs.${key}'. Valid keys: ${[...GOVERNS_KEYS].join(", ")}`, `${path}.governs.${key}`));
371
+ }
372
+ }
373
+ for (const key of GOVERNS_KEYS) {
374
+ const list = governs[key];
375
+ if (list === void 0) continue;
376
+ if (!isStringArray(list)) {
377
+ errors.push(error("BRAND_STRATEGY_GOVERNS", `'${path}.governs.${key}' must be an array of strings`, `${path}.governs.${key}`));
378
+ } else if (list.length === 0) {
379
+ errors.push(error("BRAND_STRATEGY_GOVERNS", `'${path}.governs.${key}' must not be empty when present`, `${path}.governs.${key}`));
380
+ }
381
+ }
382
+ if (governs.tokens === void 0 && governs.components === void 0 && governs.surfaces === void 0) {
383
+ errors.push(error("BRAND_STRATEGY_GOVERNS", `'${path}.governs' must declare at least one of: ${[...GOVERNS_KEYS].join(", ")}`, `${path}.governs`));
384
+ }
385
+ }
386
+ return errors;
387
+ }
388
+ function checkBrandStrategyCoherence(strategy, ctx = {}) {
389
+ const errors = [];
390
+ const surfaces = ctx.surfaces ?? new Set(DEFAULT_BRAND_STRATEGY_SURFACES);
391
+ const states = ctx.states ?? new Set(DEFAULT_BRAND_STRATEGY_TONE_STATES);
392
+ strategy.pillars.forEach((pillar, i) => {
393
+ const path = `brand-strategy.pillars[${i}].governs`;
394
+ if (ctx.tokens && pillar.governs.tokens) {
395
+ for (const ref of pillar.governs.tokens) {
396
+ if (ref === GOVERNS_WILDCARD) continue;
397
+ if (!ctx.tokens.has(normalizeTokenRef(ref))) {
398
+ errors.push(error("BRAND_STRATEGY_GOVERNS_TOKEN", `Pillar '${pillar.id}' governs token '${ref}', which is not a known Visor token`, `${path}.tokens`));
399
+ }
400
+ }
401
+ }
402
+ if (pillar.governs.components) {
403
+ for (const ref of pillar.governs.components) {
404
+ if (ref === GOVERNS_WILDCARD) continue;
405
+ if (ctx.components && !ctx.components.has(ref)) {
406
+ errors.push(error("BRAND_STRATEGY_GOVERNS_COMPONENT", `Pillar '${pillar.id}' governs component '${ref}', which is not a known Visor component`, `${path}.components`));
407
+ }
408
+ }
409
+ }
410
+ if (pillar.governs.surfaces) {
411
+ for (const ref of pillar.governs.surfaces) {
412
+ if (!surfaces.has(ref)) {
413
+ errors.push(error("BRAND_STRATEGY_GOVERNS_SURFACE", `Pillar '${pillar.id}' governs surface '${ref}', which is not a recognized meta-surface (${[...surfaces].join(", ")})`, `${path}.surfaces`));
414
+ }
415
+ }
416
+ }
417
+ });
418
+ for (const state of Object.keys(strategy.tone)) {
419
+ if (!states.has(state)) {
420
+ errors.push(error("BRAND_STRATEGY_TONE_STATE", `tone key '${state}' does not map to a recognized UI state (${[...states].join(", ")})`, `brand-strategy.tone.${state}`));
421
+ }
422
+ }
423
+ return errors;
424
+ }
425
+ function validateBrandStrategy(block, ctx = {}) {
426
+ const errors = checkBrandStrategyStructure(block);
427
+ const warnings = [];
428
+ if (errors.length > 0) {
429
+ return { valid: false, errors, warnings };
430
+ }
431
+ for (const issue2 of checkBrandStrategyCoherence(block, ctx)) {
432
+ (issue2.severity === "error" ? errors : warnings).push(issue2);
433
+ }
434
+ return { valid: errors.length === 0, errors, warnings };
435
+ }
436
+
437
+ // src/brand-strategy/serialize.ts
438
+ function compactGoverns(governs) {
439
+ const out = {};
440
+ if (governs.tokens) out.tokens = [...governs.tokens];
441
+ if (governs.components) out.components = [...governs.components];
442
+ if (governs.surfaces) out.surfaces = [...governs.surfaces];
443
+ return out;
444
+ }
445
+ function serializeBrandStrategy(strategy) {
446
+ if (strategy.visibility === "private") {
447
+ return null;
448
+ }
449
+ const archetype = { primary: strategy.archetype.primary };
450
+ if (strategy.archetype.secondary !== void 0) archetype.secondary = strategy.archetype.secondary;
451
+ if (strategy.archetype.tertiary !== void 0) archetype.tertiary = strategy.archetype.tertiary;
452
+ const tone = {};
453
+ for (const [state, entry] of Object.entries(strategy.tone)) {
454
+ tone[state] = { feeling: entry.feeling, example: entry.example };
455
+ }
456
+ return {
457
+ positioning: {
458
+ onliness: strategy.positioning.onliness,
459
+ category: strategy.positioning.category,
460
+ differentiation: strategy.positioning.differentiation
461
+ },
462
+ essence: [...strategy.essence],
463
+ personality: strategy.personality.map((p) => ({ trait: p.trait, not: p.not })),
464
+ archetype,
465
+ pillars: strategy.pillars.map((p) => ({
466
+ id: p.id,
467
+ statement: p.statement,
468
+ governs: compactGoverns(p.governs)
469
+ })),
470
+ voice: {
471
+ traits: strategy.voice.traits.map((t) => {
472
+ const trait = {
473
+ name: t.name,
474
+ do: t.do,
475
+ dont: t.dont
476
+ };
477
+ if (t.example !== void 0) trait.example = t.example;
478
+ return trait;
479
+ })
480
+ },
481
+ tone,
482
+ lexicon: strategy.lexicon.map((l) => ({ use: l.use, avoid: l.avoid })),
483
+ core: [...strategy.core],
484
+ visibility: strategy.visibility
485
+ };
486
+ }
487
+
205
488
  // src/pipeline.ts
206
489
  import { parse as parseYaml } from "yaml";
207
490
 
@@ -230,7 +513,7 @@ var visor_theme_schema_default = {
230
513
  },
231
514
  label: {
232
515
  type: "string",
233
- description: "Human-readable display name for the theme (e.g. 'Blacklight Pro'). Overrides the name-derived label in the docs theme switcher. Optional."
516
+ description: "Human-readable display name for the theme (e.g. 'My Brand Pro'). Overrides the name-derived label in the docs theme switcher. Optional."
234
517
  },
235
518
  "default-mode": {
236
519
  type: "string",
@@ -501,6 +784,59 @@ var visor_theme_schema_default = {
501
784
  ]
502
785
  }
503
786
  },
787
+ "brand-strategy": {
788
+ type: "object",
789
+ description: "Brand-strategy block (VI-505) \u2014 positioning, personality, pillars, voice, and tone as validated, serializable data. Sibling to `brand` (assets): different lifecycle and consumer. NOT expanded into derived CSS values; coherence-checked (pillars govern real tokens/components/surfaces; tone keys map to real UI states) and serialized to the agent manifest under `brand_strategy`.",
790
+ additionalProperties: false,
791
+ required: ["positioning", "essence", "personality", "archetype", "pillars", "voice", "tone", "lexicon", "core", "visibility"],
792
+ properties: {
793
+ positioning: { $ref: "#/$defs/brandPositioning" },
794
+ essence: {
795
+ type: "array",
796
+ items: { type: "string" },
797
+ minItems: 1,
798
+ description: "2\u20133 internal-facing core words (Aaker essence)."
799
+ },
800
+ personality: {
801
+ type: "array",
802
+ items: { $ref: "#/$defs/brandPersonalityTrait" },
803
+ minItems: 1,
804
+ description: "Brand-as-person traits, each sharpened by its antonym."
805
+ },
806
+ archetype: { $ref: "#/$defs/brandArchetype" },
807
+ pillars: {
808
+ type: "array",
809
+ items: { $ref: "#/$defs/brandPillar" },
810
+ minItems: 1,
811
+ description: "Strategic pillars \u2014 essence words made operational, each governing real tokens/components/surfaces."
812
+ },
813
+ voice: { $ref: "#/$defs/brandVoice" },
814
+ tone: {
815
+ type: "object",
816
+ description: "Voice flexed per UI state. Keys must be recognized UI states.",
817
+ minProperties: 1,
818
+ propertyNames: { enum: ["error", "success", "warning", "info", "empty", "loading", "validation-warning"] },
819
+ additionalProperties: { $ref: "#/$defs/brandToneEntry" }
820
+ },
821
+ lexicon: {
822
+ type: "array",
823
+ items: { $ref: "#/$defs/brandLexiconEntry" },
824
+ minItems: 1,
825
+ description: "Words to use and the ones to avoid."
826
+ },
827
+ core: {
828
+ type: "array",
829
+ items: { type: "string" },
830
+ minItems: 1,
831
+ description: "Aaker core/extended \u2014 the immutable subset, as section names."
832
+ },
833
+ visibility: {
834
+ type: "string",
835
+ enum: ["public", "private"],
836
+ description: "Client brands are private and are omitted from the public agent manifest."
837
+ }
838
+ }
839
+ },
504
840
  spacing: {
505
841
  type: "object",
506
842
  description: "Spacing configuration.",
@@ -668,6 +1004,101 @@ var visor_theme_schema_default = {
668
1004
  description: "Letter spacing in logical pixels (Flutter TextStyle.letterSpacing). Material defaults include negative values, e.g. -0.25 for displayLarge."
669
1005
  }
670
1006
  }
1007
+ },
1008
+ brandPositioning: {
1009
+ type: "object",
1010
+ description: "Positioning \u2014 the onliness, category, and differentiation wedge.",
1011
+ additionalProperties: false,
1012
+ required: ["onliness", "category", "differentiation"],
1013
+ properties: {
1014
+ onliness: { type: "string", minLength: 1, description: `The single sentence that passes Neumeier's "only" test.` },
1015
+ category: { type: "string", minLength: 1, description: 'The category the brand competes in (e.g. "design system").' },
1016
+ differentiation: { type: "string", minLength: 1, description: "What sets the brand apart within that category." }
1017
+ }
1018
+ },
1019
+ brandPersonalityTrait: {
1020
+ type: "object",
1021
+ description: "A personality trait sharpened by its antonym (brand-as-person).",
1022
+ additionalProperties: false,
1023
+ required: ["trait", "not"],
1024
+ properties: {
1025
+ trait: { type: "string", minLength: 1 },
1026
+ not: { type: "string", minLength: 1, description: "The antonym \u2014 what the trait is not." }
1027
+ }
1028
+ },
1029
+ brandArchetype: {
1030
+ type: "object",
1031
+ description: "Brand archetype assignment (Pearson & Mark). Primary required; secondary/tertiary optional.",
1032
+ additionalProperties: false,
1033
+ required: ["primary"],
1034
+ properties: {
1035
+ primary: { type: "string", minLength: 1 },
1036
+ secondary: { type: "string", minLength: 1 },
1037
+ tertiary: { type: "string", minLength: 1 }
1038
+ }
1039
+ },
1040
+ brandGoverns: {
1041
+ type: "object",
1042
+ description: 'What a pillar governs \u2014 design tokens, registry components, and/or meta-surfaces. At least one list is expected; use "*" for all of a namespace.',
1043
+ additionalProperties: false,
1044
+ minProperties: 1,
1045
+ properties: {
1046
+ tokens: { type: "array", items: { type: "string" }, minItems: 1, description: 'Semantic token refs (with or without the leading --), or "*".' },
1047
+ components: { type: "array", items: { type: "string" }, minItems: 1, description: 'Registry component names, or "*" for all.' },
1048
+ surfaces: { type: "array", items: { type: "string", enum: ["manifest", "cli", "component-metadata"] }, minItems: 1, description: "Meta-surfaces a pillar may govern." }
1049
+ }
1050
+ },
1051
+ brandPillar: {
1052
+ type: "object",
1053
+ description: "A strategic pillar \u2014 an essence word made operational.",
1054
+ additionalProperties: false,
1055
+ required: ["id", "statement", "governs"],
1056
+ properties: {
1057
+ id: { type: "string", minLength: 1 },
1058
+ statement: { type: "string", minLength: 1, description: "The pillar's claim in one line." },
1059
+ governs: { $ref: "#/$defs/brandGoverns" }
1060
+ }
1061
+ },
1062
+ brandVoice: {
1063
+ type: "object",
1064
+ description: "Voice \u2014 fixed across the brand; never flexes.",
1065
+ additionalProperties: false,
1066
+ required: ["traits"],
1067
+ properties: {
1068
+ traits: { type: "array", items: { $ref: "#/$defs/brandVoiceTrait" }, minItems: 1 }
1069
+ }
1070
+ },
1071
+ brandVoiceTrait: {
1072
+ type: "object",
1073
+ description: "A fixed voice trait with a worked example.",
1074
+ additionalProperties: false,
1075
+ required: ["name", "do", "dont"],
1076
+ properties: {
1077
+ name: { type: "string", minLength: 1 },
1078
+ do: { type: "string", minLength: 1, description: "What to do." },
1079
+ dont: { type: "string", minLength: 1, description: "What not to do." },
1080
+ example: { type: "string", minLength: 1, description: "A worked example sentence." }
1081
+ }
1082
+ },
1083
+ brandToneEntry: {
1084
+ type: "object",
1085
+ description: "A single tone entry, keyed (in `tone`) to a recognized UI state.",
1086
+ additionalProperties: false,
1087
+ required: ["feeling", "example"],
1088
+ properties: {
1089
+ feeling: { type: "string", minLength: 1, description: "The feeling the copy should evoke in this state." },
1090
+ example: { type: "string", minLength: 1, description: "A worked example message for this state." }
1091
+ }
1092
+ },
1093
+ brandLexiconEntry: {
1094
+ type: "object",
1095
+ description: "A lexicon pairing \u2014 the word to use and the one to avoid.",
1096
+ additionalProperties: false,
1097
+ required: ["use", "avoid"],
1098
+ properties: {
1099
+ use: { type: "string", minLength: 1 },
1100
+ avoid: { type: "string", minLength: 1 }
1101
+ }
671
1102
  }
672
1103
  }
673
1104
  };
@@ -683,6 +1114,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
683
1114
  "colors-dark",
684
1115
  "typography",
685
1116
  "brand",
1117
+ "brand-strategy",
686
1118
  "spacing",
687
1119
  "radius",
688
1120
  "shadows",
@@ -1102,6 +1534,11 @@ function validateConfig(config) {
1102
1534
  }
1103
1535
  }
1104
1536
  }
1537
+ if (obj["brand-strategy"] !== void 0) {
1538
+ for (const iss of checkBrandStrategyStructure(obj["brand-strategy"])) {
1539
+ errors.push(iss.path ? `${iss.message} (at ${iss.path})` : iss.message);
1540
+ }
1541
+ }
1105
1542
  if (obj.overrides !== void 0) {
1106
1543
  if (typeof obj.overrides !== "object" || obj.overrides === null) {
1107
1544
  errors.push("'overrides' must be an object");
@@ -1735,6 +2172,18 @@ var SEMANTIC_MAP = {
1735
2172
  intent: SEMANTIC_INTENT_MAP,
1736
2173
  hairline: SEMANTIC_HAIRLINE_MAP
1737
2174
  };
2175
+ function getKnownTokenRefs() {
2176
+ const refs = /* @__PURE__ */ new Set();
2177
+ for (const key of Object.keys(SEMANTIC_TEXT_MAP)) refs.add(`text-${key}`);
2178
+ for (const key of Object.keys(SEMANTIC_SURFACE_MAP)) refs.add(`surface-${key}`);
2179
+ for (const key of Object.keys(SEMANTIC_BORDER_MAP)) refs.add(`border-${key}`);
2180
+ for (const key of Object.keys(SEMANTIC_INTERACTIVE_MAP)) refs.add(`interactive-${key}`);
2181
+ for (const key of Object.keys(SEMANTIC_INTENT_MAP)) refs.add(key);
2182
+ for (const key of Object.keys(SEMANTIC_HAIRLINE_MAP)) {
2183
+ refs.add(key === "default" ? "hairline" : `hairline-${key}`);
2184
+ }
2185
+ return refs;
2186
+ }
1738
2187
 
1739
2188
  // src/assign.ts
1740
2189
  function lookupShade(primitives, role, shade) {
@@ -2862,6 +3311,12 @@ function validate(config, options) {
2862
3311
  const resolved = resolveConfig(typedConfig);
2863
3312
  checkResolvedCompleteness(resolved, errors);
2864
3313
  }
3314
+ const brandStrategy = typedConfig["brand-strategy"];
3315
+ if (brandStrategy) {
3316
+ for (const iss of checkBrandStrategyCoherence(brandStrategy, { tokens: getKnownTokenRefs() })) {
3317
+ (iss.severity === "error" ? errors : warnings).push(iss);
3318
+ }
3319
+ }
2865
3320
  if (errors.length === 0) {
2866
3321
  checkContrastWarnings(typedConfig, warnings);
2867
3322
  checkColorSimilarity(typedConfig, warnings);
@@ -3368,8 +3823,12 @@ function extractFromCSS(files, name = "extracted-theme") {
3368
3823
  }
3369
3824
  export {
3370
3825
  BRAND_VARIANTS,
3826
+ BRAND_VISIBILITIES,
3827
+ DEFAULT_BRAND_STRATEGY_SURFACES,
3828
+ DEFAULT_BRAND_STRATEGY_TONE_STATES,
3371
3829
  DEFAULT_VISOR_BRAND,
3372
3830
  FONT_WEIGHT_ALIASES,
3831
+ GOVERNS_WILDCARD,
3373
3832
  SEMANTIC_MAP,
3374
3833
  TAILWIND_GRAY,
3375
3834
  VISOR_BRANDS_CDN,
@@ -3379,6 +3838,8 @@ export {
3379
3838
  assignSemanticTokens,
3380
3839
  buildVisorBrandUrl,
3381
3840
  buildVisorFontUrl,
3841
+ checkBrandStrategyCoherence,
3842
+ checkBrandStrategyStructure,
3382
3843
  clampToSrgb,
3383
3844
  cleanFontValue,
3384
3845
  collectBrandPassthrough,
@@ -3400,6 +3861,7 @@ export {
3400
3861
  generateThemeDataFromConfig,
3401
3862
  generateThemeFromConfig,
3402
3863
  getContrastRatio,
3864
+ getKnownTokenRefs,
3403
3865
  googleFontsCatalog,
3404
3866
  hasBrandPassthrough,
3405
3867
  hexToOklch,
@@ -3426,8 +3888,10 @@ export {
3426
3888
  resolveThemeBrand,
3427
3889
  resolveThemeFonts,
3428
3890
  rgbToHex,
3891
+ serializeBrandStrategy,
3429
3892
  serializeColor,
3430
3893
  validate,
3894
+ validateBrandStrategy,
3431
3895
  validateConfig,
3432
3896
  validateFontCoverage,
3433
3897
  visor_theme_schema_default as visorThemeSchema