UNPKG

aura-glass

Version:

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

534 lines (531 loc) 20.8 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { motion } from 'framer-motion'; import { forwardRef, useState, useRef, useCallback, useEffect } 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 { createGlassStyle } from '../../utils/createGlassStyle.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const defaultConfig = { containerPadding: 50, islandSpacing: 100, connectionDistance: 300, animationSpeed: 1.0, gravityStrength: 0.02, repulsionStrength: 100, enablePhysics: false, enableAutoArrange: false, enableCollisionDetection: true }; const GlassIslandLayout = /*#__PURE__*/forwardRef(({ // TODO: Integrate ContrastGuard for any section titles, labels, and helper text for WCAG AA compliance islands = [], connections = [], config = {}, showMinimap = true, showConnections = true, showGrid = false, showStats = true, enablePhysics = false, enableDragging = true, enableResizing = false, enableZooming = true, zoomLevel = 1.0, centerOnLoad = true, onIslandMove, onIslandResize, onIslandSelect, onConnectionCreate, className = "", ...props }, ref) => { const prefersReducedMotion = useReducedMotion(); const normalizedIslands = Array.isArray(islands) ? islands : []; const normalizedConnections = Array.isArray(connections) ? connections : []; const [layoutIslands, setLayoutIslands] = useState(normalizedIslands); const [physicsIslands, setPhysicsIslands] = useState([]); const [selectedIsland, setSelectedIsland] = useState(null); const [isDragging, setIsDragging] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [viewportOffset, setViewportOffset] = useState({ x: 0, y: 0 }); const [currentZoom, setCurrentZoom] = useState(zoomLevel); const [isResizing, setIsResizing] = useState(null); const [connectionMode, setConnectionMode] = useState(false); const [pendingConnection, setPendingConnection] = useState(null); const containerRef = useRef(null); const canvasRef = useRef(null); const animationFrameRef = useRef(); const [layoutConfig] = useState({ ...defaultConfig, ...config }); useA11yId("glass-island-layout"); const { shouldAnimate } = useMotionPreference(); const { play } = useGlassSound(); // Auto-arrange islands in a spiral pattern const autoArrange = useCallback(() => { if (!layoutConfig.enableAutoArrange) return; const arranged = normalizedIslands.map((island, index) => { const angle = index * 0.618 * 2 * Math.PI; // Golden angle const radius = Math.sqrt(index + 1) * layoutConfig.islandSpacing; return { ...island, x: Math.cos(angle) * radius + 400, y: Math.sin(angle) * radius + 400 }; }); setLayoutIslands(arranged); }, [normalizedIslands, layoutConfig]); // Initialize physics islands const initializePhysics = useCallback(() => { if (!enablePhysics) return; const physics = layoutIslands.map(island => ({ ...island, vx: 0, vy: 0, mass: island.width * island.height / 10000, fixed: island.pinned || false })); setPhysicsIslands(physics); }, [layoutIslands, enablePhysics]); // Physics simulation step const updatePhysics = useCallback(() => { if (!enablePhysics || physicsIslands.length === 0) return; setPhysicsIslands(prevIslands => { const newIslands = prevIslands.map(island => ({ ...island })); // Apply forces between islands for (let i = 0; i < newIslands.length; i++) { const island1 = newIslands[i]; if (island1.fixed) continue; let fx = 0; let fy = 0; for (let j = 0; j < newIslands.length; j++) { if (i === j) continue; const island2 = newIslands[j]; const dx = island2.x + island2.width / 2 - (island1.x + island1.width / 2); const dy = island2.y + island2.height / 2 - (island1.y + island1.height / 2); const distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Repulsion force const repulsion = layoutConfig.repulsionStrength / (distance * distance); fx -= dx / distance * repulsion; fy -= dy / distance * repulsion; // Connection attraction const hasConnection = normalizedConnections.some(conn => conn.from === island1.id && conn.to === island2.id || conn.to === island1.id && conn.from === island2.id); if (hasConnection) { const attraction = layoutConfig.gravityStrength * distance; fx += dx / distance * attraction; fy += dy / distance * attraction; } } } // Apply forces island1.vx += fx / island1.mass; island1.vy += fy / island1.mass; // Damping island1.vx *= 0.95; island1.vy *= 0.95; // Update position island1.x += island1.vx * layoutConfig.animationSpeed; island1.y += island1.vy * layoutConfig.animationSpeed; // Boundary constraints if (island1.x < layoutConfig.containerPadding) { island1.x = layoutConfig.containerPadding; island1.vx = 0; } if (island1.y < layoutConfig.containerPadding) { island1.y = layoutConfig.containerPadding; island1.vy = 0; } } return newIslands; }); }, [enablePhysics, physicsIslands, normalizedConnections, layoutConfig]); // Physics animation loop useEffect(() => { if (!enablePhysics) return; const animate = () => { updatePhysics(); animationFrameRef.current = requestAnimationFrame(animate); }; animationFrameRef.current = requestAnimationFrame(animate); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [enablePhysics, updatePhysics]); // Update layout islands from physics useEffect(() => { if (enablePhysics && physicsIslands.length > 0) { setLayoutIslands(physicsIslands); } }, [physicsIslands, enablePhysics]); // Draw connections on canvas const drawConnections = useCallback(() => { if (!showConnections || !canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.scale(currentZoom, currentZoom); ctx.translate(viewportOffset.x, viewportOffset.y); normalizedConnections.forEach(connection => { const fromIsland = layoutIslands.find(i => i.id === connection.from); const toIsland = layoutIslands.find(i => i.id === connection.to); if (!fromIsland || !toIsland) return; const fromX = fromIsland.x + fromIsland.width / 2; const fromY = fromIsland.y + fromIsland.height / 2; const toX = toIsland.x + toIsland.width / 2; const toY = toIsland.y + toIsland.height / 2; ctx.strokeStyle = connection.color || "var(--glass-bg-hover)"; ctx.lineWidth = (connection.strength || 1) * 2; if (connection.type === "dashed") { ctx.setLineDash([5, 5]); } else if (connection.type === "dotted") { ctx.setLineDash([2, 3]); } else { ctx.setLineDash([]); } ctx.beginPath(); ctx.moveTo(fromX, fromY); // Curved connection const controlX = (fromX + toX) / 2; const controlY = Math.min(fromY, toY) - Math.abs(toX - fromX) / 4; ctx.quadraticCurveTo(controlX, controlY, toX, toY); ctx.stroke(); // Draw arrow const angle = Math.atan2(toY - controlY, toX - controlX); const arrowLength = 10; ctx.fillStyle = connection.color || "var(--glass-border-hover)"; ctx.beginPath(); ctx.moveTo(toX, toY); ctx.lineTo(toX - arrowLength * Math.cos(angle - Math.PI / 6), toY - arrowLength * Math.sin(angle - Math.PI / 6)); ctx.lineTo(toX - arrowLength * Math.cos(angle + Math.PI / 6), toY - arrowLength * Math.sin(angle + Math.PI / 6)); ctx.closePath(); ctx.fill(); }); ctx.restore(); }, [showConnections, normalizedConnections, layoutIslands, currentZoom, viewportOffset]); // Handle island dragging const handleMouseDown = useCallback((e, island) => { if (!enableDragging) return; setIsDragging(true); setSelectedIsland(island.id); setDragOffset({ x: e.clientX - island.x * currentZoom, y: e.clientY - island.y * currentZoom }); if (connectionMode) { if (pendingConnection) { // Create connection onConnectionCreate?.(pendingConnection, island.id); setPendingConnection(null); setConnectionMode(false); play("connect"); } else { setPendingConnection(island.id); } return; } onIslandSelect?.(island); play("select"); }, [enableDragging, currentZoom, connectionMode, pendingConnection, onConnectionCreate, onIslandSelect, play]); const handleMouseMove = useCallback(e => { if (!isDragging || !selectedIsland) return; const newX = (e.clientX - dragOffset.x) / currentZoom; const newY = (e.clientY - dragOffset.y) / currentZoom; setLayoutIslands(prev => prev.map(island => island.id === selectedIsland ? { ...island, x: newX, y: newY } : island)); // Update physics island if physics is enabled if (enablePhysics) { setPhysicsIslands(prev => prev.map(island => island.id === selectedIsland ? { ...island, x: newX, y: newY, vx: 0, vy: 0 } : island)); } const island = layoutIslands.find(i => i.id === selectedIsland); if (island) { onIslandMove?.(island, newX, newY); } }, [isDragging, selectedIsland, dragOffset, currentZoom, enablePhysics, layoutIslands, onIslandMove]); const handleMouseUp = useCallback(() => { setIsDragging(false); setSelectedIsland(null); setIsResizing(null); }, []); // Center view on load useEffect(() => { if (centerOnLoad && layoutIslands.length > 0 && containerRef.current) { const bounds = layoutIslands.reduce((acc, island) => ({ minX: Math.min(acc.minX, island.x), minY: Math.min(acc.minY, island.y), maxX: Math.max(acc.maxX, island.x + island.width), maxY: Math.max(acc.maxY, island.y + island.height) }), { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }); const centerX = (bounds.minX + bounds.maxX) / 2; const centerY = (bounds.minY + bounds.maxY) / 2; const containerRect = containerRef.current.getBoundingClientRect(); setViewportOffset({ x: containerRect.width / 2 - centerX * currentZoom, y: containerRect.height / 2 - centerY * currentZoom }); } }, [centerOnLoad, layoutIslands, currentZoom]); // Initialize useEffect(() => { setLayoutIslands(normalizedIslands); if (layoutConfig.enableAutoArrange) { autoArrange(); } }, [normalizedIslands, autoArrange, layoutConfig.enableAutoArrange]); useEffect(() => { initializePhysics(); }, [initializePhysics]); useEffect(() => { drawConnections(); }, [drawConnections]); // Event listeners useEffect(() => { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [handleMouseMove, handleMouseUp]); const Minimap = () => { const minimapScale = 0.1; const minimapWidth = 200; const minimapHeight = 150; return jsxs("div", { className: 'absolute top-4 right-4 w-50 h-38 glass-surface-dark/50 glass-border glass-border-white/20 glass-radius-lg glass-p-2', children: [jsx("div", { className: 'glass-text-xs text-primary/70 mb-1', children: "Overview" }), jsxs("div", { className: 'relative glass-surface-dark/30 glass-radius', style: { width: minimapWidth, height: minimapHeight }, children: [layoutIslands.map(island => jsx("div", { className: `absolute rounded ${selectedIsland === island.id ? "bg-blue-400" : "bg-white/40"}`, style: { left: island.x * minimapScale, top: island.y * minimapScale, width: Math.max(2, island.width * minimapScale), height: Math.max(2, island.height * minimapScale) } }, `mini-${island.id}`)), jsx("div", { className: 'absolute glass-border glass-border-blue pointer-events-none', style: { left: -viewportOffset.x * minimapScale / currentZoom, top: -viewportOffset.y * minimapScale / currentZoom, width: 200 * minimapScale / currentZoom, height: 150 * minimapScale / currentZoom } })] })] }); }; const StatsPanel = () => jsx("div", { className: ` absolute bottom-4 left-4 p-3 rounded-lg border border-white/10 ${createGlassStyle({ blur: "sm", opacity: 0.8 }).background} `, children: jsxs("div", { className: 'glass-text-xs text-primary/90 space-y-1', children: [jsxs("div", { children: ["Islands: ", layoutIslands.length] }), jsxs("div", { children: ["Connections: ", normalizedConnections.length] }), jsxs("div", { children: ["Zoom: ", Math.round(currentZoom * 100), "%"] }), enablePhysics && jsx("div", { children: "Physics: ON" })] }) }); const Controls = () => jsxs("div", { className: 'absolute top-4 left-4 glass-flex glass-flex-col space-y-2', children: [jsx(motion.button, { className: 'glass-p-2 glass-surface-subtle/10 hover:glass-surface-subtle/20 glass-border glass-border-white/20 glass-radius-lg text-primary transition-colors glass-focus glass-touch-target glass-contrast-guard', whileHover: shouldAnimate ? { scale: 1.05 } : {}, whileTap: shouldAnimate ? { scale: 0.95 } : {}, onClick: () => setCurrentZoom(prev => Math.min(3, prev * 1.2)), children: "\uD83D\uDD0D+" }), jsx(motion.button, { className: 'glass-p-2 glass-surface-subtle/10 hover:glass-surface-subtle/20 glass-border glass-border-white/20 glass-radius-lg text-primary transition-colors glass-focus glass-touch-target glass-contrast-guard', whileHover: shouldAnimate ? { scale: 1.05 } : {}, whileTap: shouldAnimate ? { scale: 0.95 } : {}, onClick: () => setCurrentZoom(prev => Math.max(0.2, prev / 1.2)), children: "\uD83D\uDD0D-" }), jsx(motion.button, { className: `p-2 border border-white/20 rounded-lg text-white transition-colors glass-focus glass-touch-target glass-contrast-guard ${connectionMode ? "bg-blue-500/50" : "bg-white/10 hover:bg-white/20"}`, whileHover: shouldAnimate ? { scale: 1.05 } : {}, whileTap: shouldAnimate ? { scale: 0.95 } : {}, onClick: () => setConnectionMode(!connectionMode), children: "\uD83D\uDD17" }), jsx(motion.button, { className: 'glass-p-2 glass-surface-subtle/10 hover:glass-surface-subtle/20 glass-border glass-border-white/20 glass-radius-lg text-primary transition-colors glass-focus glass-touch-target glass-contrast-guard', whileHover: shouldAnimate ? { scale: 1.05 } : {}, whileTap: shouldAnimate ? { scale: 0.95 } : {}, onClick: autoArrange, children: "\u26A1" })] }); return jsxs(OptimizedGlassCore, { ref: ref, variant: "frosted", className: `relative overflow-hidden ${className}`, style: { height: "600px" }, ...props, children: [jsx("div", { className: 'absolute top-0 left-0 right-0 glass-p-4 z-10', children: jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs("div", { children: [jsx("h3", { className: 'glass-text-xl font-semibold text-primary/90', children: "Island Layout" }), jsx("p", { className: 'glass-text-sm text-primary/60', children: "Floating content islands with connections" })] }), connectionMode && jsx("div", { className: "glass-px-3 glass-py-1 glass-surface-blue/20 glass-border glass-border-blue/50 glass-radius-lg glass-text-secondary glass-text-sm", children: pendingConnection ? "Select target island" : "Select source island" })] }) }), jsxs("div", { ref: containerRef, className: 'absolute inset-0 overflow-hidden cursor-move', style: { transform: `scale(${currentZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`, transformOrigin: "0 0" }, children: [showGrid && jsx("div", { className: 'absolute inset-0 opacity-10', style: { backgroundImage: ` linear-gradient(var(--glass-bg-default) 1px, transparent 1px), linear-gradient(90deg, var(--glass-bg-default) 1px, transparent 1px) `, backgroundSize: "50px 50px" } }), jsx("canvas", { ref: canvasRef, className: 'absolute inset-0 pointer-events-none', width: 2000, height: 2000 }), layoutIslands.map((island, index) => jsx(motion.div, { className: `absolute cursor-pointer transition-all duration-200 ${selectedIsland === island.id ? "ring-2 ring-blue-400" : ""} ${island.minimized ? "opacity-50" : ""}`, style: { left: island.x, top: island.y, width: island.width, height: island.minimized ? 40 : island.height, zIndex: island.zIndex || (selectedIsland === island.id ? 1000 : index) }, initial: shouldAnimate ? { opacity: 0, scale: 0.8 } : false, animate: prefersReducedMotion ? {} : { opacity: 1, scale: 1 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, onMouseDown: e => handleMouseDown(e, island), children: jsxs(OptimizedGlassCore, { variant: "frosted", className: `w-full h-full p-4 hover:bg-white/10 transition-all duration-200 ${island.pinned ? "border-yellow-400/50" : ""} ${connectionMode ? "hover:border-blue-400" : ""}`, children: [!island.minimized && island.content, jsxs("div", { className: 'absolute glass-top-2 right-2 glass-flex space-x-1 opacity-0 hover:opacity-100 transition-opacity', children: [island.category && jsx("span", { className: 'glass-px-2 glass-py-1 glass-surface-dark/30 text-primary/70 glass-radius glass-text-xs', children: island.category }), jsx("button", { onClick: e => { e.stopPropagation(); setLayoutIslands(prev => prev.map(i => i.id === island.id ? { ...i, minimized: !i.minimized } : i)); }, className: 'w-6 h-6 glass-surface-subtle/20 hover:glass-surface-subtle/30 glass-radius text-primary/80 glass-text-xs transition-colors glass-focus glass-touch-target glass-contrast-guard', children: island.minimized ? "□" : "_" })] }), enableResizing && !island.minimized && jsx("div", { className: 'absolute bottom-0 right-0 w-4 h-4 glass-surface-subtle/20 cursor-se-resize opacity-0 hover:opacity-100 transition-opacity', onMouseDown: e => { e.stopPropagation(); setIsResizing(island.id); }, children: "\u22EE\u22EE" })] }) }, island.id))] }), jsx(Controls, {}), showMinimap && jsx(Minimap, {}), showStats && jsx(StatsPanel, {})] }); }); GlassIslandLayout.displayName = "GlassIslandLayout"; export { GlassIslandLayout }; //# sourceMappingURL=GlassIslandLayout.js.map