UNPKG

aura-glass

Version:

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

286 lines (283 loc) 9.11 kB
'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