From 7ae2594a66cfcda25be641d39a746423891eee3b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 10 Feb 2025 11:45:17 -0500 Subject: [PATCH] Fix color picker syncing, update components to tailwind ui, display all formats at once --- package-lock.json | 226 ++++++++++++++++++++++++++++ package.json | 2 + src/components/ColorPicker.tsx | 239 ++++++++++++++++++++---------- src/components/TailwindColors.tsx | 16 +- src/utils/colors.ts | 15 +- 5 files changed, 408 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4da873..7300c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "color-picker", "version": "0.0.0", "dependencies": { + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", "@tailwindcss/vite": "^4.0.6", "react": "^19.0.0", "react-colorful": "^5.6.1", @@ -879,6 +881,87 @@ "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": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1036,6 +1119,92 @@ "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": { "version": "4.34.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", @@ -1283,6 +1452,15 @@ "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": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.6.tgz", @@ -1507,6 +1685,33 @@ "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": { "version": "7.20.5", "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" } }, + "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3440,6 +3654,12 @@ "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": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.6.tgz", @@ -3481,6 +3701,12 @@ "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": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 7de6265..34b02e3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", "@tailwindcss/vite": "^4.0.6", "react": "^19.0.0", "react-colorful": "^5.6.1", diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 3fbe68e..a5a3b5f 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -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('hex') - const [displayValue, setDisplayValue] = useState(color) - const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string }>({ + const [colorValues, setColorValues] = useState({ + 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 (
-

Color Picker

+
+

Color Picker

+

Find and convert colors in different formats

+
{/* Left Column: Color Picker and Input */}
-
- {/* Color Preview */} -
- -
-
Closest Tailwind: {closestTailwind.name}
-
- {format === 'oklch' ? closestTailwind.oklch : closestTailwind.hex} +
+
+ {/* Color Preview */} +
+
+ +
+
+ + {closestTailwind.isExactMatch ? ( + Tailwind: {closestTailwind.name} + ) : ( + Closest Tailwind: {closestTailwind.name} + )} +
+
-
- {/* Color Picker */} -
- -
+ {/* Color Picker */} +
+ +
- {/* Format Buttons */} -
- {(['hex', 'rgb', 'hsl', 'hsb', 'oklch', 'tailwind'] as const).map((f) => ( - - ))} -
+ {/* Color Format Inputs */} +
+
+ + 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" + /> +
- {/* Color Input */} -
- 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..." - /> +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + +
+
{/* Right Column: Tailwind Colors */} -
- +
+ { + // 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} + />
diff --git a/src/components/TailwindColors.tsx b/src/components/TailwindColors.tsx index 8efc274..2b9e9bd 100644 --- a/src/components/TailwindColors.tsx +++ b/src/components/TailwindColors.tsx @@ -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 => ( -
+
{colorName}
{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 ( diff --git a/src/utils/colors.ts b/src/utils/colors.ts index ffb5193..2ab4ae3 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -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