UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

743 lines (739 loc) 22.1 kB
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 };