@quarksuite-two/core
Version: 
A web-focused toolkit for creating, assembling and distributing design tokens for custom design systems.
2,032 lines (1,685 loc) • 67.5 kB
JavaScript
/// <reference types="./types/color.d.ts" />
/** @typedef {"hex" | "rgb" | "hsl" | "cmyk" | "hwb" | "lab" | "lch" | "oklab" | "oklch"} CSSColorFormats */
/**
 * An action that takes any valid CSS `color` and converts it `to` another format.
 *
 * @param {CSSColorFormats} to - the target format
 *
 * @param {string} color - the color to convert
 * @returns {string} the converted color
 *
 * @example
 * Converting a hex color to RGB
 *
 * ```js
 * convert("rgb", "#7ea");
 * ```
 *
 * @example
 * Converting a hex color to HSL
 *
 * ```js
 * convert("hsl", "#7ea");
 * ```
 *
 * @example
 * Converting LCH color to hex
 *
 * ```js
 * convert("hex", "lch(49% 63 120)");
 * ```
 */
export function convert(to, color) {
  if (to === "lab") {
    return serialize(conversion(color, "cielab"));
  }
  if (to === "lch") {
    return serialize(conversion(color, "cielch"));
  }
  return serialize(conversion(color, to));
}
function validator(input) {
  const formats = [
    namedValidator,
    hexValidator,
    rgbValidator,
    hslValidator,
    cmykValidator,
    hwbValidator,
    cielabValidator,
    cielchValidator,
    oklabValidator,
    oklchValidator,
  ];
  const [format] = formats
    .map((fn) => [fn.name.replace(/Validator/, ""), fn.bind(null)])
    .find(([, fn]) => fn(input));
  if (!format) {
    return InvalidColorError(input);
  }
  return [format, input];
}
class InvalidColor extends Error {
  constructor(input, ...params) {
    super(...params);
    // Stack trace (for v8)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, InvalidColor);
    }
    this.name = "Invalid Color Format";
    this.message = `
${"-".repeat(100)}
"${input}" is not a valid color.
${"-".repeat(100)}
Supported color formats:
- Named colors
- RGB Hex
- Functional RGB
- Functional HSL
- Functional CMYK
- Functional HWB
- Functional CIELAB/CIELCH
- Functional OKLab/OKLCH
Read more about these formats at: https://www.w3.org/TR/css-color-4/
${"=".repeat(100)}
`;
  }
}
function InvalidColorError(input) {
  return new InvalidColor(input);
}
function parser(extracted) {
  const [format] = extracted;
  const parsers = [
    hexParser,
    rgbParser,
    hslParser,
    cmykParser,
    cielabParser,
    cielchParser,
    oklabParser,
    oklchParser,
  ];
  if (format === "hwb") {
    return hslParser(extracted);
  }
  const parse = parsers.find((fn) => fn.name.replace(/Parser/, "") === format);
  return parse(extracted);
}
function conversion(color, to) {
  return outputFromRgb(to, inputToRgb(color));
}
function serialize(results) {
  const [format] = results;
  const serializers = [
    hexSerializer,
    rgbSerializer,
    hslSerializer,
    cmykSerializer,
    hwbSerializer,
    cielabSerializer,
    cielchSerializer,
    oklabSerializer,
    oklchSerializer,
  ];
  const matched = serializers.find(
    (fn) => fn.name.replace(/Serializer/, "") === format
  );
  return matched(results);
}
// Color Tokenization
const NUMBER_TOKEN = /(?:-?(?!0\d)\d+(?:\.\d+)?)/;
const PERCENTAGE_TOKEN = new RegExp(
  ["(?:", NUMBER_TOKEN.source, "%)"].join("")
);
const LEGACY_DELIMITER = /(?:[\s,]+)/;
const LEGACY_ALPHA_DELIMITER = new RegExp(
  LEGACY_DELIMITER.source.replace(",", ",/")
);
const MODERN_DELIMITER = new RegExp(LEGACY_DELIMITER.source.replace(",", ""));
const MODERN_ALPHA_DELIMITER = new RegExp(
  LEGACY_ALPHA_DELIMITER.source.replace(",", "")
);
const COMPONENT_TOKEN = new RegExp(
  ["(?:", PERCENTAGE_TOKEN.source, "|", NUMBER_TOKEN.source, ")"].join("")
);
const HUE_TOKEN = new RegExp(
  ["(?:", NUMBER_TOKEN.source, "(?:deg|g?rad|turn)?)"].join("")
);
// Color Validation
const NAMED_COLOR_KEYWORDS = {
  aliceblue: "#f0f8ff",
  antiquewhite: "#faebd7",
  aqua: "#00ffff",
  aquamarine: "#7fffd4",
  azure: "#f0ffff",
  beige: "#f5f5dc",
  bisque: "#ffe4c4",
  black: "#000000",
  blanchedalmond: "#ffebcd",
  blue: "#0000ff",
  blueviolet: "#8a2be2",
  brown: "#a52a2a",
  burlywood: "#deb887",
  cadetblue: "#5f9ea0",
  chartreuse: "#7fff00",
  chocolate: "#d2691e",
  coral: "#ff7f50",
  cornflower: "#6495ed",
  cornflowerblue: "#6495ed",
  cornsilk: "#fff8dc",
  crimson: "#dc143c",
  cyan: "#00ffff",
  darkblue: "#00008b",
  darkcyan: "#008b8b",
  darkgoldenrod: "#b8860b",
  darkgray: "#a9a9a9",
  darkgreen: "#006400",
  darkgrey: "#a9a9a9",
  darkkhaki: "#bdb76b",
  darkmagenta: "#8b008b",
  darkolivegreen: "#556b2f",
  darkorange: "#ff8c00",
  darkorchid: "#9932cc",
  darkred: "#8b0000",
  darksalmon: "#e9967a",
  darkseagreen: "#8fbc8f",
  darkslateblue: "#483d8b",
  darkslategray: "#2f4f4f",
  darkslategrey: "#2f4f4f",
  darkturquoise: "#00ced1",
  darkviolet: "#9400d3",
  deeppink: "#ff1493",
  deepskyblue: "#00bfff",
  dimgray: "#696969",
  dimgrey: "#696969",
  dodgerblue: "#1e90ff",
  firebrick: "#b22222",
  floralwhite: "#fffaf0",
  forestgreen: "#228b22",
  fuchsia: "#ff00ff",
  gainsboro: "#dcdcdc",
  ghostwhite: "#f8f8ff",
  gold: "#ffd700",
  goldenrod: "#daa520",
  gray: "#808080",
  green: "#008000",
  greenyellow: "#adff2f",
  grey: "#808080",
  honeydew: "#f0fff0",
  hotpink: "#ff69b4",
  indianred: "#cd5c5c",
  indigo: "#4b0082",
  ivory: "#fffff0",
  khaki: "#f0e68c",
  laserlemon: "#ffff54",
  lavender: "#e6e6fa",
  lavenderblush: "#fff0f5",
  lawngreen: "#7cfc00",
  lemonchiffon: "#fffacd",
  lightblue: "#add8e6",
  lightcoral: "#f08080",
  lightcyan: "#e0ffff",
  lightgoldenrod: "#fafad2",
  lightgoldenrodyellow: "#fafad2",
  lightgray: "#d3d3d3",
  lightgreen: "#90ee90",
  lightgrey: "#d3d3d3",
  lightpink: "#ffb6c1",
  lightsalmon: "#ffa07a",
  lightseagreen: "#20b2aa",
  lightskyblue: "#87cefa",
  lightslategray: "#778899",
  lightslategrey: "#778899",
  lightsteelblue: "#b0c4de",
  lightyellow: "#ffffe0",
  lime: "#00ff00",
  limegreen: "#32cd32",
  linen: "#faf0e6",
  magenta: "#ff00ff",
  maroon: "#800000",
  maroon2: "#7f0000",
  maroon3: "#b03060",
  mediumaquamarine: "#66cdaa",
  mediumblue: "#0000cd",
  mediumorchid: "#ba55d3",
  mediumpurple: "#9370db",
  mediumseagreen: "#3cb371",
  mediumslateblue: "#7b68ee",
  mediumspringgreen: "#00fa9a",
  mediumturquoise: "#48d1cc",
  mediumvioletred: "#c71585",
  midnightblue: "#191970",
  mintcream: "#f5fffa",
  mistyrose: "#ffe4e1",
  moccasin: "#ffe4b5",
  navajowhite: "#ffdead",
  navy: "#000080",
  oldlace: "#fdf5e6",
  olive: "#808000",
  olivedrab: "#6b8e23",
  orange: "#ffa500",
  orangered: "#ff4500",
  orchid: "#da70d6",
  palegoldenrod: "#eee8aa",
  palegreen: "#98fb98",
  paleturquoise: "#afeeee",
  palevioletred: "#db7093",
  papayawhip: "#ffefd5",
  peachpuff: "#ffdab9",
  peru: "#cd853f",
  pink: "#ffc0cb",
  plum: "#dda0dd",
  powderblue: "#b0e0e6",
  purple: "#800080",
  purple2: "#7f007f",
  purple3: "#a020f0",
  rebeccapurple: "#663399",
  red: "#ff0000",
  rosybrown: "#bc8f8f",
  royalblue: "#4169e1",
  saddlebrown: "#8b4513",
  salmon: "#fa8072",
  sandybrown: "#f4a460",
  seagreen: "#2e8b57",
  seashell: "#fff5ee",
  sienna: "#a0522d",
  silver: "#c0c0c0",
  skyblue: "#87ceeb",
  slateblue: "#6a5acd",
  slategray: "#708090",
  slategrey: "#708090",
  snow: "#fffafa",
  springgreen: "#00ff7f",
  steelblue: "#4682b4",
  tan: "#d2b48c",
  teal: "#008080",
  thistle: "#d8bfd8",
  tomato: "#ff6347",
  turquoise: "#40e0d0",
  violet: "#ee82ee",
  wheat: "#f5deb3",
  white: "#ffffff",
  whitesmoke: "#f5f5f5",
  yellow: "#ffff00",
  yellowgreen: "#9acd32",
};
function namedValidator(color) {
  return Boolean(NAMED_COLOR_KEYWORDS[color]);
}
function hexValidator(color) {
  return /^#([\da-f]{3,4}){1,2}$/i.test(color);
}
function matchFunctionalFormat({ prefix, legacy = true }, tokens) {
  const VALUES = tokens.map((token) => token.source);
  const DELIMITER = legacy ? LEGACY_DELIMITER.source : MODERN_DELIMITER.source;
  const ALPHA_DELIMITER = legacy
    ? LEGACY_ALPHA_DELIMITER.source
    : MODERN_ALPHA_DELIMITER.source;
  return new RegExp(
    `(?:^${prefix}\\(`.concat(
      VALUES.join(DELIMITER),
      `(?:${[ALPHA_DELIMITER, COMPONENT_TOKEN.source].join("")})?\\))`
    )
  );
}
function rgbValidator(color) {
  return matchFunctionalFormat(
    { prefix: "rgba?" },
    Array(3).fill(COMPONENT_TOKEN)
  ).test(color);
}
function hslValidator(color) {
  return matchFunctionalFormat({ prefix: "hsla?" }, [
    HUE_TOKEN,
    ...Array(2).fill(PERCENTAGE_TOKEN),
  ]).test(color);
}
function cmykValidator(color) {
  return matchFunctionalFormat(
    { prefix: "device-cmyk", legacy: false },
    Array(4).fill(COMPONENT_TOKEN)
  ).test(color);
}
function hwbValidator(color) {
  return matchFunctionalFormat({ prefix: "hwb", legacy: false }, [
    HUE_TOKEN,
    ...Array(2).fill(PERCENTAGE_TOKEN),
  ]).test(color);
}
function cielabValidator(color) {
  return matchFunctionalFormat({ prefix: "lab", legacy: false }, [
    PERCENTAGE_TOKEN,
    ...Array(2).fill(NUMBER_TOKEN),
  ]).test(color);
}
function cielchValidator(color) {
  return matchFunctionalFormat({ prefix: "lch", legacy: false }, [
    PERCENTAGE_TOKEN,
    NUMBER_TOKEN,
    HUE_TOKEN,
  ]).test(color);
}
function oklabValidator(color) {
  return matchFunctionalFormat({ prefix: "oklab", legacy: false }, [
    PERCENTAGE_TOKEN,
    NUMBER_TOKEN,
    NUMBER_TOKEN,
  ]).test(color);
}
function oklchValidator(color) {
  return matchFunctionalFormat({ prefix: "oklch", legacy: false }, [
    PERCENTAGE_TOKEN,
    NUMBER_TOKEN,
    HUE_TOKEN,
  ]).test(color);
}
// Color Value Extraction
function hexExtractor(color) {
  return expandHex(color).match(/[\da-f]{2}/gi);
}
function expandHex(color) {
  const [, ...values] = color;
  if (values.length === 3 || values.length === 4) {
    return `#${values.map((channel) => channel.repeat(2)).join("")}`;
  }
  return color;
}
function componentExtractor(color) {
  return color.match(/(-?[\d.](%|deg|g?rad|turn)?)+/g);
}
function extractor(validated) {
  const [format, color] = validated;
  if (format === "named") {
    return ["hex", hexExtractor(NAMED_COLOR_KEYWORDS[color])];
  }
  if (format === "hex") {
    return ["hex", hexExtractor(color)];
  }
  return [format, componentExtractor(color)];
}
// Color Arithmetic
function clamp(x, a, b) {
  if (x < a) {
    return a;
  }
  if (x > b) {
    return b;
  }
  return x;
}
function hexFragmentToChannel(hex) {
  return parseInt(hex, 16);
}
function hexFragmentFromChannel(channel) {
  return channel.toString(16).padStart(2, "0");
}
function numberToPercentage(n) {
  return n * 100;
}
function numberFromPercentage(percentage) {
  return percentage / 100;
}
function numberToChannel(n) {
  return clamp(Math.round(n * 255), 0, 255);
}
function numberFromChannel(channel) {
  return clamp(channel / 255, 0, 1);
}
function radiansToDegrees(radians) {
  return (radians * 180) / Math.PI;
}
function radiansFromDegrees(degrees) {
  return (degrees * Math.PI) / 180;
}
function gradiansToDegrees(gradians) {
  return gradians * (180 / 200);
}
function turnsToDegrees(turns) {
  return turns * 360;
}
function hueCorrection(hue) {
  let h = hue;
  if (Math.sign(hue) === -1) {
    h = Math.abs(hue + 360);
  }
  if (hue > 360) {
    h = hue % 360;
  }
  return clamp(h, -360, 360);
}
// Color Parsing
function hexParser([format, components]) {
  const [r, g, b, A] = components;
  const [R, G, B] = [r, g, b].map((fragment) => hexFragmentToChannel(fragment));
  if (A) {
    return [format, [R, G, B, numberFromChannel(hexFragmentToChannel(A))]];
  }
  return [format, [R, G, B, 1]];
}
function parsePercentage(component) {
  if (component.endsWith("%")) {
    return numberFromPercentage(parseFloat(component));
  }
  return parseFloat(component);
}
function rgbParser([format, components]) {
  const [r, g, b, A] = components;
  const [R, G, B] = [r, g, b].map((channel) => {
    if (channel.endsWith("%")) return parsePercentage(channel);
    return numberFromChannel(parseFloat(channel));
  });
  if (A) {
    return [format, [R, G, B, parsePercentage(A)]];
  }
  return [format, [R, G, B, 1]];
}
function parseHue(hue) {
  let HUE = parseFloat(hue);
  const gradians = hue.endsWith("grad");
  const radians = hue.endsWith("rad") && !gradians;
  const turns = hue.endsWith("turn");
  if (gradians) {
    HUE = gradiansToDegrees(HUE);
  }
  if (radians) {
    HUE = radiansToDegrees(HUE);
  }
  if (turns) {
    HUE = turnsToDegrees(HUE);
  }
  return hueCorrection(HUE);
}
function hslParser([format, components]) {
  const [h, s, l, A] = components;
  const H = parseHue(h);
  const [S, L] = [s, l].map((percentage) =>
    numberFromPercentage(parseFloat(percentage))
  );
  if (A) {
    return [format, [H, S, L, parsePercentage(A)]];
  }
  return [format, [H, S, L, 1]];
}
function cmykParser([format, components]) {
  const [C, M, Y, K, A] = components.map((V) => {
    if (V.endsWith("%")) return parsePercentage(V);
    return parseFloat(V);
  });
  if (A) {
    return [format, [C, M, Y, K, A]];
  }
  return [format, [C, M, Y, K, 1]];
}
function cielabParser([format, components]) {
  const [$L, $a, $b, A] = components;
  const [L, a, b] = [$L, $a, $b].map((component) => parseFloat(component));
  if (A) {
    return [format, [L, a, b, parsePercentage(A)]];
  }
  return [format, [L, a, b, 1]];
}
function cielchParser([format, components]) {
  const [$L, c, h, A] = components;
  const [L, C] = [$L, c].map((component) => parseFloat(component));
  const H = radiansFromDegrees(parseHue(h));
  if (A) {
    return [format, [L, C, H, parsePercentage(A)]];
  }
  return [format, [L, C, H, 1]];
}
function oklabParser([format, components]) {
  const [$L, $a, $b, A] = components;
  const L = parsePercentage($L);
  const [a, b] = [$a, $b].map((component) => parseFloat(component));
  if (A) {
    return [format, [L, a, b, parsePercentage(A)]];
  }
  return [format, [L, a, b, 1]];
}
function oklchParser([format, components]) {
  const [$L, c, h, A] = components;
  const L = parsePercentage($L);
  const C = parseFloat(c);
  const H = radiansFromDegrees(parseHue(h));
  if (A) {
    return [format, [L, C, H, parsePercentage(A)]];
  }
  return [format, [L, C, H, 1]];
}
// Input -> RGB
function rgbInputIdentity([, values]) {
  const [r, g, b, A] = values;
  const [R, G, B] = [r, g, b].map((channel) => numberToChannel(channel));
  return ["rgb", [R, G, B, A]];
}
function rgbOutputIdentity([, rgbValues]) {
  return ["rgb", rgbValues];
}
function hexToRgb([, values]) {
  return ["rgb", values];
}
function calculateRgb(C, X, H) {
  return new Map([
    [[C, X, 0], 0 <= H && H < 60],
    [[X, C, 0], 60 <= H && H < 120],
    [[0, C, X], 120 <= H && H < 180],
    [[0, X, C], 180 <= H && H < 240],
    [[X, 0, C], 240 <= H && H < 300],
    [[C, 0, X], 300 <= H && H < 360],
  ]);
}
function hslToRgb([, values]) {
  const [H, S, L, A] = values;
  // Calculate chroma
  const C = (1 - Math.abs(2 * L - 1)) * S;
  const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
  const m = L - C / 2;
  const [R, G, B] = Array.from(calculateRgb(C, X, H))
    .find(([, condition]) => condition)
    .flatMap((result) => result)
    .map((n) => numberToChannel(n + m));
  return ["rgb", [R, G, B, A]];
}
function cmykToRgb([, values]) {
  const [C, M, Y, K, A] = values;
  const [R, G, B] = [C, M, Y].map((V) => numberToChannel((1 - V) * (1 - K)));
  return ["rgb", [R, G, B, A]];
}
function hwbToRgb([, values]) {
  const [H, W, BLK, A] = values;
  // Achromacity
  if (W + BLK >= 1) {
    const GRAY = numberToChannel(W / (W + BLK));
    return ["rgb", [...Array(3).fill(GRAY), A]];
  }
  // Conversion
  const [, [r, g, b]] = hslToRgb(["hsl", [H, 1, 0.5, 1]]);
  const [R, G, B] = [r, g, b].map((channel) =>
    numberToChannel(numberFromChannel(channel) * (1 - W - BLK) + W)
  );
  return ["rgb", [R, G, B, A]];
}
function cielabToCiexyz([L, a, b]) {
  // CIE standards
  const ε = 216 / 24389;
  const κ = 24389 / 27;
  const WHITE = [0.96422, 1.0, 0.82521]; // D50 reference white
  // Compute the values of F
  const FY = (L + 16) / 116;
  const FX = a / 500 + FY;
  const FZ = FY - b / 200;
  // Calculate xyz
  const [X, Y, Z] = [
    FX ** 3 > ε ? FX ** 3 : (116 * FX - 16) / κ,
    L > κ * ε ? FY ** 3 : L / κ,
    FZ ** 3 > ε ? FZ ** 3 : (116 * FZ - 16) / κ,
  ].map((V, i) => V * WHITE[i]);
  return [X, Y, Z];
}
function ciexyzToLrgb([X, Y, Z]) {
  const D65_CHROMATIC_ADAPTATION = [
    [0.9555766, -0.0230393, 0.0631636],
    [-0.0282895, 1.0099416, 0.0210077],
    [0.0122982, -0.020483, 1.3299098],
  ];
  const LINEAR_RGB_TRANSFORMATION_MATRIX = [
    [3.2404542, -1.5371385, -0.4985314],
    [-0.969266, 1.8760108, 0.041556],
    [0.0556434, -0.2040259, 1.0572252],
  ];
  const [CX, CY, CZ] = D65_CHROMATIC_ADAPTATION.map(
    ([V1, V2, V3]) => X * V1 + Y * V2 + Z * V3
  );
  const [LR, LG, LB] = LINEAR_RGB_TRANSFORMATION_MATRIX.map(
    ([V1, V2, V3]) => CX * V1 + CY * V2 + CZ * V3
  );
  return [LR, LG, LB];
}
function lrgbToRgb([LR, LG, LB]) {
  return [LR, LG, LB].map((V) =>
    V <= 0.0031308 ? 12.92 * V : 1.055 * V ** (1 / 2.4) - 0.055
  );
}
function cielabToRgb([, values]) {
  const [L, a, b, A] = values;
  const [R, G, B] = lrgbToRgb(ciexyzToLrgb(cielabToCiexyz([L, a, b]))).map(
    (n) => numberToChannel(n)
  );
  return ["rgb", [R, G, B, A]];
}
function oklabToLrgb([L, a, b]) {
  const LINEAR_LMS_CONE_ACTIVATIONS = [
    [0.3963377774, 0.2158037573],
    [0.1055613458, 0.0638541728],
    [0.0894841775, 1.291485548],
  ];
  const OKLAB_TO_LRGB_MATRIX = [
    [4.076416621, 3.3077115913, 0.2309699292],
    [-1.2684380046, 2.6097574011, 0.3413193965],
    [-0.0041960863, 0.7034186147, 1.707614701],
  ];
  const [LONG, M, S] = LINEAR_LMS_CONE_ACTIVATIONS.map(([V1, V2], pos) => {
    if (pos === 0) return L + a * V1 + b * V2;
    if (pos === 1) return L - a * V1 - b * V2;
    return L - a * V1 - b * V2;
  }).map((V) => V ** 3);
  const [LR, LG, LB] = OKLAB_TO_LRGB_MATRIX.map(([V1, V2, V3], pos) => {
    if (pos === 0) return LONG * V1 - M * V2 + S * V3;
    if (pos === 1) return LONG * V1 + M * V2 - S * V3;
    return LONG * V1 - M * V2 + S * V3;
  });
  return [LR, LG, LB];
}
function oklabToRgb([, values]) {
  const [L, a, b, A] = values;
  const [R, G, B] = lrgbToRgb(oklabToLrgb([L, a, b])).map((n) =>
    numberToChannel(n)
  );
  return ["rgb", [R, G, B, A]];
}
// RGB -> Output
function hexFromRgb([, rgbValues]) {
  const [r, g, b, a] = rgbValues;
  const [R, G, B] = [r, g, b].map((channel) => hexFragmentFromChannel(channel));
  const A = hexFragmentFromChannel(numberToChannel(a));
  return ["hex", [R, G, B, A]];
}
function calculateHue(R, G, B, cmax, delta) {
  return new Map([
    [0, delta === 0],
    [60 * (((G - B) / delta) % 6), cmax === R],
    [60 * ((B - R) / delta + 2), cmax === G],
    [60 * ((R - G) / delta + 4), cmax === B],
  ]);
}
function calculateSaturation(delta, L) {
  return delta === 0 ? 0 : delta / (1 - Math.abs(2 * L - 1));
}
function calculateLightness(cmin, cmax) {
  return (cmax + cmin) / 2;
}
function hslFromRgb([, rgbValues]) {
  const [r, g, b, A] = rgbValues;
  const [R, G, B] = [r, g, b].map((channel) => numberFromChannel(channel));
  const cmin = Math.min(R, G, B);
  const cmax = Math.max(R, G, B);
  const delta = cmax - cmin;
  const L = calculateLightness(cmin, cmax);
  const [H] = Array.from(calculateHue(R, G, B, cmax, delta)).find(
    ([, condition]) => condition
  );
  const S = calculateSaturation(delta, L);
  return ["hsl", [H, S, L, A]];
}
function cmykFromRgb([, rgbValues]) {
  const [r, g, b, A] = rgbValues;
  const [R, G, B] = [r, g, b].map((channel) => numberFromChannel(channel));
  const K = 1 - Math.max(R, G, B);
  const [C, M, Y] = [R, G, B].map((channel) => (1 - channel - K) / (1 - K));
  return ["cmyk", [C, M, Y, K, A]];
}
function hwbFromRgb([, rgbValues]) {
  const [r, g, b, A] = rgbValues;
  const [R, G, B] = [r, g, b].map((channel) => numberFromChannel(channel));
  const cmax = Math.max(R, G, B);
  const cmin = Math.min(R, G, B);
  const delta = cmax - cmin;
  const [H] = Array.from(calculateHue(R, G, B, cmax, delta)).find(
    ([, condition]) => condition
  );
  const [W, BLK] = [cmin, 1 - cmax];
  return ["hwb", [H, W, BLK, A]];
}
function rgbToLrgb([R, G, B]) {
  return [R, G, B].map((V) =>
    V <= 0.04045 ? V / 12.92 : ((V + 0.055) / 1.055) ** 2.4
  );
}
function lrgbToCiexyz([LR, LG, LB]) {
  const D65_REFERENCE_WHITE = [
    [0.4124564, 0.3575761, 0.1804375],
    [0.2126729, 0.7151522, 0.072175],
    [0.0193339, 0.119192, 0.9503041],
  ];
  const D50_CHROMATIC_ADAPTATION = [
    [1.0478112, 0.0228866, -0.050127],
    [0.0295424, 0.9904844, -0.0170491],
    [-0.0092345, 0.0150436, 0.7521316],
  ];
  const [x, y, z] = D65_REFERENCE_WHITE.map(
    ([V1, V2, V3]) => LR * V1 + LG * V2 + LB * V3
  );
  const [X, Y, Z] = D50_CHROMATIC_ADAPTATION.map(
    ([V1, V2, V3]) => x * V1 + y * V2 + z * V3
  );
  return [X, Y, Z];
}
function ciexyzToCielab([X, Y, Z]) {
  // CIE standards
  const ε = 216 / 24389;
  const κ = 24389 / 27;
  const D50_WHITE = [0.96422, 1.0, 0.82521];
  // Calculating F for each value
  const [FX, FY, FZ] = [X, Y, Z]
    .map((V, i) => V / D50_WHITE[i])
    .map((V) => (V > ε ? Math.cbrt(V) : (κ * V + 16) / 116));
  const [L, a, b] = [116 * FY - 16, 500 * (FX - FY), 200 * (FY - FZ)];
  return [L, a, b];
}
function cielabFromRgb([, rgbValues]) {
  const [r, g, $b, A] = rgbValues;
  const [R, G, B] = [r, g, $b].map((channel) => numberFromChannel(channel));
  const [L, a, b] = ciexyzToCielab(lrgbToCiexyz(rgbToLrgb([R, G, B])));
  return ["cielab", [L, a, b, A]];
}
function lrgbToOklab([LR, LG, LB]) {
  const NONLINEAR_LMS_CONE_ACTIVATIONS = [
    [0.4122214708, 0.5363325363, 0.0514459929],
    [0.2119034982, 0.6806995451, 0.1073969566],
    [0.0883024619, 0.2817188376, 0.6299787005],
  ];
  const RGB_OKLAB_MATRIX = [
    [0.2104542553, 0.793617785, 0.0040720468],
    [1.9779984951, 2.428592205, 0.4505937099],
    [0.0259040371, 0.7827717662, 0.808675766],
  ];
  const [L, M, S] = NONLINEAR_LMS_CONE_ACTIVATIONS.map(
    ([L, M, S]) => L * LR + M * LG + S * LB
  ).map((V) => Math.cbrt(V));
  return RGB_OKLAB_MATRIX.map(([V1, V2, V3], pos) => {
    if (pos === 0) return V1 * L + V2 * M - V3 * S;
    if (pos === 1) return V1 * L - V2 * M + V3 * S;
    return V1 * L + V2 * M - V3 * S;
  });
}
function oklabFromRgb([, rgbValues]) {
  const [r, g, $b, A] = rgbValues;
  const [R, G, B] = [r, g, $b].map((channel) => numberFromChannel(channel));
  const [L, a, b] = lrgbToOklab(rgbToLrgb([R, G, B]));
  return ["oklab", [L, a, b, A]];
}
function scalarToPolar([, scalarValues]) {
  const [L, a, b, A] = scalarValues;
  const C = Math.sqrt(a ** 2 + b ** 2);
  const H = Math.atan2(b, a);
  return [L, C, H, A];
}
function scalarFromPolar([, polarValues]) {
  const [L, C, H, A] = polarValues;
  const a = C * Math.cos(H);
  const b = C * Math.sin(H);
  return [L, a, b, A];
}
function cielabToCielch([, cielabValues]) {
  return ["cielch", scalarToPolar(["cielab", cielabValues])];
}
function cielabFromCielch([, cielchValues]) {
  return ["cielab", scalarFromPolar(["cielch", cielchValues])];
}
function oklabToOklch([, oklabValues]) {
  return ["oklch", scalarToPolar(["oklab", oklabValues])];
}
function oklabFromOklch([, oklchValues]) {
  return ["oklab", scalarFromPolar(["oklch", oklchValues])];
}
// Conversion pipeline
function inputToRgb(color) {
  const valid = validator(color);
  const extraction = extractor(valid);
  const parsed = parser(extraction);
  const [format] = parsed;
  // Input -> RGB
  const RGB = [hslToRgb, cmykToRgb, hwbToRgb, cielabToRgb, oklabToRgb];
  const input = (data, funcs) => {
    const [format] = data;
    return funcs.find((fn) => fn.name.startsWith(format))(data);
  };
  if (format === "hex" || format === "named") {
    return hexToRgb(parsed);
  }
  if (format === "rgb") {
    return rgbInputIdentity(parsed);
  }
  if (format.endsWith("lch")) {
    const scalar = [cielabFromCielch, oklabFromOklch].find((fn) =>
      fn.name.toLowerCase().endsWith(format)
    );
    return input(scalar(parsed), RGB);
  }
  return input(parsed, RGB);
}
function outputFromRgb(target, data) {
  // RGB -> Output
  const RGB = [
    hslFromRgb,
    cmykFromRgb,
    hwbFromRgb,
    cielabFromRgb,
    oklabFromRgb,
  ];
  const output = (funcs) => {
    return funcs.find((fn) => fn.name.startsWith(target))(data);
  };
  if (target === "hex") {
    return hexFromRgb(data);
  }
  if (target === "rgb") {
    return rgbOutputIdentity(data);
  }
  if (target === "cielch") {
    return cielabToCielch(cielabFromRgb(data));
  }
  if (target === "oklch") {
    return oklabToOklch(oklabFromRgb(data));
  }
  return output(RGB);
}
// Color Serialization
function hexSerializer([, hexResult]) {
  const [R, G, B, A] = hexResult;
  if (A === "ff") {
    return "#".concat(R, G, B);
  }
  return "#".concat(R, G, B, A);
}
function serializeFunctionalFormat({ prefix, legacy = true }, components) {
  const DELIMITER = legacy ? ", " : " ";
  const ALPHA_DELIMITER = legacy ? ", " : " / ";
  // Coercing the result of toFixed() to a number preserves precision while removing trailing zeroes.
  const isOpaque = components[components.length - 1] === 1;
  const values = components.slice(0, components.length - 1);
  const alpha = Number(components.slice(-1)).toFixed(3);
  return (legacy && !isOpaque ? `${prefix}a(` : `${prefix}(`).concat(
    values.join(DELIMITER),
    isOpaque ? "" : ALPHA_DELIMITER.concat(+alpha),
    ")"
  );
}
function rgbSerializer([, rgbResult]) {
  return serializeFunctionalFormat({ prefix: "rgb" }, rgbResult);
}
function hslSerializer([, hslResult]) {
  const [h, s, l, A] = hslResult;
  // Correct the hue result
  const H = hueCorrection(+h.toFixed(3));
  // format saturation, lightness to percentages
  const [S, L] = [s, l].map(
    (n) => `${+clamp(numberToPercentage(isNaN(n) ? 0 : n), 0, 100).toFixed(3)}%`
  );
  return serializeFunctionalFormat({ prefix: "hsl" }, [H, S, L, A]);
}
function cmykSerializer([, cmykResult]) {
  const [c, m, y, k, A] = cmykResult;
  // Format to percentage, cap at 0-100
  const [C, M, Y, K] = [c, m, y, k].map(
    (n) => `${+clamp(numberToPercentage(isNaN(n) ? 0 : n), 0, 100).toFixed(3)}%`
  );
  return serializeFunctionalFormat({ prefix: "device-cmyk", legacy: false }, [
    C,
    M,
    Y,
    K,
    A,
  ]);
}
function hwbSerializer([, hslResult]) {
  const [h, w, blk, A] = hslResult;
  // Correct the hue result
  const H = hueCorrection(+h.toFixed(3));
  // format white, black to percentages
  const [W, BLK] = [w, blk].map(
    (n) => `${+clamp(numberToPercentage(isNaN(n) ? 0 : n), 0, 100).toFixed(3)}%`
  );
  return serializeFunctionalFormat({ prefix: "hwb", legacy: false }, [
    H,
    W,
    BLK,
    A,
  ]);
}
function cielabSerializer([, cielabValues]) {
  const [$L, $a, $b, A] = cielabValues;
  // Clamp lightness at 0-100
  const L = `${+clamp($L, 0, 100).toFixed(3)}%`;
  // Clamp a, b at ±127
  const [a, b] = [$a, $b].map((n) => +clamp(n, -128, 128).toFixed(3));
  return serializeFunctionalFormat({ prefix: "lab", legacy: false }, [
    L,
    a,
    b,
    A,
  ]);
}
function cielchSerializer([, cielchValues]) {
  const [$L, c, h, A] = cielchValues;
  // Clamp lightness at 0-100
  const L = `${+clamp($L, 0, 100).toFixed(3)}%`;
  // Clamp chroma at 0-230
  const C = +clamp(c, 0, 230).toFixed(3);
  let H = h;
  // Hue is powerless if chroma is 0
  if (C === 0) {
    H = 0;
  } else {
    // Otherwise, format hue to degrees, correct hue
    H = +hueCorrection(radiansToDegrees(h)).toFixed(3);
  }
  return serializeFunctionalFormat({ prefix: "lch", legacy: false }, [
    L,
    C,
    H,
    A,
  ]);
}
function oklabSerializer([, oklabValues]) {
  const [$L, $a, $b, A] = oklabValues;
  // Format number to percentage, clamp at 0-100
  const L = `${+clamp(numberToPercentage($L), 0, 100).toFixed(3)}%`;
  // Clamp a, b at ±0.5
  const [a, b] = [$a, $b].map((n) => +clamp(n, -0.5, 0.5).toFixed(5));
  return serializeFunctionalFormat({ prefix: "oklab", legacy: false }, [
    L,
    a,
    b,
    A,
  ]);
}
function oklchSerializer([, oklchValues]) {
  const [$L, c, h, A] = oklchValues;
  // Format lightness to percentage, clamp at 0-100
  const L = `${+clamp(numberToPercentage($L), 0, 100).toFixed(3)}%`;
  // Clamp chroma at 0-0.5
  const C = +clamp(c, 0, 0.5).toFixed(5);
  let H = h;
  // Hue is powerless if chroma is 0
  if (C === 0) {
    H = 0;
  } else {
    // Otherwise, format hue to degrees, correct hue
    H = +hueCorrection(radiansToDegrees(h)).toFixed(3);
  }
  return serializeFunctionalFormat({ prefix: "oklch", legacy: false }, [
    L,
    C,
    H,
    A,
  ]);
}
/**
 * An action that takes any valid CSS `color` and adjusts its properties
 * according to user `settings`.
 *
 * @param {object} settings - color adjustment settings
 * @param {number} [settings.lightness] - adjust the color lightness (as a percentage)
 * @param {number} [settings.chroma] - adjust the color chroma/intensity (as a percentage)
 * @param {number} [settings.hue] - adjust the color hue (in degrees)
 * @param {number} [settings.alpha] - adjust the color alpha/transparency (as a percentage)
 * @param {number} [settings.steps] - activates interpolated color adjustment (up to number of steps)
 *
 * @param {string} color - the color to adjust
 * @returns {string | string[]} the adjusted color or interpolation results
 *
 * @example
 * Some sample color adjustments
 *
 * ```js
 * const swatch = "rebeccapurple";
 *
 * // Positive values increase
 * adjust({ lightness: 20 }, swatch);
 * adjust({ chroma: 8 }, swatch);
 * adjust({ hue: 90 }, swatch);
 *
 * // Negative values decrease
 * adjust({ lightness: -36 }, swatch);
 * adjust({ chroma: -15 }, swatch);
 * adjust({ hue: -220 }, swatch);
 * adjust({ alpha: -25 }, swatch);
 *
 * // Multiple adjustments allowed
 * adjust({ lightness: 25, chroma: -8, hue: 240 }, swatch);
 *
 * // Interpolated
 * adjust({ lightness: -75, steps: 6 }, swatch);
 * ```
 */
