feat: new day new graph
This commit is contained in:
634
frontend/package-lock.json
generated
634
frontend/package-lock.json
generated
@@ -16,31 +16,25 @@
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
"@cosmograph/cosmos": "^1.6.1",
|
||||
"@cosmos.gl/graph": "^2.6.4",
|
||||
"@dagrejs/dagre": "^3.0.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@sigma/node-square": "^3.0.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.3.3",
|
||||
"graphology": "^0.26.0",
|
||||
"graphology-layout-force": "^0.2.4",
|
||||
"lucide-react": "^1.7.0",
|
||||
"micron-parser": "^1.0.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-force-graph-3d": "^1.29.1",
|
||||
"react-resizable-panels": "^4.8.0",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"shadcn": "^4.1.1",
|
||||
"sigma": "^3.0.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"three-spritetext": "^1.10.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
@@ -669,70 +663,6 @@
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@cosmograph/cosmos": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@cosmograph/cosmos/-/cosmos-1.6.1.tgz",
|
||||
"integrity": "sha512-A91YabqDxCRqYZXmlOs5ykqkDw1pui0TnuXA65sYFRHwskOJ+BNy7z2IFb/vVIBlsnl3Jdyzm9G93A/xipbHIA==",
|
||||
"deprecated": "This package has been moved to @cosmos.gl/graph. If you're using version >2, please update",
|
||||
"license": "CC-BY-NC-4.0",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.0",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"gl-bench": "^1.0.42",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"random": "^4.1.0",
|
||||
"regl": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.2.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cosmos.gl/graph": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@cosmos.gl/graph/-/graph-2.6.4.tgz",
|
||||
"integrity": "sha512-i+N9lSpAjGLTUPelo/bKNbQnKPDqt3k2UnRlfIWe2Lrambc4J3QFgOfpR8AalQ/1tgLRoeNtVBZ1GPpsNqae5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.0",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"gl-bench": "^1.0.42",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"random": "^4.1.0",
|
||||
"regl": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.2.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dagrejs/dagre": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.0.0.tgz",
|
||||
"integrity": "sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dagrejs/graphlib": "4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@dagrejs/graphlib": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.1.tgz",
|
||||
"integrity": "sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@dotenvx/dotenvx": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz",
|
||||
@@ -1865,15 +1795,6 @@
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sigma/node-square": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sigma/node-square/-/node-square-3.0.0.tgz",
|
||||
"integrity": "sha512-hPX2oWo7WeaSe6M3D56AXsrLyg3F+7N/YsodaJh4Sw3KTce0GAFVWWPZZklu9CITz0xi3kEmlCGulqAH0cVG2w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"sigma": ">=3.0.0-beta.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
@@ -2190,6 +2111,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
@@ -2200,55 +2127,6 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"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-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-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-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-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/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
@@ -2643,64 +2521,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
|
||||
"integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
|
||||
"node_modules/3d-force-graph": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.80.0.tgz",
|
||||
"integrity": "sha512-tzI353gW1nXPpnC7VTa3JjMg+3cp77qOLUFO0vucPTfF+q5R6sQsNsIqVTbRIb7RSypn14nBa4yfkOe9ThxASw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.76",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react/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"
|
||||
"accessor-fn": "1",
|
||||
"kapsule": "^1.16",
|
||||
"three": ">=0.179 <1",
|
||||
"three-forcegraph": "1",
|
||||
"three-render-objects": "^1.41"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
|
||||
"integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -2716,6 +2550,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/accessor-fn": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
|
||||
"integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -3059,12 +2902,6 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"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/cli-cursor": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
@@ -3366,6 +3203,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
@@ -3384,28 +3227,22 @@
|
||||
"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",
|
||||
"node_modules/d3-force-3d": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
||||
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-binarytree": "1",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
"d3-octree": "1",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 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-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
@@ -3427,6 +3264,21 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-octree": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
|
||||
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
@@ -3443,6 +3295,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "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",
|
||||
@@ -3485,36 +3350,13 @@
|
||||
"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",
|
||||
"node_modules/data-bind-mapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz",
|
||||
"integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
"accessor-fn": "1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4017,15 +3859,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
@@ -4355,6 +4188,20 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/float-tooltip": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz",
|
||||
"integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-selection": "2 - 3",
|
||||
"kapsule": "^1.16",
|
||||
"preact": "10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
@@ -4523,18 +4370,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-bench": {
|
||||
"version": "1.0.42",
|
||||
"resolved": "https://registry.npmjs.org/gl-bench/-/gl-bench-1.0.42.tgz",
|
||||
"integrity": "sha512-zuMsA/NCPmI8dPy6q3zTUH8OUM5cqKg7uVWwqzrtXJPBqoypM0XeFWEc8iFOqbf/1qtXieWOrbmgFEByKTQt4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -4579,46 +4414,6 @@
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/graphology": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz",
|
||||
"integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events": "^3.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphology-types": ">=0.24.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphology-layout-force": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/graphology-layout-force/-/graphology-layout-force-0.2.4.tgz",
|
||||
"integrity": "sha512-NYZz0YAnDkn5pkm30cvB0IScFoWGtbzJMrqaiH070dYlYJiag12Oc89dbVfaMaVR/w8DMIKxn/ix9Bqj+Umm9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graphology-utils": "^2.4.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphology-types": ">=0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphology-types": {
|
||||
"version": "0.24.8",
|
||||
"resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
|
||||
"integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/graphology-utils": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
|
||||
"integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"graphology-types": ">=0.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.13.2",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz",
|
||||
@@ -5015,6 +4810,15 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jerrypick": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz",
|
||||
"integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@@ -5120,6 +4924,18 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/kapsule": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz",
|
||||
"integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -5424,6 +5240,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -5471,6 +5293,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -5748,6 +5582,44 @@
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/ngraph.events": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz",
|
||||
"integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ngraph.forcelayout": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
|
||||
"integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ngraph.events": "^1.0.0",
|
||||
"ngraph.merge": "^1.0.0",
|
||||
"ngraph.random": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngraph.graph": {
|
||||
"version": "20.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.2.tgz",
|
||||
"integrity": "sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ngraph.events": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngraph.merge": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
|
||||
"integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ngraph.random": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz",
|
||||
"integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -6106,6 +5978,18 @@
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/polished": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
|
||||
"integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
@@ -6159,6 +6043,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.29.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
|
||||
"integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -6206,6 +6100,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -6264,18 +6169,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/random": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/random/-/random-4.1.0.tgz",
|
||||
"integrity": "sha512-6Ajb7XmMSE9EFAMGC3kg9mvE7fGlBip25mYYuSMzw/uUSrmGilvZo2qwX3RnTRjwXkwkS+4swse9otZ92VjAtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -6321,6 +6214,44 @@
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-force-graph-3d": {
|
||||
"version": "1.29.1",
|
||||
"resolved": "https://registry.npmjs.org/react-force-graph-3d/-/react-force-graph-3d-1.29.1.tgz",
|
||||
"integrity": "sha512-5Vp+PGpYnO+zLwgK2NvNqdXHvsWLrFzpDfJW1vUA1twjo9SPvXqfUYQrnRmAbD+K2tOxkZw1BkbH31l5b4TWHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"3d-force-graph": "^1.79",
|
||||
"prop-types": "15",
|
||||
"react-kapsule": "^2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-kapsule": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz",
|
||||
"integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jerrypick": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.8.0.tgz",
|
||||
@@ -6385,12 +6316,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/regl": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/regl/-/regl-2.1.1.tgz",
|
||||
"integrity": "sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -6568,12 +6493,6 @@
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/seedrandom": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -6787,16 +6706,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sigma": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz",
|
||||
"integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events": "^3.3.0",
|
||||
"graphology-utils": "^2.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -7019,12 +6928,79 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-forcegraph": {
|
||||
"version": "1.43.2",
|
||||
"resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.43.2.tgz",
|
||||
"integrity": "sha512-KUlqDaWVsrYtKx0NVVi5M3NR46K5JQIiPEzZnTMvBq7EHVF2tJpWtgGiAT1mhaerrlJ7F4UGNS2rIVYHmVrzYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accessor-fn": "1",
|
||||
"d3-array": "1 - 3",
|
||||
"d3-force-3d": "2 - 3",
|
||||
"d3-scale": "1 - 4",
|
||||
"d3-scale-chromatic": "1 - 3",
|
||||
"data-bind-mapper": "1",
|
||||
"kapsule": "^1.16",
|
||||
"ngraph.forcelayout": "3",
|
||||
"ngraph.graph": "20",
|
||||
"tinycolor2": "1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.118.3"
|
||||
}
|
||||
},
|
||||
"node_modules/three-render-objects": {
|
||||
"version": "1.41.1",
|
||||
"resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.41.1.tgz",
|
||||
"integrity": "sha512-0H7l7yREPVKfO3HL7RjPQ67T0phHgnyMeEc4ww/OCEfK6jbsm7psEcrR0SGFqGDyS/pDQTPi4DyPbS/xlHRJKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "18 - 25",
|
||||
"accessor-fn": "1",
|
||||
"float-tooltip": "^1.7",
|
||||
"kapsule": "^1.16",
|
||||
"polished": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.179"
|
||||
}
|
||||
},
|
||||
"node_modules/three-spritetext": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/three-spritetext/-/three-spritetext-1.10.0.tgz",
|
||||
"integrity": "sha512-t08iP1FCU1lQh8T5MmCpdijKgas8GDHJE0LqMGBuVu3xqMMpFnEZhTlih7FlxLPQizHIGoumUSpfOlY1GO/Tgg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.86.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
@@ -18,31 +18,25 @@
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
"@cosmograph/cosmos": "^1.6.1",
|
||||
"@cosmos.gl/graph": "^2.6.4",
|
||||
"@dagrejs/dagre": "^3.0.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@sigma/node-square": "^3.0.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.3.3",
|
||||
"graphology": "^0.26.0",
|
||||
"graphology-layout-force": "^0.2.4",
|
||||
"lucide-react": "^1.7.0",
|
||||
"micron-parser": "^1.0.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-force-graph-3d": "^1.29.1",
|
||||
"react-resizable-panels": "^4.8.0",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"shadcn": "^4.1.1",
|
||||
"sigma": "^3.0.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"three-spritetext": "^1.10.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
|
||||
@@ -7,22 +7,39 @@ interface BrowseSearchBarProps {
|
||||
filter: string;
|
||||
onFilterChange: (value: string) => void;
|
||||
onClear: () => void;
|
||||
allNodes: NetworkNode[];
|
||||
suggestions: NetworkNode[];
|
||||
onSelectNode: (node: NetworkNode) => void;
|
||||
onHighlightNode: (node: NetworkNode | null) => void;
|
||||
onSearchFocusChange: (focused: boolean) => void;
|
||||
focusedWinId: string | null;
|
||||
windowCount: number;
|
||||
}
|
||||
|
||||
export default function BrowseSearchBar({
|
||||
filter, onFilterChange, onClear,
|
||||
suggestions, onSelectNode,
|
||||
allNodes, suggestions, onSelectNode, onHighlightNode, onSearchFocusChange,
|
||||
focusedWinId, windowCount,
|
||||
}: BrowseSearchBarProps) {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(-1);
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
// Reset selection when suggestions change
|
||||
useEffect(() => { setSelectedSuggestion(-1); }, [suggestions]);
|
||||
// The visible list: when focused with no filter, show all nodes; otherwise filtered suggestions
|
||||
const visibleList = focused && !filter.trim() ? allNodes : suggestions;
|
||||
const maxVisible = 12;
|
||||
const displayList = visibleList.slice(0, maxVisible);
|
||||
const hasMore = visibleList.length > maxVisible;
|
||||
|
||||
// Reset selection when list changes
|
||||
useEffect(() => { setSelectedSuggestion(-1); }, [visibleList.length, filter]);
|
||||
|
||||
// Notify parent of highlight changes for camera fly-to
|
||||
useEffect(() => {
|
||||
const entry = selectedSuggestion >= 0 ? displayList[selectedSuggestion] : null;
|
||||
onHighlightNode(entry ?? null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedSuggestion, onHighlightNode]);
|
||||
|
||||
// Auto-focus search when no windows open
|
||||
useEffect(() => { searchInputRef.current?.focus(); }, []);
|
||||
@@ -43,6 +60,20 @@ export default function BrowseSearchBar({
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [focusedWinId]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocused(true);
|
||||
onSearchFocusChange(true);
|
||||
}, [onSearchFocusChange]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
// Delay to allow click on suggestion to fire before closing
|
||||
setTimeout(() => {
|
||||
setFocused(false);
|
||||
onSearchFocusChange(false);
|
||||
setSelectedSuggestion(-1);
|
||||
}, 150);
|
||||
}, [onSearchFocusChange]);
|
||||
|
||||
// Draggable position
|
||||
const [searchPos, setSearchPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const searchDragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
||||
@@ -60,19 +91,24 @@ export default function BrowseSearchBar({
|
||||
|
||||
if (!searchPos) return null;
|
||||
|
||||
const showDropdown = focused && displayList.length > 0;
|
||||
|
||||
return createPortal(
|
||||
<div onMouseDown={onSearchDragStart}
|
||||
className="fixed z-999 flex items-center gap-3 px-3 py-1.5 bg-popover border-2 border-border focus-within:border-primary rounded-lg cursor-grab active:cursor-grabbing transition-[border-color] duration-150"
|
||||
className="fixed z-999 flex flex-col bg-popover border-2 border-border focus-within:border-primary rounded-lg cursor-grab active:cursor-grabbing transition-[border-color] duration-150"
|
||||
style={{ left: searchPos.x, top: searchPos.y, width: 400, boxShadow: DITHERED_SHADOW }}>
|
||||
<div className="flex items-center gap-3 px-3 py-1.5">
|
||||
<input ref={searchInputRef} type="text" value={filter}
|
||||
onChange={(e) => onFilterChange(e.target.value)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") { onClear(); e.currentTarget.blur(); return; }
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); setSelectedSuggestion(i => Math.min(i + 1, suggestions.length - 1)); return; }
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); setSelectedSuggestion(i => Math.min(i + 1, displayList.length - 1)); return; }
|
||||
if (e.key === "ArrowUp") { e.preventDefault(); setSelectedSuggestion(i => Math.max(i - 1, -1)); return; }
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const entry = selectedSuggestion >= 0 ? suggestions[selectedSuggestion] : suggestions[0];
|
||||
const entry = selectedSuggestion >= 0 ? displayList[selectedSuggestion] : displayList[0];
|
||||
if (entry && entry.type !== "interface") { onSelectNode(entry); onClear(); }
|
||||
return;
|
||||
}
|
||||
@@ -84,10 +120,15 @@ export default function BrowseSearchBar({
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
{suggestions.length > 0 && (
|
||||
<div className="absolute left-0 right-0 top-full mt-1 bg-popover border border-border rounded-lg overflow-hidden"
|
||||
style={{ boxShadow: "0 4px 12px rgba(0,0,0,0.4)" }}>
|
||||
{suggestions.map((entry, i) => (
|
||||
{!filter && focused && (
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider shrink-0">
|
||||
{allNodes.length} nodes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showDropdown && (
|
||||
<div className="border-t border-border max-h-[360px] overflow-y-auto">
|
||||
{displayList.map((entry, i) => (
|
||||
<button
|
||||
key={entry.hash}
|
||||
onMouseDown={(e) => { e.preventDefault(); if (entry.type !== "interface") { onSelectNode(entry); onClear(); } }}
|
||||
@@ -109,6 +150,11 @@ export default function BrowseSearchBar({
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{hasMore && (
|
||||
<div className="px-3 py-1 text-[9px] text-muted-foreground text-center border-t border-border/50">
|
||||
{visibleList.length - maxVisible} more — type to narrow
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>, document.body);
|
||||
|
||||
@@ -1,101 +1,106 @@
|
||||
import type { NetworkNode } from "@/api/client";
|
||||
import { statusRGBA, type RGBA, type StatusColors } from "./graphColors";
|
||||
import { statusRGBA, rgbaToHex, type StatusColors } from "./graphColors";
|
||||
|
||||
export const SPACE_SIZE = 4096;
|
||||
const CENTER = SPACE_SIZE / 2;
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
name: string;
|
||||
entry: NetworkNode;
|
||||
type: "self" | "peer" | "interface";
|
||||
color: string;
|
||||
size: number;
|
||||
cluster?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
z?: number;
|
||||
}
|
||||
|
||||
export interface BuiltGraph {
|
||||
entries: NetworkNode[];
|
||||
positions: Float32Array;
|
||||
colors: Float32Array;
|
||||
sizes: Float32Array;
|
||||
clusterIndices: (number | undefined)[];
|
||||
clusterPositions: (number | undefined)[];
|
||||
clusterStrength: Float32Array;
|
||||
export interface GraphLink {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[];
|
||||
links: GraphLink[];
|
||||
clusterNames: string[];
|
||||
hashToIndex: Map<string, number>;
|
||||
}
|
||||
|
||||
function findParentIface(
|
||||
entry: NetworkNode,
|
||||
interfaces: NetworkNode[],
|
||||
peerIndex: number,
|
||||
): string | undefined {
|
||||
): NetworkNode | undefined {
|
||||
if (entry.interface) {
|
||||
const iface = interfaces.find(i => i.name === entry.interface);
|
||||
if (iface) return iface.hash;
|
||||
if (iface) return iface;
|
||||
}
|
||||
if (interfaces.length > 0) return interfaces[peerIndex % interfaces.length].hash;
|
||||
if (interfaces.length > 0) return interfaces[peerIndex % interfaces.length];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildGraphArrays(
|
||||
export function buildGraphData(
|
||||
rawNodes: NetworkNode[],
|
||||
prevHashToIndex: Map<string, number>,
|
||||
prevPositions: number[],
|
||||
prevPositions: Map<string, { x: number; y: number; z: number }>,
|
||||
theme: StatusColors,
|
||||
): BuiltGraph {
|
||||
const interfaces = rawNodes.filter(e => e.type === "interface").sort((a, b) => a.name.localeCompare(b.name));
|
||||
): GraphData {
|
||||
const interfaces = rawNodes
|
||||
.filter(e => e.type === "interface")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const selfNode = rawNodes.find(e => e.is_self && e.type !== "interface");
|
||||
const peers = rawNodes.filter(e => !e.is_self && e.type !== "interface");
|
||||
|
||||
const entries = selfNode ? [selfNode, ...peers] : peers;
|
||||
const selfIndex = selfNode ? 0 : -1;
|
||||
const n = entries.length;
|
||||
const nClusters = interfaces.length;
|
||||
const hashToIndex = new Map<string, number>();
|
||||
const positions = new Float32Array(n * 2);
|
||||
const colors = new Float32Array(n * 4);
|
||||
const sizes = new Float32Array(n);
|
||||
const clusterIndices: (number | undefined)[] = [];
|
||||
const clusterStrength = new Float32Array(n);
|
||||
const nodes: GraphNode[] = [];
|
||||
const links: GraphLink[] = [];
|
||||
|
||||
const clusterMap = new Map<string, number>();
|
||||
interfaces.forEach((iface, i) => clusterMap.set(iface.name, i));
|
||||
const selfClusterIndex = interfaces.length;
|
||||
|
||||
const clusterNames = [...interfaces.map(i => i.name), ...(selfNode ? [selfNode.name] : [])];
|
||||
const clusterPositions: (number | undefined)[] = [];
|
||||
|
||||
entries.forEach((entry, i) => {
|
||||
hashToIndex.set(entry.hash, i);
|
||||
|
||||
// Preserve existing position, or jitter near center for new points
|
||||
const prevIdx = prevHashToIndex.get(entry.hash);
|
||||
if (prevIdx !== undefined && prevPositions.length >= (prevIdx + 1) * 2) {
|
||||
positions[i * 2] = prevPositions[prevIdx * 2]!;
|
||||
positions[i * 2 + 1] = prevPositions[prevIdx * 2 + 1]!;
|
||||
} else {
|
||||
// Tighter initial spread for small graphs so they don't scatter
|
||||
const spread = n <= 20 ? 0.1 : n <= 100 ? 0.25 : 0.5;
|
||||
positions[i * 2] = CENTER + (Math.random() - 0.5) * SPACE_SIZE * spread;
|
||||
positions[i * 2 + 1] = CENTER + (Math.random() - 0.5) * SPACE_SIZE * spread;
|
||||
// Add interface nodes
|
||||
for (const iface of interfaces) {
|
||||
const prev = prevPositions.get(iface.hash);
|
||||
nodes.push({
|
||||
id: iface.hash,
|
||||
name: iface.name,
|
||||
entry: iface,
|
||||
type: "interface",
|
||||
color: rgbaToHex(theme.stale),
|
||||
size: 2,
|
||||
...(prev ? { x: prev.x, y: prev.y, z: prev.z } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Self node = bright white, others by status
|
||||
const rgba: RGBA = i === selfIndex
|
||||
? [1, 1, 1, 1]
|
||||
: statusRGBA(entry, theme);
|
||||
colors[i * 4 + 0] = rgba[0];
|
||||
colors[i * 4 + 1] = rgba[1];
|
||||
colors[i * 4 + 2] = rgba[2];
|
||||
colors[i * 4 + 3] = rgba[3];
|
||||
|
||||
// Scale node size inversely with count: bigger dots when fewer nodes
|
||||
sizes[i] = n <= 10 ? 8 : n <= 50 ? 5 : n <= 200 ? 4 : 3;
|
||||
|
||||
// Cluster assignment
|
||||
if (i === selfIndex) {
|
||||
clusterIndices.push(selfClusterIndex);
|
||||
} else {
|
||||
const pi = peers.indexOf(entry);
|
||||
const parentHash = findParentIface(entry, interfaces, pi);
|
||||
const parentName = parentHash ? interfaces.find(f => f.hash === parentHash)?.name : undefined;
|
||||
clusterIndices.push(parentName !== undefined ? clusterMap.get(parentName) : undefined);
|
||||
// Add self node
|
||||
if (selfNode) {
|
||||
const prev = prevPositions.get(selfNode.hash);
|
||||
nodes.push({
|
||||
id: selfNode.hash,
|
||||
name: selfNode.name,
|
||||
entry: selfNode,
|
||||
type: "self",
|
||||
color: "#ffffff",
|
||||
size: 2,
|
||||
...(prev ? { x: prev.x, y: prev.y, z: prev.z } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
clusterStrength[i] = nClusters > 1 ? (nClusters - (i % nClusters)) / nClusters : 1;
|
||||
// Add peer nodes + links to parent interface
|
||||
peers.forEach((peer, i) => {
|
||||
const prev = prevPositions.get(peer.hash);
|
||||
const parentIface = findParentIface(peer, interfaces, i);
|
||||
nodes.push({
|
||||
id: peer.hash,
|
||||
name: peer.name,
|
||||
entry: peer,
|
||||
type: "peer",
|
||||
color: rgbaToHex(statusRGBA(peer, theme)),
|
||||
size: 2,
|
||||
cluster: parentIface?.name,
|
||||
...(prev ? { x: prev.x, y: prev.y, z: prev.z } : {}),
|
||||
});
|
||||
|
||||
return { entries, positions, colors, sizes, clusterIndices, clusterPositions, clusterStrength, clusterNames, hashToIndex };
|
||||
if (parentIface) {
|
||||
links.push({ source: parentIface.hash, target: peer.hash });
|
||||
}
|
||||
});
|
||||
|
||||
const clusterNames = interfaces.map(i => i.name);
|
||||
|
||||
return { nodes, links, clusterNames };
|
||||
}
|
||||
|
||||
@@ -55,3 +55,10 @@ export function statusRGBA(entry: NetworkNode, theme: StatusColors): RGBA {
|
||||
if (age < 3600) return theme.stale;
|
||||
return theme.offline;
|
||||
}
|
||||
|
||||
export function rgbaToHex(c: RGBA): string {
|
||||
const r = Math.round(c[0] * 255);
|
||||
const g = Math.round(c[1] * 255);
|
||||
const b = Math.round(c[2] * 255);
|
||||
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
@@ -426,17 +426,6 @@
|
||||
cursor: nwse-resize !important;
|
||||
}
|
||||
|
||||
/* ── Graph cursors ── */
|
||||
.graph-canvas canvas {
|
||||
cursor: grab !important;
|
||||
}
|
||||
.graph-canvas canvas:active {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
.graph-canvas.graph-node-hovered canvas {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* ── Editor Pointer ── */
|
||||
|
||||
.editor-pointer {
|
||||
|
||||
@@ -1,23 +1,360 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Graph } from "@cosmos.gl/graph";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ForceGraph3D, { type ForceGraphMethods, type NodeObject } from "react-force-graph-3d";
|
||||
import SpriteText from "three-spritetext";
|
||||
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
|
||||
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
|
||||
import { Vector2 } from "three";
|
||||
import { subscribeBrowseNodes, fetchRemotePage, type NetworkNode } from "@/api/client";
|
||||
import { renderMicron } from "@/components/editor/micronRenderer";
|
||||
import { useWindowManager } from "@/hooks/useWindowManager";
|
||||
import { getThemeStatusColors } from "@/components/browse/graphColors";
|
||||
import { buildGraphArrays, SPACE_SIZE } from "@/components/browse/buildGraph";
|
||||
import { buildGraphData, type GraphNode, type GraphData } from "@/components/browse/buildGraph";
|
||||
import type { BrowseWinData, HistoryEntry } from "@/components/browse/types";
|
||||
import BrowseNodeWindow from "@/components/browse/BrowseNodeWindow";
|
||||
import BrowseSearchBar from "@/components/browse/BrowseSearchBar";
|
||||
import Loader from "@/components/shared/Loader";
|
||||
|
||||
function cssVarToHex(varName: string): string {
|
||||
const raw = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type FGNode = NodeObject<GraphNode>;
|
||||
|
||||
function cssVar(name: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
/** Resolve a CSS variable to a normalised hex string (#rrggbb) */
|
||||
function cssVarToHex(name: string): string {
|
||||
const raw = cssVar(name);
|
||||
if (!raw) return "#888888";
|
||||
const ctx = document.createElement("canvas").getContext("2d")!;
|
||||
ctx.fillStyle = raw;
|
||||
return ctx.fillStyle;
|
||||
return ctx.fillStyle; // always "#rrggbb"
|
||||
}
|
||||
|
||||
const DIM_COLOR = "rgba(60,60,60,0.15)";
|
||||
const DIM_LINK = "rgba(60,60,60,0.03)";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Retro post-processing shader: pixelation + posterize + scanlines + vignette
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const RetroShader = {
|
||||
uniforms: {
|
||||
tDiffuse: { value: null },
|
||||
resolution: { value: new Vector2(800, 600) },
|
||||
pixelSize: { value: 2.0 },
|
||||
colorLevels: { value: 48.0 },
|
||||
scanlineIntensity: { value: 0.03 },
|
||||
scanlineDensity: { value: 1.0 },
|
||||
vignetteIntensity: { value: 0.15 },
|
||||
tintColor: { value: [1.0, 0.95, 0.85] },
|
||||
},
|
||||
vertexShader: /* glsl */ `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: /* glsl */ `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform vec2 resolution;
|
||||
uniform float pixelSize;
|
||||
uniform float colorLevels;
|
||||
uniform float scanlineIntensity;
|
||||
uniform float scanlineDensity;
|
||||
uniform float vignetteIntensity;
|
||||
uniform vec3 tintColor;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
// Pixelation
|
||||
vec2 dxy = pixelSize / resolution;
|
||||
vec2 coord = dxy * floor(vUv / dxy) + dxy * 0.5;
|
||||
vec4 color = texture2D(tDiffuse, coord);
|
||||
|
||||
// Posterize (reduce color depth)
|
||||
color.rgb = floor(color.rgb * colorLevels + 0.5) / colorLevels;
|
||||
|
||||
// Subtle tint towards theme color
|
||||
color.rgb *= tintColor;
|
||||
|
||||
// Scanlines
|
||||
float scanline = sin(vUv.y * resolution.y * scanlineDensity) * 0.5 + 0.5;
|
||||
color.rgb -= scanlineIntensity * (1.0 - scanline);
|
||||
|
||||
// Vignette
|
||||
vec2 vig = vUv * (1.0 - vUv);
|
||||
float vigFactor = pow(vig.x * vig.y * 15.0, vignetteIntensity);
|
||||
color.rgb *= vigFactor;
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Memoized 3D graph — isolated from window/UI state changes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type FlyToFn = (pos: { x: number; y: number; z: number }, lookAt: any, durationMs: number) => void;
|
||||
|
||||
interface Graph3DProps {
|
||||
graphData: GraphData;
|
||||
searchMatchIds: Set<string> | null;
|
||||
themeRev: number;
|
||||
width: number;
|
||||
height: number;
|
||||
onNodeClick: (node: NetworkNode) => void;
|
||||
fgRef: React.MutableRefObject<ForceGraphMethods<FGNode> | undefined>;
|
||||
flyToRef: React.MutableRefObject<FlyToFn | undefined>;
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
const Graph3D = memo(function Graph3D({ graphData, searchMatchIds, themeRev, width, height, onNodeClick, fgRef, flyToRef, containerRef }: Graph3DProps) {
|
||||
// react-kapsule diffs props during every render — graphData triggers a full
|
||||
// simulation restart (alpha=1). Stabilise the reference.
|
||||
const stableDataRef = useRef(graphData);
|
||||
const prevNodeIds = useRef("");
|
||||
const nodeIds = graphData.nodes.map(n => n.id).join(",");
|
||||
if (nodeIds !== prevNodeIds.current) {
|
||||
stableDataRef.current = graphData;
|
||||
prevNodeIds.current = nodeIds;
|
||||
}
|
||||
|
||||
// Dim non-matching nodes/links during search
|
||||
const nodeColor = useCallback((node: FGNode) => {
|
||||
if (!searchMatchIds) return (node as GraphNode).color;
|
||||
return searchMatchIds.has(node.id as string) ? (node as GraphNode).color : DIM_COLOR;
|
||||
}, [searchMatchIds]);
|
||||
|
||||
const linkColor = useCallback((link: any) => {
|
||||
if (!searchMatchIds) return "rgba(100,100,100,0.15)";
|
||||
const srcId = typeof link.source === "object" ? (link.source.id as string) : link.source;
|
||||
const tgtId = typeof link.target === "object" ? (link.target.id as string) : link.target;
|
||||
if (searchMatchIds.has(srcId) || searchMatchIds.has(tgtId)) return "rgba(100,100,100,0.15)";
|
||||
return DIM_LINK;
|
||||
}, [searchMatchIds]);
|
||||
|
||||
// Fly camera to search matches
|
||||
useEffect(() => {
|
||||
const fg = fgRef.current;
|
||||
if (!fg || !searchMatchIds) return;
|
||||
if (searchMatchIds.size <= 10) {
|
||||
fg.zoomToFit(600, 80, (n: FGNode) => searchMatchIds.has(n.id as string));
|
||||
}
|
||||
}, [searchMatchIds, fgRef]);
|
||||
|
||||
// ── Camera auto-orbit with smooth ease-in / ease-out ──
|
||||
const orbitAngleRef = useRef(0);
|
||||
const orbitTargetSpeed = useRef(1.0); // 1 = full speed, 0 = stopped
|
||||
const orbitCurrentSpeed = useRef(0.0); // smoothed value
|
||||
const hoveringNodeRef = useRef(false);
|
||||
const flyingRef = useRef(false); // true while cameraPosition transition is active — hard-blocks orbit
|
||||
const idleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const flyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const IDLE_RESUME_MS = 3000;
|
||||
const BASE_SPEED = Math.PI / 600;
|
||||
const HOVER_FACTOR = 0.1; // 10% speed when hovering
|
||||
const EASE_RATE = 0.02; // lerp factor per tick — smaller = smoother
|
||||
|
||||
/** Fly camera to a position. Completely blocks orbit during the transition. */
|
||||
const flyTo = useCallback((pos: { x: number; y: number; z: number }, lookAt: any, durationMs: number) => {
|
||||
const fg = fgRef.current;
|
||||
if (!fg) return;
|
||||
flyingRef.current = true;
|
||||
orbitCurrentSpeed.current = 0;
|
||||
orbitTargetSpeed.current = 0;
|
||||
if (flyTimerRef.current) clearTimeout(flyTimerRef.current);
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
fg.cameraPosition(pos, lookAt, durationMs);
|
||||
flyTimerRef.current = setTimeout(() => {
|
||||
flyingRef.current = false;
|
||||
const cam = fg.camera();
|
||||
orbitAngleRef.current = Math.atan2(cam.position.x, cam.position.z);
|
||||
idleTimerRef.current = setTimeout(() => { orbitTargetSpeed.current = 1; }, IDLE_RESUME_MS);
|
||||
}, durationMs);
|
||||
}, [fgRef]);
|
||||
|
||||
// Expose flyTo to parent
|
||||
flyToRef.current = flyTo;
|
||||
|
||||
const pauseOrbit = useCallback(() => {
|
||||
orbitTargetSpeed.current = 0;
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
idleTimerRef.current = setTimeout(() => { orbitTargetSpeed.current = 1; }, IDLE_RESUME_MS);
|
||||
}, []);
|
||||
|
||||
const onNodeHover = useCallback((node: FGNode | null) => {
|
||||
hoveringNodeRef.current = !!node;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const events = ["mousedown", "wheel", "touchstart"] as const;
|
||||
for (const evt of events) el.addEventListener(evt, pauseOrbit, { passive: true });
|
||||
return () => { for (const evt of events) el.removeEventListener(evt, pauseOrbit); };
|
||||
}, [pauseOrbit, containerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const fg = fgRef.current;
|
||||
if (!fg || flyingRef.current) return;
|
||||
|
||||
// Smooth target: full speed or hover-reduced
|
||||
const target = orbitTargetSpeed.current * (hoveringNodeRef.current ? HOVER_FACTOR : 1);
|
||||
// Ease towards target
|
||||
orbitCurrentSpeed.current += (target - orbitCurrentSpeed.current) * EASE_RATE;
|
||||
// Skip negligible movement
|
||||
if (Math.abs(orbitCurrentSpeed.current) < 0.001) return;
|
||||
|
||||
const cam = fg.camera();
|
||||
const distance = Math.sqrt(cam.position.x ** 2 + cam.position.z ** 2) || 400;
|
||||
orbitAngleRef.current += BASE_SPEED * orbitCurrentSpeed.current;
|
||||
fg.cameraPosition({
|
||||
x: distance * Math.sin(orbitAngleRef.current),
|
||||
z: distance * Math.cos(orbitAngleRef.current),
|
||||
});
|
||||
}, 20);
|
||||
return () => { clearInterval(interval); if (idleTimerRef.current) clearTimeout(idleTimerRef.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fgRef]);
|
||||
|
||||
// ── Post-processing: bloom + retro ──
|
||||
const postProcInitRef = useRef(false);
|
||||
const retroPassRef = useRef<ShaderPass | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fg = fgRef.current;
|
||||
if (!fg || postProcInitRef.current) return;
|
||||
// Wait a tick for the renderer to be ready
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const composer = fg.postProcessingComposer();
|
||||
// Bloom — subtle glow
|
||||
const bloom = new UnrealBloomPass(new Vector2(width, height), 0.3, 0.3, 0.9);
|
||||
composer.addPass(bloom);
|
||||
// Retro shader
|
||||
const retro = new ShaderPass(RetroShader);
|
||||
retro.uniforms.resolution.value.set(width, height);
|
||||
// Tint towards theme primary
|
||||
const hex = cssVarToHex("--primary");
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
// Blend towards white so the tint is subtle
|
||||
retro.uniforms.tintColor.value = [0.7 + r * 0.3, 0.7 + g * 0.3, 0.7 + b * 0.3];
|
||||
composer.addPass(retro);
|
||||
retroPassRef.current = retro;
|
||||
postProcInitRef.current = true;
|
||||
} catch { /* renderer not ready yet, will retry */ }
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fgRef, width, height]);
|
||||
|
||||
// Update retro tint when theme changes
|
||||
useEffect(() => {
|
||||
const retro = retroPassRef.current;
|
||||
if (!retro) return;
|
||||
const hex = cssVarToHex("--primary");
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
retro.uniforms.tintColor.value = [0.7 + r * 0.3, 0.7 + g * 0.3, 0.7 + b * 0.3];
|
||||
}, [themeRev]);
|
||||
|
||||
// Update resolution uniform on resize
|
||||
useEffect(() => {
|
||||
const retro = retroPassRef.current;
|
||||
if (retro) retro.uniforms.resolution.value.set(width, height);
|
||||
}, [width, height]);
|
||||
|
||||
// ── Custom node objects: text labels above every node ──
|
||||
const nodeThreeObject = useCallback((node: FGNode) => {
|
||||
const gn = node as GraphNode;
|
||||
const sprite = new SpriteText(gn.name);
|
||||
(sprite as any).material.depthWrite = false;
|
||||
(sprite as any).renderOrder = 999;
|
||||
sprite.color = gn.type === "interface"
|
||||
? (cssVar("--foreground") || "#888")
|
||||
: gn.color;
|
||||
sprite.textHeight = gn.type === "interface" ? 4 : 3;
|
||||
sprite.fontFace = "JetBrains Mono, monospace";
|
||||
sprite.fontWeight = gn.type === "interface" ? "700" : "400";
|
||||
sprite.backgroundColor = "transparent";
|
||||
if (gn.type === "interface") {
|
||||
(sprite as any).center.set(0.5, 0.5);
|
||||
} else {
|
||||
(sprite as any).center.set(0.5, 2.5);
|
||||
}
|
||||
return sprite;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [themeRev]);
|
||||
|
||||
const nodeVisibility = useCallback((node: FGNode) => {
|
||||
return (node as GraphNode).type !== "interface";
|
||||
}, []);
|
||||
|
||||
const nodeThreeObjectExtend = useCallback((node: FGNode) => {
|
||||
return (node as GraphNode).type !== "interface";
|
||||
}, []);
|
||||
|
||||
const showPointerCursor = useCallback((obj: any) => {
|
||||
if (!obj || !("type" in obj)) return false;
|
||||
return (obj as GraphNode).type !== "interface";
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback((node: FGNode) => {
|
||||
const gn = node as GraphNode;
|
||||
if (gn.type === "interface") return;
|
||||
|
||||
if (node.x !== undefined && node.y !== undefined && node.z !== undefined) {
|
||||
const distance = 40;
|
||||
const dist = Math.hypot(node.x, node.y, node.z);
|
||||
const newPos = dist > 0
|
||||
? { x: node.x * (1 + distance / dist), y: node.y * (1 + distance / dist), z: node.z * (1 + distance / dist) }
|
||||
: { x: 0, y: 0, z: distance };
|
||||
flyTo(newPos, node as any, 1500);
|
||||
}
|
||||
|
||||
onNodeClick(gn.entry);
|
||||
}, [onNodeClick, flyTo]);
|
||||
|
||||
return (
|
||||
<ForceGraph3D
|
||||
ref={fgRef}
|
||||
graphData={stableDataRef.current}
|
||||
width={width}
|
||||
height={height}
|
||||
backgroundColor="rgba(0,0,0,0)"
|
||||
nodeId="id"
|
||||
nodeVal="size"
|
||||
nodeColor={nodeColor}
|
||||
nodeLabel=""
|
||||
nodeOpacity={0.9}
|
||||
nodeResolution={12}
|
||||
nodeVisibility={nodeVisibility}
|
||||
nodeThreeObject={nodeThreeObject}
|
||||
nodeThreeObjectExtend={nodeThreeObjectExtend}
|
||||
onNodeClick={handleClick}
|
||||
onNodeHover={onNodeHover}
|
||||
showPointerCursor={showPointerCursor}
|
||||
linkColor={linkColor}
|
||||
linkWidth={0.3}
|
||||
linkOpacity={0.12}
|
||||
cooldownTicks={150}
|
||||
warmupTicks={0}
|
||||
enableNodeDrag={true}
|
||||
enableNavigationControls={true}
|
||||
showNavInfo={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -26,7 +363,6 @@ export default function BrowseView() {
|
||||
const [nodes, setNodes] = useState<NetworkNode[]>([]);
|
||||
const [filter, setFilter] = useState("");
|
||||
const { windows, focusedId: focusedWinId, open: openWindow, update: updateWindow, close: closeWindowById, focus: focusWindow } = useWindowManager<BrowseWinData>();
|
||||
const [hoveredLabel, setHoveredLabel] = useState<{ text: string; x: number; y: number } | null>(null);
|
||||
|
||||
const [themeRev, setThemeRev] = useState(0);
|
||||
|
||||
@@ -38,290 +374,108 @@ export default function BrowseView() {
|
||||
}, []);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const graphRef = useRef<Graph | null>(null);
|
||||
const entriesRef = useRef<NetworkNode[]>([]);
|
||||
const nodesMapRef = useRef<Map<string, NetworkNode>>(new Map());
|
||||
const hashToIndexRef = useRef<Map<string, number>>(new Map());
|
||||
const clusterNamesRef = useRef<string[]>([]);
|
||||
const hasUserInteractedRef = useRef(false);
|
||||
const initialFitDoneRef = useRef(false);
|
||||
const fgRef = useRef<ForceGraphMethods<FGNode>>(undefined);
|
||||
const flyToRef = useRef<FlyToFn>(undefined);
|
||||
const prevPositionsRef = useRef<Map<string, { x: number; y: number; z: number }>>(new Map());
|
||||
|
||||
// Container sizing
|
||||
const [dims, setDims] = useState<{ w: number; h: number }>({ w: 800, h: 600 });
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(([entry]) => {
|
||||
if (!entry) return;
|
||||
setDims({ w: entry.contentRect.width, h: entry.contentRect.height });
|
||||
});
|
||||
ro.observe(el);
|
||||
setDims({ w: el.clientWidth, h: el.clientHeight });
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
// Build graph data, preserving existing positions
|
||||
const graphData = useMemo(() => {
|
||||
const graph = graphRef.current;
|
||||
const prevPositions = graph ? graph.getPointPositions() : [];
|
||||
return buildGraphArrays(nodes, hashToIndexRef.current, prevPositions, getThemeStatusColors());
|
||||
const fg = fgRef.current;
|
||||
if (fg) {
|
||||
try {
|
||||
// @ts-expect-error — graphData() is on the underlying instance
|
||||
const live = fg.graphData?.() as { nodes: FGNode[] } | undefined;
|
||||
if (live?.nodes) {
|
||||
const map = new Map<string, { x: number; y: number; z: number }>();
|
||||
for (const n of live.nodes) {
|
||||
if (n.x !== undefined && n.y !== undefined && n.z !== undefined) {
|
||||
map.set(n.id as string, { x: n.x, y: n.y, z: n.z });
|
||||
}
|
||||
}
|
||||
prevPositionsRef.current = map;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return buildGraphData(nodes, prevPositionsRef.current, getThemeStatusColors());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodes, themeRev]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = new Map<string, NetworkNode>();
|
||||
for (const n of nodes) map.set(n.hash, n);
|
||||
nodesMapRef.current = map;
|
||||
entriesRef.current = graphData.entries;
|
||||
hashToIndexRef.current = graphData.hashToIndex;
|
||||
clusterNamesRef.current = graphData.clusterNames;
|
||||
}, [nodes, graphData]);
|
||||
|
||||
// Search — matches + autocomplete suggestions
|
||||
const searchMatchIndices = useMemo(() => {
|
||||
const searchMatchIds = useMemo(() => {
|
||||
if (!filter.trim()) return null;
|
||||
const q = filter.trim().toLowerCase();
|
||||
const indices: number[] = [];
|
||||
graphData.entries.forEach((entry, i) => {
|
||||
if (entry.name.toLowerCase().includes(q)) indices.push(i);
|
||||
});
|
||||
return indices.length > 0 ? indices : null;
|
||||
const ids = new Set<string>();
|
||||
for (const n of graphData.nodes) {
|
||||
if (n.name.toLowerCase().includes(q)) ids.add(n.id);
|
||||
}
|
||||
return ids.size > 0 ? ids : null;
|
||||
}, [filter, graphData]);
|
||||
|
||||
const allPeerNodes = useMemo(() => {
|
||||
return graphData.nodes
|
||||
.filter(e => e.type !== "interface")
|
||||
.map(e => e.entry);
|
||||
}, [graphData]);
|
||||
|
||||
const suggestions = useMemo(() => {
|
||||
if (!filter.trim()) return [];
|
||||
const q = filter.trim().toLowerCase();
|
||||
return graphData.entries
|
||||
.filter(e => e.name.toLowerCase().includes(q))
|
||||
.slice(0, 8);
|
||||
}, [filter, graphData]);
|
||||
return allPeerNodes.filter(e => e.name.toLowerCase().includes(q));
|
||||
}, [filter, allPeerNodes]);
|
||||
|
||||
// ── Cluster labels — direct DOM like the example's create-cluster-labels.ts ──
|
||||
const labelDivsRef = useRef<HTMLDivElement[]>([]);
|
||||
const updateClusterLabels = useCallback(() => {
|
||||
const graph = graphRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!graph || !container) return;
|
||||
|
||||
const positions = graph.getClusterPositions();
|
||||
const names = clusterNamesRef.current;
|
||||
const nClusters = Math.min(names.length, positions.length / 2);
|
||||
|
||||
// Rebuild label divs if count changed
|
||||
if (labelDivsRef.current.length !== nClusters) {
|
||||
labelDivsRef.current.forEach(d => d.remove());
|
||||
labelDivsRef.current = [];
|
||||
for (let i = 0; i < nClusters; i++) {
|
||||
const div = document.createElement("div");
|
||||
div.style.position = "absolute";
|
||||
div.style.pointerEvents = "none";
|
||||
div.style.whiteSpace = "nowrap";
|
||||
div.style.transform = "translate(-50%, -100%)";
|
||||
div.style.padding = "0";
|
||||
div.style.background = "none";
|
||||
div.style.border = "none";
|
||||
div.style.color = "var(--foreground)";
|
||||
div.style.fontFamily = "JetBrains Mono, monospace";
|
||||
div.style.fontSize = "14px";
|
||||
div.style.fontWeight = "700";
|
||||
div.style.letterSpacing = "0.05em";
|
||||
div.style.textTransform = "uppercase";
|
||||
div.style.opacity = "0.85";
|
||||
div.textContent = names[i] ?? "";
|
||||
container.appendChild(div);
|
||||
labelDivsRef.current.push(div);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const pad = 8;
|
||||
|
||||
for (let i = 0; i < nClusters; i++) {
|
||||
const x = positions[i * 2];
|
||||
const y = positions[i * 2 + 1];
|
||||
if (x === undefined || y === undefined) continue;
|
||||
const screen = graph.spaceToScreenPosition([x, y]);
|
||||
const div = labelDivsRef.current[i]!;
|
||||
|
||||
// Measure label so we can clamp within container
|
||||
const lw = div.offsetWidth;
|
||||
const lh = div.offsetHeight;
|
||||
// Default transform is translate(-50%, -100%), so anchor is center-bottom
|
||||
let left = screen[0] - lw / 2;
|
||||
let top = screen[1] - lh;
|
||||
// Clamp to container bounds
|
||||
left = Math.max(pad, Math.min(left, rect.width - lw - pad));
|
||||
top = Math.max(pad, Math.min(top, rect.height - lh - pad));
|
||||
|
||||
div.style.transform = "none";
|
||||
div.style.left = `${left}px`;
|
||||
div.style.top = `${top}px`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ── Search highlighting labels ──
|
||||
const searchLabelDivsRef = useRef<Map<number, HTMLDivElement>>(new Map());
|
||||
const searchIndicesRef = useRef<number[] | null>(null);
|
||||
|
||||
const clearSearchLabels = useCallback(() => {
|
||||
searchLabelDivsRef.current.forEach(d => d.remove());
|
||||
searchLabelDivsRef.current.clear();
|
||||
searchIndicesRef.current = null;
|
||||
}, []);
|
||||
|
||||
const updateSearchLabels = useCallback(() => {
|
||||
const graph = graphRef.current;
|
||||
const indices = searchIndicesRef.current;
|
||||
if (!graph || !indices || indices.length === 0) return;
|
||||
|
||||
const positions = graph.getPointPositions();
|
||||
const entries = entriesRef.current;
|
||||
const containerEl = containerRef.current;
|
||||
if (!containerEl) return;
|
||||
const rect = containerEl.getBoundingClientRect();
|
||||
|
||||
for (const idx of indices) {
|
||||
const x = positions[idx * 2];
|
||||
const y = positions[idx * 2 + 1];
|
||||
if (x === undefined || y === undefined) continue;
|
||||
const screen = graph.spaceToScreenPosition([x, y]);
|
||||
|
||||
let div = searchLabelDivsRef.current.get(idx);
|
||||
if (!div) {
|
||||
div = document.createElement("div");
|
||||
div.style.position = "fixed";
|
||||
div.style.pointerEvents = "none";
|
||||
div.style.padding = "2px 8px";
|
||||
div.style.borderRadius = "4px";
|
||||
div.style.background = "var(--popover)";
|
||||
div.style.border = "1px solid var(--primary)";
|
||||
div.style.color = "var(--primary)";
|
||||
div.style.fontFamily = "JetBrains Mono, monospace";
|
||||
div.style.fontWeight = "bold";
|
||||
div.style.fontSize = "11px";
|
||||
div.style.boxShadow = "0 2px 8px rgba(0,0,0,0.4)";
|
||||
div.style.whiteSpace = "nowrap";
|
||||
div.style.zIndex = "998";
|
||||
div.textContent = entries[idx]?.name ?? "";
|
||||
document.body.appendChild(div);
|
||||
searchLabelDivsRef.current.set(idx, div);
|
||||
}
|
||||
div.style.left = `${rect.left + screen[0] + 10}px`;
|
||||
div.style.top = `${rect.top + screen[1] - 8}px`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ── Init Cosmos ──
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const graph = new Graph(containerRef.current, {
|
||||
spaceSize: SPACE_SIZE,
|
||||
backgroundColor: "transparent",
|
||||
pointDefaultColor: "#888888",
|
||||
pointDefaultSize: 10,
|
||||
renderLinks: false,
|
||||
fitViewOnInit: true,
|
||||
fitViewDelay: 1500,
|
||||
fitViewPadding: 0.2,
|
||||
renderHoveredPointRing: true,
|
||||
hoveredPointRingColor: cssVarToHex("--primary"),
|
||||
scalePointsOnZoom: true,
|
||||
pointGreyoutOpacity: 0.1,
|
||||
simulationGravity: 0.5,
|
||||
simulationRepulsion: 1,
|
||||
simulationCluster: 0.5,
|
||||
simulationDecay: 5000,
|
||||
simulationFriction: 0.85,
|
||||
simulationLinkSpring: 0,
|
||||
simulationLinkDistance: 1,
|
||||
onClick: (index, _pos, _event) => {
|
||||
if (index === undefined) return;
|
||||
const entry = entriesRef.current[index];
|
||||
if (entry && entry.type !== "interface") handleNodeClick(entry);
|
||||
},
|
||||
onMouseMove: (index, pointPosition) => {
|
||||
if (index === undefined || !pointPosition || !graphRef.current) {
|
||||
setHoveredLabel(null);
|
||||
containerRef.current?.classList.remove("graph-node-hovered");
|
||||
return;
|
||||
}
|
||||
const entry = entriesRef.current[index];
|
||||
if (!entry) return;
|
||||
if (entry.type !== "interface") {
|
||||
containerRef.current?.classList.add("graph-node-hovered");
|
||||
} else {
|
||||
containerRef.current?.classList.remove("graph-node-hovered");
|
||||
}
|
||||
const screen = graphRef.current.spaceToScreenPosition(pointPosition);
|
||||
setHoveredLabel({ text: entry.name, x: screen[0], y: screen[1] });
|
||||
},
|
||||
onSimulationTick: () => { updateClusterLabels(); updateSearchLabels(); },
|
||||
onSimulationEnd: () => {
|
||||
// Only auto-fit on initial load; after that, respect user's viewport
|
||||
if (!initialFitDoneRef.current) {
|
||||
graphRef.current?.fitView(300, 0.2);
|
||||
initialFitDoneRef.current = true;
|
||||
}
|
||||
updateClusterLabels(); updateSearchLabels();
|
||||
},
|
||||
onZoom: () => { hasUserInteractedRef.current = true; updateClusterLabels(); updateSearchLabels(); },
|
||||
});
|
||||
|
||||
graphRef.current = graph;
|
||||
|
||||
return () => {
|
||||
labelDivsRef.current.forEach(d => d.remove());
|
||||
labelDivsRef.current = [];
|
||||
graph.destroy();
|
||||
graphRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ── Update graph data ──
|
||||
const isFirstLoadRef = useRef(true);
|
||||
useEffect(() => {
|
||||
const graph = graphRef.current;
|
||||
if (!graph || graphData.entries.length === 0) return;
|
||||
|
||||
const n = graphData.entries.length;
|
||||
const scale = Math.log10(Math.max(10, n)) / Math.log10(10000);
|
||||
// Stronger gravity + lower repulsion for small graphs keeps them compact and centered
|
||||
const isSmall = n <= 20;
|
||||
graph.setConfig({
|
||||
simulationRepulsion: isSmall ? 0.3 : 0.5 + scale * 9.5,
|
||||
simulationCluster: 1.0 - scale * 0.75,
|
||||
simulationGravity: isSmall ? 1.0 : 0.25 + scale * 1.75,
|
||||
simulationDecay: isSmall ? 3000 : 5000,
|
||||
hoveredPointRingColor: cssVarToHex("--primary"),
|
||||
});
|
||||
|
||||
graph.setPointPositions(graphData.positions);
|
||||
graph.setPointColors(graphData.colors);
|
||||
graph.setPointSizes(graphData.sizes);
|
||||
graph.setPointClusters(graphData.clusterIndices);
|
||||
graph.setClusterPositions(graphData.clusterPositions);
|
||||
graph.setPointClusterStrength(graphData.clusterStrength);
|
||||
graph.setLinks(new Float32Array(0));
|
||||
|
||||
if (isFirstLoadRef.current) {
|
||||
graph.render(1);
|
||||
isFirstLoadRef.current = false;
|
||||
} else {
|
||||
graph.render(0.1);
|
||||
}
|
||||
updateClusterLabels();
|
||||
// Fly camera to a highlighted search suggestion.
|
||||
// ForceGraph3D mutates graphData.nodes in-place with x/y/z after simulation,
|
||||
// so we read positions directly from the graph data nodes.
|
||||
const onHighlightNode = useCallback((node: NetworkNode | null) => {
|
||||
if (!node) return;
|
||||
const fly = flyToRef.current;
|
||||
if (!fly) return;
|
||||
const match = graphData.nodes.find(n => n.id === node.hash) as FGNode | undefined;
|
||||
if (!match || match.x === undefined || match.y === undefined || match.z === undefined) return;
|
||||
const distance = 60;
|
||||
const dist = Math.hypot(match.x, match.y, match.z);
|
||||
const newPos = dist > 0
|
||||
? { x: match.x * (1 + distance / dist), y: match.y * (1 + distance / dist), z: match.z * (1 + distance / dist) }
|
||||
: { x: 0, y: 0, z: distance };
|
||||
fly(newPos, match as any, 800);
|
||||
}, [graphData]);
|
||||
|
||||
// ── Search highlighting ──
|
||||
const onSearchFocusChange = useCallback((_focused: boolean) => {
|
||||
// Could be used to dim graph when search is active
|
||||
}, []);
|
||||
|
||||
// ── Fly camera to focused window's node ──
|
||||
useEffect(() => {
|
||||
const graph = graphRef.current;
|
||||
if (!graph) return;
|
||||
|
||||
clearSearchLabels();
|
||||
|
||||
if (searchMatchIndices) {
|
||||
searchIndicesRef.current = searchMatchIndices;
|
||||
graph.selectPointsByIndices(searchMatchIndices);
|
||||
if (searchMatchIndices.length <= 10) {
|
||||
graph.fitViewByPointIndices(searchMatchIndices, 500);
|
||||
}
|
||||
updateSearchLabels();
|
||||
} else {
|
||||
graph.unselectPoints();
|
||||
graph.fitView(300, 0.2);
|
||||
}
|
||||
|
||||
return () => clearSearchLabels();
|
||||
}, [searchMatchIndices, clearSearchLabels, updateSearchLabels]);
|
||||
if (!focusedWinId) return;
|
||||
const fly = flyToRef.current;
|
||||
if (!fly) return;
|
||||
const match = graphData.nodes.find(n => n.id === focusedWinId) as FGNode | undefined;
|
||||
if (!match || match.x === undefined || match.y === undefined || match.z === undefined) return;
|
||||
const distance = 60;
|
||||
const dist = Math.hypot(match.x, match.y, match.z);
|
||||
const newPos = dist > 0
|
||||
? { x: match.x * (1 + distance / dist), y: match.y * (1 + distance / dist), z: match.z * (1 + distance / dist) }
|
||||
: { x: 0, y: 0, z: distance };
|
||||
fly(newPos, match as any, 1000);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [focusedWinId]);
|
||||
|
||||
// ── SSE stream ──
|
||||
const nodeMapRef = useRef<Map<string, NetworkNode>>(new Map());
|
||||
useEffect(() => {
|
||||
let pending: NetworkNode[] = [];
|
||||
let batchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -329,11 +483,15 @@ export default function BrowseView() {
|
||||
batchTimer = null;
|
||||
if (pending.length === 0) return;
|
||||
const batch = pending; pending = [];
|
||||
setNodes((prev) => {
|
||||
const map = new Map(prev.map((n) => [n.hash, n]));
|
||||
for (const node of batch) map.set(node.hash, node);
|
||||
return Array.from(map.values());
|
||||
});
|
||||
const map = nodeMapRef.current;
|
||||
let changed = false;
|
||||
for (const node of batch) {
|
||||
if (!map.has(node.hash)) changed = true;
|
||||
map.set(node.hash, node);
|
||||
}
|
||||
if (changed) {
|
||||
setNodes(Array.from(map.values()));
|
||||
}
|
||||
};
|
||||
const unsub = subscribeBrowseNodes((node) => { pending.push(node); if (!batchTimer) batchTimer = setTimeout(flush, 200); });
|
||||
return () => { unsub(); if (batchTimer) clearTimeout(batchTimer); flush(); };
|
||||
@@ -398,13 +556,6 @@ export default function BrowseView() {
|
||||
}, [openWindow, navigateTo]);
|
||||
|
||||
// ── Handle micron link clicks via event delegation ──
|
||||
//
|
||||
// Micron link destinations come in several forms:
|
||||
// /page.mu — same-node, absolute path
|
||||
// page.mu — same-node, relative
|
||||
// :/page/page.mu — NomadNet "request" link (: prefix + /page/ segment)
|
||||
// <32-hex-hash>/page.mu — cross-node link
|
||||
// nomadnetwork://... — already stripped by micron-parser's data-destination
|
||||
const handleContentClick = useCallback((e: React.MouseEvent, winId: string, data: BrowseWinData) => {
|
||||
const anchor = (e.target as HTMLElement).closest("a");
|
||||
if (!anchor) return;
|
||||
@@ -414,63 +565,58 @@ export default function BrowseView() {
|
||||
if (!dest) return;
|
||||
|
||||
let raw = dest
|
||||
.replace(/^nomadnetwork:\/\//, "") // strip scheme if present
|
||||
.replace(/^:/, "") // strip NomadNet request prefix
|
||||
.replace(/^\/page\//, "") // strip /page/ path segment
|
||||
.replace(/^\/+/, ""); // strip remaining leading slashes
|
||||
.replace(/^nomadnetwork:\/\//, "")
|
||||
.replace(/^:/, "")
|
||||
.replace(/^\/page\//, "")
|
||||
.replace(/^\/+/, "");
|
||||
|
||||
// A bare 32-char hex hash with no path — nothing to navigate to
|
||||
if (/^[0-9a-f]{32}$/i.test(raw)) return;
|
||||
|
||||
// If the destination starts with a 32-char hex hash followed by "/",
|
||||
// it's a cross-node link: <hash>/page.mu → use that node's hash
|
||||
let targetNode = data.node;
|
||||
let path = raw;
|
||||
const crossNodeMatch = raw.match(/^([0-9a-f]{32})\/(.+)$/i);
|
||||
if (crossNodeMatch) {
|
||||
const targetHash = crossNodeMatch[1]!;
|
||||
path = crossNodeMatch[2]!;
|
||||
// Strip /page/ from the path portion too
|
||||
path = path.replace(/^\/page\//, "").replace(/^\/+/, "");
|
||||
const known = nodesMapRef.current.get(targetHash);
|
||||
if (known) {
|
||||
targetNode = known;
|
||||
}
|
||||
const known = nodes.find(n => n.hash === targetHash);
|
||||
if (known) targetNode = known;
|
||||
}
|
||||
|
||||
if (!path || path === "/") return;
|
||||
if (!path.endsWith(".mu")) path += ".mu";
|
||||
|
||||
navigateTo(winId, targetNode, path, data);
|
||||
}, [navigateTo]);
|
||||
}, [navigateTo, nodes]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setFilter("");
|
||||
graphRef.current?.unselectPoints();
|
||||
// Re-center when explicitly clearing search
|
||||
graphRef.current?.fitView(300, 0.2);
|
||||
hasUserInteractedRef.current = false;
|
||||
updateClusterLabels();
|
||||
}, [updateClusterLabels]);
|
||||
fgRef.current?.zoomToFit(400, 60);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden" style={{ height: "100%" }}>
|
||||
<div ref={containerRef} className="absolute inset-0 graph-canvas" />
|
||||
|
||||
{/* Hover label */}
|
||||
{hoveredLabel && (
|
||||
<div className="absolute pointer-events-none px-2 py-0.5 bg-popover border border-border rounded text-xs font-mono text-foreground whitespace-nowrap"
|
||||
style={{ left: Math.min(hoveredLabel.x + 12, (containerRef.current?.offsetWidth ?? 9999) - 160), top: Math.max(8, hoveredLabel.y - 10), boxShadow: "0 2px 8px rgba(0,0,0,0.3)" }}>
|
||||
{hoveredLabel.text}
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="relative overflow-hidden" style={{ height: "100%" }}>
|
||||
<Graph3D
|
||||
graphData={graphData}
|
||||
searchMatchIds={searchMatchIds}
|
||||
themeRev={themeRev}
|
||||
width={dims.w}
|
||||
height={dims.h}
|
||||
onNodeClick={handleNodeClick}
|
||||
fgRef={fgRef}
|
||||
flyToRef={flyToRef}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
|
||||
<BrowseSearchBar
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
onClear={clearSearch}
|
||||
allNodes={allPeerNodes}
|
||||
suggestions={suggestions}
|
||||
onSelectNode={handleNodeClick}
|
||||
onHighlightNode={onHighlightNode}
|
||||
onSearchFocusChange={onSearchFocusChange}
|
||||
focusedWinId={focusedWinId}
|
||||
windowCount={windows.length}
|
||||
/>
|
||||
@@ -482,6 +628,10 @@ export default function BrowseView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stop React synthetic events from portaled windows bubbling into the
|
||||
graph — portals bubble through the React tree, not the DOM tree. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div onMouseDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
|
||||
{windows.map((win) => (
|
||||
<BrowseNodeWindow
|
||||
key={win.id}
|
||||
@@ -497,5 +647,6 @@ export default function BrowseView() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
40
frontend/src/types/three-shims.d.ts
vendored
Normal file
40
frontend/src/types/three-shims.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
declare module "three" {
|
||||
export class Vector2 {
|
||||
constructor(x?: number, y?: number);
|
||||
set(x: number, y: number): this;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "three/examples/jsm/postprocessing/UnrealBloomPass.js" {
|
||||
import { Vector2 } from "three";
|
||||
export class UnrealBloomPass {
|
||||
constructor(resolution: Vector2, strength: number, radius: number, threshold: number);
|
||||
strength: number;
|
||||
radius: number;
|
||||
threshold: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "three/examples/jsm/postprocessing/ShaderPass.js" {
|
||||
export class ShaderPass {
|
||||
constructor(shader: any);
|
||||
uniforms: Record<string, { value: any }>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "three/examples/jsm/renderers/CSS2DRenderer.js" {
|
||||
export class CSS2DRenderer {
|
||||
constructor();
|
||||
setSize(width: number, height: number): void;
|
||||
domElement: HTMLElement;
|
||||
render(scene: any, camera: any): void;
|
||||
}
|
||||
export class CSS2DObject {
|
||||
constructor(element: HTMLElement);
|
||||
position: { set(x: number, y: number, z: number): void };
|
||||
center: { set(x: number, y: number): void };
|
||||
layers: { set(n: number): void };
|
||||
}
|
||||
}
|
||||
31
package-lock.json
generated
Normal file
31
package-lock.json
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "micronomicon",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"three-spritetext": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/three-spritetext": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/three-spritetext/-/three-spritetext-1.10.0.tgz",
|
||||
"integrity": "sha512-t08iP1FCU1lQh8T5MmCpdijKgas8GDHJE0LqMGBuVu3xqMMpFnEZhTlih7FlxLPQizHIGoumUSpfOlY1GO/Tgg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.86.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"three-spritetext": "^1.10.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user