@loworbitstudio/visor-theme-engine 0.4.1 → 0.4.2

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 { c as GeneratedPrimitives, i as SemanticTokens, R as ResolvedThemeConfig } from '../types-gAlkt__C.js';
1
+ import { c as GeneratedPrimitives, i as SemanticTokens, R as ResolvedThemeConfig } from '../types-ljcTtODU.js';
2
2
 
3
3
  /**
4
4
  * Adapter types for the Visor theme engine.
@@ -139,8 +139,10 @@ declare function deckAdapter(input: AdapterInput, options?: DeckAdapterOptions):
139
139
  /**
140
140
  * Generate docs-site CSS for a theme.
141
141
  *
142
- * Output is class-scoped (.{slug}-theme) with no @layer wrapping,
143
- * matching the hand-written theme files in packages/docs/app/.
142
+ * Output is class-scoped (.{slug}-theme) and wrapped in @layer visor-adaptive
143
+ * so consumer overrides (unlayered) and visor-core's own visor-adaptive layer
144
+ * cascade correctly. Font @import / @font-face statements stay outside the
145
+ * layer block per CSS spec (VI-312).
144
146
  */
145
147
  declare function docsAdapter(input: AdapterInput, options?: DocsAdapterOptions): string;
146
148
 
@@ -177,9 +179,11 @@ declare function flutterAdapter(input: AdapterInput, options?: FlutterAdapterOpt
177
179
  /**
178
180
  * CSS @layer utilities for adapter output.
179
181
  *
180
- * Establishes a specificity ordering so theme overrides work
181
- * without !important. Layers are adapter-only the base
182
- * generateFullBundleCss output is not wrapped in layers.
182
+ * Establishes a specificity ordering so theme overrides work without
183
+ * !important. Both adapter output (here) and visor-core's emitted CSS
184
+ * (packages/tokens/src/generate/generate-css.ts) declare this same layer
185
+ * order — defense in depth, so whichever stylesheet loads first establishes
186
+ * the cascade.
183
187
  */
184
188
  /** Layer order declaration — must appear before any @layer blocks. */
185
189
  declare const LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
@@ -11,7 +11,7 @@ import {
11
11
  parseColor,
12
12
  resolveThemeFonts,
13
13
  sectionComment
14
- } from "../chunk-G4B57FB3.js";
14
+ } from "../chunk-SXT2KY6D.js";
15
15
 
16
16
  // src/adapters/layers.ts
17
17
  var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
