reka-ui
Version:
Vue port for Radix UI Primitives.
148 lines (126 loc) • 3.55 kB
text/typescript
import type { Color, HSBColor, HSLColor, RGBColor } from './types'
const HEX3_RE = /^[0-9A-F]{3}$/i
const HEX6_RE = /^[0-9A-F]{6}$/i
const HEX8_RE = /^[0-9A-F]{8}$/i
const RGB_RE = /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)/
const HSL_RE = /hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*([\d.]+)\s*)?\)/
const HSB_RE = /hsb[av]?\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*(?:,\s*([\d.]+)\s*)?\)/
/**
* Parses a color string into a Color object.
* Supports hex (#rrggbb, #rgb), rgb(), hsl(), and hsb()/hsv() formats.
*/
export function parseColor(value: string): Color {
const trimmed = value.trim().toLowerCase()
// Hex format
if (trimmed.startsWith('#')) {
return parseHex(trimmed)
}
// rgb() format
if (trimmed.startsWith('rgb')) {
return parseRgb(trimmed)
}
// hsl() format
if (trimmed.startsWith('hsl')) {
return parseHsl(trimmed)
}
// hsb() or hsv() format
if (trimmed.startsWith('hsb') || trimmed.startsWith('hsv')) {
return parseHsb(trimmed)
}
throw new Error(`Unable to parse color: ${value}`)
}
function parseHex(hex: string): RGBColor {
let normalized = hex.slice(1)
// Validate hex format (3, 6, or 8 hex digits)
if (!HEX3_RE.test(normalized) && !HEX6_RE.test(normalized) && !HEX8_RE.test(normalized)) {
throw new Error(`Invalid hex color: ${hex}. Expected format: #RGB, #RRGGBB, or #RRGGBBAA`)
}
// Expand shorthand (e.g., #f00 -> #ff0000)
if (normalized.length === 3) {
normalized = normalized.split('').map(c => c + c).join('')
}
// Handle 6-digit hex
if (normalized.length === 6) {
const bigint = parseInt(normalized, 16)
return {
space: 'rgb',
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255,
alpha: 1,
}
}
// Handle 8-digit hex (with alpha)
if (normalized.length === 8) {
const bigint = parseInt(normalized, 16)
return {
space: 'rgb',
r: (bigint >> 24) & 255,
g: (bigint >> 16) & 255,
b: (bigint >> 8) & 255,
alpha: (bigint & 255) / 255,
}
}
throw new Error(`Invalid hex color: ${hex}`)
}
function parseRgb(rgb: string): RGBColor {
const match = rgb.match(RGB_RE)
if (!match) {
throw new Error(`Invalid RGB color: ${rgb}`)
}
return {
space: 'rgb',
r: parseFloat(match[1]),
g: parseFloat(match[2]),
b: parseFloat(match[3]),
alpha: match[4] ? parseFloat(match[4]) : 1,
}
}
function parseHsl(hsl: string): HSLColor {
const match = hsl.match(HSL_RE)
if (!match) {
throw new Error(`Invalid HSL color: ${hsl}`)
}
return {
space: 'hsl',
h: parseFloat(match[1]),
s: parseFloat(match[2]),
l: parseFloat(match[3]),
alpha: match[4] ? parseFloat(match[4]) : 1,
}
}
function parseHsb(hsb: string): HSBColor {
const match = hsb.match(HSB_RE)
if (!match) {
throw new Error(`Invalid HSB color: ${hsb}`)
}
return {
space: 'hsb',
h: parseFloat(match[1]),
s: parseFloat(match[2]),
b: parseFloat(match[3]),
alpha: match[4] ? parseFloat(match[4]) : 1,
}
}
/**
* Normalizes a value to a Color object.
* If already a Color, returns it. If a string, parses it.
*/
export function normalizeColor(value: string | Color): Color {
if (typeof value === 'string') {
return parseColor(value)
}
return value
}
/**
* Checks if a string is a valid color.
*/
export function isValidColor(value: string): boolean {
try {
parseColor(value)
return true
}
catch {
return false
}
}