@tenphi/glaze 0.11.1 → 0.12.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.
package/dist/index.mjs CHANGED
@@ -577,7 +577,8 @@ function defaultConfig() {
577
577
  modes: {
578
578
  dark: true,
579
579
  highContrast: false
580
- }
580
+ },
581
+ autoFlip: true
581
582
  };
582
583
  }
583
584
  let globalConfig = defaultConfig();
@@ -616,7 +617,8 @@ function configure(config) {
616
617
  dark: config.modes?.dark ?? globalConfig.modes.dark,
617
618
  highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
618
619
  },
619
- shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
620
+ shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
621
+ autoFlip: config.autoFlip ?? globalConfig.autoFlip
620
622
  };
621
623
  }
622
624
  function resetConfig() {
@@ -825,47 +827,64 @@ function findLightnessForContrast(options) {
825
827
  branch: "preferred"
826
828
  };
827
829
  const [minL, maxL] = lightnessRange;
828
- const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
829
- const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
830
- if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
831
- if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
832
- const darkerPasses = darkerResult?.met ?? false;
833
- const lighterPasses = lighterResult?.met ?? false;
834
- if (darkerPasses && lighterPasses) {
835
- if (Math.abs(darkerResult.lightness - preferredLightness) <= Math.abs(lighterResult.lightness - preferredLightness)) return {
836
- ...darkerResult,
837
- branch: "darker"
838
- };
839
- return {
840
- ...lighterResult,
841
- branch: "lighter"
842
- };
843
- }
844
- if (darkerPasses) return {
845
- ...darkerResult,
846
- branch: "darker"
847
- };
848
- if (lighterPasses) return {
849
- ...lighterResult,
850
- branch: "lighter"
851
- };
852
- const candidates = [];
853
- if (darkerResult) candidates.push({
854
- ...darkerResult,
855
- branch: "darker"
856
- });
857
- if (lighterResult) candidates.push({
858
- ...lighterResult,
859
- branch: "lighter"
860
- });
861
- if (candidates.length === 0) return {
830
+ const canDarker = preferredLightness > minL;
831
+ const canLighter = preferredLightness < maxL;
832
+ let initialIsDarker;
833
+ if (options.initialDirection !== void 0) initialIsDarker = options.initialDirection === "darker";
834
+ else if (canDarker && !canLighter) initialIsDarker = true;
835
+ else if (!canDarker && canLighter) initialIsDarker = false;
836
+ else if (!canDarker && !canLighter) return {
862
837
  lightness: preferredLightness,
863
838
  contrast: crPref,
864
839
  met: false,
865
840
  branch: "preferred"
866
841
  };
867
- candidates.sort((a, b) => b.contrast - a.contrast);
868
- return candidates[0];
842
+ else {
843
+ const yMinExt = cachedLuminance(hue, saturation, minL);
844
+ const yMaxExt = cachedLuminance(hue, saturation, maxL);
845
+ initialIsDarker = contrastRatioFromLuminance(yMinExt, yBase) >= contrastRatioFromLuminance(yMaxExt, yBase);
846
+ }
847
+ const searchInitial = () => initialIsDarker ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
848
+ const searchOpposite = () => initialIsDarker ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
849
+ const initialBranchName = initialIsDarker ? "darker" : "lighter";
850
+ const oppositeBranchName = initialIsDarker ? "lighter" : "darker";
851
+ const initialResult = searchInitial();
852
+ initialResult.met = initialResult.contrast >= target;
853
+ if (initialResult.met && !options.flip) return {
854
+ ...initialResult,
855
+ branch: initialBranchName
856
+ };
857
+ if (options.flip) {
858
+ const oppositeResult = (initialIsDarker ? canLighter : canDarker) ? searchOpposite() : null;
859
+ if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
860
+ if (initialResult.met && oppositeResult?.met) {
861
+ if (Math.abs(initialResult.lightness - preferredLightness) <= Math.abs(oppositeResult.lightness - preferredLightness)) return {
862
+ ...initialResult,
863
+ branch: initialBranchName
864
+ };
865
+ return {
866
+ ...oppositeResult,
867
+ branch: oppositeBranchName,
868
+ flipped: true
869
+ };
870
+ }
871
+ if (initialResult.met) return {
872
+ ...initialResult,
873
+ branch: initialBranchName
874
+ };
875
+ if (oppositeResult?.met) return {
876
+ ...oppositeResult,
877
+ branch: oppositeBranchName,
878
+ flipped: true
879
+ };
880
+ }
881
+ const extreme = initialIsDarker ? minL : maxL;
882
+ return {
883
+ lightness: extreme,
884
+ contrast: contrastRatioFromLuminance(cachedLuminance(hue, saturation, extreme), yBase),
885
+ met: false,
886
+ branch: initialBranchName
887
+ };
869
888
  }
870
889
  /**
871
890
  * Binary-search one branch [lo, hi] for the nearest passing mix value
@@ -947,53 +966,59 @@ function findValueForMixContrast(options) {
947
966
  contrast: crPref,
948
967
  met: true
949
968
  };
950
- const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
951
- const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
952
- if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
953
- if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
954
- const darkerPasses = darkerResult?.met ?? false;
955
- const lighterPasses = lighterResult?.met ?? false;
956
- if (darkerPasses && lighterPasses) {
957
- if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
958
- value: darkerResult.lightness,
959
- contrast: darkerResult.contrast,
960
- met: true
961
- };
962
- return {
963
- value: lighterResult.lightness,
964
- contrast: lighterResult.contrast,
965
- met: true
966
- };
967
- }
968
- if (darkerPasses) return {
969
- value: darkerResult.lightness,
970
- contrast: darkerResult.contrast,
971
- met: true
972
- };
973
- if (lighterPasses) return {
974
- value: lighterResult.lightness,
975
- contrast: lighterResult.contrast,
976
- met: true
977
- };
978
- const candidates = [];
979
- if (darkerResult) candidates.push({
980
- ...darkerResult,
981
- branch: "lower"
982
- });
983
- if (lighterResult) candidates.push({
984
- ...lighterResult,
985
- branch: "upper"
986
- });
987
- if (candidates.length === 0) return {
969
+ const canLower = preferredValue > 0;
970
+ const canUpper = preferredValue < 1;
971
+ let initialIsLower;
972
+ if (canLower && !canUpper) initialIsLower = true;
973
+ else if (!canLower && canUpper) initialIsLower = false;
974
+ else if (!canLower && !canUpper) return {
988
975
  value: preferredValue,
989
976
  contrast: crPref,
990
977
  met: false
991
978
  };
992
- candidates.sort((a, b) => b.contrast - a.contrast);
979
+ else initialIsLower = contrastRatioFromLuminance(luminanceAtValue(0), yBase) >= contrastRatioFromLuminance(luminanceAtValue(1), yBase);
980
+ const searchInitial = () => initialIsLower ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue);
981
+ const searchOpposite = () => initialIsLower ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue);
982
+ const initialResult = searchInitial();
983
+ initialResult.met = initialResult.contrast >= target;
984
+ if (initialResult.met && !options.flip) return {
985
+ value: initialResult.lightness,
986
+ contrast: initialResult.contrast,
987
+ met: true
988
+ };
989
+ if (options.flip) {
990
+ const oppositeResult = (initialIsLower ? canUpper : canLower) ? searchOpposite() : null;
991
+ if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
992
+ if (initialResult.met && oppositeResult?.met) {
993
+ if (Math.abs(initialResult.lightness - preferredValue) <= Math.abs(oppositeResult.lightness - preferredValue)) return {
994
+ value: initialResult.lightness,
995
+ contrast: initialResult.contrast,
996
+ met: true
997
+ };
998
+ return {
999
+ value: oppositeResult.lightness,
1000
+ contrast: oppositeResult.contrast,
1001
+ met: true,
1002
+ flipped: true
1003
+ };
1004
+ }
1005
+ if (initialResult.met) return {
1006
+ value: initialResult.lightness,
1007
+ contrast: initialResult.contrast,
1008
+ met: true
1009
+ };
1010
+ if (oppositeResult?.met) return {
1011
+ value: oppositeResult.lightness,
1012
+ contrast: oppositeResult.contrast,
1013
+ met: true,
1014
+ flipped: true
1015
+ };
1016
+ }
1017
+ const extreme = initialIsLower ? 0 : 1;
993
1018
  return {
994
- value: candidates[0].lightness,
995
- contrast: candidates[0].contrast,
996
- met: candidates[0].met
1019
+ value: extreme,
1020
+ contrast: contrastRatioFromLuminance(luminanceAtValue(extreme), yBase),
1021
+ met: false
997
1022
  };
998
1023
  }
999
1024
 
@@ -1307,13 +1332,19 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1307
1332
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1308
1333
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1309
1334
  const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.scaling);
1335
+ const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
1336
+ let initialDirection;
1337
+ if (preferredL < baseL) initialDirection = "darker";
1338
+ else if (preferredL > baseL) initialDirection = "lighter";
1310
1339
  const result = findLightnessForContrast({
1311
1340
  hue: effectiveHue,
1312
1341
  saturation: effectiveSat,
1313
1342
  preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
1314
1343
  baseLinearRgb,
1315
1344
  contrast: minCr,
1316
- lightnessRange: [0, 1]
1345
+ lightnessRange: [0, 1],
1346
+ initialDirection,
1347
+ flip: autoFlip
1317
1348
  });
1318
1349
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
1319
1350
  return {
@@ -1429,12 +1460,14 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1429
1460
  else luminanceAt = (v) => {
1430
1461
  return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1431
1462
  };
1463
+ const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
1432
1464
  t = findValueForMixContrast({
1433
1465
  preferredValue: t,
1434
1466
  baseLinearRgb: baseLinear,
1435
1467
  targetLinearRgb: targetLinear,
1436
1468
  contrast: minCr,
1437
- luminanceAtValue: luminanceAt
1469
+ luminanceAtValue: luminanceAt,
1470
+ flip: autoFlip
1438
1471
  }).value;
1439
1472
  }
1440
1473
  if (blend === "transparent") return {
@@ -1495,15 +1528,17 @@ function seedField(order, ctx, field, source) {
1495
1528
  });
1496
1529
  }
1497
1530
  }
1498
- function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1531
+ function resolveAllColors(hue, saturation, defs, scaling, externalBases, overrideAutoFlip) {
1499
1532
  validateColorDefs(defs, externalBases);
1500
1533
  const order = topoSort(defs);
1534
+ const cfg = getConfig();
1501
1535
  const ctx = {
1502
1536
  hue,
1503
1537
  saturation,
1504
1538
  defs,
1505
1539
  resolved: /* @__PURE__ */ new Map(),
1506
- scaling
1540
+ scaling,
1541
+ autoFlip: overrideAutoFlip ?? cfg.autoFlip
1507
1542
  };
