aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
286 lines (283 loc) • 9.11 kB
JavaScript
'use client';
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import React, { 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';
/**
* GlassContextMenu component
* A glassmorphism context menu that appears on right-click
*/
const GlassContextMenu = ({
items = [],
children,
trigger,
open: controlledOpen,
onOpenChange,
align = "start",
side = "bottom",
className,
menuClassName,
...rest
}) => {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({
x: 0,
y: 0
});
const triggerRef = useRef(null);
const open = controlledOpen !== undefined ? controlledOpen : isOpen;
const setOpen = onOpenChange || setIsOpen;
// Handle right-click
const handleContextMenu = e => {
e.preventDefault();
e.stopPropagation();
const rect = triggerRef.current?.getBoundingClientRect();
if (rect) {
let x = e.clientX;
let y = e.clientY;
// Adjust position based on alignment and side
if (align === "end") x = rect.right;else if (align === "center") x = rect.left + rect.width / 2;
if (side === "top") y = rect.top;else if (side === "bottom") y = rect.bottom;else if (side === "left") x = rect.left;else if (side === "right") x = rect.right;
setPosition({
x,
y
});
setOpen(true);
}
};
// Handle outside click
useEffect(() => {
const handleClickOutside = () => {
if (open) setOpen(false);
};
if (open) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [open, setOpen]);
// Handle escape key
useEffect(() => {
const handleEscape = e => {
if (e.key === "Escape" && open) {
setOpen(false);
}
};
if (open) {
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}
}, [open, setOpen]);
return jsxs(Fragment, {
children: [jsx("div", {
ref: triggerRef,
onContextMenu: handleContextMenu,
className: cn("cursor-context-menu", className),
...rest,
children: trigger || children
}), open && jsx(GlassContextMenuContent, {
items: items,
position: position,
onClose: () => setOpen(false),
className: menuClassName ?? className
})]
});
};
/**
* GlassContextMenuContent component
* The actual menu content with positioning
*/
const GlassContextMenuContent = ({
items,
position,
onClose,
maxWidth = "200px",
className
}) => {
const menuRef = useRef(null);
const [submenuOpen, setSubmenuOpen] = useState(null);
// Adjust position to keep menu in viewport
useEffect(() => {
if (menuRef.current) {
const menu = menuRef.current;
const rect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let newX = position.x;
let newY = position.y;
// Adjust horizontal position
if (newX + rect.width > viewportWidth) {
newX = viewportWidth - rect.width - 10;
}
// Adjust vertical position
if (newY + rect.height > viewportHeight) {
newY = viewportHeight - rect.height - 10;
}
if (newX !== position.x || newY !== position.y) {
menu.style.left = `${newX}px`;
menu.style.top = `${newY}px`;
}
}
}, [position]);
return jsx(MotionFramer, {
preset: "scaleIn",
className: 'fixed z-[9999]',
children: jsx(OptimizedGlassCore, {
intent: "neutral",
elevation: "level4",
intensity: "strong",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
ref: menuRef,
liftOnHover: true,
hoverSheen: true,
role: "menu",
"aria-label": "Context menu",
"aria-orientation": "vertical",
className: cn("glass-backdrop-blur-md bg-black/20 border border-white/20 shadow-2xl", "min-w-48 max-w-xs overflow-hidden glass-radial-reveal glass-lift", className),
style: {
left: position.x,
top: position.y,
maxWidth
},
children: jsx("div", {
className: "glass-py-1",
children: items.map((item, index) => jsxs(React.Fragment, {
children: [item?.separator && index > 0 && jsx("div", {
className: 'h-px glass-surface-subtle/20 glass-mx-2 glass-my-1',
role: "separator"
}), jsx(GlassContextMenuItem, {
item: item,
onClick: clickedItem => {
if (clickedItem.children) {
setSubmenuOpen(submenuOpen === clickedItem.id ? null : clickedItem.id);
} else {
clickedItem.action?.();
clickedItem.onClick?.();
onClose();
}
},
onClose: onClose,
isSubmenu: false
}), item?.children && submenuOpen === item?.id && jsx("div", {
className: 'absolute left-full top-0 glass-ml-1',
children: jsx(GlassContextMenuContent, {
items: item?.children,
position: {
x: 0,
y: 0
},
onClose: () => setSubmenuOpen(null)
})
})]
}, item?.id))
})
})
});
};
/**
* GlassContextMenuItem component
* Individual menu item
*/
const GlassContextMenuItem = ({
item,
isSelected = false,
onClick,
onClose,
isSubmenu = false
}) => {
const [isHovered, setIsHovered] = useState(false);
const itemRef = useRef(null);
if (item?.separator) {
return jsx("div", {
className: 'h-px glass-surface-subtle/20 glass-mx-2 glass-my-1',
role: "separator"
});
}
const handleKeyDown = e => {
const menuContainer = itemRef.current?.closest('[role="menu"]');
if (!menuContainer) return;
const menuItems = Array.from(menuContainer.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])'));
const currentIndex = menuItems.indexOf(itemRef.current);
switch (e.key) {
case "ArrowDown":
e.preventDefault();
const nextItem = menuItems[currentIndex + 1] || menuItems[0];
nextItem?.focus();
break;
case "ArrowUp":
e.preventDefault();
const prevItem = menuItems[currentIndex - 1] || menuItems[menuItems.length - 1];
prevItem?.focus();
break;
case "ArrowRight":
if (item?.children) {
e.preventDefault();
onClick(item);
}
break;
case "ArrowLeft":
if (isSubmenu) {
e.preventDefault();
onClose();
}
break;
case "Enter":
case " ":
e.preventDefault();
if (!item?.disabled) onClick(item);
break;
case "Escape":
e.preventDefault();
onClose();
break;
case "Home":
e.preventDefault();
menuItems[0]?.focus();
break;
case "End":
e.preventDefault();
menuItems[menuItems.length - 1]?.focus();
break;
}
};
return jsxs("div", {
ref: itemRef,
role: "menuitem",
"aria-disabled": item?.disabled,
"aria-haspopup": item?.children ? "menu" : undefined,
tabIndex: item?.disabled ? -1 : 0,
className: cn("relative flex items-center glass-px-3 glass-py-2 cursor-pointer transition-all duration-200", "glass-text-primary/90 hover:glass-text-primary glass-radius-lg", "hover:bg-white/10 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/10", "glass-press", {
"bg-white/20 glass-text-primary": isSelected,
"opacity-50 cursor-not-allowed": item?.disabled,
"text-red-400 hover:text-red-300": item?.destructive
}),
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
onClick: e => !item?.disabled && onClick(item),
onKeyDown: handleKeyDown,
children: [item?.icon && jsx("div", {
className: 'glass-flex glass-items-center glass-justify-center w-4 h-4 mr-3',
children: item?.icon
}), jsx("span", {
className: 'glass-flex-1 glass-text-sm font-medium truncate',
children: item?.label
}), item?.shortcut && jsx("span", {
className: 'ml-6 glass-text-xs text-primary/50 font-mono',
children: item?.shortcut
}), item?.children && jsx("div", {
className: 'ml-3 text-primary/50',
"aria-hidden": "true",
children: "\u25B6"
})]
});
};
export { GlassContextMenu, GlassContextMenuContent, GlassContextMenuItem };
//# sourceMappingURL=GlassContextMenu.js.map