UNPKG

aura-glass

Version:

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

474 lines (471 loc) 16.1 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useEffect, useCallback } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; import { useGlassSound } from '../../utils/soundDesign.js'; // Color conversion utilities const hsvToRgb = (h, s, v) => { h = h / 360; s = s / 100; v = v / 100; const i = Math.floor(h * 6); const f = h * 6 - i; const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s); let r, g, b; switch (i % 6) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; default: r = 0; g = 0; b = 0; break; } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; }; const rgbToHsv = (r, g, b) => { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const diff = max - min; let h = 0; let s = max === 0 ? 0 : diff / max * 100; let v = max * 100; if (diff !== 0) { switch (max) { case r: h = ((g - b) / diff + (g < b ? 6 : 0)) / 6; break; case g: h = ((b - r) / diff + 2) / 6; break; case b: h = ((r - g) / diff + 4) / 6; break; } } return { h: h * 360, s, v }; }; const hexToRgb = hex => { 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) } : null; }; const rgbToHex = (r, g, b) => { return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; }; const GlassColorWheel = /*#__PURE__*/forwardRef(({ value, defaultValue = "#ff0000", size = "md", disabled = false, showAlpha = false, alpha, defaultAlpha = 1, swatches = ["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "var(--glass-black)", "var(--glass-white)"], showSwatches = true, showInputs = true, inputFormat = "hex", onChange, onAlphaChange, respectMotionPreference = true, className, ...props }, ref) => { const { prefersReducedMotion, isMotionSafe } = useMotionPreferenceContext(); const { play } = useGlassSound(); const wheelRef = useRef(null); const saturationRef = useRef(null); const alphaRef = useRef(null); const colorWheelId = useA11yId("glass-color-wheel"); const [currentColor, setCurrentColor] = useState(value || defaultValue); const [currentAlpha, setCurrentAlpha] = useState(alpha ?? defaultAlpha); const [isDragging, setIsDragging] = useState(null); // Parse initial color const initialRgb = hexToRgb(currentColor) || { r: 255, g: 0, b: 0 }; const [hsv, setHsv] = useState(rgbToHsv(initialRgb.r, initialRgb.g, initialRgb.b)); const sizeConfig = { sm: { wheel: 120, saturation: 120, alpha: 120, thumb: 12, container: "w-40", input: "glass-text-xs h-6" }, md: { wheel: 160, saturation: 160, alpha: 160, thumb: 16, container: "w-52", input: "glass-text-sm h-8" }, lg: { wheel: 200, saturation: 200, alpha: 200, thumb: 20, container: "w-64", input: "glass-text-sm h-10" }, xl: { wheel: 240, saturation: 240, alpha: 240, thumb: 24, container: "w-80", input: "glass-text-base h-10" } }; const config = sizeConfig[size]; // Draw color wheel useEffect(() => { const canvas = wheelRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = Math.min(centerX, centerY) - 10; ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw color wheel for (let angle = 0; angle < 360; angle++) { const startAngle = (angle - 2) * Math.PI / 180; const endAngle = angle * Math.PI / 180; ctx.beginPath(); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.arc(centerX, centerY, radius * 0.6, endAngle, startAngle, true); ctx.closePath(); const gradient = ctx.createRadialGradient(centerX, centerY, radius * 0.6, centerX, centerY, radius); gradient.addColorStop(0, `hsl(${angle}, 100%, 50%)`); gradient.addColorStop(1, `hsl(${angle}, 100%, 50%)`); ctx.fillStyle = gradient; ctx.fill(); } // Draw inner circle (saturation/brightness selector area) ctx.beginPath(); ctx.arc(centerX, centerY, radius * 0.6, 0, 2 * Math.PI); ctx.fillStyle = "var(--glass-white)"; ctx.fill(); ctx.strokeStyle = "#dddddd"; ctx.stroke(); }, [config.wheel]); // Draw saturation/brightness area useEffect(() => { const canvas = saturationRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); // Create base color from hue hsvToRgb(hsv.h, 100, 100); // Draw saturation/brightness gradient const imageData = ctx.createImageData(width, height); const data = imageData.data; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const saturation = x / width * 100; const brightness = (height - y) / height * 100; const rgb = hsvToRgb(hsv.h, saturation, brightness); const index = (y * width + x) * 4; data[index] = rgb.r; // Red data[index + 1] = rgb.g; // Green data[index + 2] = rgb.b; // Blue data[index + 3] = 255; // Alpha } } ctx.putImageData(imageData, 0, 0); }, [hsv.h, config.saturation]); // Draw alpha slider useEffect(() => { if (!showAlpha) return; const canvas = alphaRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); // Draw checkerboard background const checkSize = 8; for (let y = 0; y < height; y += checkSize) { for (let x = 0; x < width; x += checkSize) { const isEven = (Math.floor(x / checkSize) + Math.floor(y / checkSize)) % 2 === 0; ctx.fillStyle = isEven ? "var(--glass-white)" : "#cccccc"; ctx.fillRect(x, y, checkSize, checkSize); } } // Draw alpha gradient const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v); const gradient = ctx.createLinearGradient(0, 0, width, 0); gradient.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`); gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); }, [hsv, currentAlpha, showAlpha, config.alpha]); const updateColor = useCallback((newHsv, newAlpha) => { setHsv(newHsv); const rgb = hsvToRgb(newHsv.h, newHsv.s, newHsv.v); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); setCurrentColor(hex); if (newAlpha !== undefined) { setCurrentAlpha(newAlpha); } onChange?.(hex, newHsv, rgb, newAlpha ?? currentAlpha); play("tap"); }, [onChange, currentAlpha, play]); // Handle color wheel interaction const handleWheelInteraction = useCallback(event => { if (disabled) return; const canvas = wheelRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const centerX = canvas.width / 2; const centerY = canvas.height / 2; const x = event.clientX - rect.left - centerX; const y = event.clientY - rect.top - centerY; const distance = Math.sqrt(x * x + y * y); const maxRadius = Math.min(centerX, centerY) - 10; const minRadius = maxRadius * 0.6; // Check if click is in the wheel ring if (distance >= minRadius && distance <= maxRadius) { let angle = Math.atan2(y, x) * 180 / Math.PI; if (angle < 0) angle += 360; updateColor({ ...hsv, h: angle }); } }, [disabled, hsv, updateColor]); // Handle saturation/brightness area interaction const handleSaturationInteraction = useCallback(event => { if (disabled) return; const canvas = saturationRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const saturation = Math.max(0, Math.min(100, x / canvas.width * 100)); const brightness = Math.max(0, Math.min(100, (canvas.height - y) / canvas.height * 100)); updateColor({ ...hsv, s: saturation, v: brightness }); }, [disabled, hsv, updateColor]); // Handle alpha slider interaction const handleAlphaInteraction = useCallback(event => { if (disabled || !showAlpha) return; const canvas = alphaRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const newAlpha = Math.max(0, Math.min(1, x / canvas.width)); setCurrentAlpha(newAlpha); onAlphaChange?.(newAlpha); const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); onChange?.(hex, hsv, rgb, newAlpha); }, [disabled, showAlpha, hsv, onChange, onAlphaChange]); // Handle swatch selection const handleSwatchClick = useCallback(color => { if (disabled) return; const rgb = hexToRgb(color); if (rgb) { const newHsv = rgbToHsv(rgb.r, rgb.g, rgb.b); updateColor(newHsv); } }, [disabled, updateColor]); // Format color value for input const formatColorValue = useCallback(() => { const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v); switch (inputFormat) { case "rgb": return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; case "hsl": // Convert HSV to HSL (simplified) const hsl = { h: hsv.h, s: hsv.s, l: hsv.v }; return `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)`; case "hsv": return `hsv(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%)`; default: return currentColor; } }, [hsv, currentColor, inputFormat]); return jsx(OptimizedGlassCore, { ref: ref, id: colorWheelId, elevation: "level2", intensity: "medium", depth: 2, tint: "neutral", border: "subtle", className: cn("glass-color-wheel glass-p-4 glass-radius-lg glass-backdrop-blur-md border border-border/20", config.container, disabled && "opacity-50 pointer-events-none", className), ...props, children: jsxs(MotionFramer, { preset: isMotionSafe && respectMotionPreference ? "fadeIn" : "none", className: "glass-gap-4", children: [jsxs("div", { className: 'relative glass-flex glass-justify-center', children: [jsx("canvas", { ref: wheelRef, width: config.wheel, height: config.wheel, className: 'cursor-crosshair glass-radius-full', onClick: handleWheelInteraction, onMouseDown: () => setIsDragging("wheel"), onMouseMove: e => isDragging === "wheel" && handleWheelInteraction(e), onMouseUp: () => setIsDragging(null) }), jsx("div", { className: 'absolute w-4 h-4 glass-border-2 glass-border-white glass-radius-full glass-shadow-lg pointer-events-none', style: { left: "50%", top: "50%", transform: `translate(-50%, -50%) translate(${Math.cos(hsv.h * Math.PI / 180) * (config.wheel / 2 - 20)}px, ${Math.sin(hsv.h * Math.PI / 180) * (config.wheel / 2 - 20)}px)` } })] }), jsxs("div", { className: 'relative', children: [jsx("canvas", { ref: saturationRef, width: config.saturation, height: config.saturation * 0.6, className: 'glass-w-full cursor-crosshair glass-radius-md glass-border glass-border-glass-border/20', onClick: handleSaturationInteraction, onMouseDown: () => setIsDragging("saturation"), onMouseMove: e => isDragging === "saturation" && handleSaturationInteraction(e), onMouseUp: () => setIsDragging(null) }), jsx("div", { className: 'absolute w-3 h-3 glass-border-2 glass-border-white glass-radius-full glass-shadow-lg pointer-events-none', style: { left: `${hsv.s}%`, top: `${100 - hsv.v}%`, transform: "translate(-50%, -50%)" } })] }), showAlpha && jsxs("div", { className: 'relative', children: [jsx("canvas", { ref: alphaRef, width: config.alpha, height: 20, className: 'glass-w-full cursor-crosshair glass-radius-md glass-border glass-border-glass-border/20', onClick: handleAlphaInteraction, onMouseDown: () => setIsDragging("alpha"), onMouseMove: e => isDragging === "alpha" && handleAlphaInteraction(e), onMouseUp: () => setIsDragging(null) }), jsx("div", { className: 'absolute w-3 h-6 glass-border-2 glass-border-white glass-radius-md glass-shadow-lg pointer-events-none', style: { left: `${currentAlpha * 100}%`, top: "50%", transform: "translate(-50%, -50%)" } })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("div", { className: 'w-12 h-8 glass-radius-md glass-border glass-border-glass-border/20 glass-shadow-inner', style: { backgroundColor: showAlpha ? `${currentColor}${Math.round(currentAlpha * 255).toString(16).padStart(2, "0")}` : currentColor } }), showInputs && jsx("input", { type: "text", value: formatColorValue(), onChange: e => { const newColor = e.target.value; if (inputFormat === "hex" && /^#[0-9A-F]{6}$/i.test(newColor)) { const rgb = hexToRgb(newColor); if (rgb) { const newHsv = rgbToHsv(rgb.r, rgb.g, rgb.b); updateColor(newHsv); } } }, className: cn("flex-1 glass-px-2 glass-radius-md border border-border/20 bg-background/50", "focus:outline-none focus:ring-2 focus:ring-primary/50", "glass-focus glass-touch-target glass-contrast-guard", config.input) })] }), showSwatches && swatches.length > 0 && jsx("div", { className: "glass-grid glass-grid-cols-8 glass-gap-1", children: swatches.map((swatch, index) => jsx(MotionFramer, { preset: isMotionSafe && respectMotionPreference ? "scaleIn" : "none", delay: index * 20, onClick: () => handleSwatchClick(swatch), className: cn("w-6 h-6 glass-radius-md border border-border/20 cursor-pointer", "hover:scale-110 active:scale-95 transition-transform", currentColor.toLowerCase() === swatch.toLowerCase() && "ring-2 ring-primary"), style: { backgroundColor: swatch } }, index)) })] }) }); }); GlassColorWheel.displayName = "GlassColorWheel"; export { GlassColorWheel, GlassColorWheel as default }; //# sourceMappingURL=GlassColorWheel.js.map