Add files via upload

This commit is contained in:
Egor
2026-01-15 19:18:17 +03:00
committed by GitHub
parent b118008cdc
commit 7be6b5c0ae
70 changed files with 21835 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
import { useState, useRef, useEffect } from 'react'
interface ColorPickerProps {
value: string
onChange: (color: string) => void
label: string
description?: string
disabled?: boolean
}
const PRESET_COLORS = [
'#3b82f6', // Blue
'#ef4444', // Red
'#22c55e', // Green
'#f59e0b', // Amber
'#8b5cf6', // Violet
'#ec4899', // Pink
'#06b6d4', // Cyan
'#14b8a6', // Teal
'#84cc16', // Lime
'#f97316', // Orange
'#6366f1', // Indigo
'#a855f7', // Purple
]
export function ColorPicker({ value, onChange, label, description, disabled }: ColorPickerProps) {
const [isOpen, setIsOpen] = useState(false)
const [localValue, setLocalValue] = useState(value)
const containerRef = useRef<HTMLDivElement>(null)
const colorInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
setLocalValue(value)
}, [value])
// Close on outside click
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handleColorInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value
setLocalValue(newColor)
onChange(newColor)
}
const handleHexInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let newValue = e.target.value
// Add # if not present
if (newValue && !newValue.startsWith('#')) {
newValue = '#' + newValue
}
// Validate hex format
if (newValue === '' || newValue.match(/^#[0-9A-Fa-f]{0,6}$/)) {
setLocalValue(newValue)
// Only trigger onChange for valid complete hex
if (newValue.match(/^#[0-9A-Fa-f]{6}$/)) {
onChange(newValue)
}
}
}
const handlePresetClick = (color: string) => {
setLocalValue(color)
onChange(color)
setIsOpen(false)
}
return (
<div ref={containerRef} className="relative">
<label className="block text-sm font-medium text-dark-200 mb-1">{label}</label>
{description && <p className="text-xs text-dark-500 mb-2">{description}</p>}
<div className="flex items-center gap-3">
{/* Color preview button */}
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className="w-10 h-10 rounded-lg border-2 border-dark-700 shadow-inner transition-all hover:scale-105 hover:border-dark-600 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
style={{ backgroundColor: localValue || '#000000' }}
title={localValue}
/>
{/* Hex input */}
<input
type="text"
value={localValue}
onChange={handleHexInputChange}
disabled={disabled}
className="input w-28 py-2 font-mono text-sm uppercase"
placeholder="#000000"
maxLength={7}
/>
{/* Hidden native color picker for direct access */}
<input
ref={colorInputRef}
type="color"
value={localValue || '#000000'}
onChange={handleColorInputChange}
disabled={disabled}
className="sr-only"
/>
{/* Native picker button */}
<button
type="button"
onClick={() => colorInputRef.current?.click()}
disabled={disabled}
className="btn-secondary p-2 disabled:opacity-50"
title="Open color picker"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 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 003.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>
</button>
</div>
{/* Dropdown with presets */}
{isOpen && (
<div className="absolute z-50 mt-2 p-3 bg-dark-800 rounded-xl border border-dark-700 shadow-xl animate-fade-in">
<div className="text-xs text-dark-400 mb-2">Preset colors</div>
<div className="grid grid-cols-6 gap-2">
{PRESET_COLORS.map((preset) => (
<button
key={preset}
onClick={() => handlePresetClick(preset)}
className={`w-7 h-7 rounded-lg border-2 transition-all hover:scale-110 ${
localValue === preset ? 'border-white' : 'border-dark-600 hover:border-dark-500'
}`}
style={{ backgroundColor: preset }}
title={preset}
/>
))}
</div>
</div>
)}
</div>
)
}