|
| 1 | +'use client' |
| 2 | + |
| 3 | +import { useEffect, useRef, useState } from 'react' |
| 4 | + |
| 5 | +export interface ColorPickerProps { |
| 6 | + value: string |
| 7 | + onChange: (color: string) => void |
| 8 | + /** Label trigger — es. nome del tag */ |
| 9 | + label?: string |
| 10 | + disabled?: boolean |
| 11 | +} |
| 12 | + |
| 13 | +const PALETTE = [ |
| 14 | + '#00e87a','#4ade80','#22d3ee','#60a5fa','#818cf8','#a78bfa', |
| 15 | + '#f472b6','#fb7185','#f87171','#fb923c','#facc15','#e2e8f0', |
| 16 | +] |
| 17 | + |
| 18 | +function isValidHex(v: string) { return /^#[0-9a-fA-F]{6}$/.test(v) } |
| 19 | + |
| 20 | +function contrastColor(hex: string): string { |
| 21 | + const r = parseInt(hex.slice(1,3),16) |
| 22 | + const g = parseInt(hex.slice(3,5),16) |
| 23 | + const b = parseInt(hex.slice(5,7),16) |
| 24 | + return (r*299 + g*587 + b*114) / 1000 > 128 ? '#000' : '#fff' |
| 25 | +} |
| 26 | + |
| 27 | +export default function ColorPicker({ value, onChange, label, disabled = false }: ColorPickerProps) { |
| 28 | + const [open, setOpen] = useState(false) |
| 29 | + const [hex, setHex] = useState(value) |
| 30 | + const [hexError, setHexError] = useState(false) |
| 31 | + const containerRef = useRef<HTMLDivElement>(null) |
| 32 | + |
| 33 | + // Sincronizza input hex con value esterno |
| 34 | + useEffect(() => { setHex(value) }, [value]) |
| 35 | + |
| 36 | + // Chiudi su click fuori |
| 37 | + useEffect(() => { |
| 38 | + const fn = (e: MouseEvent) => { if (!containerRef.current?.contains(e.target as Node)) setOpen(false) } |
| 39 | + document.addEventListener('mousedown', fn) |
| 40 | + return () => document.removeEventListener('mousedown', fn) |
| 41 | + }, []) |
| 42 | + |
| 43 | + const select = (color: string) => { onChange(color); setHex(color); setHexError(false); setOpen(false) } |
| 44 | + |
| 45 | + const handleHexChange = (v: string) => { |
| 46 | + setHex(v) |
| 47 | + if (isValidHex(v)) { setHexError(false); onChange(v) } |
| 48 | + else setHexError(true) |
| 49 | + } |
| 50 | + |
| 51 | + const handleHexBlur = () => { |
| 52 | + if (!isValidHex(hex)) { setHex(value); setHexError(false) } |
| 53 | + } |
| 54 | + |
| 55 | + const safeColor = isValidHex(value) ? value : '#00e87a' |
| 56 | + |
| 57 | + return ( |
| 58 | + <div ref={containerRef} style={{ position: 'relative', display: 'inline-block' }}> |
| 59 | + {/* Trigger */} |
| 60 | + <button |
| 61 | + onClick={() => !disabled && setOpen(v => !v)} |
| 62 | + disabled={disabled} |
| 63 | + style={{ |
| 64 | + display: 'flex', alignItems: 'center', gap: 8, |
| 65 | + padding: '5px 10px', borderRadius: 8, |
| 66 | + border: `1px solid ${open ? safeColor : 'var(--color-border)'}`, |
| 67 | + background: 'var(--color-panel)', |
| 68 | + cursor: disabled ? 'default' : 'pointer', |
| 69 | + opacity: disabled ? 0.5 : 1, |
| 70 | + transition: 'border-color 0.2s', |
| 71 | + }}> |
| 72 | + {/* Swatch preview */} |
| 73 | + <span style={{ |
| 74 | + display: 'inline-block', width: 16, height: 16, borderRadius: 4, |
| 75 | + background: safeColor, flexShrink: 0, |
| 76 | + boxShadow: `0 0 0 1px rgba(0,0,0,0.2)`, |
| 77 | + }} /> |
| 78 | + {label && <span style={{ fontSize: 11, color: 'var(--color-muted)' }}>{label}</span>} |
| 79 | + <span style={{ fontSize: 10, color: 'var(--color-dim)', fontFamily: 'monospace' }}>{safeColor}</span> |
| 80 | + <span style={{ fontSize: 8, color: 'var(--color-dim)', marginLeft: 2 }}>▾</span> |
| 81 | + </button> |
| 82 | + |
| 83 | + {/* Dropdown */} |
| 84 | + {open && ( |
| 85 | + <div style={{ |
| 86 | + position: 'absolute', top: '100%', left: 0, marginTop: 6, zIndex: 50, |
| 87 | + background: 'var(--color-panel)', border: '1px solid var(--color-border)', |
| 88 | + borderRadius: 10, padding: 12, boxShadow: '0 4px 20px rgba(0,0,0,0.35)', |
| 89 | + minWidth: 188, animation: 'fade-in 0.1s ease both', |
| 90 | + }}> |
| 91 | + {/* Palette */} |
| 92 | + <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6,1fr)', gap: 6, marginBottom: 10 }}> |
| 93 | + {PALETTE.map(color => ( |
| 94 | + <button |
| 95 | + key={color} |
| 96 | + onClick={() => select(color)} |
| 97 | + title={color} |
| 98 | + style={{ |
| 99 | + width: 24, height: 24, borderRadius: 6, border: 'none', |
| 100 | + background: color, cursor: 'pointer', |
| 101 | + outline: value === color ? `2px solid #fff` : 'none', |
| 102 | + outlineOffset: 2, |
| 103 | + boxShadow: value === color ? `0 0 0 3px ${color}55` : '0 1px 3px rgba(0,0,0,0.3)', |
| 104 | + transform: value === color ? 'scale(1.15)' : 'scale(1)', |
| 105 | + transition: 'transform 0.12s, box-shadow 0.12s', |
| 106 | + }} |
| 107 | + /> |
| 108 | + ))} |
| 109 | + </div> |
| 110 | + |
| 111 | + {/* Separatore */} |
| 112 | + <div style={{ height: 1, background: 'var(--color-border)', marginBottom: 10 }} /> |
| 113 | + |
| 114 | + {/* Custom hex */} |
| 115 | + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> |
| 116 | + {/* Mini swatch live */} |
| 117 | + <span style={{ |
| 118 | + width: 20, height: 20, borderRadius: 4, flexShrink: 0, |
| 119 | + background: isValidHex(hex) ? hex : 'var(--color-border)', |
| 120 | + boxShadow: '0 1px 3px rgba(0,0,0,0.3)', |
| 121 | + display: 'inline-block', |
| 122 | + }} /> |
| 123 | + <input |
| 124 | + type="text" |
| 125 | + value={hex} |
| 126 | + onChange={e => handleHexChange(e.target.value)} |
| 127 | + onBlur={handleHexBlur} |
| 128 | + maxLength={7} |
| 129 | + placeholder="#000000" |
| 130 | + style={{ |
| 131 | + flex: 1, fontSize: 11, fontFamily: 'monospace', |
| 132 | + padding: '4px 8px', borderRadius: 6, |
| 133 | + border: `1px solid ${hexError ? 'var(--color-red)' : 'var(--color-border)'}`, |
| 134 | + background: 'var(--color-row)', color: 'var(--color-bright)', |
| 135 | + outline: 'none', |
| 136 | + }} |
| 137 | + /> |
| 138 | + {/* Conferma hex */} |
| 139 | + <button |
| 140 | + onClick={() => { if (isValidHex(hex)) select(hex) }} |
| 141 | + disabled={hexError || !isValidHex(hex)} |
| 142 | + style={{ |
| 143 | + padding: '4px 8px', borderRadius: 6, fontSize: 10, fontWeight: 700, |
| 144 | + border: 'none', cursor: hexError ? 'default' : 'pointer', |
| 145 | + background: !hexError && isValidHex(hex) ? safeColor : 'var(--color-border)', |
| 146 | + color: !hexError && isValidHex(hex) ? contrastColor(safeColor) : 'var(--color-dim)', |
| 147 | + transition: 'background 0.15s', |
| 148 | + }}> |
| 149 | + ✓ |
| 150 | + </button> |
| 151 | + </div> |
| 152 | + </div> |
| 153 | + )} |
| 154 | + </div> |
| 155 | + ) |
| 156 | +} |
0 commit comments