272 lines
9.8 KiB
TypeScript
272 lines
9.8 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { HexColorPicker } from 'react-colorful'
|
|
import { SwatchIcon } from '@heroicons/react/24/outline'
|
|
import {
|
|
hexToRgb, rgbToHex, rgbToHsl, hslToRgb, rgbToHsb, hsbToRgb,
|
|
findClosestTailwindColor, rgbToOklch, oklchToRgb, parseOklch, oklchToString
|
|
} from '../utils/colors'
|
|
import { TailwindColors } from './TailwindColors'
|
|
|
|
type RGB = {
|
|
r: number
|
|
g: number
|
|
b: number
|
|
}
|
|
|
|
type HSL = {
|
|
h: number
|
|
s: number
|
|
l: number
|
|
}
|
|
|
|
type HSB = {
|
|
h: number
|
|
s: number
|
|
b: number
|
|
}
|
|
|
|
type OKLCH = {
|
|
l: number
|
|
c: number
|
|
h: number
|
|
}
|
|
|
|
type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hsb' | 'oklch' | 'tailwind'
|
|
|
|
type ColorValues = {
|
|
hex: string
|
|
rgb: string
|
|
hsl: string
|
|
hsb: string
|
|
oklch: string
|
|
tailwind: string
|
|
}
|
|
|
|
export function ColorPicker() {
|
|
const [color, setColor] = useState('#6366F1') // Default to indigo-500
|
|
const [colorValues, setColorValues] = useState<ColorValues>({
|
|
hex: '#6366F1',
|
|
rgb: 'rgb(99, 102, 241)',
|
|
hsl: 'hsl(239, 84%, 67%)',
|
|
hsb: 'hsb(239, 59%, 95%)',
|
|
oklch: 'oklch(0.585 0.233 277.117)',
|
|
tailwind: 'indigo-500'
|
|
})
|
|
const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string; isExactMatch: boolean }>({
|
|
name: 'indigo-500',
|
|
hex: '#6366F1',
|
|
oklch: 'oklch(0.585 0.233 277.117)',
|
|
isExactMatch: true
|
|
})
|
|
|
|
useEffect(() => {
|
|
const rgb = hexToRgb(color)
|
|
if (!rgb) return
|
|
|
|
// Always update closest Tailwind color
|
|
const closest = findClosestTailwindColor(color)
|
|
setClosestTailwind(closest)
|
|
|
|
// Update all color format values
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
|
const hsb = rgbToHsb(rgb.r, rgb.g, rgb.b)
|
|
const oklch = rgbToOklch(rgb.r, rgb.g, rgb.b)
|
|
|
|
setColorValues({
|
|
hex: color,
|
|
rgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
hsl: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`,
|
|
hsb: `hsb(${hsb.h}, ${hsb.s}%, ${hsb.b}%)`,
|
|
oklch: oklchToString(oklch.l, oklch.c, oklch.h),
|
|
tailwind: closest.name
|
|
})
|
|
}, [color])
|
|
|
|
const handleInputChange = (value: string, format: ColorFormat) => {
|
|
let newHex = color
|
|
|
|
if (format === 'hex' && /^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
newHex = value
|
|
} else if (format === 'rgb') {
|
|
const match = value.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
|
|
if (match) {
|
|
const [_, r, g, b] = match.map(Number)
|
|
if (r <= 255 && g <= 255 && b <= 255) {
|
|
newHex = rgbToHex(r, g, b)
|
|
}
|
|
}
|
|
} else if (format === 'hsl') {
|
|
const match = value.match(/^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/)
|
|
if (match) {
|
|
const [_, h, s, l] = match.map(Number)
|
|
if (h <= 360 && s <= 100 && l <= 100) {
|
|
const rgb = hslToRgb(h, s, l)
|
|
newHex = rgbToHex(rgb.r, rgb.g, rgb.b)
|
|
}
|
|
}
|
|
} else if (format === 'hsb') {
|
|
const match = value.match(/^hsb\((\d+),\s*(\d+)%,\s*(\d+)%\)$/)
|
|
if (match) {
|
|
const [_, h, s, b] = match.map(Number)
|
|
if (h <= 360 && s <= 100 && b <= 100) {
|
|
const rgb = hsbToRgb(h, s, b)
|
|
newHex = rgbToHex(rgb.r, rgb.g, rgb.b)
|
|
}
|
|
}
|
|
} else if (format === 'oklch') {
|
|
const oklch = parseOklch(value)
|
|
if (oklch) {
|
|
const rgb = oklchToRgb(oklch.l, oklch.c, oklch.h)
|
|
newHex = rgbToHex(rgb.r, rgb.g, rgb.b)
|
|
}
|
|
}
|
|
|
|
setColor(newHex)
|
|
setColorValues(prev => ({
|
|
...prev,
|
|
[format]: value
|
|
}))
|
|
}
|
|
|
|
return (
|
|
<div className="w-full max-w-7xl mx-auto p-6">
|
|
<div className="text-center space-y-2 mb-8">
|
|
<h1 className="text-4xl font-bold tracking-tight text-gray-900">Color Picker</h1>
|
|
<p className="text-lg text-gray-600">Find and convert colors in different formats</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-8">
|
|
{/* Left Column: Color Picker and Input */}
|
|
<div className="space-y-6">
|
|
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-6">
|
|
<div className="space-y-6">
|
|
{/* Color Preview */}
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div
|
|
className="w-32 h-32 rounded-lg shadow-lg ring-1 ring-gray-900/5"
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
|
|
<div className="text-sm text-gray-600 text-center">
|
|
<div className="flex items-center justify-center gap-1.5">
|
|
<SwatchIcon className="h-4 w-4" />
|
|
{closestTailwind.isExactMatch ? (
|
|
<span>Tailwind: <span className="font-medium text-gray-900">{closestTailwind.name}</span></span>
|
|
) : (
|
|
<span>Closest Tailwind: <span className="font-medium text-gray-900">{closestTailwind.name}</span></span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Color Picker */}
|
|
<div className="w-full max-w-[200px] mx-auto">
|
|
<HexColorPicker color={color} onChange={setColor} />
|
|
</div>
|
|
|
|
{/* Color Format Inputs */}
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label htmlFor="hex-value" className="block text-sm font-medium text-gray-700 mb-1">
|
|
HEX
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="hex-value"
|
|
value={colorValues.hex}
|
|
onChange={(e) => handleInputChange(e.target.value, 'hex')}
|
|
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="rgb-value" className="block text-sm font-medium text-gray-700 mb-1">
|
|
RGB
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="rgb-value"
|
|
value={colorValues.rgb}
|
|
onChange={(e) => handleInputChange(e.target.value, 'rgb')}
|
|
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="hsl-value" className="block text-sm font-medium text-gray-700 mb-1">
|
|
HSL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="hsl-value"
|
|
value={colorValues.hsl}
|
|
onChange={(e) => handleInputChange(e.target.value, 'hsl')}
|
|
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="hsb-value" className="block text-sm font-medium text-gray-700 mb-1">
|
|
HSB
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="hsb-value"
|
|
value={colorValues.hsb}
|
|
onChange={(e) => handleInputChange(e.target.value, 'hsb')}
|
|
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="oklch-value" className="block text-sm font-medium text-gray-700 mb-1">
|
|
OKLCH
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="oklch-value"
|
|
value={colorValues.oklch}
|
|
onChange={(e) => handleInputChange(e.target.value, 'oklch')}
|
|
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="tailwind-value" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tailwind
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="tailwind-value"
|
|
value={colorValues.tailwind}
|
|
readOnly
|
|
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 bg-gray-50 shadow-sm ring-1 ring-inset ring-gray-300 sm:text-sm sm:leading-6"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column: Tailwind Colors */}
|
|
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-4">
|
|
<TailwindColors
|
|
onColorSelect={(selectedColor) => {
|
|
// Always set the hex color for the color picker
|
|
if (selectedColor.startsWith('oklch')) {
|
|
const oklch = parseOklch(selectedColor)
|
|
if (oklch) {
|
|
const rgb = oklchToRgb(oklch.l, oklch.c, oklch.h)
|
|
setColor(rgbToHex(rgb.r, rgb.g, rgb.b))
|
|
}
|
|
} else {
|
|
setColor(selectedColor)
|
|
}
|
|
}}
|
|
selectedColor={color}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|