@tenphi/glaze 0.0.0-snapshot.c8281e2 → 0.0.0-snapshot.cdd8acc

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
@@ -344,6 +344,11 @@ function gamutClampedLuminance(linearRgb) {
344
344
  const linearSrgbToOklab = (rgb) => {
345
345
  return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
346
346
  };
347
+ /**
348
+ * Convert OKLab to OKHSL.
349
+ * Input: [L, a, b] where L: 0–1, a/b: roughly -0.5 to 0.5.
350
+ * Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
351
+ */
347
352
  const oklabToOkhsl = (lab) => {
348
353
  const L = lab[0];
349
354
  const a = lab[1];
@@ -391,6 +396,39 @@ function srgbToOkhsl(rgb) {
391
396
  ]));
392
397
  }
393
398
  /**
399
+ * Convert CSS HSL (sRGB-based) to gamma-encoded sRGB [r, g, b] in 0–1 range.
400
+ * h: 0–360, s: 0–1, l: 0–1.
401
+ *
402
+ * Note: CSS HSL is not the same as OKHSL — it's HSL in the sRGB color space.
403
+ * Use this when parsing `hsl(...)` strings before passing to `srgbToOkhsl`.
404
+ */
405
+ function hslToSrgb(h, s, l) {
406
+ const hh = (h % 360 + 360) % 360 / 360;
407
+ const ss = clampVal(s, 0, 1);
408
+ const ll = clampVal(l, 0, 1);
409
+ if (ss === 0) return [
410
+ ll,
411
+ ll,
412
+ ll
413
+ ];
414
+ const q = ll < .5 ? ll * (1 + ss) : ll + ss - ll * ss;
415
+ const p = 2 * ll - q;
416
+ const hueToChannel = (t) => {
417
+ let tt = t;
418
+ if (tt < 0) tt += 1;
419
+ if (tt > 1) tt -= 1;
420
+ if (tt < 1 / 6) return p + (q - p) * 6 * tt;
421
+ if (tt < 1 / 2) return q;
422
+ if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
423
+ return p;
424
+ };
425
+ return [
426
+ hueToChannel(hh + 1 / 3),
427
+ hueToChannel(hh),
428
+ hueToChannel(hh - 1 / 3)
429
+ ];
430
+ }
431
+ /**
394
432
  * Parse a hex color string (#rgb or #rrggbb) to sRGB [r, g, b] in 0–1 range.
395
433
  * Returns null if the string is not a valid hex color.
396
434
  */