1508
1543
  if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
1509
1544
  const lightMap = runPass(order, defs, ctx, false, false, "light");
@@ -1625,7 +1660,7 @@ function buildCssMap(resolved, prefix, suffix, format) {
1625
1660
  * Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
1626
1661
  *
1627
1662
  * Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
1628
- * `oklch()`, OkhslColor object, [r, g, b] tuple), the structured-input
1663
+ * `oklch()`, `{ r, g, b }`, `{ h, s, l }`, `{ l, c, h }`), the structured-input
1629
1664
  * validator, the two factory paths (value vs structured), and the
1630
1665
  * JSON-safe export / rehydration round-trip.
1631
1666
  *
@@ -1648,29 +1683,24 @@ const RESERVED_STANDALONE_NAMES = new Set([
1648
1683
  STANDALONE_BASE
1649
1684
  ]);
1650
1685
  /**
1651
- * Build the create-time scaling snapshot used when the caller did not
1652
- * pass an explicit `scaling`. All windows are snapshotted from the
1653
- * current `globalConfig` so later `glaze.configure()` calls don't
1654
- * retroactively change the resolved variants of an already-created
1655
- * token (matches the documented "frozen at create time" semantics).
1656
- *
1657
- * String value-shorthand inputs preserve their light lightness exactly
1658
- * (`lightLightness: false`) and use an extended dark window
1659
- * `[globalConfig.darkLightness[0], 100]` so a totally-black input can
1660
- * Möbius-invert to totally-white in dark mode. Object / tuple /
1661
- * structured inputs snapshot both windows from `globalConfig` verbatim
1662
- * so they behave like an ordinary theme color (auto-adapted on both
1663
- * sides).
1664
- */
1665
- function defaultStandaloneScaling(isString) {
1686
+ * Create-time scaling for all value-shorthand `glaze.color()` inputs.
1687
+ * Light lightness is preserved (`lightLightness: false`); dark uses the
1688
+ * theme window from `globalConfig.darkLightness`, snapshotted at create
1689
+ * time so later `configure()` does not retroactively change tokens.
1690
+ */
1691
+ function defaultValueShorthandScaling() {
1692
+ return {
1693
+ lightLightness: false,
1694
+ darkLightness: getConfig().darkLightness
1695
+ };
1696
+ }
1697
+ /**
1698
+ * Create-time scaling for structured `glaze.color({ hue, saturation,
1699
+ * lightness, ... })`. Both windows come from `globalConfig` so the
1700
+ * token behaves like an ordinary theme color on light and dark sides.
1701
+ */
1702
+ function defaultStructuredScaling() {
1666
1703
  const cfg = getConfig();
1667
- if (isString) {
1668
- const [darkLo] = cfg.darkLightness;
1669
- return {
1670
- lightLightness: false,
1671
- darkLightness: [darkLo, 100]
1672
- };
1673
- }
1674
1704
  return {
1675
1705
  lightLightness: cfg.lightLightness,
1676
1706
  darkLightness: cfg.darkLightness
@@ -1804,9 +1834,41 @@ function validateOkhslColor(value) {
1804
1834
  if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
1805
1835
  if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, lightness } (which uses 0–100)?");
1806
1836
  }
1807
- /** Validate a user-supplied `[r, g, b]` tuple in 0-255. */
1808
- function validateRgbTuple(value) {
1809
- for (const n of value) if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RGB tuple components must be finite numbers in 0–255 (got [${value.join(", ")}]).`);
1837
+ /** Validate a user-supplied `{ r, g, b }` object in 0255. */
1838
+ function validateRgbColor(value) {
1839
+ for (const key of [
1840
+ "r",
1841
+ "g",
1842
+ "b"
1843
+ ]) {
1844
+ const n = value[key];
1845
+ if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
1846
+ }
1847
+ }
1848
+ /** Validate a user-supplied `{ l, c, h }` OKLCh object. */
1849
+ function validateOklchColor(value) {
1850
+ const { l, c, h } = value;
1851
+ if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
1852
+ if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
1853
+ }
1854
+ function oklchComponentsToOkhsl(l, c, hDeg) {
1855
+ const hRad = hDeg * Math.PI / 180;
1856
+ const [h, s, outL] = oklabToOkhsl([
1857
+ l,
1858
+ c * Math.cos(hRad),
1859
+ c * Math.sin(hRad)
1860
+ ]);
1861
+ return {
1862
+ h,
1863
+ s,
1864
+ l: outL
1865
+ };
1866
+ }
1867
+ function isRgbColorObject(value) {
1868
+ return "r" in value && "g" in value && "b" in value;
1869
+ }
1870
+ function isOklchColorObject(value) {
1871
+ return "c" in value && "l" in value && "h" in value;
1810
1872
  }
1811
1873
  /**
1812
1874
  * Validate a user-supplied `opacity` override on `glaze.color()`.
@@ -1852,18 +1914,17 @@ function validateStandaloneName(name) {
1852
1914
  /**
1853
1915
  * Extract an OKHSL color from any `GlazeColorValue` form. Also used by
1854
1916
  * `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
1855
- * RGB tuple) go through one parser.
1917
+ * literal objects) go through one parser.
1856
1918
  */
1857
1919
  function extractOkhslFromValue(value) {
1858
1920
  if (typeof value === "string") return parseColorString(value);
1859
- if (Array.isArray(value)) {
1860
- const tuple = value;
1861
- validateRgbTuple(tuple);
1862
- const [r, g, b] = tuple;
1921
+ if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
1922
+ if (isRgbColorObject(value)) {
1923
+ validateRgbColor(value);
1863
1924
  const [h, s, l] = srgbToOkhsl([
1864
- r / 255,
1865
- g / 255,
1866
- b / 255
1925
+ value.r / 255,
1926
+ value.g / 255,
1927
+ value.b / 255
1867
1928
  ]);
1868
1929
  return {
1869
1930
  h,
@@ -1871,6 +1932,10 @@ function extractOkhslFromValue(value) {
1871
1932
  l
1872
1933
  };
1873
1934
  }
1935
+ if (isOklchColorObject(value)) {
1936
+ validateOklchColor(value);
1937
+ return oklchComponentsToOkhsl(value.l, value.c, value.h);
1938
+ }
1874
1939
  validateOkhslColor(value);
1875
1940
  return value;
1876
1941
  }
@@ -1878,11 +1943,9 @@ function extractOkhslFromValue(value) {
1878
1943
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
1879
1944
  *
1880
1945
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
1881
- * across every value-shorthand form. String inputs pair with the
1882
- * extended dark window so a totally-black input renders as totally-white
1883
- * in dark mode; `OkhslColor` / RGB-tuple inputs auto-adapt into the
1884
- * snapshotted `globalConfig.lightLightness` / `globalConfig.darkLightness`
1885
- * windows.
1946
+ * across every value-shorthand form, using the snapshotted
1947
+ * `globalConfig.darkLightness` window (light lightness preserved via
1948
+ * `lightLightness: false`).
1886
1949
  *
1887
1950
  * When the user requests `contrast` or relative `lightness`, a hidden
1888
1951
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
@@ -1923,11 +1986,11 @@ function buildStandaloneValueDefs(main, options) {
1923
1986
  primary
1924
1987
  };
1925
1988
  }
1926
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData) {
1989
+ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip) {
1927
1990
  let cached;
1928
1991
  const resolveOnce = () => {
1929
1992
  if (cached) return cached;
1930
- cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
1993
+ cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0, autoFlip);
1931
1994
  return cached;
1932
1995
  };
1933
1996
  const resolveStates = (options) => {
@@ -1966,7 +2029,7 @@ function resolveBaseToken(base) {
1966
2029
  if (isGlazeColorToken(base)) return base;
1967
2030
  return createColorTokenFromValue(base, void 0, void 0);
1968
2031
  }
1969
- function createColorToken(input, scaling) {
2032
+ function createColorToken(input, scaling, overrideAutoFlip) {
1970
2033
  validateStructuredInput(input);
1971
2034
  const userName = input.name;
1972
2035
  if (userName !== void 0) validateStandaloneName(userName);
@@ -1987,27 +2050,30 @@ function createColorToken(input, scaling) {
1987
2050
  saturation: 1,
1988
2051
  mode: "static"
1989
2052
  };
1990
- const effectiveScaling = scaling ?? defaultStandaloneScaling(false);
2053
+ const effectiveScaling = scaling ?? defaultStructuredScaling();
2054
+ const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
1991
2055
  const exportData = () => ({
1992
2056
  form: "structured",
1993
2057
  input: buildStructuredInputExport(input),
1994
- scaling: effectiveScaling
2058
+ scaling: effectiveScaling,
2059
+ autoFlip
1995
2060
  });
1996
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
2061
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
1997
2062
  }
1998
- function createColorTokenFromValue(value, options, scaling) {
1999
- const inputIsString = typeof value === "string";
2063
+ function createColorTokenFromValue(value, options, scaling, overrideAutoFlip) {
2000
2064
  const main = extractOkhslFromValue(value);
2001
2065
  const baseToken = resolveBaseToken(options?.base);
2002
2066
  const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
2003
- const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
2067
+ const effectiveScaling = scaling ?? defaultValueShorthandScaling();
2068
+ const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
2004
2069
  const exportData = () => ({
2005
2070
  form: "value",
2006
2071
  input: value,
2007
2072
  ...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
2008
- scaling: effectiveScaling
2073
+ scaling: effectiveScaling,
2074
+ autoFlip
2009
2075
  });
2010
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
2076
+ return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
2011
2077
  }
2012
2078
  /**
2013
2079
  * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
@@ -2086,9 +2152,15 @@ function colorFromExport(data) {
2086
2152
  if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
2087
2153
  if (data.form === "value") {
2088
2154
  const value = data.input;
2089
- return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
2155
+ const overrides = data.overrides ? rehydrateOverrides(data.overrides) : void 0;
2156
+ const cfg = getConfig();
2157
+ const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
2158
+ return createColorTokenFromValue(value, overrides, data.scaling, effectiveAutoFlip);
2090
2159
  }
2091
- return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
2160
+ const input = rehydrateStructuredInput(data.input);
2161
+ const cfg = getConfig();
2162
+ const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
2163
+ return createColorToken(input, data.scaling, effectiveAutoFlip);
2092
2164
  }
2093
2165
 
2094
2166
  //#endregion
@@ -2364,22 +2436,16 @@ glaze.from = function from(data) {
2364
2436
  * lightness-window override.
2365
2437
  * - `glaze.color(value, overrides?, scaling?)` — value-shorthand: a hex
2366
2438
  * string (3/6/8 digits), one of the CSS color functions Glaze itself
2367
- * emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor`
2368
- * object `{ h, s, l }` (0–1 ranges), or an `[r, g, b]` (0–255) tuple.
2439
+ * emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), or literal objects
2440
+ * `{ r, g, b }` (0–255), `{ h, s, l }` (OKHSL 0–1), `{ l, c, h }`
2441
+ * (OKLCh, matching `oklch()` strings).
2369
2442
  *
2370
- * Defaults: every input form defaults to `mode: 'auto'` so colors
2371
- * automatically adapt between light and dark like an ordinary theme
2372
- * color. The scaling snapshot taken at create time differs by input
2373
- * form:
2374
- * - String value-shorthand: `{ lightLightness: false, darkLightness:
2375
- * [globalConfig.darkLightness[0], 100] }`. Light preserves the input
2376
- * exactly; dark Möbius-inverts up to 100, so `glaze.color('#000')`
2377
- * renders as `#fff` in dark mode (and `glaze.color('#fff')` falls to
2378
- * the dark `lo` floor).
2379
- * - `OkhslColor` object / RGB-tuple / structured value-shorthand:
2380
- * `{ lightLightness: globalConfig.lightLightness, darkLightness:
2381
- * globalConfig.darkLightness }` — both windows come straight from
2382
- * `globalConfig`, so the resulting token behaves like a theme color.
2443
+ * Defaults: every input form defaults to `mode: 'auto'`. Value-shorthand
2444
+ * (strings and literal objects) snapshots `{ lightLightness: false,
2445
+ * darkLightness: globalConfig.darkLightness }` light preserves the
2446
+ * input; dark uses the theme window. Structured `{ hue, saturation,
2447
+ * lightness, ... }` snapshots both `globalConfig` windows like a theme
2448
+ * color.
2383
2449
  *
2384
2450
  * Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
2385
2451
  * inverting mapping, or `{ mode: 'static' }` to pin the same lightness
@@ -2404,7 +2470,7 @@ glaze.color = function color(input, arg2, arg3) {
2404
2470
  *
2405
2471
  * Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
2406
2472
  * `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
2407
- * strings, `OkhslColor` objects, or `[r, g, b]` (0–255) tuples.
2473
+ * strings, or `{ r, g, b }` / `{ h, s, l }` / `{ l, c, h }` objects.
2408
2474
  */
2409
2475
  glaze.shadow = function shadow(input) {
2410
2476
  const bg = extractOkhslFromValue(input.bg);