aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
598 lines (595 loc) • 18.5 kB
JavaScript
'use client';
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
import { easings } from './AdvancedAnimations.js';
// Glass transition variants
const glassTransitionVariants = {
// Shatter transition
shatter: {
initial: {
scale: 1,
opacity: 1,
filter: 'blur(0px)'
},
exit: {
scale: [1, 1.02, 0.98, 1.1, 0],
opacity: [1, 0.8, 0.6, 0.3, 0],
filter: ['blur(0px)', 'blur(1px)', 'blur(3px)', 'blur(var(--glass-glass-blur-md))', 'blur(15px)'],
rotate: [0, -2, 4, -8, 15],
transition: {
duration: 0.8,
times: [0, 0.2, 0.4, 0.7, 1],
ease: easings.easeOutExpo
}
},
enter: {
scale: [0, 0.8, 1.05, 1],
opacity: [0, 0.3, 0.8, 1],
filter: ['blur(15px)', 'blur(var(--glass-glass-blur-md))', 'blur(2px)', 'blur(0px)'],
rotate: [15, -5, 2, 0],
transition: {
duration: 0.8,
times: [0, 0.3, 0.7, 1],
ease: easings.easeOutBack
}
}
},
// Liquid transition
liquid: {
initial: {
clipPath: 'inset(0% 0% 0% 0% round 16px)',
scale: 1,
opacity: 1
},
exit: {
clipPath: ['inset(0% 0% 0% 0% round 16px)', 'inset(10% 5% 10% 5% round 32px)', 'inset(30% 20% 30% 20% round 50%)', 'inset(50% 50% 50% 50% round 50%)'],
scale: [1, 0.95, 0.8, 0.5],
opacity: [1, 0.8, 0.4, 0],
transition: {
duration: 0.6,
ease: easings.easeInOutCubic
}
},
enter: {
clipPath: ['inset(50% 50% 50% 50% round 50%)', 'inset(30% 20% 30% 20% round 50%)', 'inset(10% 5% 10% 5% round 32px)', 'inset(0% 0% 0% 0% round 16px)'],
scale: [0.5, 0.8, 0.95, 1],
opacity: [0, 0.4, 0.8, 1],
transition: {
duration: 0.6,
ease: easings.easeOutExpo
}
}
},
// Ripple transition
ripple: {
initial: {
scale: 1,
opacity: 1,
borderRadius: '16px'
},
exit: {
scale: [1, 1.5, 0],
opacity: [1, 0.3, 0],
borderRadius: ['16px', '50%', '50%'],
transition: {
duration: 0.5,
ease: easings.easeInOutCubic
}
},
enter: {
scale: [0, 1.2, 1],
opacity: [0, 0.6, 1],
borderRadius: ['50%', '50%', '16px'],
transition: {
duration: 0.5,
ease: easings.easeOutBack
}
}
},
// Morph transition
morph: {
initial: {
borderRadius: '16px',
scaleX: 1,
scaleY: 1,
opacity: 1
},
exit: {
borderRadius: ['16px', '50px 16px 30px 40px', '20px 60px 40px 20px', '50%'],
scaleX: [1, 0.8, 1.2, 0],
scaleY: [1, 1.2, 0.8, 0],
opacity: [1, 0.7, 0.3, 0],
transition: {
duration: 0.7,
ease: easings.easeInOutCubic
}
},
enter: {
borderRadius: ['50%', '20px 60px 40px 20px', '50px 16px 30px 40px', '16px'],
scaleX: [0, 1.2, 0.8, 1],
scaleY: [0, 0.8, 1.2, 1],
opacity: [0, 0.3, 0.7, 1],
transition: {
duration: 0.7,
ease: easings.easeOutExpo
}
}
},
// Frost transition
frost: {
initial: {
scale: 1,
opacity: 1,
filter: 'blur(0px) brightness(1)'
},
exit: {
scale: [1, 1.1, 0.9, 0],
opacity: [1, 0.8, 0.4, 0],
filter: ['blur(0px) brightness(1)', 'blur(2px) brightness(1.2)', 'blur(var(--glass-glass-blur-md)) brightness(0.8)', 'blur(var(--glass-glass-blur-lg)) brightness(0.5)'],
transition: {
duration: 0.8,
ease: easings.easeInOutCubic
}
},
enter: {
scale: [0, 0.9, 1.1, 1],
opacity: [0, 0.4, 0.8, 1],
filter: ['blur(var(--glass-glass-blur-lg)) brightness(0.5)', 'blur(var(--glass-glass-blur-md)) brightness(0.8)', 'blur(2px) brightness(1.2)', 'blur(0px) brightness(1)'],
transition: {
duration: 0.8,
ease: easings.easeOutExpo
}
}
}
};
function GlassTransition({
children,
variant = 'liquid',
duration,
className,
style
}) {
useReducedMotion();
const transitionVariant = glassTransitionVariants[variant];
// Override duration if provided
const customVariant = duration ? {
...transitionVariant,
exit: {
...transitionVariant.exit,
transition: {
...transitionVariant.exit.transition,
duration
}
},
enter: {
...transitionVariant.enter,
transition: {
...transitionVariant.enter.transition,
duration
}
}
} : transitionVariant;
return jsx(motion.div, {
className: cn('glass-transition-container', className),
style: style,
variants: customVariant,
initial: "initial",
animate: "initial",
exit: "exit",
children: children
});
}
function GlassPageTransition({
children,
variant = 'morph',
duration = 0.6,
className
}) {
return jsx(AnimatePresence, {
mode: "wait",
children: jsx(GlassTransition, {
variant: variant,
duration: duration,
className: className,
children: children
})
});
}
function SwipeableGlassCards({
cards,
onSwipe,
className
}) {
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState('left');
const handleDragEnd = info => {
const threshold = 100;
const velocity = info.velocity.x;
const offset = info.offset.x;
if (Math.abs(velocity) > 500 || Math.abs(offset) > threshold) {
const swipeDirection = velocity > 0 || offset > 0 ? 'right' : 'left';
setDirection(swipeDirection);
const newIndex = swipeDirection === 'left' ? Math.min(currentIndex + 1, cards.length - 1) : Math.max(currentIndex - 1, 0);
if (newIndex !== currentIndex) {
setCurrentIndex(newIndex);
onSwipe?.(swipeDirection, cards[currentIndex].id);
}
}
};
const slideVariants = {
enter: direction => ({
x: direction === 'left' ? 300 : -300,
opacity: 0,
scale: 0.8,
rotateY: direction === 'left' ? 45 : -45
}),
center: {
x: 0,
opacity: 1,
scale: 1,
rotateY: 0,
transition: {
duration: 0.5,
ease: easings.easeOutExpo
}
},
exit: direction => ({
x: direction === 'left' ? -300 : 300,
opacity: 0,
scale: 0.8,
rotateY: direction === 'left' ? -45 : 45,
transition: {
duration: 0.3,
ease: easings.easeInOutCubic
}
})
};
return jsxs("div", {
className: cn('glass-relative glass-overflow-hidden', className),
children: [jsx(AnimatePresence, {
mode: "wait",
custom: direction,
children: jsx(motion.div, {
custom: direction,
variants: slideVariants,
initial: "enter",
animate: "center",
exit: "exit",
drag: "x",
dragConstraints: {
left: 0,
right: 0
},
dragElastic: 0.3,
onDragEnd: (_, info) => handleDragEnd(info),
className: cn("glass-foundation-complete glass-surface-primary glass-border glass-border-primary glass-radius-xl glass-p-6 glass-cursor-grab active:glass-cursor-grabbing"),
style: {
background: cards[currentIndex].background || 'var(--glass-bg-default)'
},
children: cards[currentIndex].content
}, currentIndex)
}), jsx("div", {
className: cn("glass-flex glass-justify-center glass-mt-4 glass-gap-2"),
children: cards.map((_, index) => jsx("button", {
onClick: () => {
setDirection(index > currentIndex ? 'left' : 'right');
setCurrentIndex(index);
},
className: cn('glass-w-2 glass-h-2 glass-radius-full glass-transition-all glass-duration-300 glass-focus glass-touch-target glass-contrast-guard', index === currentIndex ? 'glass-surface-white glass-scale-125' : 'glass-surface-muted hover:glass-surface-secondary')
}, index))
})]
});
}
function GlassAccordion({
items,
allowMultiple = false,
className
}) {
const prefersReducedMotion = useReducedMotion();
const [openItems, setOpenItems] = useState([]);
const toggleItem = id => {
setOpenItems(prev => {
if (prev.includes(id)) {
return prev.filter(item => item !== id);
} else {
return allowMultiple ? [...prev, id] : [id];
}
});
};
return jsx("div", {
className: cn('glass-space-y-2', className),
children: items.map(item => {
const isOpen = openItems.includes(item.id);
return jsxs(motion.div, {
className: cn("glass-foundation-complete glass-surface-subtle glass-border glass-border-subtle glass-radius-lg glass-overflow-hidden"),
layout: true,
children: [jsxs(motion.button, {
onClick: () => toggleItem(item.id),
className: cn("glass-w-full glass-p-4 glass-text-left glass-flex glass-items-center glass-justify-between hover:glass-surface-hover glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"),
whileHover: {
scale: 1.01
},
whileTap: {
scale: 0.99
},
children: [jsxs("div", {
className: cn("glass-flex glass-items-center glass-gap-3"),
children: [item.icon, jsx("span", {
className: cn("glass-font-medium glass-text-white"),
children: item.title
})]
}), jsx(motion.div, {
animate: prefersReducedMotion ? {} : {
rotate: isOpen ? 180 : 0
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 0.3
},
children: jsx("svg", {
className: cn("glass-w-5 glass-h-5 glass-text-secondary"),
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M19 9l-7 7-7-7"
})
})
})]
}), jsx(AnimatePresence, {
children: isOpen && jsx(motion.div, {
initial: {
height: 0,
opacity: 0
},
animate: prefersReducedMotion ? {} : {
height: 'auto',
opacity: 1
},
exit: {
height: 0,
opacity: 0
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 0.3,
ease: easings.easeInOutCubic
},
className: cn("glass-border-t glass-border-subtle"),
children: jsx(motion.div, {
initial: {
y: -10
},
animate: prefersReducedMotion ? {} : {
y: 0
},
exit: {
y: -10
},
className: cn("glass-p-4 glass-text-primary"),
children: item.content
})
})
})]
}, item.id);
})
});
}
function GlassModal({
isOpen,
onClose,
children,
variant = 'ripple',
className
}) {
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
const handleEscape = e => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
return jsx(AnimatePresence, {
children: isOpen && jsxs(motion.div, {
initial: {
opacity: 0
},
animate: prefersReducedMotion ? {} : {
opacity: 1
},
exit: {
opacity: 0
},
className: cn("glass-fixed glass-inset-0 glass-z-50 glass-flex glass-items-center glass-justify-center glass-p-4"),
onClick: onClose,
children: [jsx(motion.div, {
initial: {
opacity: 0
},
animate: prefersReducedMotion ? {} : {
opacity: 1
},
exit: {
opacity: 0
},
className: cn("glass-absolute glass-inset-0 glass-surface-overlay")
}), jsx(GlassTransition, {
variant: variant,
className: cn('glass-relative glass-max-w-lg glass-w-full glass-max-h-90vh glass-overflow-auto', 'glass-foundation-complete glass-surface-primary glass-border glass-border-primary glass-radius-xl', className),
children: jsx("div", {
onClick: e => e.stopPropagation(),
children: children
})
})]
})
});
}
function GlassTabs({
tabs,
defaultTab,
orientation = 'horizontal',
className,
onTabChange
}) {
const prefersReducedMotion = useReducedMotion();
const [activeTab, setActiveTab] = useState(defaultTab || tabs?.[0]?.id);
const handleTabChange = tabId => {
setActiveTab(tabId);
onTabChange?.(tabId);
};
return jsxs("div", {
className: cn('glass-tabs', orientation === 'vertical' ? 'glass-flex glass-gap-6' : 'glass-space-y-4', className),
children: [jsx("div", {
className: cn('glass-foundation-complete glass-surface-subtle glass-border glass-border-subtle glass-radius-lg glass-p-1', orientation === 'vertical' ? 'glass-flex-col glass-w-48' : 'glass-flex', 'glass-relative'),
children: tabs?.map(tab => jsxs("button", {
onClick: () => handleTabChange(tab.id),
className: cn('glass-relative glass-px-4 glass-py-2 glass-radius-md glass-transition-all glass-duration-300 glass-flex glass-items-center glass-gap-2 glass-focus glass-touch-target glass-contrast-guard', 'glass-text-sm glass-font-medium glass-whitespace-nowrap', activeTab === tab.id ? 'glass-text-white glass-surface-subtle' : 'glass-text-secondary hover:glass-text-white hover:glass-surface-hover'),
children: [tab.icon, jsx("span", {
children: tab.label
})]
}, tab.id))
}), jsx("div", {
className: cn('glass-flex-1 glass-foundation-complete glass-surface-subtle glass-border glass-border-subtle glass-radius-lg', 'glass-min-h-200'),
children: jsx(AnimatePresence, {
mode: "wait",
children: jsx(motion.div, {
initial: {
opacity: 0,
y: 20
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
y: 0
},
exit: {
opacity: 0,
y: -20
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 0.3,
ease: easings.easeOutExpo
},
className: cn("glass-p-6"),
children: tabs?.find(tab => tab.id === activeTab)?.content
}, activeTab)
})
})]
});
}
const demoCards = [{
id: 'insight',
content: jsxs("div", {
children: [jsx("p", {
className: "glass-text-xs glass-text-tertiary uppercase tracking-wide",
children: "Insight"
}), jsx("h3", {
className: "glass-text-xl glass-text-primary font-semibold",
children: "Fluid page transitions"
}), jsx("p", {
className: "glass-text-sm glass-text-secondary",
children: "Use material-aware easing to keep movement intuitive."
})]
})
}, {
id: 'motion',
content: jsxs("div", {
children: [jsx("p", {
className: "glass-text-xs glass-text-tertiary uppercase tracking-wide",
children: "Motion"
}), jsx("h3", {
className: "glass-text-xl glass-text-primary font-semibold",
children: "Directional swipes"
}), jsx("p", {
className: "glass-text-sm glass-text-secondary",
children: "Cards respond to drag gestures with depth and blur."
})]
})
}, {
id: 'layers',
content: jsxs("div", {
children: [jsx("p", {
className: "glass-text-xs glass-text-tertiary uppercase tracking-wide",
children: "Layers"
}), jsx("h3", {
className: "glass-text-xl glass-text-primary font-semibold",
children: "Frosted overlays"
}), jsx("p", {
className: "glass-text-sm glass-text-secondary",
children: "Blend regions using the glass transition variants."
})]
})
}];
const GlassTransitionsComponent = ({
variant = 'liquid',
className,
children,
...rest
}) => {
const [panelIndex, setPanelIndex] = useState(0);
const panels = [{
id: 'panel-0',
title: 'Glass dashboards',
description: 'Morph between layouts while preserving context.'
}, {
id: 'panel-1',
title: 'Immersive nav',
description: 'Page transitions ripple out from focal points.'
}];
const nextPanel = () => {
setPanelIndex(prev => (prev + 1) % panels.length);
};
return jsx("div", {
className: cn('glass-transitions-demo glass-space-y-6', className),
...rest,
children: children ?? jsxs(Fragment, {
children: [jsx(GlassPageTransition, {
variant: variant,
children: jsxs(motion.div, {
className: "glass-surface-primary glass-radius-3xl glass-p-8 glass-border glass-border-white/10",
children: [jsx("p", {
className: "glass-text-xs glass-text-tertiary uppercase tracking-wide mb-2",
children: panels[panelIndex].title
}), jsx("h2", {
className: "glass-text-2xl glass-text-primary font-semibold",
children: panels[panelIndex].description
}), jsx("button", {
type: "button",
onClick: nextPanel,
className: "glass-mt-6 glass-text-sm glass-text-secondary glass-focus glass-touch-target glass-contrast-guard",
children: "Cycle transition"
})]
}, panels[panelIndex].id)
}), jsx(SwipeableGlassCards, {
cards: demoCards
})]
})
});
};
const GlassTransitions = Object.assign(GlassTransitionsComponent, {
GlassTransition,
GlassPageTransition,
SwipeableGlassCards,
GlassAccordion,
GlassModal,
GlassTabs,
glassTransitionVariants
});
export { GlassAccordion, GlassModal, GlassPageTransition, GlassTabs, GlassTransition, GlassTransitions, SwipeableGlassCards, GlassTransitions as default };
//# sourceMappingURL=GlassTransitions.js.map