Skip to content

Commit 983c870

Browse files
committed
merge: ColorPicker — palette 12 colori, hex custom con validazione, contrasto auto, close-outside
2 parents 24ece20 + 7300ed4 commit 983c870

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

web/components/ColorPicker.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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

Comments
 (0)