export function adjust(settings, color) {
  // Do nothing by default
  const { lightness = 0, chroma = 0, hue = 0, alpha = 0, steps } = settings;
  if (steps) {
    return colorInterpolation(
      colorAdjustment,
      {
        lightness,
        chroma,
        hue,
        alpha,
        steps,
      },
      color
    );
  }
  return colorAdjustment({ lightness, chroma, hue, alpha }, color);
}
// Color Adjustment Internals
function extractOklchValues(color) {
  const formattedOklch = serialize(conversion(color, "oklch"));
  const [, components] = extractor(["oklch", formattedOklch]);
  return components.map((V) => parseFloat(V));
}
function adjustColorProperties(
  { lightness, chroma, hue, alpha },
  [l, c, h, a]
) {
  // Adjust properties only if defined, make values parseable
  const L = numberFromPercentage(lightness ? l + lightness : l);
  const C = chroma ? c + numberFromPercentage(chroma) * 0.5 * 0.5 : c;
  const H = radiansFromDegrees(hue ? hueCorrection(h + hue) : h);
  const A = alpha ? (a ?? 1) + numberFromPercentage(alpha) : a ?? 1;
  // Return adjusted values
  return [L, C, H, A];
}
function colorAdjustment(
  { lightness = 0, chroma = 0, hue = 0, alpha = 0 },
  color
) {
  // Ensure color is valid and store its format
  const [format] = validator(color);
  // Extract its OKLCH values
  const values = extractOklchValues(color);
  // Adjust target properties
  const [L, C, H, A] = adjustColorProperties(
    { lightness, chroma, hue, alpha },
    values
  );
  // Serialize oklch result
  const oklch = serialize(["oklch", [L, C, H, A]]);
  // If input format is named, format to hex
  if (format === "named") {
    return serialize(conversion(oklch, "hex"));
  }
  // Otherwise use input format
  return serialize(conversion(oklch, format));
}
// Color Interpolation Behavior
function colorInterpolation(action, settings, input) {
  // Set default for shared step property
  const { steps = 1 } = settings;
  // Fill an array with a length of steps with the input color
  return [
    ...new Set(
      Array(steps)
        .fill(input)
        .map((color, pos) => {
          // General interpolation formula
          const interpolate = (property, index) =>
            property - (property / steps) * index;
          // Store result
          let result = "";
          // Now, we vary the behavior here based on the name of the action
          if (action.name === "colorAdjustment") {
            // Destructure unique properties
            const { lightness = 0, chroma = 0, hue = 0, alpha = 0 } = settings;
            result = colorAdjustment(
              {
                lightness: interpolate(lightness, pos),
                chroma: interpolate(chroma, pos),
                hue: interpolate(hue, pos),
                alpha: interpolate(alpha, pos),
              },
              color
            );
          }
          if (action.name === "colorMix") {
            // Destructure unique properties
            const { strength = 0, target = color } = settings;
            result = colorMix(
              { strength: interpolate(strength, pos), target },
              color
            );
          }
          return result;
        })
    ),
  ].reverse();
}
/**
 * An action that takes any valid CSS `color` and mixes it according to user `settings`.
 *
 * @param {object} settings - color blending settings
 * @param {string} [settings.target] - set the blend target
 * @param {number} [settings.strength] - set the blend strength (as a percentage)
 * @param {number} [settings.steps] - activates interpolated color blending (up to number of steps)
 *
 * @param {string} color - the input color
 * @returns {string | string[]} the blended color or interpolation results
 *
 * @example
 * Some sample color blends
 *
 * ```js
 * const swatch = "dodgerblue";
 * const target = "crimson";
 *
 * // Positive strength blends toward target
 * mix({ target, strength: 72 }, swatch);
 *
 * // Negative strength blends from target
 * mix({ target, strength: -64 }, swatch);
 *
 * // Interpolated
 * mix({ target, strength: 50, steps: 6 }, swatch);
 * ```
 */
