@loworbitstudio/visor-theme-engine 0.19.0 → 0.21.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.
@@ -1,4 +1,4 @@
1
- import { k as GeneratedPrimitives, s as SemanticTokens, R as ResolvedThemeConfig } from '../types-DF5StphJ.js';
1
+ import { k as GeneratedPrimitives, s as SemanticTokens, R as ResolvedThemeConfig } from '../types-BG-YT4JW.js';
2
2
 
3
3
  /**
4
4
  * Adapter types for the Visor theme engine.
@@ -37,6 +37,10 @@ interface NextJSAdapterOptions extends AdapterOptions {
37
37
  * `@layer visor-base` origination block that binds theme tokens to the page
38
38
  * root (default: true). Set false for consumers that ship Tailwind preflight
39
39
  * or their own reset. See VI-616.
40
+ *
41
+ * VI-638: the base block also carries `font-size: var(--font-size-base)`,
42
+ * which is how `typography.scale` reaches the page. A consumer that declines
43
+ * it must bind the base size itself — on `body`, not `:root`.
40
44
  */
41
45
  includeBaseLayer?: boolean;
42
46
  }
@@ -6,12 +6,16 @@ import {
6
6
  block,
7
7
  buildVisorFontUrl,
8
8
  collectBrandPassthrough,
9
+ collectDeclaredProperties,
9
10
  fontStack,
10
11
  generateDarkCss,
12
+ generateFontSizeDecls,
13
+ generateFontWeightDecls,
11
14
  generateHairlineDecls,
12
15
  generateIntentDecls,
13
16
  generateLightCss,
14
17
  generatePrimitivesCss,
18
+ generateSemanticAliasDecls,
15
19
  generateShadeScale,
16
20
  generateSpaceAliasDecls,
17
21
  generateTextScaleAliasDecls,
@@ -21,7 +25,7 @@ import {
21
25
  resolveThemeBrand,
22
26
  resolveThemeFonts,
23
27
  sectionComment
24
- } from "../chunk-32DC5DAR.js";
28
+ } from "../chunk-LY5LXQYE.js";
25
29
 
26
30
  // src/adapters/brand-passthrough.ts
27
31
  var SENTINEL_COLOR = "#ff00ff";
@@ -212,7 +216,11 @@ function nextjsAdapter(input, options) {
212
216
  baseLines.push(
213
217
  block(scopePrefix ?? "body", [
214
218
  "font-family: var(--font-body);",
215
- "font-size: 1rem;",
219
+ // VI-638: the scaled ramp is the single mechanism that sets type size,
220
+ // and the base step is what the page inherits. This binding lives on
221
+ // `body` (or the body-class scope), never `:root` — the ramp is
222
+ // expressed in `rem`, so a scaled root would multiply it a second time.
223
+ "font-size: var(--font-size-base, 1rem);",
216
224
  "color: var(--text-primary);",
217
225
  "background: var(--surface-page, var(--surface-background));"
218
226
  ])
@@ -295,14 +303,27 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
295
303
  }
296
304
  semanticLines.push("");
297
305
  }
306
+ const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix, colorScheme }));
307
+ const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix, colorScheme }));
308
+ const adaptiveBody = [lightBody, darkBody].filter(Boolean).join("\n\n");
309
+ if (scopePrefix) {
310
+ const declared = collectDeclaredProperties(
311
+ [primitivesBody, semanticLines.join("\n"), adaptiveBody].join("\n")
312
+ );
313
+ const aliasDecls = generateSemanticAliasDecls(declared);
314
+ if (aliasDecls.length > 0) {
315
+ semanticLines.push(
316
+ sectionComment("visor-core alias re-substitution at theme scope (VI-648)")
317
+ );
318
+ semanticLines.push(block(hostSelector, aliasDecls));
319
+ semanticLines.push("");
320
+ }
321
+ }
298
322
  const semanticLayer = wrapInLayer("visor-semantic", semanticLines.join("\n").trim());
299
323
  if (semanticLayer) {
300
324
  lines.push(semanticLayer);
301
325
  lines.push("");
302
326
  }
303
- const lightBody = stripHeader(generateLightCss(input.tokens, { scopePrefix, colorScheme }));
304
- const darkBody = stripHeader(generateDarkCss(input.tokens, { scopePrefix, colorScheme }));
305
- const adaptiveBody = [lightBody, darkBody].filter(Boolean).join("\n\n");
306
327
  lines.push(
307
328
  wrapInLayer("visor-adaptive", adaptiveBody)
308
329
  );
