@tenphi/glaze 0.0.0-snapshot.718119b → 0.0.0-snapshot.78261ef

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.mjs CHANGED
@@ -618,7 +618,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
618
618
  function findLightnessForContrast(options) {
619
619
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
620
620
  const target = resolveMinContrast(contrastInput);
621
- const searchTarget = target * 1.005;
621
+ const searchTarget = target * 1.007;
622
622
  const yBase = gamutClampedLuminance(baseLinearRgb);
623
623
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
624
624
  if (crPref >= searchTarget) return {
@@ -812,6 +812,7 @@ let globalConfig = {
812
812
  lightLightness: [10, 100],
813
813
  darkLightness: [15, 95],
814
814
  darkDesaturation: .1,
815
+ darkCurve: .5,
815
816
  states: {
816
817
  dark: "@dark",
817
818
  highContrast: "@high-contrast"
@@ -840,7 +841,8 @@ const DEFAULT_SHADOW_TUNING = {
840
841
  lightnessBounds: [.05, .2],
841
842
  minGapTarget: .05,
842
843
  alphaMax: 1,
843
- bgHueBlend: .2
844
+ bgHueBlend: .2,
845
+ darkShadowCurve: .4
844
846
  };
845
847
  function resolveShadowTuning(perColor) {
846
848
  return {
@@ -959,24 +961,42 @@ function topoSort(defs) {
959
961
  for (const name of Object.keys(defs)) visit(name);
960
962
  return result;
961
963
  }
962
- function mapLightnessLight(l, mode) {
964
+ function lightnessWindow(isHighContrast, kind) {
965
+ if (isHighContrast) return [0, 100];
966
+ return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
967
+ }
968
+ function mapLightnessLight(l, mode, isHighContrast) {
963
969
  if (mode === "static") return l;
964
- const [lo, hi] = globalConfig.lightLightness;
970
+ const [lo, hi] = lightnessWindow(isHighContrast, "light");
965
971
  return l * (hi - lo) / 100 + lo;
966
972
  }
967
- function mapLightnessDark(l, mode) {
973
+ function mobiusCurve(t, beta) {
974
+ if (beta >= 1) return t;
975
+ return t / (t + beta * (1 - t));
976
+ }
977
+ function mapLightnessDark(l, mode, isHighContrast) {
968
978
  if (mode === "static") return l;
969
- const [lo, hi] = globalConfig.darkLightness;
970
- if (mode === "fixed") return l * (hi - lo) / 100 + lo;
971
- return (100 - l) * (hi - lo) / 100 + lo;
979
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
980
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
981
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
982
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
983
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
984
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
985
+ }
986
+ function lightMappedToDark(lightL, isHighContrast) {
987
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
988
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
989
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
990
+ const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
991
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
972
992
  }
973
993
  function mapSaturationDark(s, mode) {
974
994
  if (mode === "static") return s;
975
995
  return s * (1 - globalConfig.darkDesaturation);
976
996
  }
977
- function schemeLightnessRange(isDark, mode) {
997
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
978
998
  if (mode === "static") return [0, 1];
979
- const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
999
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
980
1000
  return [lo / 100, hi / 100];
981
1001
  }
982
1002
  function clamp(v, min, max) {
@@ -1035,26 +1055,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1035
1055
  else {
1036
1056
  const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
1037
1057
  if (parsed.relative) {
1038
- let delta = parsed.value;
1039
- if (isDark && mode === "auto") delta = -delta;
1040
- preferredL = clamp(baseL + delta, 0, 100);
1041
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
1042
- else preferredL = mapLightnessLight(parsed.value, mode);
1058
+ const delta = parsed.value;
1059
+ if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast);
1060
+ else preferredL = clamp(baseL + delta, 0, 100);
1061
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1062
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1043
1063
  }
1044
1064
  const rawContrast = def.contrast;
1045
1065
  if (rawContrast !== void 0) {
1046
1066
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1047
1067
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1048
1068
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1049
- const lightnessRange = schemeLightnessRange(isDark, mode);
1069
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
1050
1070
  return {
1051
1071
  l: findLightnessForContrast({
1052
1072
  hue: effectiveHue,
1053
1073
  saturation: effectiveSat,
1054
- preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
1074
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
1055
1075
  baseLinearRgb,
1056
1076
  contrast: minCr,
1057
- lightnessRange
1077
+ lightnessRange: [0, 1]
1058
1078
  }).lightness * 100,
1059
1079
  satFactor
1060
1080
  };
@@ -1091,13 +1111,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1091
1111
  let finalL;
1092
1112
  let finalSat;
1093
1113
  if (isDark && isRoot) {
1094
- finalL = mapLightnessDark(lightL, mode);
1114
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
1095
1115
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1096
1116
  } else if (isDark && !isRoot) {
1097
1117
  finalL = lightL;
1098
1118
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1099
1119
  } else if (isRoot) {
1100
- finalL = mapLightnessLight(lightL, mode);
1120
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
1101
1121
  finalSat = satFactor * ctx.saturation / 100;
1102
1122
  } else {
1103
1123
  finalL = lightL;
@@ -1116,7 +1136,13 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
1116
1136
  if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
1117
1137
  const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
1118
1138
  const tuning = resolveShadowTuning(def.tuning);
1119
- return computeShadow(bgVariant, fgVariant, intensity, tuning);
1139
+ const result = computeShadow(bgVariant, fgVariant, intensity, tuning);
1140
+ if (isDark && tuning.darkShadowCurve < 1 && result.alpha > 0) {
1141
+ const normalized = result.alpha / tuning.alphaMax;
1142
+ const exponent = 1 / tuning.darkShadowCurve;
1143
+ result.alpha = tuning.alphaMax * Math.pow(normalized, exponent);
1144
+ }
1145
+ return result;
1120
1146
  }
1121
1147
  function variantToLinearRgb(v) {
1122
1148
  return okhslToLinearSrgb(v.h, v.s, v.l);
@@ -1394,10 +1420,14 @@ function createTheme(hue, saturation, initialColors) {
1394
1420
  };
1395
1421
  },
1396
1422
  extend(options) {
1397
- return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
1398
- ...colorDefs,
1423
+ const newHue = options.hue ?? hue;
1424
+ const newSat = options.saturation ?? saturation;
1425
+ const inheritedColors = {};
1426
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
1427
+ return createTheme(newHue, newSat, options.colors ? {
1428
+ ...inheritedColors,
1399
1429
  ...options.colors
1400
- } : { ...colorDefs });
1430
+ } : { ...inheritedColors });
1401
1431
  },
1402
1432
  resolve() {
1403
1433
  return resolveAllColors(hue, saturation, colorDefs);
@@ -1431,40 +1461,74 @@ function validatePrimaryTheme(primary, themes) {
1431
1461
  throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1432
1462
  }
1433
1463
  }
1434
- function createPalette(themes) {
1464
+ /**
1465
+ * Resolve the effective primary for an export call.
1466
+ * `false` disables, a string overrides, `undefined` inherits from palette.
1467
+ */
1468
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1469
+ if (exportPrimary === false) return void 0;
1470
+ return exportPrimary ?? palettePrimary;
1471
+ }
1472
+ /**
1473
+ * Filter a resolved color map, skipping keys already in `seen`.
1474
+ * Warns on collision and keeps the first-written value (first-write-wins).
1475
+ * Returns a new map containing only non-colliding entries.
1476
+ */
1477
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1478
+ const filtered = /* @__PURE__ */ new Map();
1479
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
1480
+ for (const [name, color] of resolved) {
1481
+ const key = `${prefix}${name}`;
1482
+ if (seen.has(key)) {
1483
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1484
+ continue;
1485
+ }
1486
+ seen.set(key, label);
1487
+ filtered.set(name, color);
1488
+ }
1489
+ return filtered;
1490
+ }
1491
+ function createPalette(themes, paletteOptions) {
1492
+ validatePrimaryTheme(paletteOptions?.primary, themes);
1435
1493
  return {
1436
1494
  tokens(options) {
1437
- validatePrimaryTheme(options?.primary, themes);
1495
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1496
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1438
1497
  const modes = resolveModes(options?.modes);
1439
1498
  const allTokens = {};
1499
+ const seen = /* @__PURE__ */ new Map();
1440
1500
  for (const [themeName, theme] of Object.entries(themes)) {
1441
1501
  const resolved = theme.resolve();
1442
- const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1502
+ const prefix = resolvePrefix(options, themeName, true);
1503
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1443
1504
  for (const variant of Object.keys(tokens)) {
1444
1505
  if (!allTokens[variant]) allTokens[variant] = {};
1445
1506
  Object.assign(allTokens[variant], tokens[variant]);
1446
1507
  }
1447
- if (themeName === options?.primary) {
1448
- const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1508
+ if (themeName === effectivePrimary) {
1509
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1449
1510
  for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1450
1511
  }
1451
1512
  }
1452
1513
  return allTokens;
1453
1514
  },
1454
1515
  tasty(options) {
1455
- validatePrimaryTheme(options?.primary, themes);
1516
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1517
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1456
1518
  const states = {
1457
1519
  dark: options?.states?.dark ?? globalConfig.states.dark,
1458
1520
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1459
1521
  };
1460
1522
  const modes = resolveModes(options?.modes);
1461
1523
  const allTokens = {};
1524
+ const seen = /* @__PURE__ */ new Map();
1462
1525
  for (const [themeName, theme] of Object.entries(themes)) {
1463
1526
  const resolved = theme.resolve();
1464
- const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1527
+ const prefix = resolvePrefix(options, themeName, true);
1528
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1465
1529
  Object.assign(allTokens, tokens);
1466
- if (themeName === options?.primary) {
1467
- const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1530
+ if (themeName === effectivePrimary) {
1531
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1468
1532
  Object.assign(allTokens, unprefixed);
1469
1533
  }
1470
1534
  }
@@ -1477,7 +1541,8 @@ function createPalette(themes) {
1477
1541
  return result;
1478
1542
  },
1479
1543
  css(options) {
1480
- validatePrimaryTheme(options?.primary, themes);
1544
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1545
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1481
1546
  const suffix = options?.suffix ?? "-color";
1482
1547
  const format = options?.format ?? "rgb";
1483
1548
  const allLines = {
@@ -1486,17 +1551,19 @@ function createPalette(themes) {
1486
1551
  lightContrast: [],
1487
1552
  darkContrast: []
1488
1553
  };
1554
+ const seen = /* @__PURE__ */ new Map();
1489
1555
  for (const [themeName, theme] of Object.entries(themes)) {
1490
1556
  const resolved = theme.resolve();
1491
- const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1557
+ const prefix = resolvePrefix(options, themeName, true);
1558
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1492
1559
  for (const key of [
1493
1560
  "light",
1494
1561
  "dark",
1495
1562
  "lightContrast",
1496
1563
  "darkContrast"
1497
1564
  ]) if (css[key]) allLines[key].push(css[key]);
1498
- if (themeName === options?.primary) {
1499
- const unprefixed = buildCssMap(resolved, "", suffix, format);
1565
+ if (themeName === effectivePrimary) {
1566
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1500
1567
  for (const key of [
1501
1568
  "light",
1502
1569
  "dark",
@@ -1563,6 +1630,7 @@ glaze.configure = function configure(config) {
1563
1630
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1564
1631
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1565
1632
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1633
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1566
1634
  states: {
1567
1635
  dark: config.states?.dark ?? globalConfig.states.dark,
1568
1636
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1577,8 +1645,8 @@ glaze.configure = function configure(config) {
1577
1645
  /**
1578
1646
  * Compose multiple themes into a palette.
1579
1647
  */
1580
- glaze.palette = function palette(themes) {
1581
- return createPalette(themes);
1648
+ glaze.palette = function palette(themes, options) {
1649
+ return createPalette(themes, options);
1582
1650
  };
1583
1651
  /**
1584
1652
  * Create a theme from a serialized export.
@@ -1599,13 +1667,19 @@ glaze.shadow = function shadow(input) {
1599
1667
  const bg = parseOkhslInput(input.bg);
1600
1668
  const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
1601
1669
  const tuning = resolveShadowTuning(input.tuning);
1602
- return computeShadow({
1670
+ const result = computeShadow({
1603
1671
  ...bg,
1604
1672
  alpha: 1
1605
1673
  }, fg ? {
1606
1674
  ...fg,
1607
1675
  alpha: 1
1608
1676
  } : void 0, input.intensity, tuning);
1677
+ if (input.dark && tuning.darkShadowCurve < 1 && result.alpha > 0) {
1678
+ const normalized = result.alpha / tuning.alphaMax;
1679
+ const exponent = 1 / tuning.darkShadowCurve;
1680
+ result.alpha = tuning.alphaMax * Math.pow(normalized, exponent);
1681
+ }
1682
+ return result;
1609
1683
  };
1610
1684
  /**
1611
1685
  * Format a resolved color variant as a CSS string.
@@ -1662,6 +1736,7 @@ glaze.resetConfig = function resetConfig() {
1662
1736
  lightLightness: [10, 100],
1663
1737
  darkLightness: [15, 95],
1664
1738
  darkDesaturation: .1,
1739
+ darkCurve: .5,
1665
1740
  states: {
1666
1741
  dark: "@dark",
1667
1742
  highContrast: "@high-contrast"