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