export function mix(settings, color) {
  // Do nothing by default
  const { target = color, strength = 0, steps } = settings;
  if (steps) {
    return colorInterpolation(colorMix, { target, strength, steps }, color);
  }
  return colorMix({ target, strength }, color);
}
// Color Mixture Internals
function getOklabValues(color) {
  return conversion(color, "oklab");
}
function calculateMixture(color, target, strength) {
  // format blend target and input color to OKLab
  const [, [$L, $a, $b, $A]] = getOklabValues(color);
  const [, [$$L, $$a, $$b, $$A]] = getOklabValues(target);
  // calculate the blend result
  const [L, a, b, A] = [
    [$L, $$L],
    [$a, $$a],
    [$b, $$b],
    [$A, $$A],
  ].map(([X, Y]) => {
    // if -strength, blend FROM target
    // --------------------------------------------------------------
    // Note: Object.is() is a handy way to explicitly check for a strength of -0, which
    // should also trigger a blend inversion. This is not caught by Math.sign() alone,
    // because the way JS treats signed zeroes is identical.
    //
    // Which also means `Math.sign(strength) === -0` didn't work either.
    if (Math.sign(strength) === -1 || Object.is(strength, -0)) {
      return Y + (X - Y) * Math.abs(strength);
    }
    // Otherwise, blend TO target
    return X + (Y - X) * strength;
  });
  return [L, a, b, A];
}
function colorMix({ target, strength = 0 }, color) {
  // Validate input color and store its format
  const [format] = validator(color);
  // Calculate blend
  const [L, a, b, A] = calculateMixture(
    color,
    target,
    numberFromPercentage(strength)
  );
  // Serialize the blend result
  const oklab = serialize(["oklab", [L, a, b, A]]);
  if (format === "named") {
    return serialize(conversion(oklab, "hex"));
  }
  return serialize(conversion(oklab, format));
}
/** @typedef {"dyadic" | "complementary" | "analogous" | "split" | "clash" | "triadic" | "double" | "tetradic" | "square"} ColorHarmonies */
/**
 * An action that takes any valid CSS `color` and generates an artistic color
 * harmony according to user `settings`.
 *
 * @param {object} settings - color harmony settings
 * @param {ColorHarmonies} [settings.configuration] - set the artistic color harmony
 * @param {boolean} [settings.accented] - include the complement as an accent?
 *
 * @param {string} color - the input color
 * @returns {[string, string, string?, string?]} the generated color harmony
 *
 * @example
 * Generating an analogous harmony from a color
 *
 * ```js
 * const swatch = "#bada55";
 *
 * harmony({ configuration: "analogous" }, swatch);
 * ```
 *
 * @example
 * Generating an accented split complementary harmony from a color
 *
 * ```js
 * const swatch = "#deaded";
 *
 * harmony({ configuration: "split" accented: true }, swatch);
 * ```
 */
