diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index e841b7a..6b90673 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useMemo, useCallback } from 'react' interface ColorPickerProps { value: string @@ -8,6 +8,28 @@ interface ColorPickerProps { disabled?: boolean } +// Check if running in Telegram WebApp (native color picker causes crash) +const isTelegramWebApp = (): boolean => { + return !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp +} + +// Convert hex to RGB +const hexToRgb = (hex: string): { r: number; g: number; b: number } => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : { r: 0, g: 0, b: 0 } +} + +// Convert RGB to hex +const rgbToHex = (r: number, g: number, b: number): string => { + return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('') +} + const PRESET_COLORS = [ '#3b82f6', // Blue '#ef4444', // Red @@ -21,18 +43,36 @@ const PRESET_COLORS = [ '#f97316', // Orange '#6366f1', // Indigo '#a855f7', // Purple + '#ffffff', // White + '#64748b', // Slate + '#1e293b', // Dark + '#000000', // Black ] export function ColorPicker({ value, onChange, label, description, disabled }: ColorPickerProps) { const [isOpen, setIsOpen] = useState(false) const [localValue, setLocalValue] = useState(value) + const [rgb, setRgb] = useState(() => hexToRgb(value)) const containerRef = useRef(null) const colorInputRef = useRef(null) + // Memoize Telegram check to avoid recalculating + const isTelegram = useMemo(() => isTelegramWebApp(), []) + useEffect(() => { setLocalValue(value) + setRgb(hexToRgb(value)) }, [value]) + // Handle RGB slider change + const handleRgbChange = useCallback((channel: 'r' | 'g' | 'b', val: number) => { + const newRgb = { ...rgb, [channel]: val } + setRgb(newRgb) + const hex = rgbToHex(newRgb.r, newRgb.g, newRgb.b) + setLocalValue(hex) + onChange(hex) + }, [rgb, onChange]) + // Close on outside click useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -103,46 +143,107 @@ export function ColorPicker({ value, onChange, label, description, disabled }: C maxLength={7} /> - {/* Hidden native color picker for direct access */} - - - {/* Native picker button - min 44px for touch accessibility */} - + + {/* Native picker button - min 44px for touch accessibility */} + + + )} - {/* Dropdown with presets */} + {/* Dropdown with presets and RGB sliders */} {isOpen && ( -
+
+ {/* RGB Sliders - shown in Telegram instead of native picker */} + {isTelegram && ( +
+
RGB
+
+ {/* Red */} +
+ R + handleRgbChange('r', parseInt(e.target.value))} + className="flex-1 h-2 rounded-full appearance-none cursor-pointer" + style={{ + background: `linear-gradient(to right, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))`, + }} + /> + {rgb.r} +
+ {/* Green */} +
+ G + handleRgbChange('g', parseInt(e.target.value))} + className="flex-1 h-2 rounded-full appearance-none cursor-pointer" + style={{ + background: `linear-gradient(to right, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))`, + }} + /> + {rgb.g} +
+ {/* Blue */} +
+ B + handleRgbChange('b', parseInt(e.target.value))} + className="flex-1 h-2 rounded-full appearance-none cursor-pointer" + style={{ + background: `linear-gradient(to right, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))`, + }} + /> + {rgb.b} +
+
+
+ )} +
Preset colors
-
+
{PRESET_COLORS.map((preset) => (