feat: 3D network graph with trust clustering and broadcast mode
3D Visualization (NetworkGraph3D): - Three.js force-directed graph with React Three Fiber - Trust-level shells: trusted (inner), connected (middle), unconnected (outer) - Node sizing proportional to decision power (incoming connections) - Animated particle flows along edges showing delegation direction - Zoom to user with smooth camera animation - Orbit controls for 3D navigation (drag rotate, scroll zoom) Broadcast Mode: - "View as User" button syncs camera to selected user's view - Visual indicator at top: "Viewing as [User] - ESC to exit" - ESC or X key to stop following - URL deep linking with ?followId parameter UI Improvements: - Panel now stacks directly above tldraw minimap - Matched width (200px) with minimap for alignment - Fixed D3 simulation stability (was reinitializing every render) - 3-state display: minimized icon, normal panel, maximized 3D modal Dependencies: - three@^0.182.0 - @react-three/fiber@8.17.10 (React 18 compatible) - @react-three/drei@9.114.3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0fde2edf05
commit
98a4aee927
|
|
@ -22,6 +22,8 @@
|
|||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@fal-ai/client": "^1.7.2",
|
||||
"@mdxeditor/editor": "^3.51.0",
|
||||
"@react-three/drei": "^9.114.3",
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"@tldraw/assets": "^3.15.4",
|
||||
"@tldraw/tldraw": "^3.15.4",
|
||||
"@tldraw/tlschema": "^3.15.4",
|
||||
|
|
@ -58,6 +60,7 @@
|
|||
"react-router-dom": "^7.0.2",
|
||||
"recoil": "^0.7.7",
|
||||
"sharp": "^0.33.5",
|
||||
"three": "^0.182.0",
|
||||
"tldraw": "^3.15.4",
|
||||
"use-whisper": "^0.0.1",
|
||||
"webcola": "^3.4.0"
|
||||
|
|
@ -2147,6 +2150,12 @@
|
|||
"recoil": "^0.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||
|
|
@ -3892,6 +3901,24 @@
|
|||
"react-dom": ">= 18 || >= 19"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
|
||||
"integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@monogrid/gainmap-js": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
|
||||
"integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-worker-transferable": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz",
|
||||
|
|
@ -5660,6 +5687,205 @@
|
|||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/animated": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
|
||||
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/shared": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/core": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
|
||||
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.6.1",
|
||||
"@react-spring/rafz": "~9.6.1",
|
||||
"@react-spring/shared": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-spring/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/rafz": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
|
||||
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-spring/shared": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
|
||||
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/rafz": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/three": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
|
||||
"integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.6.1",
|
||||
"@react-spring/core": "~9.6.1",
|
||||
"@react-spring/shared": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": ">=6.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"three": ">=0.126"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/types": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
|
||||
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-three/drei": {
|
||||
"version": "9.114.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.114.3.tgz",
|
||||
"integrity": "sha512-hPKPYmxTb2P1mOdhkouJbKJVcfFK5JmThr/97i4zkweoNzWBHNde090A6r0SFFb4tGaTtHM4/kyfVx5PrzjTMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@mediapipe/tasks-vision": "0.10.8",
|
||||
"@monogrid/gainmap-js": "^3.0.5",
|
||||
"@react-spring/three": "~9.6.1",
|
||||
"@use-gesture/react": "^10.2.24",
|
||||
"camera-controls": "^2.4.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-gpu": "^5.0.28",
|
||||
"glsl-noise": "^0.0.0",
|
||||
"hls.js": "1.3.5",
|
||||
"maath": "^0.10.7",
|
||||
"meshline": "^3.1.6",
|
||||
"react-composer": "^5.0.3",
|
||||
"stats-gl": "^2.0.0",
|
||||
"stats.js": "^0.17.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"three-mesh-bvh": "^0.7.8",
|
||||
"three-stdlib": "^2.29.9",
|
||||
"troika-three-text": "^0.49.0",
|
||||
"tunnel-rat": "^0.1.2",
|
||||
"utility-types": "^3.10.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zustand": "^3.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": ">=8.0",
|
||||
"react": ">=18.0",
|
||||
"react-dom": ">=18.0",
|
||||
"three": ">=0.137"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber": {
|
||||
"version": "8.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.17.10.tgz",
|
||||
"integrity": "sha512-S6bqa4DqUooEkInYv/W+Jklv2zjSYCXAhm6qKpAQyOXhTEt5gBXnA7W6aoJ0bjmp9pAeaSj/AZUoz1HCSof/uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/react-reconciler": "^0.26.7",
|
||||
"@types/webxr": "*",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"debounce": "^1.2.1",
|
||||
"its-fine": "^1.0.6",
|
||||
"react-reconciler": "^0.27.0",
|
||||
"scheduler": "^0.21.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"zustand": "^3.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": ">=43.0",
|
||||
"expo-asset": ">=8.4",
|
||||
"expo-file-system": ">=11.0",
|
||||
"expo-gl": ">=11.0",
|
||||
"react": ">=18.0",
|
||||
"react-dom": ">=18.0",
|
||||
"react-native": ">=0.64",
|
||||
"three": ">=0.133"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-asset": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-file-system": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-gl": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber/node_modules/scheduler": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
||||
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remirror/core-constants": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||
|
|
@ -7060,6 +7286,12 @@
|
|||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
|
|
@ -7432,6 +7664,12 @@
|
|||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debounce": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.4.tgz",
|
||||
"integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
|
|
@ -7463,6 +7701,12 @@
|
|||
"@types/trusted-types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/draco3d": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -7633,6 +7877,12 @@
|
|||
"form-data": "^4.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/offscreencanvas": {
|
||||
"version": "2019.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
|
|
@ -7677,7 +7927,6 @@
|
|||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -7693,6 +7942,15 @@
|
|||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-reconciler": {
|
||||
"version": "0.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
|
||||
"integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
|
|
@ -7726,6 +7984,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/statuses": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
|
||||
|
|
@ -7751,6 +8015,21 @@
|
|||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~0.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
|
|
@ -7769,6 +8048,12 @@
|
|||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
|
|
@ -8187,6 +8472,12 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.68",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz",
|
||||
"integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xenova/transformers": {
|
||||
"version": "2.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
|
||||
|
|
@ -8558,7 +8849,6 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
|
|
@ -8814,6 +9104,15 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/camera-controls": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
|
||||
"integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.126.1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001757",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||
|
|
@ -9586,6 +9885,38 @@
|
|||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
|
|
@ -10204,6 +10535,12 @@
|
|||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
|
@ -10308,6 +10645,15 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-gpu": {
|
||||
"version": "5.0.70",
|
||||
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
|
||||
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webgl-constants": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
|
|
@ -10509,6 +10855,12 @@
|
|||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/draco3d": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
|
||||
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -11380,6 +11732,12 @@
|
|||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/glsl-noise": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
|
||||
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
|
@ -11825,6 +12183,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.5.tgz",
|
||||
"integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "3.13.15",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.15.tgz",
|
||||
|
|
@ -12234,6 +12598,12 @@
|
|||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
|
|
@ -12252,6 +12622,12 @@
|
|||
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
|
|
@ -12319,6 +12695,27 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/its-fine": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
|
||||
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react-reconciler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/its-fine/node_modules/@types/react-reconciler": {
|
||||
"version": "0.28.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
||||
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/itty-router": {
|
||||
"version": "5.0.22",
|
||||
"resolved": "https://registry.npmjs.org/itty-router/-/itty-router-5.0.22.tgz",
|
||||
|
|
@ -12705,6 +13102,16 @@
|
|||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/maath": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/three": ">=0.134.0",
|
||||
"three": ">=0.134.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
|
@ -13258,6 +13665,21 @@
|
|||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/meshline": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
|
||||
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
|
||||
"integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
|
|
@ -14745,6 +15167,15 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
|
|
@ -14942,6 +15373,16 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.1.0",
|
||||
"lie": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
|
@ -15440,6 +15881,18 @@
|
|||
"react-dom": "^16.x || ^17.x || ^18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-composer": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz",
|
||||
"integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-devtools-inline": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-devtools-inline/-/react-devtools-inline-4.4.0.tgz",
|
||||
|
|
@ -15527,6 +15980,31 @@
|
|||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
|
||||
"integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler/node_modules/scheduler": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
||||
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
|
@ -16439,6 +16917,27 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
|
|
@ -16635,6 +17134,32 @@
|
|||
"outvariant": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "^0.170.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl/node_modules/three": {
|
||||
"version": "0.170.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
||||
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stats.js": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
|
||||
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
@ -16816,6 +17341,15 @@
|
|||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/suspend-react": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
||||
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
|
|
@ -16934,6 +17468,51 @@
|
|||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz",
|
||||
"integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==",
|
||||
"deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.151.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib": {
|
||||
"version": "2.36.1",
|
||||
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
|
||||
"integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/draco3d": "^1.4.0",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/webxr": "^0.5.2",
|
||||
"draco3d": "^1.4.1",
|
||||
"fflate": "^0.6.9",
|
||||
"potpack": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.128.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib/node_modules/fflate": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-stdlib/node_modules/potpack": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||
|
|
@ -17133,6 +17712,36 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-three-text": {
|
||||
"version": "0.49.1",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz",
|
||||
"integrity": "sha512-lXGWxgjJP9kw4i4Wh+0k0Q/7cRfS6iOME4knKht/KozPu9GcFA9NnNpRvehIhrUawq9B0ZRw+0oiFHgRO+4Wig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.2",
|
||||
"troika-three-utils": "^0.49.0",
|
||||
"troika-worker-utils": "^0.49.0",
|
||||
"webgl-sdf-generator": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-three-utils": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.49.0.tgz",
|
||||
"integrity": "sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-worker-utils": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz",
|
||||
"integrity": "sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/trough": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
||||
|
|
@ -17179,6 +17788,43 @@
|
|||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-rat": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zustand": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-rat/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
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||
|
|
@ -17534,6 +18180,15 @@
|
|||
"integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utility-types": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
|
||||
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
|
@ -18002,6 +18657,17 @@
|
|||
"integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/webgl-constants": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
|
||||
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
|
||||
},
|
||||
"node_modules/webgl-sdf-generator": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
|
|
@ -18062,6 +18728,21 @@
|
|||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
|
|
@ -18812,6 +19493,23 @@
|
|||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
|
||||
"integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@fal-ai/client": "^1.7.2",
|
||||
"@mdxeditor/editor": "^3.51.0",
|
||||
"@react-three/drei": "^9.114.3",
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"@tldraw/assets": "^3.15.4",
|
||||
"@tldraw/tldraw": "^3.15.4",
|
||||
"@tldraw/tlschema": "^3.15.4",
|
||||
|
|
@ -84,6 +86,7 @@
|
|||
"react-router-dom": "^7.0.2",
|
||||
"recoil": "^0.7.7",
|
||||
"sharp": "^0.33.5",
|
||||
"three": "^0.182.0",
|
||||
"tldraw": "^3.15.4",
|
||||
"use-whisper": "^0.0.1",
|
||||
"webcola": "^3.4.0"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,29 +1,37 @@
|
|||
/**
|
||||
* NetworkGraphMinimap Component
|
||||
*
|
||||
* A 2D force-directed graph visualization in the bottom-right corner.
|
||||
* A force-directed social graph visualization positioned above the minimap.
|
||||
* Shows:
|
||||
* - User's full network in grey
|
||||
* - User's full network with trust-level coloring
|
||||
* - Room participants in their presence colors
|
||||
* - Connections as edges between nodes
|
||||
* - Mutual connections as thicker lines
|
||||
*
|
||||
* Features:
|
||||
* - Three display modes: minimized (icon), normal (small window), maximized (modal)
|
||||
* - Click node to view profile / connect
|
||||
* - Click edge to edit metadata
|
||||
* - Hover for tooltips
|
||||
* - Expand button to open full 3D view
|
||||
* - Stable simulation that doesn't constantly reinitialize
|
||||
*
|
||||
* Positioned in bottom-left, above the tldraw minimap.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback, useMemo, Suspense, lazy } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import { type GraphNode, type GraphEdge, type TrustLevel } from '../../lib/networking';
|
||||
import { UserSearchModal } from './UserSearchModal';
|
||||
|
||||
// Lazy load the 3D component to avoid loading Three.js unless needed
|
||||
const NetworkGraph3D = lazy(() => import('./NetworkGraph3D'));
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
type DisplayMode = 'minimized' | 'normal' | 'maximized';
|
||||
|
||||
interface NetworkGraphMinimapProps {
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
|
|
@ -54,54 +62,101 @@ interface SimulationLink extends d3.SimulationLinkDatum<SimulationNode> {
|
|||
// Styles - Theme-aware functions
|
||||
// =============================================================================
|
||||
|
||||
const getStyles = (isDarkMode: boolean) => ({
|
||||
// Match tldraw minimap dimensions
|
||||
const MINIMAP_WIDTH = 200;
|
||||
const MINIMAP_HEIGHT = 100;
|
||||
const MINIMAP_BOTTOM = 40; // tldraw minimap position from bottom
|
||||
const MINIMAP_LEFT = 8; // tldraw minimap position from left
|
||||
const STACK_GAP = 8; // gap between network panel and minimap
|
||||
|
||||
const getStyles = (isDarkMode: boolean, displayMode: DisplayMode) => ({
|
||||
// Container - positioned bottom LEFT, directly above the tldraw minimap
|
||||
container: {
|
||||
position: 'fixed' as const,
|
||||
bottom: '60px',
|
||||
right: '10px',
|
||||
zIndex: 1000,
|
||||
// Stack directly above tldraw minimap: minimap_bottom + minimap_height + gap
|
||||
bottom: displayMode === 'maximized' ? '0' : `${MINIMAP_BOTTOM + MINIMAP_HEIGHT + STACK_GAP}px`,
|
||||
left: displayMode === 'maximized' ? '0' : `${MINIMAP_LEFT}px`,
|
||||
right: displayMode === 'maximized' ? '0' : 'auto',
|
||||
top: displayMode === 'maximized' ? '0' : 'auto',
|
||||
zIndex: displayMode === 'maximized' ? 10000 : 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'flex-end',
|
||||
alignItems: displayMode === 'maximized' ? 'center' : 'flex-start',
|
||||
justifyContent: displayMode === 'maximized' ? 'center' : 'flex-start',
|
||||
gap: '8px',
|
||||
backgroundColor: displayMode === 'maximized' ? 'rgba(0, 0, 0, 0.5)' : 'transparent',
|
||||
pointerEvents: displayMode === 'maximized' ? 'auto' as const : 'none' as const,
|
||||
},
|
||||
// Main panel
|
||||
panel: {
|
||||
backgroundColor: isDarkMode ? 'rgba(20, 20, 25, 0.95)' : 'rgba(255, 255, 255, 0.98)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: isDarkMode ? '0 4px 20px rgba(0, 0, 0, 0.4)' : '0 4px 20px rgba(0, 0, 0, 0.15)',
|
||||
borderRadius: displayMode === 'maximized' ? '16px' : '12px',
|
||||
boxShadow: isDarkMode
|
||||
? '0 4px 20px rgba(0, 0, 0, 0.4)'
|
||||
: displayMode === 'maximized'
|
||||
? '0 8px 40px rgba(0, 0, 0, 0.3)'
|
||||
: '0 4px 20px rgba(0, 0, 0, 0.15)',
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.2s ease',
|
||||
transition: 'all 0.3s ease',
|
||||
border: isDarkMode ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)',
|
||||
pointerEvents: 'auto' as const,
|
||||
// Sphere-like gradient background for the graph area
|
||||
background: isDarkMode
|
||||
? 'radial-gradient(ellipse at center, rgba(40, 40, 50, 0.95) 0%, rgba(20, 20, 25, 0.98) 100%)'
|
||||
: 'radial-gradient(ellipse at center, rgba(255, 255, 255, 0.98) 0%, rgba(245, 245, 250, 0.98) 100%)',
|
||||
},
|
||||
panelCollapsed: {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
// Minimized state - small icon
|
||||
panelMinimized: {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
background: isDarkMode
|
||||
? 'radial-gradient(circle, rgba(60, 60, 80, 0.9) 0%, rgba(30, 30, 40, 0.95) 100%)'
|
||||
: 'radial-gradient(circle, rgba(255, 255, 255, 0.95) 0%, rgba(240, 240, 245, 0.98) 100%)',
|
||||
boxShadow: isDarkMode
|
||||
? '0 2px 12px rgba(100, 100, 255, 0.2), inset 0 0 20px rgba(100, 100, 255, 0.1)'
|
||||
: '0 2px 12px rgba(0, 0, 0, 0.15), inset 0 0 20px rgba(100, 100, 255, 0.05)',
|
||||
border: isDarkMode ? '1px solid rgba(100, 100, 255, 0.3)' : '1px solid rgba(100, 100, 255, 0.2)',
|
||||
},
|
||||
// Normal state dimensions - match tldraw minimap width
|
||||
panelNormal: {
|
||||
width: `${MINIMAP_WIDTH}px`,
|
||||
maxHeight: '200px',
|
||||
},
|
||||
// Maximized state dimensions
|
||||
panelMaximized: {
|
||||
width: '90vw',
|
||||
maxWidth: '800px',
|
||||
maxHeight: '80vh',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 12px',
|
||||
padding: displayMode === 'maximized' ? '12px 16px' : '8px 12px',
|
||||
borderBottom: isDarkMode ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: isDarkMode ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.02)',
|
||||
},
|
||||
title: {
|
||||
fontSize: '12px',
|
||||
fontSize: displayMode === 'maximized' ? '14px' : '12px',
|
||||
fontWeight: 600,
|
||||
color: isDarkMode ? '#e0e0e0' : '#374151',
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
},
|
||||
headerButtons: {
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
},
|
||||
iconButton: {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
width: displayMode === 'maximized' ? '32px' : '28px',
|
||||
height: displayMode === 'maximized' ? '32px' : '28px',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
borderRadius: '6px',
|
||||
|
|
@ -109,31 +164,54 @@ const getStyles = (isDarkMode: boolean) => ({
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px',
|
||||
fontSize: displayMode === 'maximized' ? '16px' : '14px',
|
||||
color: isDarkMode ? '#a0a0a0' : '#6b7280',
|
||||
transition: 'background-color 0.15s, color 0.15s',
|
||||
},
|
||||
canvas: {
|
||||
display: 'block',
|
||||
backgroundColor: isDarkMode ? 'transparent' : 'rgba(249, 250, 251, 0.5)',
|
||||
// Sphere-like inner gradient
|
||||
background: isDarkMode
|
||||
? 'radial-gradient(ellipse at center, rgba(50, 50, 70, 0.3) 0%, transparent 70%)'
|
||||
: 'radial-gradient(ellipse at center, rgba(200, 200, 255, 0.15) 0%, transparent 70%)',
|
||||
},
|
||||
tooltip: {
|
||||
position: 'absolute' as const,
|
||||
backgroundColor: isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.95)',
|
||||
backgroundColor: isDarkMode ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.98)',
|
||||
color: isDarkMode ? '#fff' : '#1f2937',
|
||||
padding: '6px 10px',
|
||||
borderRadius: '6px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
pointerEvents: 'none' as const,
|
||||
whiteSpace: 'nowrap' as const,
|
||||
zIndex: 1001,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
marginTop: '-8px',
|
||||
boxShadow: isDarkMode ? 'none' : '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
border: isDarkMode ? 'none' : '1px solid rgba(0, 0, 0, 0.1)',
|
||||
marginTop: '-10px',
|
||||
boxShadow: isDarkMode ? '0 4px 12px rgba(0, 0, 0, 0.3)' : '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
border: isDarkMode ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
collapsedIcon: {
|
||||
fontSize: '20px',
|
||||
minimizedIcon: {
|
||||
fontSize: '18px',
|
||||
filter: 'drop-shadow(0 0 4px rgba(100, 100, 255, 0.4))',
|
||||
},
|
||||
// Network stats in maximized view
|
||||
statsBar: {
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
padding: '8px 16px',
|
||||
borderBottom: isDarkMode ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: isDarkMode ? 'rgba(255, 255, 255, 0.02)' : 'rgba(0, 0, 0, 0.01)',
|
||||
fontSize: '11px',
|
||||
color: isDarkMode ? '#888' : '#666',
|
||||
},
|
||||
statItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
},
|
||||
statValue: {
|
||||
fontWeight: 600,
|
||||
color: isDarkMode ? '#a0a0ff' : '#4f46e5',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -154,56 +232,181 @@ export function NetworkGraphMinimap({
|
|||
onOpenProfile,
|
||||
onEdgeClick,
|
||||
onExpandClick,
|
||||
width = 240,
|
||||
height = 180,
|
||||
width: propWidth = MINIMAP_WIDTH - 16, // Account for padding
|
||||
height: propHeight = 120, // Compact height to match minimap proportions
|
||||
isCollapsed = false,
|
||||
onToggleCollapse,
|
||||
isDarkMode = false,
|
||||
}: NetworkGraphMinimapProps) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const simulationRef = useRef<d3.Simulation<SimulationNode, SimulationLink> | null>(null);
|
||||
const simNodesRef = useRef<SimulationNode[]>([]);
|
||||
const simLinksRef = useRef<SimulationLink[]>([]);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<{ node: GraphNode; x: number; y: number } | null>(null);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const simulationRef = useRef<d3.Simulation<SimulationNode, SimulationLink> | null>(null);
|
||||
|
||||
// Three-state display mode: minimized, normal, maximized
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>(isCollapsed ? 'minimized' : 'normal');
|
||||
|
||||
// Sync with legacy isCollapsed prop
|
||||
useEffect(() => {
|
||||
if (isCollapsed && displayMode !== 'minimized') {
|
||||
setDisplayMode('minimized');
|
||||
}
|
||||
}, [isCollapsed]);
|
||||
|
||||
// Calculate dimensions based on display mode
|
||||
const { width, height } = useMemo(() => {
|
||||
switch (displayMode) {
|
||||
case 'minimized':
|
||||
return { width: 0, height: 0 };
|
||||
case 'normal':
|
||||
return { width: propWidth, height: propHeight };
|
||||
case 'maximized':
|
||||
return {
|
||||
width: Math.min(700, window.innerWidth * 0.85),
|
||||
height: Math.min(500, window.innerHeight * 0.6)
|
||||
};
|
||||
default:
|
||||
return { width: propWidth, height: propHeight };
|
||||
}
|
||||
}, [displayMode, propWidth, propHeight]);
|
||||
|
||||
// Get theme-aware styles
|
||||
const styles = React.useMemo(() => getStyles(isDarkMode), [isDarkMode]);
|
||||
const styles = useMemo(() => getStyles(isDarkMode, displayMode), [isDarkMode, displayMode]);
|
||||
|
||||
// Initialize and update the D3 simulation
|
||||
// Network stats for maximized view
|
||||
const networkStats = useMemo(() => {
|
||||
const inRoomCount = nodes.filter(n => n.isInRoom).length;
|
||||
const connectionCount = edges.length;
|
||||
const mutualCount = edges.filter(e => e.isMutual).length;
|
||||
const trustedCount = edges.filter(e => e.trustLevel === 'trusted').length;
|
||||
return { inRoomCount, connectionCount, mutualCount, trustedCount, totalNodes: nodes.length };
|
||||
}, [nodes, edges]);
|
||||
|
||||
// Cleanup simulation on unmount
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || isCollapsed || nodes.length === 0) return;
|
||||
return () => {
|
||||
if (simulationRef.current) {
|
||||
simulationRef.current.stop();
|
||||
simulationRef.current = null;
|
||||
}
|
||||
isInitializedRef.current = false;
|
||||
simNodesRef.current = [];
|
||||
simLinksRef.current = [];
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Initialize and update the D3 simulation - STABLE VERSION
|
||||
// This effect uses refs to persist simulation state and only updates incrementally
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || displayMode === 'minimized' || nodes.length === 0) return;
|
||||
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
// Create simulation nodes and links
|
||||
const simNodes: SimulationNode[] = nodes.map(n => ({ ...n }));
|
||||
const nodeMap = new Map(simNodes.map(n => [n.id, n]));
|
||||
// Check if we need to initialize or just update
|
||||
const needsInit = !isInitializedRef.current || !simulationRef.current;
|
||||
const nodeMap = new Map(simNodesRef.current.map(n => [n.id, n]));
|
||||
|
||||
const simLinks: SimulationLink[] = edges
|
||||
.filter(e => nodeMap.has(e.source) && nodeMap.has(e.target))
|
||||
.map(e => ({
|
||||
source: nodeMap.get(e.source)!,
|
||||
target: nodeMap.get(e.target)!,
|
||||
id: e.id,
|
||||
isMutual: e.isMutual,
|
||||
}));
|
||||
if (needsInit) {
|
||||
// Full initialization - only happens once
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
// Create the simulation with faster decay for stabilization
|
||||
const simulation = d3.forceSimulation<SimulationNode>(simNodes)
|
||||
.force('link', d3.forceLink<SimulationNode, SimulationLink>(simLinks)
|
||||
.id(d => d.id)
|
||||
.distance(40))
|
||||
.force('charge', d3.forceManyBody().strength(-80))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(12))
|
||||
// Speed up stabilization: higher decay = faster settling
|
||||
.alphaDecay(0.05)
|
||||
// Lower alpha min threshold for stopping
|
||||
.alphaMin(0.01);
|
||||
// Create simulation nodes, preserving existing positions if available
|
||||
simNodesRef.current = nodes.map(n => {
|
||||
const existing = nodeMap.get(n.id);
|
||||
return {
|
||||
...n,
|
||||
x: existing?.x ?? width / 2 + (Math.random() - 0.5) * 100,
|
||||
y: existing?.y ?? height / 2 + (Math.random() - 0.5) * 100,
|
||||
vx: existing?.vx ?? 0,
|
||||
vy: existing?.vy ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
simulationRef.current = simulation;
|
||||
const newNodeMap = new Map(simNodesRef.current.map(n => [n.id, n]));
|
||||
|
||||
simLinksRef.current = edges
|
||||
.filter(e => newNodeMap.has(e.source) && newNodeMap.has(e.target))
|
||||
.map(e => ({
|
||||
source: newNodeMap.get(e.source)!,
|
||||
target: newNodeMap.get(e.target)!,
|
||||
id: e.id,
|
||||
isMutual: e.isMutual,
|
||||
}));
|
||||
|
||||
// Create the simulation with smooth, stable parameters
|
||||
const simulation = d3.forceSimulation<SimulationNode>(simNodesRef.current)
|
||||
.force('link', d3.forceLink<SimulationNode, SimulationLink>(simLinksRef.current)
|
||||
.id(d => d.id)
|
||||
.distance(displayMode === 'maximized' ? 80 : 50)
|
||||
.strength(0.5))
|
||||
.force('charge', d3.forceManyBody()
|
||||
.strength(displayMode === 'maximized' ? -150 : -100)
|
||||
.distanceMax(displayMode === 'maximized' ? 300 : 200))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2).strength(0.05))
|
||||
.force('collision', d3.forceCollide().radius(d => (d as SimulationNode).isCurrentUser ? 14 : 10))
|
||||
// Gentler alpha decay for smoother settling
|
||||
.alphaDecay(0.02)
|
||||
// Higher alpha min so it stops sooner
|
||||
.alphaMin(0.05)
|
||||
// Add velocity decay for smoother movement
|
||||
.velocityDecay(0.4);
|
||||
|
||||
simulationRef.current = simulation;
|
||||
isInitializedRef.current = true;
|
||||
} else {
|
||||
// Incremental update - preserve existing node positions
|
||||
const existingNodeMap = new Map(simNodesRef.current.map(n => [n.id, n]));
|
||||
|
||||
// Update existing nodes and add new ones
|
||||
const newNodes: SimulationNode[] = nodes.map(n => {
|
||||
const existing = existingNodeMap.get(n.id);
|
||||
if (existing) {
|
||||
// Update properties but keep position
|
||||
return { ...existing, ...n, x: existing.x, y: existing.y, vx: existing.vx, vy: existing.vy };
|
||||
}
|
||||
// New node - place near center with slight randomness
|
||||
return {
|
||||
...n,
|
||||
x: width / 2 + (Math.random() - 0.5) * 50,
|
||||
y: height / 2 + (Math.random() - 0.5) * 50,
|
||||
};
|
||||
});
|
||||
|
||||
simNodesRef.current = newNodes;
|
||||
const newNodeMap = new Map(newNodes.map(n => [n.id, n]));
|
||||
|
||||
simLinksRef.current = edges
|
||||
.filter(e => newNodeMap.has(e.source) && newNodeMap.has(e.target))
|
||||
.map(e => ({
|
||||
source: newNodeMap.get(e.source)!,
|
||||
target: newNodeMap.get(e.target)!,
|
||||
id: e.id,
|
||||
isMutual: e.isMutual,
|
||||
}));
|
||||
|
||||
// Update simulation with new nodes/links
|
||||
simulationRef.current!
|
||||
.nodes(simNodesRef.current)
|
||||
.force('link', d3.forceLink<SimulationNode, SimulationLink>(simLinksRef.current)
|
||||
.id(d => d.id)
|
||||
.distance(displayMode === 'maximized' ? 80 : 50)
|
||||
.strength(0.5))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2).strength(0.05));
|
||||
|
||||
// Gentle reheat to settle new nodes
|
||||
simulationRef.current!.alpha(0.3).restart();
|
||||
|
||||
// Re-render the graph with updated data
|
||||
svg.selectAll('*').remove();
|
||||
}
|
||||
|
||||
const simulation = simulationRef.current!;
|
||||
|
||||
// Create container group
|
||||
const g = svg.append('g');
|
||||
|
|
@ -279,10 +482,10 @@ export function NetworkGraphMinimap({
|
|||
const link = g.append('g')
|
||||
.attr('class', 'links')
|
||||
.selectAll('line')
|
||||
.data(simLinks)
|
||||
.data(simLinksRef.current)
|
||||
.join('line')
|
||||
.attr('stroke', d => getEdgeColor(d))
|
||||
.attr('stroke-width', d => d.isMutual ? 2.5 : 1.5)
|
||||
.attr('stroke-width', d => d.isMutual ? (displayMode === 'maximized' ? 3 : 2.5) : (displayMode === 'maximized' ? 2 : 1.5))
|
||||
.attr('marker-end', d => getArrowMarker(d))
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', (event, d) => {
|
||||
|
|
@ -307,22 +510,30 @@ export function NetworkGraphMinimap({
|
|||
return '#9ca3af';
|
||||
};
|
||||
|
||||
// Node sizes based on display mode
|
||||
const nodeRadius = displayMode === 'maximized' ? 10 : 6;
|
||||
const currentUserRadius = displayMode === 'maximized' ? 14 : 8;
|
||||
|
||||
// Create nodes
|
||||
const node = g.append('g')
|
||||
.attr('class', 'nodes')
|
||||
.selectAll('circle')
|
||||
.data(simNodes)
|
||||
.data(simNodesRef.current)
|
||||
.join('circle')
|
||||
.attr('r', d => d.isCurrentUser ? 8 : 6)
|
||||
.attr('r', d => d.isCurrentUser ? currentUserRadius : nodeRadius)
|
||||
.attr('fill', d => getNodeColor(d))
|
||||
.attr('stroke', d => d.isInRoom ? 'rgba(100, 200, 255, 0.8)' : 'transparent')
|
||||
.attr('stroke-width', d => d.isInRoom ? 2 : 0)
|
||||
.style('cursor', 'pointer')
|
||||
.style('filter', d => d.isInRoom ? 'drop-shadow(0 0 4px rgba(100, 200, 255, 0.5))' : 'none')
|
||||
.on('mouseenter', (event, d) => {
|
||||
const rect = svgRef.current!.getBoundingClientRect();
|
||||
const name = d.displayName || d.username;
|
||||
const status = d.isInRoom ? ' (in room)' : '';
|
||||
setTooltip({
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
text: d.isCurrentUser ? `${name} (you)` : name,
|
||||
text: d.isCurrentUser ? `${name} (you)` : `${name}${status}`,
|
||||
});
|
||||
})
|
||||
.on('mouseleave', () => {
|
||||
|
|
@ -367,84 +578,234 @@ export function NetworkGraphMinimap({
|
|||
.attr('x2', d => (d.target as SimulationNode).x!)
|
||||
.attr('y2', d => (d.target as SimulationNode).y!);
|
||||
|
||||
const margin = displayMode === 'maximized' ? 12 : 8;
|
||||
node
|
||||
.attr('cx', d => Math.max(8, Math.min(width - 8, d.x!)))
|
||||
.attr('cy', d => Math.max(8, Math.min(height - 8, d.y!)));
|
||||
.attr('cx', d => Math.max(margin, Math.min(width - margin, d.x!)))
|
||||
.attr('cy', d => Math.max(margin, Math.min(height - margin, d.y!)));
|
||||
});
|
||||
|
||||
// Stop simulation when it stabilizes (alpha reaches alphaMin)
|
||||
simulation.on('end', () => {
|
||||
// Simulation has stabilized, nodes will stay in place unless dragged
|
||||
simulation.stop();
|
||||
// Don't call stop() - let it stay ready for interactions
|
||||
});
|
||||
|
||||
// Cleanup function - only stop if component unmounts
|
||||
return () => {
|
||||
simulation.stop();
|
||||
// Don't reset simulation on every re-render
|
||||
// Only cleanup on actual unmount
|
||||
};
|
||||
}, [nodes, edges, width, height, isCollapsed, onNodeClick, onEdgeClick]);
|
||||
}, [nodes, edges, width, height, displayMode, onNodeClick, onEdgeClick]);
|
||||
|
||||
// Handle collapsed state click
|
||||
const handleCollapsedClick = useCallback(() => {
|
||||
if (onToggleCollapse) {
|
||||
onToggleCollapse();
|
||||
}
|
||||
// Handle display mode changes
|
||||
const handleMinimize = useCallback(() => {
|
||||
setDisplayMode('minimized');
|
||||
onToggleCollapse?.();
|
||||
}, [onToggleCollapse]);
|
||||
|
||||
if (isCollapsed) {
|
||||
const handleNormal = useCallback(() => {
|
||||
setDisplayMode('normal');
|
||||
}, []);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
setDisplayMode('maximized');
|
||||
onExpandClick?.();
|
||||
}, [onExpandClick]);
|
||||
|
||||
const handleMinimizedClick = useCallback(() => {
|
||||
setDisplayMode('normal');
|
||||
onToggleCollapse?.();
|
||||
}, [onToggleCollapse]);
|
||||
|
||||
// Handle ESC to close maximized view
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && displayMode === 'maximized') {
|
||||
setDisplayMode('normal');
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [displayMode]);
|
||||
|
||||
// Minimized state - small circular icon
|
||||
if (displayMode === 'minimized') {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div
|
||||
style={{ ...styles.panel, ...styles.panelCollapsed }}
|
||||
onClick={handleCollapsedClick}
|
||||
title="Show network graph"
|
||||
style={{ ...styles.panel, ...styles.panelMinimized }}
|
||||
onClick={handleMinimizedClick}
|
||||
title="Show social network"
|
||||
>
|
||||
<span style={styles.collapsedIcon}>🕸️</span>
|
||||
<span style={styles.minimizedIcon}>🕸️</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Get panel size styles based on display mode
|
||||
const panelSizeStyle = displayMode === 'maximized' ? styles.panelMaximized : styles.panelNormal;
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.panel}>
|
||||
<div
|
||||
style={styles.container}
|
||||
onClick={displayMode === 'maximized' ? handleNormal : undefined}
|
||||
>
|
||||
<div
|
||||
style={{ ...styles.panel, ...panelSizeStyle }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={styles.header}>
|
||||
<h3 style={styles.title}>Social Network</h3>
|
||||
<h3 style={styles.title}>
|
||||
<span>🕸️</span>
|
||||
Social Network
|
||||
{displayMode === 'maximized' && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '10px',
|
||||
background: isDarkMode ? 'rgba(100, 100, 255, 0.2)' : 'rgba(100, 100, 255, 0.1)',
|
||||
color: isDarkMode ? '#a0a0ff' : '#4f46e5',
|
||||
marginLeft: '8px',
|
||||
}}>
|
||||
{networkStats.totalNodes} people
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<div style={styles.headerButtons}>
|
||||
{/* Search button */}
|
||||
<button
|
||||
style={styles.iconButton}
|
||||
onClick={() => setIsSearchOpen(true)}
|
||||
title="Find people"
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none' }}
|
||||
>
|
||||
🔍
|
||||
</button>
|
||||
{onExpandClick && (
|
||||
|
||||
{/* Maximize/Restore button */}
|
||||
{displayMode === 'normal' && (
|
||||
<button
|
||||
style={styles.iconButton}
|
||||
onClick={onExpandClick}
|
||||
title="Open full view"
|
||||
onClick={handleMaximize}
|
||||
title="Expand to full view"
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none' }}
|
||||
>
|
||||
⛶
|
||||
</button>
|
||||
)}
|
||||
{onToggleCollapse && (
|
||||
{displayMode === 'maximized' && (
|
||||
<button
|
||||
style={styles.iconButton}
|
||||
onClick={onToggleCollapse}
|
||||
title="Collapse"
|
||||
onClick={handleNormal}
|
||||
title="Restore to small view"
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none' }}
|
||||
>
|
||||
−
|
||||
⛶
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Minimize button */}
|
||||
<button
|
||||
style={styles.iconButton}
|
||||
onClick={handleMinimize}
|
||||
title="Minimize"
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none' }}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
|
||||
{/* Close button (maximized only) */}
|
||||
{displayMode === 'maximized' && (
|
||||
<button
|
||||
style={styles.iconButton}
|
||||
onClick={handleNormal}
|
||||
title="Close (Esc)"
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = isDarkMode ? 'rgba(255,100,100,0.2)' : 'rgba(255,0,0,0.1)' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none' }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative' }} onClick={() => setSelectedNode(null)}>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={styles.canvas}
|
||||
/>
|
||||
{/* Stats bar (maximized only) */}
|
||||
{displayMode === 'maximized' && (
|
||||
<div style={styles.statsBar}>
|
||||
<div style={styles.statItem}>
|
||||
<span>👥</span>
|
||||
<span>In room:</span>
|
||||
<span style={styles.statValue}>{networkStats.inRoomCount}</span>
|
||||
</div>
|
||||
<div style={styles.statItem}>
|
||||
<span>🔗</span>
|
||||
<span>Connections:</span>
|
||||
<span style={styles.statValue}>{networkStats.connectionCount}</span>
|
||||
</div>
|
||||
<div style={styles.statItem}>
|
||||
<span>🤝</span>
|
||||
<span>Mutual:</span>
|
||||
<span style={styles.statValue}>{networkStats.mutualCount}</span>
|
||||
</div>
|
||||
<div style={styles.statItem}>
|
||||
<span>⭐</span>
|
||||
<span>Trusted:</span>
|
||||
<span style={styles.statValue}>{networkStats.trustedCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3D View for maximized mode */}
|
||||
{displayMode === 'maximized' ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: height,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: isDarkMode
|
||||
? 'radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d1a 100%)'
|
||||
: 'radial-gradient(ellipse at center, #f8f8ff 0%, #e8e8f0 100%)',
|
||||
color: isDarkMode ? '#888' : '#666',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
Loading 3D view...
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ width: '100%', height: height }}>
|
||||
<NetworkGraph3D
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
currentUserId={currentUserId}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onZoomToUser={onGoToUser}
|
||||
onViewAsUser={onFollowUser}
|
||||
isDarkMode={isDarkMode}
|
||||
sphereRadius={3}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
) : (
|
||||
/* 2D SVG View for normal mode */
|
||||
<div style={{ position: 'relative' }} onClick={() => setSelectedNode(null)}>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={styles.canvas}
|
||||
/>
|
||||
|
||||
{tooltip && (
|
||||
<div
|
||||
|
|
@ -596,7 +957,8 @@ export function NetworkGraphMinimap({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<UserSearchModal
|
||||
|
|
|
|||
|
|
@ -5,13 +5,146 @@
|
|||
* Extracts room participants from the editor and provides connection actions.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useEditor, useValue } from 'tldraw';
|
||||
import { NetworkGraphMinimap } from './NetworkGraphMinimap';
|
||||
import { useNetworkGraph } from './useNetworkGraph';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import type { GraphEdge, TrustLevel } from '../../lib/networking';
|
||||
|
||||
// =============================================================================
|
||||
// Broadcast Mode Indicator Component
|
||||
// =============================================================================
|
||||
|
||||
interface BroadcastIndicatorProps {
|
||||
followingUser: { id: string; username: string; color?: string } | null;
|
||||
onStop: () => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
function BroadcastIndicator({ followingUser, onStop, isDarkMode }: BroadcastIndicatorProps) {
|
||||
if (!followingUser) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '12px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 10000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '10px 16px',
|
||||
background: isDarkMode
|
||||
? 'linear-gradient(135deg, rgba(168, 85, 247, 0.95), rgba(139, 92, 246, 0.95))'
|
||||
: 'linear-gradient(135deg, rgba(168, 85, 247, 0.95), rgba(139, 92, 246, 0.95))',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 20px rgba(168, 85, 247, 0.4)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
animation: 'pulse-glow 2s ease-in-out infinite',
|
||||
}}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 4px 20px rgba(168, 85, 247, 0.4); }
|
||||
50% { box-shadow: 0 4px 30px rgba(168, 85, 247, 0.6); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{/* Live indicator */}
|
||||
<div
|
||||
style={{
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '50%',
|
||||
background: '#ef4444',
|
||||
animation: 'pulse 1.5s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
<style>
|
||||
{`
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(0.9); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{/* User avatar */}
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: followingUser.color || '#6366f1',
|
||||
border: '2px solid rgba(255, 255, 255, 0.5)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Text */}
|
||||
<div style={{ color: '#fff', fontSize: '13px', fontWeight: 500 }}>
|
||||
<span style={{ opacity: 0.8 }}>Viewing as</span>{' '}
|
||||
<strong>{followingUser.username}</strong>
|
||||
</div>
|
||||
|
||||
{/* Exit hint */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
marginLeft: '8px',
|
||||
padding: '4px 8px',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
<kbd
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '11px',
|
||||
color: '#fff',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
ESC
|
||||
</kbd>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '11px' }}>to exit</span>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onStop}
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
color: '#fff',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)')}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
|
@ -30,13 +163,20 @@ export function NetworkGraphPanel({ onExpand }: NetworkGraphPanelProps) {
|
|||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [selectedEdge, setSelectedEdge] = useState<GraphEdge | null>(null);
|
||||
|
||||
// Broadcast mode state - tracks who we're following
|
||||
const [followingUser, setFollowingUser] = useState<{
|
||||
id: string;
|
||||
username: string;
|
||||
color?: string;
|
||||
} | null>(null);
|
||||
|
||||
// Detect dark mode
|
||||
const [isDarkMode, setIsDarkMode] = useState(
|
||||
typeof document !== 'undefined' && document.documentElement.classList.contains('dark')
|
||||
);
|
||||
|
||||
// Listen for theme changes
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
|
|
@ -48,6 +188,53 @@ export function NetworkGraphPanel({ onExpand }: NetworkGraphPanelProps) {
|
|||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Stop following user - cleanup function
|
||||
const stopFollowingUser = useCallback(() => {
|
||||
if (!editor) return;
|
||||
|
||||
editor.stopFollowingUser();
|
||||
setFollowingUser(null);
|
||||
|
||||
// Remove followId from URL if present
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has('followId')) {
|
||||
url.searchParams.delete('followId');
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
}
|
||||
|
||||
console.log('Stopped following user');
|
||||
}, [editor]);
|
||||
|
||||
// Keyboard handler for ESC and X to exit broadcast mode
|
||||
useEffect(() => {
|
||||
if (!followingUser) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// ESC or X (lowercase or uppercase) stops following
|
||||
if (e.key === 'Escape' || e.key === 'x' || e.key === 'X') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
stopFollowingUser();
|
||||
}
|
||||
};
|
||||
|
||||
// Use capture phase to intercept before tldraw
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
};
|
||||
}, [followingUser, stopFollowingUser]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (followingUser && editor) {
|
||||
editor.stopFollowingUser();
|
||||
}
|
||||
};
|
||||
}, [followingUser, editor]);
|
||||
|
||||
// Get collaborators from tldraw
|
||||
const collaborators = useValue(
|
||||
'collaborators',
|
||||
|
|
@ -150,7 +337,20 @@ export function NetworkGraphPanel({ onExpand }: NetworkGraphPanelProps) {
|
|||
// Use tldraw's built-in follow functionality - needs userId
|
||||
const userId = targetCollaborator.userId || targetCollaborator.id;
|
||||
editor.startFollowingUser(userId);
|
||||
console.log('Now following user:', node.username);
|
||||
|
||||
// Set state to show broadcast indicator and enable keyboard exit
|
||||
setFollowingUser({
|
||||
id: userId,
|
||||
username: node.username || node.displayName || 'User',
|
||||
color: targetCollaborator.color || node.avatarColor || node.roomPresenceColor,
|
||||
});
|
||||
|
||||
// Optionally add followId to URL for deep linking
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('followId', userId);
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
|
||||
console.log('Now following user:', node.username, '- Press ESC or X to exit');
|
||||
} else {
|
||||
console.log('Could not find user to follow:', node.username);
|
||||
}
|
||||
|
|
@ -200,23 +400,33 @@ export function NetworkGraphPanel({ onExpand }: NetworkGraphPanelProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<NetworkGraphMinimap
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
myConnections={myConnections}
|
||||
currentUserId={session.username}
|
||||
onConnect={handleConnect}
|
||||
onDisconnect={handleDisconnect}
|
||||
onNodeClick={handleNodeClick}
|
||||
onGoToUser={handleGoToUser}
|
||||
onFollowUser={handleFollowUser}
|
||||
onOpenProfile={handleOpenProfile}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onExpandClick={handleExpand}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<>
|
||||
{/* Broadcast mode indicator - shows when following a user */}
|
||||
<BroadcastIndicator
|
||||
followingUser={followingUser}
|
||||
onStop={stopFollowingUser}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
|
||||
{/* Network graph minimap */}
|
||||
<NetworkGraphMinimap
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
myConnections={myConnections}
|
||||
currentUserId={session.username}
|
||||
onConnect={handleConnect}
|
||||
onDisconnect={handleDisconnect}
|
||||
onNodeClick={handleNodeClick}
|
||||
onGoToUser={handleGoToUser}
|
||||
onFollowUser={handleFollowUser}
|
||||
onOpenProfile={handleOpenProfile}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onExpandClick={handleExpand}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
export { NetworkGraphMinimap } from './NetworkGraphMinimap';
|
||||
export { NetworkGraph3D } from './NetworkGraph3D';
|
||||
export { NetworkGraphPanel } from './NetworkGraphPanel';
|
||||
export { UserSearchModal } from './UserSearchModal';
|
||||
export { useNetworkGraph, useRoomParticipantsFromEditor } from './useNetworkGraph';
|
||||
|
|
|
|||
Loading…
Reference in New Issue