add properties view

This commit is contained in:
ArnoChen
2025-02-10 22:02:06 +08:00
parent 07f19b939c
commit a08f59f663
18 changed files with 723 additions and 347 deletions

View File

@@ -37,23 +37,23 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.20.0", "@eslint/js": "^9.20.0",
"@stylistic/eslint-plugin-js": "^3.1.0", "@stylistic/eslint-plugin-js": "^3.1.0",
"@tailwindcss/vite": "^4.0.4", "@tailwindcss/vite": "^4.0.5",
"@types/bun": "^1.2.2", "@types/bun": "^1.2.2",
"@types/node": "^22.13.1", "@types/node": "^22.13.1",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.20.0", "eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.14.0", "globals": "^15.14.0",
"graphology-types": "^0.24.8", "graphology-types": "^0.24.8",
"prettier": "^3.4.2", "prettier": "^3.5.0",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.4", "tailwindcss": "^4.0.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"typescript-eslint": "^8.23.0", "typescript-eslint": "^8.23.0",
@@ -320,59 +320,59 @@
"@stylistic/eslint-plugin-js": ["@stylistic/eslint-plugin-js@3.1.0", "", { "dependencies": { "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0" }, "peerDependencies": { "eslint": ">=8.40.0" } }, "sha512-lQktsOiCr8S6StG29C5fzXYxLOD6ID1rp4j6TRS+E/qY1xd59Fm7dy5qm9UauJIEoSTlYx6yGsCHYh5UkgXPyg=="], "@stylistic/eslint-plugin-js": ["@stylistic/eslint-plugin-js@3.1.0", "", { "dependencies": { "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0" }, "peerDependencies": { "eslint": ">=8.40.0" } }, "sha512-lQktsOiCr8S6StG29C5fzXYxLOD6ID1rp4j6TRS+E/qY1xd59Fm7dy5qm9UauJIEoSTlYx6yGsCHYh5UkgXPyg=="],
"@swc/core": ["@swc/core@1.10.14", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.14", "@swc/core-darwin-x64": "1.10.14", "@swc/core-linux-arm-gnueabihf": "1.10.14", "@swc/core-linux-arm64-gnu": "1.10.14", "@swc/core-linux-arm64-musl": "1.10.14", "@swc/core-linux-x64-gnu": "1.10.14", "@swc/core-linux-x64-musl": "1.10.14", "@swc/core-win32-arm64-msvc": "1.10.14", "@swc/core-win32-ia32-msvc": "1.10.14", "@swc/core-win32-x64-msvc": "1.10.14" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WSrnE6JRnH20ZYjOOgSS4aOaPv9gxlkI2KRkN24kagbZnPZMnN8bZZyzw1rrLvwgpuRGv17Uz+hflosbR+SP6w=="], "@swc/core": ["@swc/core@1.10.15", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.15", "@swc/core-darwin-x64": "1.10.15", "@swc/core-linux-arm-gnueabihf": "1.10.15", "@swc/core-linux-arm64-gnu": "1.10.15", "@swc/core-linux-arm64-musl": "1.10.15", "@swc/core-linux-x64-gnu": "1.10.15", "@swc/core-linux-x64-musl": "1.10.15", "@swc/core-win32-arm64-msvc": "1.10.15", "@swc/core-win32-ia32-msvc": "1.10.15", "@swc/core-win32-x64-msvc": "1.10.15" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/iFeQuNaGdK7mfJbQcObhAhsMqLT7qgMYl7jX2GEIO+VDTejESpzAyKwaMeYXExN8D6e5BRHBCe7M5YlsuzjDA=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dh4VyrhDDb05tdRmqJ/MucOPMTnrB4pRJol18HVyLlqu1HOT5EzonUniNTCdQbUXjgdv5UVJSTE1lYTzrp+myA=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zFdZ6/yHqMCPk7OhLFqHy/MQ1EqJhcZMpNHd1gXYT7VRU3FaqvvKETrUlG3VYl65McPC7AhMRfXPyJ0JO/jARQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-KpzotL/I0O12RE3tF8NmQErINv0cQe/0mnN/Q50ESFzB5kU6bLgp2HMnnwDTm/XEZZRJCNe0oc9WJ5rKbAJFRQ=="], "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-8g4yiQwbr8fxOOjKXdot0dEkE5zgE8uNZudLy/ZyAhiwiZ8pbJ8/wVrDOu6dqbX7FBXAoDnvZ7fwN1jk4C8jdA=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.14", "", { "os": "linux", "cpu": "arm" }, "sha512-20yRXZjMJVz1wp1TcscKiGTVXistG+saIaxOmxSNQia1Qun3hSWLL+u6+5kXbfYGr7R2N6kqSwtZbIfJI25r9Q=="], "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.15", "", { "os": "linux", "cpu": "arm" }, "sha512-rl+eVOltl2+7WXOnvmWBpMgh6aO13G5x0U0g8hjwlmD6ku3Y9iRcThpOhm7IytMEarUp5pQxItNoPq+VUGjVHg=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gy7cGrNkiMfPxQyLGxdgXPwyWzNzbHuWycJFcoKBihxZKZIW8hkPBttkGivuLC+0qOgsV2/U+S7tlvAju7FtmQ=="], "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-qxWEQeyAJMWJqjaN4hi58WMpPdt3Tn0biSK9CYRegQtvZWCbewr6v2agtSu5AZ2rudeH6OfCWAMDQQeSgn6PJQ=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-+oYVqJvFw62InZ8PIy1rBACJPC2WTe4vbVb9kM1jJj2D7dKLm9acnnYIVIDsM5Wo7Uab8RvPHXVbs19IBurzuw=="], "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-QcELd9/+HjZx0WCxRrKcyKGWTiQ0485kFb5w8waxcSNd0d9Lgk4EFfWWVyvIb5gIHpDQmhrgzI/yRaWQX4YSZQ=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.14", "", { "os": "linux", "cpu": "x64" }, "sha512-OmEbVEKQFLQVHwo4EJl9osmlulURy46k232Opfpn/1ji0t2KcNCci3POsnfMuoZjLkGJv8vGNJdPQxX+CP+wSA=="], "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-S1+ZEEn3+a/MiMeQqQypbwTGoBG8/sPoCvpNbk+uValyygT+jSn3U0xVr45FbukpmMB+NhBMqfedMLqKA0QnJA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.14", "", { "os": "linux", "cpu": "x64" }, "sha512-OZW+Icm8DMPqHbhdxplkuG8qrNnPk5i7xJOZWYi1y5bTjgGFI4nEzrsmmeHKMdQTaWwsFrm3uK1rlyQ48MmXmg=="], "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-qW+H9g/2zTJ4jP7NDw4VAALY0ZlNEKzYsEoSj/HKi7k3tYEHjMzsxjfsY9I8WZCft23bBdV3RTCPoxCshaj1CQ=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-sTvc+xrDQXy3HXZFtTEClY35Efvuc3D+busYm0+rb1+Thau4HLRY9WP+sOKeGwH9/16rzfzYEqD7Ds8A9ykrHw=="], "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-AhRB11aA6LxjIqut+mg7qsu/7soQDmbK6MKR9nP3hgBszpqtXbRba58lr24xIbBCMr+dpo6kgEapWt+t5Po6Zg=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.14", "", { "os": "win32", "cpu": "ia32" }, "sha512-j2iQ4y9GWTKtES5eMU0sDsFdYni7IxME7ejFej25Tv3Fq4B+U9tgtYWlJwh1858nIWDXelHiKcSh/UICAyVMdQ=="], "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.15", "", { "os": "win32", "cpu": "ia32" }, "sha512-UGdh430TQwbDn6KjgvRTg1fO022sbQ4yCCHUev0+5B8uoBwi9a89qAz3emy2m56C8TXxUoihW9Y9OMfaRwPXUw=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.14", "", { "os": "win32", "cpu": "x64" }, "sha512-TYtWkUSMkjs0jGPeWdtWbex4B+DlQZmN/ySVLiPI+EltYCLEXsFMkVFq6aWn48dqFHggFK0UYfvDrJUR2c3Qxg=="], "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.15", "", { "os": "win32", "cpu": "x64" }, "sha512-XJzBCqO1m929qbJsOG7FZXQWX26TnEoMctS3QjuCoyBmkHxxQmZsy78KjMes1aomTcKHCyFYgrRGWgVmk7tT4Q=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.17", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="], "@swc/types": ["@swc/types@0.1.17", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.0.4", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.4" } }, "sha512-VLFq80IyoV1hsHPcCm1mmlyPyUT6NlovQLoO2y7PGm84mW94ZrNJ7ax5H6K4M7Aj/fdMfem5IX7Ka+LXWZpDGg=="], "@tailwindcss/node": ["@tailwindcss/node@4.0.5", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.5" } }, "sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.4", "@tailwindcss/oxide-darwin-arm64": "4.0.4", "@tailwindcss/oxide-darwin-x64": "4.0.4", "@tailwindcss/oxide-freebsd-x64": "4.0.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.4", "@tailwindcss/oxide-linux-arm64-musl": "4.0.4", "@tailwindcss/oxide-linux-x64-gnu": "4.0.4", "@tailwindcss/oxide-linux-x64-musl": "4.0.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.4", "@tailwindcss/oxide-win32-x64-msvc": "4.0.4" } }, "sha512-vPpu30KFLiGyPOoElkYt8WRvzGKVrrOz49KpfiGGtnQGmyUpL8VCbJzzEEcpKT5BpaaQidhFok+OXscf6hHjOQ=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.5", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.5", "@tailwindcss/oxide-darwin-arm64": "4.0.5", "@tailwindcss/oxide-darwin-x64": "4.0.5", "@tailwindcss/oxide-freebsd-x64": "4.0.5", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.5", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.5", "@tailwindcss/oxide-linux-arm64-musl": "4.0.5", "@tailwindcss/oxide-linux-x64-gnu": "4.0.5", "@tailwindcss/oxide-linux-x64-musl": "4.0.5", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.5", "@tailwindcss/oxide-win32-x64-msvc": "4.0.5" } }, "sha512-iWGyOCu0TuzvCBisWbGv2K9+7QCfE0ztgtrZOvb9iF7V7ChVkD15Obe3HevZrhjngAc34jDA+OMSuSvkrpTy4A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.4", "", { "os": "android", "cpu": "arm64" }, "sha512-hiGUA8d15ynH/LdurQNObnuTjri7i4ApAzhesusNxoz4br7vhZ6QO5CFgniYAYNZvf8Q8wCTBg0nj61RalBeVQ=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.5", "", { "os": "android", "cpu": "arm64" }, "sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vTca+ysNl8BYmYJTni9pLC+L3S4bvrj0ai1eUV3yYXYa5Cpugr5Fni6ylV0gcTZOyETm2RCCJ/0azU6MgqE6HA=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-rxPWb5AQJ/aAM/5UDCjaQaMYIcrZHe/Dr9xZu9+P9nJf3WAweNsGi+e+SW9EYGRiF3hkBtP2dvxVNAkTiEbNQQ=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UOnRHzlS5V5cxaMgBo6rk1E92tTDUtO/falc9vOpNiRdWhNcofYNN9zvZP63Wuo5FC6/XCyAnJo6OXUm18TwrQ=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-0Ry9Qfnf22rmJwHxsCFmHQIl5RZw+yOUUGHaqNT42REL8r308cU/bi4UqdrjqVRfAlu51gOGxTRf2NRueczuIA=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-5a7WD30nVdI7Rl1ohZ0Ojj9t5yRnZkJBizvh3uIW52h9UeNpon8TfoknF6rU/TwD32dQ0Cjo5CcCHtQ2wW9PCA=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-m6s5jKSqos07l6NtHFd49Ljcaw4jIWHE7jq6eNPNz9SCzQqRzs4esP1t7jH8UljQ7JffKOl7yZPwK5Nf+irliw=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-K5dBjGHzby9eyUBwy9YHFhKY+5i8fzIBZM1NBWp6L2xpM7OzW9WJDgNcgESkZami9g+EozkQLt3ZmMZHAieXkw=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-J8sskt+fA5ooq+kxy0Tf4E2TRWZD9Y8j3K+pnBwp9zdilLmSd8OHrB3e0/rO78KveZ6BE9ae75cKOWrT6wONmw=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-z2RzUvOQl0ZqrZqmCFP53tJbBXQ3UmLD/E6J7+q0e+4VaFnXCcIYTfQbHgI8f3fash+q6gK80Ko/ywEQ+bvv6Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-flFaaMc77NQbz0Fq73wBs9EH2lX1Oc2Z/3JuxoewpnGHpAGJ/j05tvBNMyTaGrKcHvf/+dk+mCDxb6+PmzGgnQ=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-WzMA0aL/24/JyNrv2Yhr/Og24QGRPWJMjRyCJ4HRoGMs6/8svOQKrnnZ/9LUFwn56irAndFBjWWnlaqykH+g5Q=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.5", "", { "os": "win32", "cpu": "x64" }, "sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.4", "", { "dependencies": { "@tailwindcss/node": "^4.0.4", "@tailwindcss/oxide": "^4.0.4", "lightningcss": "^1.29.1", "tailwindcss": "4.0.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-zrWGbluPeXeoetUQoDFmt1dQIeiOBThfznla7zPIqST69rMmiDD4SZwJrHVoL5CvXz06AYQXz/M/jELSakL7Rg=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.0.5", "", { "dependencies": { "@tailwindcss/node": "^4.0.5", "@tailwindcss/oxide": "^4.0.5", "lightningcss": "^1.29.1", "tailwindcss": "4.0.5" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-/i4hjLTUYVjUG0MTUviQP3HR/hzwyzv8Sq4sz2pnsNuf+FIjjhJB0vcnIMH1KIX0k8ozD6CBv2Dl76tlm/JFFA=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
@@ -410,7 +410,7 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.23.0", "", { "dependencies": { "@typescript-eslint/types": "8.23.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.23.0", "", { "dependencies": { "@typescript-eslint/types": "8.23.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ=="],
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.7.2", "", { "dependencies": { "@swc/core": "^1.7.26" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew=="], "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
"@yomguithereal/helpers": ["@yomguithereal/helpers@1.1.1", "", {}, "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg=="], "@yomguithereal/helpers": ["@yomguithereal/helpers@1.1.1", "", {}, "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg=="],
@@ -540,7 +540,7 @@
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.18", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
@@ -834,7 +834,7 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="], "prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="], "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
@@ -932,7 +932,7 @@
"tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="], "tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
"tailwindcss": ["tailwindcss@4.0.4", "", {}, "sha512-/ezDLEkOLf1lXkr9F2iI5BHJbexJpty5zkV2B8bGHCqAdbc9vk85Jgdkq+ZOvNkNPa3yAaqJ8DjRt584Bc84kw=="], "tailwindcss": ["tailwindcss@4.0.5", "", {}, "sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg=="],
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -43,23 +43,23 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.20.0", "@eslint/js": "^9.20.0",
"@stylistic/eslint-plugin-js": "^3.1.0", "@stylistic/eslint-plugin-js": "^3.1.0",
"@tailwindcss/vite": "^4.0.4", "@tailwindcss/vite": "^4.0.5",
"@types/bun": "^1.2.2", "@types/bun": "^1.2.2",
"@types/node": "^22.13.1", "@types/node": "^22.13.1",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.20.0", "eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.14.0", "globals": "^15.14.0",
"graphology-types": "^0.24.8", "graphology-types": "^0.24.8",
"prettier": "^3.4.2", "prettier": "^3.5.0",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.4", "tailwindcss": "^4.0.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"typescript-eslint": "^8.23.0", "typescript-eslint": "^8.23.0",

View File

@@ -1,8 +1,8 @@
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback, useMemo } from 'react'
// import { MiniMap } from '@react-sigma/minimap' // import { MiniMap } from '@react-sigma/minimap'
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core' import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
import { Settings as SigmaSettings } from 'sigma/settings' import { Settings as SigmaSettings } from 'sigma/settings'
import { GraphSearchOption } from '@react-sigma/graph-search' import { GraphSearchOption, OptionItem } from '@react-sigma/graph-search'
import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering' import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering'
import { NodeBorderProgram } from '@sigma/node-border' import { NodeBorderProgram } from '@sigma/node-border'
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve' import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
@@ -15,8 +15,10 @@ import ZoomControl from '@/components/ZoomControl'
import FullScreenControl from '@/components/FullScreenControl' import FullScreenControl from '@/components/FullScreenControl'
import Settings from '@/components/Settings' import Settings from '@/components/Settings'
import GraphSearch from '@/components/GraphSearch' import GraphSearch from '@/components/GraphSearch'
import PropertiesView from '@/components/PropertiesView'
import { useSettingsStore } from '@/lib/settings' import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph'
import '@react-sigma/core/lib/style.css' import '@react-sigma/core/lib/style.css'
import '@react-sigma/graph-search/lib/style.css' import '@react-sigma/graph-search/lib/style.css'
@@ -97,10 +99,11 @@ const GraphEvents = () => {
} }
export const GraphViewer = () => { export const GraphViewer = () => {
const [selectedNode, setSelectedNode] = useState<string | null>(null)
const [focusedNode, setFocusedNode] = useState<string | null>(null)
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings) const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
const [autoMoveToFocused, setAutoMoveToFocused] = useState(false)
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents() const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
const enableNodeDrag = useSettingsStore.use.enableNodeDrag() const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
@@ -114,45 +117,39 @@ export const GraphViewer = () => {
}) })
}, [enableEdgeEvents, renderEdgeLabels]) }, [enableEdgeEvents, renderEdgeLabels])
const onFocus = useCallback( const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
(value: GraphSearchOption | null) => { if (value === null) useGraphStore.getState().setFocusedNode(null)
if (value === null) setFocusedNode(null) else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id)
else if (value.type === 'nodes') setFocusedNode(value.id) }, [])
},
[setFocusedNode]
)
const onSelect = useCallback( const onSearchSelect = useCallback((value: GraphSearchOption | null) => {
(value: GraphSearchOption | null) => { if (value === null) {
if (value === null) setSelectedNode(null) useGraphStore.getState().setSelectedNode(null)
else if (value.type === 'nodes') { } else if (value.type === 'nodes') {
setAutoMoveToFocused(true) useGraphStore.getState().setSelectedNode(value.id, true)
setSelectedNode(value.id) }
setTimeout(() => setAutoMoveToFocused(false), 100) }, [])
}
}, const autoFocusedNode = useMemo(() => focusedNode ?? selectedNode, [focusedNode, selectedNode])
[setSelectedNode, setAutoMoveToFocused] const searchInitSelectedNode = useMemo(
(): OptionItem | null => (selectedNode ? { type: 'nodes', id: selectedNode } : null),
[selectedNode]
) )
return ( return (
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden"> <SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
<GraphControl <GraphControl />
selectedNode={selectedNode}
setSelectedNode={setSelectedNode}
focusedNode={focusedNode}
setFocusedNode={setFocusedNode}
/>
{enableNodeDrag && <GraphEvents />} {enableNodeDrag && <GraphEvents />}
<FocusOnNode node={focusedNode ?? selectedNode} move={autoMoveToFocused} /> <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
<div className="absolute top-2 right-2"> <div className="absolute top-2 left-2">
<GraphSearch <GraphSearch
type="nodes" type="nodes"
value={selectedNode ? { type: 'nodes', id: selectedNode } : null} value={searchInitSelectedNode}
onFocus={onFocus} onFocus={onSearchFocus}
onChange={onSelect} onChange={onSearchSelect}
/> />
</div> </div>
@@ -164,6 +161,10 @@ export const GraphViewer = () => {
<ThemeToggle /> <ThemeToggle />
</div> </div>
<div className="absolute top-2 right-2">
<PropertiesView />
</div>
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2"> {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
<MiniMap width="100px" height="100px" /> <MiniMap width="100px" height="100px" />
</div> */} </div> */}

View File

@@ -1,5 +1,6 @@
import { useCamera, useSigma } from '@react-sigma/core' import { useCamera, useSigma } from '@react-sigma/core'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useGraphStore } from '@/stores/graph'
/** /**
* Component that highlights a node and centers the camera on it. * Component that highlights a node and centers the camera on it.
@@ -14,7 +15,10 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
useEffect(() => { useEffect(() => {
if (!node) return if (!node) return
sigma.getGraph().setNodeAttribute(node, 'highlighted', true) sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
if (move) gotoNode(node) if (move) {
gotoNode(node)
useGraphStore.getState().setMoveToSelectedNode(false)
}
return () => { return () => {
sigma.getGraph().setNodeAttribute(node, 'highlighted', false) sigma.getGraph().setNodeAttribute(node, 'highlighted', false)

View File

@@ -1,13 +1,15 @@
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
// import { useLayoutCircular } from '@react-sigma/layout-circular' // import { useLayoutCircular } from '@react-sigma/layout-circular'
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
import { useEffect, useState } from 'react' import { useEffect } from 'react'
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph' // import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph' import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
import useTheme from '@/hooks/useTheme' import useTheme from '@/hooks/useTheme'
import * as Constants from '@/lib/constants' import * as Constants from '@/lib/constants'
import { useSettingsStore } from '@/lib/settings'
import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph'
const isButtonPressed = (ev: MouseEvent | TouchEvent) => { const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
if (ev.type.startsWith('mouse')) { if (ev.type.startsWith('mouse')) {
@@ -18,19 +20,7 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
return false return false
} }
const GraphControl = ({ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
disableHoverEffect,
selectedNode,
setSelectedNode,
focusedNode,
setFocusedNode
}: {
disableHoverEffect?: boolean
selectedNode: string | null
setSelectedNode: (node: string | null) => void
focusedNode: string | null
setFocusedNode: (node: string | null) => void
}) => {
const { lightrageGraph } = useLightragGraph() const { lightrageGraph } = useLightragGraph()
const sigma = useSigma<NodeType, EdgeType>() const sigma = useSigma<NodeType, EdgeType>()
const registerEvents = useRegisterEvents<NodeType, EdgeType>() const registerEvents = useRegisterEvents<NodeType, EdgeType>()
@@ -39,11 +29,13 @@ const GraphControl = ({
const { assign: assignLayout } = useLayoutForceAtlas2({ const { assign: assignLayout } = useLayoutForceAtlas2({
iterations: 20 iterations: 20
}) })
const [focusedEdge, setfocusedEdge] = useState<string | null>(null)
const [selectedEdge, setSelectedEdge] = useState<string | null>(null)
const { theme } = useTheme() const { theme } = useTheme()
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges() const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
const selectedEdge = useGraphStore.use.selectedEdge()
const focusedEdge = useGraphStore.use.focusedEdge()
/** /**
* When component mount * When component mount
@@ -58,6 +50,9 @@ const GraphControl = ({
Object.assign(graph, { __force_applied: true }) Object.assign(graph, { __force_applied: true })
} }
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
useGraphStore.getState()
// Register the events // Register the events
registerEvents({ registerEvents({
enterNode: (event) => { enterNode: (event) => {
@@ -80,20 +75,17 @@ const GraphControl = ({
}, },
enterEdge: (event) => { enterEdge: (event) => {
if (!isButtonPressed(event.event.original)) { if (!isButtonPressed(event.event.original)) {
setfocusedEdge(event.edge) setFocusedEdge(event.edge)
} }
}, },
leaveEdge: (event) => { leaveEdge: (event) => {
if (!isButtonPressed(event.event.original)) { if (!isButtonPressed(event.event.original)) {
setfocusedEdge(null) setFocusedEdge(null)
} }
}, },
clickStage: () => { clickStage: () => clearSelection()
setSelectedEdge(null)
setSelectedNode(null)
}
}) })
}, [assignLayout, loadGraph, registerEvents, lightrageGraph, setFocusedNode, setSelectedNode]) }, [assignLayout, loadGraph, registerEvents, lightrageGraph])
/** /**
* When component mount or hovered node change * When component mount or hovered node change

View File

@@ -70,7 +70,7 @@ export const GraphSearchInput = ({
return ( return (
<AsyncSelect <AsyncSelect
className="w-52 rounded-xl border-1 backdrop-blur-lg" className="bg-background/20 w-52 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-opacity hover:opacity-100"
fetcher={loadOptions} fetcher={loadOptions}
renderOption={OptionComponent} renderOption={OptionComponent}
getOptionValue={(item) => item.id} getOptionValue={(item) => item.id}

View File

@@ -0,0 +1,231 @@
import { useEffect, useState } from 'react'
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
import Text from '@/components/ui/Text'
import useLightragGraph from '@/hooks/useLightragGraph'
/**
* Component that view properties of elements in graph.
*/
const PropertiesView = () => {
const { getNode, getEdge } = useLightragGraph()
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
const selectedEdge = useGraphStore.use.selectedEdge()
const focusedEdge = useGraphStore.use.focusedEdge()
const [currentElement, setCurrentElement] = useState<NodeType | EdgeType | null>(null)
const [currentType, setCurrentType] = useState<'node' | 'edge' | null>(null)
useEffect(() => {
let type: 'node' | 'edge' | null = null
let element: RawNodeType | RawEdgeType | null = null
if (focusedNode) {
type = 'node'
element = getNode(focusedNode)
} else if (selectedNode) {
type = 'node'
element = getNode(selectedNode)
} else if (focusedEdge) {
type = 'edge'
element = getEdge(focusedEdge, true)
} else if (selectedEdge) {
type = 'edge'
element = getEdge(selectedEdge, true)
}
if (element) {
if (type == 'node') {
setCurrentElement(refineNodeProperties(element as any))
} else {
setCurrentElement(refineEdgeProperties(element as any))
}
setCurrentType(type)
} else {
setCurrentElement(null)
setCurrentType(null)
}
}, [
focusedNode,
selectedNode,
focusedEdge,
selectedEdge,
setCurrentElement,
setCurrentType,
getNode,
getEdge
])
if (!currentElement) {
return <></>
}
return (
<div className="bg-background/20 max-w-sm rounded-xl border-2 p-2 backdrop-blur-lg">
{currentType == 'node' ? (
<NodePropertiesView node={currentElement as any} />
) : (
<EdgePropertiesView edge={currentElement as any} />
)}
</div>
)
}
type NodeType = RawNodeType & {
relationships: {
type: string
id: string
label: string
}[]
}
type EdgeType = RawEdgeType & {
sourceNode?: RawNodeType
targetNode?: RawNodeType
}
const refineNodeProperties = (node: RawNodeType): NodeType => {
const state = useGraphStore.getState()
const relationships = []
if (state.sigmaGraph && state.rawGraph) {
for (const edgeId of state.sigmaGraph.edges(node.id)) {
const edge = state.rawGraph.getEdge(edgeId, true)
if (edge) {
const isTarget = node.id === edge.source
const neighbourId = isTarget ? edge.target : edge.source
const neighbour = state.rawGraph.getNode(neighbourId)
if (neighbour) {
relationships.push({
type: isTarget ? 'Target' : 'Source',
id: neighbourId,
label: neighbour.labels.join(', ')
})
}
}
}
}
return {
...node,
relationships
}
}
const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
const state = useGraphStore.getState()
const sourceNode = state.rawGraph?.getNode(edge.source)
const targetNode = state.rawGraph?.getNode(edge.target)
return {
...edge,
sourceNode,
targetNode
}
}
const PropertyRow = ({
name,
value,
onClick,
tooltip
}: {
name: string
value: any
onClick?: () => void
tooltip?: string
}) => {
return (
<div className="flex items-center gap-2 text-sm">
<label className="text-primary/60 tracking-wide">{name}</label>:
<Text
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
tooltipClassName="max-w-80"
text={value}
tooltip={tooltip || value}
side="left"
onClick={onClick}
/>
</div>
)
}
const NodePropertiesView = ({ node }: { node: NodeType }) => {
return (
<div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded-md p-1">
<PropertyRow name={'Id'} value={node.id} />
<PropertyRow
name={'Labels'}
value={node.labels.join(', ')}
onClick={() => {
useGraphStore.getState().setSelectedNode(node.id, true)
}}
/>
<PropertyRow name={'Degree'} value={node.degree} />
</div>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded-md p-1">
{Object.keys(node.properties)
.sort()
.map((name) => {
return <PropertyRow key={name} name={name} value={node.properties[name]} />
})}
</div>
{node.relationships.length > 0 && (
<>
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
Relationships
</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{node.relationships.map(({ type, id, label }) => {
return (
<PropertyRow
key={id}
name={type}
value={label}
onClick={() => {
useGraphStore.getState().setSelectedNode(id, true)
}}
/>
)
})}
</div>
</>
)}
</div>
)
}
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
return (
<div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={'Id'} value={edge.id} />
<PropertyRow name={'Type'} value={edge.type} />
<PropertyRow
name={'Source'}
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
onClick={() => {
useGraphStore.getState().setSelectedNode(edge.source, true)
}}
/>
<PropertyRow
name={'Target'}
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
onClick={() => {
useGraphStore.getState().setSelectedNode(edge.target, true)
}}
/>
</div>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(edge.properties)
.sort()
.map((name) => {
return <PropertyRow key={name} name={name} value={edge.properties[name]} />
})}
</div>
</div>
)
}
export default PropertiesView

