UNPKG

aura-glass

Version:

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

453 lines (450 loc) 15.2 kB
'use client'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import React, { forwardRef, useState, useRef, useCallback, useEffect } from 'react'; import { FocusTrap } from '../../primitives/focus/FocusTrap.js'; 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 { LiquidGlassMaterial } from '../../primitives/LiquidGlassMaterial.js'; /** * GlassPopover component * A floating popover with glassmorphism styling */ const GlassPopover = /*#__PURE__*/forwardRef(({ open, onOpenChange, defaultOpen = false, content, children, placement = "bottom", material = "glass", materialProps, trigger: triggerType = "click", showDelay = 200, hideDelay = 200, showArrow = true, offset = 8, closeOnClickOutside = true, closeOnEscape = true, animation = "scale", zIndex = 9999, contentClassName, appearance = "glass", radialReveal = true, title, description, ...props }, ref) => { const [position, setPosition] = useState({ top: 0, left: 0 }); const [actualPlacement, setActualPlacement] = useState(placement); const triggerRef = useRef(null); const popoverRef = useRef(null); const showTimeoutRef = useRef(undefined); const hideTimeoutRef = useRef(undefined); // Support controlled and uncontrolled usage const isControlled = typeof open === "boolean"; const [internalOpen, setInternalOpen] = useState(defaultOpen); const isOpen = isControlled ? open : internalOpen; const setOpen = useCallback(value => { if (typeof onOpenChange === "function") { onOpenChange(value); } else { setInternalOpen(value); } }, [onOpenChange]); // Calculate position const calculatePosition = useCallback(() => { if (!triggerRef.current || !popoverRef.current) return; const triggerRect = triggerRef.current.getBoundingClientRect(); const popoverRect = popoverRef.current.getBoundingClientRect(); const viewport = { width: window.innerWidth, height: window.innerHeight }; let newPlacement = placement; let top = 0; let left = 0; // Calculate initial position based on placement switch (placement) { case "top": top = triggerRect.top - popoverRect.height - offset; left = triggerRect.left + (triggerRect.width - popoverRect.width) / 2; break; case "top-start": top = triggerRect.top - popoverRect.height - offset; left = triggerRect.left; break; case "top-end": top = triggerRect.top - popoverRect.height - offset; left = triggerRect.right - popoverRect.width; break; case "right": top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2; left = triggerRect.right + offset; break; case "right-start": top = triggerRect.top; left = triggerRect.right + offset; break; case "right-end": top = triggerRect.bottom - popoverRect.height; left = triggerRect.right + offset; break; case "bottom": top = triggerRect.bottom + offset; left = triggerRect.left + (triggerRect.width - popoverRect.width) / 2; break; case "bottom-start": top = triggerRect.bottom + offset; left = triggerRect.left; break; case "bottom-end": top = triggerRect.bottom + offset; left = triggerRect.right - popoverRect.width; break; case "left": top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2; left = triggerRect.left - popoverRect.width - offset; break; case "left-start": top = triggerRect.top; left = triggerRect.left - popoverRect.width - offset; break; case "left-end": top = triggerRect.bottom - popoverRect.height; left = triggerRect.left - popoverRect.width - offset; break; } // Flip if out of bounds if (top < 0 && placement.startsWith("top")) { newPlacement = placement.replace("top", "bottom"); top = triggerRect.bottom + offset; } else if (top + popoverRect.height > viewport.height && placement.startsWith("bottom")) { newPlacement = placement.replace("bottom", "top"); top = triggerRect.top - popoverRect.height - offset; } if (left < 0 && placement.startsWith("left")) { newPlacement = placement.replace("left", "right"); left = triggerRect.right + offset; } else if (left + popoverRect.width > viewport.width && placement.startsWith("right")) { newPlacement = placement.replace("right", "left"); left = triggerRect.left - popoverRect.width - offset; } // Adjust for viewport boundaries if (left < 0) left = 8; if (left + popoverRect.width > viewport.width) { left = viewport.width - popoverRect.width - 8; } if (top < 0) top = 8; if (top + popoverRect.height > viewport.height) { top = viewport.height - popoverRect.height - 8; } setPosition({ top, left }); setActualPlacement(newPlacement); }, [placement, offset]); // Handle trigger interactions const handleShow = useCallback(() => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = undefined; } if (triggerType === "hover") { showTimeoutRef.current = setTimeout(() => { setOpen(true); }, showDelay); } else { setOpen(true); } }, [triggerType, showDelay, setOpen]); const handleHide = useCallback(() => { if (showTimeoutRef.current) { clearTimeout(showTimeoutRef.current); showTimeoutRef.current = undefined; } if (triggerType === "hover") { hideTimeoutRef.current = setTimeout(() => { setOpen(false); }, hideDelay); } else { setOpen(false); } }, [triggerType, hideDelay, setOpen]); const handleClick = useCallback(() => { if (triggerType === "click") { setOpen(!isOpen); } }, [triggerType, isOpen, setOpen]); // Update position when open useEffect(() => { if (isOpen) { calculatePosition(); const handleResize = () => calculatePosition(); const handleScroll = () => calculatePosition(); window.addEventListener("resize", handleResize); window.addEventListener("scroll", handleScroll, true); return () => { window.removeEventListener("resize", handleResize); window.removeEventListener("scroll", handleScroll, true); }; } }, [isOpen, calculatePosition]); // Handle click outside useEffect(() => { if (!isOpen || !closeOnClickOutside) return; const handleClickOutside = event => { const target = event.target; if (triggerRef.current?.contains(target) || popoverRef.current?.contains(target)) { return; } setOpen(false); }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen, closeOnClickOutside, setOpen]); // Get arrow position const getArrowClasses = () => { const base = "absolute w-2 h-2 bg-background border border-border/20 rotate-45"; switch (actualPlacement) { case "top": case "top-start": case "top-end": return `${base} -bottom-1 border-t-0 border-l-0`; case "right": case "right-start": case "right-end": return `${base} -left-1 border-r-0 border-b-0`; case "bottom": case "bottom-start": case "bottom-end": return `${base} -top-1 border-b-0 border-r-0`; case "left": case "left-start": case "left-end": return `${base} -right-1 border-l-0 border-t-0`; default: return base; } }; const getArrowPosition = () => { switch (actualPlacement) { case "top": case "bottom": return { left: "50%", transform: "translateX(-50%)" }; case "top-start": case "bottom-start": return { left: "12px" }; case "top-end": case "bottom-end": return { right: "12px" }; case "left": case "right": return { top: "50%", transform: "translateY(-50%)" }; case "left-start": case "right-start": return { top: "12px" }; case "left-end": case "right-end": return { bottom: "12px" }; default: return {}; } }; const getAnimationPreset = () => { switch (animation) { case "fade": return "fadeIn"; case "scale": return "scaleIn"; case "slide": if (actualPlacement.startsWith("top")) return "slideUp"; if (actualPlacement.startsWith("bottom")) return "slideDown"; if (actualPlacement.startsWith("left")) return "slideLeft"; if (actualPlacement.startsWith("right")) return "slideRight"; return "slideDown"; default: return "scaleIn"; } }; // Clone trigger with event handlers const triggerElement = /*#__PURE__*/React.cloneElement(children, { ref: triggerRef, onClick: e => { children.props?.onClick?.(e); handleClick(); }, onMouseEnter: e => { children.props?.onMouseEnter?.(e); if (triggerType === "hover") handleShow(); }, onMouseLeave: e => { children.props?.onMouseLeave?.(e); if (triggerType === "hover") handleHide(); }, onFocus: e => { children.props?.onFocus?.(e); if (triggerType === "focus") handleShow(); }, onBlur: e => { children.props?.onBlur?.(e); if (triggerType === "focus") handleHide(); } }); return jsxs(Fragment, { children: [triggerElement, isOpen && jsx("div", { className: 'fixed inset-0 pointer-events-none', style: { zIndex }, children: jsx(MotionFramer, { preset: getAnimationPreset(), className: 'pointer-events-auto', style: { position: "absolute", top: position.top, left: position.left }, children: appearance === "glass" ? material === "liquid" ? jsxs(LiquidGlassMaterial, { ior: materialProps?.ior || 1.42, thickness: materialProps?.thickness || 6, tint: materialProps?.tint || { r: 0, g: 0, b: 0, a: 0.04 }, variant: materialProps?.variant || "clear", quality: materialProps?.quality || "balanced", environmentAdaptation: true, motionResponsive: false, ref: popoverRef, className: cn("relative max-w-xs liquid-glass-popover-surface", radialReveal && "glass-radial-reveal", contentClassName), style: { "--liquid-glass-popover-density": "0.8", "--liquid-glass-micro-refraction": "0.3" }, "data-liquid-glass-popover": "true", "data-popover-placement": placement, onMouseEnter: triggerType === "hover" ? handleShow : undefined, onMouseLeave: triggerType === "hover" ? handleHide : undefined, ...props, children: [jsx(FocusTrap, { active: isOpen && (triggerType === "focus" || triggerType === "click"), onEscape: closeOnEscape ? () => setOpen(false) : undefined, children: jsx("div", { className: "glass-p-4", children: content }) }), showArrow && jsx("div", { className: getArrowClasses(), style: getArrowPosition() })] }) : jsxs(OptimizedGlassCore, { intent: "neutral", elevation: "level4", intensity: "strong", depth: 2, tint: "neutral", border: "subtle", animation: "none", performanceMode: "medium", ref: popoverRef, className: cn("relative max-w-xs glass-lift glass-sheen", radialReveal && "glass-radial-reveal", "bg-background/95 glass-backdrop-blur-md", "border border-border/20 shadow-lg", contentClassName), onMouseEnter: triggerType === "hover" ? handleShow : undefined, onMouseLeave: triggerType === "hover" ? handleHide : undefined, ...props, children: [jsx(FocusTrap, { active: isOpen && (triggerType === "focus" || triggerType === "click"), onEscape: closeOnEscape ? () => setOpen(false) : undefined, children: jsxs("div", { className: "glass-p-3", children: [title && jsx("h3", { className: 'font-medium text-primary mb-1', children: title }), description && jsx("p", { className: 'glass-text-sm glass-text-secondary mb-2', children: description }), content] }) }), showArrow && jsx("div", { className: getArrowClasses(), style: getArrowPosition() })] }) : jsx("div", { ref: popoverRef, className: cn("relative max-w-xs glass-radius-xl bg-background text-foreground", "border border-border/30 shadow-2xl", contentClassName), onMouseEnter: triggerType === "hover" ? handleShow : undefined, onMouseLeave: triggerType === "hover" ? handleHide : undefined, children: jsx(FocusTrap, { active: isOpen && (triggerType === "focus" || triggerType === "click"), onEscape: closeOnEscape ? () => setOpen(false) : undefined, children: jsxs("div", { className: "glass-p-3", children: [title && jsx("h3", { className: 'font-medium mb-1', children: title }), description && jsx("p", { className: 'glass-text-sm glass-text-secondary mb-2', children: description }), content] }) }) }) }) })] }); }); GlassPopover.displayName = "GlassPopover"; const GlassTooltip = /*#__PURE__*/forwardRef(({ content, disabled = false, ...props }, ref) => { if (disabled) { return props?.children; } const [open, setOpen] = React.useState(false); return jsx(GlassPopover, { "data-glass-component": true, ref: ref, open: open, onOpenChange: setOpen, trigger: "hover", showDelay: 500, hideDelay: 0, placement: "top", radialReveal: false, content: jsx("span", { className: 'glass-text-sm text-primary', children: content }), contentClassName: "glass-px-2 glass-py-1", ...props }); }); GlassTooltip.displayName = "GlassTooltip"; export { GlassPopover, GlassTooltip }; //# sourceMappingURL=GlassPopover.js.map