aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
559 lines (556 loc) • 21.7 kB
JavaScript
'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