View File

@@ -3,7 +3,7 @@ import { Checkbox } from '@/components/ui/Checkbox'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { controlButtonVariant } from '@/lib/constants' import { controlButtonVariant } from '@/lib/constants'
import { useSettingsStore } from '@/lib/settings' import { useSettingsStore } from '@/stores/settings'
import { SettingsIcon } from 'lucide-react' import { SettingsIcon } from 'lucide-react'

View File

@@ -1,5 +1,5 @@
import { createContext, useEffect, useState } from 'react' import { createContext, useEffect, useState } from 'react'
import { Theme, useSettingsStore } from '@/lib/settings' import { Theme, useSettingsStore } from '@/stores/settings'
type ThemeProviderProps = { type ThemeProviderProps = {
children: React.ReactNode children: React.ReactNode

View File

@@ -144,7 +144,7 @@ export function AsyncSelect<T>({
setOptions(originalOptions) setOptions(originalOptions)
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher, debouncedSearchTerm, mounted, preload, filterFn]) }, [fetcher, debouncedSearchTerm, mounted, preload, filterFn])
const handleSelect = useCallback( const handleSelect = useCallback(
@@ -191,7 +191,7 @@ export function AsyncSelect<T>({
</div> </div>
)} )}
</div> </div>
<CommandList className="max-h-auto" hidden={!open}> <CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
{error && <div className="text-destructive p-4 text-center">{error}</div>} {error && <div className="text-destructive p-4 text-center">{error}</div>}
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)} {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
{!loading && {!loading &&

View File

@@ -0,0 +1,49 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
import { cn } from '@/lib/utils'
const Text = ({
text,
className,
tooltipClassName,
tooltip,
side,
onClick
}: {
text: string
className?: string
tooltipClassName?: string
tooltip?: string
side?: 'top' | 'right' | 'bottom' | 'left'
onClick?: () => void
}) => {
if (!tooltip) {
return (
<label
className={cn(className, onClick !== undefined ? 'cursor-pointer' : undefined)}
onClick={onClick}
>
{text}
</label>
)
}
return (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<label
className={cn(className, onClick !== undefined ? 'cursor-pointer' : undefined)}
onClick={onClick}
>
{text}
</label>
</TooltipTrigger>
<TooltipContent side={side} className={tooltipClassName}>
{tooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export default Text

View File

@@ -2,50 +2,7 @@ import Graph, { DirectedGraph } from 'graphology'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { randomColor } from '@/lib/utils' import { randomColor } from '@/lib/utils'
import * as Constants from '@/lib/constants' import * as Constants from '@/lib/constants'
import { useGraphStore, RawGraph } from '@/stores/graph'
type RawNodeType = {
id: string
labels: string[]
properties: Record<string, any>
size: number
x: number
y: number
color: string
degree: number
}
type RawEdgeType = {
id: string
source: string
target: string
type: string
properties: Record<string, any>
}
class RawGraph {
nodes: RawNodeType[] = []
edges: RawEdgeType[] = []
nodeIdMap: Record<string, number> = {}
edgeIdMap: Record<string, number> = {}
getNode = (nodeId: string) => {
const nodeIndex = this.nodeIdMap[nodeId]
if (nodeIndex !== undefined) {
return this.nodes[nodeIndex]
}
return undefined
}
getEdge = (edgeId: string) => {
const edgeIndex = this.edgeIdMap[edgeId]
if (edgeIndex !== undefined) {
return this.edges[edgeIndex]
}
return undefined
}
}
const validateGraph = (graph: RawGraph) => { const validateGraph = (graph: RawGraph) => {
if (!graph) { if (!graph) {
@@ -158,67 +115,79 @@ const fetchGraph = async (label: string) => {
return rawGraph return rawGraph
} }
const graphCache: { const createSigmaGraph = (rawGraph: RawGraph | null) => {
label: string | null const graph = new DirectedGraph()
rawGraph: RawGraph | null
convertedGraph: DirectedGraph | null for (const rawNode of rawGraph?.nodes ?? []) {
} = { graph.addNode(rawNode.id, {
label: null, label: rawNode.labels.join(', '),
rawGraph: null, color: rawNode.color,
convertedGraph: null x: rawNode.x,
y: rawNode.y,
size: rawNode.size,
// for node-border
borderColor: Constants.nodeBorderColor,
borderSize: 0.2
})
}
for (const rawEdge of rawGraph?.edges ?? []) {
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
label: rawEdge.type
})
}
return graph
} }
const useLightrangeGraph = () => { const useLightrangeGraph = () => {
const [fetchLabel, setFetchLabel] = useState<string>('*') const [fetchLabel, setFetchLabel] = useState<string>('*')
const [rawGraph, setRawGraph] = useState<RawGraph | null>(graphCache.rawGraph) const rawGraph = useGraphStore.use.rawGraph()
const sigmaGraph = useGraphStore.use.sigmaGraph()
const getNode = useCallback(
(nodeId: string) => {
return rawGraph?.getNode(nodeId) || null
},
[rawGraph]
)
const getEdge = useCallback(
(edgeId: string, dynamicId: boolean = true) => {
return rawGraph?.getEdge(edgeId, dynamicId) || null
},
[rawGraph]
)
useEffect(() => { useEffect(() => {
if (fetchLabel) { if (fetchLabel) {
if (graphCache.label !== fetchLabel) { const state = useGraphStore.getState()
if (state.queryLabel !== fetchLabel) {
state.reset()
fetchGraph(fetchLabel).then((data) => { fetchGraph(fetchLabel).then((data) => {
graphCache.convertedGraph = null state.setQueryLabel(fetchLabel)
graphCache.rawGraph = data state.setSigmaGraph(createSigmaGraph(data))
graphCache.label = fetchLabel data?.buildDynamicMap()
setRawGraph(data) state.setRawGraph(data)
}) })
} }
} else { } else {
setRawGraph(null) const state = useGraphStore.getState()
state.reset()
state.setSigmaGraph(new DirectedGraph())
} }
}, [fetchLabel, setRawGraph]) }, [fetchLabel])
const lightrageGraph = useCallback(() => { const lightrageGraph = useCallback(() => {
if (graphCache.convertedGraph) { if (sigmaGraph) {
return graphCache.convertedGraph as Graph<NodeType, EdgeType> return sigmaGraph as Graph<NodeType, EdgeType>
} }
// Create the graph
const graph = new DirectedGraph() const graph = new DirectedGraph()
useGraphStore.getState().setSigmaGraph(graph)
for (const rawNode of rawGraph?.nodes ?? []) {
graph.addNode(rawNode.id, {
label: rawNode.labels.join(' '),
color: rawNode.color,
x: rawNode.x,
y: rawNode.y,
size: rawNode.size,
// for node-border
borderColor: Constants.nodeBorderColor,
borderSize: 0.2
})
}
for (const rawEdge of rawGraph?.edges ?? []) {
graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
label: rawEdge.type
})
}
graphCache.convertedGraph = graph
return graph as Graph<NodeType, EdgeType> return graph as Graph<NodeType, EdgeType>
}, [rawGraph]) }, [sigmaGraph])
return { lightrageGraph, fetchLabel, setFetchLabel } return { lightrageGraph, fetchLabel, setFetchLabel, getNode, getEdge }
} }
export default useLightrangeGraph export default useLightrangeGraph

View File

@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from 'react'
import seedrandom from 'seedrandom' import seedrandom from 'seedrandom'
import { randomColor } from '@/lib/utils' import { randomColor } from '@/lib/utils'
import * as Constants from '@/lib/constants' import * as Constants from '@/lib/constants'
import { useGraphStore } from '@/stores/graph'
export type NodeType = { export type NodeType = {
x: number x: number
@@ -36,6 +37,8 @@ const useRandomGraph = () => {
}, []) }, [])
const randomGraph = useCallback(() => { const randomGraph = useCallback(() => {
useGraphStore.getState().reset()
// Create the graph // Create the graph
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.1 }) const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.1 })
graph.nodes().forEach((node: string) => { graph.nodes().forEach((node: string) => {

View File

@@ -4,54 +4,114 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme { :root {
--color-background: hsl(var(--background)); --background: hsl(0 0% 100%);
--color-foreground: hsl(var(--foreground)); --foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
--primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
--accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
--ring: hsl(240 10% 3.9%);
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.6rem;
--sidebar-background: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
--color-card: hsl(var(--card)); .dark {
--color-card-foreground: hsl(var(--card-foreground)); --background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%);
--accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%);
--ring: hsl(240 4.9% 83.9%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--sidebar-background: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
--color-popover: hsl(var(--popover)); @theme inline {
--color-popover-foreground: hsl(var(--popover-foreground)); --color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: hsl(var(--primary)); --color-card: var(--card);
--color-primary-foreground: hsl(var(--primary-foreground)); --color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-secondary: hsl(var(--secondary)); --color-popover-foreground: var(--popover-foreground);
--color-secondary-foreground: hsl(var(--secondary-foreground)); --color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-muted: hsl(var(--muted)); --color-secondary: var(--secondary);
--color-muted-foreground: hsl(var(--muted-foreground)); --color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-accent: hsl(var(--accent)); --color-muted-foreground: var(--muted-foreground);
--color-accent-foreground: hsl(var(--accent-foreground)); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: hsl(var(--destructive)); --color-destructive: var(--destructive);
--color-destructive-foreground: hsl(var(--destructive-foreground)); --color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-border: hsl(var(--border)); --color-input: var(--input);
--color-input: hsl(var(--input)); --color-ring: var(--ring);
--color-ring: hsl(var(--ring)); --color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-1: hsl(var(--chart-1)); --color-chart-3: var(--chart-3);
--color-chart-2: hsl(var(--chart-2)); --color-chart-4: var(--chart-4);
--color-chart-3: hsl(var(--chart-3)); --color-chart-5: var(--chart-5);
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar-background);
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out;
@@ -63,6 +123,7 @@
height: var(--radix-accordion-content-height); height: var(--radix-accordion-content-height);
} }
} }
@keyframes accordion-up { @keyframes accordion-up {
from { from {
height: var(--radix-accordion-content-height); height: var(--radix-accordion-content-height);
@@ -73,105 +134,9 @@
} }
} }
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer utilities {
body {
font-family: Arial, Helvetica, sans-serif;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;

View File

@@ -1,5 +1,6 @@
import { clsx, type ClassValue } from 'clsx' import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { StoreApi, UseBoundStore } from 'zustand'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -13,3 +14,17 @@ export function randomColor() {
} }
return code return code
} }
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
const store = _store as WithSelectors<typeof _store>
store.use = {}
for (const k of Object.keys(store.getState())) {
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
}
return store
}

View File

@@ -0,0 +1,139 @@
import { create } from 'zustand'
import { createSelectors } from '@/lib/utils'
import { DirectedGraph } from 'graphology'
export type RawNodeType = {
id: string
labels: string[]
properties: Record<string, any>
size: number
x: number
y: number
color: string
degree: number
}
export type RawEdgeType = {
id: string
source: string
target: string
type: string
properties: Record<string, any>
dynamicId: string
}
export class RawGraph {
nodes: RawNodeType[] = []
edges: RawEdgeType[] = []
nodeIdMap: Record<string, number> = {}
edgeIdMap: Record<string, number> = {}
edgeDynamicIdMap: Record<string, number> = {}
getNode = (nodeId: string) => {
const nodeIndex = this.nodeIdMap[nodeId]
if (nodeIndex !== undefined) {
return this.nodes[nodeIndex]
}
return undefined
}
getEdge = (edgeId: string, dynamicId: boolean = true) => {
const edgeIndex = dynamicId ? this.edgeDynamicIdMap[edgeId] : this.edgeIdMap[edgeId]
if (edgeIndex !== undefined) {
return this.edges[edgeIndex]
}
return undefined
}
buildDynamicMap = () => {
this.edgeDynamicIdMap = {}
for (let i = 0; i < this.edges.length; i++) {
const edge = this.edges[i]
this.edgeDynamicIdMap[edge.dynamicId] = i
}
}
}
interface GraphState {
selectedNode: string | null
focusedNode: string | null
selectedEdge: string | null
focusedEdge: string | null
queryLabel: string | null
rawGraph: RawGraph | null
sigmaGraph: DirectedGraph | null
moveToSelectedNode: boolean
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
setFocusedNode: (nodeId: string | null) => void
setSelectedEdge: (edgeId: string | null) => void
setFocusedEdge: (edgeId: string | null) => void
clearSelection: () => void
reset: () => void
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
setQueryLabel: (queryLabel: string | null) => void
setRawGraph: (rawGraph: RawGraph | null) => void
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
}
const useGraphStoreBase = create<GraphState>()((set) => ({
selectedNode: null,
focusedNode: null,
selectedEdge: null,
focusedEdge: null,
moveToSelectedNode: false,
queryLabel: null,
rawGraph: null,
sigmaGraph: null,
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
set({ selectedNode: nodeId, moveToSelectedNode }),
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
setSelectedEdge: (edgeId: string | null) => set({ selectedEdge: edgeId }),
setFocusedEdge: (edgeId: string | null) => set({ focusedEdge: edgeId }),
clearSelection: () =>
set({
selectedNode: null,
focusedNode: null,
selectedEdge: null,
focusedEdge: null
}),
reset: () =>
set({
selectedNode: null,
focusedNode: null,
selectedEdge: null,
focusedEdge: null,
queryLabel: null,
rawGraph: null,
sigmaGraph: null,
moveToSelectedNode: false
}),
setQueryLabel: (queryLabel: string | null) =>
set({
queryLabel
}),
setRawGraph: (rawGraph: RawGraph | null) =>
set({
rawGraph
}),
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => set({ sigmaGraph }),
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
}))
const useGraphStore = createSelectors(useGraphStoreBase)
export { useGraphStore }

View File

@@ -1,5 +1,6 @@
import { create, StoreApi, UseBoundStore } from 'zustand' import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware' import { persist, createJSONStorage } from 'zustand/middleware'
import { createSelectors } from '@/lib/utils'
type Theme = 'dark' | 'light' | 'system' type Theme = 'dark' | 'light' | 'system'
@@ -36,20 +37,6 @@ const useSettingsStoreBase = create<SettingsState>()(
) )
) )
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
const store = _store as WithSelectors<typeof _store>
store.use = {}
for (const k of Object.keys(store.getState())) {
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
}
return store
}
const useSettingsStore = createSelectors(useSettingsStoreBase) const useSettingsStore = createSelectors(useSettingsStoreBase)
export { useSettingsStore, type Theme } export { useSettingsStore, type Theme }