import React from "react"; import { RangeSlider } from "./RangeSlider"; export interface ColorPickerProps { /** The current color value (hex) */ value: string; /** Callback when the color changes */ onChange: (value: string) => void; /** Label for the color picker */ label: string; /** Optional description displayed below the label */ description?: string; /** Optional additional class name for the container */ className?: string; /** Optional placeholder for the text input */ placeholder?: string; /** Optional action button (e.g., "Reset to Auto") */ actionButton?: React.ReactNode; /** Enable an opacity slider for rgba-style colors */ allowAlpha?: boolean; } export interface ColorPickerActionButtonProps { label: string; onClick: () => void; disabled?: boolean; } export function ColorPickerActionButton({ label, onClick, disabled = false, }: ColorPickerActionButtonProps): React.ReactElement { return ( ); } /** * A reusable color picker component with color input and text input. */ export function ColorPicker({ value, onChange, label, description, className = "", placeholder, actionButton, allowAlpha = false, }: ColorPickerProps): React.ReactElement { const clamp = (rawValue: number, min: number, max: number): number => Math.min(max, Math.max(min, rawValue)); const expandHex = (hex: string): string => { const raw = hex.replace("#", "").trim(); if (raw.length === 3 || raw.length === 4) { return raw .slice(0, raw.length === 4 ? 4 : 3) .split("") .map((char) => char + char) .join("") .slice(0, 6); } return raw.slice(0, 6); }; const rgbToHex = (r: number, g: number, b: number): string => `#${[r, g, b] .map((channel) => clamp(Math.round(channel), 0, 255).toString(16).padStart(2, "0"), ) .join("")}`; const parseColorValue = ( rawValue: string, ): { hex: string; alpha: number } => { const trimmed = (rawValue || "").trim(); if (!trimmed || trimmed.toLowerCase() === "transparent") { return { hex: "#000000", alpha: 0 }; } const hexMatch = trimmed.match(/^#([0-9a-f]{3,8})$/i); if (hexMatch) { const normalized = expandHex(trimmed); const rawHex = hexMatch[1]; const alphaHex = rawHex.length === 4 ? `${rawHex[3]}${rawHex[3]}` : rawHex.length === 8 ? rawHex.slice(6, 8) : ""; return { hex: `#${normalized}`.toLowerCase(), alpha: alphaHex ? clamp(parseInt(alphaHex, 16) / 255, 0, 1) : 1, }; } const rgbaMatch = trimmed.match(/^rgba?\((.+)\)$/i); if (rgbaMatch) { const parts = rgbaMatch[1].split(",").map((part) => part.trim()); if (parts.length >= 3) { const red = Number(parts[0]); const green = Number(parts[1]); const blue = Number(parts[2]); const alpha = parts.length >= 4 ? Number(parts[3]) : 1; if ( [red, green, blue].every(Number.isFinite) && Number.isFinite(alpha) ) { return { hex: rgbToHex(red, green, blue).toLowerCase(), alpha: clamp(alpha, 0, 1), }; } } } return { hex: "#000000", alpha: 1 }; }; const formatAlpha = (alpha: number): string => { const rounded = Math.round(clamp(alpha, 0, 1) * 100) / 100; return rounded.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""); }; const toColorString = (hex: string, alpha: number): string => { const normalizedHex = expandHex(hex); const red = parseInt(normalizedHex.slice(0, 2), 16); const green = parseInt(normalizedHex.slice(2, 4), 16); const blue = parseInt(normalizedHex.slice(4, 6), 16); const safeAlpha = clamp(alpha, 0, 1); if (safeAlpha <= 0) { return `rgba(${red}, ${green}, ${blue}, 0)`; } if (safeAlpha >= 1) { return `#${normalizedHex}`.toLowerCase(); } return `rgba(${red}, ${green}, ${blue}, ${formatAlpha(safeAlpha)})`; }; const parsedColor = parseColorValue(value); return (
{description && (

{description}

)}
onChange( allowAlpha ? toColorString(e.target.value, parsedColor.alpha) : e.target.value, ) } className="b3-wvs-h-10 b3-wvs-w-14 b3-wvs-cursor-pointer b3-wvs-rounded-md b3-wvs-border b3-wvs-border-slate-300 b3-wvs-bg-white" /> onChange(e.target.value)} placeholder={placeholder} className="b3-wvs-flex-1 b3-wvs-min-w-0 b3-wvs-rounded-md b3-wvs-border b3-wvs-border-slate-300 b3-wvs-bg-white b3-wvs-px-3 b3-wvs-py-2 b3-wvs-text-sm b3-wvs-font-mono b3-wvs-text-slate-700 focus:b3-wvs-border-admin focus:b3-wvs-outline-none focus:b3-wvs-ring-2 focus:b3-wvs-ring-admin" /> {actionButton}
{allowAlpha && ( onChange(toColorString(parsedColor.hex, 1 - sliderValue / 100)) } /> )}
); }