export function harmony(settings, color) {
  // Set defaults
  const { configuration = "complementary", accented = false } = settings;
  return colorHarmonies({ type: configuration, accented }, color);
}
// Color Harmony Internals
function colorHarmonies({ type, accented = false }, color) {
  const opposite = colorAdjustment({ hue: 180 }, color);
  const withComplement = accented ? [opposite] : [];
  const uniform = ({ arc = 30, values = 2 }, color) =>
    Array(values)
      .fill(color)
      .map((color, pos) => colorAdjustment({ hue: arc * pos }, color));
  const triad = (color, arc = 30, accented = false) => {
    const [a, b] = [
      colorAdjustment({ hue: 180 - arc }, color),
      colorAdjustment({ hue: 180 + arc }, color),
    ];
    return [
      colorAdjustment({ hue: 0 }, color),
      a,
      ...(accented ? [opposite] : []),
      b,
    ];
  };
  const tetrad = (color, arc = 30) => [
    colorAdjustment({ hue: 0 }, color),
    colorAdjustment({ hue: arc }, color),
    colorAdjustment({ hue: 180 }, color),
    colorAdjustment({ hue: 180 + arc }, color),
  ];
  const harmonies = {
    dyadic: [...uniform({}, color), ...withComplement],
    complementary: uniform({ arc: 180 }, color),
    analogous: [...uniform({ values: 3 }, color), ...withComplement],
    split: triad(color, 30, accented),
    triadic: triad(color, 60, accented),
    clash: triad(color, 90),
    double: tetrad(color),
    tetradic: tetrad(color, 60),
    square: uniform({ arc: 90, values: 4 }, color),
  };
  return harmonies[type] || color;
}
/**
 * @typedef {{ bg: string; fg: string }} SurfaceTokens - BG, FG
 *
 * @typedef {Partial<{
 *   50: string;
 *   100: string;
 *   200: string;
 *   300: string;
 *   400: string;
 *   500: string;
 *   600: string;
 *   700: string;
 *   800: string;
 *   900: string;
 *   a50: string;
 *   a100: string;
 *   a200: string;
 *   a300: string;
 *   a400: string;
 *   a500: string;
 *   a600: string;
 *   a700: string;
 *   a800: string;
 *   a900: string; }>} MaterialVariantTokens - MAIN, ACCENTS?
 *
 * @typedef {Partial<{
 *   light: { [key: string]: string };
 *   muted: { [key: string]: string };
 *   dark: { [key: string]: string }; }>} ArtisticVariantTokens - LIGHT?, MUTED?, DARK?
 *
 * @typedef {Partial<{ accent: { [key: string]: string; } }>} ArtisticAccentTokens
 *
 * @typedef {Partial<{
 *  state: {
 *    pending: string;
 *    success: string;
 *    warning: string;
 *    error: string;
 *  } }>} StateTokens
 *
 * @typedef {SurfaceTokens & MaterialVariantTokens & StateTokens} MaterialTokens
 * @typedef {SurfaceTokens & ArtisticVariantTokens & ArtisticAccentTokens} ArtisticTokens
 * @typedef {MaterialTokens | ArtisticTokens} PaletteTokens - assembled palette token object
 */
