Live Preview

Export

Demo Code (JSX)
1"use client";
2
3import React, { useState, useCallback } from "react";
4import ColorPicker from "./ColorPicker";
5
6export default function MyColorPicker() {
7  const [color, setColor] = useState("#06B5EF");
8
9  const handleColorChange = useCallback((newColor: string) => {
10    setColor(newColor);
11  }, []);
12
13  return (
14    <div style={{ maxWidth: "364px", width: "100%" }}>
15      <ColorPicker
16        value={color}
17        onValueChange={handleColorChange}
18      />
19    </div>
20  );
21}
Component Source
1"use client";
2    
3import React, {
4  useState,
5  useEffect,
6  useRef,
7  useCallback,
8  useMemo,
9} from "react";
10import {
11  hexToHsva,
12  hsvaToHex,
13  hsvaToHsla,
14  hsvaToRgba,
15  rgbaToHsva,
16  hslaToHsva,
17  type HsvaColor,
18} from "@uiw/color-convert";
19import { Check, ChevronDown, CheckCircle2, XCircle } from "lucide-react";
20import { cn } from "@/lib/utils";
21
22// --- Utility: Color Math for Contrast ---
23
24const parseToHsva = (color: string): HsvaColor => {
25  const str = color.toLowerCase().trim();
26
27  try {
28    if (str.startsWith("#")) return hexToHsva(str);
29
30    if (str.startsWith("rgb")) {
31      const p = str.match(/[d.]+/g);
32      if (p)
33        return rgbaToHsva({
34          r: +p[0],
35          g: +p[1],
36          b: +p[2],
37          a: p[3] ? +p[3] : 1,
38        });
39    }
40
41    if (str.startsWith("hsl")) {
42      const p = str.match(/[d.]+/g);
43      if (p)
44        return hslaToHsva({
45          h: +p[0],
46          s: +p[1],
47          l: +p[2],
48          a: p[3] ? +p[3] : 1,
49        });
50    }
51  } catch (e) {}
52
53  return { h: 0, s: 0, v: 0, a: 1 };
54};
55
56const getRelativeLuminance = (color: string) => {
57  const hsva = parseToHsva(color);
58  const { r, g, b } = hsvaToRgba(hsva);
59  const [rs, gs, bs] = [r, g, b].map((c) => {
60    const s = c / 255;
61    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
62  });
63  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
64};
65
66const getContrastRatio = (lum1: number, lum2: number) => {
67  const brightest = Math.max(lum1, lum2);
68  const darkest = Math.min(lum1, lum2);
69  return (brightest + 0.05) / (darkest + 0.05);
70};
71
72// --- Component Props ---
73
74export interface ColorPickerProps {
75  value?: string;
76  onValueChange?: (val: string) => void;
77  isRTL?: boolean;
78  showContrast?: boolean;
79
80  // NEW: channel fields & modes
81  enabledModes?: Array<"hex" | "rgb" | "hsl">;
82  defaultFormat?: "hex" | "rgb" | "hsl";
83  
84  // Mode selector labels (full names)
85  hexLabel?: string;
86  rgbLabel?: string;
87  hslLabel?: string;
88  modeLabel?: string;
89  
90  // Channel input labels (single letters)
91  rLabel?: string;
92  gLabel?: string;
93  bLabel?: string;
94  hLabel?: string;
95  sLabel?: string;
96  lLabel?: string;
97  
98  contrastBgLuminance?: number;
99  colorPreviewAreaText?: string;
100
101  // Container styling
102  containerBg?: string;
103  containerBorderColor?: string;
104  containerBorderWidth?: number;
105  containerRadius?: string;
106  containerPadding?: string;
107  containerElementGap?: string;
108
109  // Saturation area
110  saturationHeight?: number;
111  saturationRadius?: number;
112  saturationBorderColor?: string;
113  saturationBorderWidth?: number;
114
115  // Saturation THUMB (draggable circle inside the saturation area)
116  saturationThumbWidth?: number | string;
117  saturationThumbHeight?: number | string;
118  saturationThumbRadius?: number | string;
119  saturationThumbBorderStyle?: "solid" | "dashed" | "none";
120  saturationThumbBorderWidth?: number;
121  saturationThumbBorderColor?: string;
122  saturationThumbBgColor?: string;
123
124  // Hue TRACK (static colorful background)
125  hueTrackHeight?: number | string;
126  hueTrackRadius?: number | string;
127  hueTrackBorderWidth?: number;
128  hueTrackBorderColor?: string;
129
130  // Hue THUMB (draggable slider circle) - base
131  hueThumbSize?: number | string;
132  hueThumbRadius?: number | string;
133  hueThumbBorderWidth?: number;
134  
135  // Hue THUMB states (hover/active colors)
136  hueThumbBgDefault?: string;
137  hueThumbBgHover?: string;
138  hueThumbBgActive?: string;
139  hueThumbBorderDefault?: string;
140  hueThumbBorderHover?: string;
141  hueThumbBorderActive?: string;
142
143  // Contrast stuff
144  contrastLabel?: string;
145  contrastLabelSize?: string;
146  contrastLabelColor?: string;
147  contrastLabelWeight?: number;
148  contrastValueSize?: string;
149  contrastValueColor?: string;
150  contrastValueWeight?: number;
151  contrastFormat?: "value:1" | "1:value" | "value";
152  contrastLabelGap?: string;
153  contrastItemGap?: string;
154  contrastBadgeGap?: string;
155  showContrastAALabel?: boolean;
156  showContrastAAALabel?: boolean;
157  contrastAreaTopMargin?: string;
158
159  // Input / display field (HEX only)
160  inputHeight?: number;
161  inputBg?: string;
162  inputBorderColor?: string;
163  inputBorderWidth?: number;
164  inputRadius?: number;
165  inputTextColor?: string;
166  floatingLabelFocusBorderColor?: string;
167  inputFocusRingColor?: string;
168  inputErrorOutlineColor?: string;
169
170  // Floating label styling
171  floatingLabelBg?: string;
172  floatingLabelTextColor?: string;
173  floatingLabelActiveTextColor?: string;
174  floatingLabelRadius?: number;
175  floatingLabelBorderColor?: string;
176  floatingLabelBorderWidth?: number;
177  floatingLabelMainTextSize?: number;
178
179  // Dropdown / combobox button
180  dropdownHeight?: number;
181  dropdownBg?: string;
182  dropdownBorderColor?: string;
183  dropdownBorderWidth?: number;
184  dropdownRadius?: number;
185  dropdownTextColor?: string;
186  dropdownFocusBorderColor?: string;
187  dropdownFocusRingColor?: string;
188  dropdownChevronColor?: string;
189
190  // Dropdown menu
191  dropdownMenuBg?: string;
192  dropdownMenuBorderColor?: string;
193  dropdownMenuBorderWidth?: number;
194  dropdownMenuRadius?: number;
195  dropdownMenuTextColor?: string;
196  dropdownMenuActiveTextColor?: string;
197  dropdownMenuHoverBg?: string;
198  dropdownMenuActiveBg?: string;
199  modeDropdownWidth?: string;
200  modeDropdownFullWidth?: boolean;
201
202  // Preview / letter area
203  showLetterTop?: boolean;
204  previewBgFallback?: string;
205  previewBorderColor?: string;
206  previewBorderWidth?: number;
207  previewRadius?: number;
208  previewFontSize?: number;
209  previewFontWeight?: number;
210  previewTextColor?: string;
211  colorPreviewPosition?: "top" | "contrast" | "none"
212  previewWidth?: number | string;
213  previewHeight?: number;
214
215  // Badge group customization
216  badgeBorderWidth?: number | string;
217  badgeBorderRadius?: number | string;
218  badgeFontSize?: number | string;
219  badgeFontWeight?: number;
220  badgeIconSize?: number | string;
221  badgePadding?: string;
222  badgeIconStrokeWidth?: number;
223  badgeBgPass?: string;
224  badgeBgFail?: string;
225  badgeBorderPass?: string;
226  badgeBorderFail?: string;
227  badgeTextPass?: string;
228  badgeTextFail?: string;
229}
230
231// --- Main Component ---
232
233export default function ColorPicker({
234  value = "#06B5EF",
235  onValueChange,
236  isRTL = false,
237  showContrast = true,
238  colorPreviewAreaText = "A",
239  enabledModes = ["hex", "rgb", "hsl"],
240  defaultFormat = "hex",
241  contrastBgLuminance = 0,
242  hexLabel = "HEX",
243  rgbLabel = "RGB", 
244  hslLabel = "HSL",
245  modeLabel = "Mode",
246  contrastLabel = "Contrast",
247  rLabel = "R",
248  gLabel = "G", 
249  bLabel = "B",
250  hLabel = "H",
251  sLabel = "S",
252  lLabel = "L",
253
254  // Container
255  containerBg = "#000",
256  containerBorderColor = "#242424",
257  containerBorderWidth = 1,
258  containerRadius = "12px",
259  containerPadding = "16px",
260  containerElementGap = "16px",
261
262  // Saturation
263  saturationHeight = 140,
264  saturationRadius = 8,
265  saturationBorderColor = "#242424",
266  saturationBorderWidth = 1,
267
268  // Saturation thumb (indicator)
269  saturationThumbWidth = 14,
270  saturationThumbHeight = 14,
271  saturationThumbRadius = 50,
272  saturationThumbBorderStyle = "solid",
273  saturationThumbBorderWidth = 2,
274  saturationThumbBorderColor = "#ffffff",
275  saturationThumbBgColor = "transparent",
276
277  // NEW: Hue TRACK (static colorful background)
278  hueTrackHeight = 10,
279  hueTrackRadius = "8px",
280  hueTrackBorderWidth = 1,
281  hueTrackBorderColor = "transparent",
282
283  // NEW: Hue THUMB (draggable slider circle) - base
284  hueThumbSize = 16,
285  hueThumbRadius = "50%",
286  hueThumbBorderWidth = 3,
287
288  // NEW: Hue THUMB states (hover/active colors)
289  hueThumbBgDefault = "#f0f0f0",
290  hueThumbBgHover = "#e5e5e5",
291  hueThumbBgActive = "#f0f0f0",
292  hueThumbBorderDefault = "#e5e5e5",
293  hueThumbBorderHover = "#f0f0f0",
294  hueThumbBorderActive = "#fff",
295
296  // NEW: Badge group (AA/AAA contrast badges)
297  badgeBorderWidth = 1,
298  badgeBorderRadius = "50px",
299  badgeFontSize = "10px",
300  badgeFontWeight = 600,
301  badgeIconSize = 10,
302  badgePadding = "0.25rem 0.5rem",
303  badgeIconStrokeWidth = 2.25,
304  badgeBgPass = "rgba(65, 239, 6, 0.1)",
305  badgeBgFail = "rgba(239, 6, 65, 0.1)",
306  badgeBorderPass = "rgba(65, 239, 6, 0.5)",
307  badgeBorderFail = "rgba(239, 6, 65, 0.5)",
308  badgeTextPass = "#41EF06",
309  badgeTextFail = "#EF0641",
310
311  // Contrast
312  contrastLabelSize = "12px",
313  contrastLabelColor = "#737373",
314  contrastLabelWeight = 700,
315  contrastValueSize = "14px",
316  contrastValueColor = "#ffffff",
317  contrastValueWeight = 400,
318  contrastFormat = "value:1",
319  contrastLabelGap = "0.125rem",
320  contrastItemGap = "16px",
321  contrastBadgeGap = "8px",
322  showContrastAALabel = true,
323  showContrastAAALabel = true,
324  contrastAreaTopMargin = "0px",
325
326  // Inputs
327  inputHeight = 44,
328  inputBg = "#000",
329  inputBorderColor = "#242424",
330  inputBorderWidth = 1,
331  inputRadius = 8,
332  inputTextColor = "#ffffff",
333  floatingLabelFocusBorderColor = "#06B5EF",
334  inputErrorOutlineColor = "#EF0641",
335
336  // Floating labels
337  floatingLabelBg,
338  floatingLabelTextColor = "#777777",
339  floatingLabelActiveTextColor = "#ffffff",
340  floatingLabelRadius = 4,
341  floatingLabelBorderColor,
342  floatingLabelBorderWidth = 0,
343  floatingLabelMainTextSize = 14,
344
345  // Dropdown
346  dropdownHeight = 44,
347  dropdownBg,
348  dropdownBorderColor,
349  dropdownBorderWidth,
350  dropdownRadius,
351  dropdownTextColor,
352  dropdownFocusBorderColor,
353  dropdownChevronColor = "#6b7280",
354
355  // Dropdown menu
356  dropdownMenuBg = "#111111",
357  dropdownMenuBorderColor = "#242424",
358  dropdownMenuBorderWidth = 1,
359  dropdownMenuRadius = 10,
360  dropdownMenuTextColor = "#d1d5db",
361  dropdownMenuActiveTextColor = "#ffffff",
362  dropdownMenuHoverBg = "rgba(255,255,255,0.05)",
363  dropdownMenuActiveBg = "rgba(255,255,255,0.10)",
364  modeDropdownWidth = "128px",
365  modeDropdownFullWidth = false,
366
367  // Preview area
368  previewBgFallback = "#111111",
369  previewBorderColor = "rgba(255,255,255,0.14)",
370  previewBorderWidth = 1,
371  previewRadius = 8,
372  previewFontSize = 18,
373  previewFontWeight = 600,
374  previewTextColor = "#ffffff",
375
376  colorPreviewPosition = "contrast",
377  previewWidth = 44,
378  previewHeight = inputHeight, 
379}: ColorPickerProps) {
380  // Normalize modes
381  const normalizedModes = enabledModes.length ? enabledModes : ["hex"];
382  const initialFormat = normalizedModes.includes(defaultFormat) ? defaultFormat : normalizedModes[0];
383  
384  const [hsva, setHsva] = useState<HsvaColor>(parseToHsva(value));
385  const [format, setFormat] = useState<"hex" | "rgb" | "hsl">(initialFormat);
386  
387  // ✅ FIXED: Track source of change + hex completely independent
388  const [hexInput, setHexInput] = useState("");
389  const [hexIsFocused, setHexIsFocused] = useState(false);
390  const [hexError, setHexError] = useState(false);
391  const [changeSource, setChangeSource] = useState<'hex' | 'external' | null>(null);
392  
393  const [rgbInputs, setRgbInputs] = useState({ r: "0", g: "0", b: "0" });
394  const [rgbIsFocused, setRgbIsFocused] = useState<string | null>(null);
395  const [rgbErrors, setRgbErrors] = useState({ r: false, g: false, b: false });
396  
397  const [hslInputs, setHslInputs] = useState({ h: "0", s: "0", l: "0" });
398  const [hslIsFocused, setHslIsFocused] = useState<string | null>(null);
399  const [hslErrors, setHslErrors] = useState({ h: false, s: false, l: false });
400  
401  const satContainerRef = useRef<HTMLDivElement>(null);
402
403  // Computed defaults
404  const resolvedDropdownBg = dropdownBg ?? inputBg;
405  const resolvedDropdownBorderColor = dropdownBorderColor ?? inputBorderColor;
406  const resolvedDropdownBorderWidth = dropdownBorderWidth ?? inputBorderWidth;
407  const resolvedDropdownRadius = dropdownRadius ?? inputRadius;
408  const resolvedDropdownTextColor = dropdownTextColor ?? inputTextColor;
409  const resolvedDropdownFocusBorderColor = dropdownFocusBorderColor ?? floatingLabelFocusBorderColor;
410  const resolvedFloatingLabelBg = floatingLabelBg ?? containerBg;
411
412  // ✅ FIXED: Manual hex sync ONLY for external changes
413  const updateHexFromColor = useCallback(() => {
414    if (changeSource !== 'hex' && !hexIsFocused) {
415      const newHex = hsvaToHex(hsva).toUpperCase();
416      setHexInput(newHex);
417      setHexError(false);
418    }
419  }, [hsva, hexIsFocused, changeSource]);
420
421  // External prop sync
422  useEffect(() => {
423    const next = parseToHsva(value);
424    setHsva(next);
425    setChangeSource('external');
426    updateHexFromColor();
427  }, [value]);
428
429  // RGB/HSL sync ONLY
430  useEffect(() => {
431    if (!rgbIsFocused) {
432      const { r, g, b } = hsvaToRgba(hsva);
433      setRgbInputs({
434        r: Math.round(r).toString(),
435        g: Math.round(g).toString(),
436        b: Math.round(b).toString()
437      });
438      setRgbErrors({ r: false, g: false, b: false });
439    }
440    if (!hslIsFocused) {
441      const { h, s, l } = hsvaToHsla(hsva);
442      setHslInputs({
443        h: Math.round(h).toString(),
444        s: Math.round(s).toString(),
445        l: Math.round(l).toString()
446      });
447      setHslErrors({ h: false, s: false, l: false });
448    }
449  }, [hsva, format, rgbIsFocused, hslIsFocused]);
450
451  const clamp = (val: number, min: number, max: number) =>
452    Number.isNaN(val) ? min : Math.min(max, Math.max(min, val));
453
454  const parseNumericChannel = (raw: string, min: number, max: number) => {
455    const trimmed = raw.trim();
456    if (!trimmed.length) return { value: 0, isValid: false };
457    const n = Number(trimmed);
458    if (Number.isNaN(n)) return { value: 0, isValid: false };
459    return { value: clamp(n, min, max), isValid: true };
460  };
461
462  const triggerChange = useCallback((next: HsvaColor, source: 'hex' | 'external' = 'external') => {
463    setChangeSource(source);
464    setHsva(next);
465    let out = "";
466    if (format === "hex") out = hsvaToHex(next);
467    else if (format === "rgb") {
468      const { r, g, b } = hsvaToRgba(next);
469      out = `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
470    } else {
471      const { h, s, l } = hsvaToHsla(next);
472      out = `hsl(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%)`;
473    }
474    onValueChange?.(out);
475  }, [format, onValueChange]);
476
477  // ✅ FIXED HEX - TRACKS SOURCE + NEVER AUTO-CORRECTS
478  const handleHexChange = (val: string) => {
479    if (val.length > 7) return;
480    setHexInput(val); // ALWAYS free - NO corrections ever
481    
482    const trimmed = val.trim();
483    if (!trimmed) {
484      setHexError(false);
485      return;
486    }
487    
488    const parsed = parseToHsva(trimmed);
489    const isFallbackZero = parsed.h === 0 && parsed.s === 0 && parsed.v === 0;
490    setHexError(!!trimmed && isFallbackZero && trimmed !== "#000");
491    
492    triggerChange(parsed, 'hex'); // Mark as hex source - NO input correction
493  };
494
495  const handleHexFocus = () => setHexIsFocused(true);
496  const handleHexBlur = () => setHexIsFocused(false);
497
498  // RGB handlers (mark source)
499  const handleRgbChannelChange = (channel: "r"|"g"|"b", raw: string) => {
500    if (raw.length > 3) return;
501    const newInputs = { ...rgbInputs, [channel]: raw };
502    setRgbInputs(newInputs);
503    const ranges = { r: [0,255], g: [0,255], b: [0,255] } as const;
504    const parsed = parseNumericChannel(raw, ranges[channel][0], ranges[channel][1]);
505    setRgbErrors(prev => ({ ...prev, [channel]: !parsed.isValid }));
506    const allValid = ["r","g","b"].every(c => {
507      const [min, max] = ranges[c];
508      return parseNumericChannel(newInputs[c], min, max).isValid;
509    });
510    if (allValid) {
511      const r = parseNumericChannel(newInputs.r, 0, 255).value;
512      const g = parseNumericChannel(newInputs.g, 0, 255).value;
513      const b = parseNumericChannel(newInputs.b, 0, 255).value;
514      triggerChange(rgbaToHsva({ r, g, b, a: 1 }), 'external');
515    }
516  };
517
518  const handleRgbFocus = (channel: "r"|"g"|"b") => setRgbIsFocused(channel);
519  const handleRgbBlur = () => setRgbIsFocused(null);
520
521  // HSL handlers (mark source)
522  const handleHslChannelChange = (channel: "h"|"s"|"l", raw: string) => {
523    if (raw.length > 3) return;
524    const newInputs = { ...hslInputs, [channel]: raw };
525    setHslInputs(newInputs);
526    const ranges = { h: [0,360], s: [0,100], l: [0,100] } as const;
527    const parsed = parseNumericChannel(raw, ranges[channel][0], ranges[channel][1]);
528    setHslErrors(prev => ({ ...prev, [channel]: !parsed.isValid }));
529    const allValid = ["h","s","l"].every(c => {
530      const [min, max] = ranges[c];
531      return parseNumericChannel(newInputs[c], min, max).isValid;
532    });
533    if (allValid) {
534      const h = parseNumericChannel(newInputs.h, 0, 360).value;
535      const s = parseNumericChannel(newInputs.s, 0, 100).value;
536      const l = parseNumericChannel(newInputs.l, 0, 100).value;
537      triggerChange(hslaToHsva({ h, s, l, a: 1 }), 'external');
538    }
539  };
540
541  const handleHslFocus = (channel: "h"|"s"|"l") => setHslIsFocused(channel);
542  const handleHslBlur = () => setHslIsFocused(null);
543
544  // ✅ FIXED Saturation - external source
545  const handleSatMouseDown = (e: React.MouseEvent) => {
546    e.preventDefault();
547    const handleMove = (moveEvent: MouseEvent) => {
548      if (!satContainerRef.current) return;
549      const rect = satContainerRef.current.getBoundingClientRect();
550      const x = Math.max(0, Math.min((moveEvent.clientX - rect.left) / rect.width, 1));
551      const y = Math.max(0, Math.min(1 - (moveEvent.clientY - rect.top) / rect.height, 1));
552      const newSat = isRTL ? (1 - x) * 100 : x * 100;
553      const newBright = y * 100;
554      triggerChange({ ...hsva, s: newSat, v: newBright }, 'external');
555      updateHexFromColor();
556    };
557    const handleUp = () => {
558      window.removeEventListener("mousemove", handleMove);
559      window.removeEventListener("mouseup", handleUp);
560    };
561    window.addEventListener("mousemove", handleMove);
562    window.addEventListener("mouseup", handleUp);
563    handleMove(e.nativeEvent as unknown as MouseEvent);
564  };
565
566  const formatContrast = (ratio: number) => {
567    switch (contrastFormat) {
568      case "1:value": return `1:${ratio.toFixed(2)}`;
569      case "value": return ratio.toFixed(2);
570      default: return `${ratio.toFixed(2)}:1`;
571    }
572  };
573
574  const contrastRatio = useMemo(() => {
575    const lum = getRelativeLuminance(hsvaToHex(hsva));
576    return getContrastRatio(lum, contrastBgLuminance);
577  }, [hsva, contrastBgLuminance]);
578
579  const autoPreviewTextColor = hsva.v > 60 && hsva.s < 30 ? "#000000" : "#ffffff";
580  const showModeDropdown = normalizedModes.length > 1;
581
582  // ✅ FIXED: Add autocomplete="off" to prevent browser interference
583  return (
584    <div className={cn("flex flex-col select-none", isRTL && "rtl")} style={{
585      backgroundColor: containerBg, borderStyle: "solid", borderColor: containerBorderColor,
586      borderWidth: containerBorderWidth, borderRadius: containerRadius, padding: containerPadding,
587      gap: containerElementGap, direction: isRTL ? "rtl" : "ltr"
588    }}>
589      {/* Saturation Area */}
590      <div ref={satContainerRef} onMouseDown={handleSatMouseDown} className="relative w-full cursor-crosshair" style={{
591        height: saturationHeight, borderRadius: saturationRadius, borderStyle: "solid",
592        borderColor: saturationBorderColor, borderWidth: saturationBorderWidth, overflow: "hidden",
593        background: `linear-gradient(to top, #000000, transparent), ${isRTL ? "linear-gradient(to left, #ffffff, transparent)" : "linear-gradient(to right, #ffffff, transparent)"}, hsl(${hsva.h}, 100%, 50%)`,
594        backgroundOrigin: "border-box", backgroundClip: "border-box"
595      }}>
596        <div
597          className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none"
598          style={{
599            left: isRTL ? `${100 - hsva.s}%` : `${hsva.s}%`,
600            top: `${100 - hsva.v}%`,
601            width: saturationThumbWidth,
602            height: saturationThumbHeight,
603            borderRadius: saturationThumbRadius,
604            borderStyle: saturationThumbBorderStyle,
605            borderWidth: saturationThumbBorderWidth,
606            backgroundColor: saturationThumbBgColor,
607            borderColor: saturationThumbBorderColor,
608          }}
609        />
610      </div>
611
612      {/* Hue Slider - Custom thumb states + keyboard */}
613      <CustomSlider
614        id="hue"
615        min={0}
616        max={360}
617        step={0.1}
618        value={hsva.h}
619        onValueChange={(newHue) => {
620          triggerChange({ ...hsva, h: newHue }, 'external');
621          updateHexFromColor();
622        }}
623        trackHeight={typeof hueTrackHeight === 'number' ? `${hueTrackHeight}px` : hueTrackHeight}
624        thumbWidth={typeof hueThumbSize === 'number' ? `${hueThumbSize}px` : hueThumbSize}
625        thumbHeight={typeof hueThumbSize === 'number' ? `${hueThumbSize}px` : hueThumbSize}
626        thumbBorderWidth={`${hueThumbBorderWidth}px`}
627        thumbBorderRadius={typeof hueThumbRadius === 'number' ? `${hueThumbRadius}px` : hueThumbRadius}
628        // ✅ FIXED: All 3 static track props
629        trackBorderRadius={typeof hueTrackRadius === 'number' ? `${hueTrackRadius}px` : hueTrackRadius}
630        trackBorderWidth={`${hueTrackBorderWidth}px`}
631        trackBorderColor={hueTrackBorderColor}
632        trackFillBorderRadius={typeof hueTrackRadius === 'number' ? `${hueTrackRadius}px` : hueTrackRadius}
633        colorTrackBackground={isRTL 
634          ? "linear-gradient(to left, red, yellow, lime, cyan, blue, magenta, red)" 
635          : "linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red)"
636        }
637        colorFillDefault="transparent"
638        colorFillHover="transparent"
639        colorFillActive="transparent"
640        colorThumbDefault={hueThumbBgDefault}
641        colorThumbHover={hueThumbBgHover}
642        colorThumbActive={hueThumbBgActive}
643        colorThumbBorderDefault={hueThumbBorderDefault}
644        colorThumbBorderHover={hueThumbBorderHover}
645        colorThumbBorderActive={hueThumbBorderActive}
646        isRTL={isRTL}
647        keyStep={1}
648      />
649
650      {/* Controls Row */}
651      <div className={cn("flex gap-2", modeDropdownFullWidth ? "flex-col" : "items-start")}>
652        <div className={cn("flex w-full gap-2", modeDropdownFullWidth ? "flex-col" : "items-start")}>
653          {colorPreviewPosition === "top" && (
654            <ColorPreviewBox
655              bg={hsvaToHex(hsva) || previewBgFallback}
656              label={colorPreviewAreaText}
657              width={previewWidth}
658              height={previewHeight}
659              radius={previewRadius}
660              borderColor={previewBorderColor}
661              borderWidth={previewBorderWidth}
662              fontSize={previewFontSize}
663              fontWeight={previewFontWeight}
664              fontColor={previewTextColor ?? autoPreviewTextColor}
665            />
666          )}
667
668          {showModeDropdown && (
669            <div style={{width: modeDropdownFullWidth ? "100%" : modeDropdownWidth, flexShrink: 0}}>
670              <FloatingLabelSelect
671                value={format}
672                onChange={(f) => {
673                  setFormat(f as "hex" | "rgb" | "hsl");
674                  if (f === "hex") {
675                    setChangeSource('external');
676                    updateHexFromColor();
677                  }
678                }}
679                label={modeLabel}
680                options={normalizedModes}
681                isRTL={isRTL}
682                t={(key) => key === "hex" ? hexLabel : key === "rgb" ? rgbLabel : hslLabel}
683                height={dropdownHeight}
684                bg={resolvedDropdownBg}
685                borderColor={resolvedDropdownBorderColor}
686                borderWidth={resolvedDropdownBorderWidth}
687                radius={resolvedDropdownRadius}
688                fontSize={floatingLabelMainTextSize}
689                textColor={resolvedDropdownTextColor}
690                focusBorderColor={resolvedDropdownFocusBorderColor}
691                chevronColor={dropdownChevronColor}
692                floatingLabelBg={resolvedFloatingLabelBg}
693                floatingLabelTextColor={floatingLabelTextColor}
694                floatingLabelActiveTextColor={floatingLabelActiveTextColor}
695                floatingLabelRadius={floatingLabelRadius}
696                floatingLabelBorderColor={floatingLabelBorderColor}
697                floatingLabelBorderWidth={floatingLabelBorderWidth}
698                menuBg={dropdownMenuBg}
699                menuBorderColor={dropdownMenuBorderColor}
700                menuBorderWidth={dropdownMenuBorderWidth}
701                menuRadius={dropdownMenuRadius}
702                menuTextColor={dropdownMenuTextColor}
703                menuActiveTextColor={dropdownMenuActiveTextColor}
704                menuHoverBg={dropdownMenuHoverBg}
705                menuActiveBg={dropdownMenuActiveBg}
706              />
707            </div>
708          )}
709
710          <div className="flex flex-col flex-1 gap-2">
711            {format === "hex" && (
712              <FloatingLabelInput
713                value={hexInput}
714                onChange={handleHexChange}
715                onFocus={handleHexFocus}
716                onBlur={handleHexBlur}
717                label={hexLabel}
718                isRTL={isRTL}
719                maxLength={7}
720                height={inputHeight}
721                bg={inputBg}
722                borderColor={inputBorderColor}
723                borderWidth={inputBorderWidth}
724                radius={inputRadius}
725                fontSize={floatingLabelMainTextSize}
726                textColor={inputTextColor}
727                focusBorderColor={floatingLabelFocusBorderColor}
728                floatingLabelBg={resolvedFloatingLabelBg}
729                floatingLabelTextColor={floatingLabelTextColor}
730                floatingLabelActiveTextColor={floatingLabelActiveTextColor}
731                floatingLabelRadius={floatingLabelRadius}
732                floatingLabelBorderColor={floatingLabelBorderColor}
733                floatingLabelBorderWidth={floatingLabelBorderWidth}
734                hasError={hexError}
735                errorOutlineColor={inputErrorOutlineColor}
736              />
737            )}
738            {/* RGB/HSL inputs unchanged */}
739            {format === "rgb" && (
740              <div className="grid grid-cols-3 gap-2">
741                {(["r", "g", "b"] as const).map((ch) => (
742                  <FloatingLabelInput
743                    key={ch}
744                    value={rgbInputs[ch]}
745                    onChange={(v) => handleRgbChannelChange(ch, v)}
746                    onFocus={() => handleRgbFocus(ch)}
747                    onBlur={handleRgbBlur}
748                    label={ch === "r" ? rLabel : ch === "g" ? gLabel : bLabel}
749                    isRTL={isRTL}
750                    maxLength={3}
751                    height={inputHeight}
752                    bg={inputBg}
753                    borderColor={inputBorderColor}
754                    borderWidth={inputBorderWidth}
755                    radius={inputRadius}
756                    fontSize={floatingLabelMainTextSize}
757                    textColor={inputTextColor}
758                    focusBorderColor={floatingLabelFocusBorderColor}
759                    floatingLabelBg={resolvedFloatingLabelBg}
760                    floatingLabelTextColor={floatingLabelTextColor}
761                    floatingLabelActiveTextColor={floatingLabelActiveTextColor}
762                    floatingLabelRadius={floatingLabelRadius}
763                    floatingLabelBorderColor={floatingLabelBorderColor}
764                    floatingLabelBorderWidth={floatingLabelBorderWidth}
765                    hasError={rgbErrors[ch]}
766                    errorOutlineColor={inputErrorOutlineColor}
767                  />
768                ))}
769              </div>
770            )}
771            {format === "hsl" && (
772              <div className="grid grid-cols-3 gap-2">
773                {(["h", "s", "l"] as const).map((ch) => (
774                  <FloatingLabelInput
775                    key={ch}
776                    value={hslInputs[ch]}
777                    onChange={(v) => handleHslChannelChange(ch, v)}
778                    onFocus={() => handleHslFocus(ch)}
779                    onBlur={handleHslBlur}
780                    label={ch === "h" ? hLabel : ch === "s" ? sLabel : lLabel}
781                    isRTL={isRTL}
782                    maxLength={3}
783                    height={inputHeight}
784                    bg={inputBg}
785                    borderColor={inputBorderColor}
786                    borderWidth={inputBorderWidth}
787                    radius={inputRadius}
788                    fontSize={floatingLabelMainTextSize}
789                    textColor={inputTextColor}
790                    focusBorderColor={floatingLabelFocusBorderColor}
791                    floatingLabelBg={resolvedFloatingLabelBg}
792                    floatingLabelTextColor={floatingLabelTextColor}
793                    floatingLabelActiveTextColor={floatingLabelActiveTextColor}
794                    floatingLabelRadius={floatingLabelRadius}
795                    floatingLabelBorderColor={floatingLabelBorderColor}
796                    floatingLabelBorderWidth={floatingLabelBorderWidth}
797                    hasError={hslErrors[ch]}
798                    errorOutlineColor={inputErrorOutlineColor}
799                  />
800                ))}
801              </div>
802            )}
803          </div>
804        </div>
805      </div>
806
807      {/* 5. Contrast Row */}
808      {showContrast && (
809        <div 
810          className="flex items-center justify-between"
811          style={{ 
812            marginTop: contrastAreaTopMargin || undefined 
813          }}
814        >
815          <div className="flex items-center" style={{ gap: contrastItemGap }}>
816            {colorPreviewPosition === "contrast" && (
817              <div className="inline-flex items-center justify-center select-none" style={{
818                backgroundColor: hsvaToHex(hsva) || previewBgFallback,
819                width: previewWidth, height: previewHeight,
820                borderColor: previewBorderColor, borderWidth: previewBorderWidth,
821                borderStyle: "solid", borderRadius: previewRadius,
822                color: previewTextColor ?? autoPreviewTextColor,
823                fontSize: previewFontSize, fontWeight: previewFontWeight,
824              }}>
825                {colorPreviewAreaText}
826              </div>
827            )}
828            <div className="flex flex-col" style={{ gap: contrastLabelGap }}>
829              <span style={{fontSize: contrastLabelSize, color: contrastLabelColor, fontWeight: contrastLabelWeight}}>
830                {contrastLabel}
831              </span>
832              <span style={{fontSize: contrastValueSize, color: contrastValueColor, fontWeight: contrastValueWeight, fontFamily: "monospace"}}>
833                {formatContrast(contrastRatio)}
834              </span>
835            </div>
836          </div>
837          <div className="flex" style={{ gap: contrastBadgeGap }}>
838            {showContrastAALabel && (
839              <Badge
840                pass={contrastRatio >= 4.5}
841                label="AA"
842                badgeBorderWidth={badgeBorderWidth}
843                badgeBorderRadius={badgeBorderRadius}
844                badgeFontSize={badgeFontSize}
845                badgeFontWeight={badgeFontWeight}
846                badgeIconSize={badgeIconSize}
847                badgePadding={badgePadding}
848                badgeIconStrokeWidth={badgeIconStrokeWidth}
849                badgeBgPass={badgeBgPass}
850                badgeBgFail={badgeBgFail}
851                badgeBorderPass={badgeBorderPass}
852                badgeBorderFail={badgeBorderFail}
853                badgeTextPass={badgeTextPass}
854                badgeTextFail={badgeTextFail}
855              />
856            )}
857            {showContrastAAALabel && (
858              <Badge
859                pass={contrastRatio >= 7}
860                label="AAA"
861                badgeBorderWidth={badgeBorderWidth}
862                badgeBorderRadius={badgeBorderRadius}
863                badgeFontSize={badgeFontSize}
864                badgeFontWeight={badgeFontWeight}
865                badgeIconSize={badgeIconSize}
866                badgePadding={badgePadding}
867                badgeIconStrokeWidth={badgeIconStrokeWidth}
868                badgeBgPass={badgeBgPass}
869                badgeBgFail={badgeBgFail}
870                badgeBorderPass={badgeBorderPass}
871                badgeBorderFail={badgeBorderFail}
872                badgeTextPass={badgeTextPass}
873                badgeTextFail={badgeTextFail}
874              />
875            )}
876          </div>
877        </div>
878      )}
879    </div>
880  );
881}
882
883
884// --- Sub-Components ---
885
886function Badge({
887  pass,
888  label,
889  // All props now support string | number for sizes
890  badgeBorderWidth = 1,
891  badgeBorderRadius = "999px",
892  badgeFontSize = "10px",
893  badgeFontWeight = 700,
894  badgeIconSize = 10,
895  badgePadding = "0.25rem 0.5rem",
896  badgeIconStrokeWidth = 1.5,
897  badgeBgPass = "rgba(6, 181, 239, 0.1)",
898  badgeBgFail = "rgba(255, 59, 59, 0.1)",
899  badgeBorderPass = "rgba(6, 181, 239, 0.5)",
900  badgeBorderFail = "rgba(255, 59, 59, 0.5)",
901  badgeTextPass = "#06B5EF",
902  badgeTextFail = "#EF0641",
903}: {
904  pass: boolean;
905  label: string;
906  badgeBorderWidth?: number | string;
907  badgeBorderRadius?: number | string;
908  badgeFontSize?: number | string;
909  badgeFontWeight?: number;
910  badgeIconSize?: number | string;
911  badgePadding?: string;
912  badgeIconStrokeWidth?: number;
913  badgeBgPass?: string;
914  badgeBgFail?: string;
915  badgeBorderPass?: string;
916  badgeBorderFail?: string;
917  badgeTextPass?: string;
918  badgeTextFail?: string;
919}) {
920  const borderWidthStr = typeof badgeBorderWidth === 'number' ? `${badgeBorderWidth}px` : badgeBorderWidth;
921  const borderRadiusStr = typeof badgeBorderRadius === 'number' ? `${badgeBorderRadius}px` : badgeBorderRadius;
922  const fontSizeStr = typeof badgeFontSize === 'number' ? `${badgeFontSize}px` : badgeFontSize;
923  const iconSizeStr = typeof badgeIconSize === 'number' ? `${badgeIconSize}px` : badgeIconSize;
924
925  return (
926    <div
927      className="flex items-center gap-1 px-2 py-0.5 font-bold border select-none"
928      style={{
929        backgroundColor: pass ? badgeBgPass : badgeBgFail,
930        borderColor: pass ? badgeBorderPass : badgeBorderFail,
931        borderWidth: borderWidthStr,
932        borderRadius: borderRadiusStr,
933        fontSize: fontSizeStr,
934        fontWeight: badgeFontWeight,
935        padding: badgePadding,
936        color: pass ? badgeTextPass : badgeTextFail,
937      }}
938    >
939      {pass ? (
940        <CheckCircle2 size={Number(iconSizeStr.replace('px', ''))} strokeWidth={badgeIconStrokeWidth} />
941      ) : (
942        <XCircle size={Number(iconSizeStr.replace('px', ''))} strokeWidth={badgeIconStrokeWidth} />
943      )}
944      {label}
945    </div>
946  );
947}
948
949// --- FIXED Floating Label Input ---
950// IMMEDIATE parent prop sync + perfect color erasing respect
951
952type FloatingLabelInputProps = {
953  value: string;
954  onChange: (v: string) => void;
955  label: string;
956  isRTL: boolean;
957  maxLength?: number;
958  height: number;
959  bg: string;
960  borderColor: string;      // ← IMMEDIATELY responsive
961  borderWidth: number;
962  radius: number;
963  fontSize: number;
964  textColor: string;
965  focusBorderColor: string;
966  floatingLabelBg: string;  // ← IMMEDIATELY responsive
967  floatingLabelTextColor: string;
968  floatingLabelActiveTextColor: string;
969  floatingLabelRadius: number;
970  floatingLabelBorderColor?: string;
971  floatingLabelBorderWidth: number;
972  hasError?: boolean;
973  errorOutlineColor: string;
974};
975
976export const FloatingLabelInput: React.FC<FloatingLabelInputProps> = ({
977  value, onChange, label, isRTL, hasError = false,
978  maxLength = 64, height, bg, borderColor, borderWidth, radius, fontSize,
979  textColor, focusBorderColor, floatingLabelBg, floatingLabelTextColor,
980  floatingLabelActiveTextColor, floatingLabelRadius, floatingLabelBorderColor,
981  floatingLabelBorderWidth, errorOutlineColor,
982}) => {
983  const [isFocused, setIsFocused] = React.useState(false);
984  const hasValue = value.length > 0;
985
986  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
987    const newVal = e.target.value;
988    onChange(newVal);
989  };
990
991  // ✅ FIXED: IMMEDIATE parent prop reactivity
992  const labelTextColor = hasError 
993    ? errorOutlineColor 
994    : isFocused ? floatingLabelActiveTextColor : floatingLabelTextColor;
995
996  const currentBorderColor = hasError ? errorOutlineColor : borderColor;
997  const currentFocusBorderColor = hasError ? errorOutlineColor : focusBorderColor;
998
999  const inputRef = React.useRef<HTMLInputElement>(null);
1000
1001  // ✅ FIXED: useEffect for immediate parent prop changes
1002  React.useEffect(() => {
1003    if (inputRef.current) {
1004      inputRef.current.style.borderColor = isFocused 
1005        ? currentFocusBorderColor 
1006        : currentBorderColor;
1007    }
1008  }, [borderColor, focusBorderColor, hasError, errorOutlineColor, isFocused]);
1009
1010  // ✅ FIXED: Manual style updates respect parent changes instantly
1011  const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
1012    setIsFocused(true);
1013    e.currentTarget.style.borderColor = currentFocusBorderColor;
1014  };
1015
1016  const handleBlur = (e: React.BlurEvent<HTMLInputElement>) => {
1017    setIsFocused(false);
1018    e.currentTarget.style.borderColor = currentBorderColor;
1019  };
1020
1021  return (
1022    <div 
1023      className={[
1024        "floating-input-group",
1025        isRTL ? "rtl" : "",
1026        isFocused ? "active" : "",
1027        hasValue ? "has-value" : "",
1028      ].join(" ")}
1029      style={{
1030        "--input-height": `${height}px`,
1031        "--input-font-size": `${fontSize}px`,
1032        "--label-font-size": `${fontSize}px`,
1033        "--label-active-font-size": `${fontSize * 0.75}px`,
1034        "--input-padding": "12px 16px 8px",
1035        "--label-padding": "0 8px",
1036        "--label-active-padding": "0 6px",
1037        "--input-outline": currentBorderColor,
1038        "--input-outline-focus": currentFocusBorderColor,
1039        "--foreground": textColor,
1040        "--muted-foreground": floatingLabelTextColor,
1041        "--accent-color": floatingLabelActiveTextColor,
1042        "--parent-background": floatingLabelBg,  // ← IMMEDIATELY updates
1043        "--general-rounding": `${radius}px`,
1044      }}
1045    >
1046      <input
1047        ref={inputRef}
1048        type="text"
1049        value={value}
1050        onChange={handleChange}
1051        className="floating-input"
1052        maxLength={maxLength}
1053        autoComplete="off"  // ✅ Browser autofill prevention
1054        inputMode="text"    // ✅ Better mobile
1055        spellCheck="false"  // ✅ No autocorrect
1056        style={{
1057          backgroundColor: bg,
1058          borderRadius: radius,
1059          borderStyle: "solid",
1060          borderWidth,
1061          color: textColor,
1062          direction: "ltr",
1063          textAlign: isRTL ? "right" : "left",
1064        }}
1065        onFocus={handleFocus}
1066        onBlur={handleBlur}
1067      />
1068      <label 
1069        className="floating-label"
1070        style={{
1071          color: labelTextColor,  // ← IMMEDIATELY responsive
1072          borderRadius: floatingLabelRadius,
1073          borderStyle: floatingLabelBorderColor ? "solid" : "none",
1074          borderColor: floatingLabelBorderColor,
1075          borderWidth: floatingLabelBorderWidth,
1076          backgroundColor: floatingLabelBg,  // ← EXPLICIT background
1077        }}
1078      >
1079        {label}
1080      </label>
1081
1082      <style jsx>{`
1083        .floating-input-group {
1084          position: relative;
1085          width: 100%;
1086          margin-top: 8px;
1087        }
1088
1089        .floating-input {
1090          width: 100%;
1091          height: var(--input-height);
1092          padding: var(--input-padding);
1093          font-size: var(--input-font-size);
1094          font-weight: 400;
1095          font-family: 'SF Mono', Monaco, 'Roboto Mono', monospace;
1096          color: var(--foreground);
1097          background: var(--parent-background);
1098          border: 2px solid var(--input-outline);
1099          border-radius: var(--general-rounding);
1100          outline: none;
1101          box-sizing: border-box;
1102          text-transform: uppercase;
1103          box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
1104          transition: border-color 0.15s ease;  // ✅ Faster transition
1105          line-height: 1.4;
1106        }
1107
1108        .floating-input:focus {
1109          border-color: var(--input-outline-focus);
1110        }
1111
1112        .floating-input-group.rtl .floating-input {
1113          direction: rtl;
1114          text-align: right;
1115        }
1116
1117        .floating-label {
1118          position: absolute;
1119          left: 12px;
1120          top: 50%;
1121          transform: translateY(-50%);
1122          font-size: var(--label-font-size);
1123          pointer-events: none;
1124          padding: var(--label-padding);
1125          transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1126          z-index: 2;
1127          white-space: nowrap;
1128          overflow: hidden;
1129          text-overflow: ellipsis;
1130          transform-origin: top left;
1131        }
1132
1133        .floating-input-group.rtl .floating-label {
1134          right: 12px;
1135          left: auto;
1136          transform-origin: top right;
1137        }
1138
1139        .floating-input-group:not(.active):not(.has-value) .floating-label {
1140          top: 50%;
1141          transform: translateY(-50%);
1142          font-size: var(--label-font-size);
1143          padding: var(--label-padding);
1144        }
1145
1146        .floating-input-group.active .floating-label,
1147        .floating-input-group.has-value .floating-label {
1148          top: 1px;
1149          left: 12px;
1150          font-size: var(--label-active-font-size);
1151          padding: var(--label-active-padding);
1152        }
1153
1154        .floating-input-group.rtl.active .floating-label,
1155        .floating-input-group.rtl.has-value .floating-label {
1156          right: 12px;
1157          left: auto;
1158        }
1159      `}</style>
1160    </div>
1161  );
1162};
1163
1164// --- Floating Label Select ---
1165type FloatingLabelSelectProps = {
1166  value: string;
1167  onChange: (v: string) => void;
1168  label: string;
1169  options: string[];
1170  isRTL: boolean;
1171  t: (key: string) => string;
1172  height: number;
1173  bg: string;
1174  borderColor: string;
1175  borderWidth: number;
1176  radius: number;
1177  fontSize: number;
1178  textColor: string;
1179  focusBorderColor: string;
1180  focusRingColor: string;
1181  chevronColor: string;
1182  floatingLabelBg: string;
1183  floatingLabelTextColor: string;
1184  floatingLabelActiveTextColor: string;
1185  floatingLabelRadius: number;
1186  floatingLabelBorderColor?: string;
1187  floatingLabelBorderWidth: number;
1188  menuBg: string;
1189  menuBorderColor: string;
1190  menuBorderWidth: number;
1191  menuRadius: number;
1192  menuTextColor: string;
1193  menuActiveTextColor: string;
1194  menuHoverBg: string;
1195  menuActiveBg: string;
1196};
1197
1198export const FloatingLabelSelect: React.FC<FloatingLabelSelectProps> = ({
1199  value,
1200  onChange,
1201  label,
1202  options,
1203  isRTL,
1204  t,
1205  height,
1206  bg,
1207  borderColor,
1208  borderWidth,
1209  radius,
1210  fontSize,
1211  textColor,
1212  focusBorderColor,
1213  chevronColor,
1214  floatingLabelBg,
1215  floatingLabelTextColor,
1216  floatingLabelActiveTextColor,
1217  floatingLabelRadius,
1218  floatingLabelBorderColor,
1219  floatingLabelBorderWidth,
1220  menuBg,
1221  menuBorderColor,
1222  menuBorderWidth,
1223  menuRadius,
1224  menuTextColor,
1225  menuActiveTextColor,
1226  menuHoverBg,
1227  menuActiveBg,
1228}) => {
1229  const [open, setOpen] = React.useState(false);
1230  const [isFocused, setIsFocused] = React.useState(false);
1231  const hasValue = value !== null && value !== undefined && value !== ""; // Tracks if select has selection
1232
1233  const buttonRef = React.useRef<HTMLButtonElement | null>(null);
1234  const listRef = React.useRef<HTMLDivElement | null>(null);
1235
1236  React.useEffect(() => {
1237    if (!open) return;
1238    const handler = (e: MouseEvent) => {
1239      if (
1240        !buttonRef.current?.contains(e.target as Node) &&
1241        !listRef.current?.contains(e.target as Node)
1242      ) {
1243        setOpen(false);
1244      }
1245    };
1246    window.addEventListener("mousedown", handler);
1247    return () => window.removeEventListener("mousedown", handler);
1248  }, [open]);
1249
1250  const currentLabel = value ? t(value) : label;
1251
1252  return (
1253    <div 
1254      className={[
1255        "floating-select-group",
1256        isRTL ? "rtl" : "",
1257        isFocused ? "active" : "",
1258        hasValue ? "has-value" : "",
1259      ].join(" ")}
1260      style={{
1261        "--input-height": `${height}px`,
1262        "--input-font-size": `${fontSize}px`,
1263        "--label-font-size": `${fontSize}px`,
1264        "--label-active-font-size": `${fontSize * 0.75}px`,
1265        "--input-padding": "12px 16px 8px",
1266        "--label-padding": "0 8px",
1267        "--label-active-padding": "0 6px",
1268        "--input-outline": borderColor,
1269        "--input-outline-focus": focusBorderColor,
1270        "--foreground": textColor,
1271        "--muted-foreground": floatingLabelTextColor,
1272        "--accent-color": floatingLabelActiveTextColor,
1273        "--parent-background": floatingLabelBg,
1274        "--general-rounding": `${radius}px`,
1275      }}
1276    >
1277      <button
1278        ref={buttonRef}
1279        type="button"
1280        className="floating-select"
1281        style={{
1282          backgroundColor: bg,
1283          borderRadius: radius,
1284          borderStyle: "solid",
1285          borderWidth,
1286          color: textColor,
1287          direction: "ltr",
1288          textAlign: isRTL ? "right" : "left",
1289        }}
1290        onClick={() => setOpen((o) => !o)}
1291        onFocus={(e) => {
1292          setIsFocused(true);
1293          e.currentTarget.style.borderColor = focusBorderColor;
1294        }}
1295        onBlur={(e) => {
1296          setIsFocused(false);
1297          e.currentTarget.style.borderColor = borderColor;
1298        }}
1299      >
1300        <span className="truncate">{currentLabel}</span>
1301        <ChevronDown size={16} className="ml-2 shrink-0" color={chevronColor} />
1302      </button>
1303
1304      <label 
1305        className="floating-label"
1306        style={{
1307          color: isFocused ? floatingLabelActiveTextColor : floatingLabelTextColor,
1308          borderRadius: floatingLabelRadius,
1309          borderStyle: floatingLabelBorderColor ? "solid" : "none",
1310          borderColor: floatingLabelBorderColor,
1311          borderWidth: floatingLabelBorderWidth,
1312        }}
1313      >
1314        {label}
1315      </label>
1316
1317      {open && (
1318        <div
1319          ref={listRef}
1320          className={cn(
1321            "absolute z-50 mt-1 py-1 shadow-2xl border min-w-[120px] max-h-48 overflow-auto",
1322            isRTL ? "right-0" : "left-0"
1323          )}
1324          style={{
1325            backgroundColor: menuBg,
1326            borderRadius: menuRadius,
1327            borderStyle: "solid",
1328            borderColor: menuBorderColor,
1329            borderWidth: menuBorderWidth,
1330          }}
1331        >
1332          {options.map((opt) => {
1333            const active = opt === value;
1334            return (
1335              <button
1336                key={opt}
1337                type="button"
1338                onClick={() => {
1339                  onChange(opt);
1340                  setOpen(false);
1341                }}
1342                className="w-full px-3 py-2 text-[11px] uppercase cursor-pointer flex items-center justify-between gap-2 transition-colors"
1343                style={{
1344                  direction: isRTL ? "rtl" : "ltr",
1345                  backgroundColor: active ? menuActiveBg : "transparent",
1346                  color: active ? menuActiveTextColor : menuTextColor,
1347                }}
1348                onMouseEnter={(e) => {
1349                  if (!active) e.currentTarget.style.backgroundColor = menuHoverBg;
1350                }}
1351                onMouseLeave={(e) => {
1352                  if (!active) e.currentTarget.style.backgroundColor = "transparent";
1353                }}
1354              >
1355                <span className="truncate">{t(opt)}</span>
1356                {active && <Check size={12} />}
1357              </button>
1358            );
1359          })}
1360        </div>
1361      )}
1362
1363      <style jsx>{`
1364        .floating-select-group {
1365          position: relative;
1366          width: 100%;
1367          margin-top: 8px;
1368        }
1369
1370        .floating-select {
1371          width: 100%;
1372          height: var(--input-height);
1373          padding: var(--input-padding);
1374          font-size: var(--input-font-size);
1375          font-weight: 400;
1376          font-family: 'SF Mono', Monaco, 'Roboto Mono', monospace;
1377          color: var(--foreground);
1378          background: var(--parent-background);
1379          border: 2px solid var(--input-outline);
1380          border-radius: var(--general-rounding);
1381          outline: none;
1382          box-sizing: border-box;
1383          text-transform: uppercase;
1384          box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
1385          transition: border-color 0.3s ease;
1386          line-height: 1.4;
1387          cursor: pointer;
1388          display: flex;
1389          align-items: center;
1390          justify-content: space-between;
1391        }
1392
1393        .floating-select:focus {
1394          border-color: var(--input-outline-focus);
1395        }
1396
1397        .floating-select-group.rtl .floating-select {
1398          direction: rtl;
1399          text-align: right;
1400        }
1401
1402        .floating-label {
1403          position: absolute;
1404          left: 12px;
1405          top: 50%;
1406          transform: translateY(-50%);
1407          font-size: var(--label-font-size);
1408          pointer-events: none;
1409          background: var(--parent-background);
1410          padding: var(--label-padding);
1411          transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1412          z-index: 2;
1413          white-space: nowrap;
1414          overflow: hidden;
1415          text-overflow: ellipsis;
1416          transform-origin: top left;
1417        }
1418
1419        .floating-select-group.rtl .floating-label {
1420          right: 12px;
1421          left: auto;
1422          transform-origin: top right;
1423        }
1424
1425        .floating-select-group:not(.active):not(.has-value) .floating-label {
1426          top: 50%;
1427          transform: translateY(-50%);
1428          font-size: var(--label-font-size);
1429          padding: var(--label-padding);
1430        }
1431
1432        .floating-select-group.active .floating-label,
1433        .floating-select-group.has-value .floating-label {
1434          top: 1px;
1435          left: 12px;
1436          font-size: var(--label-active-font-size);
1437          padding: var(--label-active-padding);
1438        }
1439
1440        .floating-select-group.rtl.active .floating-label,
1441        .floating-select-group.rtl.has-value .floating-label {
1442          right: 12px;
1443          left: auto;
1444        }
1445      `}</style>
1446    </div>
1447  );
1448};
1449
1450type ColorPreviewBoxProps = {
1451  label?: string;
1452  height?: string | number;
1453  width?: string | number;
1454  bg?: string;
1455  borderColor?: string;
1456  borderWidth?: string | number;
1457  radius?: string | number;
1458  fontSize?: string | number;
1459  fontWeight?: string | number;
1460  fontColor?: string;
1461};
1462
1463export const ColorPreviewBox: React.FC<ColorPreviewBoxProps> = ({
1464  label,
1465  height,
1466  width,
1467  bg,
1468  borderColor,
1469  borderWidth,
1470  radius,
1471  fontSize,
1472  fontWeight,
1473  fontColor,
1474}) => {
1475  return (
1476    <div
1477      className="relative flex items-center justify-center mt-2"
1478      style={{ width, height }}
1479    >
1480      <input
1481        type="text"
1482        readOnly
1483        tabIndex={-1}
1484        value=""
1485        className="absolute inset-0 font-mono shadow-inner uppercase transition-all outline-none"
1486        style={{
1487          pointerEvents: "none",
1488          userSelect: "none",
1489          backgroundColor: bg,
1490          borderRadius: radius,
1491          borderStyle: "solid",
1492          borderColor,
1493          borderWidth,
1494          color: fontColor,
1495          fontSize,
1496          fontWeight,
1497          margin: 0,
1498        }}
1499      />
1500      <label
1501        className="absolute text-center"
1502        style={{
1503          color: fontColor,
1504          fontSize,
1505          fontWeight,
1506          pointerEvents: "none",
1507          userSelect: "none",
1508        }}
1509      >
1510        {label}
1511      </label>
1512    </div>
1513  );
1514};
1515
1516export interface CustomSliderProps {
1517  id: string;
1518  min?: number;
1519  max?: number;
1520  step?: number;
1521  value: number;
1522  onValueChange: (value: number) => void;
1523  disabled?: boolean;
1524  trackHeight?: string;
1525  thumbWidth?: string;
1526  thumbHeight?: string;
1527  width?: string;
1528  trackBorderRadius?: string;
1529  trackBorderWidth?: string;
1530  trackBorderColor?: string;
1531  thumbBorderRadius?: string;
1532  thumbBorderWidth?: string;
1533  colorTrackBackground?: string;
1534  colorFillDefault?: string;
1535  colorFillHover?: string;
1536  colorFillActive?: string;
1537  colorThumbDefault?: string;
1538  colorThumbHover?: string;
1539  colorThumbActive?: string;
1540  colorThumbBorderDefault?: string;
1541  colorThumbBorderHover?: string;
1542  colorThumbBorderActive?: string;
1543  ariaLabel?: string;
1544  isRTL?: boolean;
1545  keyStep?: number;
1546}
1547
1548export function CustomSlider({
1549  id,
1550  min = 0,
1551  max = 100,
1552  step = 1,
1553  value,
1554  onValueChange,
1555  disabled = false,
1556  trackHeight = '8px',
1557  thumbWidth = '20px',
1558  thumbHeight = '20px',
1559  width = '100%',
1560  trackBorderRadius = '8px',       // ✅ NEW DEFAULT
1561  trackBorderWidth = '0px',        // ✅ NEW DEFAULT (0px = no border)
1562  trackBorderColor = 'transparent', // ✅ NEW DEFAULT
1563  thumbBorderRadius = '50%',
1564  thumbBorderWidth = '2px',
1565  colorTrackBackground = '#262626',
1566  colorFillDefault = '#00A7FA',
1567  colorFillHover = '#55C7FF',
1568  colorFillActive = '#55C7FF',
1569  colorThumbDefault = '#262626',
1570  colorThumbHover = '#121212',
1571  colorThumbActive = '#262626',
1572  colorThumbBorderDefault = '#0083C4',
1573  colorThumbBorderHover = '#0079B5',
1574  colorThumbBorderActive = '#FFFFFF',
1575  ariaLabel = 'slider',
1576  isRTL = false,
1577  keyStep = 1,
1578}: CustomSliderProps) {
1579  const sliderRef = useRef<HTMLDivElement>(null);
1580  const [isDragging, setIsDragging] = useState(false);
1581  const [isFocused, setIsFocused] = useState(false);
1582  const [isHovered, setIsHovered] = useState(false);
1583  const [sliderWidth, setSliderWidth] = useState(0);
1584
1585  const DEAD_ZONE = 8;
1586  const BASE_FILL_LENGTH = 20;
1587
1588  const updateSliderWidth = useCallback(() => {
1589    if (sliderRef.current) {
1590      setSliderWidth(sliderRef.current.clientWidth);
1591    }
1592  }, []);
1593
1594  useEffect(() => {
1595    updateSliderWidth();
1596    window.addEventListener('resize', updateSliderWidth);
1597    return () => window.removeEventListener('resize', updateSliderWidth);
1598  }, [updateSliderWidth]);
1599
1600  const getPercentage = useCallback(() => {
1601    return ((value - min) / (max - min)) * 100;
1602  }, [value, min, max]);
1603
1604  const handleInteraction = useCallback(
1605    (clientX: number) => {
1606      if (disabled || !sliderRef.current) return;
1607      const rect = sliderRef.current.getBoundingClientRect();
1608      const effectiveWidth = rect.width - DEAD_ZONE * 2;
1609      let percentage;
1610      
1611      if (isRTL) {
1612        percentage = ((rect.right - clientX - DEAD_ZONE) / effectiveWidth) * 100;
1613      } else {
1614        percentage = ((clientX - rect.left - DEAD_ZONE) / effectiveWidth) * 100;
1615      }
1616      
1617      percentage = Math.max(0, Math.min(100, percentage));
1618      let newValue = min + (percentage / 100) * (max - min);
1619      if (step !== 0) newValue = Math.round(newValue / step) * step;
1620      newValue = Math.max(min, Math.min(max, newValue));
1621      onValueChange(newValue);
1622    },
1623    [disabled, min, max, step, onValueChange, isRTL, DEAD_ZONE]
1624  );
1625
1626  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
1627    if (disabled) return;
1628    e.preventDefault();
1629    setIsDragging(true);
1630    handleInteraction(e.clientX);
1631    sliderRef.current?.focus();
1632  };
1633
1634  const handleMouseMove = useCallback(
1635    (e: MouseEvent) => {
1636      if (isDragging) handleInteraction(e.clientX);
1637    },
1638    [isDragging, handleInteraction]
1639  );
1640
1641  const handleMouseUp = useCallback(() => {
1642    setIsDragging(false);
1643  }, []);
1644
1645  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
1646    if (disabled) return;
1647    if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
1648      e.preventDefault();
1649      const direction = (e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : -1) * (isRTL ? -1 : 1);
1650      const increment = keyStep ?? step;
1651      const newValue = Math.max(min, Math.min(max, value + direction * increment));
1652      onValueChange(newValue);
1653    }
1654  };
1655
1656  useEffect(() => {
1657    if (isDragging) {
1658      window.addEventListener('mousemove', handleMouseMove);
1659      window.addEventListener('mouseup', handleMouseUp);
1660    }
1661    return () => {
1662      window.removeEventListener('mousemove', handleMouseMove);
1663      window.removeEventListener('mouseup', handleMouseUp);
1664    };
1665  }, [handleMouseMove, handleMouseUp, isDragging]);
1666
1667  const percentage = getPercentage();
1668  const fillColor = isDragging || isFocused ? colorFillActive : isHovered ? colorFillHover : colorFillDefault;
1669  const thumbColor = isDragging || isFocused ? colorThumbActive : isHovered ? colorThumbHover : colorThumbDefault;
1670  const thumbBorderColor = isDragging || isFocused ? colorThumbBorderActive : isHovered ? colorThumbBorderHover : colorThumbBorderDefault;
1671
1672  const usableWidth = Math.max(sliderWidth - DEAD_ZONE * 2, 0);
1673  const computedFillWidth = (percentage / 100) * usableWidth;
1674  const baseFillWidth = Math.min(BASE_FILL_LENGTH, usableWidth);
1675
1676  const wrapperClass = `custom-slider-${id}-wrapper${disabled ? ' disabled' : ''}`;
1677  const trackClass = `custom-slider-${id}-track`;
1678  const rangeClass = `custom-slider-${id}-range`;
1679  const thumbClass = `custom-slider-${id}-thumb`;
1680
1681  return (
1682    <div className="w-full">
1683      <style jsx>{`
1684        .${wrapperClass} {
1685          position: relative;
1686          width: ${width};
1687          height: ${thumbHeight};
1688          display: flex;
1689          align-items: center;
1690          cursor: ${disabled ? 'not-allowed' : 'pointer'};
1691          touch-action: none;
1692          outline: none;
1693        }
1694        .${trackClass}, .${rangeClass} {
1695          position: absolute;
1696          height: ${trackHeight};
1697          top: 50%;
1698          transform: translateY(-50%);
1699          width: 100%;
1700          transition: background-color 0.2s ease;
1701          box-sizing: border-box;
1702        }
1703        .${trackClass} {
1704          background: ${colorTrackBackground} !important;
1705          border-radius: ${trackBorderRadius} !important;           // ✅ NEW: Static rounding
1706          border: ${trackBorderWidth} solid ${trackBorderColor} !important;  // ✅ NEW: Static border (0px = none)
1707          z-index: 1;
1708        }
1709        .${rangeClass} {
1710          background: ${fillColor};
1711          border-radius: ${trackBorderRadius};                       // ✅ Matches track rounding
1712          ${isRTL ? 'right: 0;' : 'left: 0;'}
1713          width: ${percentage}%;
1714          z-index: 2;
1715        }
1716        .${thumbClass} {
1717          position: absolute;
1718          width: ${thumbWidth};
1719          height: ${thumbHeight};
1720          border-radius: ${thumbBorderRadius};
1721          top: 50%;
1722          transform: translateY(-50%);
1723          background: ${thumbColor};
1724          border: ${thumbBorderWidth} solid ${thumbBorderColor};
1725          box-shadow: 0 2px 6px rgba(0,0,0,0.2);
1726          transition: all 0.2s ease;
1727          z-index: 3;
1728          pointer-events: none;
1729          ${isRTL
1730            ? `right: calc(${DEAD_ZONE}px + ${computedFillWidth}px - ${parseFloat(thumbWidth.replace('px', ''))}px / 2);`
1731            : `left: calc(${DEAD_ZONE}px + ${computedFillWidth}px - ${parseFloat(thumbWidth.replace('px', ''))}px / 2);`
1732          }
1733        }
1734        .${wrapperClass}:hover .${thumbClass} {
1735          box-shadow: 0 4px 8px rgba(0,0,0,0.3);
1736        }
1737      `}</style>
1738
1739      <div
1740        ref={sliderRef}
1741        className={wrapperClass}
1742        onMouseDown={handleMouseDown}
1743        onMouseEnter={() => !disabled && setIsHovered(true)}
1744        onMouseLeave={() => setIsHovered(false)}
1745        onKeyDown={handleKeyDown}
1746        onFocus={() => setIsFocused(true)}
1747        onBlur={() => setIsFocused(false)}
1748        tabIndex={disabled ? -1 : 0}
1749        role="slider"
1750        aria-label={ariaLabel}
1751        aria-valuemin={min}
1752        aria-valuemax={max}
1753        aria-valuenow={value}
1754        aria-disabled={disabled}
1755        dir={isRTL ? 'rtl' : 'ltr'}
1756      >
1757        <div className={trackClass} />
1758        <div className={rangeClass} />
1759        <div className={thumbClass} />
1760      </div>
1761    </div>
1762  );
1763}