aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
319 lines (316 loc) • 11.8 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { AnimatePresence, motion } from 'framer-motion';
import { forwardRef, useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useMotionPreference } from '../../hooks/useMotionPreference.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 '../../primitives/motion/MotionFramer.js';
import { useA11yId } from '../../utils/a11y.js';
import { useGlassSound } from '../../utils/soundDesign.js';
const GlassFractalLayout = /*#__PURE__*/forwardRef(({
// TODO: Integrate ContrastGuard for any section titles, labels, and helper text for WCAG AA compliance
nodes = [],
maxDepth = 5,
fractalType = "tree",
scaleFactor = 0.618,
// Golden ratio
branchAngle = 30,
initialScale = 1,
recursive = true,
animateGrowth = true,
zoomLevel = 1,
centerNode = true,
interactiveZoom = true,
onNodeClick,
onNodeHover,
glassConfig = {},
soundEnabled = true,
className = "",
style = {},
...props
}, ref) => {
const [hoveredNode, setHoveredNode] = useState(null);
const [selectedNode, setSelectedNode] = useState(null);
const [currentZoom, setCurrentZoom] = useState(zoomLevel);
const [growthProgress, setGrowthProgress] = useState(0);
const containerRef = useRef(null);
const animationRef = useRef();
const {
prefersReducedMotion
} = useMotionPreference();
const layoutId = useA11yId();
const {
play
} = useGlassSound();
useEffect(() => {
if (animateGrowth && !prefersReducedMotion) {
const startTime = Date.now();
const duration = 2000; // 2 seconds
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
setGrowthProgress(progress);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
}
};
animationRef.current = requestAnimationFrame(animate);
} else {
setGrowthProgress(1);
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [animateGrowth, prefersReducedMotion]);
const generateFractalPositions = useCallback((nodes, depth = 0, parentPos = {
x: 0,
y: 0
}, parentAngle = -90, parentScale = initialScale) => {
if (depth >= maxDepth || depth >= growthProgress * maxDepth) return [];
return nodes.map((node, index) => {
const currentScale = parentScale * Math.pow(scaleFactor, depth);
let position = {
x: 0,
y: 0
};
let rotation = 0;
switch (fractalType) {
case "tree":
const angleOffset = (index - (nodes.length - 1) / 2) * branchAngle;
const currentAngle = parentAngle + angleOffset;
const distance = 100 * currentScale;
position = {
x: parentPos.x + Math.cos(currentAngle * Math.PI / 180) * distance,
y: parentPos.y + Math.sin(currentAngle * Math.PI / 180) * distance
};
rotation = currentAngle + 90;
break;
case "spiral":
const spiralAngle = parentAngle + index * 137.5; // Golden angle
const spiralRadius = depth * 50 * currentScale;
position = {
x: parentPos.x + Math.cos(spiralAngle * Math.PI / 180) * spiralRadius,
y: parentPos.y + Math.sin(spiralAngle * Math.PI / 180) * spiralRadius
};
rotation = spiralAngle;
break;
case "sierpinski":
// Sierpinski triangle pattern
const triAngle = index * 120 + parentAngle;
const triRadius = 80 * currentScale;
position = {
x: parentPos.x + Math.cos(triAngle * Math.PI / 180) * triRadius,
y: parentPos.y + Math.sin(triAngle * Math.PI / 180) * triRadius
};
break;
case "mandelbrot":
const z = {
x: index * 0.1,
y: depth * 0.1
};
position = {
x: parentPos.x + z.x * 100 * currentScale,
y: parentPos.y + z.y * 100 * currentScale
};
break;
default:
position = {
x: parentPos.x + (index - nodes.length / 2) * 80 * currentScale,
y: parentPos.y + depth * 60 * currentScale
};
}
const processedNode = {
...node,
depth,
scale: currentScale,
rotation,
position,
children: node.children && recursive ? generateFractalPositions(node.children, depth + 1, position, rotation || parentAngle, currentScale) : []
};
return processedNode;
});
}, [maxDepth, fractalType, scaleFactor, branchAngle, initialScale, recursive, growthProgress]);
const processedNodes = useMemo(() => {
const rootPosition = centerNode ? {
x: 0,
y: 0
} : {
x: -200,
y: -200
};
return generateFractalPositions(nodes, 0, rootPosition);
}, [nodes, generateFractalPositions, centerNode]);
const flattenNodes = nodes => {
return nodes.reduce((acc, node) => {
acc.push(node);
if (node.children) {
acc.push(...flattenNodes(node.children));
}
return acc;
}, []);
};
const allNodes = useMemo(() => flattenNodes(processedNodes), [processedNodes]);
const handleNodeClick = useCallback(node => {
setSelectedNode(node.id);
onNodeClick?.(node);
if (soundEnabled) {
play("click");
}
}, [onNodeClick, soundEnabled, play]);
const handleNodeHover = useCallback(node => {
setHoveredNode(node?.id || null);
onNodeHover?.(node);
if (soundEnabled && node) {
play("hover");
}
}, [onNodeHover, soundEnabled, play]);
const handleWheel = useCallback(e => {
if (!interactiveZoom) return;
e.preventDefault();
const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
setCurrentZoom(prev => Math.max(0.1, Math.min(5, prev * zoomDelta)));
}, [interactiveZoom]);
const getNodeVariants = () => ({
hidden: {
scale: 0,
opacity: 0
},
visible: delay => ({
scale: 1,
opacity: 1,
transition: {
type: "spring",
tension: 300,
friction: 25,
delay: prefersReducedMotion ? 0 : delay * 0.1
}
}),
hover: {
scale: 1.1,
transition: {
type: "spring",
tension: 400,
friction: 20
}
},
selected: {
scale: 1.2,
transition: {
type: "spring",
tension: 400,
friction: 20
}
}
});
return jsxs(OptimizedGlassCore, {
ref: ref,
className: `glass-fractal-layout relative overflow-hidden ${className}`,
style: {
width: "100%",
height: "600px",
...style
},
glassConfig: {
blur: 15,
opacity: 0.9,
saturation: 1.1,
brightness: 1.05,
...glassConfig
},
onWheel: handleWheel,
role: "application",
"aria-label": "Fractal layout visualization",
id: layoutId,
...props,
children: [jsx("div", {
ref: containerRef,
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center',
style: {
transform: `scale(${currentZoom})`,
transformOrigin: "center"
},
children: jsx(AnimatePresence, {
children: allNodes.map((node, index) => {
const isHovered = hoveredNode === node.id;
const isSelected = selectedNode === node.id;
const nodeScale = (node.scale || 1) * currentZoom;
return jsxs(motion.div, {
className: 'absolute cursor-pointer',
style: {
left: "50%",
top: "50%",
transform: `translate(-50%, -50%) translate(${node.position?.x || 0}px, ${node.position?.y || 0}px) scale(${nodeScale}) rotate(${node.rotation || 0}deg)`
},
custom: node.depth || 0,
variants: getNodeVariants(),
initial: "hidden",
animate: isSelected ? "selected" : isHovered ? "hover" : "visible",
exit: "hidden",
onMouseEnter: () => handleNodeHover(node),
onMouseLeave: () => handleNodeHover(null),
onClick: () => handleNodeClick(node),
children: [jsx("div", {
className: `
glass-surface rounded-lg border border-white/20 glass-backdrop-blur-md
transition-all duration-200 p-2 min-w-[40px] min-h-[40px]
flex items-center justify-center
${isHovered || isSelected ? "bg-white/20 border-white/40" : "bg-white/10 border-white/20"}
`,
style: {
opacity: Math.max(0.3, 1 - (node.depth || 0) * 0.2)
},
children: node.content
}), node.children?.map((child, childIndex) => jsx("div", {
className: 'absolute glass-border-l glass-border-white/30',
style: {
left: "50%",
top: "50%",
height: Math.sqrt(Math.pow((child.position?.x || 0) - (node.position?.x || 0), 2) + Math.pow((child.position?.y || 0) - (node.position?.y || 0), 2)),
transformOrigin: "0 0",
transform: `rotate(${Math.atan2((child.position?.y || 0) - (node.position?.y || 0), (child.position?.x || 0) - (node.position?.x || 0))}rad)`
}
}, `line-${child.id}`)), (node.depth || 0) > 0 && jsx("div", {
className: 'absolute glass-top-1 -right-1 glass-surface-dark/50 text-primary glass-text-xs glass-radius-full w-4 h-4 glass-flex glass-items-center glass-justify-center',
children: node.depth
})]
}, `${node.id}-${node.depth}`);
})
})
}), jsxs("div", {
className: 'absolute bottom-4 left-4 glass-flex glass-flex-col glass-gap-2',
children: [jsxs("div", {
className: 'glass-text-xs text-primary/70 glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard',
children: ["Type: ", fractalType]
}), jsxs("div", {
className: 'glass-text-xs text-primary/70 glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard',
children: ["Depth: ", Math.floor(growthProgress * maxDepth), "/", maxDepth]
}), jsxs("div", {
className: 'glass-text-xs text-primary/70 glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard',
children: ["Nodes: ", allNodes.length]
}), interactiveZoom && jsxs("div", {
className: 'glass-text-xs text-primary/70 glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard',
children: ["Zoom: ", (currentZoom * 100).toFixed(0), "%"]
})]
}), animateGrowth && growthProgress < 1 && jsx("div", {
className: 'absolute top-4 right-4',
children: jsx("div", {
className: 'w-32 h-2 glass-surface-dark/20 glass-radius-full glass-glass-backdrop-blur-sm glass-contrast-guard',
children: jsx("div", {
className: 'glass-h-full glass-surface-subtle/50 glass-radius-full transition-all duration-100',
style: {
width: `${growthProgress * 100}%`
}
})
})
})]
});
});
GlassFractalLayout.displayName = "GlassFractalLayout";
export { GlassFractalLayout };
//# sourceMappingURL=GlassFractalLayout.js.map