@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
743 lines (739 loc) • 22.1 kB
JSX
import {
createNumberFormatter
} from "./LR7LBJN3.jsx";
// src/colors/intl.ts
var COLOR_INTL_TRANSLATIONS = {
hue: "Hue",
saturation: "Saturation",
lightness: "Lightness",
brightness: "Brightness",
red: "Red",
green: "Green",
blue: "Blue",
alpha: "Alpha",
colorName: (lightness, chroma, hue) => `${lightness} ${chroma} ${hue}`,
transparentColorName: (lightness, chroma, hue, percentTransparent) => `${lightness} ${chroma} ${hue}, ${percentTransparent} transparent`,
"very dark": "very dark",
dark: "dark",
light: "light",
"very light": "very light",
pale: "pale",
grayish: "grayish",
vibrant: "vibrant",
black: "black",
white: "white",
gray: "gray",
pink: "pink",
"pink red": "pink red",
"red orange": "red orange",
brown: "brown",
orange: "orange",
"orange yellow": "orange yellow",
"brown yellow": "brown yellow",
yellow: "yellow",
"yellow green": "yellow green",
"green cyan": "green cyan",
cyan: "cyan",
"cyan blue": "cyan blue",
"blue purple": "blue purple",
purple: "purple",
"purple magenta": "purple magenta",
magenta: "magenta",
"magenta pink": "magenta pink"
};
// src/colors/utils.ts
import { clamp } from "@kobalte/utils";
function parseColor(value) {
const res = RGBColor.parse(value) || HSBColor.parse(value) || HSLColor.parse(value);
if (res) {
return res;
}
throw new Error(`Invalid color value: ${value}`);
}
function normalizeColor(v) {
if (typeof v === "string") {
return parseColor(v);
}
return v;
}
function getColorChannels(colorSpace) {
switch (colorSpace) {
case "rgb":
return RGBColor.colorChannels;
case "hsl":
return HSLColor.colorChannels;
case "hsb":
return HSBColor.colorChannels;
}
}
function normalizeHue(hue) {
if (hue === 360) {
return hue;
}
return (hue % 360 + 360) % 360;
}
var ORANGE_LIGHTNESS_THRESHOLD = 0.68;
var YELLOW_GREEN_LIGHTNESS_THRESHOLD = 0.85;
var MAX_DARK_LIGHTNESS = 0.55;
var GRAY_THRESHOLD = 1e-3;
var OKLCH_HUES = [
[0, "pink"],
[15, "red"],
[48, "orange"],
[94, "yellow"],
[135, "green"],
[175, "cyan"],
[264, "blue"],
[284, "purple"],
[320, "magenta"],
[349, "pink"]
];
var Color = class {
toHexInt() {
return this.toFormat("rgb").toHexInt();
}
getChannelValue(channel) {
if (channel in this) {
return this[channel];
}
throw new Error(`Unsupported color channel: ${channel}`);
}
withChannelValue(channel, value) {
if (channel in this) {
const x = this.clone();
x[channel] = value;
return x;
}
throw new Error(`Unsupported color channel: ${channel}`);
}
getChannelName(channel, translations) {
return translations[channel];
}
getColorSpaceAxes(xyChannels) {
const { xChannel, yChannel } = xyChannels;
const xCh = xChannel || this.getColorChannels().find((c) => c !== yChannel);
const yCh = yChannel || this.getColorChannels().find((c) => c !== xCh);
const zCh = this.getColorChannels().find((c) => c !== xCh && c !== yCh);
return { xChannel: xCh, yChannel: yCh, zChannel: zCh };
}
getColorName(translations) {
let [l, c, h] = toOKLCH(this);
if (l > 0.999) {
return translations.white;
}
if (l < 1e-3) {
return translations.black;
}
let hue;
[hue, l] = this.getOklchHue(l, c, h, translations);
let lightness = "";
let chroma = "";
if (c <= 0.1 && c >= GRAY_THRESHOLD) {
if (l >= 0.7) {
chroma = "pale";
} else {
chroma = "grayish";
}
} else if (c >= 0.15) {
chroma = "vibrant";
}
if (l < 0.3) {
lightness = "very dark";
} else if (l < MAX_DARK_LIGHTNESS) {
lightness = "dark";
} else if (l < 0.7) {
} else if (l < 0.85) {
lightness = "light";
} else {
lightness = "very light";
}
if (chroma) {
chroma = translations[chroma];
}
if (lightness) {
lightness = translations[lightness];
}
const alpha = this.getChannelValue("alpha");
if (alpha < 1) {
const percentTransparent = createNumberFormatter(() => ({
style: "percent"
}))().format(1 - alpha);
return translations.transparentColorName(lightness, chroma, hue, percentTransparent).replace(/\s+/g, " ").trim();
}
return translations.colorName(lightness, chroma, hue).replace(/\s+/g, " ").trim();
}
getOklchHue(l, c, h, translations) {
if (c < GRAY_THRESHOLD) {
return [translations.gray, l];
}
for (let i = 0; i < OKLCH_HUES.length; i++) {
let [hue, hueName] = OKLCH_HUES[i];
const [nextHue, nextHueName] = OKLCH_HUES[i + 1] || [360, "pink"];
if (h >= hue && h < nextHue) {
if (hueName === "orange") {
if (l < ORANGE_LIGHTNESS_THRESHOLD) {
hueName = "brown";
} else {
l = l - ORANGE_LIGHTNESS_THRESHOLD + MAX_DARK_LIGHTNESS;
}
}
if (h > hue + (nextHue - hue) / 2 && hueName !== nextHueName) {
hueName = `${hueName} ${nextHueName}`;
} else if (hueName === "yellow" && l < YELLOW_GREEN_LIGHTNESS_THRESHOLD) {
hueName = "yellow green";
}
const name = translations[hueName];
return [name, l];
}
}
throw new Error("Unexpected hue");
}
getHueName(translations) {
const [l, c, h] = toOKLCH(this);
const [name] = this.getOklchHue(l, c, h, translations);
return name;
}
};
var RGBColor = class _RGBColor extends Color {
constructor(red, green, blue, alpha) {
super();
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
}
static parse(value) {
let colors = [];
if (/^#[\da-f]+$/i.test(value) && [4, 5, 7, 9].includes(value.length)) {
const values = (value.length < 6 ? value.replace(/[^#]/gi, "$&$&") : value).slice(1).split("");
while (values.length > 0) {
colors.push(Number.parseInt(values.splice(0, 2).join(""), 16));
}
colors[3] = colors[3] !== void 0 ? colors[3] / 255 : void 0;
}
const match = value.match(/^rgba?\((.*)\)$/);
if (match?.[1]) {
colors = match[1].replace(
/(\d+)%$/u,
(_substring, numberValue) => (Number(numberValue) / 100).toString()
).replaceAll(/,|\//gu, " ").replaceAll(/\s{2,}/gu, " ").split(" ").map((value2) => {
return Number(value2.trim());
});
colors = colors.map((num, i) => {
return clamp(num ?? 0, 0, i < 3 ? 255 : 1);
});
}
if (colors[0] === void 0 || colors[1] === void 0 || colors[2] === void 0) {
return void 0;
}
return colors.length < 3 ? void 0 : new _RGBColor(colors[0], colors[1], colors[2], colors[3] ?? 1);
}
toString(format = "css") {
switch (format) {
case "hex":
return `#${(this.red.toString(16).padStart(2, "0") + this.green.toString(16).padStart(2, "0") + this.blue.toString(16).padStart(2, "0")).toUpperCase()}`;
case "hexa":
return `#${(this.red.toString(16).padStart(2, "0") + this.green.toString(16).padStart(2, "0") + this.blue.toString(16).padStart(2, "0") + Math.round(this.alpha * 255).toString(16).padStart(2, "0")).toUpperCase()}`;
case "css":
case "rgb":
return `rgb(${this.red}, ${this.green}, ${this.blue}${this.alpha !== 1 && this.alpha !== 100 ? ` / ${this?.alpha}` : ""})`;
case "rgba":
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`;
default:
return this.toFormat(format).toString(format);
}
}
toFormat(format) {
switch (format) {
case "hex":
case "hexa":
case "rgb":
case "rgba":
return this;
case "hsb":
case "hsba":
return this.toHSB();
case "hsl":
case "hsla":
return this.toHSL();
default:
throw new Error(`Unsupported color conversion: rgb -> ${format}`);
}
}
toHexInt() {
return this.red << 16 | this.green << 8 | this.blue;
}
/**
* Converts an RGB color value to HSB.
* Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
* @returns An HSBColor object.
*/
toHSB() {
const red = this.red / 255;
const green = this.green / 255;
const blue = this.blue / 255;
const min = Math.min(red, green, blue);
const brightness = Math.max(red, green, blue);
const chroma = brightness - min;
const saturation = brightness === 0 ? 0 : chroma / brightness;
let hue = 0;
if (chroma !== 0) {
switch (brightness) {
case red:
hue = (green - blue) / chroma + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / chroma + 2;
break;
case blue:
hue = (red - green) / chroma + 4;
break;
}
hue /= 6;
}
return new HSBColor(
toFixedNumber(hue * 360, 2),
toFixedNumber(saturation * 100, 2),
toFixedNumber(brightness * 100, 2),
this.alpha
);
}
/**
* Converts an RGB color value to HSL.
* Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
* @returns An HSLColor object.
*/
toHSL() {
const red = this.red / 255;
const green = this.green / 255;
const blue = this.blue / 255;
const min = Math.min(red, green, blue);
const max = Math.max(red, green, blue);
const lightness = (max + min) / 2;
const chroma = max - min;
let hue;
let saturation;
if (chroma === 0) {
hue = saturation = 0;
} else {
saturation = chroma / (lightness < 0.5 ? max + min : 2 - max - min);
switch (max) {
case red:
hue = (green - blue) / chroma + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / chroma + 2;
break;
default:
hue = (red - green) / chroma + 4;
break;
}
hue /= 6;
}
return new HSLColor(
toFixedNumber(hue * 360, 2),
toFixedNumber(saturation * 100, 2),
toFixedNumber(lightness * 100, 2),
this.alpha
);
}
clone() {
return new _RGBColor(this.red, this.green, this.blue, this.alpha);
}
getChannelRange(channel) {
switch (channel) {
case "red":
case "green":
case "blue":
return { minValue: 0, maxValue: 255, step: 1, pageSize: 17 };
case "alpha":
return { minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1 };
default:
throw new Error(`Unknown color channel: ${channel}`);
}
}
getChannelFormatOptions(channel) {
switch (channel) {
case "red":
case "green":
case "blue":
return { style: "decimal" };
case "alpha":
return { style: "percent", maximumFractionDigits: 2 };
default:
throw new Error(`Unknown color channel: ${channel}`);
}
}
formatChannelValue(channel) {
const options = this.getChannelFormatOptions(channel);
const value = this.getChannelValue(channel);
return createNumberFormatter(() => options)().format(value);
}
getColorSpace() {
return "rgb";
}
static colorChannels = [
"red",
"green",
"blue"
];
getColorChannels() {
return _RGBColor.colorChannels;
}
};
var HSB_REGEX = /hsb\(([-+]?\d+(?:.\d+)?\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d+(?:.\d+)?%)\)|hsba\(([-+]?\d+(?:.\d+)?\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d(.\d+)?)\)/;
var HSBColor = class _HSBColor extends Color {
constructor(hue, saturation, brightness, alpha) {
super();
this.hue = hue;
this.saturation = saturation;
this.brightness = brightness;
this.alpha = alpha;
}
static parse(value) {
let m;
if (m = value.match(HSB_REGEX)) {
const [h, s, b, a] = (m[1] ?? m[2]).split(",").map((n) => Number(n.trim().replace("%", "")));
return new _HSBColor(
normalizeHue(h),
clamp(s, 0, 100),
clamp(b, 0, 100),
clamp(a ?? 1, 0, 1)
);
}
}
toString(format = "css") {
switch (format) {
case "css":
return this.toHSL().toString("css");
case "hex":
return this.toRGB().toString("hex");
case "hexa":
return this.toRGB().toString("hexa");
case "hsb":
return `hsb(${this.hue} ${toFixedNumber(this.saturation, 2)}% ${toFixedNumber(this.brightness, 2)}%)`;
case "hsba":
return `hsba(${this.hue} ${toFixedNumber(this.saturation, 2)}% ${toFixedNumber(this.brightness, 2)}% ${this.alpha})`;
default:
return this.toFormat(format).toString(format);
}
}
toFormat(format) {
switch (format) {
case "hsb":
case "hsba":
return this;
case "hsl":
case "hsla":
return this.toHSL();
case "rgb":
case "rgba":
return this.toRGB();
default:
throw new Error(`Unsupported color conversion: hsb -> ${format}`);
}
}
/**
* Converts a HSB color to HSL.
* Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL.
* @returns An HSLColor object.
*/
toHSL() {
let saturation = this.saturation / 100;
const brightness = this.brightness / 100;
const lightness = brightness * (1 - saturation / 2);
saturation = lightness === 0 || lightness === 1 ? 0 : (brightness - lightness) / Math.min(lightness, 1 - lightness);
return new HSLColor(
toFixedNumber(this.hue, 2),
toFixedNumber(saturation * 100, 2),
toFixedNumber(lightness * 100, 2),
this.alpha
);
}
/**
* Converts a HSV color value to RGB.
* Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative.
* @returns An RGBColor object.
*/
toRGB() {
const hue = this.hue;
const saturation = this.saturation / 100;
const brightness = this.brightness / 100;
const fn = (n, k = (n + hue / 60) % 6) => brightness - saturation * brightness * Math.max(Math.min(k, 4 - k, 1), 0);
return new RGBColor(
Math.round(fn(5) * 255),
Math.round(fn(3) * 255),
Math.round(fn(1) * 255),
this.alpha
);
}
clone() {
return new _HSBColor(this.hue, this.saturation, this.brightness, this.alpha);
}
getChannelRange(channel) {
switch (channel) {
case "hue":
return { minValue: 0, maxValue: 360, step: 1, pageSize: 15 };
case "saturation":
case "brightness":
return { minValue: 0, maxValue: 100, step: 1, pageSize: 10 };
case "alpha":
return { minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1 };
default:
throw new Error(`Unknown color channel: ${channel}`);
}
}
getChannelFormatOptions(channel) {
switch (channel) {
case "hue":
return { style: "unit", unit: "degree", unitDisplay: "narrow" };
case "saturation":
case "brightness":
case "alpha":
return { style: "percent", maximumFractionDigits: 2 };
default:
throw new Error(`Unknown color channel: ${channel}`);
}
}
formatChannelValue(channel) {
const options = this.getChannelFormatOptions(channel);
let value = this.getChannelValue(channel);
if (channel === "saturation" || channel === "brightness") {
value /= 100;
}
return createNumberFormatter(() => options)().format(value);
}
getColorSpace() {
return "hsb";
}
static colorChannels = [
"hue",
"saturation",
"brightness"
];
getColorChannels() {
return _HSBColor.colorChannels;
}
};
var HSL_REGEX = /hsl\(([-+]?\d+(?:.\d+)?\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d+(?:.\d+)?%)\)|hsla\(([-+]?\d+(?:.\d+)?\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d(.\d+)?)\)/;
var HSLColor = class _HSLColor extends Color {
constructor(hue, saturation, lightness, alpha) {
super();
this.hue = hue;
this.saturation = saturation;
this.lightness = lightness;
this.alpha = alpha;
}
static parse(value) {
let m;
if (m = value.match(HSL_REGEX)) {
const [h, s, l, a] = (m[1] ?? m[2]).split(",").map((n) => Number(n.trim().replace("%", "")));
return new _HSLColor(
normalizeHue(h),
clamp(s, 0, 100),
clamp(l, 0, 100),
clamp(a ?? 1, 0, 1)
);
}
}
toString(format = "css") {
switch (format) {
case "hex":
return this.toRGB().toString("hex");
case "hexa":
return this.toRGB().toString("hexa");
case "hsl":
return `hsl(${this.hue} ${toFixedNumber(this.saturation, 2)}% ${toFixedNumber(this.lightness, 2)}%${this.alpha !== 1 && this.alpha !== 100 ? ` / ${this.alpha}` : ""})`;
case "css":
case "hsla":
return `hsla(${this.hue} ${toFixedNumber(this.saturation, 2)}% ${toFixedNumber(this.lightness, 2)}% / ${this.alpha})`;
default:
return this.toFormat(format).toString(format);
}
}
toFormat(format) {
switch (format) {
case "hsl":
case "hsla":
return this;
case "hsb":
case "hsba":
return this.toHSB();
case "rgb":
case "rgba":
return this.toRGB();
default:
throw new Error(`Unsupported color conversion: hsl -> ${format}`);
}
}
/**
* Converts a HSL color to HSB.
* Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV.
* @returns An HSBColor object.
*/
toHSB() {
let saturation = this.saturation / 100;
const lightness = this.lightness / 100;
const brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
saturation = brightness === 0 ? 0 : 2 * (1 - lightness / brightness);
return new HSBColor(
toFixedNumber(this.hue, 2),
toFixedNumber(saturation * 100, 2),
toFixedNumber(brightness * 100, 2),
this.alpha
);
}
/**
* Converts a HSL color to RGB.
* Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative.
* @returns An RGBColor object.
*/
toRGB() {
const hue = this.hue;
const saturation = this.saturation / 100;
const lightness = this.lightness / 100;
const a = saturation * Math.min(lightness, 1 - lightness);
const fn = (n, k = (n + hue / 30) % 12) => lightness - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return new RGBColor(
Math.round(fn(0) * 255),
Math.round(fn(8) * 255),
Math.round(fn(4) * 255),
this.alpha
);
}
clone() {
return new _HSLColor(this.hue, this.saturation, this.lightness, this.alpha);
}
getChannelRange(channel) {
switch (channel) {
case "hue":
return { minValue: 0, maxValue: 360, step: 1, pageSize: 15 };
case "saturation":
case "lightness":
return { minValue: 0, maxValue: 100, step: 1, pageSize: 10 };
case "alpha":
return { minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1 };
default:
throw new Error(`Unknown color channel: ${channel}`);
}
}
getChannelFormatOptions(channel) {
switch (channel) {
case "hue":
return { style: "unit", unit: "degree", unitDisplay: "narrow" };
case "saturation":
case "lightness":
case "alpha":
return { style: "percent", maximumFractionDigits: 2 };
default:
throw new Error(`Unknown color channel: ${channel}`);
}
}
formatChannelValue(channel) {
const options = this.getChannelFormatOptions(channel);
let value = this.getChannelValue(channel);
if (channel === "saturation" || channel === "lightness") {
value /= 100;
}
return createNumberFormatter(() => options)().format(value);
}
getColorSpace() {
return "hsl";
}
static colorChannels = [
"hue",
"saturation",
"lightness"
];
getColorChannels() {
return _HSLColor.colorChannels;
}
};
function toOKLCH(color) {
const rgb = color.toFormat("rgb");
let red = rgb.getChannelValue("red") / 255;
let green = rgb.getChannelValue("green") / 255;
let blue = rgb.getChannelValue("blue") / 255;
[red, green, blue] = lin_sRGB(red, green, blue);
const [x, y, z] = lin_sRGB_to_XYZ(red, green, blue);
const [l, a, b] = XYZ_to_OKLab(x, y, z);
return OKLab_to_OKLCH(l, a, b);
}
function OKLab_to_OKLCH(l, a, b) {
const hue = Math.atan2(b, a) * 180 / Math.PI;
return [
l,
Math.sqrt(a ** 2 + b ** 2),
// Chroma
hue >= 0 ? hue : hue + 360
// Hue, in degrees [0 to 360)
];
}
function lin_sRGB(r, g, b) {
return [lin_sRGB_component(r), lin_sRGB_component(g), lin_sRGB_component(b)];
}
function lin_sRGB_component(val) {
const sign = val < 0 ? -1 : 1;
const abs = Math.abs(val);
if (abs <= 0.04045) {
return val / 12.92;
}
return sign * ((abs + 0.055) / 1.055) ** 2.4;
}
function lin_sRGB_to_XYZ(r, g, b) {
const M = [
506752 / 1228815,
87881 / 245763,
12673 / 70218,
87098 / 409605,
175762 / 245763,
12673 / 175545,
7918 / 409605,
87881 / 737289,
1001167 / 1053270
];
return multiplyMatrix(M, r, g, b);
}
function XYZ_to_OKLab(x, y, z) {
const XYZtoLMS = [
0.819022437996703,
0.3619062600528904,
-0.1288737815209879,
0.0329836539323885,
0.9292868615863434,
0.0361446663506424,
0.0481771893596242,
0.2642395317527308,
0.6335478284694309
];
const LMStoOKLab = [
0.210454268309314,
0.7936177747023054,
-0.0040720430116193,
1.9779985324311684,
-2.42859224204858,
0.450593709617411,
0.0259040424655478,
0.7827717124575296,
-0.8086757549230774
];
const [a, b, c] = multiplyMatrix(XYZtoLMS, x, y, z);
return multiplyMatrix(LMStoOKLab, Math.cbrt(a), Math.cbrt(b), Math.cbrt(c));
}
function multiplyMatrix(m, x, y, z) {
const a = m[0] * x + m[1] * y + m[2] * z;
const b = m[3] * x + m[4] * y + m[5] * z;
const c = m[6] * x + m[7] * y + m[8] * z;
return [a, b, c];
}
function toFixedNumber(value, digits, base = 10) {
const pow = base ** digits;
return Math.round(value * pow) / pow;
}
export {
COLOR_INTL_TRANSLATIONS,
parseColor,
normalizeColor,
getColorChannels,
normalizeHue
};