Layout tweaks, copy buttons

This commit is contained in:
2025-02-10 12:27:20 -05:00
parent 7ae2594a66
commit f0757dcbae
8 changed files with 158 additions and 117 deletions

View File

@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Color Picker</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

3
public/favicon.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z" />
</svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,35 +1,15 @@
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 { SwatchIcon, ClipboardIcon, CheckIcon } 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
} from '../utils/colors' } from '../utils/colors'
import { TailwindColors } from './TailwindColors' import { TailwindColors } from './TailwindColors'
type RGB = {
r: number
g: number
b: number
}
type HSL = {
h: number
s: number
l: number
}
type HSB = {
h: number
s: number
b: number
}
type OKLCH = {
l: number
c: number
h: number
}
type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hsb' | 'oklch' | 'tailwind' type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hsb' | 'oklch' | 'tailwind'
@@ -52,6 +32,7 @@ export function ColorPicker() {
oklch: 'oklch(0.585 0.233 277.117)', oklch: 'oklch(0.585 0.233 277.117)',
tailwind: 'indigo-500' tailwind: 'indigo-500'
}) })
const [copiedField, setCopiedField] = useState<string | null>(null)
const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string; isExactMatch: boolean }>({ const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string; isExactMatch: boolean }>({
name: 'indigo-500', name: 'indigo-500',
hex: '#6366F1', hex: '#6366F1',
@@ -128,6 +109,35 @@ export function ColorPicker() {
})) }))
} }
const handleCopyToClipboard = async (value: string, fieldId: string) => {
try {
await navigator.clipboard.writeText(value)
setCopiedField(fieldId)
setTimeout(() => {
setCopiedField(null)
}, 500)
} catch (err) {
console.error('Failed to copy to clipboard:', err)
}
}
const CopyButton = ({ value, fieldId }: { value: string, fieldId: string }) => {
const isCopied = copiedField === fieldId
return (
<button
onClick={() => handleCopyToClipboard(value, fieldId)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-600 hover:text-indigo-600 transition-colors !bg-transparent"
title={isCopied ? "Copied!" : "Copy to clipboard"}
>
{isCopied ? (
<CheckIcon className="h-4 w-4 text-green-600" />
) : (
<ClipboardIcon className="h-4 w-4" />
)}
</button>
)
}
return ( return (
<div className="w-full max-w-7xl mx-auto p-6"> <div className="w-full max-w-7xl mx-auto p-6">
<div className="text-center space-y-2 mb-8"> <div className="text-center space-y-2 mb-8">
@@ -135,10 +145,10 @@ export function ColorPicker() {
<p className="text-lg text-gray-600">Find and convert colors in different formats</p> <p className="text-lg text-gray-600">Find and convert colors in different formats</p>
</div> </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 min-h-[600px]">
{/* Left Column: Color Picker and Input */} {/* Left Column: Color Picker and Input */}
<div className="space-y-6"> <div className="h-full">
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-6"> <div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-6 h-full">
<div className="space-y-6"> <div className="space-y-6">
{/* Color Preview */} {/* Color Preview */}
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
@@ -170,78 +180,96 @@ export function ColorPicker() {
<label htmlFor="hex-value" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="hex-value" className="block text-sm font-medium text-gray-700 mb-1">
HEX HEX
</label> </label>
<input <div className="relative">
type="text" <input
id="hex-value" type="text"
value={colorValues.hex} id="hex-value"
onChange={(e) => handleInputChange(e.target.value, 'hex')} value={colorValues.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" onChange={(e) => handleInputChange(e.target.value, 'hex')}
/> className="block w-full rounded-md border-0 py-1.5 px-3 pr-10 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"
/>
<CopyButton value={colorValues.hex} fieldId="hex" />
</div>
</div> </div>
<div> <div>
<label htmlFor="rgb-value" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="rgb-value" className="block text-sm font-medium text-gray-700 mb-1">
RGB RGB
</label> </label>
<input <div className="relative">
type="text" <input
id="rgb-value" type="text"
value={colorValues.rgb} id="rgb-value"
onChange={(e) => handleInputChange(e.target.value, 'rgb')} value={colorValues.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" onChange={(e) => handleInputChange(e.target.value, 'rgb')}
/> className="block w-full rounded-md border-0 py-1.5 px-3 pr-10 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"
/>
<CopyButton value={colorValues.rgb} fieldId="rgb" />
</div>
</div> </div>
<div> <div>
<label htmlFor="hsl-value" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="hsl-value" className="block text-sm font-medium text-gray-700 mb-1">
HSL HSL
</label> </label>
<input <div className="relative">
type="text" <input
id="hsl-value" type="text"
value={colorValues.hsl} id="hsl-value"
onChange={(e) => handleInputChange(e.target.value, 'hsl')} value={colorValues.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" onChange={(e) => handleInputChange(e.target.value, 'hsl')}
/> className="block w-full rounded-md border-0 py-1.5 px-3 pr-10 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"
/>
<CopyButton value={colorValues.hsl} fieldId="hsl" />
</div>
</div> </div>
<div> <div>
<label htmlFor="hsb-value" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="hsb-value" className="block text-sm font-medium text-gray-700 mb-1">
HSB HSB
</label> </label>
<input <div className="relative">
type="text" <input
id="hsb-value" type="text"
value={colorValues.hsb} id="hsb-value"
onChange={(e) => handleInputChange(e.target.value, 'hsb')} value={colorValues.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" onChange={(e) => handleInputChange(e.target.value, 'hsb')}
/> className="block w-full rounded-md border-0 py-1.5 px-3 pr-10 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"
/>
<CopyButton value={colorValues.hsb} fieldId="hsb" />
</div>
</div> </div>
<div> <div>
<label htmlFor="oklch-value" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="oklch-value" className="block text-sm font-medium text-gray-700 mb-1">
OKLCH OKLCH
</label> </label>
<input <div className="relative">
type="text" <input
id="oklch-value" type="text"
value={colorValues.oklch} id="oklch-value"
onChange={(e) => handleInputChange(e.target.value, 'oklch')} value={colorValues.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" onChange={(e) => handleInputChange(e.target.value, 'oklch')}
/> className="block w-full rounded-md border-0 py-1.5 px-3 pr-10 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"
/>
<CopyButton value={colorValues.oklch} fieldId="oklch" />
</div>
</div> </div>
<div> <div>
<label htmlFor="tailwind-value" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="tailwind-value" className="block text-sm font-medium text-gray-700 mb-1">
Tailwind Tailwind
</label> </label>
<input <div className="relative">
type="text" <input
id="tailwind-value" type="text"
value={colorValues.tailwind} id="tailwind-value"
readOnly value={colorValues.tailwind}
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" readOnly
/> className="block w-full rounded-md border-0 py-1.5 px-3 pr-10 text-gray-900 bg-gray-50 shadow-sm ring-1 ring-inset ring-gray-300 sm:text-sm sm:leading-6"
/>
<CopyButton value={colorValues.tailwind} fieldId="tailwind" />
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -249,22 +277,24 @@ export function ColorPicker() {
</div> </div>
{/* Right Column: Tailwind Colors */} {/* Right Column: Tailwind Colors */}
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-4"> <div className="h-full">
<TailwindColors <div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-4 h-full">
onColorSelect={(selectedColor) => { <TailwindColors
// Always set the hex color for the color picker onColorSelect={(selectedColor) => {
if (selectedColor.startsWith('oklch')) { // Always set the hex color for the color picker
const oklch = parseOklch(selectedColor) if (selectedColor.startsWith('oklch')) {
if (oklch) { const oklch = parseOklch(selectedColor)
const rgb = oklchToRgb(oklch.l, oklch.c, oklch.h) if (oklch) {
setColor(rgbToHex(rgb.r, rgb.g, rgb.b)) const rgb = oklchToRgb(oklch.l, oklch.c, oklch.h)
setColor(rgbToHex(rgb.r, rgb.g, rgb.b))
}
} else {
setColor(selectedColor)
} }
} else { }}
setColor(selectedColor) selectedColor={color}
} />
}} </div>
selectedColor={color}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -46,36 +46,41 @@ export function TailwindColors({ onColorSelect, selectedColor }: TailwindColorsP
} }
return ( return (
<div className="w-full overflow-x-auto"> <div className="h-full flex flex-col">
<div className="grid grid-cols-[auto_repeat(11,minmax(32px,1fr))] gap-1.5"> <div className="text-sm font-medium text-gray-500 mb-2 px-2">
{/* Color rows */} Tailwind Color Reference
{orderedColors.map(colorName => ( </div>
<React.Fragment key={colorName}> <div className="flex-1 overflow-x-auto overflow-y-auto">
<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"> <div className="grid grid-cols-[auto_repeat(11,minmax(32px,1fr))] gap-1.5 p-2">
{colorName} {/* Color rows */}
</div> {orderedColors.map(colorName => (
{shades.map(shade => { <React.Fragment key={colorName}>
const values = TAILWIND_COLORS[colorName][shade] <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">
const hexColor = typeof values === 'string' ? values : values.hex {colorName}
const textColorClass = getTextColor(hexColor) </div>
{shades.map(shade => {
const values = TAILWIND_COLORS[colorName][shade]
const hexColor = typeof values === 'string' ? values : values.hex
const textColorClass = getTextColor(hexColor)
return ( return (
<button <button
key={`${colorName}-${shade}`} key={`${colorName}-${shade}`}
onClick={() => onColorSelect(hexColor)} 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 className={`aspect-square transition-all hover:scale-110 hover:z-10 hover:shadow-lg 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'}`} ${selectedColor === hexColor ? 'ring-2 ring-offset-0 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}: ${hexColor}`} title={`${colorName}-${shade}`}
> >
<span className={`absolute inset-0 flex items-center justify-center text-xs font-medium ${textColorClass}`}> <span className={`absolute inset-0 flex items-center justify-center text-xs font-medium ${textColorClass}`}>
{shade} {shade}
</span> </span>
</button> </button>
) )
})} })}
</React.Fragment> </React.Fragment>
))} ))}
</div>
</div> </div>
</div> </div>
) )

View File

@@ -54,9 +54,7 @@ button {
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
button:hover {
border-color: #646cff;
}
button:focus, button:focus,
button:focus-visible { button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; outline: 4px auto -webkit-focus-ring-color;

View File

@@ -261,7 +261,7 @@ 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; isExactMatch: boolean } { 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(parseOklch(color)?.l || 0, parseOklch(color)?.c || 0, parseOklch(color)?.h || 0)
: hexToRgb(color) : hexToRgb(color)
if (!rgb1) return { name: 'Invalid color', hex: color, oklch: '', isExactMatch: false } if (!rgb1) return { name: 'Invalid color', hex: color, oklch: '', isExactMatch: false }

View File

@@ -11,4 +11,10 @@ export default defineConfig({
server: { server: {
port: 5555, port: 5555,
}, },
base: '/color-picker/',
build: {
outDir: 'dist',
assetsDir: 'assets',
emptyOutDir: true,
},
}) })