/**
 * An action that takes any valid CSS `color` and generates color tokens
 * according to user settings.
 *
 * @param {object} settings - palette settings
 * @param {"material" | "artistic"} [settings.configuration] - set the palette configuration
 * @param {number} [settings.contrast] - set the overall palette contrast
 * @param {boolean} [settings.accents] - generate accent colors?
 * @param {boolean} [settings.dark] - using dark mode?
 *
 * @param {boolean} [settings.states] - generate interface states? (material)
 *
 * @param {number} [settings.tints] - set number of tints to generate (artistic)
 * @param {number} [settings.tones] - set number of tones to generate (artistic)
 * @param {number} [settings.shades] - set number of shades to generate (artistic)
 *
 * @param {string} color - the input color
 * @returns {PaletteTokens} the generated palette
 *
 * @example
 * Generating a material-esque palette
 *
 * ```js
 * const swatch = "#feb07f";
 *
 * palette({ configuration: "material" }, swatch);
 *
 * // adjust contrast
 * palette({ configuration: "material", contrast: 90 }, swatch);
 *
 * // with accents
 * palette({ configuration: "material", accents: true }, swatch);
 *
 * // with interface states
 * palette({ configuration: "material", states: true }, swatch);
 *
 * // dark mode
 * palette({ configuration: "material", dark: true }, swatch);
 * ```
 *
 * @example
 * Generating an artistic palette
 *
 * ```js
 * const swatch = "#3a0ffa";
 *
 * palette({ configuration: "artistic" }, swatch);
 *
 * // adjust contrast
 * palette({ configuration: "artistic", contrast: 95 }, swatch);
 *
 * // with accents
 * palette({ configuration: "artistic", accents: true }, swatch);
 *
 * // adjust generated variants (tints, tones, shades)
 * palette({ configuration: "artistic", tints: 2, tones: 0, shades: 4 }, swatch);
 *
 * // dark mode
 * palette({ configuration: "artistic", dark: true }, swatch);
 * ```
 */
