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