UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

559 lines (556 loc) 21.7 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { Palette, Check, RotateCcw } from 'lucide-react'; import { useState, useRef, useEffect, useCallback } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { CardHeader, CardTitle, CardContent } from '../card/index.js'; import { GlassInput } from './GlassInput.js'; import { GlassCard } from '../card/GlassCard.js'; // Color conversion utilities const hexToRgb = hex => { const result = /^#?([a-f\d]{2})([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), a: result[4] ? parseInt(result[4], 16) / 255 : undefined } : null; }; const rgbToHex = (r, g, b, a) => { const toHex = n => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0"); const alpha = a !== undefined ? toHex(a * 255) : ""; return `#${toHex(r)}${toHex(g)}${toHex(b)}${alpha}`; }; const rgbToHsl = (r, g, b, a) => { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0, s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s * 100, l: l * 100, a }; }; const hslToRgb = (h, s, l, a) => { h /= 360; s /= 100; l /= 100; const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; let r, g, b; if (s === 0) { r = g = b = l; } else { const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), a }; }; // Predefined color palettes const defaultPalette = ["var(--glass-black)", "var(--glass-white)", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#ffa500", "#800080", "#ffc0cb", "#a52a2a", "#808080", "#000080", "#008000"]; const materialPalette = ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#795548", "#9e9e9e", "#607d8b"]; /** * GlassColorPicker component * A comprehensive color picker with multiple selection methods */ const GlassColorPicker = ({ value, defaultValue = "var(--glass-color-primary)", onChange, format = "hex", showAlpha = false, palette = defaultPalette, showInput = true, showPresets = true, disabled = false, size = "md", className, placement = "bottom", ...props }) => { const [isOpen, setIsOpen] = useState(false); const [currentColor, setCurrentColor] = useState(value || defaultValue); const [hsl, setHsl] = useState({ h: 210, s: 100, l: 50 }); const [rgb, setRgb] = useState({ r: 59, g: 130, b: 246 }); const [inputValue, setInputValue] = useState(currentColor); const triggerRef = useRef(null); const popoverRef = useRef(null); const swatchRef = useRef(null); const hueRef = useRef(null); const satRef = useRef(null); const lightRef = useRef(null); const alphaRef = useRef(null); // apply trigger color without JSX style attr useEffect(() => { if (triggerRef.current) { triggerRef.current.style.backgroundColor = currentColor; } if (swatchRef.current) { swatchRef.current.style.backgroundColor = currentColor; } if (hueRef.current) { hueRef.current.classList.add("glass-color-hue-gradient"); } if (satRef.current) { satRef.current.style.background = `linear-gradient(to right, hsl(${hsl.h}, 0%, ${hsl.l}%) 0%, hsl(${hsl.h}, 100%, ${hsl.l}%) 100%)`; } if (lightRef.current) { lightRef.current.style.background = `linear-gradient(to right, hsl(${hsl.h}, ${hsl.s}%, 0%) 0%, hsl(${hsl.h}, ${hsl.s}%, 50%) 50%, hsl(${hsl.h}, ${hsl.s}%, 100%) 100%)`; } if (alphaRef.current) { alphaRef.current.style.background = `linear-gradient(to right, transparent 0%, ${currentColor} 100%)`; } }, [currentColor, hsl.h, hsl.s, hsl.l]); // Size configurations const sizeConfigs = { sm: { trigger: "w-8 h-8", popover: "w-64" }, md: { trigger: "w-10 h-10", popover: "w-80" }, lg: { trigger: "w-12 h-12", popover: "w-96" } }; // Update color when value prop changes useEffect(() => { if (value) { setCurrentColor(value); setInputValue(value); updateColorValues(value); } }, [value]); // Update HSL and RGB when color changes const updateColorValues = useCallback(color => { const rgbColor = hexToRgb(color); if (rgbColor) { setRgb(rgbColor); const hslColor = rgbToHsl(rgbColor.r, rgbColor.g, rgbColor.b, rgbColor.a); setHsl(hslColor); } }, []); // Handle color change const handleColorChange = useCallback(newColor => { setCurrentColor(newColor); setInputValue(newColor); updateColorValues(newColor); onChange?.(newColor); }, [onChange, updateColorValues]); // Handle HSL slider changes const handleHslChange = useCallback((type, value) => { const newHsl = { ...hsl, [type]: value }; setHsl(newHsl); const newRgb = hslToRgb(newHsl.h, newHsl.s, newHsl.l, newHsl.a); setRgb(newRgb); const hexColor = rgbToHex(newRgb.r, newRgb.g, newRgb.b, newRgb.a); handleColorChange(hexColor); }, [hsl, handleColorChange]); // Handle RGB slider changes const handleRgbChange = useCallback((type, value) => { const newRgb = { ...rgb, [type]: value }; setRgb(newRgb); const newHsl = rgbToHsl(newRgb.r, newRgb.g, newRgb.b, newRgb.a); setHsl(newHsl); const hexColor = rgbToHex(newRgb.r, newRgb.g, newRgb.b, newRgb.a); handleColorChange(hexColor); }, [rgb, handleColorChange]); // Handle alpha change const handleAlphaChange = useCallback(value => { const newRgb = { ...rgb, a: value / 100 }; setRgb(newRgb); const newHsl = rgbToHsl(newRgb.r, newRgb.g, newRgb.b, newRgb.a); setHsl(newHsl); const hexColor = rgbToHex(newRgb.r, newRgb.g, newRgb.b, newRgb.a); handleColorChange(hexColor); }, [rgb, handleColorChange]); // Handle input change const handleInputChange = useCallback(newValue => { setInputValue(newValue); if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/.test(newValue)) { handleColorChange(newValue); } }, [handleColorChange]); // Format color for display const formatColorValue = useCallback(color => { const rgbColor = hexToRgb(color); if (!rgbColor) return color; switch (format) { case "rgb": return rgbColor.a !== undefined ? `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${rgbColor.a})` : `rgb(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b})`; case "hsl": const hslColor = rgbToHsl(rgbColor.r, rgbColor.g, rgbColor.b, rgbColor.a); return hslColor.a !== undefined ? `hsla(${Math.round(hslColor.h)}, ${Math.round(hslColor.s)}%, ${Math.round(hslColor.l)}%, ${hslColor.a})` : `hsl(${Math.round(hslColor.h)}, ${Math.round(hslColor.s)}%, ${Math.round(hslColor.l)}%)`; default: return color; } }, [format]); // Close popover when clicking outside useEffect(() => { const handleClickOutside = event => { if (triggerRef.current && !triggerRef.current.contains(event.target) && popoverRef.current && !popoverRef.current.contains(event.target)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const config = sizeConfigs[size]; return jsxs("div", { "data-glass-component": true, className: 'relative', children: [jsx("div", { ref: triggerRef, className: cn("glass-radius-lg border-2 border-white/20 cursor-pointer transition-all duration-200", "hover:border-white/40 focus:border-white/60 focus:outline-none", config.trigger, disabled && "opacity-50 cursor-not-allowed", className), onClick: e => !disabled && setIsOpen(!isOpen), role: "button", "aria-label": "Choose color", "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, onKeyDown: e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); !disabled && setIsOpen(!isOpen); } }, ...props }), isOpen && jsx(MotionFramer, { preset: "fadeIn", className: cn("absolute z-50 glass-mt-2", placement === "top" && "glass-bottom-full", placement === "bottom" && "glass-top-full", placement === "left" && "glass-right-full", placement === "right" && "glass-left-full"), children: jsx("div", { ref: popoverRef, role: "dialog", "aria-modal": "true", "aria-label": "Color picker", className: cn("glass-radius-xl border border-white/20 shadow-2xl glass-backdrop-blur-md bg-black/20", config.popover), children: jsxs(GlassCard, { variant: "outline", className: 'glass-border-0 bg-transparent', children: [jsx(CardHeader, { className: 'pb-4', children: jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs(CardTitle, { id: "color-picker-title", className: 'glass-text-lg font-semibold text-primary glass-flex glass-items-center glass-gap-2', children: [jsx(Palette, { className: 'w-5 h-5', "aria-hidden": "true" }), "Color Picker"] }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setIsOpen(false), "aria-label": "Close color picker", children: "\u00D7" })] }) }), jsxs(CardContent, { className: 'space-y-6', children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-4", children: [jsx("div", { ref: swatchRef, className: 'w-16 h-16 glass-radius-lg glass-border-2 glass-border-white/20' }), jsxs("div", { className: "glass-flex-1", children: [showInput && jsx(GlassInput, { value: inputValue, onChange: e => handleInputChange(e.target.value), placeholder: "var(--glass-black)", className: "glass-text-sm" }), jsx("div", { className: 'glass-text-xs text-primary/60 glass-mt-1', children: formatColorValue(currentColor) })] })] }), jsxs("div", { className: "glass-gap-4", children: [jsxs("div", { children: [jsx("label", { htmlFor: "hue-slider", id: "hue-label", className: 'glass-text-sm text-primary/80 mb-2 block', children: "Hue" }), jsx("input", { ref: hueRef, id: "hue-slider", type: "range", role: "slider", "aria-labelledby": "hue-label", "aria-valuemin": 0, "aria-valuemax": 360, "aria-valuenow": Math.round(hsl.h), "aria-valuetext": `${Math.round(hsl.h)} degrees`, min: "0", max: "360", value: hsl.h, onChange: e => handleHslChange("h", Number(e.target.value)), className: 'glass-w-full h-2 glass-surface-subtle glass-radius-lg appearance-none cursor-pointer slider' })] }), jsxs("div", { children: [jsx("label", { htmlFor: "saturation-slider", id: "saturation-label", className: 'glass-text-sm text-primary/80 mb-2 block', children: "Saturation" }), jsx("input", { ref: satRef, id: "saturation-slider", type: "range", role: "slider", "aria-labelledby": "saturation-label", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(hsl.s), "aria-valuetext": `${Math.round(hsl.s)} percent`, min: "0", max: "100", value: hsl.s, onChange: e => handleHslChange("s", Number(e.target.value)), className: 'glass-w-full h-2 glass-surface-subtle glass-radius-lg appearance-none cursor-pointer' })] }), jsxs("div", { children: [jsx("label", { htmlFor: "lightness-slider", id: "lightness-label", className: 'glass-text-sm text-primary/80 mb-2 block', children: "Lightness" }), jsx("input", { ref: lightRef, id: "lightness-slider", type: "range", role: "slider", "aria-labelledby": "lightness-label", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(hsl.l), "aria-valuetext": `${Math.round(hsl.l)} percent`, min: "0", max: "100", value: hsl.l, onChange: e => handleHslChange("l", Number(e.target.value)), className: 'glass-w-full h-2 glass-surface-subtle glass-radius-lg appearance-none cursor-pointer' })] }), showAlpha && jsxs("div", { children: [jsx("label", { htmlFor: "alpha-slider", id: "alpha-label", className: 'glass-text-sm text-primary/80 mb-2 block', children: "Alpha" }), jsx("input", { ref: alphaRef, id: "alpha-slider", type: "range", role: "slider", "aria-labelledby": "alpha-label", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round((rgb.a || 1) * 100), "aria-valuetext": `${Math.round((rgb.a || 1) * 100)} percent`, min: "0", max: "100", value: (rgb.a || 1) * 100, onChange: e => handleAlphaChange(Number(e.target.value)), className: 'glass-w-full h-2 glass-surface-subtle glass-radius-lg appearance-none cursor-pointer' })] })] }), jsxs("div", { className: "glass-grid glass-grid-cols-3 glass-gap-4", children: [jsxs("div", { children: [jsx("label", { htmlFor: "red-input", className: 'glass-text-sm text-primary/80 mb-2 block', children: "R" }), jsx(GlassInput, { id: "red-input", type: "number", min: "0", max: "255", value: rgb.r, onChange: e => handleRgbChange("r", Number(e.target.value)), "aria-label": "Red channel value", "aria-valuemin": 0, "aria-valuemax": 255, "aria-valuenow": rgb.r, className: "glass-text-sm" })] }), jsxs("div", { children: [jsx("label", { htmlFor: "green-input", className: 'glass-text-sm text-primary/80 mb-2 block', children: "G" }), jsx(GlassInput, { id: "green-input", type: "number", min: "0", max: "255", value: rgb.g, onChange: e => handleRgbChange("g", Number(e.target.value)), "aria-label": "Green channel value", "aria-valuemin": 0, "aria-valuemax": 255, "aria-valuenow": rgb.g, className: "glass-text-sm" })] }), jsxs("div", { children: [jsx("label", { htmlFor: "blue-input", className: 'glass-text-sm text-primary/80 mb-2 block', children: "B" }), jsx(GlassInput, { id: "blue-input", type: "number", min: "0", max: "255", value: rgb.b, onChange: e => handleRgbChange("b", Number(e.target.value)), "aria-label": "Blue channel value", "aria-valuemin": 0, "aria-valuemax": 255, "aria-valuenow": rgb.b, className: "glass-text-sm" })] })] }), showPresets && jsxs("div", { children: [jsx("label", { id: "palette-label", className: 'glass-text-sm text-primary/80 mb-3 block', children: "Palette" }), jsx("div", { role: "group", "aria-labelledby": "palette-label", className: "glass-grid glass-grid-cols-5 glass-gap-2", children: palette.map((color, i) => jsx("button", { className: cn("w-8 h-8 glass-radius-lg border-2 transition-all duration-200 glass-focus", currentColor === color ? "border-white scale-110" : "border-white/20 hover:border-white/40"), ref: el => { if (el) el.style.backgroundColor = color; }, onClick: e => handleColorChange(color), "aria-label": `Select color ${color}`, "aria-pressed": currentColor === color, title: color, children: currentColor === color && jsx(Check, { className: 'w-4 h-4 text-primary glass-mx-auto', "aria-hidden": "true" }) }, `${color}-${i}`)) })] }), showPresets && jsxs("div", { children: [jsx("label", { id: "material-palette-label", className: 'glass-text-sm text-primary/80 mb-3 block', children: "Material Colors" }), jsx("div", { role: "group", "aria-labelledby": "material-palette-label", className: "glass-grid glass-grid-cols-5 glass-gap-2", children: materialPalette.slice(0, 10).map((color, i) => jsx("button", { className: 'w-8 h-8 glass-radius-lg glass-border-2 glass-border-white/20 hover:border-white/40 transition-all duration-200 glass-focus', ref: el => { if (el) el.style.backgroundColor = color; }, onClick: e => handleColorChange(color), "aria-label": `Select color ${color}`, title: color }, `${color}-${i}`)) })] }), jsxs("div", { className: 'glass-flex glass-justify-between glass-gap-3 pt-4 glass-border-t glass-border-white/10', children: [jsxs(GlassButton, { variant: "ghost", size: "sm", onClick: e => handleColorChange(defaultValue), children: [jsx(RotateCcw, { className: 'w-4 h-4 glass-mr-2' }), "Reset"] }), jsxs("div", { className: "glass-flex glass-gap-2", children: [jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setIsOpen(false), children: "Cancel" }), jsx(GlassButton, { variant: "primary", size: "sm", onClick: e => setIsOpen(false), children: "Done" })] })] })] })] }) }) })] }); }; export { GlassColorPicker, GlassColorPicker as default }; //# sourceMappingURL=GlassColorPicker.js.map