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">
<head>
<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" />
<title>Vite + React + TS</title>
<title>Color Picker</title>
</head>
<body>
<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 { HexColorPicker } from 'react-colorful'
import { SwatchIcon } from '@heroicons/react/24/outline'
import { SwatchIcon, ClipboardIcon, CheckIcon } from '@heroicons/react/24/outline'
import {
hexToRgb, rgbToHex, rgbToHsl, hslToRgb, rgbToHsb, hsbToRgb,
findClosestTailwindColor, rgbToOklch, oklchToRgb, parseOklch, oklchToString
} from '../utils/colors'
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'
@@ -52,6 +32,7 @@ export function ColorPicker() {
oklch: 'oklch(0.585 0.233 277.117)',
tailwind: 'indigo-500'
})
const [copiedField, setCopiedField] = useState<string | null>(null)
const [closestTailwind, setClosestTailwind] = useState<{ name: string; hex: string; oklch: string; isExactMatch: boolean }>({
name: 'indigo-500',
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 (
<div className="w-full max-w-7xl mx-auto p-6">
<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>
</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 */}
<div className="space-y-6">
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-6">
<div className="h-full">
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-6 h-full">
<div className="space-y-6">
{/* Color Preview */}
<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">
HEX
</label>
<div className="relative">
<input
type="text"
id="hex-value"
value={colorValues.hex}
onChange={(e) => handleInputChange(e.target.value, 'hex')}
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
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>
<label htmlFor="rgb-value" className="block text-sm font-medium text-gray-700 mb-1">
RGB
</label>
<div className="relative">
<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"
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>
<label htmlFor="hsl-value" className="block text-sm font-medium text-gray-700 mb-1">
HSL
</label>
<div className="relative">
<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"
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>
<label htmlFor="hsb-value" className="block text-sm font-medium text-gray-700 mb-1">
HSB
</label>
<div className="relative">
<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"
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>
<label htmlFor="oklch-value" className="block text-sm font-medium text-gray-700 mb-1">
OKLCH
</label>
<div className="relative">
<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"
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>
<label htmlFor="tailwind-value" className="block text-sm font-medium text-gray-700 mb-1">
Tailwind
</label>
<div className="relative">
<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"
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>
@@ -249,7 +277,8 @@ export function ColorPicker() {
</div>
{/* Right Column: Tailwind Colors */}
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-4">
<div className="h-full">
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-900/5 p-4 h-full">
<TailwindColors
onColorSelect={(selectedColor) => {
// Always set the hex color for the color picker
@@ -268,5 +297,6 @@ export function ColorPicker() {
</div>
</div>
</div>
</div>
)
}

View File

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

View File

@@ -54,9 +54,7 @@ button {
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
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
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 }))
? oklchToRgb(parseOklch(color)?.l || 0, parseOklch(color)?.c || 0, parseOklch(color)?.h || 0)
: hexToRgb(color)
if (!rgb1) return { name: 'Invalid color', hex: color, oklch: '', isExactMatch: false }

View File

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