diff --git a/src/main/frontend/package-lock.json b/src/main/frontend/package-lock.json
index db7034f2..45e9c009 100644
--- a/src/main/frontend/package-lock.json
+++ b/src/main/frontend/package-lock.json
@@ -8,23 +8,30 @@
"name": "osscodeiq-ui",
"version": "0.1.0",
"dependencies": {
+ "@antv/g6": "^5.1.0",
"@monaco-editor/react": "^4.7.0",
+ "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
+ "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.1.8",
- "@types/d3": "^7.4.3",
+ "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "d3": "^7.9.0",
"lucide-react": "^0.474.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-resizable-panels": "^4.8.0",
"react-router-dom": "^7.1.5",
"swagger-ui-react": "^5.21.0",
- "tailwind-merge": "^3.0.2"
+ "tailwind-merge": "^3.0.2",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.10.1",
+ "@playwright/test": "^1.51.1",
+ "@types/node": "^22.0.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swagger-ui-react": "^4.18.3",
@@ -40,7 +47,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -49,6 +55,253 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@antv/algorithm": {
+ "version": "0.1.26",
+ "resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz",
+ "integrity": "sha512-DVhcFSQ8YQnMNW34Mk8BSsfc61iC1sAnmcfYoXTAshYHuU50p/6b7x3QYaGctDNKWGvi1ub7mPcSY0bK+aN0qg==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/util": "^2.0.13",
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@antv/algorithm/node_modules/@antv/util": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz",
+ "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==",
+ "license": "ISC",
+ "dependencies": {
+ "csstype": "^3.0.8",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/@antv/component": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.11.tgz",
+ "integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/g": "^6.1.11",
+ "@antv/scale": "^0.4.16",
+ "@antv/util": "^3.3.10",
+ "svg-path-parser": "^1.1.0"
+ }
+ },
+ "node_modules/@antv/event-emitter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz",
+ "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==",
+ "license": "MIT"
+ },
+ "node_modules/@antv/expr": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz",
+ "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==",
+ "license": "MIT"
+ },
+ "node_modules/@antv/g": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@antv/g/-/g-6.3.1.tgz",
+ "integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/g-lite": "2.7.0",
+ "@antv/util": "^3.3.5",
+ "@babel/runtime": "^7.25.6",
+ "gl-matrix": "^3.4.3",
+ "html2canvas": "^1.4.1"
+ }
+ },
+ "node_modules/@antv/g-canvas": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@antv/g-canvas/-/g-canvas-2.2.0.tgz",
+ "integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/g-lite": "2.7.0",
+ "@antv/g-math": "3.1.0",
+ "@antv/util": "^3.3.5",
+ "@babel/runtime": "^7.25.6",
+ "gl-matrix": "^3.4.3",
+ "tslib": "^2.5.3"
+ }
+ },
+ "node_modules/@antv/g-lite": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@antv/g-lite/-/g-lite-2.7.0.tgz",
+ "integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/g-math": "3.1.0",
+ "@antv/util": "^3.3.5",
+ "@antv/vendor": "^1.0.3",
+ "@babel/runtime": "^7.25.6",
+ "eventemitter3": "^5.0.1",
+ "gl-matrix": "^3.4.3",
+ "tslib": "^2.5.3"
+ }
+ },
+ "node_modules/@antv/g-math": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@antv/g-math/-/g-math-3.1.0.tgz",
+ "integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/util": "^3.3.5",
+ "@babel/runtime": "^7.25.6",
+ "gl-matrix": "^3.4.3",
+ "tslib": "^2.5.3"
+ }
+ },
+ "node_modules/@antv/g-plugin-dragndrop": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz",
+ "integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/g-lite": "2.7.0",
+ "@antv/util": "^3.3.5",
+ "@babel/runtime": "^7.25.6",
+ "tslib": "^2.5.3"
+ }
+ },
+ "node_modules/@antv/g6": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@antv/g6/-/g6-5.1.0.tgz",
+ "integrity": "sha512-tvoBDKypL/zWEG99pgwGJLWr2CKA+6zVixYxaVzDOp0+TrPY2cxB1jevxFGPjbTOLBIMYt/vKCh1jnmDtfvtpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/algorithm": "^0.1.26",
+ "@antv/component": "^2.1.7",
+ "@antv/event-emitter": "^0.1.3",
+ "@antv/g": "^6.1.28",
+ "@antv/g-canvas": "^2.0.48",
+ "@antv/g-plugin-dragndrop": "^2.0.38",
+ "@antv/graphlib": "^2.0.4",
+ "@antv/hierarchy": "^0.7.1",
+ "@antv/layout": "^2.0.0",
+ "@antv/util": "^3.3.11",
+ "bubblesets-js": "^2.3.4"
+ }
+ },
+ "node_modules/@antv/graphlib": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@antv/graphlib/-/graphlib-2.0.4.tgz",
+ "integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/event-emitter": "^0.1.3"
+ }
+ },
+ "node_modules/@antv/hierarchy": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@antv/hierarchy/-/hierarchy-0.7.1.tgz",
+ "integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==",
+ "license": "MIT"
+ },
+ "node_modules/@antv/layout": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@antv/layout/-/layout-2.0.0.tgz",
+ "integrity": "sha512-aCZ3UdNc40SfT7meFV7QTADY2HCnc0DShVw56CJNTI6oExUIVU736grPuL5Dhb8/JrVaU4Y83QPN/P7KafBzlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/event-emitter": "^0.1.3",
+ "@antv/expr": "^1.0.2",
+ "@antv/graphlib": "^2.0.0",
+ "@antv/util": "^3.3.2",
+ "comlink": "^4.4.1",
+ "d3-force": "^3.0.0",
+ "d3-force-3d": "^3.0.5",
+ "d3-octree": "^1.0.2",
+ "d3-quadtree": "^3.0.1",
+ "dagre": "^0.8.5",
+ "ml-matrix": "^6.10.4",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@antv/scale": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz",
+ "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/util": "^3.3.7",
+ "color-string": "^1.5.5",
+ "fecha": "^4.2.1"
+ }
+ },
+ "node_modules/@antv/util": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz",
+ "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "gl-matrix": "^3.3.0",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@antv/vendor": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@antv/vendor/-/vendor-1.0.11.tgz",
+ "integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.2.1",
+ "@types/d3-color": "^3.1.3",
+ "@types/d3-dispatch": "^3.0.6",
+ "@types/d3-dsv": "^3.0.7",
+ "@types/d3-ease": "^3.0.2",
+ "@types/d3-fetch": "^3.0.7",
+ "@types/d3-force": "^3.0.10",
+ "@types/d3-format": "^3.0.4",
+ "@types/d3-geo": "^3.1.0",
+ "@types/d3-hierarchy": "^3.1.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-path": "^3.1.0",
+ "@types/d3-quadtree": "^3.0.6",
+ "@types/d3-random": "^3.0.3",
+ "@types/d3-scale": "^4.0.9",
+ "@types/d3-scale-chromatic": "^3.1.0",
+ "@types/d3-shape": "^3.1.7",
+ "@types/d3-time": "^3.0.4",
+ "@types/d3-timer": "^3.0.2",
+ "d3-array": "^3.2.4",
+ "d3-color": "^3.1.0",
+ "d3-dispatch": "^3.0.1",
+ "d3-dsv": "^3.0.1",
+ "d3-ease": "^3.0.1",
+ "d3-fetch": "^3.0.1",
+ "d3-force": "^3.0.0",
+ "d3-force-3d": "^3.0.5",
+ "d3-format": "^3.1.0",
+ "d3-geo": "^3.1.1",
+ "d3-geo-projection": "^4.0.0",
+ "d3-hierarchy": "^3.1.2",
+ "d3-interpolate": "^3.0.1",
+ "d3-path": "^3.1.0",
+ "d3-quadtree": "^3.0.1",
+ "d3-random": "^3.0.1",
+ "d3-regression": "^1.3.10",
+ "d3-scale": "^4.0.2",
+ "d3-scale-chromatic": "^3.1.0",
+ "d3-shape": "^3.2.0",
+ "d3-time": "^3.1.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/@axe-core/playwright": {
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz",
+ "integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==",
+ "dev": true,
+ "dependencies": {
+ "axe-core": "~4.11.1"
+ },
+ "peerDependencies": {
+ "playwright-core": ">= 1.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -836,7 +1089,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -858,7 +1110,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -868,14 +1119,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -909,7 +1158,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -923,7 +1171,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -933,7 +1180,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -943,6 +1189,21 @@
"node": ">= 8"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
+ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -955,6 +1216,37 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -978,6 +1270,36 @@
}
}
},
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -1465,6 +1787,36 @@
}
}
},
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
@@ -2732,111 +3084,24 @@
"@babel/types": "^7.28.2"
}
},
- "node_modules/@types/d3": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
- "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-array": "*",
- "@types/d3-axis": "*",
- "@types/d3-brush": "*",
- "@types/d3-chord": "*",
- "@types/d3-color": "*",
- "@types/d3-contour": "*",
- "@types/d3-delaunay": "*",
- "@types/d3-dispatch": "*",
- "@types/d3-drag": "*",
- "@types/d3-dsv": "*",
- "@types/d3-ease": "*",
- "@types/d3-fetch": "*",
- "@types/d3-force": "*",
- "@types/d3-format": "*",
- "@types/d3-geo": "*",
- "@types/d3-hierarchy": "*",
- "@types/d3-interpolate": "*",
- "@types/d3-path": "*",
- "@types/d3-polygon": "*",
- "@types/d3-quadtree": "*",
- "@types/d3-random": "*",
- "@types/d3-scale": "*",
- "@types/d3-scale-chromatic": "*",
- "@types/d3-selection": "*",
- "@types/d3-shape": "*",
- "@types/d3-time": "*",
- "@types/d3-time-format": "*",
- "@types/d3-timer": "*",
- "@types/d3-transition": "*",
- "@types/d3-zoom": "*"
- }
- },
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
- "node_modules/@types/d3-axis": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
- "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-brush": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
- "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-chord": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
- "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
- "license": "MIT"
- },
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
- "node_modules/@types/d3-contour": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
- "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-array": "*",
- "@types/geojson": "*"
- }
- },
- "node_modules/@types/d3-delaunay": {
- "version": "6.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
- "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
- "license": "MIT"
- },
"node_modules/@types/d3-dispatch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
"license": "MIT"
},
- "node_modules/@types/d3-drag": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
- "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
"node_modules/@types/d3-dsv": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
@@ -2900,12 +3165,6 @@
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
- "node_modules/@types/d3-polygon": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
- "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
- "license": "MIT"
- },
"node_modules/@types/d3-quadtree": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
@@ -2933,12 +3192,6 @@
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
"license": "MIT"
},
- "node_modules/@types/d3-selection": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
- "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
- "license": "MIT"
- },
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
@@ -2954,37 +3207,12 @@
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
- "node_modules/@types/d3-time-format": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
- "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
- "license": "MIT"
- },
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
- "node_modules/@types/d3-transition": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
- "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-zoom": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
- "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-interpolate": "*",
- "@types/d3-selection": "*"
- }
- },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3007,6 +3235,15 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
"node_modules/@types/prismjs": {
"version": "1.26.6",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
@@ -3104,14 +3341,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -3131,7 +3366,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -3219,6 +3453,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axe-core": {
+ "version": "4.11.2",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz",
+ "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/axios": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
@@ -3239,6 +3482,15 @@
"node": "18 || 20 || >=22"
}
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3276,7 +3528,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -3301,7 +3552,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -3344,6 +3594,12 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/bubblesets-js": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/bubblesets-js/-/bubblesets-js-2.3.4.tgz",
+ "integrity": "sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==",
+ "license": "MIT"
+ },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -3419,7 +3675,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3480,7 +3735,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -3505,7 +3759,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -3514,6 +3767,18 @@
"node": ">= 6"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -3529,6 +3794,22 @@
"node": ">=6"
}
},
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3541,6 +3822,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/comlink": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
+ "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
+ "license": "Apache-2.0"
+ },
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -3555,7 +3842,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3601,6 +3887,15 @@
"url": "https://opencollective.com/core-js"
}
},
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -3611,7 +3906,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -3624,50 +3918,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
- "node_modules/d3": {
- "version": "7.9.0",
- "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
- "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
- "license": "ISC",
- "dependencies": {
- "d3-array": "3",
- "d3-axis": "3",
- "d3-brush": "3",
- "d3-chord": "3",
- "d3-color": "3",
- "d3-contour": "4",
- "d3-delaunay": "6",
- "d3-dispatch": "3",
- "d3-drag": "3",
- "d3-dsv": "3",
- "d3-ease": "3",
- "d3-fetch": "3",
- "d3-force": "3",
- "d3-format": "3",
- "d3-geo": "3",
- "d3-hierarchy": "3",
- "d3-interpolate": "3",
- "d3-path": "3",
- "d3-polygon": "3",
- "d3-quadtree": "3",
- "d3-random": "3",
- "d3-scale": "4",
- "d3-scale-chromatic": "3",
- "d3-selection": "3",
- "d3-shape": "3",
- "d3-time": "3",
- "d3-time-format": "4",
- "d3-timer": "3",
- "d3-transition": "3",
- "d3-zoom": "3"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -3680,42 +3932,11 @@
"node": ">=12"
}
},
- "node_modules/d3-axis": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
- "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-brush": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
- "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
- "license": "ISC",
- "dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-drag": "2 - 3",
- "d3-interpolate": "1 - 3",
- "d3-selection": "3",
- "d3-transition": "3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-chord": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
- "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
- "license": "ISC",
- "dependencies": {
- "d3-path": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
+ "node_modules/d3-binarytree": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
+ "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
+ "license": "MIT"
},
"node_modules/d3-color": {
"version": "3.1.0",
@@ -3726,30 +3947,6 @@
"node": ">=12"
}
},
- "node_modules/d3-contour": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
- "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
- "license": "ISC",
- "dependencies": {
- "d3-array": "^3.2.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-delaunay": {
- "version": "6.0.4",
- "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
- "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
- "license": "ISC",
- "dependencies": {
- "delaunator": "5"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
@@ -3759,19 +3956,6 @@
"node": ">=12"
}
},
- "node_modules/d3-drag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
- "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
- "license": "ISC",
- "dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-selection": "3"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/d3-dsv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
@@ -3841,6 +4025,22 @@
"node": ">=12"
}
},
+ "node_modules/d3-force-3d": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+ "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-binarytree": "1",
+ "d3-dispatch": "1 - 3",
+ "d3-octree": "1",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
@@ -3862,6 +4062,36 @@
"node": ">=12"
}
},
+ "node_modules/d3-geo-projection": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz",
+ "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "d3-array": "1 - 3",
+ "d3-geo": "1.12.0 - 3"
+ },
+ "bin": {
+ "geo2svg": "bin/geo2svg.js",
+ "geograticule": "bin/geograticule.js",
+ "geoproject": "bin/geoproject.js",
+ "geoquantize": "bin/geoquantize.js",
+ "geostitch": "bin/geostitch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo-projection/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/d3-hierarchy": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
@@ -3883,6 +4113,12 @@
"node": ">=12"
}
},
+ "node_modules/d3-octree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+ "license": "MIT"
+ },
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
@@ -3892,15 +4128,6 @@
"node": ">=12"
}
},
- "node_modules/d3-polygon": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
- "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/d3-quadtree": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
@@ -3919,6 +4146,12 @@
"node": ">=12"
}
},
+ "node_modules/d3-regression": {
+ "version": "1.3.10",
+ "resolved": "https://registry.npmjs.org/d3-regression/-/d3-regression-1.3.10.tgz",
+ "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
@@ -3948,15 +4181,6 @@
"node": ">=12"
}
},
- "node_modules/d3-selection": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
- "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
@@ -4002,39 +4226,14 @@
"node": ">=12"
}
},
- "node_modules/d3-transition": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
- "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
- "license": "ISC",
- "dependencies": {
- "d3-color": "1 - 3",
- "d3-dispatch": "1 - 3",
- "d3-ease": "1 - 3",
- "d3-interpolate": "1 - 3",
- "d3-timer": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "d3-selection": "2 - 3"
- }
- },
- "node_modules/d3-zoom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
- "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
- "license": "ISC",
+ "node_modules/dagre": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
+ "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
+ "license": "MIT",
"dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-drag": "2 - 3",
- "d3-interpolate": "1 - 3",
- "d3-selection": "2 - 3",
- "d3-transition": "2 - 3"
- },
- "engines": {
- "node": ">=12"
+ "graphlib": "^2.1.8",
+ "lodash": "^4.17.15"
}
},
"node_modules/debug": {
@@ -4103,15 +4302,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/delaunator": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
- "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
- "license": "ISC",
- "dependencies": {
- "robust-predicates": "^3.0.2"
- }
- },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -4131,14 +4321,12 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true,
"license": "MIT"
},
"node_modules/dompurify": {
@@ -4276,11 +4464,22 @@
"node": ">=6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -4297,7 +4496,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -4316,7 +4514,6 @@
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -4335,11 +4532,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/fecha": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
+ "license": "MIT"
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -4425,7 +4627,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -4501,11 +4702,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/gl-matrix": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
+ "license": "MIT"
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -4526,6 +4732,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graphlib": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
+ "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.15"
+ }
+ },
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -4622,6 +4837,19 @@
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
"license": "CC0-1.0"
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -4711,11 +4939,22 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/is-any-array": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz",
+ "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -4740,7 +4979,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -4766,7 +5004,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4776,7 +5013,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -4799,7 +5035,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -4830,7 +5065,6 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
- "dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -4890,7 +5124,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -4903,7 +5136,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash": {
@@ -4989,7 +5221,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -4999,7 +5230,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -5057,6 +5287,45 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/ml-array-max": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz",
+ "integrity": "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.0"
+ }
+ },
+ "node_modules/ml-array-min": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-1.2.3.tgz",
+ "integrity": "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.0"
+ }
+ },
+ "node_modules/ml-array-rescale": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-1.3.7.tgz",
+ "integrity": "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.0",
+ "ml-array-max": "^1.2.4",
+ "ml-array-min": "^1.2.3"
+ }
+ },
+ "node_modules/ml-matrix": {
+ "version": "6.12.1",
+ "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.12.1.tgz",
+ "integrity": "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.1",
+ "ml-array-rescale": "^1.3.7"
+ }
+ },
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
@@ -5079,7 +5348,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -5091,7 +5359,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -5191,7 +5458,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5210,7 +5476,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -5269,21 +5534,18 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -5296,7 +5558,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5306,12 +5567,55 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5325,7 +5629,6 @@
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -5354,7 +5657,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -5372,7 +5674,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -5398,7 +5699,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -5441,7 +5741,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -5467,7 +5766,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -5481,7 +5779,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/prismjs": {
@@ -5533,7 +5830,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -5767,6 +6063,16 @@
}
}
},
+ "node_modules/react-resizable-panels": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.8.0.tgz",
+ "integrity": "sha512-2uEABkewb3ky/ZgIlAUxWa1W/LjsK494fdV1QsXxst7CDRHCzo7h22tWWu3NNaBjmiuriOCt3CvhipnaYcpoIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-router": {
"version": "7.13.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz",
@@ -5851,7 +6157,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -5861,7 +6166,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -5951,7 +6255,6 @@
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
@@ -5981,19 +6284,12 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
- "node_modules/robust-predicates": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
- "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
- "license": "Unlicense"
- },
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
@@ -6043,7 +6339,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -6182,11 +6477,19 @@
"suid": "bin/short-unique-id"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6218,7 +6521,6 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -6241,7 +6543,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6250,6 +6551,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-path-parser": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz",
+ "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==",
+ "license": "MIT"
+ },
"node_modules/swagger-client": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.37.1.tgz",
@@ -6337,7 +6644,6 @@
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -6371,11 +6677,28 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -6385,7 +6708,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -6398,7 +6720,6 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -6415,7 +6736,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -6433,7 +6753,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -6460,7 +6779,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -6511,7 +6829,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/ts-mixer": {
@@ -6581,6 +6898,12 @@
"node": ">=14.17"
}
},
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true
+ },
"node_modules/unraw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz",
@@ -6684,9 +7007,17 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json
index 9c78ec69..5438d088 100644
--- a/src/main/frontend/package.json
+++ b/src/main/frontend/package.json
@@ -6,29 +6,39 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test:e2e": "playwright test",
+ "test:e2e:headed": "playwright test --headed",
+ "test:e2e:report": "playwright show-report"
},
"dependencies": {
+ "@antv/g6": "^5.1.0",
"@monaco-editor/react": "^4.7.0",
+ "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
+ "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.1.8",
- "@types/d3": "^7.4.3",
+ "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "d3": "^7.9.0",
"lucide-react": "^0.474.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-resizable-panels": "^4.8.0",
"react-router-dom": "^7.1.5",
"swagger-ui-react": "^5.21.0",
- "tailwind-merge": "^3.0.2"
+ "tailwind-merge": "^3.0.2",
+ "tailwindcss-animate": "^1.0.7"
},
"overrides": {
"dompurify": "^3.3.3"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.10.1",
+ "@playwright/test": "^1.51.1",
+ "@types/node": "^22.0.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swagger-ui-react": "^4.18.3",
diff --git a/src/main/frontend/playwright-report/index.html b/src/main/frontend/playwright-report/index.html
new file mode 100644
index 00000000..44aea3e0
--- /dev/null
+++ b/src/main/frontend/playwright-report/index.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+data:application/zip;base64,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
\ No newline at end of file
diff --git a/src/main/frontend/playwright.config.ts b/src/main/frontend/playwright.config.ts
new file mode 100644
index 00000000..d9b67875
--- /dev/null
+++ b/src/main/frontend/playwright.config.ts
@@ -0,0 +1,76 @@
+///
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Playwright E2E test configuration for Code IQ UI Redesign (Phase 7).
+ *
+ * Prerequisites:
+ * - `code-iq serve` running on localhost:8080 (Neo4j graph loaded)
+ * - Or use `webServer` below to start the Vite dev server pointing at a real backend
+ *
+ * Run all tests: npm run test:e2e
+ * Run headed: npm run test:e2e:headed
+ * Show HTML report: npm run test:e2e:report
+ */
+export default defineConfig({
+ testDir: './tests/e2e',
+ // Use the test-specific tsconfig that includes @types/node
+ tsconfig: './tsconfig.test.json',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: [['html', { outputFolder: 'playwright-report' }], ['line']],
+
+ use: {
+ baseURL: process.env.BASE_URL ?? 'http://localhost:8080',
+ trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ },
+
+ // Performance threshold constants (ms) shared via env so specs can read them
+ // Actual assertions live in performance.spec.ts
+ // PERF_THRESHOLD_100 = 500
+ // PERF_THRESHOLD_1K = 2000
+ // PERF_THRESHOLD_10K = 3000
+
+ projects: [
+ // P0 — required for release sign-off
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ // P1 — run in CI when available
+ {
+ name: 'edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ // Responsive breakpoints (chromium only — layout logic is shared)
+ {
+ name: 'desktop-1920',
+ use: { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 } },
+ testMatch: '**/responsive.spec.ts',
+ },
+ {
+ name: 'laptop-1440',
+ use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } },
+ testMatch: '**/responsive.spec.ts',
+ },
+ {
+ name: 'tablet-768',
+ use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } },
+ testMatch: '**/responsive.spec.ts',
+ },
+ ],
+});
diff --git a/src/main/frontend/src/App.tsx b/src/main/frontend/src/App.tsx
index e10dd1d9..fe581f79 100644
--- a/src/main/frontend/src/App.tsx
+++ b/src/main/frontend/src/App.tsx
@@ -1,17 +1,23 @@
+import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import Layout from './components/Layout';
import Dashboard from './components/Dashboard';
-import CodeGraphView from './components/CodeGraphView';
import ExplorerView from './components/ExplorerView';
import McpConsole from './components/McpConsole';
import SwaggerView from './components/SwaggerView';
+const CodeGraphView = lazy(() => import('./components/CodeGraphView'));
+
export default function App() {
return (
}>
} />
- } />
+ Loading graph…}>
+
+
+ } />
} />
} />
} />
diff --git a/src/main/frontend/src/components/CodeGraphView.tsx b/src/main/frontend/src/components/CodeGraphView.tsx
index 75d68d9c..b122f103 100644
--- a/src/main/frontend/src/components/CodeGraphView.tsx
+++ b/src/main/frontend/src/components/CodeGraphView.tsx
@@ -1,357 +1,1014 @@
import { useState, useEffect, useRef, useCallback } from 'react';
-import * as d3 from 'd3';
-import { useApi } from '@/hooks/useApi';
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type G6GraphType = any;
import { api } from '@/lib/api';
-import type { KindEntry, NodeResponse, NodesListResponse } from '@/types/api';
-import { ChevronRight, Home } from 'lucide-react';
-
-// Reuse the kind-color mapping from ExplorerView for consistency
-const KIND_COLORS: Record = {
- class: '#8b5cf6',
- interface: '#06b6d4',
- method: '#10b981',
- endpoint: '#f59e0b',
- entity: '#f43f5e',
- module: '#7c3aed',
- function: '#14b8a6',
- database: '#eab308',
- config: '#94a3b8',
- config_key: '#64748b',
- test: '#22c55e',
- guard: '#ef4444',
- middleware: '#f97316',
- service: '#3b82f6',
- controller: '#6366f1',
- repository: '#ec4899',
- component: '#0ea5e9',
- route: '#84cc16',
- topic: '#a855f7',
- queue: '#fb923c',
- schema: '#78716c',
- field: '#a8a29e',
- enum: '#c084fc',
- annotation: '#67e8f9',
- type: '#818cf8',
- script: '#fbbf24',
- file: '#e2e8f0',
- package: '#475569',
- import: '#6b7280',
-};
-
-function getKindColor(kind: string): string {
- return KIND_COLORS[kind.toLowerCase()] ?? '#6366f1';
+import { getKindColor, getEdgeColor, KIND_COLORS } from '@/lib/graphConstants';
+import type { TopologyResponse, EgoGraphResponse, NodesListResponse, NodeResponse } from '@/types/api';
+import { useFileSelection } from '@/contexts/FileSelectionContext';
+import { useRightPanel } from '@/components/Layout';
+import NodeDetailPanel from '@/components/NodeDetailPanel';
+import {
+ Home,
+ ChevronRight,
+ ZoomIn,
+ ZoomOut,
+ Maximize2,
+ Minimize2,
+ Crosshair,
+ LayoutDashboard,
+ Filter,
+ Loader2,
+ AlertCircle,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+type DrillLevel = 0 | 1 | 2;
+type LayoutMode = 'force' | 'dagre' | 'radial' | 'circular';
+
+interface BreadcrumbItem {
+ label: string;
+ level: DrillLevel;
+ serviceId?: string;
+ nodeId?: string;
}
-// ─── Top-level treemap: kinds sized by count ────────────────────────────────
+interface ContextMenuState {
+ x: number;
+ y: number;
+ nodeId: string;
+ nodeLabel: string;
+ nodeKind: string;
+}
-interface KindTreemapProps {
- width: number;
- height: number;
- kinds: KindEntry[];
- onDrillDown: (kind: string) => void;
+interface G6NodeDatum {
+ id: string;
+ data: {
+ label: string;
+ kind: string;
+ filePath?: string;
+ module?: string;
+ layer?: string;
+ nodeCount?: number;
+ };
+ style?: Record;
+ combo?: string;
+ [key: string]: unknown;
}
-function KindTreemap({ width, height, kinds, onDrillDown }: KindTreemapProps) {
- const svgRef = useRef(null);
+interface G6EdgeDatum {
+ id?: string;
+ source: string;
+ target: string;
+ data?: { kind?: string };
+ style?: Record;
+ [key: string]: unknown;
+}
- useEffect(() => {
- if (!svgRef.current || width === 0 || height === 0 || kinds.length === 0) return;
-
- const svg = d3.select(svgRef.current);
- svg.selectAll('*').remove();
-
- const root = d3.hierarchy<{ name: string; children: KindEntry[] }>({
- name: 'root',
- children: kinds,
- })
- .sum(d => (d as unknown as KindEntry).count ?? 0)
- .sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
-
- d3.treemap<{ name: string; children: KindEntry[] }>()
- .size([width, height])
- .padding(3)
- .tile(d3.treemapResquarify)(root);
-
- const leaves = root.leaves() as unknown as d3.HierarchyRectangularNode[];
-
- const g = svg.append('g');
-
- leaves.forEach(leaf => {
- const w = leaf.x1 - leaf.x0;
- const h = leaf.y1 - leaf.y0;
- if (w < 4 || h < 4) return;
-
- const cell = g.append('g')
- .attr('transform', `translate(${leaf.x0},${leaf.y0})`)
- .style('cursor', 'pointer')
- .on('click', () => onDrillDown(leaf.data.kind));
-
- const color = getKindColor(leaf.data.kind);
-
- cell.append('rect')
- .attr('width', w)
- .attr('height', h)
- .attr('rx', 4)
- .attr('fill', color)
- .attr('fill-opacity', 0.15)
- .attr('stroke', color)
- .attr('stroke-opacity', 0.5)
- .attr('stroke-width', 1)
- .on('mouseover', function () {
- d3.select(this).attr('fill-opacity', 0.3);
- })
- .on('mouseout', function () {
- d3.select(this).attr('fill-opacity', 0.15);
- });
-
- // Kind label
- if (w > 40 && h > 28) {
- cell.append('text')
- .attr('x', 6)
- .attr('y', 16)
- .attr('font-size', Math.min(13, w / 8))
- .attr('font-weight', '600')
- .attr('fill', color)
- .attr('pointer-events', 'none')
- .text(leaf.data.kind.toUpperCase());
- }
+interface G6ComboDatum {
+ id: string;
+ style?: Record;
+ [key: string]: unknown;
+}
- // Count
- if (w > 30 && h > 44) {
- cell.append('text')
- .attr('x', 6)
- .attr('y', 32)
- .attr('font-size', Math.min(11, w / 10))
- .attr('fill', '#94a3b8')
- .attr('pointer-events', 'none')
- .text(`${leaf.data.count} nodes`);
- }
+interface G6Data {
+ nodes: G6NodeDatum[];
+ edges: G6EdgeDatum[];
+ combos?: G6ComboDatum[];
+}
- // Tooltip title element
- cell.append('title')
- .text(`${leaf.data.kind}: ${leaf.data.count} nodes\nClick to explore`);
- });
- }, [width, height, kinds, onDrillDown]);
+// ─── Constants ───────────────────────────────────────────────────────────────
+
+const LAYOUT_CONFIG: Record = {
+ force: {
+ type: 'd3-force',
+ link: { distance: 120 },
+ charge: { strength: -400 },
+ collide: { radius: 28, strength: 0.8 },
+ },
+ dagre: {
+ type: 'antv-dagre',
+ rankdir: 'LR',
+ nodesep: 40,
+ ranksep: 80,
+ },
+ radial: {
+ type: 'radial',
+ unitRadius: 90,
+ linkDistance: 130,
+ },
+ circular: {
+ type: 'circular',
+ radius: 200,
+ ordering: 'degree',
+ },
+};
- return ;
+// ─── Helpers to build G6 data from API responses ─────────────────────────────
+
+function topologyToG6Data(topo: TopologyResponse): G6Data {
+ const nodes: G6NodeDatum[] = topo.services.map(svc => ({
+ id: svc.name,
+ data: {
+ label: svc.name,
+ kind: 'service',
+ layer: svc.layer,
+ nodeCount: svc.nodeCount,
+ },
+ style: {
+ fill: getKindColor('service'),
+ fillOpacity: 0.85,
+ stroke: getKindColor('service'),
+ strokeOpacity: 0.6,
+ lineWidth: 1.5,
+ size: Math.max(24, Math.min(56, 24 + (svc.nodeCount ?? 0) / 10)),
+ labelText: svc.name,
+ labelFontSize: 11,
+ labelFontWeight: 500,
+ labelFill: '#e2e8f0',
+ labelPosition: 'bottom',
+ cursor: 'pointer',
+ },
+ }));
+
+ const edges: G6EdgeDatum[] = topo.dependencies.map((dep, i) => ({
+ id: `edge-${i}`,
+ source: dep.source,
+ target: dep.target,
+ data: { kind: dep.kind },
+ style: {
+ stroke: getEdgeColor(dep.kind ?? 'default'),
+ lineWidth: 1.5,
+ strokeOpacity: 0.6,
+ endArrow: true,
+ },
+ }));
+
+ return { nodes, edges };
}
-// ─── Drill-down treemap: nodes of a specific kind ───────────────────────────
-
-interface NodeTreemapProps {
- width: number;
- height: number;
- nodes: NodeResponse[];
- kind: string;
+function moduleNodesToG6Data(nodes: NodeResponse[], module: string): G6Data {
+ const g6Nodes: G6NodeDatum[] = nodes.map(n => ({
+ id: n.id,
+ data: {
+ label: n.label,
+ kind: n.kind,
+ filePath: n.file_path,
+ module: n.module ?? module,
+ layer: n.layer,
+ },
+ style: {
+ fill: getKindColor(n.kind),
+ fillOpacity: 0.8,
+ stroke: getKindColor(n.kind),
+ strokeOpacity: 0.5,
+ lineWidth: 1.5,
+ size: 22,
+ labelText: n.label.length > 20 ? n.label.slice(0, 20) + '…' : n.label,
+ labelFontSize: 10,
+ labelFill: '#cbd5e1',
+ labelPosition: 'bottom',
+ cursor: 'pointer',
+ },
+ }));
+
+ return { nodes: g6Nodes, edges: [] };
}
-function NodeTreemap({ width, height, nodes, kind }: NodeTreemapProps) {
- const svgRef = useRef(null);
+function egoToG6Data(ego: EgoGraphResponse): G6Data {
+ const nodes: G6NodeDatum[] = ego.nodes.map(n => ({
+ id: n.id,
+ data: {
+ label: n.label,
+ kind: n.kind,
+ filePath: n.file_path,
+ module: n.module,
+ },
+ style: {
+ fill: n.id === ego.center ? '#f59e0b' : getKindColor(n.kind),
+ fillOpacity: n.id === ego.center ? 1 : 0.8,
+ stroke: n.id === ego.center ? '#fbbf24' : getKindColor(n.kind),
+ strokeOpacity: n.id === ego.center ? 1 : 0.5,
+ lineWidth: n.id === ego.center ? 2.5 : 1.5,
+ size: n.id === ego.center ? 30 : 20,
+ labelText: n.label.length > 18 ? n.label.slice(0, 18) + '…' : n.label,
+ labelFontSize: 10,
+ labelFill: '#cbd5e1',
+ labelPosition: 'bottom',
+ cursor: 'pointer',
+ },
+ }));
+
+ const edges: G6EdgeDatum[] = ego.edges.map((e, i) => ({
+ id: e.id ?? `ego-edge-${i}`,
+ source: e.source,
+ target: e.target ?? ego.center,
+ data: { kind: e.kind },
+ style: {
+ stroke: getEdgeColor(e.kind ?? 'default'),
+ lineWidth: 1.5,
+ strokeOpacity: 0.5,
+ endArrow: true,
+ },
+ }));
+
+ return { nodes, edges };
+}
+// ─── Context menu component ───────────────────────────────────────────────────
+
+function ContextMenu({
+ menu,
+ onClose,
+ onShowDetails,
+ onFindCallers,
+ onFindDeps,
+ onDrillDown,
+ level,
+}: {
+ menu: ContextMenuState;
+ onClose: () => void;
+ onShowDetails: (id: string) => void;
+ onFindCallers: (id: string) => void;
+ onFindDeps: (id: string) => void;
+ onDrillDown: (id: string, label: string, kind: string) => void;
+ level: DrillLevel;
+}) {
useEffect(() => {
- if (!svgRef.current || width === 0 || height === 0 || nodes.length === 0) return;
-
- const svg = d3.select(svgRef.current);
- svg.selectAll('*').remove();
-
- // Size each node tile by its edge count if available; otherwise equal weight
- const root = d3.hierarchy<{ name: string; children: NodeResponse[] }>({
- name: 'root',
- children: nodes,
- })
- .sum(() => 1)
- .sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
-
- d3.treemap<{ name: string; children: NodeResponse[] }>()
- .size([width, height])
- .padding(2)
- .tile(d3.treemapResquarify)(root);
-
- const leaves = root.leaves() as unknown as d3.HierarchyRectangularNode[];
- const color = getKindColor(kind);
-
- const g = svg.append('g');
-
- leaves.forEach(leaf => {
- const w = leaf.x1 - leaf.x0;
- const h = leaf.y1 - leaf.y0;
- if (w < 2 || h < 2) return;
-
- const cell = g.append('g')
- .attr('transform', `translate(${leaf.x0},${leaf.y0})`);
-
- cell.append('rect')
- .attr('width', w)
- .attr('height', h)
- .attr('rx', 3)
- .attr('fill', color)
- .attr('fill-opacity', 0.12)
- .attr('stroke', color)
- .attr('stroke-opacity', 0.4)
- .attr('stroke-width', 1)
- .on('mouseover', function () {
- d3.select(this).attr('fill-opacity', 0.28);
- })
- .on('mouseout', function () {
- d3.select(this).attr('fill-opacity', 0.12);
- });
-
- if (w > 30 && h > 18) {
- const label = leaf.data.label || leaf.data.id;
- cell.append('text')
- .attr('x', 4)
- .attr('y', 13)
- .attr('font-size', Math.min(11, w / 7))
- .attr('fill', color)
- .attr('font-weight', '500')
- .attr('pointer-events', 'none')
- .text(label.length > Math.floor(w / 7) ? label.slice(0, Math.floor(w / 7)) + '…' : label);
- }
+ const handler = () => onClose();
+ window.addEventListener('click', handler);
+ return () => window.removeEventListener('click', handler);
+ }, [onClose]);
+
+ const items = [
+ { label: 'Show details', action: () => onShowDetails(menu.nodeId) },
+ { label: 'Find callers', action: () => onFindCallers(menu.nodeId) },
+ { label: 'Find dependencies', action: () => onFindDeps(menu.nodeId) },
+ ...(level < 2
+ ? [{ label: 'Explore subgraph', action: () => onDrillDown(menu.nodeId, menu.nodeLabel, menu.nodeKind) }]
+ : []),
+ ];
- const tooltipText = [
- leaf.data.label,
- leaf.data.file_path ? `File: ${leaf.data.file_path}` : null,
- leaf.data.module ? `Module: ${leaf.data.module}` : null,
- ].filter(Boolean).join('\n');
+ return (
+ e.stopPropagation()}
+ >
+
+ {menu.nodeLabel}
+
+ {items.map(item => (
+
+ ))}
+
+ );
+}
- cell.append('title').text(tooltipText);
- });
- }, [width, height, nodes, kind]);
+// ─── Node filter panel ────────────────────────────────────────────────────────
+
+function NodeFilterPanel({
+ kinds,
+ hidden,
+ onToggle,
+}: {
+ kinds: string[];
+ hidden: Set;
+ onToggle: (kind: string) => void;
+}) {
+ return (
+
+
Filter node types
+
+ {kinds.map(kind => (
+
+ ))}
+
+
+ );
+}
+
+// ─── Legend ───────────────────────────────────────────────────────────────────
- return ;
+function GraphLegend({ kinds }: { kinds: string[] }) {
+ if (kinds.length === 0) return null;
+ const display = kinds.slice(0, 10);
+ return (
+
+
Legend
+
+ {display.map(kind => (
+
+ ))}
+ {kinds.length > 10 && (
+
+{kinds.length - 10} more
+ )}
+
+
+ );
}
-// ─── Main view ───────────────────────────────────────────────────────────────
+// ─── Main Component ───────────────────────────────────────────────────────────
export default function CodeGraphView() {
const containerRef = useRef(null);
- const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
- const [selectedKind, setSelectedKind] = useState(null);
- const [drillNodes, setDrillNodes] = useState([]);
- const [drillTotal, setDrillTotal] = useState(0);
- const [drillLoading, setDrillLoading] = useState(false);
+ const minimapRef = useRef(null);
+ const graphRef = useRef(null);
+ const isInitializedRef = useRef(false);
- const { data: kindsData, loading: kindsLoading } = useApi(() => api.getKinds(), []);
+ const [level, setLevel] = useState(0);
+ const [breadcrumb, setBreadcrumb] = useState([
+ { label: 'Landscape', level: 0 },
+ ]);
- // Observe container size
- useEffect(() => {
- const el = containerRef.current;
- if (!el) return;
+ const [layoutMode, setLayoutMode] = useState('force');
+ const [hiddenKinds, setHiddenKinds] = useState>(new Set());
+ const [availableKinds, setAvailableKinds] = useState([]);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [showFilter, setShowFilter] = useState(false);
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [nodeCount, setNodeCount] = useState(0);
+
+ const [contextMenu, setContextMenu] = useState(null);
+
+ const { selectedPath, selectedType } = useFileSelection();
+ const { openPanel, closePanel } = useRightPanel();
+
+ // Stable refs so graph event handlers can call the latest callbacks without rebuild
+ const openPanelRef = useRef(openPanel);
+ const closePanelRef = useRef(closePanel);
+ const loadLevel2Ref = useRef<(id: string) => void>(() => {});
+ useEffect(() => { openPanelRef.current = openPanel; }, [openPanel]);
+ useEffect(() => { closePanelRef.current = closePanel; }, [closePanel]);
+
+ // Current drill-down ids
+ const currentServiceRef = useRef(null);
+ const currentNodeIdRef = useRef(null);
+
+ // ── Build and render graph ──────────────────────────────────────────────────
+
+ const renderG6 = useCallback(async (data: G6Data, layout: LayoutMode) => {
+ if (!containerRef.current) return;
- const observer = new ResizeObserver(entries => {
- const entry = entries[0];
- if (entry) {
- setDimensions({
- width: entry.contentRect.width,
- height: entry.contentRect.height,
- });
+ const dataReceivedAt = performance.now();
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const { Graph } = await import('@antv/g6') as any;
+
+ const container = containerRef.current;
+ // Reset render state for this render pass
+ container.removeAttribute('data-render-state');
+ const { offsetWidth: width, offsetHeight: height } = container;
+
+ // Destroy previous instance
+ if (graphRef.current) {
+ graphRef.current.destroy();
+ graphRef.current = null;
+ }
+
+ // Filter hidden kinds
+ const filteredNodes = data.nodes.filter(n => !hiddenKinds.has(n.data.kind));
+ const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
+ const filteredEdges = data.edges.filter(
+ e => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
+ );
+
+ const graph = new Graph({
+ container,
+ width,
+ height,
+ autoFit: 'view',
+ animation: { duration: 400 },
+ data: {
+ nodes: filteredNodes,
+ edges: filteredEdges,
+ combos: data.combos,
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ layout: LAYOUT_CONFIG[layout] as any,
+ behaviors: [
+ 'drag-canvas',
+ 'zoom-canvas',
+ 'drag-element',
+ { type: 'click-select', multiple: false },
+ ],
+ plugins: [
+ {
+ type: 'minimap',
+ key: 'minimap',
+ container: minimapRef.current ?? undefined,
+ size: [160, 100],
+ },
+ {
+ type: 'tooltip',
+ key: 'tooltip',
+ trigger: 'hover',
+ getContent: (_: unknown, items: G6NodeDatum[]) => {
+ const item = items?.[0];
+ if (!item) return '';
+ const esc = (s: string) => s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ const { label, kind, filePath, nodeCount: nc } = item.data;
+ return `
+
+
${esc(label ?? '')}
+
Kind: ${esc(kind ?? '')}
+ ${filePath ? `
${esc(filePath)}
` : ''}
+ ${nc !== undefined ? `
Nodes: ${nc}
` : ''}
+
Double-click to drill down · Right-click for menu
+
+ `;
+ },
+ },
+ ],
+ node: {
+ state: {
+ selected: {
+ stroke: '#f59e0b',
+ lineWidth: 2.5,
+ shadowColor: '#f59e0b',
+ shadowBlur: 12,
+ },
+ highlighted: {
+ fillOpacity: 1,
+ strokeOpacity: 1,
+ },
+ dimmed: {
+ fillOpacity: 0.2,
+ strokeOpacity: 0.15,
+ labelOpacity: 0.2,
+ },
+ },
+ },
+ edge: {
+ state: {
+ highlighted: {
+ strokeOpacity: 1,
+ lineWidth: 2.5,
+ },
+ dimmed: {
+ strokeOpacity: 0.1,
+ },
+ },
+ },
+ combo: {
+ style: {
+ fillOpacity: 0.05,
+ strokeOpacity: 0.3,
+ lineWidth: 1,
+ labelFontSize: 11,
+ labelFill: '#94a3b8',
+ },
+ },
+ });
+ graphRef.current = graph;
+
+ // ── Node click: highlight dependencies + open right panel ─────────────
+ graph.on('node:click', (evt: unknown) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const nodeId = (evt as any)?.target?.id as string | undefined;
+ if (!nodeId) return;
+ setContextMenu(null);
+
+ // Highlight direct neighbors, dim others
+ const allNodes = graph.getData().nodes as G6NodeDatum[];
+ const allEdges = graph.getData().edges as G6EdgeDatum[];
+
+ const connectedEdges = allEdges.filter(
+ e => e.source === nodeId || e.target === nodeId,
+ );
+ const connectedIds = new Set([nodeId]);
+ connectedEdges.forEach(e => {
+ connectedIds.add(e.source);
+ connectedIds.add(e.target);
+ });
+
+ const stateMap: Record = {};
+ allNodes.forEach(n => {
+ stateMap[n.id] = connectedIds.has(n.id) ? ['highlighted'] : ['dimmed'];
+ });
+ stateMap[nodeId] = ['selected'];
+ allEdges.forEach((e, i) => {
+ const eid = e.id ?? `edge-${i}`;
+ stateMap[eid] = connectedEdges.includes(e) ? ['highlighted'] : ['dimmed'];
+ });
+
+ graph.setElementState(stateMap);
+
+ // Open details panel for the selected node
+ openPanelRef.current(
+ loadLevel2Ref.current(targetId)}
+ />,
+ );
+ });
+
+ // ── Canvas click: clear states + close panel ────────────────────────────
+ graph.on('canvas:click', () => {
+ setContextMenu(null);
+ const stateMap: Record = {};
+ const allNodes = graph.getData().nodes as G6NodeDatum[];
+ const allEdges = graph.getData().edges as G6EdgeDatum[];
+ allNodes.forEach(n => { stateMap[n.id] = []; });
+ allEdges.forEach((e, i) => { stateMap[e.id ?? `edge-${i}`] = []; });
+ graph.setElementState(stateMap);
+ closePanelRef.current();
+ });
+
+ // ── Double-click: drill down ────────────────────────────────────────────
+ graph.on('node:dblclick', (evt: unknown) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const nodeId = (evt as any)?.target?.id as string | undefined;
+ if (!nodeId) return;
+ const nodeData = (graph.getData().nodes as G6NodeDatum[]).find(n => n.id === nodeId);
+ if (nodeData) {
+ handleDrillDown(nodeId, nodeData.data.label, nodeData.data.kind);
}
});
- observer.observe(el);
- // Initial size
- setDimensions({ width: el.clientWidth, height: el.clientHeight });
+ // ── Right-click context menu ────────────────────────────────────────────
+ graph.on('node:contextmenu', (evt: unknown) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const e = evt as any;
+ if (e?.preventDefault) e.preventDefault();
+ const nodeId = e?.target?.id as string | undefined;
+ if (!nodeId) return;
+ const nodeData = (graph.getData().nodes as G6NodeDatum[]).find(n => n.id === nodeId);
+ setContextMenu({
+ x: e?.client?.x ?? e?.clientX ?? 0,
+ y: e?.client?.y ?? e?.clientY ?? 0,
+ nodeId,
+ nodeLabel: nodeData?.data.label ?? nodeId,
+ nodeKind: nodeData?.data.kind ?? 'unknown',
+ });
+ });
+
+ // Expose render timing + state for E2E tests
+ graph.on('afterrender', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any).__graphRenderMs = performance.now() - dataReceivedAt;
+ container.setAttribute('data-render-state', 'ready');
+ });
+
+ await graph.render();
+ }, [hiddenKinds]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ── Data loaders ────────────────────────────────────────────────────────────
+
+ const loadLevel0 = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const topo = await api.getTopology();
+ const data = topologyToG6Data(topo);
+ const kinds = [...new Set(data.nodes.map(n => n.data.kind))];
+ setAvailableKinds(kinds);
+ setNodeCount(data.nodes.length);
+ await renderG6(data, layoutMode);
+ } catch (e) {
+ // Fallback: load kind nodes if topology not available
+ try {
+ const kindsRes = await api.getKinds();
+ const topServices = kindsRes.kinds.filter(k =>
+ ['service', 'module', 'class', 'component'].includes(k.kind),
+ ).slice(0, 60);
+
+ const nodesRes = await Promise.all(
+ topServices.map(k => api.getNodesByKind(k.kind, 20, 0)),
+ );
+ const allNodes: NodeResponse[] = nodesRes.flatMap(r => r.nodes ?? []);
+ const filteredNodes = selectedPath
+ ? allNodes.filter(n => {
+ if (!n.file_path) return false;
+ return selectedType === 'directory'
+ ? n.file_path.startsWith(selectedPath)
+ : n.file_path === selectedPath;
+ })
+ : allNodes;
+
+ const data = moduleNodesToG6Data(filteredNodes, 'root');
+ const kinds = [...new Set(filteredNodes.map(n => n.kind))];
+ setAvailableKinds(kinds);
+ setNodeCount(filteredNodes.length);
+ await renderG6(data, layoutMode);
+ } catch (e2) {
+ setError(e2 instanceof Error ? e2.message : 'Failed to load graph data');
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, [layoutMode, renderG6, selectedPath, selectedType]);
- return () => observer.disconnect();
- }, []);
+ const loadLevel1 = useCallback(async (serviceId: string) => {
+ setLoading(true);
+ setError(null);
+ currentServiceRef.current = serviceId;
+ try {
+ const res: NodesListResponse = await api.getNodes(undefined, serviceId, 300, 0);
+ const nodes = res.nodes ?? [];
+ const filtered = selectedPath
+ ? nodes.filter(n => {
+ if (!n.file_path) return false;
+ return selectedType === 'directory'
+ ? n.file_path.startsWith(selectedPath)
+ : n.file_path === selectedPath;
+ })
+ : nodes;
+ const data = moduleNodesToG6Data(filtered, serviceId);
+ const kinds = [...new Set(filtered.map(n => n.kind))];
+ setAvailableKinds(kinds);
+ setNodeCount(filtered.length);
+ await renderG6(data, 'force');
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Failed to load module nodes');
+ } finally {
+ setLoading(false);
+ }
+ }, [renderG6, selectedPath, selectedType]);
- const handleDrillDown = useCallback(async (kind: string) => {
- setSelectedKind(kind);
- setDrillLoading(true);
- setDrillNodes([]);
- setDrillTotal(0);
+ const loadLevel2 = useCallback(async (nodeId: string) => {
+ setLoading(true);
+ setError(null);
+ currentNodeIdRef.current = nodeId;
try {
- const result: NodesListResponse = await api.getNodesByKind(kind, 200, 0);
- setDrillNodes(result.nodes ?? []);
- setDrillTotal(result.total ?? result.count ?? (result.nodes ?? []).length);
- } catch {
- setDrillNodes([]);
- setDrillTotal(0);
+ const ego: EgoGraphResponse = await api.getEgoGraph(nodeId, 2);
+ const data = egoToG6Data(ego);
+ const kinds = [...new Set(data.nodes.map(n => n.data.kind))];
+ setAvailableKinds(kinds);
+ setNodeCount(data.nodes.length);
+ await renderG6(data, 'dagre');
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Failed to load component graph');
} finally {
- setDrillLoading(false);
+ setLoading(false);
+ }
+ }, [renderG6]);
+
+ // Keep loadLevel2Ref in sync for stable use inside graph event handlers
+ useEffect(() => { loadLevel2Ref.current = loadLevel2; }, [loadLevel2]);
+
+ // ── Drill-down handler ──────────────────────────────────────────────────────
+
+ const handleDrillDown = useCallback((nodeId: string, label: string, kind: string) => {
+ if (level === 0) {
+ setBreadcrumb(bc => [
+ ...bc,
+ { label, level: 1, serviceId: nodeId },
+ ]);
+ setLevel(1);
+ setHiddenKinds(new Set());
+ loadLevel1(nodeId);
+ } else if (level === 1) {
+ // Only drill into substantive nodes
+ if (['class', 'function', 'component', 'service', 'controller', 'repository'].includes(kind)) {
+ setBreadcrumb(bc => [
+ ...bc,
+ { label, level: 2, nodeId },
+ ]);
+ setLevel(2);
+ setHiddenKinds(new Set());
+ loadLevel2(nodeId);
+ }
+ }
+ }, [level, loadLevel1, loadLevel2]);
+
+ // ── Navigate breadcrumb ─────────────────────────────────────────────────────
+
+ const navigateTo = useCallback((targetLevel: DrillLevel) => {
+ if (targetLevel === level) return;
+ setHiddenKinds(new Set());
+ setContextMenu(null);
+
+ if (targetLevel === 0) {
+ setBreadcrumb([{ label: 'Landscape', level: 0 }]);
+ setLevel(0);
+ loadLevel0();
+ } else if (targetLevel === 1) {
+ const serviceItem = breadcrumb.find(b => b.level === 1);
+ if (serviceItem?.serviceId) {
+ setBreadcrumb(bc => bc.slice(0, 2));
+ setLevel(1);
+ loadLevel1(serviceItem.serviceId!);
+ }
+ }
+ }, [level, breadcrumb, loadLevel0, loadLevel1]);
+
+ // ── Initial load ────────────────────────────────────────────────────────────
+
+ useEffect(() => {
+ if (isInitializedRef.current) return;
+ isInitializedRef.current = true;
+ loadLevel0();
+
+ return () => {
+ if (graphRef.current) {
+ graphRef.current.destroy();
+ graphRef.current = null;
+ }
+ };
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Re-load when file selection changes
+ useEffect(() => {
+ if (!isInitializedRef.current) return;
+ if (level === 0) loadLevel0();
+ else if (level === 1 && currentServiceRef.current) loadLevel1(currentServiceRef.current);
+ else if (level === 2 && currentNodeIdRef.current) loadLevel2(currentNodeIdRef.current);
+ }, [selectedPath]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Reload when layout changes
+ useEffect(() => {
+ if (!isInitializedRef.current || !graphRef.current) return;
+ if (level === 0) loadLevel0();
+ else if (level === 1 && currentServiceRef.current) loadLevel1(currentServiceRef.current);
+ return () => {
+ if (graphRef.current) {
+ graphRef.current.destroy();
+ graphRef.current = null;
+ }
+ };
+ }, [layoutMode]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ── Controls ─────────────────────────────────────────────────────────────────
+
+ const handleZoomIn = () => graphRef.current?.zoomBy(1.3);
+ const handleZoomOut = () => graphRef.current?.zoomBy(0.77);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const handleFitView = () => (graphRef.current as any)?.fitView({ padding: [40, 40, 40, 40] });
+
+ const handleToggleFilter = (kind: string) => {
+ setHiddenKinds(prev => {
+ const next = new Set(prev);
+ if (next.has(kind)) next.delete(kind);
+ else next.add(kind);
+ return next;
+ });
+ };
+
+ const handleFullscreen = () => {
+ const el = containerRef.current?.closest('.graph-fullscreen-wrap') as HTMLElement | null;
+ if (!el) return;
+ if (!isFullscreen) {
+ el.requestFullscreen?.();
+ setIsFullscreen(true);
+ } else {
+ document.exitFullscreen?.();
+ setIsFullscreen(false);
}
- }, []);
+ };
- const handleDrillUp = useCallback(() => {
- setSelectedKind(null);
- setDrillNodes([]);
- setDrillTotal(0);
- }, []);
+ // ── Context menu actions ────────────────────────────────────────────────────
- const kinds: KindEntry[] = kindsData?.kinds ?? [];
+ const handleShowDetails = (id: string) => {
+ openPanel(
+ loadLevel2(targetId)}
+ />,
+ );
+ };
+
+ const handleFindCallers = async (id: string) => {
+ try {
+ const res = await api.getCallers(id);
+ const nodes = (res as { nodes?: NodeResponse[] }).nodes ?? [];
+ if (nodes.length > 0) {
+ const data = moduleNodesToG6Data(nodes, 'callers');
+ setNodeCount(nodes.length);
+ await renderG6(data, 'dagre');
+ }
+ } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load callers'); }
+ };
+
+ const handleFindDeps = async (id: string) => {
+ try {
+ const res = await api.getDependencies(id);
+ const nodes = (res as { nodes?: NodeResponse[] }).nodes ?? [];
+ if (nodes.length > 0) {
+ const data = moduleNodesToG6Data(nodes, 'dependencies');
+ setNodeCount(nodes.length);
+ await renderG6(data, 'dagre');
+ }
+ } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load dependencies'); }
+ };
+
+ // ── Layout switcher labels ───────────────────────────────────────────────────
+
+ const LAYOUT_LABELS: Record = {
+ force: 'Force',
+ dagre: 'Hierarchical',
+ radial: 'Radial',
+ circular: 'Circular',
+ };
+
+ const levelLabels: Record = {
+ 0: 'Landscape — service topology',
+ 1: 'Module — components within service',
+ 2: 'Component — ego subgraph',
+ };
return (
-
- {/* Header + breadcrumb */}
-
+
+
+ {/* ── Header ── */}
+
Code Graph
-
- {selectedKind
- ? drillTotal > drillNodes.length
- ? `Showing ${drillNodes.length} of ${drillTotal} "${selectedKind}" nodes`
- : `${drillNodes.length} nodes of kind "${selectedKind}"`
- : `${kinds.length} node kinds — click a tile to explore`}
+
+ {loading
+ ? 'Loading…'
+ : error
+ ? 'Error loading graph'
+ : `${nodeCount} nodes — ${levelLabels[level]}`}
{/* Breadcrumb */}
-
- {/* Treemap container */}
-
- {(kindsLoading || drillLoading) && (
-
-
+ {/* ── Graph canvas + toolbar ── */}
+
+
+ {/* Canvas */}
+
+
+ {/* Minimap */}
+
+
+ {/* Legend */}
+
!hiddenKinds.has(k))} />
+
+ {/* Toolbar */}
+
+ {/* Layout selector */}
+
+ {(Object.keys(LAYOUT_LABELS) as LayoutMode[]).map(mode => (
+
+ ))}
+
+
+ {/* Icon controls */}
+
+
+
+
+
+
- )}
- {!kindsLoading && !drillLoading && !selectedKind && dimensions.width > 0 && (
-
+
+
+
+
+ {/* Filter panel */}
+ {showFilter && availableKinds.length > 0 && (
+
)}
- {!kindsLoading && !drillLoading && selectedKind && dimensions.width > 0 && (
-
+ {/* Loading overlay */}
+ {loading && (
+
+ )}
+
+ {/* Error overlay */}
+ {!loading && error && (
+
+ )}
+
+ {/* Empty state */}
+ {!loading && !error && nodeCount === 0 && (
+
+
No nodes found for current view.
+
)}
+
+ {/* ── Context menu ── */}
+ {contextMenu && (
+
setContextMenu(null)}
+ onShowDetails={handleShowDetails}
+ onFindCallers={handleFindCallers}
+ onFindDeps={handleFindDeps}
+ onDrillDown={handleDrillDown}
+ level={level}
+ />
+ )}
+
+ {/* ── Help text ── */}
+
+ Click node to highlight connections · Double-click to drill in · Right-click for actions · Scroll to zoom
+
);
}
+
+// Re-export KIND_COLORS for backwards compat with ExplorerView
+export { KIND_COLORS };
diff --git a/src/main/frontend/src/components/Dashboard.tsx b/src/main/frontend/src/components/Dashboard.tsx
index 2c9be591..8566e7c4 100644
--- a/src/main/frontend/src/components/Dashboard.tsx
+++ b/src/main/frontend/src/components/Dashboard.tsx
@@ -1,14 +1,140 @@
+import { useNavigate } from 'react-router-dom';
import { useApi } from '@/hooks/useApi';
import { api } from '@/lib/api';
-import type { StatsResponse } from '@/types/api';
import { isComputedStats } from '@/types/api';
import StatsCards from './StatsCards';
import FrameworkBadges from './FrameworkBadges';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
import {
- Shield, Database, Server, Layers, Globe, Code2,
- BarChart3, RefreshCw, AlertCircle, ArrowRightLeft
+ Shield, Database, Layers, Code2,
+ BarChart3, RefreshCw, AlertCircle,
+ Cpu, Network,
} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+/* ------------------------------------------------------------------ */
+/* Clickable stat row — navigates to explorer filtered by kind */
+/* ------------------------------------------------------------------ */
+interface StatRowProps {
+ label: string;
+ value: number;
+ total: number;
+ colorClass: string;
+ href?: string;
+}
+
+function StatRow({ label, value, total, colorClass, href }: StatRowProps) {
+ const navigate = useNavigate();
+ const pct = total > 0 ? (value / total) * 100 : 0;
+
+ return (
+
navigate(href) : undefined}
+ role={href ? 'button' : undefined}
+ tabIndex={href ? 0 : undefined}
+ onKeyDown={href ? (e) => { if (e.key === 'Enter' || e.key === ' ') navigate(href); } : undefined}
+ >
+
+
+ {label}
+
+
+ {value.toLocaleString()}
+
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Section card wrapper */
+/* ------------------------------------------------------------------ */
+interface SectionCardProps {
+ icon: React.ComponentType<{ className?: string }>;
+ iconClass: string;
+ title: string;
+ children: React.ReactNode;
+ className?: string;
+}
+
+function SectionCard({ icon: Icon, iconClass, title, children, className }: SectionCardProps) {
+ return (
+
+
+
+
+ {title}
+
+
+
+ {children}
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Clickable connection metric */
+/* ------------------------------------------------------------------ */
+interface ConnectionMetricProps {
+ label: string;
+ value: number;
+ colorClass: string;
+ href?: string;
+ sub?: Record
;
+}
+
+function ConnectionMetric({ label, value, colorClass, href, sub }: ConnectionMetricProps) {
+ const navigate = useNavigate();
+ return (
+ navigate(href) : undefined}
+ role={href ? 'button' : undefined}
+ tabIndex={href ? 0 : undefined}
+ onKeyDown={href ? (e) => { if (e.key === 'Enter' || e.key === ' ') navigate(href); } : undefined}
+ >
+
{label}
+
+ {value.toLocaleString()}
+
+ {sub && Object.keys(sub).length > 0 && (
+
+ {Object.entries(sub)
+ .sort(([, a], [, b]) => b - a)
+ .map(([k, v]) => (
+
+ {k}
+ {v.toLocaleString()}
+
+ ))}
+
+ )}
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Main Dashboard */
+/* ------------------------------------------------------------------ */
export default function Dashboard() {
const { data: stats, loading, error, refetch } = useApi(() => api.getStats(), []);
const { data: kinds } = useApi(() => api.getKinds(), []);
@@ -16,9 +142,9 @@ export default function Dashboard() {
if (loading) {
return (
-
-
-
Loading analysis data...
+
+
+
Loading analysis data…
);
@@ -27,41 +153,46 @@ export default function Dashboard() {
if (error) {
return (
-
-
-
No Analysis Data
-
- Run an analysis first, or check that the server is connected to an analyzed codebase.
-
-
+
+
);
}
if (!stats) return null;
- // Extract data depending on which API format we got
+ // --- Extract data from whichever API format we received ---
let totalNodes = 0;
let totalEdges = 0;
let totalFiles = 0;
let languages: Record
= {};
let frameworks: Record = {};
- let infra: { databases: Record; messaging: Record; cloud: Record } = { databases: {}, messaging: {}, cloud: {} };
- let connections: { rest: { total: number; by_method: Record }; grpc: number; websocket: number; producers: number; consumers: number } = { rest: { total: 0, by_method: {} }, grpc: 0, websocket: 0, producers: 0, consumers: 0 };
+ let infra: { databases: Record; messaging: Record; cloud: Record } =
+ { databases: {}, messaging: {}, cloud: {} };
+ let connections: {
+ rest: { total: number; by_method: Record };
+ grpc: number;
+ websocket: number;
+ producers: number;
+ consumers: number;
+ } = { rest: { total: 0, by_method: {} }, grpc: 0, websocket: 0, producers: 0, consumers: 0 };
let auth: Record = {};
let architecture: Record = {};
let nodeKinds: Record = {};
let layers: Record = {};
if (isComputedStats(stats)) {
- // Primary format from StatsService.computeStats()
totalNodes = stats.graph?.nodes || 0;
totalEdges = stats.graph?.edges || 0;
totalFiles = stats.graph?.files || 0;
@@ -72,14 +203,13 @@ export default function Dashboard() {
auth = stats.auth || {};
architecture = stats.architecture || {};
} else {
- // Fallback format from QueryService.getStats()
totalNodes = stats.node_count || 0;
totalEdges = stats.edge_count || 0;
nodeKinds = stats.nodes_by_kind || {};
layers = stats.nodes_by_layer || {};
}
- // Build nodeKinds from kinds endpoint if available (more reliable)
+ // Prefer kinds endpoint data for nodeKinds (more reliable)
if (kinds?.kinds) {
nodeKinds = {};
for (const k of kinds.kinds) {
@@ -87,28 +217,39 @@ export default function Dashboard() {
}
}
- // Build layers from architecture if we got computeStats format
- // (architecture contains classes, interfaces, etc. but not layers directly)
- // Layers come from the node data itself -- use kinds endpoint nodes or architecture data
+ const totalLangFiles = Object.values(languages).reduce((s, v) => s + v, 0);
+ const totalArchItems = Object.values(architecture).reduce((s, v) => s + v, 0);
+ const hasConnections =
+ connections.rest.total > 0 || connections.grpc > 0 ||
+ connections.websocket > 0 || connections.producers > 0 || connections.consumers > 0;
+ const hasInfra =
+ Object.keys(infra.databases || {}).length > 0 ||
+ Object.keys(infra.messaging || {}).length > 0 ||
+ Object.keys(infra.cloud || {}).length > 0;
return (
-
+
+
{/* Page header */}
-
Dashboard
-
Code knowledge graph overview
+
+ Dashboard
+
+
Code knowledge graph overview
-
-
+
- {/* Hero stats */}
+ {/* Hero stat cards */}
- {/* Frameworks */}
- {Object.keys(frameworks).length > 0 &&
}
+ {/* Frameworks row */}
+ {Object.keys(frameworks).length > 0 && (
+
+ )}
+
+ {/* --- Primary grid: Node Kinds | Languages | Architecture --- */}
+
- {/* Grid: Node Kinds + Languages + Architecture */}
-
- {/* Node Kinds breakdown */}
+ {/* Node Kinds */}
{Object.keys(nodeKinds).length > 0 && (
-
-
-
-
Node Kinds
-
-
+
+
{Object.entries(nodeKinds)
.sort(([, a], [, b]) => b - a)
- .slice(0, 12)
- .map(([kind, count]) => {
- const pct = (count / (totalNodes || 1)) * 100;
- return (
-
-
- {kind}
- {count.toLocaleString()}
-
-
-
- );
- })}
+ .slice(0, 14)
+ .map(([kind, count]) => (
+
+ ))}
-
+
)}
{/* Languages */}
{Object.keys(languages).length > 0 && (
-
-
-
-
Languages
-
-
+
+
{Object.entries(languages)
.sort(([, a], [, b]) => b - a)
- .map(([lang, count]) => {
- const total = Object.values(languages).reduce((s, v) => s + v, 0);
- const pct = (count / (total || 1)) * 100;
- return (
-
-
- {lang}
- {count.toLocaleString()}
-
-
-
- );
- })}
+ .map(([lang, count]) => (
+
+ ))}
-
+
)}
{/* Architecture */}
{Object.keys(architecture).length > 0 && (
-
-
-
-
Architecture
-
-
+
+
{Object.entries(architecture)
.sort(([, a], [, b]) => b - a)
- .map(([item, count]) => {
- const total = Object.values(architecture).reduce((s, v) => s + v, 0);
- const pct = (count / (total || 1)) * 100;
- return (
-
-
- {item.replace(/_/g, ' ')}
-
- {count.toLocaleString()} ({pct.toFixed(0)}%)
-
-
-
-
- );
- })}
+ .map(([item, count]) => (
+
+ ))}
-
+
)}
- {/* Layers fallback (from QueryService format) */}
+ {/* Architecture Layers (fallback from QueryService format) */}
{Object.keys(layers).length > 0 && (
-
-
-
-
Architecture Layers
-
-
- {Object.entries(layers)
- .sort(([, a], [, b]) => b - a)
- .map(([layer, count]) => {
- const colors: Record
= {
- frontend: 'from-cyan-500 to-blue-500',
- backend: 'from-brand-500 to-purple-500',
- infra: 'from-amber-500 to-orange-500',
- shared: 'from-emerald-500 to-green-500',
- unknown: 'from-surface-600 to-surface-500',
- };
- const total = Object.values(layers).reduce((s, v) => s + v, 0);
- const pct = (count / (total || 1)) * 100;
- return (
-
-
- {layer}
-
- {count.toLocaleString()} ({pct.toFixed(0)}%)
-
-
-
-
- );
- })}
+
+
+ {(() => {
+ const layerColors: Record = {
+ frontend: 'bg-gradient-to-r from-cyan-500 to-blue-500',
+ backend: 'bg-gradient-to-r from-indigo-500 to-purple-500',
+ infra: 'bg-gradient-to-r from-amber-500 to-orange-500',
+ shared: 'bg-gradient-to-r from-emerald-500 to-green-500',
+ unknown: 'bg-gradient-to-r from-muted to-muted-foreground/30',
+ };
+ const total = Object.values(layers).reduce((s, v) => s + v, 0);
+ return Object.entries(layers)
+ .sort(([, a], [, b]) => b - a)
+ .map(([layer, count]) => (
+
+ ));
+ })()}
-
+
)}
- {/* Connections section -- properly render nested structure */}
- {(connections.rest.total > 0 || connections.grpc > 0 || connections.websocket > 0 || connections.producers > 0 || connections.consumers > 0) && (
-
-
-
- {/* REST endpoints */}
+ {/* --- Connections section --- */}
+ {hasConnections && (
+
+
{connections.rest.total > 0 && (
-
-
REST Endpoints
-
{connections.rest.total.toLocaleString()}
- {Object.keys(connections.rest.by_method || {}).length > 0 && (
-
- {Object.entries(connections.rest.by_method)
- .sort(([, a], [, b]) => b - a)
- .map(([method, count]) => (
-
- {method}
- {count.toLocaleString()}
-
- ))}
-
- )}
-
+
)}
- {/* gRPC */}
{connections.grpc > 0 && (
-
-
gRPC Services
-
{connections.grpc.toLocaleString()}
-
+
)}
- {/* WebSocket */}
{connections.websocket > 0 && (
-
-
WebSocket
-
{connections.websocket.toLocaleString()}
-
+
)}
- {/* Producers */}
{connections.producers > 0 && (
-
-
Producers
-
{connections.producers.toLocaleString()}
-
+
)}
- {/* Consumers */}
{connections.consumers > 0 && (
-
-
Consumers
-
{connections.consumers.toLocaleString()}
-
+
)}
-
+
)}
- {/* Infrastructure + Auth side by side */}
-
- {/* Infrastructure -- nested with sub-categories */}
- {(Object.keys(infra.databases || {}).length > 0 ||
- Object.keys(infra.messaging || {}).length > 0 ||
- Object.keys(infra.cloud || {}).length > 0) && (
-
-
-
-
Infrastructure
-
-
-
-
-
-
-
- )}
+ {/* --- Infrastructure + Auth side by side --- */}
+ {(hasInfra || Object.keys(auth).length > 0) && (
+
- {/* Authentication */}
- {Object.keys(auth).length > 0 && (
-
-
-
-
Authentication
-
-
- {Object.entries(auth)
- .sort(([, a], [, b]) => b - a)
- .map(([k, v]) => (
-
- {k.replace(/_/g, ' ')}
- {v.toLocaleString()}
-
- ))}
-
-
- )}
-
+ {/* Infrastructure */}
+ {hasInfra && (
+
+
+
+
+
+
+
+ )}
+
+ {/* Authentication */}
+ {Object.keys(auth).length > 0 && (
+
+
+ {(() => {
+ const authTotal = Object.values(auth).reduce((s, n) => s + n, 0);
+ return Object.entries(auth)
+ .sort(([, a], [, b]) => b - a)
+ .map(([k, v]) => (
+
+
+
+
+ {k.replace(/_/g, ' ')}
+
+
+
+
+ {((v / authTotal) * 100).toFixed(0)}%
+
+
+ {v.toLocaleString()}
+
+
+
+ ));
+ })()}
+
+
+ )}
+
+ )}
);
}
-/** Renders a sub-section of infrastructure (databases, messaging, cloud) */
-function InfraSubSection({
- title,
- items,
- icon: Icon,
-}: {
+/* ------------------------------------------------------------------ */
+/* Infrastructure sub-section */
+/* ------------------------------------------------------------------ */
+interface InfraSubSectionProps {
title: string;
items: Record
| undefined;
- icon: React.ComponentType<{ className?: string }>;
-}) {
+ colorClass: string;
+}
+
+function InfraSubSection({ title, items, colorClass }: InfraSubSectionProps) {
const entries = Object.entries(items || {});
if (entries.length === 0) return null;
+ const total = entries.reduce((s, [, v]) => s + v, 0);
return (
-
-
+
+ {title}
+
+
{entries
.sort(([, a], [, b]) => b - a)
.map(([k, v]) => (
-
- {k}
- {v.toLocaleString()}
-
+
))}
diff --git a/src/main/frontend/src/components/ErrorBoundary.tsx b/src/main/frontend/src/components/ErrorBoundary.tsx
new file mode 100644
index 00000000..fa040926
--- /dev/null
+++ b/src/main/frontend/src/components/ErrorBoundary.tsx
@@ -0,0 +1,45 @@
+import { Component, ErrorInfo, ReactNode } from 'react';
+
+interface Props {
+ children: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+}
+
+export default class ErrorBoundary extends Component
{
+ constructor(props: Props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ console.error('[ErrorBoundary] Caught render error:', error, info);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
Something went wrong
+
+ {this.state.error?.message ?? 'An unexpected error occurred.'}
+
+
window.location.reload()}
+ className="px-4 py-2 text-sm rounded bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
+ >
+ Reload page
+
+
+ );
+ }
+ return this.props.children;
+ }
+}
diff --git a/src/main/frontend/src/components/FrameworkBadges.tsx b/src/main/frontend/src/components/FrameworkBadges.tsx
index 442c77eb..86b41a7f 100644
--- a/src/main/frontend/src/components/FrameworkBadges.tsx
+++ b/src/main/frontend/src/components/FrameworkBadges.tsx
@@ -1,67 +1,81 @@
-const frameworkColors: Record = {
- spring: 'bg-green-500/10 text-green-400 border-green-500/20',
- 'spring boot': 'bg-green-500/10 text-green-400 border-green-500/20',
- nestjs: 'bg-red-500/10 text-red-400 border-red-500/20',
- express: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',
- fastapi: 'bg-teal-500/10 text-teal-400 border-teal-500/20',
- django: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
- react: 'bg-cyan-500/10 text-cyan-400 border-cyan-500/20',
- angular: 'bg-red-500/10 text-red-400 border-red-500/20',
- vue: 'bg-green-500/10 text-green-400 border-green-500/20',
- flask: 'bg-slate-400/10 text-slate-300 border-slate-400/20',
- rails: 'bg-red-500/10 text-red-400 border-red-500/20',
- laravel: 'bg-orange-500/10 text-orange-400 border-orange-500/20',
- kafka: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
- graphql: 'bg-pink-500/10 text-pink-400 border-pink-500/20',
- grpc: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- websocket: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20',
- neo4j: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- postgres: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- mysql: 'bg-orange-500/10 text-orange-400 border-orange-500/20',
- redis: 'bg-red-500/10 text-red-400 border-red-500/20',
- mongodb: 'bg-green-500/10 text-green-400 border-green-500/20',
- docker: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- kubernetes: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- terraform: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
- aws: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
- gcp: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
- azure: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
-};
+import { useNavigate } from 'react-router-dom';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Puzzle } from 'lucide-react';
-const defaultColor = 'bg-surface-700/30 text-surface-300 border-surface-600/30';
+/** Maps lowercase framework name fragments → Badge variant */
+type BadgeVariant = 'default' | 'secondary' | 'outline' | 'muted' | 'success' | 'warning' | 'info' | 'purple';
+
+const frameworkVariants: Array<[string, BadgeVariant]> = [
+ ['spring', 'success'],
+ ['django', 'success'],
+ ['rails', 'warning'],
+ ['laravel', 'warning'],
+ ['nestjs', 'warning'],
+ ['express', 'warning'],
+ ['fastapi', 'info'],
+ ['flask', 'muted'],
+ ['react', 'info'],
+ ['angular', 'warning'],
+ ['vue', 'success'],
+ ['kafka', 'purple'],
+ ['graphql', 'purple'],
+ ['grpc', 'info'],
+ ['websocket', 'info'],
+ ['neo4j', 'info'],
+ ['postgres', 'info'],
+ ['mysql', 'warning'],
+ ['redis', 'warning'],
+ ['mongodb', 'success'],
+ ['docker', 'info'],
+ ['kubernetes', 'info'],
+ ['terraform', 'purple'],
+ ['aws', 'warning'],
+ ['gcp', 'info'],
+ ['azure', 'info'],
+];
+
+function getVariant(name: string): BadgeVariant {
+ const lower = name.toLowerCase();
+ return frameworkVariants.find(([k]) => lower.includes(k))?.[1] ?? 'muted';
+}
interface FrameworkBadgesProps {
- /** Map of framework name -> count of nodes using that framework */
frameworks: Record;
}
export default function FrameworkBadges({ frameworks }: FrameworkBadgesProps) {
+ const navigate = useNavigate();
const entries = Object.entries(frameworks || {});
if (entries.length === 0) return null;
- // Sort by count descending
const sorted = entries.sort(([, a], [, b]) => b - a);
return (
-
-
- Frameworks & Technologies
-
-
- {sorted.map(([fw, count]) => {
- const lower = fw.toLowerCase();
- const color = Object.entries(frameworkColors).find(([k]) => lower.includes(k))?.[1] || defaultColor;
- return (
-
+
+
+
+ Frameworks & Technologies
+
+
+
+
+ {sorted.map(([fw, count]) => (
+ navigate(`/explorer?q=${encodeURIComponent(fw)}`)}
+ role="listitem"
+ title={`${count.toLocaleString()} nodes`}
>
{fw}
- {count.toLocaleString()}
-
- );
- })}
-
-
+
{count.toLocaleString()}
+
+ ))}
+
+
+
);
}
diff --git a/src/main/frontend/src/components/Layout.tsx b/src/main/frontend/src/components/Layout.tsx
index 9912502d..c0efbd06 100644
--- a/src/main/frontend/src/components/Layout.tsx
+++ b/src/main/frontend/src/components/Layout.tsx
@@ -1,5 +1,10 @@
-import { useState } from 'react';
+import { useState, createContext, useContext } from 'react';
import { Outlet, NavLink, useLocation } from 'react-router-dom';
+import {
+ Group as PanelGroup,
+ Panel as ResizablePanel,
+ Separator as ResizeHandle,
+} from 'react-resizable-panels';
import {
LayoutDashboard,
FolderSearch,
@@ -9,114 +14,382 @@ import {
Menu,
X,
GitGraph,
+ ChevronLeft,
+ ChevronRight,
+ Sun,
+ Moon,
+ Monitor,
+ User,
} from 'lucide-react';
-import ThemeToggle from './ThemeToggle';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
import SearchBar from './SearchBar';
+import { useTheme } from '@/hooks/useTheme';
+import { FileSelectionProvider } from '@/contexts/FileSelectionContext';
+import ProjectFileTree from './ProjectFileTree';
+
+/* ------------------------------------------------------------------ */
+/* Right-panel context — lets child views open the details panel */
+/* ------------------------------------------------------------------ */
+interface RightPanelCtx {
+ openPanel: (content: React.ReactNode) => void;
+ closePanel: () => void;
+ isOpen: boolean;
+}
+export const RightPanelContext = createContext({
+ openPanel: () => {},
+ closePanel: () => {},
+ isOpen: false,
+});
+
+export function useRightPanel() {
+ return useContext(RightPanelContext);
+}
+
+/* ------------------------------------------------------------------ */
+/* Nav items */
+/* ------------------------------------------------------------------ */
const navItems = [
- { path: '/', label: 'Dashboard', icon: LayoutDashboard },
- { path: '/graph', label: 'Code Graph', icon: GitGraph },
- { path: '/explorer', label: 'Explorer', icon: FolderSearch },
- { path: '/console', label: 'Console', icon: Terminal },
- { path: '/api-docs', label: 'API Docs', icon: BookOpen },
+ { path: '/', label: 'Dashboard', icon: LayoutDashboard },
+ { path: '/graph', label: 'Code Graph', icon: GitGraph },
+ { path: '/explorer', label: 'Explorer', icon: FolderSearch },
+ { path: '/console', label: 'Console', icon: Terminal },
+ { path: '/api-docs', label: 'API Docs', icon: BookOpen },
];
-export default function Layout() {
- const [sidebarOpen, setSidebarOpen] = useState(false);
+/* ------------------------------------------------------------------ */
+/* Theme toggle (migrated to shadcn/ui styling) */
+/* ------------------------------------------------------------------ */
+function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+ const cycle = () => {
+ const next = theme === 'dark' ? 'light' : theme === 'light' ? 'system' : 'dark';
+ setTheme(next);
+ };
+ return (
+
+ {theme === 'dark' && }
+ {theme === 'light' && }
+ {theme === 'system' && }
+ Toggle theme
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Left Sidebar */
+/* ------------------------------------------------------------------ */
+interface SidebarProps {
+ collapsed: boolean;
+ mobileOpen: boolean;
+ onCollapse: () => void;
+ onMobileClose: () => void;
+}
+
+function Sidebar({ collapsed, mobileOpen, onCollapse, onMobileClose }: SidebarProps) {
const location = useLocation();
return (
-
- {/* Sidebar */}
+ <>
+ {/* Mobile overlay */}
+ {mobileOpen && (
+
+ )}
+
+ {/* Sidebar panel */}