aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
534 lines (531 loc) • 20.8 kB
JavaScript
'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