@@ -418,6 +418,7 @@ function docsAdapter(input, options) {
418
418
  const slug = toKebabCase2(input.config.name);
419
419
  const scopeClass = `.${slug}-theme`;
420
420
  const includeFontImports = options?.includeFontImports ?? true;
421
+ const fontLines = [];
421
422
  const lines = [];
422
423
  if (includeFontImports && input.config.typography) {
423
424
  const fontResult = resolveThemeFonts(input.config.typography);
@@ -426,8 +427,8 @@ function docsAdapter(input, options) {
426
427
  for (const font of fontSlots) {
427
428
  if (font && font.source === "google-fonts" && font.cssUrl && !seenUrls.has(font.cssUrl)) {
428
429
  seenUrls.add(font.cssUrl);
429
- lines.push(`@import url("${font.cssUrl}");`);
430
- lines.push("");
430
+ fontLines.push(`@import url("${font.cssUrl}");`);
431
+ fontLines.push("");
431
432
  }
432
433
  }
433
434
  const scale = input.config.typography?.scale ?? 1;
@@ -437,17 +438,17 @@ function docsAdapter(input, options) {
437
438
  seenFamilies.add(font.family);
438
439
  for (const weight of font.weights) {
439
440
  const url = buildVisorFontUrl(font.org ?? "", font.family, weight);
440
- lines.push("@font-face {");
441
- lines.push(` font-family: "${font.family}";`);
442
- lines.push(` src: url("${url}") format("woff2");`);
443
- lines.push(` font-weight: ${weight};`);
444
- lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
445
- lines.push(` font-display: ${font.display};`);
441
+ fontLines.push("@font-face {");
442
+ fontLines.push(` font-family: "${font.family}";`);
443
+ fontLines.push(` src: url("${url}") format("woff2");`);
444
+ fontLines.push(` font-weight: ${weight};`);
445
+ fontLines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
446
+ fontLines.push(` font-display: ${font.display};`);
446
447
  if (scale !== 1) {
447
- lines.push(` size-adjust: ${Math.round(scale * 100)}%;`);
448
+ fontLines.push(` size-adjust: ${Math.round(scale * 100)}%;`);
448
449
  }
449
- lines.push("}");
450
- lines.push("");
450
+ fontLines.push("}");
451
+ fontLines.push("");
451
452
  }
452
453
  }
453
454
  }
@@ -562,7 +563,9 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
562
563
  lines.push(sectionComment2("Fumadocs bridge: light"));
563
564
  lines.push(block(`html:not(.dark) ${scopeClass}`, generateFumadocsBridgeDecls(input.tokens, "light")));
564
565
  lines.push("");
565
- return lines.join("\n") + "\n";
566
+ const layered = wrapInLayer("visor-adaptive", lines.join("\n").trim());
567
+ const head = fontLines.length > 0 ? fontLines.join("\n") + "\n" : "";
568
+ return head + LAYER_ORDER + "\n\n" + layered + "\n";
566
569
  }
567
570
 
568
571
  // src/flutter/color-to-dart.ts
@@ -964,8 +964,11 @@ var LIGHTNESS_TARGETS = {
964
964
  200: 0.87,
965
965
  300: 0.78,
966
966
  400: 0.65,
967
- 500: -1,
968
- // placeholder replaced by input L at anchor (brand color lives at 500)
967
+ // 500 is the reference midpoint (Tailwind gray-500's OKLCH L ≈ 0.55).
968
+ // At the anchor shade the input L is used directly; this value seeds the
969
+ // interpolation math in computeLightness() for inputs that don't land at
970
+ // the reference midpoint.
971
+ 500: 0.55,
969
972
  600: 0.45,
970
973
  700: 0.38,
971
974
  800: 0.3,
@@ -1012,20 +1015,20 @@ function computeLightness(step, inputL, anchorShade) {
1012
1015
  if (step === anchorShade) {
1013
1016
  return inputL;
1014
1017
  }
1015
- const anchorTarget = anchorShade === 600 ? inputL : LIGHTNESS_TARGETS[anchorShade];
1018
+ const anchorTarget = LIGHTNESS_TARGETS[anchorShade];
1016
1019
  const stepTarget = LIGHTNESS_TARGETS[step];
1017
1020
  if (Math.abs(anchorTarget - inputL) < 0.01) {
1018
1021
  return stepTarget;
1019
1022
  }
1020
1023
  if (step < anchorShade) {
1021
- const anchorDefaultL = anchorShade === 600 ? 0.45 : LIGHTNESS_TARGETS[anchorShade];
1024
+ const anchorDefaultL = LIGHTNESS_TARGETS[anchorShade];
1022
1025
  const rawRange = 0.97 - anchorDefaultL;
1023
1026
  const newRange = 0.97 - inputL;
1024
1027
  if (rawRange <= 0) return stepTarget;
1025
1028
  const t = (stepTarget - anchorDefaultL) / rawRange;
1026
1029
  return inputL + t * newRange;
1027
1030
  } else {
1028
- const anchorDefaultL = anchorShade === 600 ? 0.45 : LIGHTNESS_TARGETS[anchorShade];
1031
+ const anchorDefaultL = LIGHTNESS_TARGETS[anchorShade];
1029
1032
  const rawRange = anchorDefaultL - 0.14;
1030
1033
  const newRange = inputL - 0.14;
1031
1034
  if (rawRange <= 0) return stepTarget;
@@ -1043,7 +1046,7 @@ function generateShadeScale(color, role) {
1043
1046
  for (const step of steps) {
1044
1047
  const targetL = computeLightness(step, inputL, anchorShade);
1045
1048
  let targetC = inputC * CHROMA_MULTIPLIERS[step];
1046
- if (role === "neutral") {
1049
+ if (role === "neutral" && step !== anchorShade) {
1047
1050
  targetC = Math.min(targetC, maxNeutralChroma);
1048
1051
  }
1049
1052
  scale[step] = oklchToHex(targetL, targetC, inputH);
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, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-gAlkt__C.js';
2
- export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-gAlkt__C.js';
1
+ import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-ljcTtODU.js';
2
+ export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-ljcTtODU.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -556,6 +556,20 @@ var properties = {
556
556
  }
557
557
  }
558
558
  }
559
+ },
560
+ migrate: {
561
+ type: "object",
562
+ description: "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
563
+ additionalProperties: false,
564
+ properties: {
565
+ "token-substitution": {
566
+ type: "object",
567
+ description: "§3.1 V7-primitive → Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
568
+ additionalProperties: {
569
+ type: "string"
570
+ }
571
+ }
572
+ }
559
573
  }
560
574
  };
561
575
  var $defs = {
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  rgbToHex,
33
33
  rgbToOklch,
34
34
  serializeColor
35
- } from "./chunk-G4B57FB3.js";
35
+ } from "./chunk-SXT2KY6D.js";
36
36
 
37
37
  // src/pipeline.ts
38
38
  import { parse as parseYaml } from "yaml";
@@ -339,6 +339,18 @@ var visor_theme_schema_default = {
339
339
  additionalProperties: { type: "string" }
340
340
  }
341
341
  }
342
+ },
343
+ migrate: {
344
+ type: "object",
345
+ description: "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
346
+ additionalProperties: false,
347
+ properties: {
348
+ "token-substitution": {
349
+ type: "object",
350
+ description: "\xA73.1 V7-primitive \u2192 Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
351
+ additionalProperties: { type: "string" }
352
+ }
353
+ }
342
354
  }