export function palette(settings, color) {
  // Set defaults
  const {
    configuration = "material",
    contrast = 100,
    accents = false,
    states = false,
    dark = false,
  } = settings;
  // Generate material-esque or artistic palette depending on configuration
  if (configuration === "artistic") {
    const { tints = 3, tones = 3, shades = 3 } = settings;
    return artisticPalette(
      { contrast, tints, tones, shades, accented: accents, dark },
      color
    );
  }
  return materialPalette(
    {
      contrast,
      accented: accents,
      stated: states,
      dark,
    },
    color
  );
}
// Color Palette Internals
function generateSurface(contrast, color, dark = false) {
  const strength = 100 * numberFromPercentage(contrast);
  const [bg, fg] = [
    colorMix({ target: "#ffffff", strength }, color),
    colorMix({ target: "#111111", strength }, color),
  ];
  return dark ? { bg: fg, fg: bg } : { bg, fg };
}
function generateMaterialVariants(contrast, [bg, fg], color) {
  const strength = 90 * numberFromPercentage(contrast);
  const interpolate = (target) =>
    colorInterpolation(colorMix, { target, strength, steps: 5 }, color);
  return [...interpolate(bg).reverse(), ...interpolate(fg)];
}
function generateMaterialAccents(
  contrast,
  variants,
  color,
  accented = false,
  dark = false
) {
  const PERCENTAGE = 50 / 1.618;
  const HUE = 120;
  const limit = (max = 90) => max * numberFromPercentage(contrast);
  const interpolate = (properties) =>
    colorInterpolation(colorAdjustment, properties, color).map((target, pos) =>
      colorMix({ target, strength: limit(100) }, variants[pos])
    );
  return accented
    ? [
        ...interpolate({
          lightness: limit(dark ? -PERCENTAGE : PERCENTAGE),
          chroma: limit(-PERCENTAGE),
          hue: limit(-HUE),
          steps: 5,
        }).reverse(),
        ...interpolate({
          lightness: limit(dark ? PERCENTAGE : -PERCENTAGE),
          chroma: limit(PERCENTAGE),
          hue: limit(HUE),
          steps: 5,
        }),
      ]
    : [];
}
function generateArtisticVariants(contrast, { tints, tones, shades }, color) {
  const strength = 90 * numberFromPercentage(contrast);
  const interpolate = (target, steps) =>
    colorInterpolation(colorMix, { target, strength, steps }, color);
  return [
    tints ? interpolate("#ffffff", tints) : [],
    tones ? interpolate("#aaaaaa", tones) : [],
    shades ? interpolate("#111111", shades) : [],
  ];
}
function generateArtisticAccents(
  contrast,
  color,
  accented = false,
  dark = false
) {
  const PERCENTAGE = 50 / 1.618;
  const HUE = 120;
  const limit = (max = 90) => max * numberFromPercentage(contrast);
  const interpolate = (properties) =>
    colorInterpolation(colorAdjustment, properties, color);
  return accented
    ? [
        ...interpolate({
          lightness: limit(dark ? -PERCENTAGE : PERCENTAGE),
          chroma: limit(-PERCENTAGE),
          hue: limit(-HUE),
          steps: 5,
        }).reverse(),
        ...interpolate({
          lightness: limit(dark ? PERCENTAGE : -PERCENTAGE),
          chroma: limit(PERCENTAGE),
          hue: limit(HUE),
          steps: 4,
        }),
      ]
    : [];
}
function generateStates(contrast, [, fg], color, stated = false) {
  const strength = 80 * numberFromPercentage(contrast);
  return stated
    ? [
        colorMix({ target: "#dddddd", strength }, color),
        colorMix({ target: "#2ecc40", strength }, color),
        colorMix({ target: "#ffdc00", strength }, color),
        colorMix({ target: "#ff4136", strength }, color),
      ].map((states) =>
        colorMix({ target: fg, strength: strength / 2 }, states)
      )
    : [];
}
function assembleMaterialData(data, prefix = "") {
  return data.reduce((acc, color, index) => {
    if (index === 0) return { ...acc, [prefix.concat(50)]: color };
    return { ...acc, [`${prefix.concat(index)}00`]: color };
  }, {});
}
function materialPalette(
  { contrast = 100, accented = false, stated = false, dark = false },
  color
) {
  const { bg, fg } = generateSurface(contrast, color, dark);
  // [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]
  const variants = generateMaterialVariants(contrast, [bg, fg], color);
  // [A100, A200, A300, A400, A500, A600, A700, A800, A900]
  const accents = generateMaterialAccents(
    contrast,
    variants,
    color,
    accented,
    dark
  );
  const [pending, success, warning, error] = generateStates(
    contrast,
    [bg, fg],
    color,
    stated
  );
  return {
    bg,
    fg,
    ...assembleMaterialData(variants),
    ...assembleMaterialData(accents, "a"),
    ...(stated ? { state: { pending, success, warning, error } } : {}),
  };
}
function assembleArtisticData(data) {
  return data.reduce((acc, color, index) => {
    return { ...acc, [`${++index}00`]: color };
  }, {});
}
function artisticPalette(
  {
    contrast = 100,
    tints = 3,
    tones = 3,
    shades = 3,
    accented = false,
    dark = false,
  },
  color
) {
  const { bg, fg } = generateSurface(contrast, color, dark);
  const [l, m, d] = generateArtisticVariants(
    contrast,
    { tints, tones, shades },
    color
  );
  // [100, 200, 300, 400, 500, 600, 700, 800, 900]
  const accents = generateArtisticAccents(contrast, color, accented, dark);
  return {
    bg,
    fg,
    ...(l.length ? { light: assembleArtisticData(l) } : {}),
    ...(m.length ? { muted: assembleArtisticData(m) } : {}),
    ...(d.length ? { dark: assembleArtisticData(d) } : {}),
    ...(accented ? { accent: assembleArtisticData(accents) } : {}),
  };
}
/**
 * An action that takes a generated `palette` and filters it for accessibility
 * according to user `settings`.
 *
 * @param {object} settings - accessibility settings
 * @param {"standard" | "custom"} [settings.mode] - set the accesibility mode
 *
 * @param {"AA" | "AAA"} [settings.rating] - set color contrast rating
 * @param {boolean} [settings.large] - use large text rating?
 *
 * @param {number} [settings.min] - set minimum contrast from background (as a percentage)
 * @param {number} [settings.max] - set maximum contrast from background (as a percentage)
 *
 * @param {PaletteTokens} palette - generated palette
 * @returns {PaletteTokens} the filtered palette
 *
 * @example
 * WCAG standard mode
 *
 * ```js
 * const swatch = "#eca0ff";
 * const tokens = palette({ configuration: "material", contrast: 95 }, swatch);
 *
 * // AA
 * a11y({ mode: "standard", rating: "AA" }, tokens);
 *
 * // AA (large)
 * a11y({ mode: "standard", rating: "AA", large: true }, tokens);
 *
 * // AAA
 * a11y({ mode: "standard", rating: "AAA" }, tokens);
 * ```
 *
 * @example
 * Custom colorimetric comparison mode
 *
 * ```js
 * const swatch = "#0ca08f";
 * const tokens = palette({ configuration: "material", contrast: 95 }, swatch);
 *
 * // valid if perceptual lightness has a 60% difference from background
 * a11y({ mode: "custom", min: 60 }, tokens);
 *
 * // valid if perceptual lightness has a 60-75% difference from background
 * a11y({ mode: "standard", min: 60, max: 75 }, tokens);
 * ```
 */
export function a11y(settings, palette) {
  // Set defaults
  const { mode = "standard", rating = "AA", large = false } = settings;
  // If mode is custom
  if (mode === "custom") {
    const { min = 85, max } = settings;
    return a11yColorimetric({ min, max }, palette);
  }
  return a11yWcag({ rating, large }, palette);
}
function a11yWcag({ rating, large }, palette) {
  if (
    ["light", "muted", "dark", "accent"].some((category) =>
      Object.hasOwn(palette, category)
    )
  ) {
    const { bg, fg, ...variants } = palette;
    const valid = (collection) =>
      collection.filter((fg) => {
        const ratio = calculateWCAGContrastRatio(bg, fg);
        return wcagContrastCriteria({ rating, large }, ratio);
      });
    return {
      bg,
      fg,
      ...Object.entries(variants).reduce((acc, [category, data]) => {
        const results = valid(Object.values(data));
        return {
          ...acc,
          ...(results.length
            ? { [category]: assembleArtisticData(results) }
            : {}),
        };
      }, {}),
    };
  }
  const { bg, fg, state, ...variants } = palette;
  const valid = Object.entries(variants).filter(([, fg]) => {
    co