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