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