✨ Enhanced mindmap visualization with professional UI
🎯 Major Features Added: - Snap to grid functionality (20x20 grid, default enabled) - Tidy Up button for instant node reorganization - Copy node values with one-click clipboard integration - HTML rendering toggle (render/raw modes for HTML content) - Accordion-style collapsible panels (Controls & Legend) - Automatic fitView on fullscreen toggle with smooth animations 🎨 UI/UX Improvements: - Professional accordion layout with exclusive panel opening - Consistent button alignment and styling across all controls - Legend moved to top-right with icon+color indicators - Vertical button stack: Controls → Legend → Tidy Up → Fullscreen - Smooth transitions and hover effects throughout - Clean, uncluttered interface with folded panels by default 🔧 Technical Enhancements: - Fixed ResizeObserver errors with proper error handling - Optimized React rendering with memo and useCallback - Debounced DOM updates to prevent infinite loops - React Flow instance management for programmatic control - Removed redundant Raw Input button for cleaner interface 🚀 Performance & Stability: - Error boundary implementation for ResizeObserver issues - Proper cleanup of event listeners and timeouts - Memoized components to prevent unnecessary re-renders - Smooth 300ms animations for all state transitions
This commit is contained in:
530
package-lock.json
generated
530
package-lock.json
generated
@@ -30,6 +30,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router-dom": "6.26.2",
|
"react-router-dom": "6.26.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"reactflow": "^11.11.4",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
@@ -3099,6 +3100,108 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reactflow/background": {
|
||||||
|
"version": "11.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
|
||||||
|
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reactflow/core": "11.11.4",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reactflow/controls": {
|
||||||
|
"version": "11.2.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
|
||||||
|
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reactflow/core": "11.11.4",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reactflow/core": {
|
||||||
|
"version": "11.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
|
||||||
|
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3": "^7.4.0",
|
||||||
|
"@types/d3-drag": "^3.0.1",
|
||||||
|
"@types/d3-selection": "^3.0.3",
|
||||||
|
"@types/d3-zoom": "^3.0.1",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"d3-drag": "^3.0.0",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reactflow/minimap": {
|
||||||
|
"version": "11.7.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
||||||
|
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reactflow/core": "11.11.4",
|
||||||
|
"@types/d3-selection": "^3.0.3",
|
||||||
|
"@types/d3-zoom": "^3.0.1",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reactflow/node-resizer": {
|
||||||
|
"version": "2.2.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
||||||
|
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reactflow/core": "11.11.4",
|
||||||
|
"classcat": "^5.0.4",
|
||||||
|
"d3-drag": "^3.0.0",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reactflow/node-toolbar": {
|
||||||
|
"version": "1.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
||||||
|
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reactflow/core": "11.11.4",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.19.2",
|
"version": "1.19.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3596,6 +3699,259 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3": {
|
||||||
|
"version": "7.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||||
|
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "*",
|
||||||
|
"@types/d3-axis": "*",
|
||||||
|
"@types/d3-brush": "*",
|
||||||
|
"@types/d3-chord": "*",
|
||||||
|
"@types/d3-color": "*",
|
||||||
|
"@types/d3-contour": "*",
|
||||||
|
"@types/d3-delaunay": "*",
|
||||||
|
"@types/d3-dispatch": "*",
|
||||||
|
"@types/d3-drag": "*",
|
||||||
|
"@types/d3-dsv": "*",
|
||||||
|
"@types/d3-ease": "*",
|
||||||
|
"@types/d3-fetch": "*",
|
||||||
|
"@types/d3-force": "*",
|
||||||
|
"@types/d3-format": "*",
|
||||||
|
"@types/d3-geo": "*",
|
||||||
|
"@types/d3-hierarchy": "*",
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-path": "*",
|
||||||
|
"@types/d3-polygon": "*",
|
||||||
|
"@types/d3-quadtree": "*",
|
||||||
|
"@types/d3-random": "*",
|
||||||
|
"@types/d3-scale": "*",
|
||||||
|
"@types/d3-scale-chromatic": "*",
|
||||||
|
"@types/d3-selection": "*",
|
||||||
|
"@types/d3-shape": "*",
|
||||||
|
"@types/d3-time": "*",
|
||||||
|
"@types/d3-time-format": "*",
|
||||||
|
"@types/d3-timer": "*",
|
||||||
|
"@types/d3-transition": "*",
|
||||||
|
"@types/d3-zoom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-axis": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-brush": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-chord": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-contour": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "*",
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-delaunay": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-dispatch": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-dsv": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-fetch": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-dsv": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-force": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-format": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-geo": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-hierarchy": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-polygon": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-quadtree": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-random": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale-chromatic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time-format": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.56.12",
|
"version": "8.56.12",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3652,6 +4008,12 @@
|
|||||||
"@types/send": "*"
|
"@types/send": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5466,6 +5828,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/classcat": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/classnames": {
|
"node_modules/classnames": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -6246,6 +6614,111 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8974,7 +9447,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "9.0.21",
|
"version": "9.0.21",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -13568,6 +14041,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reactflow": {
|
||||||
|
"version": "11.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
|
||||||
|
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reactflow/background": "11.3.14",
|
||||||
|
"@reactflow/controls": "11.2.14",
|
||||||
|
"@reactflow/core": "11.11.4",
|
||||||
|
"@reactflow/minimap": "11.7.14",
|
||||||
|
"@reactflow/node-resizer": "2.2.14",
|
||||||
|
"@reactflow/node-toolbar": "1.3.14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -15991,6 +16482,15 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -17051,6 +17551,34 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "4.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||||
|
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router-dom": "6.26.2",
|
"react-router-dom": "6.26.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"reactflow": "^11.11.4",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import CsvJsonTool from './pages/CsvJsonTool';
|
|||||||
import BeautifierTool from './pages/BeautifierTool';
|
import BeautifierTool from './pages/BeautifierTool';
|
||||||
import DiffTool from './pages/DiffTool';
|
import DiffTool from './pages/DiffTool';
|
||||||
import TextLengthTool from './pages/TextLengthTool';
|
import TextLengthTool from './pages/TextLengthTool';
|
||||||
|
import ObjectEditor from './pages/ObjectEditor';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ function App() {
|
|||||||
<Route path="/beautifier" element={<BeautifierTool />} />
|
<Route path="/beautifier" element={<BeautifierTool />} />
|
||||||
<Route path="/diff" element={<DiffTool />} />
|
<Route path="/diff" element={<DiffTool />} />
|
||||||
<Route path="/text-length" element={<TextLengthTool />} />
|
<Route path="/text-length" element={<TextLengthTool />} />
|
||||||
|
<Route path="/object-editor" element={<ObjectEditor />} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown, Type } from 'lucide-react';
|
import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown, Type, Edit3 } from 'lucide-react';
|
||||||
import ThemeToggle from './ThemeToggle';
|
import ThemeToggle from './ThemeToggle';
|
||||||
import ToolSidebar from './ToolSidebar';
|
import ToolSidebar from './ToolSidebar';
|
||||||
|
|
||||||
@@ -35,8 +35,7 @@ const Layout = ({ children }) => {
|
|||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{ path: '/json', name: 'JSON Tool', icon: FileText, description: 'Format & validate JSON' },
|
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
|
||||||
{ path: '/serialize', name: 'Serialize Tool', icon: Database, description: 'PHP serialize/unserialize' },
|
|
||||||
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
|
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
|
||||||
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
||||||
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
||||||
|
|||||||
687
src/components/MindmapView.js
Normal file
687
src/components/MindmapView.js
Normal file
@@ -0,0 +1,687 @@
|
|||||||
|
import React, { useMemo, useCallback } from 'react';
|
||||||
|
import ReactFlow, {
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Controls,
|
||||||
|
MiniMap,
|
||||||
|
Background,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
addEdge,
|
||||||
|
ConnectionLineType,
|
||||||
|
Panel,
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
} from 'reactflow';
|
||||||
|
import 'reactflow/dist/style.css';
|
||||||
|
import {
|
||||||
|
Braces,
|
||||||
|
List,
|
||||||
|
Type,
|
||||||
|
Hash,
|
||||||
|
ToggleLeft,
|
||||||
|
Calendar,
|
||||||
|
FileText,
|
||||||
|
Zap,
|
||||||
|
Copy,
|
||||||
|
Eye,
|
||||||
|
Code,
|
||||||
|
Maximize,
|
||||||
|
Minimize,
|
||||||
|
Sparkles,
|
||||||
|
Settings,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// Custom node component for different data types
|
||||||
|
const CustomNode = ({ data, selected }) => {
|
||||||
|
const [renderHtml, setRenderHtml] = React.useState(true);
|
||||||
|
|
||||||
|
// Check if value contains HTML
|
||||||
|
const isHtmlContent = data.value && typeof data.value === 'string' &&
|
||||||
|
(data.value.includes('<') && data.value.includes('>'));
|
||||||
|
|
||||||
|
// Copy value to clipboard
|
||||||
|
const copyValue = async () => {
|
||||||
|
if (data.value) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(String(data.value));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'object':
|
||||||
|
return <Braces className="h-4 w-4" />;
|
||||||
|
case 'array':
|
||||||
|
return <List className="h-4 w-4" />;
|
||||||
|
case 'string':
|
||||||
|
return <Type className="h-4 w-4" />;
|
||||||
|
case 'number':
|
||||||
|
return <Hash className="h-4 w-4" />;
|
||||||
|
case 'boolean':
|
||||||
|
return <ToggleLeft className="h-4 w-4" />;
|
||||||
|
case 'null':
|
||||||
|
return <Zap className="h-4 w-4" />;
|
||||||
|
default:
|
||||||
|
return <FileText className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeColor = () => {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'object':
|
||||||
|
return 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/20 dark:border-blue-700 dark:text-blue-300';
|
||||||
|
case 'array':
|
||||||
|
return 'bg-green-100 border-green-300 text-green-800 dark:bg-green-900/20 dark:border-green-700 dark:text-green-300';
|
||||||
|
case 'string':
|
||||||
|
return 'bg-purple-100 border-purple-300 text-purple-800 dark:bg-purple-900/20 dark:border-purple-700 dark:text-purple-300';
|
||||||
|
case 'number':
|
||||||
|
return 'bg-orange-100 border-orange-300 text-orange-800 dark:bg-orange-900/20 dark:border-orange-700 dark:text-orange-300';
|
||||||
|
case 'boolean':
|
||||||
|
return 'bg-yellow-100 border-yellow-300 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-700 dark:text-yellow-300';
|
||||||
|
case 'null':
|
||||||
|
return 'bg-gray-100 border-gray-300 text-gray-800 dark:bg-gray-900/20 dark:border-gray-700 dark:text-gray-300';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 border-gray-300 text-gray-800 dark:bg-gray-900/20 dark:border-gray-700 dark:text-gray-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`px-3 py-2 shadow-md rounded-md border-2 ${getNodeColor()} min-w-24 max-w-64 relative text-xs`}>
|
||||||
|
{/* Input handle (left side) */}
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
style={{
|
||||||
|
background: '#555',
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
border: '1px solid #fff',
|
||||||
|
opacity: selected ? 1 : 0.3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Top-right controls - HTML render toggle only */}
|
||||||
|
{isHtmlContent && (
|
||||||
|
<div className="absolute -top-1 -right-1 flex">
|
||||||
|
<button
|
||||||
|
onClick={() => setRenderHtml(true)}
|
||||||
|
className={`px-1.5 py-0.5 text-xs rounded-l ${
|
||||||
|
renderHtml
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
title="Render HTML"
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setRenderHtml(false)}
|
||||||
|
className={`px-1.5 py-0.5 text-xs rounded-r ${
|
||||||
|
!renderHtml
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
title="Show raw HTML"
|
||||||
|
>
|
||||||
|
<Code className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-2 group">
|
||||||
|
<div className="flex-shrink-0 flex flex-col items-center space-y-1">
|
||||||
|
<div className="mt-0.5">
|
||||||
|
{getIcon()}
|
||||||
|
</div>
|
||||||
|
{/* Copy button positioned below icon */}
|
||||||
|
{data.value && (
|
||||||
|
<button
|
||||||
|
onClick={copyValue}
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 text-white rounded-full p-1 opacity-80 hover:opacity-100 transition-all shadow-md"
|
||||||
|
title="Copy value to clipboard"
|
||||||
|
>
|
||||||
|
<Copy className="h-2.5 w-2.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium text-xs break-words">
|
||||||
|
{data.label}
|
||||||
|
</div>
|
||||||
|
{data.value !== undefined && (
|
||||||
|
<div className="text-xs opacity-75 mt-1 leading-relaxed">
|
||||||
|
{isHtmlContent && renderHtml ? (
|
||||||
|
<div
|
||||||
|
className="break-words"
|
||||||
|
dangerouslySetInnerHTML={{ __html: String(data.value) }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="break-words whitespace-pre-wrap">
|
||||||
|
{String(data.value)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.count !== undefined && (
|
||||||
|
<div className="text-xs opacity-75 mt-1">
|
||||||
|
{data.count} items
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Output handle (right side) */}
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
style={{
|
||||||
|
background: '#555',
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
border: '1px solid #fff',
|
||||||
|
opacity: selected ? 1 : 0.3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
custom: CustomNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MindmapView = React.memo(({ data }) => {
|
||||||
|
// User preferences state
|
||||||
|
const [edgeType, setEdgeType] = React.useState('default'); // bezier as default
|
||||||
|
const [layoutCompact, setLayoutCompact] = React.useState(true);
|
||||||
|
const [edgeColor, setEdgeColor] = React.useState('#9ca3af'); // gray-400 default
|
||||||
|
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
||||||
|
const [snapToGrid, setSnapToGrid] = React.useState(true);
|
||||||
|
const [activePanel, setActivePanel] = React.useState(null); // 'controls', 'legend', or null
|
||||||
|
// Convert JSON data to React Flow nodes and edges
|
||||||
|
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
|
||||||
|
const nodes = [];
|
||||||
|
const edges = [];
|
||||||
|
let nodeId = 0;
|
||||||
|
const nodeInfo = new Map(); // Store node info for positioning
|
||||||
|
|
||||||
|
// First pass: create nodes and collect structure info
|
||||||
|
const createNodeStructure = (value, key, parentId = null, level = 0) => {
|
||||||
|
const currentId = `node-${nodeId++}`;
|
||||||
|
|
||||||
|
// Determine node type and properties
|
||||||
|
let nodeType, nodeLabel, nodeValue, nodeCount;
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
nodeType = 'null';
|
||||||
|
nodeLabel = key || 'null';
|
||||||
|
nodeValue = 'null';
|
||||||
|
} else if (typeof value === 'boolean') {
|
||||||
|
nodeType = 'boolean';
|
||||||
|
nodeLabel = key || 'boolean';
|
||||||
|
nodeValue = String(value);
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
nodeType = 'number';
|
||||||
|
nodeLabel = key || 'number';
|
||||||
|
nodeValue = String(value);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
nodeType = 'string';
|
||||||
|
nodeLabel = key || 'string';
|
||||||
|
nodeValue = value; // No truncation
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
nodeType = 'array';
|
||||||
|
nodeLabel = key || 'Array';
|
||||||
|
nodeCount = value.length;
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
nodeType = 'object';
|
||||||
|
nodeLabel = key || 'Object';
|
||||||
|
nodeCount = Object.keys(value).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store node info
|
||||||
|
nodeInfo.set(currentId, {
|
||||||
|
id: currentId,
|
||||||
|
parentId,
|
||||||
|
level,
|
||||||
|
type: nodeType,
|
||||||
|
label: nodeLabel,
|
||||||
|
value: nodeValue,
|
||||||
|
count: nodeCount,
|
||||||
|
children: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to parent's children
|
||||||
|
if (parentId && nodeInfo.has(parentId)) {
|
||||||
|
nodeInfo.get(parentId).children.push(currentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create edge from parent
|
||||||
|
if (parentId) {
|
||||||
|
edges.push({
|
||||||
|
id: `edge-${parentId}-${currentId}`,
|
||||||
|
source: parentId,
|
||||||
|
target: currentId,
|
||||||
|
type: edgeType,
|
||||||
|
style: {
|
||||||
|
stroke: edgeColor,
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrowclosed',
|
||||||
|
color: edgeColor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively create child nodes
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item, idx) => {
|
||||||
|
createNodeStructure(item, `[${idx}]`, currentId, level + 1);
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
|
Object.entries(value).forEach(([childKey, childValue]) => {
|
||||||
|
createNodeStructure(childValue, childKey, currentId, level + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estimate node height based on content
|
||||||
|
const estimateNodeHeight = (node) => {
|
||||||
|
const baseHeight = 40; // Base node height
|
||||||
|
const lineHeight = 16; // Approximate line height
|
||||||
|
|
||||||
|
if (node.value && typeof node.value === 'string') {
|
||||||
|
// Estimate lines needed for text wrapping (assuming ~30 chars per line)
|
||||||
|
const estimatedLines = Math.ceil(node.value.length / 30);
|
||||||
|
return baseHeight + (estimatedLines - 1) * lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second pass: calculate positions with vertical centering and collision avoidance
|
||||||
|
const calculatePositions = (nodeId, startY = 0) => {
|
||||||
|
const node = nodeInfo.get(nodeId);
|
||||||
|
if (!node) return startY;
|
||||||
|
|
||||||
|
const baseSpacing = layoutCompact ? 120 : 180;
|
||||||
|
const nodeHeight = estimateNodeHeight(node);
|
||||||
|
const minSpacing = Math.max(baseSpacing, nodeHeight + 20);
|
||||||
|
|
||||||
|
if (node.children.length === 0) {
|
||||||
|
// Leaf node
|
||||||
|
const x = node.level * (layoutCompact ? 250 : 350);
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
type: 'custom',
|
||||||
|
position: { x, y: startY },
|
||||||
|
data: {
|
||||||
|
label: node.label,
|
||||||
|
type: node.type,
|
||||||
|
value: node.value,
|
||||||
|
count: node.count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return startY + minSpacing;
|
||||||
|
} else {
|
||||||
|
// Parent node - calculate children positions first
|
||||||
|
let childStartY = startY;
|
||||||
|
let childEndY = startY;
|
||||||
|
const childPositions = [];
|
||||||
|
|
||||||
|
node.children.forEach(childId => {
|
||||||
|
const childNode = nodeInfo.get(childId);
|
||||||
|
const childHeight = estimateNodeHeight(childNode);
|
||||||
|
const childSpacing = Math.max(baseSpacing, childHeight + 20);
|
||||||
|
|
||||||
|
childEndY = calculatePositions(childId, childStartY);
|
||||||
|
childPositions.push({ start: childStartY, end: childEndY - childSpacing });
|
||||||
|
childStartY = childEndY;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Center parent between first and last child, but ensure minimum spacing
|
||||||
|
let centerY;
|
||||||
|
if (childPositions.length > 0) {
|
||||||
|
const firstChildY = childPositions[0].start;
|
||||||
|
const lastChildY = childPositions[childPositions.length - 1].end;
|
||||||
|
centerY = (firstChildY + lastChildY) / 2;
|
||||||
|
|
||||||
|
// Ensure parent doesn't overlap with any child
|
||||||
|
const parentHeight = nodeHeight;
|
||||||
|
const parentTop = centerY - parentHeight / 2;
|
||||||
|
const parentBottom = centerY + parentHeight / 2;
|
||||||
|
|
||||||
|
// Check for overlaps and adjust if needed
|
||||||
|
for (const childPos of childPositions) {
|
||||||
|
if (parentBottom > childPos.start && parentTop < childPos.end) {
|
||||||
|
// Overlap detected, move parent up
|
||||||
|
centerY = childPos.start - parentHeight / 2 - 10;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
centerY = startY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = node.level * (layoutCompact ? 250 : 350);
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
type: 'custom',
|
||||||
|
position: { x, y: centerY },
|
||||||
|
data: {
|
||||||
|
label: node.label,
|
||||||
|
type: node.type,
|
||||||
|
value: node.value,
|
||||||
|
count: node.count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return childEndY;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data && Object.keys(data).length > 0) {
|
||||||
|
const rootId = createNodeStructure(data, 'root', null, 0);
|
||||||
|
calculatePositions(rootId, 0);
|
||||||
|
} else {
|
||||||
|
// Empty state
|
||||||
|
nodes.push({
|
||||||
|
id: 'empty',
|
||||||
|
type: 'custom',
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
data: {
|
||||||
|
label: 'No Data',
|
||||||
|
type: 'null',
|
||||||
|
value: 'Add data to see mindmap',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}, [data, edgeType, layoutCompact, edgeColor]);
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
// React Flow instance ref for fitView
|
||||||
|
const reactFlowInstance = React.useRef(null);
|
||||||
|
|
||||||
|
// Memoize the tidy up function to prevent unnecessary re-renders
|
||||||
|
const tidyUpNodes = useCallback(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setNodes(initialNodes);
|
||||||
|
}, 0);
|
||||||
|
}, [initialNodes, setNodes]);
|
||||||
|
|
||||||
|
// Accordion panel toggles
|
||||||
|
const toggleControls = () => {
|
||||||
|
setActivePanel(activePanel === 'controls' ? null : 'controls');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleLegend = () => {
|
||||||
|
setActivePanel(activePanel === 'legend' ? null : 'legend');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle fullscreen with fitView
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
setIsFullscreen(!isFullscreen);
|
||||||
|
// Trigger fitView after a short delay to allow DOM to update
|
||||||
|
setTimeout(() => {
|
||||||
|
if (reactFlowInstance.current) {
|
||||||
|
reactFlowInstance.current.fitView({
|
||||||
|
padding: 0.1,
|
||||||
|
minZoom: 0.5,
|
||||||
|
maxZoom: 1.5,
|
||||||
|
duration: 300
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(params) => setEdges((eds) => addEdge(params, eds)),
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// Update nodes when data changes with debouncing to prevent ResizeObserver errors
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setNodes(initialNodes);
|
||||||
|
setEdges(initialEdges);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [initialNodes, initialEdges, setNodes, setEdges]);
|
||||||
|
|
||||||
|
// Suppress ResizeObserver errors
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleResizeObserverError = (e) => {
|
||||||
|
if (e.message === 'ResizeObserver loop completed with undelivered notifications.') {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('error', handleResizeObserverError);
|
||||||
|
return () => window.removeEventListener('error', handleResizeObserverError);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-full border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 ${
|
||||||
|
isFullscreen
|
||||||
|
? 'fixed inset-0 z-50 rounded-none'
|
||||||
|
: 'h-[600px]'
|
||||||
|
}`}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onInit={(instance) => { reactFlowInstance.current = instance; }}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
connectionLineType={ConnectionLineType.SmoothStep}
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{
|
||||||
|
padding: 0.1,
|
||||||
|
minZoom: 0.5,
|
||||||
|
maxZoom: 1.5,
|
||||||
|
}}
|
||||||
|
minZoom={0.3}
|
||||||
|
maxZoom={2}
|
||||||
|
snapToGrid={snapToGrid}
|
||||||
|
snapGrid={[20, 20]}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
defaultEdgeOptions={{
|
||||||
|
type: edgeType,
|
||||||
|
style: { stroke: edgeColor, strokeWidth: 2 },
|
||||||
|
markerEnd: { type: 'arrowclosed', color: edgeColor }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(node) => {
|
||||||
|
switch (node.data.type) {
|
||||||
|
case 'object': return '#3b82f6';
|
||||||
|
case 'array': return '#10b981';
|
||||||
|
case 'string': return '#8b5cf6';
|
||||||
|
case 'number': return '#f59e0b';
|
||||||
|
case 'boolean': return '#eab308';
|
||||||
|
case 'null': return '#6b7280';
|
||||||
|
default: return '#6b7280';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maskColor="rgb(240, 240, 240, 0.6)"
|
||||||
|
/>
|
||||||
|
<Background variant="dots" gap={12} size={1} />
|
||||||
|
{/* Top Right Accordion Panel Stack */}
|
||||||
|
<Panel position="top-right">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Controls Toggle Button */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={toggleControls}
|
||||||
|
className="w-full flex items-center justify-between p-3 text-xs font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
<span>Controls</span>
|
||||||
|
</div>
|
||||||
|
{activePanel === 'controls' ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls Panel Content - Accordion Style */}
|
||||||
|
{activePanel === 'controls' && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 transition-all duration-200 ease-in-out">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-gray-600 dark:text-gray-400 block mb-1">Edge Type</label>
|
||||||
|
<select
|
||||||
|
value={edgeType}
|
||||||
|
onChange={(e) => setEdgeType(e.target.value)}
|
||||||
|
className="text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 w-full"
|
||||||
|
>
|
||||||
|
<option value="default">Bezier</option>
|
||||||
|
<option value="straight">Straight</option>
|
||||||
|
<option value="step">Step</option>
|
||||||
|
<option value="smoothstep">Smooth Step</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-gray-600 dark:text-gray-400 block mb-1">Line Color</label>
|
||||||
|
<select
|
||||||
|
value={edgeColor}
|
||||||
|
onChange={(e) => setEdgeColor(e.target.value)}
|
||||||
|
className="text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 w-full"
|
||||||
|
>
|
||||||
|
<option value="#9ca3af">Gray (Default)</option>
|
||||||
|
<option value="#6366f1">Blue</option>
|
||||||
|
<option value="#10b981">Green</option>
|
||||||
|
<option value="#f59e0b">Orange</option>
|
||||||
|
<option value="#ef4444">Red</option>
|
||||||
|
<option value="#8b5cf6">Purple</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="compact"
|
||||||
|
checked={layoutCompact}
|
||||||
|
onChange={(e) => setLayoutCompact(e.target.checked)}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<label htmlFor="compact" className="text-xs text-gray-600 dark:text-gray-400">Compact Layout</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="snapToGrid"
|
||||||
|
checked={snapToGrid}
|
||||||
|
onChange={(e) => setSnapToGrid(e.target.checked)}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<label htmlFor="snapToGrid" className="text-xs text-gray-600 dark:text-gray-400">Snap to Grid</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Legend Toggle Button */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={toggleLegend}
|
||||||
|
className="w-full flex items-center justify-between p-3 text-xs font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span>Legend</span>
|
||||||
|
</div>
|
||||||
|
{activePanel === 'legend' ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend Panel Content - Accordion Style */}
|
||||||
|
{activePanel === 'legend' && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 transition-all duration-200 ease-in-out">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-4 h-4 bg-blue-100 border-2 border-blue-300 rounded flex items-center justify-center">
|
||||||
|
<Braces className="h-2.5 w-2.5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400">Object</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-4 h-4 bg-green-100 border-2 border-green-300 rounded flex items-center justify-center">
|
||||||
|
<List className="h-2.5 w-2.5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400">Array</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-4 h-4 bg-purple-100 border-2 border-purple-300 rounded flex items-center justify-center">
|
||||||
|
<Type className="h-2.5 w-2.5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400">String</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-4 h-4 bg-orange-100 border-2 border-orange-300 rounded flex items-center justify-center">
|
||||||
|
<Hash className="h-2.5 w-2.5 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400">Number</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-4 h-4 bg-yellow-100 border-2 border-yellow-300 rounded flex items-center justify-center">
|
||||||
|
<ToggleLeft className="h-2.5 w-2.5 text-yellow-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400">Boolean</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tidy Up Button */}
|
||||||
|
<div className="bg-green-500 hover:bg-green-600 rounded-lg shadow-lg transition-colors">
|
||||||
|
<button
|
||||||
|
onClick={tidyUpNodes}
|
||||||
|
className="w-full flex items-center justify-between p-3 text-xs font-medium text-white transition-colors rounded-lg"
|
||||||
|
title="Tidy Up Nodes"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
<span>Tidy Up</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fullscreen Button */}
|
||||||
|
<div className="bg-blue-500 hover:bg-blue-600 rounded-lg shadow-lg transition-colors">
|
||||||
|
<button
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
className="w-full flex items-center justify-between p-3 text-xs font-medium text-white transition-colors rounded-lg"
|
||||||
|
title={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isFullscreen ? <Minimize className="h-4 w-4" /> : <Maximize className="h-4 w-4" />}
|
||||||
|
<span>{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MindmapView;
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react';
|
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react';
|
||||||
|
|
||||||
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||||
const [data, setData] = useState(initialData);
|
const [data, setData] = useState(initialData);
|
||||||
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
||||||
|
|
||||||
|
// Update internal data when initialData prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('📥 INITIAL DATA CHANGED:', {
|
||||||
|
keys: Object.keys(initialData),
|
||||||
|
hasData: Object.keys(initialData).length > 0,
|
||||||
|
data: initialData
|
||||||
|
});
|
||||||
|
setData(initialData);
|
||||||
|
// Expand root node if there's data
|
||||||
|
if (Object.keys(initialData).length > 0) {
|
||||||
|
setExpandedNodes(new Set(['root']));
|
||||||
|
}
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
const updateData = (newData) => {
|
const updateData = (newData) => {
|
||||||
console.log('📊 DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length });
|
console.log('📊 DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length });
|
||||||
setData(newData);
|
setData(newData);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { Search, FileText, Database, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type } from 'lucide-react';
|
import { Search, FileText, Database, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
|
||||||
|
|
||||||
const ToolSidebar = () => {
|
const ToolSidebar = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -9,8 +9,7 @@ const ToolSidebar = () => {
|
|||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
|
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
|
||||||
{ path: '/json', name: 'JSON Tool', icon: FileText, description: 'Format & validate JSON' },
|
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
|
||||||
{ path: '/serialize', name: 'Serialize Tool', icon: Database, description: 'PHP serialize/unserialize' },
|
|
||||||
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
|
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
|
||||||
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
||||||
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type } from 'lucide-react';
|
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type, Edit3 } from 'lucide-react';
|
||||||
import ToolCard from '../components/ToolCard';
|
import ToolCard from '../components/ToolCard';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
@@ -7,18 +7,11 @@ const Home = () => {
|
|||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{
|
{
|
||||||
icon: Code,
|
icon: Edit3,
|
||||||
title: 'JSON Encoder/Decoder',
|
title: 'Object Editor',
|
||||||
description: 'Format, validate, and minify JSON data with syntax highlighting',
|
description: 'Visual editor for JSON and PHP serialized objects with format conversion',
|
||||||
path: '/json',
|
path: '/object-editor',
|
||||||
tags: ['JSON', 'Format', 'Validate']
|
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor']
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Database,
|
|
||||||
title: 'Serialize Encoder/Decoder',
|
|
||||||
description: 'Encode and decode serialized data (PHP serialize format)',
|
|
||||||
path: '/serialize',
|
|
||||||
tags: ['PHP', 'Serialize', 'Unserialize']
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Link2,
|
icon: Link2,
|
||||||
|
|||||||
506
src/pages/ObjectEditor.js
Normal file
506
src/pages/ObjectEditor.js
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { Edit3, Upload, FileText, Download, Copy, Map } from 'lucide-react';
|
||||||
|
import ToolLayout from '../components/ToolLayout';
|
||||||
|
import CopyButton from '../components/CopyButton';
|
||||||
|
import StructuredEditor from '../components/StructuredEditor';
|
||||||
|
import MindmapView from '../components/MindmapView';
|
||||||
|
|
||||||
|
const ObjectEditor = () => {
|
||||||
|
const [structuredData, setStructuredData] = useState({});
|
||||||
|
const [showInput, setShowInput] = useState(false);
|
||||||
|
const [inputText, setInputText] = useState('');
|
||||||
|
const [inputFormat, setInputFormat] = useState('');
|
||||||
|
const [inputValid, setInputValid] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap'
|
||||||
|
const [outputs, setOutputs] = useState({
|
||||||
|
jsonPretty: '',
|
||||||
|
jsonMinified: '',
|
||||||
|
serialized: ''
|
||||||
|
});
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// PHP serialize implementation (reused from SerializeTool)
|
||||||
|
const phpSerialize = (data) => {
|
||||||
|
if (data === null) return 'N;';
|
||||||
|
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
|
||||||
|
if (typeof data === 'number') {
|
||||||
|
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
|
||||||
|
}
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
const byteLength = new TextEncoder().encode(escapedData).length;
|
||||||
|
return `s:${byteLength}:"${escapedData}";`;
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
let result = `a:${data.length}:{`;
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
result += phpSerialize(index) + phpSerialize(item);
|
||||||
|
});
|
||||||
|
result += '}';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
let result = `a:${keys.length}:{`;
|
||||||
|
keys.forEach(key => {
|
||||||
|
result += phpSerialize(key) + phpSerialize(data[key]);
|
||||||
|
});
|
||||||
|
result += '}';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return 'N;';
|
||||||
|
};
|
||||||
|
|
||||||
|
// PHP unserialize implementation (reused from SerializeTool)
|
||||||
|
const phpUnserialize = (str) => {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const parseValue = () => {
|
||||||
|
if (index >= str.length) {
|
||||||
|
throw new Error('Unexpected end of string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = str[index];
|
||||||
|
|
||||||
|
if (type === 'N') {
|
||||||
|
index += 2;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str[index + 1] !== ':') {
|
||||||
|
throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 2;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'b':
|
||||||
|
const boolVal = str[index] === '1';
|
||||||
|
index += 2;
|
||||||
|
return boolVal;
|
||||||
|
|
||||||
|
case 'i':
|
||||||
|
let intStr = '';
|
||||||
|
while (index < str.length && str[index] !== ';') {
|
||||||
|
intStr += str[index++];
|
||||||
|
}
|
||||||
|
if (index >= str.length) {
|
||||||
|
throw new Error('Unexpected end of string while parsing integer');
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
return parseInt(intStr);
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
let floatStr = '';
|
||||||
|
while (index < str.length && str[index] !== ';') {
|
||||||
|
floatStr += str[index++];
|
||||||
|
}
|
||||||
|
if (index >= str.length) {
|
||||||
|
throw new Error('Unexpected end of string while parsing float');
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
return parseFloat(floatStr);
|
||||||
|
|
||||||
|
case 's':
|
||||||
|
let lenStr = '';
|
||||||
|
while (index < str.length && str[index] !== ':') {
|
||||||
|
lenStr += str[index++];
|
||||||
|
}
|
||||||
|
if (index >= str.length) {
|
||||||
|
throw new Error('Unexpected end of string while parsing string length');
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
|
if (str[index] !== '"') {
|
||||||
|
throw new Error(`Expected '"' at position ${index}`);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
|
const byteLength = parseInt(lenStr);
|
||||||
|
|
||||||
|
if (isNaN(byteLength) || byteLength < 0) {
|
||||||
|
throw new Error(`Invalid string length: ${lenStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteLength === 0) {
|
||||||
|
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
|
||||||
|
throw new Error(`Expected '";' after empty string at position ${index}`);
|
||||||
|
}
|
||||||
|
index += 2;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = index;
|
||||||
|
let endQuotePos = -1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i < str.length - 1; i++) {
|
||||||
|
if (str[i] === '"' && str[i + 1] === ';') {
|
||||||
|
endQuotePos = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endQuotePos === -1) {
|
||||||
|
throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringVal = str.substring(startIndex, endQuotePos);
|
||||||
|
index = endQuotePos + 2;
|
||||||
|
|
||||||
|
return stringVal;
|
||||||
|
|
||||||
|
case 'a':
|
||||||
|
let arrayLenStr = '';
|
||||||
|
while (index < str.length && str[index] !== ':') {
|
||||||
|
arrayLenStr += str[index++];
|
||||||
|
}
|
||||||
|
if (index >= str.length) {
|
||||||
|
throw new Error('Unexpected end of string while parsing array length');
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
|
if (str[index] !== '{') {
|
||||||
|
throw new Error(`Expected '{' at position ${index}`);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
|
const arrayLength = parseInt(arrayLenStr);
|
||||||
|
if (isNaN(arrayLength) || arrayLength < 0) {
|
||||||
|
throw new Error(`Invalid array length: ${arrayLenStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
let isArray = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < arrayLength; i++) {
|
||||||
|
const key = parseValue();
|
||||||
|
const value = parseValue();
|
||||||
|
result[key] = value;
|
||||||
|
|
||||||
|
if (typeof key !== 'number' || key !== i) {
|
||||||
|
isArray = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= str.length || str[index] !== '}') {
|
||||||
|
throw new Error(`Expected '}' at position ${index}`);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
|
if (isArray && arrayLength > 0) {
|
||||||
|
const arr = [];
|
||||||
|
for (let i = 0; i < arrayLength; i++) {
|
||||||
|
arr[i] = result[i];
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown type: '${type}' at position ${index - 2}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = parseValue();
|
||||||
|
if (index < str.length) {
|
||||||
|
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Parse error at position ${index}: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-detect input format
|
||||||
|
const detectInputFormat = (input) => {
|
||||||
|
if (!input.trim()) return { format: '', valid: false, data: null };
|
||||||
|
|
||||||
|
// Try JSON first
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(input);
|
||||||
|
return { format: 'JSON', valid: true, data: jsonData };
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Try PHP serialize
|
||||||
|
try {
|
||||||
|
const serializedData = phpUnserialize(input);
|
||||||
|
return { format: 'PHP Serialized', valid: true, data: serializedData };
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return { format: 'Unknown', valid: false, data: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle input text change
|
||||||
|
const handleInputChange = (value) => {
|
||||||
|
setInputText(value);
|
||||||
|
const detection = detectInputFormat(value);
|
||||||
|
setInputFormat(detection.format);
|
||||||
|
setInputValid(detection.valid);
|
||||||
|
|
||||||
|
if (detection.valid) {
|
||||||
|
console.log('🎯 SETTING STRUCTURED DATA:', detection.data);
|
||||||
|
setStructuredData(detection.data);
|
||||||
|
setError('');
|
||||||
|
} else if (value.trim()) {
|
||||||
|
setError('Invalid format. Please enter valid JSON or PHP serialized data.');
|
||||||
|
} else {
|
||||||
|
setError('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle structured data change from visual editor
|
||||||
|
const handleStructuredDataChange = (newData) => {
|
||||||
|
setStructuredData(newData);
|
||||||
|
generateOutputs(newData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate all output formats
|
||||||
|
const generateOutputs = (data) => {
|
||||||
|
try {
|
||||||
|
const jsonPretty = JSON.stringify(data, null, 2);
|
||||||
|
const jsonMinified = JSON.stringify(data);
|
||||||
|
const serialized = phpSerialize(data);
|
||||||
|
|
||||||
|
setOutputs({
|
||||||
|
jsonPretty,
|
||||||
|
jsonMinified,
|
||||||
|
serialized
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating outputs:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle file import
|
||||||
|
const handleFileImport = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target.result;
|
||||||
|
setInputText(content);
|
||||||
|
handleInputChange(content);
|
||||||
|
setShowInput(true);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load sample data
|
||||||
|
const loadSample = () => {
|
||||||
|
const sample = {
|
||||||
|
"user": {
|
||||||
|
"name": "John Doe",
|
||||||
|
"age": 30,
|
||||||
|
"email": "john@example.com",
|
||||||
|
"preferences": {
|
||||||
|
"theme": "dark",
|
||||||
|
"notifications": true
|
||||||
|
},
|
||||||
|
"tags": ["developer", "javascript", "react"]
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"language": "en",
|
||||||
|
"timezone": "UTC"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setStructuredData(sample);
|
||||||
|
generateOutputs(sample);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize outputs when component mounts or data changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
generateOutputs(structuredData);
|
||||||
|
}, [structuredData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolLayout
|
||||||
|
title="Object Editor"
|
||||||
|
description="Visual editor for JSON and PHP serialized objects with format conversion"
|
||||||
|
icon={Edit3}
|
||||||
|
>
|
||||||
|
{/* Input Controls */}
|
||||||
|
<div className="flex flex-wrap gap-3 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowInput(!showInput)}
|
||||||
|
className="flex items-center space-x-2 tool-button"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span>{showInput ? 'Hide Input' : 'Input Data'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="flex items-center space-x-2 tool-button-secondary"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
<span>Import File</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={loadSample}
|
||||||
|
className="flex items-center space-x-2 tool-button-secondary"
|
||||||
|
>
|
||||||
|
<Edit3 className="h-4 w-4" />
|
||||||
|
<span>Load Sample</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json,.txt"
|
||||||
|
onChange={handleFileImport}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Mode Toggle */}
|
||||||
|
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('visual')}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||||
|
viewMode === 'visual'
|
||||||
|
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-4 w-4" />
|
||||||
|
<span>Visual Editor</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('mindmap')}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||||
|
viewMode === 'mindmap'
|
||||||
|
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Map className="h-4 w-4" />
|
||||||
|
<span>Mindmap View</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Section */}
|
||||||
|
{showInput && (
|
||||||
|
<div className="mb-6 space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Input Data
|
||||||
|
</label>
|
||||||
|
{inputFormat && (
|
||||||
|
<span className={`text-xs px-2 py-1 rounded ${
|
||||||
|
inputValid
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
|
||||||
|
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
||||||
|
}`}>
|
||||||
|
{inputFormat} {inputValid ? '✓' : '✗'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={inputText}
|
||||||
|
onChange={(e) => handleInputChange(e.target.value)}
|
||||||
|
placeholder="Paste JSON or PHP serialized data here..."
|
||||||
|
className="tool-input h-32"
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Editor Area */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
{viewMode === 'visual' && 'Visual Editor'}
|
||||||
|
{viewMode === 'mindmap' && 'Mindmap Visualization'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{viewMode === 'visual' && (
|
||||||
|
<div className="min-h-96 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<StructuredEditor
|
||||||
|
initialData={structuredData}
|
||||||
|
onDataChange={handleStructuredDataChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'mindmap' && (
|
||||||
|
<MindmapView data={structuredData} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Output Actions */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* JSON Pretty */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
JSON (Pretty)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
value={outputs.jsonPretty}
|
||||||
|
readOnly
|
||||||
|
placeholder="JSON pretty format will appear here..."
|
||||||
|
className="tool-input h-48 bg-gray-50 dark:bg-gray-800 text-sm font-mono"
|
||||||
|
/>
|
||||||
|
{outputs.jsonPretty && <CopyButton text={outputs.jsonPretty} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JSON Minified */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
JSON (Minified)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
value={outputs.jsonMinified}
|
||||||
|
readOnly
|
||||||
|
placeholder="JSON minified format will appear here..."
|
||||||
|
className="tool-input h-48 bg-gray-50 dark:bg-gray-800 text-sm font-mono"
|
||||||
|
/>
|
||||||
|
{outputs.jsonMinified && <CopyButton text={outputs.jsonMinified} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PHP Serialized */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
PHP Serialized
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
value={outputs.serialized}
|
||||||
|
readOnly
|
||||||
|
placeholder="PHP serialized format will appear here..."
|
||||||
|
className="tool-input h-48 bg-gray-50 dark:bg-gray-800 text-sm font-mono"
|
||||||
|
/>
|
||||||
|
{outputs.serialized && <CopyButton text={outputs.serialized} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Tips */}
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
||||||
|
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
|
||||||
|
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
|
||||||
|
<li>• <strong>Visual Editor:</strong> Create and modify object structures with forms</li>
|
||||||
|
<li>• <strong>Mindmap View:</strong> Visualize complex JSON structures as interactive diagrams</li>
|
||||||
|
<li>• <strong>Input Data:</strong> Paste JSON/PHP serialized data with auto-detection in the input field</li>
|
||||||
|
<li>• Import data from files or use the sample data to get started</li>
|
||||||
|
<li>• Export your data in any format: JSON pretty, minified, or PHP serialized</li>
|
||||||
|
<li>• Use copy buttons to quickly copy any output format</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ToolLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectEditor;
|
||||||
Reference in New Issue
Block a user