Fix color picker syncing, update components to tailwind ui, display all formats at once
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -32,14 +33,30 @@ type OKLCH = {
|
||||
|
||||
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 [format, setFormat] = useState<ColorFormat>('hex')
|
||||
const [displayValue, setDisplayValue] = useState(color)
|
||||
const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string }>({
|
||||
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)'
|
||||
oklch: 'oklch(0.585 0.233 277.117)',
|
||||
isExactMatch: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,35 +67,22 @@ export function ColorPicker() {
|
||||
const closest = findClosestTailwindColor(color)
|
||||
setClosestTailwind(closest)
|
||||
|
||||
switch (format) {
|
||||
case 'hex':
|
||||
setDisplayValue(color)
|
||||
break
|
||||
case 'rgb':
|
||||
setDisplayValue(`rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`)
|
||||
break
|
||||
case 'hsl': {
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
||||
setDisplayValue(`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`)
|
||||
break
|
||||
}
|
||||
case 'hsb': {
|
||||
const hsb = rgbToHsb(rgb.r, rgb.g, rgb.b)
|
||||
setDisplayValue(`hsb(${hsb.h}, ${hsb.s}%, ${hsb.b}%)`)
|
||||
break
|
||||
}
|
||||
case 'oklch': {
|
||||
const oklch = rgbToOklch(rgb.r, rgb.g, rgb.b)
|
||||
setDisplayValue(oklchToString(oklch.l, oklch.c, oklch.h))
|
||||
break
|
||||
}
|
||||
case 'tailwind':
|
||||
setDisplayValue(closest.name)
|
||||
break
|
||||
}
|
||||
}, [color, format])
|
||||
// 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)
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
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)) {
|
||||
@@ -118,68 +122,149 @@ export function ColorPicker() {
|
||||
}
|
||||
|
||||
setColor(newHex)
|
||||
setDisplayValue(value)
|
||||
setColorValues(prev => ({
|
||||
...prev,
|
||||
[format]: value
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-7xl mx-auto p-6">
|
||||
<h1 className="text-3xl font-bold text-center mb-8">Color Picker</h1>
|
||||
<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="flex flex-col items-center gap-4 p-6 bg-white rounded-xl shadow-sm">
|
||||
{/* Color Preview */}
|
||||
<div
|
||||
className="w-32 h-32 rounded-lg shadow-lg border border-zinc-200"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
|
||||
<div className="text-sm text-zinc-600 text-center">
|
||||
<div>Closest Tailwind: <span className="font-medium">{closestTailwind.name}</span></div>
|
||||
<div className="text-xs mt-1 text-zinc-400">
|
||||
{format === 'oklch' ? closestTailwind.oklch : closestTailwind.hex}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Color Picker */}
|
||||
<div className="w-full max-w-[200px]">
|
||||
<HexColorPicker color={color} onChange={setColor} />
|
||||
</div>
|
||||
{/* Color Picker */}
|
||||
<div className="w-full max-w-[200px] mx-auto">
|
||||
<HexColorPicker color={color} onChange={setColor} />
|
||||
</div>
|
||||
|
||||
{/* Format Buttons */}
|
||||
<div className="flex flex-wrap gap-2 justify-center w-full">
|
||||
{(['hex', 'rgb', 'hsl', 'hsb', 'oklch', 'tailwind'] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFormat(f)}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors
|
||||
${format === f
|
||||
? 'bg-indigo-500 text-white'
|
||||
: 'bg-zinc-100 text-zinc-700 hover:bg-zinc-200'
|
||||
}`}
|
||||
>
|
||||
{f.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Color Input */}
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
className="w-full px-4 py-2 rounded-md border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="Enter color value..."
|
||||
/>
|
||||
<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 p-4">
|
||||
<TailwindColors onColorSelect={setColor} selectedColor={color} format={format} />
|
||||
<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>
|
||||
|
||||
@@ -4,10 +4,9 @@ import { TAILWIND_COLORS } from '../utils/colors'
|
||||
interface TailwindColorsProps {
|
||||
onColorSelect: (color: string) => void
|
||||
selectedColor?: string
|
||||
format?: 'hex' | 'rgb' | 'hsl' | 'hsb' | 'oklch' | 'tailwind'
|
||||
}
|
||||
|
||||
export function TailwindColors({ onColorSelect, selectedColor, format = 'hex' }: TailwindColorsProps) {
|
||||
export function TailwindColors({ onColorSelect, selectedColor }: TailwindColorsProps) {
|
||||
// Order colors in specified sequence
|
||||
const orderedColors = useMemo(() => [
|
||||
'red',
|
||||
@@ -52,25 +51,24 @@ export function TailwindColors({ onColorSelect, selectedColor, format = 'hex' }:
|
||||
{/* Color rows */}
|
||||
{orderedColors.map(colorName => (
|
||||
<React.Fragment key={colorName}>
|
||||
<div className="sticky left-0 z-10 bg-white text-sm font-medium text-zinc-600 capitalize py-1 pr-3 whitespace-nowrap">
|
||||
<div className="flex sticky left-0 z-10 bg-white text-sm font-medium items-center justify-end text-gray-900 py-1 pr-3 whitespace-nowrap border-r border-gray-100">
|
||||
{colorName}
|
||||
</div>
|
||||
{shades.map(shade => {
|
||||
const values = TAILWIND_COLORS[colorName][shade]
|
||||
const hexColor = typeof values === 'string' ? values : values.hex
|
||||
const displayValue = format === 'oklch' && typeof values !== 'string' ? values.oklch : hexColor
|
||||
const textColorClass = getTextColor(hexColor)
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${colorName}-${shade}`}
|
||||
onClick={() => onColorSelect(displayValue)}
|
||||
className={`aspect-square transition-all hover:scale-105 hover:shadow-lg group relative
|
||||
${selectedColor === displayValue ? 'ring-2 ring-offset-1 ring-indigo-500 z-10' : ''}`}
|
||||
onClick={() => onColorSelect(hexColor)}
|
||||
className={`aspect-square transition-all hover:scale-110 hover:z-10 hover:shadow-lg hover:ring-2 hover:ring-offset-2 hover:ring-indigo-600 relative
|
||||
${selectedColor === hexColor ? 'ring-2 ring-offset-2 ring-indigo-600 z-10 shadow-lg' : 'shadow-sm ring-1 ring-inset ring-gray-900/5'}`}
|
||||
style={{ backgroundColor: hexColor }}
|
||||
title={`${colorName}-${shade}: ${displayValue}`}
|
||||
title={`${colorName}-${shade}: ${hexColor}`}
|
||||
>
|
||||
<span className={`absolute inset-0 flex items-center justify-center text-[10px] font-medium ${textColorClass}`}>
|
||||
<span className={`absolute inset-0 flex items-center justify-center text-xs font-medium ${textColorClass}`}>
|
||||
{shade}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -259,17 +259,18 @@ export function oklchToString(l: number, c: number, h: number): string {
|
||||
}
|
||||
|
||||
// Function to find the closest Tailwind color using CIELAB color space
|
||||
export function findClosestTailwindColor(color: string): { name: string; hex: string; oklch: string } {
|
||||
export function findClosestTailwindColor(color: string): { name: string; hex: string; oklch: string; isExactMatch: boolean } {
|
||||
const rgb1 = color.startsWith('oklch')
|
||||
? oklchToRgb(...Object.values(parseOklch(color) || { l: 0, c: 0, h: 0 }))
|
||||
: hexToRgb(color)
|
||||
|
||||
if (!rgb1) return { name: 'Invalid color', hex: color, oklch: '' }
|
||||
if (!rgb1) return { name: 'Invalid color', hex: color, oklch: '', isExactMatch: false }
|
||||
|
||||
let closestColor = ''
|
||||
let closestHex = ''
|
||||
let closestOklch = ''
|
||||
let minDistance = Infinity
|
||||
let isExactMatch = false
|
||||
|
||||
const lab1 = rgbToLab(rgb1.r, rgb1.g, rgb1.b)
|
||||
|
||||
@@ -281,7 +282,13 @@ export function findClosestTailwindColor(color: string): { name: string; hex: st
|
||||
const lab2 = rgbToLab(rgb2.r, rgb2.g, rgb2.b)
|
||||
const distance = deltaE(lab1, lab2)
|
||||
|
||||
if (distance < minDistance) {
|
||||
if (distance === 0) {
|
||||
isExactMatch = true
|
||||
closestColor = `${colorName}-${shade}`
|
||||
closestHex = values.hex
|
||||
closestOklch = values.oklch
|
||||
minDistance = 0
|
||||
} else if (distance < minDistance && !isExactMatch) {
|
||||
minDistance = distance
|
||||
closestColor = `${colorName}-${shade}`
|
||||
closestHex = values.hex
|
||||
@@ -290,7 +297,7 @@ export function findClosestTailwindColor(color: string): { name: string; hex: st
|
||||
})
|
||||
})
|
||||
|
||||
return { name: closestColor, hex: closestHex, oklch: closestOklch }
|
||||
return { name: closestColor, hex: closestHex, oklch: closestOklch, isExactMatch }
|
||||
}
|
||||
|
||||
// CIELAB color space conversion functions
|
||||
|
||||
Reference in New Issue
Block a user