Fix color picker syncing, update components to tailwind ui, display all formats at once

This commit is contained in:
2025-02-10 11:45:17 -05:00
parent 4261e6f436
commit 7ae2594a66
5 changed files with 408 additions and 90 deletions

226
package-lock.json generated
View File

@@ -8,6 +8,8 @@
"name": "color-picker", "name": "color-picker",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
@@ -879,6 +881,87 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
"license": "MIT",
"peerDependencies": {
"react": ">= 16 || ^19.0.0-rc"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1036,6 +1119,92 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@react-aria/focus": {
"version": "3.19.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz",
"integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.23.0",
"@react-aria/utils": "^3.27.0",
"@react-types/shared": "^3.27.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz",
"integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.7",
"@react-aria/utils": "^3.27.0",
"@react-types/shared": "^3.27.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz",
"integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.7",
"@react-stately/utils": "^3.10.5",
"@react-types/shared": "^3.27.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.5",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz",
"integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.6", "version": "4.34.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz",
@@ -1283,6 +1452,15 @@
"win32" "win32"
] ]
}, },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.6.tgz",
@@ -1507,6 +1685,33 @@
"vite": "^5.2.0 || ^6" "vite": "^5.2.0 || ^6"
} }
}, },
"node_modules/@tanstack/react-virtual": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz",
"integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz",
"integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2006,6 +2211,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3440,6 +3654,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.6.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.6.tgz",
@@ -3481,6 +3701,12 @@
"typescript": ">=4.8.4" "typescript": ">=4.8.4"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -10,6 +10,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { HexColorPicker } from 'react-colorful' import { HexColorPicker } from 'react-colorful'
import { SwatchIcon } from '@heroicons/react/24/outline'
import { import {
hexToRgb, rgbToHex, rgbToHsl, hslToRgb, rgbToHsb, hsbToRgb, hexToRgb, rgbToHex, rgbToHsl, hslToRgb, rgbToHsb, hsbToRgb,
findClosestTailwindColor, rgbToOklch, oklchToRgb, parseOklch, oklchToString findClosestTailwindColor, rgbToOklch, oklchToRgb, parseOklch, oklchToString
@@ -32,14 +33,30 @@ type OKLCH = {
type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hsb' | 'oklch' | 'tailwind' 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() { export function ColorPicker() {
const [color, setColor] = useState('#6366F1') // Default to indigo-500 const [color, setColor] = useState('#6366F1') // Default to indigo-500
const [format, setFormat] = useState<ColorFormat>('hex') const [colorValues, setColorValues] = useState<ColorValues>({
const [displayValue, setDisplayValue] = useState(color) hex: '#6366F1',
const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string }>({ 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', name: 'indigo-500',
hex: '#6366F1', hex: '#6366F1',
oklch: 'oklch(0.585 0.233 277.117)' oklch: 'oklch(0.585 0.233 277.117)',
isExactMatch: true
}) })
useEffect(() => { useEffect(() => {
@@ -50,35 +67,22 @@ export function ColorPicker() {
const closest = findClosestTailwindColor(color) const closest = findClosestTailwindColor(color)
setClosestTailwind(closest) setClosestTailwind(closest)
switch (format) { // Update all color format values
case 'hex': const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
setDisplayValue(color) const hsb = rgbToHsb(rgb.r, rgb.g, rgb.b)
break const oklch = rgbToOklch(rgb.r, rgb.g, rgb.b)
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])
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 let newHex = color
if (format === 'hex' && /^#[0-9A-Fa-f]{6}$/.test(value)) { if (format === 'hex' && /^#[0-9A-Fa-f]{6}$/.test(value)) {
@@ -118,68 +122,149 @@ export function ColorPicker() {
} }
setColor(newHex) setColor(newHex)
setDisplayValue(value) setColorValues(prev => ({
...prev,
[format]: value
}))
} }
return ( return (
<div className="w-full max-w-7xl mx-auto p-6"> <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"> <div className="grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-8">
{/* Left Column: Color Picker and Input */} {/* Left Column: Color Picker and Input */}
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col items-center gap-4 p-6 bg-white rounded-xl shadow-sm"> <div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-6">
{/* Color Preview */} <div className="space-y-6">
<div {/* Color Preview */}
className="w-32 h-32 rounded-lg shadow-lg border border-zinc-200" <div className="flex flex-col items-center gap-4">
style={{ backgroundColor: color }} <div
/> className="w-32 h-32 rounded-lg shadow-lg ring-1 ring-gray-900/5"
style={{ backgroundColor: color }}
/>
<div className="text-sm text-zinc-600 text-center"> <div className="text-sm text-gray-600 text-center">
<div>Closest Tailwind: <span className="font-medium">{closestTailwind.name}</span></div> <div className="flex items-center justify-center gap-1.5">
<div className="text-xs mt-1 text-zinc-400"> <SwatchIcon className="h-4 w-4" />
{format === 'oklch' ? closestTailwind.oklch : closestTailwind.hex} {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>
</div>
{/* Color Picker */} {/* Color Picker */}
<div className="w-full max-w-[200px]"> <div className="w-full max-w-[200px] mx-auto">
<HexColorPicker color={color} onChange={setColor} /> <HexColorPicker color={color} onChange={setColor} />
</div> </div>
{/* Format Buttons */} {/* Color Format Inputs */}
<div className="flex flex-wrap gap-2 justify-center w-full"> <div className="space-y-3">
{(['hex', 'rgb', 'hsl', 'hsb', 'oklch', 'tailwind'] as const).map((f) => ( <div>
<button <label htmlFor="hex-value" className="block text-sm font-medium text-gray-700 mb-1">
key={f} HEX
onClick={() => setFormat(f)} </label>
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors <input
${format === f type="text"
? 'bg-indigo-500 text-white' id="hex-value"
: 'bg-zinc-100 text-zinc-700 hover:bg-zinc-200' 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"
{f.toUpperCase()} />
</button> </div>
))}
</div>
{/* Color Input */} <div>
<div className="w-full"> <label htmlFor="rgb-value" className="block text-sm font-medium text-gray-700 mb-1">
<input RGB
type="text" </label>
value={displayValue} <input
onChange={(e) => handleInputChange(e.target.value)} type="text"
className="w-full px-4 py-2 rounded-md border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-indigo-500" id="rgb-value"
placeholder="Enter color 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> </div>
</div> </div>
{/* Right Column: Tailwind Colors */} {/* Right Column: Tailwind Colors */}
<div className="bg-white rounded-xl shadow-sm p-4"> <div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-4">
<TailwindColors onColorSelect={setColor} selectedColor={color} format={format} /> <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> </div>
</div> </div>

View File

@@ -4,10 +4,9 @@ import { TAILWIND_COLORS } from '../utils/colors'
interface TailwindColorsProps { interface TailwindColorsProps {
onColorSelect: (color: string) => void onColorSelect: (color: string) => void
selectedColor?: string 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 // Order colors in specified sequence
const orderedColors = useMemo(() => [ const orderedColors = useMemo(() => [
'red', 'red',
@@ -52,25 +51,24 @@ export function TailwindColors({ onColorSelect, selectedColor, format = 'hex' }:
{/* Color rows */} {/* Color rows */}
{orderedColors.map(colorName => ( {orderedColors.map(colorName => (
<React.Fragment key={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} {colorName}
</div> </div>
{shades.map(shade => { {shades.map(shade => {
const values = TAILWIND_COLORS[colorName][shade] const values = TAILWIND_COLORS[colorName][shade]
const hexColor = typeof values === 'string' ? values : values.hex const hexColor = typeof values === 'string' ? values : values.hex
const displayValue = format === 'oklch' && typeof values !== 'string' ? values.oklch : hexColor
const textColorClass = getTextColor(hexColor) const textColorClass = getTextColor(hexColor)
return ( return (
<button <button
key={`${colorName}-${shade}`} key={`${colorName}-${shade}`}
onClick={() => onColorSelect(displayValue)} onClick={() => onColorSelect(hexColor)}
className={`aspect-square transition-all hover:scale-105 hover:shadow-lg group relative 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 === displayValue ? 'ring-2 ring-offset-1 ring-indigo-500 z-10' : ''}`} ${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 }} 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} {shade}
</span> </span>
</button> </button>

View File

@@ -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 // 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') const rgb1 = color.startsWith('oklch')
? oklchToRgb(...Object.values(parseOklch(color) || { l: 0, c: 0, h: 0 })) ? oklchToRgb(...Object.values(parseOklch(color) || { l: 0, c: 0, h: 0 }))
: hexToRgb(color) : 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 closestColor = ''
let closestHex = '' let closestHex = ''
let closestOklch = '' let closestOklch = ''
let minDistance = Infinity let minDistance = Infinity
let isExactMatch = false
const lab1 = rgbToLab(rgb1.r, rgb1.g, rgb1.b) 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 lab2 = rgbToLab(rgb2.r, rgb2.g, rgb2.b)
const distance = deltaE(lab1, lab2) 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 minDistance = distance
closestColor = `${colorName}-${shade}` closestColor = `${colorName}-${shade}`
closestHex = values.hex 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 // CIELAB color space conversion functions