UNPKG

aura-glass

Version:

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

432 lines (429 loc) 14.7 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useState, useRef, useCallback, useEffect } 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 '../../primitives/motion/MotionFramer.js'; const GlassMindMap = ({ data, connections = [], editable = false, showMinimap = true, zoomable = true, direction = "horizontal", nodeSpacing = 120, className = "", onNodeClick, onNodeDoubleClick, onNodeChange, onNodeAdd, onNodeDelete }) => { const [selectedNode, setSelectedNode] = useState(null); const [draggedNode, setDraggedNode] = useState(null); const [zoom, setZoom] = useState(1); const [pan, setPan] = useState({ x: 0, y: 0 }); const [isPanning, setIsPanning] = useState(false); const [editingNode, setEditingNode] = useState(null); const [editValue, setEditValue] = useState(""); const svgRef = useRef(null); const containerRef = useRef(null); const dragStartRef = useRef(null); // Calculate positions for all nodes const calculatePositions = useCallback((node, level = 0, parentId) => { const positionedNodes = []; // Calculate position based on direction and level let x = 0, y = 0; switch (direction) { case "horizontal": x = level * nodeSpacing; y = positionedNodes.length * 60 - (node.children?.length || 1) * 30; break; case "vertical": x = positionedNodes.length * 60 - (node.children?.length || 1) * 30; y = level * nodeSpacing; break; case "radial": const angle = positionedNodes.length / (node.children?.length || 1) * Math.PI * 2; const radius = level * nodeSpacing; x = Math.cos(angle) * radius; y = Math.sin(angle) * radius; break; } const positionedNode = { ...node, position: node.position || { x, y }, level, parentId }; positionedNodes.push(positionedNode); // Process children node.children?.forEach(child => { positionedNodes.push(...calculatePositions(child, level + 1, node.id)); }); return positionedNodes; }, [direction, nodeSpacing]); const positionedNodes = calculatePositions(data); // Handle node click const handleNodeClick = node => { setSelectedNode(node.id); onNodeClick?.(node); }; // Handle node double click const handleNodeDoubleClick = node => { if (editable) { setEditingNode(node.id); setEditValue(node.label); } onNodeDoubleClick?.(node); }; // Handle editing const handleEditSubmit = () => { if (editingNode && editValue.trim()) { onNodeChange?.(editingNode, { label: editValue.trim() }); } setEditingNode(null); setEditValue(""); }; const handleEditCancel = () => { setEditingNode(null); setEditValue(""); }; // Handle drag and drop const handleMouseDown = (e, nodeId) => { if (!editable) return; setDraggedNode(nodeId); dragStartRef.current = { x: e.clientX, y: e.clientY }; }; const handleMouseMove = useCallback(e => { if (!draggedNode || !dragStartRef.current) return; const deltaX = e.clientX - dragStartRef.current.x; const deltaY = e.clientY - dragStartRef.current.y; const node = positionedNodes.find(n => n.id === draggedNode); if (node) { onNodeChange?.(draggedNode, { position: { x: node.position.x + deltaX, y: node.position.y + deltaY } }); } }, [draggedNode, positionedNodes, onNodeChange]); const handleMouseUp = useCallback(() => { setDraggedNode(null); dragStartRef.current = null; }, []); // Handle zoom and pan const handleWheel = e => { if (!zoomable) return; e.preventDefault(); const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = Math.max(0.1, Math.min(3, zoom * zoomFactor)); setZoom(newZoom); }; const handlePanStart = e => { if (!zoomable) return; setIsPanning(true); dragStartRef.current = { x: e.clientX - pan.x, y: e.clientY - pan.y }; }; const handlePanMove = useCallback(e => { if (!isPanning || !dragStartRef.current) return; setPan({ x: e.clientX - dragStartRef.current.x, y: e.clientY - dragStartRef.current.y }); }, [isPanning]); const handlePanEnd = useCallback(() => { setIsPanning(false); dragStartRef.current = null; }, []); // Event listeners useEffect(() => { if (draggedNode) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } if (isPanning) { document.addEventListener("mousemove", handlePanMove); document.addEventListener("mouseup", handlePanEnd); } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener("mousemove", handlePanMove); document.removeEventListener("mouseup", handlePanEnd); }; }, [draggedNode, isPanning, handleMouseMove, handleMouseUp, handlePanMove, handlePanEnd]); // Render connection lines const renderConnections = () => { const lines = []; const addConnection = (fromNode, toNode) => { const connection = connections.find(c => c.from === fromNode.id && c.to === toNode.id); const color = connection?.color || "var(--glass-white)40"; const strokeDasharray = connection?.type === "dashed" ? "5,5" : connection?.type === "dotted" ? "2,2" : "none"; lines.push(jsx("line", { x1: fromNode.position.x + 50, y1: fromNode.position.y + 25, x2: toNode.position.x + 50, y2: toNode.position.y + 25, stroke: color, strokeWidth: "2", strokeDasharray: strokeDasharray, markerEnd: "url(#arrowhead)" }, `${fromNode.id}-${toNode.id}`)); if (connection?.label) { const midX = (fromNode.position.x + toNode.position.x) / 2 + 50; const midY = (fromNode.position.y + toNode.position.y) / 2 + 25; lines.push(jsx("text", { x: midX, y: midY - 5, textAnchor: "middle", className: 'glass-text-xs fill-white/70', children: connection.label }, `${fromNode.id}-${toNode.id}-label`)); } }; // Add connections from custom connections array connections.forEach(conn => { const fromNode = positionedNodes.find(n => n.id === conn.from); const toNode = positionedNodes.find(n => n.id === conn.to); if (fromNode && toNode) { addConnection(fromNode, toNode); } }); // Add default parent-child connections const addParentChildConnections = node => { node.children?.forEach(child => { const childNode = positionedNodes.find(n => n.id === child.id); if (childNode) { addConnection(node, childNode); addParentChildConnections(childNode); } }); }; addParentChildConnections(positionedNodes[0]); return lines; }; // Render nodes const renderNodes = () => { return positionedNodes.map(node => { const isSelected = selectedNode === node.id; const isEditing = editingNode === node.id; const isDragged = draggedNode === node.id; const nodeSize = node.size === "lg" ? 100 : node.size === "sm" ? 60 : 80; const nodeHeight = nodeSize * 0.5; let nodeElement; if (isEditing) { nodeElement = jsx("foreignObject", { x: node.position.x, y: node.position.y, width: nodeSize, height: nodeHeight, children: jsx("input", { autoFocus: true, value: editValue, onChange: e => setEditValue(e.target.value), onKeyDown: e => { if (e.key === "Enter") handleEditSubmit(); if (e.key === "Escape") handleEditCancel(); }, onBlur: handleEditSubmit, className: 'glass-w-full glass-h-full glass-px-2 glass-py-1 bg-transparent glass-border glass-border-white/30 glass-radius-md text-primary glass-text-sm focus:outline-none focus:border-white/60 glass-focus glass-touch-target glass-contrast-guard' }) }); } else { let shapeElement; switch (node.shape) { case "rectangle": shapeElement = jsx("rect", { x: node.position.x, y: node.position.y, width: nodeSize, height: nodeHeight, rx: "8", fill: node.color || "var(--glass-white)20", stroke: isSelected ? "var(--glass-white)60" : "var(--glass-white)30", strokeWidth: isSelected ? "2" : "1" }); break; case "diamond": const centerX = node.position.x + nodeSize / 2; const centerY = node.position.y + nodeHeight / 2; shapeElement = jsx("polygon", { points: `${centerX},${centerY - nodeHeight / 2} ${centerX + nodeSize / 2},${centerY} ${centerX},${centerY + nodeHeight / 2} ${centerX - nodeSize / 2},${centerY}`, fill: node.color || "var(--glass-white)20", stroke: isSelected ? "var(--glass-white)60" : "var(--glass-white)30", strokeWidth: isSelected ? "2" : "1" }); break; default: // circle shapeElement = jsx("circle", { cx: node.position.x + nodeSize / 2, cy: node.position.y + nodeHeight / 2, r: Math.min(nodeSize, nodeHeight) / 2, fill: node.color || "var(--glass-white)20", stroke: isSelected ? "var(--glass-white)60" : "var(--glass-white)30", strokeWidth: isSelected ? "2" : "1" }); } nodeElement = jsxs("g", { className: `cursor-pointer glass-focus glass-touch-target glass-contrast-guard ${isDragged ? "cursor-grabbing" : "cursor-grab"}`, onMouseDown: e => handleMouseDown(e, node.id), onClick: e => handleNodeClick(node), onDoubleClick: () => handleNodeDoubleClick(node), children: [shapeElement, jsxs("text", { x: node.position.x + nodeSize / 2, y: node.position.y + nodeHeight / 2 + 4, textAnchor: "middle", className: 'glass-text-sm fill-white font-medium pointer-events-none select-none', children: [node.icon && jsx("tspan", { x: node.position.x + nodeSize / 2 - 15, children: node.icon }), jsx("tspan", { x: node.icon ? node.position.x + nodeSize / 2 + 15 : node.position.x + nodeSize / 2, children: node.label })] })] }); } return jsx("g", { children: nodeElement }, node.id); }); }; return jsxs(OptimizedGlassCore, { "data-glass-component": true, className: `relative overflow-hidden ${className}`, intensity: "medium", elevation: "level1", children: [jsxs("div", { className: 'absolute top-4 left-4 z-10 glass-flex glass-gap-2', children: [jsx(OptimizedGlassCore, { className: 'glass-px-3 glass-py-1 glass-radius-md glass-text-sm cursor-pointer hover:glass-surface-subtle/10 glass-focus glass-touch-target glass-contrast-guard', intensity: "subtle", onClick: e => setZoom(1), children: "Reset Zoom" }), jsxs(OptimizedGlassCore, { className: "glass-px-3 glass-py-1 glass-radius-md glass-text-sm", intensity: "subtle", children: ["Zoom: ", (zoom * 100).toFixed(0), "%"] })] }), showMinimap && jsx("div", { className: 'absolute bottom-4 right-4 z-10 w-32 h-24 glass-surface-dark/20 glass-radius-md glass-border glass-border-white/20', children: jsx("svg", { className: "glass-w-full glass-h-full", viewBox: "0 0 320 240", children: positionedNodes.map(node => jsx("circle", { cx: (node.position.x + 160) / 3, cy: (node.position.y + 120) / 3, r: "2", fill: "var(--glass-white)60" }, `mini-${node.id}`)) }) }), jsx("div", { ref: containerRef, className: 'glass-w-full glass-h-full overflow-hidden', onWheel: handleWheel, onMouseDown: handlePanStart, style: { cursor: isPanning ? "grabbing" : "grab" }, children: jsxs("svg", { ref: svgRef, className: "glass-w-full glass-h-full", style: { transform: `scale(${zoom}) translate(${pan.x / zoom}px, ${pan.y / zoom}px)`, transformOrigin: "center" }, children: [jsx("defs", { children: jsx("marker", { id: "arrowhead", markerWidth: "10", markerHeight: "7", refX: "9", refY: "3.5", orient: "auto", children: jsx("polygon", { points: "0 0, 10 3.5, 0 7", fill: "var(--glass-white)40" }) }) }), renderConnections(), renderNodes()] }) })] }); }; // Utility hook for mind map data management const useMindMap = initialData => { const [data, setData] = useState(initialData); const addNode = (parentId, newNode) => { const updateNode = node => { if (node.id === parentId) { return { ...node, children: [...(node.children || []), newNode] }; } return { ...node, children: node.children?.map(updateNode) }; }; setData(updateNode); }; const updateNode = (nodeId, changes) => { const updateNodeRecursive = node => { if (node.id === nodeId) { return { ...node, ...changes }; } return { ...node, children: node.children?.map(updateNodeRecursive) }; }; setData(updateNodeRecursive); }; const deleteNode = nodeId => { const deleteNodeRecursive = node => { return { ...node, children: node.children?.filter(child => { if (child.id === nodeId) return false; return true; }).map(deleteNodeRecursive) }; }; setData(deleteNodeRecursive); }; return { data, addNode, updateNode, deleteNode, setData }; }; export { GlassMindMap, useMindMap }; //# sourceMappingURL=GlassMindMap.js.map