Fix color picker syncing, update components to tailwind ui, display all formats at once
This commit is contained in:
226
package-lock.json
generated
226
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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':
|
|
||||||
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)
|
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)
|
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)
|
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">
|
||||||
|
<div className="space-y-6">
|
||||||
{/* Color Preview */}
|
{/* Color Preview */}
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
<div
|
<div
|
||||||
className="w-32 h-32 rounded-lg shadow-lg border border-zinc-200"
|
className="w-32 h-32 rounded-lg shadow-lg ring-1 ring-gray-900/5"
|
||||||
style={{ backgroundColor: color }}
|
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
|
|
||||||
${format === f
|
|
||||||
? 'bg-indigo-500 text-white'
|
|
||||||
: 'bg-zinc-100 text-zinc-700 hover:bg-zinc-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{f.toUpperCase()}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Color Input */}
|
|
||||||
<div className="w-full">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={displayValue}
|
id="hex-value"
|
||||||
onChange={(e) => handleInputChange(e.target.value)}
|
value={colorValues.hex}
|
||||||
className="w-full px-4 py-2 rounded-md border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
onChange={(e) => handleInputChange(e.target.value, 'hex')}
|
||||||
placeholder="Enter color value..."
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user