UNPKG

aura-glass

Version:

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

614 lines (611 loc) 24.6 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useEffect, useCallback } 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 { FocusTrap } from '../../primitives/focus/FocusTrap.js'; import { LiquidGlassMaterial } from '../../primitives/LiquidGlassMaterial.js'; import { useA11yId, focusUtils, announceToScreenReader, createModalA11y } from '../../utils/a11y.js'; import { GlassButton, IconButton } from '../button/GlassButton.js'; export { GlassDialog } from './GlassDialog.js'; export { GlassDrawer } from './GlassDrawer.js'; /** * GlassModal component * A versatile modal with glassmorphism styling */ const GlassModal = /*#__PURE__*/forwardRef(({ open = true, onClose = () => {}, title, description, size = "md", variant = "default", material = "glass", materialProps, closeOnBackdropClick = true, closeOnEscape = true, closeButton, showCloseButton = true, footer, children = null, backdrop, backdropBlur = "md", animation = "scale", lockScroll = true, zIndex = 50, className, role = "dialog", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, initialFocus, restoreFocus, contentClassName, // Consciousness features consciousness, predictive, adaptive, eyeTracking, spatialAudio, trackAchievements, ...props }, ref) => { const [mounted, setMounted] = useState(false); const contentRef = useRef(null); const previousActiveElement = useRef(null); // Consciousness state const [interactionCount, setInteractionCount] = useState(0); const [modalFocusTime, setModalFocusTime] = useState(0); const [contentEngagement, setContentEngagement] = useState({ scrollDepth: 0, timeSpent: 0, interactions: 0 }); const [modalInsights, setModalInsights] = useState(null); // Consciousness hooks (mock implementations) const predictiveEngine = predictive ? { analyzeModalEngagement: async context => ({ urgency: "low", complexity: 0.5, userStress: 0.3 }) } : null; const eyeTracker = eyeTracking ? { startTracking: handler => {}, stopTracking: () => {} } : null; const biometricAdapter = adaptive ? { getCurrentBiometrics: () => ({ stressLevel: 0.3 }) } : null; const spatialAudioEngine = spatialAudio ? { playSound: (sound, config) => {} } : null; const interactionRecorder = consciousness ? { recordInteraction: (type, data) => {} } : null; const achievementTracker = trackAchievements ? { recordInteraction: (type, data) => {} } : null; // Generate unique IDs for accessibility const modalId = useA11yId("glass-modal"); const titleId = title ? useA11yId("glass-modal-title") : undefined; const descriptionId = description ? useA11yId("glass-modal-desc") : undefined; // Create accessibility attributes const baseA11yProps = createModalA11y({ id: modalId, titleId: ariaLabelledBy || titleId, descriptionId: ariaDescribedBy || descriptionId, modal: true }); // Create final a11y props with role override const a11yProps = { ...baseA11yProps, ...(role && { role }), ...(ariaLabel && !ariaLabelledBy && !titleId && { "aria-label": ariaLabel }) }; useEffect(() => { setMounted(true); }, []); // Focus management useEffect(() => { if (!mounted) return; if (open) { // Store the previously focused element previousActiveElement.current = document.activeElement; // Focus management after modal renders setTimeout(() => { if (initialFocus?.current) { initialFocus.current.focus(); } else if (contentRef.current) { // Focus first focusable element or the modal itself focusUtils.focusFirst(contentRef.current); if (document.activeElement === document.body) { contentRef.current.focus(); } } }, 100); // Announce modal opening to screen readers if (role === "alertdialog") { announceToScreenReader(`Alert dialog opened: ${title || ariaLabel || "Dialog"}`, "assertive"); } else { announceToScreenReader(`Dialog opened: ${title || ariaLabel || "Dialog"}`, "polite"); } } else if (previousActiveElement.current && mounted) { // Restore focus when modal closes if (restoreFocus?.current) { restoreFocus.current.focus(); } else { previousActiveElement.current.focus(); } } }, [open, mounted, title, ariaLabel, role, initialFocus, restoreFocus]); useEffect(() => { if (!mounted) return; if (open && lockScroll) { const scrollY = window.scrollY; const body = document.body; body.style.position = "fixed"; body.style.top = `-${scrollY}px`; body.style.width = "100%"; body.style.overflow = "hidden"; return () => { body.style.position = ""; body.style.top = ""; body.style.width = ""; body.style.overflow = ""; window.scrollTo(0, scrollY); }; } }, [open, lockScroll, mounted]); // Consciousness effects // Modal opening/closing tracking with spatial audio useEffect(() => { if (!mounted) return; if (open) { const openTime = Date.now(); setModalFocusTime(openTime); setInteractionCount(prev => prev + 1); // Record modal opening interaction if (consciousness && interactionRecorder) { interactionRecorder.recordInteraction("modal_open", { title: title || "Untitled Modal", size, variant, role, timestamp: openTime }); } // Track achievement for modal interaction if (trackAchievements && achievementTracker) { achievementTracker.recordInteraction("modal_opened", { modalType: role, title: title || "Modal", timestamp: openTime }); } // Play spatial audio for modal opening if (spatialAudio && spatialAudioEngine) { const audioConfig = role === "alertdialog" ? { sound: "alert-modal-open", volume: 0.7, position: "center", reverb: 0.4 } : { sound: "modal-open", volume: 0.5, position: "center", reverb: 0.3 }; spatialAudioEngine.playSound(audioConfig.sound, audioConfig); } } else if (modalFocusTime > 0) { const closeTime = Date.now(); const timeSpent = closeTime - modalFocusTime; // Record modal closing interaction if (consciousness && interactionRecorder) { interactionRecorder.recordInteraction("modal_close", { title: title || "Untitled Modal", timeSpent, interactions: contentEngagement.interactions, scrollDepth: contentEngagement.scrollDepth, timestamp: closeTime }); } // Update content engagement setContentEngagement(prev => ({ ...prev, timeSpent })); // Play spatial audio for modal closing if (spatialAudio && spatialAudioEngine) { spatialAudioEngine.playSound("modal-close", { volume: 0.4, position: "center", reverb: 0.2 }); } } }, [open, mounted, consciousness, interactionRecorder, trackAchievements, achievementTracker, spatialAudio, spatialAudioEngine, title, size, variant, role, modalFocusTime, contentEngagement]); // Eye tracking for modal engagement useEffect(() => { if (!eyeTracking || !eyeTracker || !open) return; const handleGazeData = gazeData => { // Track if user is looking at modal content if (contentRef.current) { const rect = contentRef.current.getBoundingClientRect(); const isLookingAtModal = gazeData.x >= rect.left && gazeData.x <= rect.right && gazeData.y >= rect.top && gazeData.y <= rect.bottom; if (isLookingAtModal && trackAchievements && achievementTracker) { achievementTracker.recordInteraction("modal_gaze_engagement", { modalTitle: title, gazeTime: Date.now(), modalArea: "content" }); } } }; eyeTracker.startTracking(handleGazeData); return () => eyeTracker.stopTracking(); }, [eyeTracking, eyeTracker, open, trackAchievements, achievementTracker, title]); // Biometric adaptation for modal behavior useEffect(() => { if (!adaptive || !biometricAdapter || !open) return; const updateAdaptiveFeatures = () => { const biometrics = biometricAdapter.getCurrentBiometrics(); // Analyze modal content complexity and user stress setModalInsights({ urgency: role === "alertdialog" ? "high" : biometrics.stressLevel > 0.7 ? "medium" : "low", complexity: (children?.toString().length || 0) > 500 ? 0.8 : 0.4, userStress: biometrics.stressLevel }); // Record biometric adaptation for achievements if (trackAchievements && achievementTracker) { achievementTracker.recordInteraction("modal_biometric_adaptation", { stressLevel: biometrics.stressLevel, modalComplexity: (children?.toString().length || 0) > 500 ? "high" : "low", adaptations: { urgencyLevel: role === "alertdialog" ? "high" : "normal" } }); } }; const interval = setInterval(updateAdaptiveFeatures, 3000); updateAdaptiveFeatures(); // Run immediately return () => clearInterval(interval); }, [adaptive, biometricAdapter, open, role, children, trackAchievements, achievementTracker]); // Predictive modal insights useEffect(() => { if (!predictive || !predictiveEngine || !open) return; const generateModalInsights = async () => { try { const modalContext = { title: title || "Modal", role, size, variant, hasFooter: !!footer, contentLength: children?.toString().length || 0, interactionCount, timeSpent: modalFocusTime ? Date.now() - modalFocusTime : 0 }; const insights = await predictiveEngine.analyzeModalEngagement(modalContext); setModalInsights(prevInsights => ({ urgency: insights.urgency || prevInsights?.urgency || "low", complexity: insights.complexity || prevInsights?.complexity || 0.5, userStress: insights.userStress || prevInsights?.userStress || 0.3 })); } catch (error) { console.warn("Predictive modal analysis failed:", error); } }; const timeoutId = setTimeout(generateModalInsights, 1000); return () => clearTimeout(timeoutId); }, [predictive, predictiveEngine, open, title, role, size, variant, footer, children, interactionCount, modalFocusTime]); // Enhanced handlers with consciousness tracking const handleBackdropClick = useCallback(e => { if (closeOnBackdropClick && e.target === e.currentTarget) { // Record backdrop click interaction if (consciousness && interactionRecorder) { interactionRecorder.recordInteraction("modal_backdrop_click", { modalTitle: title, timestamp: Date.now() }); } // Play spatial audio for backdrop close if (spatialAudio && spatialAudioEngine) { spatialAudioEngine.playSound("modal-backdrop-close", { volume: 0.3, position: "left", reverb: 0.1 }); } onClose(); } }, [closeOnBackdropClick, onClose, consciousness, interactionRecorder, title, spatialAudio, spatialAudioEngine]); const handleEscapeKey = useCallback(() => { if (closeOnEscape) { // Record escape key close if (consciousness && interactionRecorder) { interactionRecorder.recordInteraction("modal_escape_close", { modalTitle: title, timestamp: Date.now() }); } // Play spatial audio for escape close if (spatialAudio && spatialAudioEngine) { spatialAudioEngine.playSound("modal-escape-close", { volume: 0.4, position: "right", reverb: 0.2 }); } onClose(); } }, [closeOnEscape, onClose, consciousness, interactionRecorder, title, spatialAudio, spatialAudioEngine]); const sizeClasses = { xs: "max-w-xs", sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", "2xl": "max-w-2xl", full: "max-w-full glass-mx-4" }; const variantClasses = { default: "items-center justify-center glass-p-4", centered: "items-center justify-center glass-p-4", drawer: "items-end justify-center pb-0", fullscreen: "items-center justify-center glass-p-0" }; const backdropBlurClasses = { none: "", sm: "glass-backdrop-blur-md", md: "glass-backdrop-blur-md", lg: "glass-backdrop-blur-md" }; const getAnimationPreset = () => { switch (animation) { case "fade": return "fadeIn"; case "scale": return "scaleIn"; case "slide": return variant === "drawer" ? "slideUp" : "slideDown"; case "flip": return "scaleIn"; default: return "scaleIn"; } }; if (!mounted || !open) { return null; } return jsxs("div", { "data-glass-component": true, className: cn("fixed inset-0 flex", variantClasses[variant], backdropBlurClasses[backdropBlur], consciousness && "consciousness-modal-container", adaptive && modalInsights?.urgency === "high" && "consciousness-urgent-modal", eyeTracking && "consciousness-eye-trackable"), style: { zIndex }, "data-consciousness-modal": "true", "data-consciousness-active": String(!!consciousness), "data-modal-title": title, "data-modal-role": role, "data-modal-urgency": modalInsights?.urgency, "data-user-stress": modalInsights?.userStress, "data-interaction-count": interactionCount, children: [backdrop || jsx(MotionFramer, { preset: "fadeIn", className: 'absolute inset-0 glass-surface-dark/50', onClick: handleBackdropClick }), jsx(MotionFramer, { preset: getAnimationPreset(), className: cn("relative w-full", sizeClasses[size], variant === "fullscreen" ? "h-full" : "max-h-full", consciousness && "consciousness-modal-content", predictive && modalInsights && "consciousness-predictive-modal", adaptive && modalInsights?.urgency === "high" && "consciousness-urgent-content"), "data-consciousness-content": "true", "data-modal-complexity": modalInsights?.complexity, "data-time-spent": modalFocusTime ? Date.now() - modalFocusTime : 0, children: jsx(FocusTrap, { active: open, onEscape: handleEscapeKey, lockScroll: false, children: material === "liquid" ? jsxs(LiquidGlassMaterial, { ior: materialProps?.ior || 1.52, thickness: materialProps?.thickness || 12, tint: materialProps?.tint || { r: 0, g: 0, b: 0, a: 0.1 }, variant: materialProps?.variant || "regular", quality: materialProps?.quality || "high", environmentAdaptation: true, motionResponsive: true, ref: ref, tabIndex: -1, ...a11yProps, className: cn("w-full flex flex-col liquid-glass-modal-surface", "focus:outline-none", variant === "fullscreen" ? "h-full" : "max-h-full overflow-hidden", consciousness && "consciousness-modal-glass", eyeTracking && "consciousness-eye-trackable-content", adaptive && modalInsights?.urgency === "high" && "consciousness-urgent-glass", predictive && modalInsights && "consciousness-predictive-glass", className), style: { "--liquid-glass-depth-offset": variant === "drawer" ? "8px" : "12px", "--liquid-glass-tint-adaptive": modalInsights?.urgency === "high" ? "rgba(220, 38, 38, 0.15)" : "rgba(var(--glass-color-black) / var(--glass-opacity-10))" }, "data-liquid-glass-modal": "true", "data-modal-urgency": modalInsights?.urgency, ...props, children: [(title || description || showCloseButton) && jsx("div", { className: "glass-flex-shrink-0 glass-p-6 glass-border-b glass-border-glass-border/20", children: jsxs("div", { className: "glass-flex glass-items-start glass-justify-between", children: [jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [title && jsx("h2", { id: titleId, className: 'glass-text-lg font-semibold text-primary', children: title }), description && jsx("p", { id: descriptionId, className: 'glass-text-sm text-muted-foreground glass-mt-1', children: description })] }), showCloseButton && jsx(GlassButton, { variant: "ghost", size: "sm", iconOnly: true, onClick: onClose, "aria-label": "Close modal", className: "glass-flex-shrink-0 glass-ml-4", children: "\u00D7" })] }) }), jsx("div", { className: cn("flex-1 overflow-y-auto glass-p-6", contentClassName), children: children }), footer && jsx("div", { className: "glass-flex-shrink-0 glass-p-6 glass-border-t glass-border-glass-border/20", children: footer })] }) : jsxs(OptimizedGlassCore, { intent: "neutral", elevation: "level4", intensity: "strong", depth: 2, tint: "neutral", border: "subtle", animation: "none", performanceMode: "medium", ref: ref, tabIndex: -1, ...a11yProps, className: cn("w-full flex flex-col glass-overlay-noise glass-edge glass-overlay-specular glass-typography-reset", "focus:outline-none", variant === "fullscreen" ? "h-full" : "max-h-full overflow-hidden", consciousness && "consciousness-modal-glass", eyeTracking && "consciousness-eye-trackable-content", adaptive && modalInsights?.urgency === "high" && "consciousness-urgent-glass", predictive && modalInsights && "consciousness-predictive-glass", className), ...props, children: [(title || description || showCloseButton) && jsx("div", { className: "glass-flex-shrink-0 glass-p-6 glass-border-b glass-border-glass-border/20", children: jsxs("div", { className: "glass-flex glass-items-start glass-justify-between", children: [jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [title && jsx("h2", { id: titleId, className: 'glass-text-lg font-semibold text-primary', children: title }), description && jsx("p", { id: descriptionId, className: "glass-mt-1 glass-text-sm glass-text-secondary", children: description })] }), showCloseButton && jsx("div", { className: "glass-flex-shrink-0 glass-ml-4", children: closeButton || jsx(IconButton, { icon: jsx("svg", { className: 'w-4 h-4', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }), variant: "ghost", size: "sm", onClick: () => { // Record close button interaction if (consciousness && interactionRecorder) { interactionRecorder.recordInteraction("modal_close_button", { modalTitle: title, timestamp: Date.now() }); } // Play spatial audio for button close if (spatialAudio && spatialAudioEngine) { spatialAudioEngine.playSound("modal-button-close", { volume: 0.3, position: "right", reverb: 0.1 }); } onClose(); }, "aria-label": "Close modal", className: cn(consciousness && "consciousness-close-button", adaptive && modalInsights?.urgency === "high" && "consciousness-urgent-close"), "data-consciousness-close": "true" }) })] }) }), jsxs("div", { ref: contentRef, className: cn("flex-1 overflow-y-auto glass-p-6", consciousness && "consciousness-modal-body", eyeTracking && "consciousness-eye-trackable-body", adaptive && modalInsights?.urgency === "high" && "consciousness-urgent-body"), tabIndex: 0, "data-consciousness-body": "true", "data-content-complexity": modalInsights?.complexity, "data-scroll-tracking": String(!!consciousness), onScroll: e => { if (consciousness && interactionRecorder) { const scrollElement = e.target; const scrollDepth = scrollElement.scrollTop / (scrollElement.scrollHeight - scrollElement.clientHeight) || 0; setContentEngagement(prev => ({ ...prev, scrollDepth: Math.max(prev.scrollDepth, scrollDepth), interactions: prev.interactions + 1 })); interactionRecorder.recordInteraction("modal_scroll", { modalTitle: title, scrollDepth, timestamp: Date.now() }); } }, onClick: e => { if (consciousness && interactionRecorder) { setContentEngagement(prev => ({ ...prev, interactions: prev.interactions + 1 })); interactionRecorder.recordInteraction("modal_content_click", { modalTitle: title, timestamp: Date.now() }); } }, children: [children, predictive && modalInsights && jsx("div", { className: "glass-mt-4 glass-p-3 glass-surface-primary/10 glass-radius-lg glass-border glass-border-primary/20 glass-text-xs", children: jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsx("span", { className: 'text-primary', children: "Modal Insights" }), jsxs("div", { className: "glass-flex glass-gap-2", children: [jsxs("span", { className: cn("glass-px-2 glass-py-1 glass-radius-md", modalInsights.urgency === "high" ? "bg-red-500/20 text-red-300" : modalInsights.urgency === "medium" ? "bg-yellow-500/20 text-yellow-300" : "bg-green-500/20 text-green-300"), children: [modalInsights.urgency, " urgency"] }), modalInsights.userStress > 0.7 && jsx("span", { className: "glass-px-2 glass-py-1 glass-radius-md glass-surface-primary/20 glass-text-secondary", children: "high stress" })] })] }) })] }), footer && jsx("div", { className: "glass-flex-shrink-0 glass-p-6 glass-border-t glass-border-glass-border/20", children: footer })] }) }) })] }); }); GlassModal.displayName = "GlassModal"; export { GlassModal }; //# sourceMappingURL=GlassModal.js.map