@@ -537,23 +558,8 @@ function generateTypographyDecls(config, aliases) {
537
558
  decls.push(`--font-heading: ${fontStack(headingFamily, aliases)};`);
538
559
  decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
539
560
  decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
540
- const fontSizes = {
541
- xs: 12,
542
- sm: 14,
543
- base: 16,
544
- lg: 18,
545
- xl: 20,
546
- "2xl": 24,
547
- "3xl": 30,
548
- "4xl": 36
549
- };
550
- for (const [name, px] of Object.entries(fontSizes)) {
551
- decls.push(`--font-size-${name}: ${px / 16}rem; /* ${px}px */`);
552
- }
553
- decls.push(`--font-weight-normal: ${config.typography.body.weight};`);
554
- decls.push("--font-weight-medium: 500;");
555
- decls.push(`--font-weight-semibold: ${config.typography.heading.weight};`);
556
- decls.push("--font-weight-bold: 700;");
561
+ decls.push(...generateFontSizeDecls(config.typography.scale));
562
+ decls.push(...generateFontWeightDecls(config.typography));
557
563
  const lineHeights = {
558
564
  none: 1,
559
565
  tight: 1.25,
@@ -686,7 +692,6 @@ function docsAdapter(input, options) {
686
692
  fontLines.push("");
687
693
  }
688
694
  }
689
- const scale = input.config.typography?.scale ?? 1;
690
695
  const emittedFamilies = /* @__PURE__ */ new Set();
691
696
  for (const font of fontSlots) {
692
697
  if (font && font.source === "visor-fonts" && !emittedFamilies.has(font.family)) {
@@ -700,9 +705,6 @@ function docsAdapter(input, options) {
700
705
  fontLines.push(` font-weight: ${weight};`);
701
706
  fontLines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
702
707
  fontLines.push(` font-display: ${font.display};`);
703
- if (scale !== 1) {
704
- fontLines.push(` size-adjust: ${Math.round(scale * 100)}%;`);
705
- }
706
708
  fontLines.push("}");
707
709
  fontLines.push("");
708
710
  }
@@ -712,7 +714,11 @@ function docsAdapter(input, options) {
712
714
  lines.push("\n/* \u2500\u2500 Section 1: Shared tokens (mode-independent) \u2500\u2500 */");
713
715
  const sharedDecls = [
714
716
  "min-height: 100vh;",
715
- "font-size: 1rem;",
717
+ // VI-638: the scaled ramp is the single mechanism that sets type size, and
718
+ // the base step is what the page inherits. Safe here because the scope is a
719
+ // class on a wrapper, never `:root` — a scaled root would multiply the
720
+ // rem-expressed ramp a second time.
721
+ "font-size: var(--font-size-base, 1rem);",
716
722
  // BO-56: pin UA chrome to the brand's single mode (adaptive emits nothing).
717
723
  ...singleMode ? [`color-scheme: ${singleMode};`] : [],
718
724
  `background: var(--surface-page, var(--surface-background));`,
@@ -1928,6 +1928,99 @@ function applyOverrides(tokens, overrides) {
1928
1928
  return result;
1929
1929
  }
1930
1930
 
1931
+ // src/font-weights.ts
1932
+ var WEIGHT_RAMP_TARGETS = {
1933
+ medium: 500,
1934
+ semibold: 600,
1935
+ bold: 700
1936
+ };
1937
+ var SLOTS = ["heading", "display", "body", "mono"];
1938
+ function loadedWeights(typography) {
1939
+ const set = /* @__PURE__ */ new Set();
1940
+ for (const slot of SLOTS) {
1941
+ const cfg = typography[slot];
1942
+ if (!cfg) continue;
1943
+ if (cfg.weights?.length) {
1944
+ for (const w of cfg.weights) set.add(w);
1945
+ } else if (typeof cfg.weight === "number") {
1946
+ set.add(cfg.weight);
1947
+ }
1948
+ }
1949
+ return [...set].sort((a, b) => a - b);
1950
+ }
1951
+ function declaresWeights(typography) {
1952
+ return SLOTS.some((slot) => Boolean(typography[slot]?.weights?.length));
1953
+ }
1954
+ function matchLoadedWeight(desired, loaded) {
1955
+ if (loaded.length === 0) return desired;
1956
+ if (loaded.includes(desired)) return desired;
1957
+ const ascending = [...loaded].sort((a, b) => a - b);
1958
+ const lighter = ascending.filter((w) => w < desired).reverse();
1959
+ const heavier = ascending.filter((w) => w > desired);
1960
+ if (desired >= 400 && desired <= 500) {
1961
+ const upToFive = heavier.filter((w) => w <= 500);
1962
+ const aboveFive = heavier.filter((w) => w > 500);
1963
+ return upToFive[0] ?? lighter[0] ?? aboveFive[0];
1964
+ }
1965
+ if (desired < 400) {
1966
+ return lighter[0] ?? heavier[0];
1967
+ }
1968
+ return heavier[0] ?? lighter[0];
1969
+ }
1970
+ function resolveWeightRamp(typography) {
1971
+ const bodyWeight = typography.body?.weight ?? 400;
1972
+ if (!declaresWeights(typography)) {
1973
+ return {
1974
+ normal: bodyWeight,
1975
+ medium: WEIGHT_RAMP_TARGETS.medium,
1976
+ semibold: WEIGHT_RAMP_TARGETS.semibold,
1977
+ bold: WEIGHT_RAMP_TARGETS.bold
1978
+ };
1979
+ }
1980
+ const loaded = loadedWeights(typography);
1981
+ return {
1982
+ normal: matchLoadedWeight(bodyWeight, loaded),
1983
+ medium: matchLoadedWeight(WEIGHT_RAMP_TARGETS.medium, loaded),
1984
+ semibold: matchLoadedWeight(WEIGHT_RAMP_TARGETS.semibold, loaded),
1985
+ bold: matchLoadedWeight(WEIGHT_RAMP_TARGETS.bold, loaded)
1986
+ };
1987
+ }
1988
+ function resolveRoleWeight(declared, typography) {
1989
+ if (!declaresWeights(typography)) return declared;
1990
+ return matchLoadedWeight(declared, loadedWeights(typography));
1991
+ }
1992
+ function generateFontWeightDecls(typography) {
1993
+ const ramp = resolveWeightRamp(typography);
1994
+ const decls = [
1995
+ `--font-weight-normal: ${ramp.normal};`,
1996
+ `--font-weight-medium: ${ramp.medium};`,
1997
+ `--font-weight-semibold: ${ramp.semibold};`,
1998
+ `--font-weight-bold: ${ramp.bold};`,
1999
+ // Role primitives. These are why `semibold` was pinned in the first place:
2000
+ // packages/tokens defined `--weight-heading: var(--font-weight-semibold)`
2001
+ // and the engine emitted no per-theme value, so hijacking `semibold` was
2002
+ // the only channel a theme's heading weight had. Each role has its own
2003
+ // primitive now, and the semantic `--weight-*` roles resolve through them.
2004
+ //
2005
+ // Emitted as `--font-weight-*` primitives rather than the bare `--weight-*`
2006
+ // names on purpose: the semantic names are defined in @layer visor-semantic,
2007
+ // which wins over visor-primitives, so a theme writing them directly would
2008
+ // be overridden by the tokens package on a `:root`-scoped consumer.
2009
+ `--font-weight-heading: ${resolveRoleWeight(typography.heading.weight, typography)};`,
2010
+ `--font-weight-body: ${resolveRoleWeight(typography.body.weight, typography)};`,
2011
+ `--font-weight-display: ${resolveRoleWeight(typography.display.weight, typography)};`,
2012
+ // Retained: `--weight-display` has no competing definition in the tokens
2013
+ // package and predates this change, so consumers already reference it.
2014
+ `--weight-display: ${resolveRoleWeight(typography.display.weight, typography)};`
2015
+ ];
2016
+ if (declaresWeights(typography)) {
2017
+ for (const weight of loadedWeights(typography)) {
2018
+ decls.push(`--font-weight-${weight}: ${weight};`);
2019
+ }
2020
+ }
2021
+ return decls;
2022
+ }
2023
+
1931
2024
  // src/fonts/theme-alias.ts
1932
2025
  var EMPTY_ALIASES = /* @__PURE__ */ new Map();
1933
2026
  function aliasFamily(family, themeSlug) {
@@ -2024,33 +2117,32 @@ function generateShadowPrimitives(config) {
2024
2117
  `--shadow-xl: ${config.shadows.xl};`
2025
2118
  ];
2026
2119
  }
2120
+ var FONT_SIZE_RAMP_PX = {
2121
+ xs: 12,
2122
+ sm: 14,
2123
+ base: 16,
2124
+ lg: 18,
2125
+ xl: 20,
2126
+ "2xl": 24,
2127
+ "3xl": 30,
2128
+ "4xl": 36
2129
+ };
2130
+ function generateFontSizeDecls(scale) {
2131
+ return Object.entries(FONT_SIZE_RAMP_PX).map(([name, px]) => {
2132
+ const scaled = px * scale;
2133
+ const rem = Number((scaled / 16).toFixed(5));
2134
+ return `--font-size-${name}: ${rem}rem; /* ${Number(scaled.toFixed(2))}px */`;
2135
+ });
2136
+ }
2027
2137
  function generateTypographyPrimitives(config, aliases = EMPTY_ALIASES) {
2028
2138
  const decls = [];
2029
- const scale = config.typography.scale;
2030
- decls.push(`font-size: ${scale === 1 ? "1rem" : `${scale}rem`};`);
2031
2139
  decls.push(`--font-heading: ${fontStack(config.typography.heading.family, aliases)};`);
2032
2140
  decls.push(`--font-display: ${fontStack(config.typography.display.family, aliases)};`);
2033
2141
  decls.push(`--font-sans: ${fontStack(config.typography.body.family, aliases)};`);
2034
2142
  decls.push(`--font-body: ${fontStack(config.typography.body.family, aliases)};`);
2035
2143
  decls.push(`--font-mono: ${fontStack(config.typography.mono.family, aliases)};`);
2036
- const fontSizes = {
2037
- xs: 12,
2038
- sm: 14,
2039
- base: 16,
2040
- lg: 18,
2041
- xl: 20,
2042
- "2xl": 24,
2043
- "3xl": 30,
2044
- "4xl": 36
2045
- };
2046
- for (const [name, px] of Object.entries(fontSizes)) {
2047
- decls.push(`--font-size-${name}: ${px / 16}rem; /* ${px}px */`);
2048
- }
2049
- decls.push(`--font-weight-normal: ${config.typography.body.weight};`);
2050
- decls.push("--font-weight-medium: 500;");
2051
- decls.push(`--font-weight-semibold: ${config.typography.heading.weight};`);
2052
- decls.push("--font-weight-bold: 700;");
2053
- decls.push(`--weight-display: ${config.typography.display.weight};`);
2144
+ decls.push(...generateFontSizeDecls(config.typography.scale));
2145
+ decls.push(...generateFontWeightDecls(config.typography));
2054
2146
  const lineHeights = {
2055
2147
  none: 1,
2056
2148
  tight: 1.25,
@@ -2392,6 +2484,86 @@ function generateFullBundleCss(primitives, tokens, config) {
2392
2484
  return lines.join("\n");
2393
2485
  }
2394
2486
 
2487
+ // src/semantic-aliases.ts
2488
+ var VISOR_CORE_SEMANTIC_ALIASES = {
2489
+ // Border — the one entry the engine's own `SemanticTokens.border` set lacks.
2490
+ "border-input": "color-neutral-200",
2491
+ // Component + layout spacing (4px grid multiples).
2492
+ "component-xs": "spacing-1",
2493
+ "component-sm": "spacing-2",
2494
+ "component-md": "spacing-4",
2495
+ "component-lg": "spacing-6",
2496
+ "component-xl": "spacing-8",
2497
+ "layout-sm": "spacing-4",
2498
+ "layout-md": "spacing-8",
2499
+ "layout-lg": "spacing-12",
2500
+ "layout-xl": "spacing-16",
2501
+ "layout-2xl": "spacing-24",
2502
+ "layout-section-padding": "spacing-24",
2503
+ "layout-section-padding-mobile": "spacing-16",
2504
+ // Typography size roles — track the theme's `typography.scale` ramp.
2505
+ "size-body": "font-size-base",
2506
+ "size-body-sm": "font-size-sm",
2507
+ "size-label": "font-size-sm",
2508
+ "size-caption": "font-size-xs",
2509
+ "size-heading-sm": "font-size-lg",
2510
+ "size-heading-md": "font-size-xl",
2511
+ "size-heading-lg": "font-size-2xl",
2512
+ "size-heading-xl": "font-size-3xl",
2513
+ // Typography weight roles — `--weight-heading` is the token VI-648 was filed on.
2514
+ "weight-body": "font-weight-body",
2515
+ "weight-label": "font-weight-medium",
2516
+ "weight-heading": "font-weight-heading",
2517
+ "weight-strong": "font-weight-bold",
2518
+ // Motion. `--motion-easing-spring` is absent because the engine emits it
2519
+ // under its own name, so it is already correct on the scope.
2520
+ "motion-duration-fast": "motion-duration-100",
2521
+ "motion-duration-normal": "motion-duration-200",
2522
+ "motion-duration-slow": "motion-duration-500",
2523
+ "motion-easing-default": "motion-easing-ease-in-out",
2524
+ "motion-easing-enter": "motion-easing-ease-out",
2525
+ "motion-easing-exit": "motion-easing-ease-in",
2526
+ // Skeleton shimmer. Resolves through *semantic* surfaces, not primitives —
2527
+ // the engine emits those in `visor-adaptive`, a later layer, which does not
2528
+ // affect substitution: `var()` reads the computed value on the element.
2529
+ "skeleton-from": "surface-muted",
2530
+ "skeleton-to": "surface-subtle",
2531
+ // Chart series.
2532
+ "chart-1": "color-primary-500",
2533
+ "chart-2": "color-success-500",
2534
+ "chart-3": "color-warning-500",
2535
+ "chart-4": "color-info-500",
2536
+ "chart-5": "color-error-500",
2537
+ // Sidebar chrome.
2538
+ "sidebar-bg": "color-neutral-50",
2539
+ "sidebar-text": "color-neutral-700",
2540
+ "sidebar-primary-bg": "color-primary-600",
2541
+ "sidebar-primary-text": "color-white",
2542
+ "sidebar-accent-bg": "color-neutral-100",
2543
+ "sidebar-accent-text": "color-neutral-900",
2544
+ "sidebar-border": "color-neutral-200",
2545
+ "sidebar-ring": "color-primary-500",
2546
+ "sidebar-text-muted": "color-neutral-500",
2547
+ // Field-attached floating panels (VI-497).
2548
+ "field-menu-bg": "surface-popover"
2549
+ };
2550
+ function collectDeclaredProperties(css) {
2551
+ const declared = /* @__PURE__ */ new Set();
2552
+ for (const match of css.matchAll(/(^|[{;\s])--([a-zA-Z0-9-]+)\s*:/g)) {
2553
+ declared.add(match[2]);
2554
+ }
2555
+ return declared;
2556
+ }
2557
+ function generateSemanticAliasDecls(declared) {
2558
+ const decls = [];
2559
+ for (const [alias, referent] of Object.entries(VISOR_CORE_SEMANTIC_ALIASES)) {
2560
+ if (declared.has(alias)) continue;
2561
+ if (!declared.has(referent)) continue;
2562
+ decls.push(`--${alias}: var(--${referent});`);
2563
+ }
2564
+ return decls;
2565
+ }
2566
+
2395
2567
  // src/types.ts
2396
2568
  var MATERIAL_TEXT_SLOTS = [
2397
2569
  "displayLarge",
@@ -2464,11 +2636,19 @@ export {
2464
2636
  collectBrandPassthrough,
2465
2637
  hasBrandPassthrough,
2466
2638
  applyOverrides,
2639
+ WEIGHT_RAMP_TARGETS,
2640
+ loadedWeights,
2641
+ declaresWeights,
2642
+ matchLoadedWeight,
2643
+ resolveWeightRamp,
2644
+ resolveRoleWeight,
2645
+ generateFontWeightDecls,
2467
2646
  aliasFamily,
2468
2647
  fontStack,
2469
2648
  header,
2470
2649
  sectionComment,
2471
2650
  block2 as block,
2651
+ generateFontSizeDecls,
2472
2652
  generatePrimitivesCss,
2473
2653
  generateSemanticCss,
2474
2654
  generateTextScaleAliasDecls,
@@ -2477,5 +2657,8 @@ export {
2477
2657
  generateHairlineDecls,
2478
2658
  generateLightCss,
2479
2659
  generateDarkCss,
2480
- generateFullBundleCss
2660
+ generateFullBundleCss,
2661
+ VISOR_CORE_SEMANTIC_ALIASES,
2662
+ collectDeclaredProperties,
2663
+ generateSemanticAliasDecls
2481
2664
  };
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, 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-DF5StphJ.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 COMPONENT_TOKEN_FAMILIES, Z as COMPONENT_TOKEN_FAMILY_BY_NAME, _ as ColorFormat, $ as ComponentTokenBinding, a0 as ComponentTokenBindings, a1 as ComponentTokenConsumer, a2 as ComponentTokenFamily, a3 as ComponentTokenSpec, a4 as DEFAULT_BRAND_STRATEGY_SURFACES, a5 as DEFAULT_BRAND_STRATEGY_TONE_STATES, a6 as FontSource, a7 as GOVERNS_WILDCARD, a8 as RGBA, a9 as ResolvedComponentTokens, aa as SemanticTokenValue, ab as allComponentTokenNames, ac as componentTokenName, ad as hasComponentBindings, ae as resolveComponentBindings, af as validateComponentBindings } from './types-DF5StphJ.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-BG-YT4JW.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 COMPONENT_TOKEN_FAMILIES, Z as COMPONENT_TOKEN_FAMILY_BY_NAME, _ as ColorFormat, $ as ComponentTokenBinding, a0 as ComponentTokenBindings, a1 as ComponentTokenConsumer, a2 as ComponentTokenFamily, a3 as ComponentTokenSpec, a4 as DEFAULT_BRAND_STRATEGY_SURFACES, a5 as DEFAULT_BRAND_STRATEGY_TONE_STATES, a6 as FontSource, a7 as GOVERNS_WILDCARD, a8 as RGBA, a9 as ResolvedComponentTokens, aa as SemanticTokenValue, ab as allComponentTokenNames, ac as componentTokenName, ad as hasComponentBindings, ae as resolveComponentBindings, af as validateComponentBindings } from './types-BG-YT4JW.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -585,7 +585,7 @@ var properties = {
585
585
  },
586
586
  scale: {
587
587
  type: "number",
588
- description: "Type scale multiplier applied to the font-size ramp. Default: 1."
588
+ description: "Type scale multiplier applied to the font-size ramp: every --font-size-* step is multiplied by it, and the page inherits the scaled --font-size-base. Default: 1. CSS adapters only — the Flutter adapter emits Material 3 slot sizes verbatim and ignores this field."
589
589
  },
590
590
  "text-on-light": {
591
591
  type: "string",
@@ -1876,6 +1876,87 @@ declare const SEMANTIC_MAP: {
1876
1876
  */
1877
1877
  declare function getKnownTokenRefs(): Set<string>;
1878
1878
 
1879
+ /**
1880
+ * visor-core semantic aliases that need re-substituting at a theme scope (VI-648).
1881
+ *
1882
+ * `packages/tokens` emits its semantic layer as indirections — `--chart-1:
1883
+ * var(--color-primary-500)`, `--weight-heading: var(--font-weight-heading)` —
1884
+ * declared on `:root`.
1885
+ *
1886
+ * Custom-property substitution resolves **where the property is declared**. On
1887
+ * a `scopePrefix` theme (the VI-368 body-class repaint pattern that every
1888
+ * Blacklight theme uses) the engine emits the theme's primitives on the scope
1889
+ * selector, e.g. `body.blacklight-pro-theme`. From `:root`, a descendant's
1890
+ * value is invisible, so `var(--color-primary-500)` sees only visor-core's
1891
+ * untuned default. `body` then *inherits* the already-substituted default, and
1892
+ * the alias silently ignores the theme:
1893
+ *
1894
+ * ```
1895
+ * --font-weight-heading : 500 <- correct, emitted by the engine on the scope
1896
+ * --weight-heading : 600 <- visor-core's :root default
1897
+ * --chart-1 : #3b82f6 <- stock blue on a monochrome theme
1898
+ * --skeleton-from : #f3f4f6 <- light gray on a dark theme
1899
+ * ```
1900
+ *
1901
+ * The engine independently resolves 66 of visor-core's 114 aliases to concrete
1902
+ * values (`--text-*`, `--surface-*`, `--border-*`, `--interactive-*`, `--font-*`)
1903
+ * and emits those on the scope already, so they are correct. The 48 below are
1904
+ * the ones only visor-core knows how to derive. Re-declaring them on the scope
1905
+ * selector makes the substitution happen there, against the theme's own
1906
+ * primitives.
1907
+ *
1908
+ * ## Why this table lives in the engine, not in `packages/tokens`
1909
+ *
1910
+ * `packages/tokens` is the natural owner — these are its aliases. But
1911
+ * `packages/tokens` already depends on this package (its build calls
1912
+ * `generateThemeData` + `docsAdapter` to emit `dist/themes/<slug>.css`), so an
1913
+ * engine → tokens import would be a build cycle. The table therefore sits on
1914
+ * the importable side of that edge.
1915
+ *
1916
+ * It is not a hand-maintained copy. `packages/tokens` owns the authoring
1917
+ * tables in `src/tokens/semantic.ts` and this list is asserted against the
1918
+ * *emitted* `tokens.css` by `semantic-alias-coverage` — a stronger check than
1919
+ * comparing two constants, because it compares against what actually ships.
1920
+ * Adding an alias in `packages/tokens` without adding it here fails that test
1921
+ * with the missing names.
1922
+ */
1923
+ /**
1924
+ * Alias name → the token it resolves through. Both written without the leading
1925
+ * `--`. Order mirrors visor-core's emission order in `tokens.css`, which keeps
1926
+ * a diff against that file readable.
1927
+ */
1928
+ declare const VISOR_CORE_SEMANTIC_ALIASES: Readonly<Record<string, string>>;
1929
+ /**
1930
+ * Every custom property declared anywhere in `css`, without the leading `--`.
1931
+ *
1932
+ * Read off the generated CSS rather than the token data so the filter in
1933
+ * `generateSemanticAliasDecls` reflects what this theme *actually* emits —
1934
+ * including conditional output such as the `--font-weight-<n>` ladder, which
1935
+ * only appears when a theme declares an explicit `weights` array.
1936
+ */
1937
+ declare function collectDeclaredProperties(css: string): Set<string>;
1938
+ /**
1939
+ * The `--alias: var(--referent);` declarations to re-emit on a theme's scope
1940
+ * selector, given everything the theme has emitted so far.
1941
+ *
1942
+ * Filtered on both sides, so the table can never make output worse:
1943
+ *
1944
+ * - **Skip an alias the theme already declares.** The theme's own value is
1945
+ * authoritative, and re-declaring it in `visor-semantic` would override a
1946
+ * `visor-primitives` declaration of the same name. `--font-body` is the live
1947
+ * case: the engine emits it as a concrete family, and re-aliasing it to
1948
+ * `var(--font-sans)` would win the layer contest and resolve to nothing.
1949
+ * - **Skip an alias whose referent the theme does not declare.** There is then
1950
+ * nothing to re-resolve against, and inheriting visor-core's `:root` value is
1951
+ * the correct outcome — re-emitting would replace a working value with an
1952
+ * invalid one.
1953
+ *
1954
+ * Returns `[]` for a `:root`-scoped theme's caller, which does not need this at
1955
+ * all: there, visor-core's aliases and the theme's primitives are declared on
1956
+ * the same element, so substitution already sees the theme's values.
1957
+ */
1958
+ declare function generateSemanticAliasDecls(declared: ReadonlySet<string>): string[];
1959
+
1879
1960
  /**
1880
1961
  * Per-theme font-family aliasing — substrate fix for VI-354.
1881
1962
  *
@@ -1922,6 +2003,135 @@ declare function generateDarkCss(tokens: SemanticTokens, options?: {
1922
2003
  }): string;
1923
2004
  declare function generateFullBundleCss(primitives: GeneratedPrimitives, tokens: SemanticTokens, config: ResolvedThemeConfig): string;
1924
2005
 
2006
+ /**
2007
+ * Font-weight resolution (VI-639).
2008
+ *
2009
+ * The named weight ramp used to be three literals and one role leak:
2010
+ *
2011
+ * ```
2012
+ * --font-weight-normal: <body.weight>
2013
+ * --font-weight-medium: 500 // literal
2014
+ * --font-weight-semibold: <heading.weight> // a different concept entirely
2015
+ * --font-weight-bold: 700 // literal
2016
+ * ```
2017
+ *
2018
+ * `semibold` is not a heading. Pinning it there collapsed it onto `medium` on
2019
+ * every theme whose heading weight is 500 (the Blacklight family), onto
2020
+ * `normal` where it is 400 (knowmentum), and inverted the ramp where it is
2021
+ * above `bold` (strata emitted `semibold: 800` over `bold: 700`). Meanwhile the
2022
+ * literals ignored the `weights` array a theme actually loaded, so a theme
2023
+ * declaring `[200, 400, 600, 900]` got tokens pointing at faces it never
2024
+ * fetched — which the browser then silently substituted.
2025
+ *
2026
+ * Resolution here follows the CSS font-matching algorithm's weight-selection
2027
+ * order (CSS Fonts 4 §5.2) rather than naive nearest-numeric. The browser is
2028
+ * already performing exactly this substitution at render time, so an emitted
2029
+ * token becomes the weight that was *already rendering* — which is what makes
2030
+ * this change visually neutral everywhere except where the token was wrong.
2031
+ */
2032
+ /** Canonical targets for the named ramp above `normal`. */
2033
+ declare const WEIGHT_RAMP_TARGETS: {
2034
+ readonly medium: 500;
2035
+ readonly semibold: 600;
2036
+ readonly bold: 700;
2037
+ };
2038
+ /** The typography slots that can declare a family, weight and weights array. */
2039
+ declare const SLOTS: readonly ["heading", "display", "body", "mono"];
2040
+ type WeightedSlot = {
2041
+ weight?: number;
2042
+ weights?: number[];
2043
+ } | undefined;
2044
+ type SlottedTypography = Partial<Record<(typeof SLOTS)[number], WeightedSlot>>;
2045
+ /**
2046
+ * The weights a theme actually loads: each slot's explicit `weights` array, or
2047
+ * its single `weight` when it declares none — mirroring how the font pipeline
2048
+ * resolves the set it fetches.
2049
+ */
2050
+ declare function loadedWeights(typography: SlottedTypography): number[];
2051
+ /**
2052
+ * Whether any slot declares an explicit `weights` array.
2053
+ *
2054
+ * Resolution is gated on this. A theme that declares one has told us what it
2055
+ * loads, so we can hold every emitted token to that set. A theme that hasn't
2056
+ * keeps the historical emission untouched — snapping against a set inferred
2057
+ * from three `weight` fields would move tokens on a claim the theme never made
2058
+ * (and says nothing at all about a system font stack, which has whatever the OS
2059
+ * has). `validateFontWeights` warns about those instead.
2060
+ */
2061
+ declare function declaresWeights(typography: SlottedTypography): boolean;
2062
+ /**
2063
+ * Pick the weight a browser would actually use for `desired`, given the faces
2064
+ * the theme loaded — CSS Fonts 4 §5.2 weight-matching order:
2065
+ *
2066
+ * - exact match wins;
2067
+ * - desired 400–500: heavier faces up to 500 ascending, then lighter descending,
2068
+ * then heavier than 500 ascending;
2069
+ * - desired below 400: lighter descending, then heavier ascending;
2070
+ * - desired above 500: heavier ascending, then lighter descending.
2071
+ *
2072
+ * Returns `desired` unchanged when the theme loads nothing (nothing to match
2073
+ * against, and the system font has its own ideas).
2074
+ */
2075
+ declare function matchLoadedWeight(desired: number, loaded: readonly number[]): number;
2076
+ /** The four named ramp steps a theme emits, resolved against what it loaded. */
2077
+ interface ResolvedWeightRamp {
2078
+ normal: number;
2079
+ medium: number;
2080
+ semibold: number;
2081
+ bold: number;
2082
+ }
2083
+ /**
2084
+ * Resolve the named ramp.
2085
+ *
2086
+ * `normal` tracks the theme's body weight rather than a hard 400 — it is the
2087
+ * ramp's zero point, and Blacklight's `body.weight: 300` is a deliberate
2088
+ * choice, not a rounding error. It is still coerced to a loaded face (strata
2089
+ * declares `body.weight: 450` and loads `[400, 700, 900]`).
2090
+ *
2091
+ * `medium / semibold / bold` resolve the canonical 500 / 600 / 700 targets.
2092
+ * Names can still share a value on a family that cannot separate them — two
2093
+ * loaded faces cannot back four distinct names — but the result is now the
2094
+ * truth about what renders rather than a number nothing will honour.
2095
+ */
2096
+ declare function resolveWeightRamp(typography: SlottedTypography & {
2097
+ body?: WeightedSlot;
2098
+ }): ResolvedWeightRamp;
2099
+ /**
2100
+ * Resolve a role weight (`--weight-heading`, `--weight-body`,
2101
+ * `--weight-display`) to a loaded face. Roles carry theme intent — what this
2102
+ * theme's headings weigh — and are the channel components should read when they
2103
+ * mean "the heading weight" rather than "a step above medium".
2104
+ */
2105
+ declare function resolveRoleWeight(declared: number, typography: SlottedTypography): number;
2106
+ /**
2107
+ * Emit the shared weight declarations: the named ramp, the role tokens, and the
2108
+ * discrete ladder.
2109
+ *
2110
+ * The ladder (`--font-weight-300` … `--font-weight-800`, one per loaded face)
2111
+ * is what makes the `weights` array authoritative instead of decorative. Before
2112
+ * it, a theme could load five faces and reach four of them by name at best —
2113
+ * Blacklight's 800 was unreachable from CSS, so BL-987 had to write a literal.
2114
+ * It follows the `--text-N` / `--space-N` discrete-alias precedent from VI-451.
2115
+ */
2116
+ declare function generateFontWeightDecls(typography: {
2117
+ heading: {
2118
+ weight: number;
2119
+ weights?: number[];
2120
+ };
2121
+ display: {
2122
+ weight: number;
2123
+ weights?: number[];
2124
+ };
2125
+ body: {
2126
+ weight: number;
2127
+ weights?: number[];
2128
+ };
2129
+ mono: {
2130
+ weight?: number;
2131
+ weights?: number[];
2132
+ };
2133
+ }): string[];
2134
+
1925
2135
  /**
1926
2136
  * Theme Extraction Engine
1927
2137
  *
@@ -1997,4 +2207,4 @@ declare function cleanFontValue(val: string): string;
1997
2207
  */
1998
2208
  declare function extractFromCSS(files: CSSFile[], name?: string): ExtractionResult;
1999
2209
 
2000
- 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 };
2210
+ 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, type ResolvedWeightRamp, SEMANTIC_MAP, SelectiveShadeScale, SemanticTokens, SerializedBrandStrategy, ShadeStep, TAILWIND_GRAY, ThemeBrandResult, ThemeData, ThemeFontResult, ThemeOutput, type ThemeValidationResult, VISOR_BRANDS_CDN, VISOR_CORE_SEMANTIC_ALIASES, VISOR_DEFAULT_BRAND_PATH, VISOR_FONTS_CDN, type ValidateOptions, type ValidationIssue, type ValidationSeverity, VisorBrand, VisorThemeConfig, VisorTypography, WEIGHT_RAMP_TARGETS, applyOverrides, assignSemanticTokens, buildVisorBrandUrl, buildVisorFontUrl, checkBrandStrategyCoherence, checkBrandStrategyStructure, clampToSrgb, cleanFontValue, collectBrandPassthrough, collectDeclaredProperties, compositeOverBackground, declaresWeights, exportTheme, extractFromCSS, formatFontCoverageError, generateDarkCss, generateFontWeightDecls, generateFullBundleCss, generateLightCss, generatePreloadLinks, generatePrimitives, generatePrimitivesCss, generateSemanticAliasDecls, generateSemanticCss, generateShadeScale, generateStylesheetLinks, generateTheme, generateThemeData, generateThemeDataFromConfig, generateThemeFromConfig, getContrastRatio, getKnownTokenRefs, getLuminance, googleFontsCatalog, hasBrandPassthrough, hexToOklch, hexToRgb, isValidColor, isValidHex, isVisorThemeConfig, loadedWeights, lookupFontWeightAlias, lookupGoogleFont, matchLoadedWeight, normalizeHex, oklchToHex, parseCSSDeclarations, parseColor, parseConfig, parseFontFaceDeclarations, parseHex, parseHsla, parseOklch, parseRgba, resolveBrandSlot, resolveBrandSource, resolveConfig, resolveFont, resolveRoleWeight, resolveThemeBrand, resolveThemeFonts, resolveWeightRamp, rgbToHex, serializeBrandStrategy, serializeColor, validate, validateBrandStrategy, validateConfig, validateFontCoverage, visorTheme_schema as visorThemeSchema };
package/dist/index.js CHANGED
@@ -7,21 +7,27 @@ import {
7
7
  MATERIAL_TEXT_SLOTS,
8
8
  TAILWIND_GRAY,
9
9
  VISOR_BRANDS_CDN,
10
+ VISOR_CORE_SEMANTIC_ALIASES,
10
11
  VISOR_DEFAULT_BRAND_PATH,
11
12
  VISOR_FONTS_CDN,
13
+ WEIGHT_RAMP_TARGETS,
12
14
  allComponentTokenNames,
13
15
  applyOverrides,
14
16
  buildVisorBrandUrl,
15
17
  buildVisorFontUrl,
16
18
  clampToSrgb,
17
19
  collectBrandPassthrough,
20
+ collectDeclaredProperties,
18
21
  componentTokenName,
19
22
  compositeOverBackground,
23
+ declaresWeights,
20
24
  generateDarkCss,
25
+ generateFontWeightDecls,
21
26
  generateFullBundleCss,
22
27
  generateLightCss,
23
28
  generatePreloadLinks,
24
29
  generatePrimitivesCss,
30
+ generateSemanticAliasDecls,
25
31
  generateSemanticCss,
26
32
  generateShadeScale,
27
33
  generateStylesheetLinks,
@@ -34,8 +40,10 @@ import {
34
40
  hexToRgb,
35
41
  isValidColor,
36
42
  isValidHex,
43
+ loadedWeights,
37
44
  lookupFontWeightAlias,
38
45
  lookupGoogleFont,
46
+ matchLoadedWeight,
39
47
  normalizeHex,
40
48
  oklchToHex,
41
49
  parseColor,
@@ -47,13 +55,15 @@ import {
47
55
  resolveBrandSource,
48
56
  resolveComponentBindings,
49
57
  resolveFont,
58
+ resolveRoleWeight,
50
59
  resolveThemeBrand,
51
60
  resolveThemeFonts,
61
+ resolveWeightRamp,
52
62
  rgbToHex,
53
63
  rgbToOklch,
54
64
  serializeColor,
55
65
  validateComponentBindings
56
- } from "./chunk-32DC5DAR.js";
66
+ } from "./chunk-LY5LXQYE.js";
57
67
 
58
68
  // src/fonts/validate-coverage.ts
59
69
  var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
@@ -793,7 +803,7 @@ var visor_theme_schema_default = {
793
803
  },
794
804
  scale: {
795
805
  type: "number",
796
- description: "Type scale multiplier applied to the font-size ramp. Default: 1."
806
+ description: "Type scale multiplier applied to the font-size ramp: every --font-size-* step is multiplied by it, and the page inherits the scaled --font-size-base. Default: 1. CSS adapters only \u2014 the Flutter adapter emits Material 3 slot sizes verbatim and ignores this field."
797
807
  },
798
808
  "text-on-light": {
799
809
  type: "string",
@@ -3137,6 +3147,46 @@ function checkTypeScaleCoherence(config, issues) {
3137
3147
  }
3138
3148
  }
3139
3149
  }
3150
+ function checkFontWeightCoverage(config, issues) {
3151
+ const typography = config.typography;
3152
+ if (!typography) return;
3153
+ const HOSTED = /* @__PURE__ */ new Set(["visor-fonts", "google-fonts", "fontshare"]);
3154
+ const slots = ["heading", "display", "body", "mono"];
3155
+ if (!declaresWeights(typography)) {
3156
+ const hosted = slots.filter((slot) => {
3157
+ const source = typography[slot]?.source;
3158
+ return typeof source === "string" && HOSTED.has(source);
3159
+ });
3160
+ if (hosted.length > 0) {
3161
+ issues.push(
3162
+ issue(
3163
+ "warning",
3164
+ "FONT_WEIGHTS_UNDECLARED",
3165
+ `typography.${hosted[0]} loads from '${typography[hosted[0]].source}' but no slot declares a 'weights' array. Named weight tokens fall back to the canonical 500/600/700 literals, which may not be faces this theme fetched. Declare 'weights' to make them resolve against what you actually load.`,
3166
+ `typography.${hosted[0]}.weights`
3167
+ )
3168
+ );
3169
+ }
3170
+ return;
3171
+ }
3172
+ const loaded = loadedWeights(typography);
3173
+ const ramp = resolveWeightRamp(typography);
3174
+ const collapsed = [
3175
+ ["medium", "normal"],
3176
+ ["semibold", "medium"],
3177
+ ["bold", "semibold"]
3178
+ ].filter(([heavier, lighter]) => ramp[heavier] === ramp[lighter]).map(([heavier, lighter]) => `${heavier} = ${lighter} (${ramp[heavier]})`);
3179
+ if (collapsed.length > 0) {
3180
+ issues.push(
3181
+ issue(
3182
+ "warning",
3183
+ "FONT_WEIGHT_RAMP_COLLAPSED",
3184
+ `Named weight steps share a value because this theme loads only [${loaded.join(", ")}]: ${collapsed.join(", ")}. Asking for the heavier name renders no differently from the lighter one. This is what the browser already does \u2014 load an intermediate face, or use the discrete ladder (--font-weight-${loaded.join(" / --font-weight-")}) where a real step is needed.`,
3185
+ "typography"
3186
+ )
3187
+ );
3188
+ }
3189
+ }
3140
3190
  function checkLetterSpacing(config, issues) {
3141
3191
  const ls = config.typography?.["letter-spacing"];
3142
3192
  if (!ls) return;
@@ -3643,6 +3693,7 @@ function validate(config, options) {
3643
3693
  const typedConfig = config;
3644
3694
  checkCompleteness(typedConfig, errors);
3645
3695
  checkTypeScaleCoherence(typedConfig, errors);
3696
+ checkFontWeightCoverage(typedConfig, warnings);
3646
3697
  checkLetterSpacing(typedConfig, errors);
3647
3698
  checkMotionEasing(typedConfig, errors);
3648
3699
  const durationIssues = [];
@@ -4214,8 +4265,10 @@ export {
4214
4265
  SEMANTIC_MAP,
4215
4266
  TAILWIND_GRAY,
4216
4267
  VISOR_BRANDS_CDN,
4268
+ VISOR_CORE_SEMANTIC_ALIASES,
4217
4269
  VISOR_DEFAULT_BRAND_PATH,
4218
4270
  VISOR_FONTS_CDN,
4271
+ WEIGHT_RAMP_TARGETS,
4219
4272
  allComponentTokenNames,
4220
4273
  applyOverrides,
4221
4274
  assignSemanticTokens,
@@ -4226,17 +4279,21 @@ export {
4226
4279
  clampToSrgb,
4227
4280
  cleanFontValue,
4228
4281
  collectBrandPassthrough,
4282
+ collectDeclaredProperties,
4229
4283
  componentTokenName,
4230
4284
  compositeOverBackground,
4285
+ declaresWeights,
4231
4286
  exportTheme,
4232
4287
  extractFromCSS,
4233
4288
  formatFontCoverageError,
4234
4289
  generateDarkCss,
4290
+ generateFontWeightDecls,
4235
4291
  generateFullBundleCss,
4236
4292
  generateLightCss,
4237
4293
  generatePreloadLinks,
4238
4294
  generatePrimitives,
4239
4295
  generatePrimitivesCss,
4296
+ generateSemanticAliasDecls,
4240
4297
  generateSemanticCss,
4241
4298
  generateShadeScale,
4242
4299
  generateStylesheetLinks,
@@ -4255,8 +4312,10 @@ export {
4255
4312
  isValidColor,
4256
4313
  isValidHex,
4257
4314
  isVisorThemeConfig,
4315
+ loadedWeights,
4258
4316
  lookupFontWeightAlias,
4259
4317
  lookupGoogleFont,
4318
+ matchLoadedWeight,
4260
4319
  normalizeHex,
4261
4320
  oklchToHex,
4262
4321
  parseCSSDeclarations,
@@ -4272,8 +4331,10 @@ export {
4272
4331
  resolveComponentBindings,
4273
4332
  resolveConfig,
4274
4333
  resolveFont,
4334
+ resolveRoleWeight,
4275
4335
  resolveThemeBrand,
4276
4336
  resolveThemeFonts,
4337
+ resolveWeightRamp,
4277
4338
  rgbToHex,
4278
4339
  serializeBrandStrategy,
4279
4340
  serializeColor,
@@ -678,6 +678,12 @@ interface VisorThemeConfig {
678
678
  info?: string;
679
679
  };
680
680
  typography?: {
681
+ /**
682
+ * Type-scale multiplier applied to the `--font-size-*` ramp (VI-638).
683
+ * Every step is multiplied by it, and the page inherits the scaled
684
+ * `--font-size-base`. CSS adapters only — the Flutter adapter emits
685
+ * Material 3 slot sizes verbatim. Default: 1.
686
+ */
681
687
  scale?: number;
682
688
  /**
683
689
  * VI-375: text color placed on a LIGHT interactive background. Auto-picked
@@ -867,6 +873,12 @@ interface ResolvedThemeConfig {
867
873
  };
868
874
  "colors-dark"?: VisorThemeConfig["colors-dark"];
869
875
  typography: {
876
+ /**
877
+ * Type-scale multiplier applied to the `--font-size-*` ramp (VI-638).
878
+ * Every step is multiplied by it, and the page inherits the scaled
879
+ * `--font-size-base`. CSS adapters only — the Flutter adapter emits
880
+ * Material 3 slot sizes verbatim. Default: 1.
881
+ */
870
882
  scale: number;
871
883
  /**
872
884
  * VI-375: resolved default text color for LIGHT interactive backgrounds.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",
@@ -187,7 +187,7 @@
187
187
  },
188
188
  "scale": {
189
189
  "type": "number",
190
- "description": "Type scale multiplier applied to the font-size ramp. Default: 1."
190
+ "description": "Type scale multiplier applied to the font-size ramp: every --font-size-* step is multiplied by it, and the page inherits the scaled --font-size-base. Default: 1. CSS adapters only — the Flutter adapter emits Material 3 slot sizes verbatim and ignores this field."
191
191
  },
192
192
  "text-on-light": {
193
193
  "type": "string",