UNPKG

@pastel-palette/colors

Version:

Core color definitions package for the UI Color System, featuring OKLCH color space support, TypeScript types, and a kawaii aesthetic.

287 lines (251 loc) 7.65 kB
import { oklch, p3, rgb } from 'culori' import type { ColorValue } from './types' import type { ColorValidationResult, ContrastRatio, OKLCH, RGB, } from './types/utilities' export function mapHexToRGBString(hex: string): string { const r = Number.parseInt(hex.slice(1, 3), 16) const g = Number.parseInt(hex.slice(3, 5), 16) const b = Number.parseInt(hex.slice(5, 7), 16) return `rgb(${r} ${g} ${b})` } export function addAlphaToHex(hex: string, alpha: number): string { const alphaHex = Math.round(alpha * 255) .toString(16) .padStart(2, '0') return hex + alphaHex } export function parseOKLCH(oklchString: string): OKLCH | null { const match = oklchString.match( /oklch\(([\d.]+)%?\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\)/, ) if (!match) return null const l = Number.parseFloat(match[1]) / (match[1].includes('%') ? 100 : 1) const c = Number.parseFloat(match[2]) const h = Number.parseFloat(match[3]) const a = match[4] ? Number.parseFloat(match[4]) : 1 return { l, c, h, a } } export function formatOKLCH(oklch: OKLCH): string { const { l, c, h, a = 1 } = oklch if (a < 1) { return `oklch(${l} ${c} ${h} / ${a})` } return `oklch(${l} ${c} ${h})` } export function parseRGB(rgbString: string): RGB | null { const match = rgbString.match( /rgba?\(([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\)/, ) if (!match) return null const r = Number.parseInt(match[1]) const g = Number.parseInt(match[2]) const b = Number.parseInt(match[3]) const a = match[4] ? Number.parseFloat(match[4]) : 1 return { r, g, b, a } } export function formatRGB(rgb: RGB): string { const { r, g, b, a = 1 } = rgb if (a < 1) { return `rgba(${r} ${g} ${b} / ${a})` } return `rgb(${r} ${g} ${b})` } export function validateColor(colorValue: string): ColorValidationResult { const errors: string[] = [] const warnings: string[] = [] if (colorValue.startsWith('oklch(')) { const oklch = parseOKLCH(colorValue) if (!oklch) { errors.push('Invalid OKLCH format') } else { if (oklch.l < 0 || oklch.l > 1) { errors.push('Lightness must be between 0 and 1') } if (oklch.c < 0 || oklch.c > 0.37) { errors.push('Chroma must be between 0 and 0.37') } if (oklch.h < 0 || oklch.h >= 360) { errors.push('Hue must be between 0 and 360') } if (oklch.c > 0.3) { warnings.push( 'Chroma values above 0.3 may not display correctly in sRGB', ) } } } else if (colorValue.startsWith('rgb')) { const rgb = parseRGB(colorValue) if (!rgb) { errors.push('Invalid RGB format') } else { if ( rgb.r < 0 || rgb.r > 255 || rgb.g < 0 || rgb.g > 255 || rgb.b < 0 || rgb.b > 255 ) { errors.push('RGB values must be between 0 and 255') } } } else { errors.push('Unknown color format') } return { valid: errors.length === 0, errors, warnings, } } export function calculateRelativeLuminance(rgb: RGB): number { const rsRGB = rgb.r / 255 const gsRGB = rgb.g / 255 const bsRGB = rgb.b / 255 const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4) const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4) const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4) return 0.2126 * r + 0.7152 * g + 0.0722 * b } export function calculateContrastRatio(rgb1: RGB, rgb2: RGB): ContrastRatio { const l1 = calculateRelativeLuminance(rgb1) const l2 = calculateRelativeLuminance(rgb2) const lighter = Math.max(l1, l2) const darker = Math.min(l1, l2) const ratio = (lighter + 0.05) / (darker + 0.05) return { ratio, passes: { aa: ratio >= 4.5, aaa: ratio >= 7, largeTextAa: ratio >= 3, largeTextAaa: ratio >= 4.5, }, } } export function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)) } export function toFixed(value: number, precision = 3): number { return Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision) } /** * Generate color formats from OKLCH string */ export function createColorFromOKLCH(oklchString: string): ColorValue { // Parse OKLCH string manually to get the raw values const match = oklchString.match( /oklch\(([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\)/, ) if (!match) { throw new Error(`Invalid OKLCH format: ${oklchString}`) } const l = Number.parseFloat(match[1]) const c = Number.parseFloat(match[2]) const h = Number.parseFloat(match[3]) const alpha = match[4] ? Number.parseFloat(match[4]) : 1 // Create culori OKLCH object with proper mode const oklchColor = { mode: 'oklch' as const, l, c, h, alpha } // Convert to sRGB const srgbColor = rgb(oklchColor) const srgbString = srgbColor ? alpha < 1 ? `rgb(${Math.max( 0, Math.min(255, Math.round(srgbColor.r * 255)), )} ${Math.max( 0, Math.min(255, Math.round(srgbColor.g * 255)), )} ${Math.max( 0, Math.min(255, Math.round(srgbColor.b * 255)), )} / ${alpha})` : `rgb(${Math.max( 0, Math.min(255, Math.round(srgbColor.r * 255)), )} ${Math.max( 0, Math.min(255, Math.round(srgbColor.g * 255)), )} ${Math.max(0, Math.min(255, Math.round(srgbColor.b * 255)))})` : alpha < 1 ? `rgb(0 0 0 / ${alpha})` : 'rgb(0 0 0)' // Convert to P3 const p3Color = p3(oklchColor) const p3String = p3Color ? alpha < 1 ? `color(display-p3 ${toFixed(p3Color.r, 3)} ${toFixed( p3Color.g, 3, )} ${toFixed(p3Color.b, 3)} / ${alpha})` : `color(display-p3 ${toFixed(p3Color.r, 3)} ${toFixed( p3Color.g, 3, )} ${toFixed(p3Color.b, 3)})` : undefined return { oklch: oklchString, srgb: srgbString, p3: p3String, } } /** * Generate color formats from sRGB string */ export function createColorFromSRGB(srgbString: string): ColorValue { // Parse sRGB string const match = srgbString.match( /rgba?\((\d+)\s+(\d+)\s+(\d+)(?:\s*\/\s*([\d.]+))?\)/, ) if (!match) { throw new Error(`Invalid sRGB format: ${srgbString}`) } const r = Number.parseInt(match[1]) / 255 const g = Number.parseInt(match[2]) / 255 const b = Number.parseInt(match[3]) / 255 const alpha = match[4] ? Number.parseFloat(match[4]) : 1 // Create culori RGB object with proper mode const rgbColor = { mode: 'rgb' as const, r, g, b, alpha } // Convert to OKLCH const oklchColor = oklch(rgbColor) const oklchString = oklchColor ? `oklch(${toFixed(oklchColor.l, 3)} ${toFixed(oklchColor.c, 3)} ${toFixed( oklchColor.h || 0, 0, )})` : 'oklch(0 0 0)' // Convert to P3 const p3Color = p3(rgbColor) const p3String = p3Color ? `color(display-p3 ${toFixed(p3Color.r, 3)} ${toFixed( p3Color.g, 3, )} ${toFixed(p3Color.b, 3)})` : undefined return { oklch: oklchString, srgb: srgbString, p3: p3String, } } /** * Create color from either OKLCH or sRGB input */ export function createColor(input: string): ColorValue { if (input.startsWith('oklch(')) { return createColorFromOKLCH(input) } else if (input.startsWith('rgb')) { return createColorFromSRGB(input) } else { throw new Error( `Unsupported color format: ${input}. Use oklch() or rgb() format.`, ) } }