343
355
  },
344
356
  $defs: {
@@ -839,13 +851,17 @@ var SEMANTIC_TEXT_MAP = {
839
851
  light: { role: "neutral", shade: 900 },
840
852
  dark: { role: "neutral", shade: 50 }
841
853
  },
854
+ // VI-346: rebalanced to fixed-L shades (700/300, 600/400) so AA contrast holds
855
+ // by default for any reasonable input neutral. Shade 500 is brand-anchored
856
+ // (variable L = input itself) and therefore forbidden for any text level that
857
+ // must clear AA. See docs/token-rules.md and SEMANTIC_TEXT_MAP rationale.
842
858
  secondary: {
843
- light: { role: "neutral", shade: 600 },
844
- dark: { role: "neutral", shade: 400 }
859
+ light: { role: "neutral", shade: 700 },
860
+ dark: { role: "neutral", shade: 300 }
845
861
  },
846
862
  tertiary: {
847
- light: { role: "neutral", shade: 400 },
848
- dark: { role: "neutral", shade: 500 }
863
+ light: { role: "neutral", shade: 600 },
864
+ dark: { role: "neutral", shade: 400 }
849
865
  },
850
866
  disabled: {
851
867
  light: { role: "neutral", shade: 300 },
@@ -976,6 +992,29 @@ var SEMANTIC_SURFACE_MAP = {
976
992
  "info-default": {
977
993
  light: { role: "info", shade: 500 },
978
994
  dark: { role: "info", shade: 500 }
995
+ },
996
+ // 5-tier ordinal elevation scale — deepest (0) to highest (4)
997
+ // Light mode: BO-10 near-white ramp (white → neutral-300).
998
+ // Dark mode: deep neutral ramp (neutral-950 → neutral-600).
999
+ "elev-0": {
1000
+ light: { constant: "#ffffff" },
1001
+ dark: { role: "neutral", shade: 950 }
1002
+ },
1003
+ "elev-1": {
1004
+ light: { role: "neutral", shade: 50 },
1005
+ dark: { role: "neutral", shade: 900 }
1006
+ },
1007
+ "elev-2": {
1008
+ light: { role: "neutral", shade: 100 },
1009
+ dark: { role: "neutral", shade: 800 }
1010
+ },
1011
+ "elev-3": {
1012
+ light: { role: "neutral", shade: 200 },
1013
+ dark: { role: "neutral", shade: 700 }
1014
+ },
1015
+ "elev-4": {
1016
+ light: { role: "neutral", shade: 300 },
1017
+ dark: { role: "neutral", shade: 600 }
979
1018
  }
980
1019
  };
981
1020
  var SEMANTIC_BORDER_MAP = {
@@ -1826,35 +1865,75 @@ function colorToRgb(color) {
1826
1865
  const parsed = parseColor(color);
1827
1866
  return parsed ? parsed.rgb : [0, 0, 0];
1828
1867
  }
1868
+ var STANDARD_TEXT_TOKENS = ["primary", "secondary", "tertiary"];
1869
+ var STATUS_TEXT_TOKENS = ["error", "warning", "success", "info"];
1829
1870
  function checkContrastWarnings(config, issues) {
1830
1871
  const resolved = resolveConfig(config);
1872
+ const lightPrimitives = generatePrimitives(resolved);
1873
+ const darkPrimitives = generateDarkPrimitives(resolved, lightPrimitives);
1874
+ const tokens = applyOverrides(
1875
+ assignSemanticTokens(lightPrimitives, darkPrimitives, resolved),
1876
+ resolved.overrides
1877
+ );
1831
1878
  const lightBg = resolved.colors.background;
1832
1879
  const lightSurface = resolved.colors.surface;
1833
1880
  const primary = resolved.colors.primary;
1834
1881
  const lightBgRgb = colorToRgb(lightBg);
1835
1882
  const lightSurfaceRgb = colorToRgb(lightSurface);
1836
- const textDark = "#111827";
1837
- const textOnBg = getContrastRatio(textDark, lightBg, lightBgRgb);
1838
- if (textOnBg < CONTRAST_TEXT_AA) {
1839
- issues.push(
1840
- issue(
1841
- "warning",
1842
- "WCAG_CONTRAST",
1843
- `Light mode: text-primary on background has contrast ratio ${textOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1844
- "colors.background"
1845
- )
1846
- );
1847
- }
1848
- const textOnSurface = getContrastRatio(textDark, lightSurface, lightSurfaceRgb);
1849
- if (textOnSurface < CONTRAST_TEXT_AA) {
1850
- issues.push(
1851
- issue(
1852
- "warning",
1853
- "WCAG_CONTRAST",
1854
- `Light mode: text-primary on surface has contrast ratio ${textOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1855
- "colors.surface"
1856
- )
1857
- );
1883
+ const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
1884
+ const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
1885
+ const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
1886
+ const darkBgRgb = colorToRgb(darkBg);
1887
+ const darkSurfaceRgb = colorToRgb(darkSurface);
1888
+ const textTokensToCheck = [...STANDARD_TEXT_TOKENS, ...STATUS_TEXT_TOKENS];
1889
+ for (const tokenName of textTokensToCheck) {
1890
+ const tokenValue = tokens.text[tokenName];
1891
+ if (!tokenValue) continue;
1892
+ const fullName = `text-${tokenName}`;
1893
+ const lightOnBg = getContrastRatio(tokenValue.light, lightBg, lightBgRgb);
1894
+ if (lightOnBg < CONTRAST_TEXT_AA) {
1895
+ issues.push(
1896
+ issue(
1897
+ "warning",
1898
+ "WCAG_CONTRAST",
1899
+ `Light mode: ${fullName} on background has contrast ratio ${lightOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1900
+ "colors.background"
1901
+ )
1902
+ );
1903
+ }
1904
+ const lightOnSurface = getContrastRatio(tokenValue.light, lightSurface, lightSurfaceRgb);
1905
+ if (lightOnSurface < CONTRAST_TEXT_AA) {
1906
+ issues.push(
1907
+ issue(
1908
+ "warning",
1909
+ "WCAG_CONTRAST",
1910
+ `Light mode: ${fullName} on surface has contrast ratio ${lightOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1911
+ "colors.surface"
1912
+ )
1913
+ );
1914
+ }
1915
+ const darkOnBg = getContrastRatio(tokenValue.dark, darkBg, darkBgRgb);
1916
+ if (darkOnBg < CONTRAST_TEXT_AA) {
1917
+ issues.push(
1918
+ issue(
1919
+ "warning",
1920
+ "WCAG_CONTRAST",
1921
+ `Dark mode: ${fullName} on background has contrast ratio ${darkOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1922
+ "colors-dark.background"
1923
+ )
1924
+ );
1925
+ }
1926
+ const darkOnSurface = getContrastRatio(tokenValue.dark, darkSurface, darkSurfaceRgb);
1927
+ if (darkOnSurface < CONTRAST_TEXT_AA) {
1928
+ issues.push(
1929
+ issue(
1930
+ "warning",
1931
+ "WCAG_CONTRAST",
1932
+ `Dark mode: ${fullName} on surface has contrast ratio ${darkOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1933
+ "colors-dark.surface"
1934
+ )
1935
+ );
1936
+ }
1858
1937
  }
1859
1938
  const primaryOnBg = getContrastRatio(primary, lightBg, lightBgRgb);
1860
1939
  if (primaryOnBg < CONTRAST_INTERACTIVE_AA) {
@@ -1878,34 +1957,6 @@ function checkContrastWarnings(config, issues) {
1878
1957
  )
1879
1958
  );
1880
1959
  }
1881
- const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
1882
- const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
1883
- const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
1884
- const darkBgRgb = colorToRgb(darkBg);
1885
- const darkSurfaceRgb = colorToRgb(darkSurface);
1886
- const textLight = "#f9fafb";
1887
- const textOnDarkBg = getContrastRatio(textLight, darkBg, darkBgRgb);
1888
- if (textOnDarkBg < CONTRAST_TEXT_AA) {
1889
- issues.push(
1890
- issue(
1891
- "warning",
1892
- "WCAG_CONTRAST",
1893
- `Dark mode: text-primary on background has contrast ratio ${textOnDarkBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1894
- "colors-dark.background"
1895
- )
1896
- );
1897
- }
1898
- const textOnDarkSurface = getContrastRatio(textLight, darkSurface, darkSurfaceRgb);
1899
- if (textOnDarkSurface < CONTRAST_TEXT_AA) {
1900
- issues.push(
1901
- issue(
1902
- "warning",
1903
- "WCAG_CONTRAST",
1904
- `Dark mode: text-primary on surface has contrast ratio ${textOnDarkSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
1905
- "colors-dark.surface"
1906
- )
1907
- );
1908
- }
1909
1960
  const darkPrimaryOnBg = getContrastRatio(darkPrimary, darkBg, darkBgRgb);
1910
1961
  if (darkPrimaryOnBg < CONTRAST_INTERACTIVE_AA) {
1911
1962
  issues.push(
@@ -247,6 +247,21 @@ interface VisorThemeConfig {
247
247
  light?: Record<string, string>;
248
248
  dark?: Record<string, string>;
249
249
  };
250
+ /**
251
+ * Migration metadata — optional, additive, consumed by `visor migrate` commands.
252
+ * Does not affect CSS generation or theme application.
253
+ */
254
+ migrate?: {
255
+ /**
256
+ * §3.1 V7-primitive → Visor-semantic substitution table.
257
+ * Maps V7 CSS custom property names (without `var()`) to Visor semantic
258
+ * token names. Consumed by `visor migrate token-substitution`.
259
+ *
260
+ * @example
261
+ * { "--panel": "--surface-card", "--text": "--text-primary" }
262
+ */
263
+ "token-substitution"?: Record<string, string>;
264
+ };
250
265
  }
251
266
  /** Config with all defaults resolved */
252
267
  interface ResolvedThemeConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",
@@ -299,6 +299,18 @@
299
299
  "additionalProperties": { "type": "string" }
300
300
  }
301
301
  }
302
+ },
303
+ "migrate": {
304
+ "type": "object",
305
+ "description": "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
306
+ "additionalProperties": false,
307
+ "properties": {
308
+ "token-substitution": {
309
+ "type": "object",
310
+ "description": "§3.1 V7-primitive → Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
311
+ "additionalProperties": { "type": "string" }
312
+ }
313
+ }
302
314
  }
303
315
  },
304
316
  "$defs": {