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