@@ -618,7 +656,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
618
656
  function findLightnessForContrast(options) {
619
657
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
620
658
  const target = resolveMinContrast(contrastInput);
621
- const searchTarget = target * 1.007;
659
+ const searchTarget = target * 1.01;
622
660
  const yBase = gamutClampedLuminance(baseLinearRgb);
623
661
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
624
662
  if (crPref >= searchTarget) return {
@@ -812,6 +850,7 @@ let globalConfig = {
812
850
  lightLightness: [10, 100],
813
851
  darkLightness: [15, 95],
814
852
  darkDesaturation: .1,
853
+ darkCurve: .5,
815
854
  states: {
816
855
  dark: "@dark",
817
856
  highContrast: "@high-contrast"
@@ -959,25 +998,42 @@ function topoSort(defs) {
959
998
  for (const name of Object.keys(defs)) visit(name);
960
999
  return result;
961
1000
  }
1001
+ function lightnessWindow(isHighContrast, kind) {
1002
+ if (isHighContrast) return [0, 100];
1003
+ return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
1004
+ }
962
1005
  function mapLightnessLight(l, mode, isHighContrast) {
963
- if (mode === "static" || isHighContrast) return l;
964
- const [lo, hi] = globalConfig.lightLightness;
1006
+ if (mode === "static") return l;
1007
+ const [lo, hi] = lightnessWindow(isHighContrast, "light");
965
1008
  return l * (hi - lo) / 100 + lo;
966
1009
  }
1010
+ function mobiusCurve(t, beta) {
1011
+ if (beta >= 1) return t;
1012
+ return t / (t + beta * (1 - t));
1013
+ }
967
1014
  function mapLightnessDark(l, mode, isHighContrast) {
968
1015
  if (mode === "static") return l;
969
- if (isHighContrast) return mode === "fixed" ? l : 100 - l;
970
- const [lo, hi] = globalConfig.darkLightness;
971
- if (mode === "fixed") return l * (hi - lo) / 100 + lo;
972
- return (100 - l) * (hi - lo) / 100 + lo;
1016
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
1017
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
1018
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
1019
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
1020
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
1021
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1022
+ }
1023
+ function lightMappedToDark(lightL, isHighContrast) {
1024
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
1025
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
1026
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
1027
+ const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
1028
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
973
1029
  }
974
1030
  function mapSaturationDark(s, mode) {
975
1031
  if (mode === "static") return s;
976
1032
  return s * (1 - globalConfig.darkDesaturation);
977
1033
  }
978
1034
  function schemeLightnessRange(isDark, mode, isHighContrast) {
979
- if (mode === "static" || isHighContrast) return [0, 1];
980
- const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
1035
+ if (mode === "static") return [0, 1];
1036
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
981
1037
  return [lo / 100, hi / 100];
982
1038
  }
983
1039
  function clamp(v, min, max) {
@@ -1036,9 +1092,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1036
1092
  else {
1037
1093
  const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
1038
1094
  if (parsed.relative) {
1039
- let delta = parsed.value;
1040
- if (isDark && mode === "auto") delta = -delta;
1041
- preferredL = clamp(baseL + delta, 0, 100);
1095
+ const delta = parsed.value;
1096
+ if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast);
1097
+ else preferredL = clamp(baseL + delta, 0, 100);
1042
1098
  } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1043
1099
  else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1044
1100
  }
@@ -1047,15 +1103,15 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1047
1103
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1048
1104
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1049
1105
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1050
- const lightnessRange = schemeLightnessRange(isDark, mode, isHighContrast);
1106
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
1051
1107
  return {
1052
1108
  l: findLightnessForContrast({
1053
1109
  hue: effectiveHue,
1054
1110
  saturation: effectiveSat,
1055
- preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
1111
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
1056
1112
  baseLinearRgb,
1057
1113
  contrast: minCr,
1058
- lightnessRange
1114
+ lightnessRange: [0, 1]
1059
1115
  }).lightness * 100,
1060
1116
  satFactor
1061
1117
  };
@@ -1395,10 +1451,14 @@ function createTheme(hue, saturation, initialColors) {
1395
1451
  };
1396
1452
  },
1397
1453
  extend(options) {
1398
- return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
1399
- ...colorDefs,
1454
+ const newHue = options.hue ?? hue;
1455
+ const newSat = options.saturation ?? saturation;
1456
+ const inheritedColors = {};
1457
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
1458
+ return createTheme(newHue, newSat, options.colors ? {
1459
+ ...inheritedColors,
1400
1460
  ...options.colors
1401
- } : { ...colorDefs });
1461
+ } : { ...inheritedColors });
1402
1462
  },
1403
1463
  resolve() {
1404
1464
  return resolveAllColors(hue, saturation, colorDefs);
@@ -1432,40 +1492,74 @@ function validatePrimaryTheme(primary, themes) {
1432
1492
  throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1433
1493
  }
1434
1494
  }
1435
- function createPalette(themes) {
1495
+ /**
1496
+ * Resolve the effective primary for an export call.
1497
+ * `false` disables, a string overrides, `undefined` inherits from palette.
1498
+ */
1499
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1500
+ if (exportPrimary === false) return void 0;
1501
+ return exportPrimary ?? palettePrimary;
1502
+ }
1503
+ /**
1504
+ * Filter a resolved color map, skipping keys already in `seen`.
1505
+ * Warns on collision and keeps the first-written value (first-write-wins).
1506
+ * Returns a new map containing only non-colliding entries.
1507
+ */
1508
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1509
+ const filtered = /* @__PURE__ */ new Map();
1510
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
1511
+ for (const [name, color] of resolved) {
1512
+ const key = `${prefix}${name}`;
1513
+ if (seen.has(key)) {
1514
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1515
+ continue;
1516
+ }
1517
+ seen.set(key, label);
1518
+ filtered.set(name, color);
1519
+ }
1520
+ return filtered;
1521
+ }
1522
+ function createPalette(themes, paletteOptions) {
1523
+ validatePrimaryTheme(paletteOptions?.primary, themes);
1436
1524
  return {
1437
1525
  tokens(options) {
1438
- validatePrimaryTheme(options?.primary, themes);
1526
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1527
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1439
1528
  const modes = resolveModes(options?.modes);
1440
1529
  const allTokens = {};
1530
+ const seen = /* @__PURE__ */ new Map();
1441
1531
  for (const [themeName, theme] of Object.entries(themes)) {
1442
1532
  const resolved = theme.resolve();
1443
- const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1533
+ const prefix = resolvePrefix(options, themeName, true);
1534
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1444
1535
  for (const variant of Object.keys(tokens)) {
1445
1536
  if (!allTokens[variant]) allTokens[variant] = {};
1446
1537
  Object.assign(allTokens[variant], tokens[variant]);
1447
1538
  }
1448
- if (themeName === options?.primary) {
1449
- const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1539
+ if (themeName === effectivePrimary) {
1540
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1450
1541
  for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1451
1542
  }
1452
1543
  }
1453
1544
  return allTokens;
1454
1545
  },
1455
1546
  tasty(options) {
1456
- validatePrimaryTheme(options?.primary, themes);
1547
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1548
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1457
1549
  const states = {
1458
1550
  dark: options?.states?.dark ?? globalConfig.states.dark,
1459
1551
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1460
1552
  };
1461
1553
  const modes = resolveModes(options?.modes);
1462
1554
  const allTokens = {};
1555
+ const seen = /* @__PURE__ */ new Map();
1463
1556
  for (const [themeName, theme] of Object.entries(themes)) {
1464
1557
  const resolved = theme.resolve();
1465
- const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1558
+ const prefix = resolvePrefix(options, themeName, true);
1559
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1466
1560
  Object.assign(allTokens, tokens);
1467
- if (themeName === options?.primary) {
1468
- const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1561
+ if (themeName === effectivePrimary) {
1562
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1469
1563
  Object.assign(allTokens, unprefixed);
1470
1564
  }
1471
1565
  }
@@ -1478,7 +1572,8 @@ function createPalette(themes) {
1478
1572
  return result;
1479
1573
  },
1480
1574
  css(options) {
1481
- validatePrimaryTheme(options?.primary, themes);
1575
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1576
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1482
1577
  const suffix = options?.suffix ?? "-color";
1483
1578
  const format = options?.format ?? "rgb";
1484
1579
  const allLines = {
@@ -1487,17 +1582,19 @@ function createPalette(themes) {
1487
1582
  lightContrast: [],
1488
1583
  darkContrast: []
1489
1584
  };
1585
+ const seen = /* @__PURE__ */ new Map();
1490
1586
  for (const [themeName, theme] of Object.entries(themes)) {
1491
1587
  const resolved = theme.resolve();
1492
- const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1588
+ const prefix = resolvePrefix(options, themeName, true);
1589
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1493
1590
  for (const key of [
1494
1591
  "light",
1495
1592
  "dark",
1496
1593
  "lightContrast",
1497
1594
  "darkContrast"
1498
1595
  ]) if (css[key]) allLines[key].push(css[key]);
1499
- if (themeName === options?.primary) {
1500
- const unprefixed = buildCssMap(resolved, "", suffix, format);
1596
+ if (themeName === effectivePrimary) {
1597
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1501
1598
  for (const key of [
1502
1599
  "light",
1503
1600
  "dark",
@@ -1515,33 +1612,183 @@ function createPalette(themes) {
1515
1612
  }
1516
1613
  };
1517
1614
  }
1518
- function createColorToken(input) {
1519
- const defs = { __color__: {
1520
- lightness: input.lightness,
1521
- saturation: input.saturationFactor,
1522
- mode: input.mode
1523
- } };
1615
+ /** Matches CSS color functions Glaze itself emits, plus their legacy alpha aliases. */
1616
+ const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
1617
+ function parseNumberOrPercent(raw, percentScale) {
1618
+ if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
1619
+ return parseFloat(raw);
1620
+ }
1621
+ function parseColorString(input) {
1622
+ if (input.startsWith("#")) {
1623
+ const rgb = parseHex(input);
1624
+ if (!rgb) throw new Error(`glaze: invalid hex color "${input}".`);
1625
+ const [h, s, l] = srgbToOkhsl(rgb);
1626
+ return {
1627
+ h,
1628
+ s,
1629
+ l
1630
+ };
1631
+ }
1632
+ const m = input.match(COLOR_FN_RE);
1633
+ if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
1634
+ const fn = m[1].toLowerCase();
1635
+ const body = m[2].trim();
1636
+ let parts;
1637
+ let hasAlpha = false;
1638
+ const slashIdx = body.indexOf("/");
1639
+ if (slashIdx !== -1) {
1640
+ parts = body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean);
1641
+ hasAlpha = body.slice(slashIdx + 1).trim().length > 0;
1642
+ } else {
1643
+ parts = body.split(/[\s,]+/).filter(Boolean);
1644
+ if (parts.length === 4) {
1645
+ parts.pop();
1646
+ hasAlpha = true;
1647
+ }
1648
+ }
1649
+ if (hasAlpha) console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
1650
+ if (parts.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
1651
+ switch (fn) {
1652
+ case "rgb":
1653
+ case "rgba": {
1654
+ const [h, s, l] = srgbToOkhsl([
1655
+ parseNumberOrPercent(parts[0], 255) / 255,
1656
+ parseNumberOrPercent(parts[1], 255) / 255,
1657
+ parseNumberOrPercent(parts[2], 255) / 255
1658
+ ]);
1659
+ return {
1660
+ h,
1661
+ s,
1662
+ l
1663
+ };
1664
+ }
1665
+ case "hsl":
1666
+ case "hsla": {
1667
+ const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(parts[0]), parseNumberOrPercent(parts[1], 1), parseNumberOrPercent(parts[2], 1)));
1668
+ return {
1669
+ h: oh,
1670
+ s: os,
1671
+ l: ol
1672
+ };
1673
+ }
1674
+ case "okhsl": return {
1675
+ h: parseFloat(parts[0]),
1676
+ s: parseNumberOrPercent(parts[1], 1),
1677
+ l: parseNumberOrPercent(parts[2], 1)
1678
+ };
1679
+ case "oklch": {
1680
+ const L = parseNumberOrPercent(parts[0], 1);
1681
+ const C = parseFloat(parts[1]);
1682
+ const hRad = parseFloat(parts[2]) * Math.PI / 180;
1683
+ const [h, s, l] = oklabToOkhsl([
1684
+ L,
1685
+ C * Math.cos(hRad),
1686
+ C * Math.sin(hRad)
1687
+ ]);
1688
+ return {
1689
+ h,
1690
+ s,
1691
+ l
1692
+ };
1693
+ }
1694
+ }
1695
+ throw new Error(`glaze: unsupported color function "${fn}".`);
1696
+ }
1697
+ function extractOkhslFromValue(value) {
1698
+ if (typeof value === "string") return parseColorString(value);
1699
+ if (Array.isArray(value)) {
1700
+ const [r, g, b] = value;
1701
+ const [h, s, l] = srgbToOkhsl([
1702
+ r / 255,
1703
+ g / 255,
1704
+ b / 255
1705
+ ]);
1706
+ return {
1707
+ h,
1708
+ s,
1709
+ l
1710
+ };
1711
+ }
1712
+ return value;
1713
+ }
1714
+ function buildValueDefs(main, options) {
1715
+ const absoluteSeedHue = typeof options?.hue === "number" ? options.hue : main.h;
1716
+ const seedSaturation = options?.saturation ?? main.s * 100;
1717
+ const relativeHue = typeof options?.hue === "string" ? options.hue : void 0;
1718
+ if (options?.base !== void 0) {
1719
+ const baseOkhsl = extractOkhslFromValue(options.base);
1720
+ const baseSatFactor = seedSaturation > 0 ? Math.min(baseOkhsl.s * 100 / seedSaturation, 1) : 0;
1721
+ return {
1722
+ seedHue: absoluteSeedHue,
1723
+ seedSaturation,
1724
+ defs: {
1725
+ __base__: {
1726
+ hue: baseOkhsl.h,
1727
+ saturation: baseSatFactor,
1728
+ lightness: baseOkhsl.l * 100,
1729
+ mode: options.mode
1730
+ },
1731
+ __color__: {
1732
+ base: "__base__",
1733
+ hue: relativeHue,
1734
+ saturation: options.saturationFactor,
1735
+ lightness: options.lightness ?? main.l * 100,
1736
+ contrast: options.contrast,
1737
+ mode: options.mode
1738
+ }
1739
+ },
1740
+ primary: "__color__"
1741
+ };
1742
+ }
1743
+ return {
1744
+ seedHue: absoluteSeedHue,
1745
+ seedSaturation,
1746
+ defs: { __color__: {
1747
+ hue: relativeHue,
1748
+ saturation: options?.saturationFactor,
1749
+ lightness: options?.lightness ?? main.l * 100,
1750
+ contrast: options?.contrast,
1751
+ mode: options?.mode
1752
+ } },
1753
+ primary: "__color__"
1754
+ };
1755
+ }
1756
+ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary) {
1757
+ const resolveStates = (options) => ({
1758
+ dark: options?.states?.dark ?? globalConfig.states.dark,
1759
+ highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1760
+ });
1524
1761
  return {
1525
1762
  resolve() {
1526
- return resolveAllColors(input.hue, input.saturation, defs).get("__color__");
1763
+ return resolveAllColors(seedHue, seedSaturation, defs).get(primary);
1527
1764
  },
1528
1765
  token(options) {
1529
- return buildTokenMap(resolveAllColors(input.hue, input.saturation, defs), "", {
1530
- dark: options?.states?.dark ?? globalConfig.states.dark,
1531
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1532
- }, resolveModes(options?.modes), options?.format)["#__color__"];
1766
+ return buildTokenMap(resolveAllColors(seedHue, seedSaturation, defs), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
1533
1767
  },
1534
1768
  tasty(options) {
1535
- return buildTokenMap(resolveAllColors(input.hue, input.saturation, defs), "", {
1536
- dark: options?.states?.dark ?? globalConfig.states.dark,
1537
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1538
- }, resolveModes(options?.modes), options?.format)["#__color__"];
1769
+ return buildTokenMap(resolveAllColors(seedHue, seedSaturation, defs), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
1539
1770
  },
1540
1771
  json(options) {
1541
- return buildJsonMap(resolveAllColors(input.hue, input.saturation, defs), resolveModes(options?.modes), options?.format)["__color__"];
1772
+ return buildJsonMap(resolveAllColors(seedHue, seedSaturation, defs), resolveModes(options?.modes), options?.format)[primary];
1773
+ },
1774
+ css(options) {
1775
+ const resolved = resolveAllColors(seedHue, seedSaturation, defs);
1776
+ return buildCssMap(new Map([[options.name, resolved.get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb");
1542
1777
  }
1543
1778
  };
1544
1779
  }
1780
+ function createColorToken(input) {
1781
+ const defs = { __color__: {
1782
+ lightness: input.lightness,
1783
+ saturation: input.saturationFactor,
1784
+ mode: input.mode
1785
+ } };
1786
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, "__color__");
1787
+ }
1788
+ function createColorTokenFromValue(value, options) {
1789
+ const { seedHue, seedSaturation, defs, primary } = buildValueDefs(extractOkhslFromValue(value), options);
1790
+ return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary);
1791
+ }
1545
1792
  /**
1546
1793
  * Create a single-hue glaze theme.
1547
1794
  *
@@ -1564,6 +1811,7 @@ glaze.configure = function configure(config) {
1564
1811
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1565
1812
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1566
1813
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1814
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1567
1815
  states: {
1568
1816
  dark: config.states?.dark ?? globalConfig.states.dark,
1569
1817
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1578,8 +1826,8 @@ glaze.configure = function configure(config) {
1578
1826
  /**
1579
1827
  * Compose multiple themes into a palette.
1580
1828
  */
1581
- glaze.palette = function palette(themes) {
1582
- return createPalette(themes);
1829
+ glaze.palette = function palette(themes, options) {
1830
+ return createPalette(themes, options);
1583
1831
  };
1584
1832
  /**
1585
1833
  * Create a theme from a serialized export.
@@ -1587,11 +1835,24 @@ glaze.palette = function palette(themes) {
1587
1835
  glaze.from = function from(data) {
1588
1836
  return createTheme(data.hue, data.saturation, data.colors);
1589
1837
  };
1838
+ function isStructuredColorInput(input) {
1839
+ return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
1840
+ }
1590
1841
  /**
1591
1842
  * Create a standalone single-color token.
1843
+ *
1844
+ * Two overloads:
1845
+ * - `glaze.color(input)` — structured form: `{ hue, saturation, lightness, ... }`.
1846
+ * - `glaze.color(value, overrides?)` — value-shorthand: a hex string,
1847
+ * one of the CSS color functions Glaze itself emits
1848
+ * (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor` object
1849
+ * `{ h, s, l }`, or an `[r, g, b]` (0–255) tuple. Optional overrides
1850
+ * accept absolute or relative `hue` / `lightness`, `saturation`,
1851
+ * `mode`, `base` (any value form), and `contrast` against the base.
1592
1852
  */
1593
- glaze.color = function color(input) {
1594
- return createColorToken(input);
1853
+ glaze.color = function color(input, options) {
1854
+ if (isStructuredColorInput(input)) return createColorToken(input);
1855
+ return createColorTokenFromValue(input, options);
1595
1856
  };
1596
1857
  /**
1597
1858
  * Compute a shadow color from a bg/fg pair and intensity.
@@ -1663,6 +1924,7 @@ glaze.resetConfig = function resetConfig() {
1663
1924
  lightLightness: [10, 100],
1664
1925
  darkLightness: [15, 95],
1665
1926
  darkDesaturation: .1,
1927
+ darkCurve: .5,
1666
1928
  states: {
1667
1929
  dark: "@dark",
1668
1930
  highContrast: "@high-contrast"
@@ -1675,5 +1937,5 @@ glaze.resetConfig = function resetConfig() {
1675
1937
  };
1676
1938
 
1677
1939
  //#endregion
1678
- export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
1940
+ export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, hslToSrgb, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, oklabToOkhsl, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
1679
1941
  //# sourceMappingURL=index.mjs.map