Spaces:
Running
Running
Download index.html from masoudmm/mindflow-mapper: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/spaces/masoudmm/mindflow-mapper/resolve/main/index.html
- Command line
-
hf download hf://spaces/masoudmm/mindflow-mapper/index.html
-
curl -L -o index.html https://huggingface.co/spaces/masoudmm/mindflow-mapper/resolve/main/index.html
15 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MindFlow Mapper - Visual Thinking Tool</title> | |
| <link rel="icon" type="image/x-icon" href="https://static.photos/abstract/200x200/42"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| overflow: hidden; | |
| touch-action: none; | |
| } | |
| #canvas { | |
| background-color: #f8fafc; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .node { | |
| position: absolute; | |
| min-width: 120px; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| padding: 12px; | |
| cursor: move; | |
| transition: all 0.3s ease; | |
| border: 2px solid transparent; | |
| } | |
| .node:hover { | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| transform: translateY(-2px); | |
| } | |
| .node.selected { | |
| border-color: #3b82f6; | |
| } | |
| .node-input { | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| font-family: inherit; | |
| font-size: inherit; | |
| font-weight: 500; | |
| width: 100%; | |
| resize: none; | |
| } | |
| .connector { | |
| position: absolute; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 12px; | |
| cursor: pointer; | |
| z-index: 10; | |
| } | |
| .controls { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| display: flex; | |
| gap: 0.5rem; | |
| z-index: 100; | |
| } | |
| .control-btn { | |
| background: white; | |
| border-radius: 50%; | |
| width: 48px; | |
| height: 48px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .control-btn:hover { | |
| transform: scale(1.1); | |
| background: #3b82f6; | |
| color: white; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="fixed top-0 left-0 w-full h-16 bg-white shadow-sm flex items-center px-6 z-50"> | |
| <h1 class="text-2xl font-bold text-blue-600 flex items-center gap-2"> | |
| <i data-feather="cpu"></i> | |
| MindFlow Mapper | |
| </h1> | |
| <div class="ml-auto flex gap-4"> | |
| <button id="save-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition flex items-center gap-2"> | |
| <i data-feather="save"></i> | |
| Save | |
| </button> | |
| <button id="load-btn" class="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300 transition flex items-center gap-2"> | |
| <i data-feather="folder"></i> | |
| Load | |
| </button> | |
| <button id="clear-btn" class="px-4 py-2 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition flex items-center gap-2"> | |
| <i data-feather="trash-2"></i> | |
| Clear | |
| </button> | |
| </div> | |
| </div> | |
| <canvas id="canvas"></canvas> | |
| <div class="controls"> | |
| <button id="zoom-in" class="control-btn"> | |
| <i data-feather="zoom-in"></i> | |
| </button> | |
| <button id="zoom-out" class="control-btn"> | |
| <i data-feather="zoom-out"></i> | |
| </button> | |
| <button id="center-btn" class="control-btn"> | |
| <i data-feather="target"></i> | |
| </button> | |
| </div> | |
| <script> | |
| feather.replace(); | |
| // Canvas setup | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let scale = 1; | |
| let offsetX = 0; | |
| let offsetY = 0; | |
| let isDragging = false; | |
| let dragStartX, dragStartY; | |
| let nodes = []; | |
| let selectedNode = null; | |
| let nextId = 1; | |
| let isConnecting = false; | |
| let connectionStartNode = null; | |
| // Initialize canvas size | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| draw(); | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| // Create initial node | |
| function createNode(text, x, y, parentId = null) { | |
| const node = { | |
| id: nextId++, | |
| text, | |
| x, | |
| y, | |
| width: 120, | |
| height: 60, | |
| parentId, | |
| children: [] | |
| }; | |
| nodes.push(node); | |
| if (parentId) { | |
| const parent = nodes.find(n => n.id === parentId); | |
| if (parent) parent.children.push(node.id); | |
| } | |
| createNodeElement(node); | |
| draw(); | |
| return node; | |
| } | |
| // Create DOM element for node | |
| function createNodeElement(node) { | |
| const nodeElement = document.createElement('div'); | |
| nodeElement.className = 'node'; | |
| nodeElement.id = `node-${node.id}`; | |
| nodeElement.style.left = `${node.x}px`; | |
| nodeElement.style.top = `${node.y}px`; | |
| const input = document.createElement('textarea'); | |
| input.className = 'node-input'; | |
| input.value = node.text; | |
| input.rows = 1; | |
| input.addEventListener('input', (e) => { | |
| node.text = e.target.value; | |
| draw(); | |
| }); | |
| nodeElement.appendChild(input); | |
| // Add connector | |
| const connector = document.createElement('div'); | |
| connector.className = 'connector'; | |
| connector.style.bottom = '-12px'; | |
| connector.style.left = 'calc(50% - 12px)'; | |
| connector.innerHTML = '<i data-feather="plus"></i>'; | |
| connector.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| if (isConnecting) { | |
| // Complete connection | |
| if (connectionStartNode && connectionStartNode.id !== node.id) { | |
| node.parentId = connectionStartNode.id; | |
| connectionStartNode.children.push(node.id); | |
| draw(); | |
| } | |
| isConnecting = false; | |
| connectionStartNode = null; | |
| document.querySelectorAll('.node').forEach(n => n.classList.remove('connecting')); | |
| } else { | |
| // Create child node | |
| const newNode = createNode('New Idea', node.x + 100, node.y + 100, node.id); | |
| node.children.push(newNode.id); | |
| draw(); | |
| } | |
| }); | |
| nodeElement.appendChild(connector); | |
| // Drag events | |
| nodeElement.addEventListener('mousedown', (e) => { | |
| if (e.target.tagName === 'TEXTAREA') return; | |
| selectedNode = node; | |
| document.querySelectorAll('.node').forEach(n => n.classList.remove('selected')); | |
| nodeElement.classList.add('selected'); | |
| dragStartX = e.clientX - node.x; | |
| dragStartY = e.clientY - node.y; | |
| e.preventDefault(); | |
| }); | |
| document.body.appendChild(nodeElement); | |
| feather.replace(); | |
| } | |
| // Draw connections | |
| function draw() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw connections | |
| ctx.strokeStyle = '#94a3b8'; | |
| ctx.lineWidth = 2; | |
| nodes.forEach(node => { | |
| if (node.parentId) { | |
| const parent = nodes.find(n => n.id === node.parentId); | |
| if (parent) { | |
| ctx.beginPath(); | |
| ctx.moveTo( | |
| parent.x * scale + parent.width/2 * scale + offsetX, | |
| parent.y * scale + parent.height * scale + offsetY | |
| ); | |
| ctx.lineTo( | |
| node.x * scale + node.width/2 * scale + offsetX, | |
| node.y * scale + offsetY | |
| ); | |
| ctx.stroke(); | |
| } | |
| } | |
| }); | |
| // Update node positions in DOM | |
| nodes.forEach(node => { | |
| const element = document.getElementById(`node-${node.id}`); | |
| if (element) { | |
| element.style.transform = `translate(${node.x * scale + offsetX}px, ${node.y * scale + offsetY}px) scale(${scale})`; | |
| element.style.zIndex = node.id; | |
| } | |
| }); | |
| } | |
| // Mouse events | |
| canvas.addEventListener('mousedown', (e) => { | |
| if (e.button === 1 || (e.button === 0 && e.ctrlKey)) { // Middle mouse or Ctrl+Left | |
| isDragging = true; | |
| dragStartX = e.clientX - offsetX; | |
| dragStartY = e.clientY - offsetY; | |
| canvas.style.cursor = 'grabbing'; | |
| } | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (isDragging) { | |
| offsetX = e.clientX - dragStartX; | |
| offsetY = e.clientY - dragStartY; | |
| draw(); | |
| } | |
| if (selectedNode) { | |
| selectedNode.x = (e.clientX - offsetX) / scale; | |
| selectedNode.y = (e.clientY - offsetY) / scale; | |
| const element = document.getElementById(`node-${selectedNode.id}`); | |
| if (element) { | |
| element.style.left = `${selectedNode.x}px`; | |
| element.style.top = `${selectedNode.y}px`; | |
| } | |
| draw(); | |
| } | |
| }); | |
| canvas.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| selectedNode = null; | |
| canvas.style.cursor = 'default'; | |
| }); | |
| canvas.addEventListener('wheel', (e) => { | |
| e.preventDefault(); | |
| const delta = -e.deltaY; | |
| const zoomIntensity = 0.1; | |
| const zoomFactor = 1 + delta * zoomIntensity / 100; | |
| const newScale = Math.max(0.1, Math.min(3, scale * zoomFactor)); | |
| // Keep center point stable during zoom | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| offsetX = centerX - (centerX - offsetX) * zoomFactor; | |
| offsetY = centerY - (centerY - offsetY) * zoomFactor; | |
| scale = newScale; | |
| document.querySelectorAll('.node').forEach(node => { | |
| node.style.transform = `translate(${parseInt(node.style.left) * scale + offsetX}px, ${parseInt(node.style.top) * scale + offsetY}px) scale(${scale})`; | |
| }); | |
| draw(); | |
| }); | |
| // Double click to create new node | |
| canvas.addEventListener('dblclick', (e) => { | |
| const x = (e.clientX - offsetX) / scale; | |
| const y = (e.clientY - offsetY) / scale; | |
| if (!document.elementFromPoint(e.clientX, e.clientY).classList.contains('node')) { | |
| createNode('New Idea', x, y); | |
| } | |
| }); | |
| // Button events | |
| document.getElementById('zoom-in').addEventListener('click', () => { | |
| scale = Math.min(3, scale * 1.1); | |
| applyTransform(); | |
| }); | |
| document.getElementById('zoom-out').addEventListener('click', () => { | |
| scale = Math.max(0.1, scale / 1.1); | |
| applyTransform(); | |
| }); | |
| document.getElementById('center-btn').addEventListener('click', () => { | |
| offsetX = (canvas.width - (canvas.width * scale)) / 2; | |
| offsetY = (canvas.height - (canvas.height * scale)) / 2; | |
| applyTransform(); | |
| }); | |
| document.getElementById('save-btn').addEventListener('click', () => { | |
| const data = JSON.stringify(nodes); | |
| localStorage.setItem('mindflow-map', data); | |
| alert('Map saved successfully!'); | |
| }); | |
| document.getElementById('load-btn').addEventListener('click', () => { | |
| const data = localStorage.getItem('mindflow-map'); | |
| if (data) { | |
| nodes = JSON.parse(data); | |
| nextId = Math.max(...nodes.map(n => n.id)) + 1; | |
| // Clear existing nodes | |
| document.querySelectorAll('.node').forEach(node => node.remove()); | |
| // Recreate nodes | |
| nodes.forEach(node => createNodeElement(node)); | |
| draw(); | |
| alert('Map loaded successfully!'); | |
| } else { | |
| alert('No saved map found!'); | |
| } | |
| }); | |
| document.getElementById('clear-btn').addEventListener('click', () => { | |
| if (confirm('Are you sure you want to clear the map?')) { | |
| nodes = []; | |
| nextId = 1; | |
| document.querySelectorAll('.node').forEach(node => node.remove()); | |
| draw(); | |
| } | |
| }); | |
| // Apply transform to all nodes | |
| function applyTransform() { | |
| nodes.forEach(node => { | |
| const element = document.getElementById(`node-${node.id}`); | |
| if (element) { | |
| element.style.transform = `translate(${node.x * scale + offsetX}px, ${node.y * scale + offsetY}px) scale(${scale})`; | |
| } | |
| }); | |
| draw(); | |
| } | |
| // Create initial node positioned center left with padding | |
| createNode('Central Idea', 60, canvas.height/2 - 30); | |
| </script> | |
| </body> | |
| </html> | |