UNPKG

aura-glass

Version:

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

241 lines (238 loc) 8.17 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useEffect } 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 { useA11yId, announceToScreenReader } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; /** * GlassHoverCard component * A glassmorphism hover card that appears on hover */ const GlassHoverCard = /*#__PURE__*/forwardRef(({ content, children, placement = "top", align = "center", offset = 8, showDelay = 300, hideDelay = 100, open: controlledOpen, onOpenChange, className, maxWidth = "320px", triggerClassName, title, description, respectMotionPreference = true, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...props }, ref) => { const [isOpen, setIsOpen] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const triggerRef = useRef(null); const contentRef = useRef(null); const showTimeoutRef = useRef(); const hideTimeoutRef = useRef(); const { prefersReducedMotion } = useMotionPreferenceContext(); const open = controlledOpen !== undefined ? controlledOpen : isOpen; const setOpen = onOpenChange || setIsOpen; // Generate unique IDs for accessibility const hoverCardId = useA11yId("glass-hover-card"); const titleId = title ? useA11yId("glass-hover-card-title") : undefined; const descriptionId = description ? useA11yId("glass-hover-card-desc") : undefined; const shouldAnimate = respectMotionPreference ? !prefersReducedMotion : true; // Calculate position based on placement and alignment const calculatePosition = () => { if (!triggerRef.current || !contentRef.current) return; const triggerRect = triggerRef.current.getBoundingClientRect(); const contentRect = contentRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let x = 0; let y = 0; // Horizontal positioning switch (placement) { case "left": case "left-start": case "left-end": x = triggerRect.left - contentRect.width - offset; break; case "right": case "right-start": case "right-end": x = triggerRect.right + offset; break; default: // top, bottom, top-start, top-end, bottom-start, bottom-end x = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2; } // Vertical positioning switch (placement) { case "top": case "top-start": case "top-end": y = triggerRect.top - contentRect.height - offset; break; case "bottom": case "bottom-start": case "bottom-end": y = triggerRect.bottom + offset; break; default: // left, right, left-start, left-end, right-start, right-end y = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2; } // Alignment adjustments switch (placement) { case "top-start": case "bottom-start": x = triggerRect.left; break; case "top-end": case "bottom-end": x = triggerRect.right - contentRect.width; break; case "left-start": case "right-start": y = triggerRect.top; break; case "left-end": case "right-end": y = triggerRect.bottom - contentRect.height; break; } // Keep within viewport bounds x = Math.max(10, Math.min(x, viewportWidth - contentRect.width - 10)); y = Math.max(10, Math.min(y, viewportHeight - contentRect.height - 10)); setPosition({ x, y }); }; // Handle mouse enter const handleMouseEnter = () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = undefined; } if (!open) { showTimeoutRef.current = setTimeout(() => { setOpen(true); }, showDelay); } }; // Handle mouse leave const handleMouseLeave = () => { if (showTimeoutRef.current) { clearTimeout(showTimeoutRef.current); showTimeoutRef.current = undefined; } if (open) { hideTimeoutRef.current = setTimeout(() => { setOpen(false); }, hideDelay); } }; // Update position when content opens useEffect(() => { if (open) { // Small delay to ensure content is rendered setTimeout(calculatePosition, 0); // Announce to screen readers when opened announceToScreenReader(`Hover card opened: ${title || ariaLabel || "Hover card"}`, "polite"); } }, [open, placement, align, offset, title, ariaLabel]); // Recalculate position on window resize useEffect(() => { if (!open) return; const handleResize = () => calculatePosition(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [open]); return jsxs("div", { className: 'relative inline-block', children: [jsx("div", { ref: triggerRef, className: triggerClassName, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: children }), open && jsx(MotionFramer, { preset: "fadeIn", duration: shouldAnimate ? 200 : 0, className: 'fixed z-[9999]', style: { left: position.x, top: position.y, maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth }, children: jsxs("div", { ref: contentRef, className: cn("relative", className), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, role: "tooltip", id: hoverCardId, "aria-labelledby": ariaLabelledBy || titleId, "aria-describedby": ariaDescribedBy || descriptionId, "aria-label": !ariaLabelledBy && !titleId ? ariaLabel : undefined, ...props, children: [jsx(OptimizedGlassCore, { ref: ref, intent: "neutral", elevation: "level3", intensity: "strong", depth: 2, tint: "neutral", border: "subtle", animation: "none", performanceMode: "medium", liftOnHover: true, hoverSheen: true, className: "glass-glass-backdrop-blur-md glass-surface-dark/20 glass-border glass-border-white/20 glass-shadow-2xl glass-radial-reveal glass-contrast-guard", children: jsxs("div", { className: "glass-p-4", children: [(title || description) && jsxs("div", { className: 'mb-2', children: [title && jsx("h3", { id: titleId, className: 'font-medium text-primary glass-text-sm mb-1', children: title }), description && jsx("p", { id: descriptionId, className: "glass-text-xs glass-text-secondary", children: description })] }), content] }) }), jsx("div", { className: cn("absolute w-3 h-3 bg-black/20 border border-white/20 glass-backdrop-blur-md", "rotate-45", { "-bottom-2 left-1/2 -translate-x-1/2": placement.startsWith("top"), "-top-2 left-1/2 -translate-x-1/2": placement.startsWith("bottom"), "-right-2 top-1/2 -translate-y-1/2": placement.startsWith("left"), "-left-2 top-1/2 -translate-y-1/2": placement.startsWith("right") }), style: { clipPath: "polygon(0 0, 100% 100%, 0 100%)" } })] }) })] }); }); GlassHoverCard.displayName = "GlassHoverCard"; export { GlassHoverCard }; //# sourceMappingURL=GlassHoverCard.js.map