diff --git a/api/graph.py b/api/graph.py index ffa208fb..3c666b10 100644 --- a/api/graph.py +++ b/api/graph.py @@ -131,9 +131,17 @@ def __init__(self, name: str, branch: Optional[str] = None) -> None: # Normalize empty / None to DEFAULT_BRANCH so the stored # branch matches the key actually used by compose_graph_name. self.branch = branch or DEFAULT_BRANCH - self.name = compose_graph_name(self.project, self.branch) + self.db = create_falkordb() + # Legacy (pre-T17) graphs are stored under the bare project + # name. Prefer that existing graph over composing a new, + # empty ``code:{project}:{branch}`` key out from under it. + if branch in (None, "", DEFAULT_BRANCH) and name in self.db.list_graphs(): + self.name = name + else: + self.name = compose_graph_name(self.project, self.branch) - self.db = create_falkordb() + if not hasattr(self, "db"): + self.db = create_falkordb() self.g = self.db.select_graph(self.name) # Initialize the backlog as disabled by default @@ -829,19 +837,48 @@ def __init__(self, name: str, branch: Optional[str] = None) -> None: if parsed is not None: self.project, self.branch = parsed self.name = name + self._needs_resolution = False else: self.project = name self.branch = branch or DEFAULT_BRANCH - self.name = compose_graph_name(self.project, self.branch) + if branch in (None, "", DEFAULT_BRANCH): + # Composition is deferred until ``graph_exists``/first query + # so we can prefer an existing legacy (pre-T17) bare graph + # over composing a new, empty ``code:{project}:_default`` + # key; see ``_resolve_name``. + self.name = name + self._needs_resolution = True + else: + # A specific branch was requested: compose immediately, + # same as before this bare-name fallback existed. + self.name = compose_graph_name(self.project, self.branch) + self._needs_resolution = False self.db = _async_db() self.g = self.db.select_graph(self.name) + async def _resolve_name(self, graphs: Optional[list[str]] = None) -> None: + """Resolve a bare project name to its actual FalkorDB graph key. + + Prefers an existing legacy graph (stored under the bare project + name) over composing a new ``code:{project}:{branch}`` key. + """ + if not getattr(self, "_needs_resolution", False): + return + self._needs_resolution = False + if graphs is None: + graphs = await self.db.list_graphs() + if self.name not in graphs: + self.name = compose_graph_name(self.project, self.branch) + self.g = self.db.select_graph(self.name) + async def graph_exists(self) -> bool: """Check if this graph exists, reusing the current connection.""" graphs = await self.db.list_graphs() + await self._resolve_name(graphs) return self.name in graphs async def _query(self, q: str, params: Optional[dict] = None): + await self._resolve_name() return await self.g.query(q, params) async def get_sub_graph(self, limit: int) -> dict: diff --git a/api/index.py b/api/index.py index e608456c..ff6dfeba 100644 --- a/api/index.py +++ b/api/index.py @@ -193,7 +193,9 @@ async def repo_info(data: RepoRequest, _=Depends(public_or_auth)): stats = await g.stats() finally: await g.close() - info = await async_get_repo_info(data.repo, data.branch) + # Use the parsed project/branch so full graph keys + # (``code:{project}:{branch}``) resolve the same as plain repo names. + info = await async_get_repo_info(g.project, g.branch) if info is None: return JSONResponse({"status": f'Missing repository "{data.repo}"'}, status_code=400) diff --git a/app/package-lock.json b/app/package-lock.json index 39022c62..b6c184cf 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.3.0", "dependencies": { - "@falkordb/canvas": "^0.0.44", + "@falkordb/canvas": "^0.2.5", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -27,10 +27,9 @@ "embla-carousel-react": "^8.5.2", "html2canvas": "^1.4.1", "lucide-react": "^0.486.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-gtm-module": "^2.0.11", - "react-json-tree": "^0.19.0", "react-resizable-panels": "^2.1.7", "react-syntax-highlighter": "^15.6.1", "react-type-animation": "^3.2.0", @@ -40,16 +39,16 @@ }, "devDependencies": { "@types/node": "^20.19.4", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "@types/react-gtm-module": "^2.0.4", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react-swc": "^3.10.2", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.21", "postcss": "^8.5.4", "tailwindcss": "^3.4.17", "typescript": "^5.7.3", - "vite": "^6.4.2" + "vite": "^8.1.5" } }, "node_modules/@alloc/quick-lru": { @@ -73,595 +72,577 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@falkordb/canvas": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.2.5.tgz", + "integrity": "sha512-BSVDHYC9WeuGF9iPUkGmh11KgyV3whVAjmLWDaai29GKXyJrG49LPiwxRAaKY2bA9ha7bU8ZWMJ77EqfUPXhmA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "d3": "^7.9.0", + "force-graph": "^1.44.4", + "react": "^19.2.3" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "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==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, + "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==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "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", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "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-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "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/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": 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", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.21.tgz", + "integrity": "sha512-h+7qMDDmZJ8qTSPrwNyKb/PACY0ehtN8QOBlCz+C2C1jgehKekdhmHddG9YQk8BF/sHJqglPjte+jA1Jrp9HcA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-context": "1.2.1", + "@radix-ui/react-dismissable-layer": "1.1.17", + "@radix-ui/react-focus-guards": "1.1.5", + "@radix-ui/react-focus-scope": "1.1.14", + "@radix-ui/react-id": "1.1.3", + "@radix-ui/react-portal": "1.1.15", + "@radix-ui/react-presence": "1.1.9", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-slot": "1.3.1", + "@radix-ui/react-use-controllable-state": "1.2.5", + "@radix-ui/react-use-layout-effect": "1.1.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", + "integrity": "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@falkordb/canvas": { - "version": "0.0.44", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.44.tgz", - "integrity": "sha512-ljVc6h+4kkTJw7cBSWth5soRjnV1LtURWa3x9DFXqbLXmVtItBy5EdnSvmhp6GCE43ZBmdVY8o12QDHxU2/vqQ==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.1.tgz", + "integrity": "sha512-EraVbFjiIjibpLr6EjvEDmSCYJU2SlKDMiO+qEK/D9GOWnQoAQlpQo2occGYC1UM9MBeEx5Bek3UtW/Qi57vAg==", "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "force-graph": "^1.44.4", - "react": "^19.2.3" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@falkordb/canvas/node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.5.tgz", + "integrity": "sha512-UQvlB7L/BYh3P8MLvwZnQkH521EDos40Rwnbt5+Qpg4Vbk0z3xJjRUmR6+aka4aT1IQQXFdO5bNPoE7cvFl5xQ==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.3.tgz", + "integrity": "sha512-f/Wxm0ctyMymUJK0fqTSQlm85rbzdAkoNbPXJQ5+6caowVO8Yx+NWGjGz/oGhs/D+WIbbQpOrU0hU2Li2/42xQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.9.tgz", + "integrity": "sha512-LTi1v05bprIb8/GSY/GWusI0jfsYjQ3CD3Nin8o7jVxnpHzVQfzjOQJoJTQkE9bdmOnsS7SFdhkXiBv8PrYnxw==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "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/@floating-ui/react-dom": { + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.8.tgz", + "integrity": "sha512-DOlK1BdcIeYYUcFkSYFka4v1h95XTov93b0jCgW1EEiZuIhdwHY2NlE1teLIh+p0uBsuZI5A+voay+iVWpprfA==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.6" + "@radix-ui/react-slot": "1.3.1" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@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/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "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==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.1.tgz", + "integrity": "sha512-Bu/aAQHFFh6/QAvXAeUMurJ9fbW0JUIqlojU/yBXZ7cAVqy75Y7JYYyuCr9zLNF0p4WWoJYV54CTUIf4l7FzTw==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "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==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.5.tgz", + "integrity": "sha512-UB1dXpxvHjR48poyKdKdTm7jT0kp3elkUKdKQiOkirlbYumqXinSJtrjDsr9maXNPvL12bKI4CDSmydms/9Aeg==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.4", + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.4.tgz", + "integrity": "sha512-XYcfa6wlXDCwQtePuEiPmXLSAhGL4DWtedSyRgGbG3y10mw+OnrLp6SyeY1gJFMiYF0Dx0nMAX9InylKbLEFQQ==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@radix-ui/react-use-layout-effect": "1.1.3" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.3.tgz", + "integrity": "sha512-rDiah9wvtqihWtWz02XreeRKIxt2EJF8y5D9rtY9l5A2zxePAtcPiOMpDugNRw5bFHz+1/8viVoc7ZVKiJknCw==", "license": "MIT", - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">= 8" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "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", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.17.tgz", + "integrity": "sha512-QAXwa38pG0xNAYh1pjdSaf86NrkqsMoDNmget/Y7X8O8E/C3Iqlj9GAPE4DfX9BPLXc7WH2TWSzMRnIoCdcjzQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-callback-ref": "1.1.3", + "@radix-ui/react-use-effect-event": "0.0.4" }, "peerDependencies": { "@types/react": "*", @@ -678,20 +659,34 @@ } } }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", + "integrity": "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.8.tgz", + "integrity": "sha512-DOlK1BdcIeYYUcFkSYFka4v1h95XTov93b0jCgW1EEiZuIhdwHY2NlE1teLIh+p0uBsuZI5A+voay+iVWpprfA==", "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-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-slot": "1.3.1" }, "peerDependencies": { "@types/react": "*", @@ -708,40 +703,30 @@ } } }, - "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", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.1.tgz", + "integrity": "sha512-Bu/aAQHFFh6/QAvXAeUMurJ9fbW0JUIqlojU/yBXZ7cAVqy75Y7JYYyuCr9zLNF0p4WWoJYV54CTUIf4l7FzTw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4" }, "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" + "react": "^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/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.3.tgz", + "integrity": "sha512-AUS7HoBBAncIsGMLNG+CcpLuJ+JIBbZzmyM8Qdb1eIThX0AlhSSC6wn40xfBlPE+ypx/vSSiRWnklUAjy3U3UA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -752,11 +737,14 @@ } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.4.tgz", + "integrity": "sha512-XYcfa6wlXDCwQtePuEiPmXLSAhGL4DWtedSyRgGbG3y10mw+OnrLp6SyeY1gJFMiYF0Dx0nMAX9InylKbLEFQQ==", "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.3" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -767,10 +755,10 @@ } } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.3.tgz", + "integrity": "sha512-rDiah9wvtqihWtWz02XreeRKIxt2EJF8y5D9rtY9l5A2zxePAtcPiOMpDugNRw5bFHz+1/8viVoc7ZVKiJknCw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -782,26 +770,19 @@ } } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "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-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -818,28 +799,56 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.14.tgz", + "integrity": "sha512-/x4htnJfmW53MplkrePaDpf1o/rN1C++g88WpVobULXbSyC19NtLkXmewuJ/HCaceSmfKDNL5gOXcBGnuAvnvQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-callback-ref": "1.1.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@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-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", + "integrity": "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -851,17 +860,13 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.8.tgz", + "integrity": "sha512-DOlK1BdcIeYYUcFkSYFka4v1h95XTov93b0jCgW1EEiZuIhdwHY2NlE1teLIh+p0uBsuZI5A+voay+iVWpprfA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/react-slot": "1.3.1" }, "peerDependencies": { "@types/react": "*", @@ -878,39 +883,29 @@ } } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.1.tgz", + "integrity": "sha512-Bu/aAQHFFh6/QAvXAeUMurJ9fbW0JUIqlojU/yBXZ7cAVqy75Y7JYYyuCr9zLNF0p4WWoJYV54CTUIf4l7FzTw==", "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-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4" }, "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" + "react": "^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-focus-guards": { + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.3.tgz", + "integrity": "sha512-AUS7HoBBAncIsGMLNG+CcpLuJ+JIBbZzmyM8Qdb1eIThX0AlhSSC6wn40xfBlPE+ypx/vSSiRWnklUAjy3U3UA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -922,15 +917,21 @@ } } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", "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-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -947,21 +948,41 @@ } } }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "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-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@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-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-hover-card/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1015,11 +1036,87 @@ "@radix-ui/react-portal": "1.1.9", "@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-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-menu/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1087,13 +1184,57 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.15.tgz", + "integrity": "sha512-kAfBVJUKNNKZuyGQXXG6rKolAV2KAmxxVkPXJgoq9dEFTl39286RufHQFNTL8rzha4vP8159BJ6hMGpB+bqv7A==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "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-portal/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", + "integrity": "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.8.tgz", + "integrity": "sha512-DOlK1BdcIeYYUcFkSYFka4v1h95XTov93b0jCgW1EEiZuIhdwHY2NlE1teLIh+p0uBsuZI5A+voay+iVWpprfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.1" }, "peerDependencies": { "@types/react": "*", @@ -1110,6 +1251,40 @@ } } }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.1.tgz", + "integrity": "sha512-Bu/aAQHFFh6/QAvXAeUMurJ9fbW0JUIqlojU/yBXZ7cAVqy75Y7JYYyuCr9zLNF0p4WWoJYV54CTUIf4l7FzTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.3.tgz", + "integrity": "sha512-rDiah9wvtqihWtWz02XreeRKIxt2EJF8y5D9rtY9l5A2zxePAtcPiOMpDugNRw5bFHz+1/8viVoc7ZVKiJknCw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -1311,6 +1486,82 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@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-select/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1399,23 +1650,131 @@ } } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@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-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "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-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@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-toast/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", - "@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-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { @@ -1433,13 +1792,17 @@ } } }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1456,24 +1819,14 @@ } } }, - "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", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "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-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1719,31 +2072,10 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -1752,12 +2084,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -1766,12 +2101,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -1780,26 +2118,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -1808,26 +2135,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -1836,26 +2152,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], @@ -1864,54 +2169,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], @@ -1920,40 +2203,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], @@ -1962,12 +2220,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], @@ -1976,12 +2237,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], @@ -1990,26 +2254,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -2018,40 +2271,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -2060,32 +2324,28 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", - "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.46.tgz", + "integrity": "sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" + "@swc/types": "^0.1.27" }, "engines": { "node": ">=10" @@ -2095,16 +2355,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.18", - "@swc/core-darwin-x64": "1.15.18", - "@swc/core-linux-arm-gnueabihf": "1.15.18", - "@swc/core-linux-arm64-gnu": "1.15.18", - "@swc/core-linux-arm64-musl": "1.15.18", - "@swc/core-linux-x64-gnu": "1.15.18", - "@swc/core-linux-x64-musl": "1.15.18", - "@swc/core-win32-arm64-msvc": "1.15.18", - "@swc/core-win32-ia32-msvc": "1.15.18", - "@swc/core-win32-x64-msvc": "1.15.18" + "@swc/core-darwin-arm64": "1.15.46", + "@swc/core-darwin-x64": "1.15.46", + "@swc/core-linux-arm-gnueabihf": "1.15.46", + "@swc/core-linux-arm64-gnu": "1.15.46", + "@swc/core-linux-arm64-musl": "1.15.46", + "@swc/core-linux-ppc64-gnu": "1.15.46", + "@swc/core-linux-s390x-gnu": "1.15.46", + "@swc/core-linux-x64-gnu": "1.15.46", + "@swc/core-linux-x64-musl": "1.15.46", + "@swc/core-win32-arm64-msvc": "1.15.46", + "@swc/core-win32-ia32-msvc": "1.15.46", + "@swc/core-win32-x64-msvc": "1.15.46" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -2116,9 +2378,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", - "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz", + "integrity": "sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==", "cpu": [ "arm64" ], @@ -2133,9 +2395,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", - "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz", + "integrity": "sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==", "cpu": [ "x64" ], @@ -2150,9 +2412,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", - "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz", + "integrity": "sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==", "cpu": [ "arm" ], @@ -2167,9 +2429,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", - "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz", + "integrity": "sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==", "cpu": [ "arm64" ], @@ -2184,9 +2446,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", - "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz", + "integrity": "sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==", "cpu": [ "arm64" ], @@ -2200,10 +2462,44 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz", + "integrity": "sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz", + "integrity": "sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", - "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz", + "integrity": "sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==", "cpu": [ "x64" ], @@ -2218,9 +2514,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", - "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz", + "integrity": "sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==", "cpu": [ "x64" ], @@ -2235,9 +2531,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", - "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz", + "integrity": "sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==", "cpu": [ "arm64" ], @@ -2252,9 +2548,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", - "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz", + "integrity": "sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==", "cpu": [ "ia32" ], @@ -2269,9 +2565,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", - "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz", + "integrity": "sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==", "cpu": [ "x64" ], @@ -2293,9 +2589,9 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2308,12 +2604,16 @@ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@types/hast": { "version": "2.3.10", @@ -2324,12 +2624,6 @@ "@types/unist": "^2" } }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", @@ -2340,30 +2634,24 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", - "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/react-gtm-module": { @@ -2390,17 +2678,20 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", - "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.2.tgz", + "integrity": "sha512-MlSlmSAYbYwPjGa+CjOAqwij09iPYAQDHwx8osVkwoDamCxXlg+avpkC65Ysv+L/uqUWvRTsnJCKPjVhZih/sA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" + "@rolldown/pluginutils": "^1.0.1", + "@swc/core": "^1.15.46" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/accessor-fn": { @@ -2694,62 +2985,21 @@ "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/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "color-name": "~1.1.4" + "clsx": "^2.1.1" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://polar.sh/cva" } }, - "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==", + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "engines": { + "node": ">=6" } }, "node_modules/comma-separated-tokens": { @@ -2796,6 +3046,7 @@ "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": { @@ -3236,6 +3487,16 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -3289,48 +3550,6 @@ "embla-carousel": "8.6.0" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3418,9 +3637,9 @@ } }, "node_modules/force-graph": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.1.tgz", - "integrity": "sha512-uEEX8iRzgq1IKRISOw6RrB2RLMhcI25xznQYrCTVvxZHZZ+A2jH6qIolYuwavVxAMi64pFp2yZm4KFVdD993cg==", + "version": "1.51.4", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz", + "integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==", "license": "MIT", "dependencies": { "@tweenjs/tween.js": "18 - 25", @@ -3630,12 +3849,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "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", @@ -3726,7 +3939,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/kapsule": { "version": "1.16.3", @@ -3740,6 +3954,267 @@ "node": ">=12" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3769,6 +4244,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", + "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -3833,9 +4309,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -3945,9 +4421,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "funding": [ { "type": "opencollective", @@ -3964,7 +4440,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4101,13 +4577,21 @@ "license": "MIT" }, "node_modules/preact": { - "version": "10.28.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", - "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "version": "10.29.7", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz", + "integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } } }, "node_modules/prismjs": { @@ -4165,40 +4649,24 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-base16-styling": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.10.0.tgz", - "integrity": "sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.17.0", - "color": "^4.2.3", - "csstype": "^3.1.3", - "lodash-es": "^4.17.21" - } - }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.8" } }, "node_modules/react-gtm-module": { @@ -4214,20 +4682,6 @@ "license": "MIT", "peer": true }, - "node_modules/react-json-tree": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.19.0.tgz", - "integrity": "sha512-PqT1WRVcWP+RROsZPQfNEKIC1iM/ZMfY4g5jN6oDnXp5593PPRAYgoHcgYCDjflAHQMtxl8XGdlTwIBdEGUXvw==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.17.0", - "react-base16-styling": "^0.10.0" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -4407,49 +4861,38 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/run-parallel": { @@ -4488,22 +4931,10 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "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" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/source-map-js": { "version": "1.2.1", @@ -4660,13 +5091,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -4852,24 +5283,23 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4878,14 +5308,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4894,13 +5325,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -4926,28 +5360,10 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "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" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { diff --git a/app/package.json b/app/package.json index 5b38394f..42c2630c 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "@falkordb/canvas": "^0.0.44", + "@falkordb/canvas": "^0.2.5", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -30,10 +30,9 @@ "embla-carousel-react": "^8.5.2", "html2canvas": "^1.4.1", "lucide-react": "^0.486.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-gtm-module": "^2.0.11", - "react-json-tree": "^0.19.0", "react-resizable-panels": "^2.1.7", "react-syntax-highlighter": "^15.6.1", "react-type-animation": "^3.2.0", @@ -47,15 +46,15 @@ }, "devDependencies": { "@types/node": "^20.19.4", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "@types/react-gtm-module": "^2.0.4", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react-swc": "^3.10.2", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.21", "postcss": "^8.5.4", "tailwindcss": "^3.4.17", "typescript": "^5.7.3", - "vite": "^6.4.2" + "vite": "^8.1.5" } } diff --git a/app/src/App.tsx b/app/src/App.tsx index 156b1313..99ae5b70 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -12,9 +12,10 @@ import { Carousel, CarouselApi, CarouselContent, CarouselItem, CarouselNext, Car import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'; import Input from './components/Input'; import { Labels } from './components/labels'; -import { Toolbar } from './components/toolbar'; -import { cn, GraphRef, Message, Path, PathData, PathNode } from '@/lib/utils'; +import { Toolbar, ZoomControls } from './components/toolbar'; +import { cn, DEFAULT_BRANCH, GraphRef, Message, Path, PathData, PathNode, RepoOption } from '@/lib/utils'; import type { GraphNode } from '@falkordb/canvas'; +import { convertToCanvasData } from './components/ForceGraph'; import { Toaster } from '@/components/ui/toaster'; import GTM from './GTM'; import { Button } from '@/components/ui/button'; @@ -71,7 +72,7 @@ export default function App() { const [createURL, setCreateURL] = useState("") const [createOpen, setCreateOpen] = useState(false) const [tipOpen, setTipOpen] = useState(false) - const [options, setOptions] = useState([]); + const [options, setOptions] = useState([]); const [path, setPath] = useState(); const [isSubmit, setIsSubmit] = useState(false); const desktopChartRef = useRef(null) @@ -79,7 +80,8 @@ export default function App() { const [menuOpen, setMenuOpen] = useState(false) const [chatOpen, setChatOpen] = useState(false) const [searchNode, setSearchNode] = useState({}); - const [cooldownTicks, setCooldownTicks] = useState(0) + const [animation, setAnimation] = useState(false) + const [manualDimmed, setManualDimmed] = useState(true) const [optionsOpen, setOptionsOpen] = useState(false) const [messages, setMessages] = useState([]); const [query, setQuery] = useState(''); @@ -140,7 +142,7 @@ export default function App() { const graphName = createURL.split('/').pop()! - setOptions(prev => [...prev, graphName]) + setOptions(prev => [...prev, { project: graphName, branch: DEFAULT_BRANCH, graph: graphName }]) setSelectedValue(graphName) setCreateURL("") setCreateOpen(false) @@ -174,8 +176,6 @@ export default function App() { const g = Graph.create(json.entities, graphName) setGraph(g) - if (cooldownTicks === 0) setCooldownTicks(-1) - setIsPathResponse(false) chatPanel.current?.expand() // @ts-ignore @@ -225,34 +225,10 @@ export default function App() { if (!chartNode) { chartNode = graph.extend({ nodes: [node], edges: [] }).nodes[0] - if (cooldownTicks === 0) setCooldownTicks(-1) - setZoomedNodes([chartNode]) graph.visibleLinks(true, [chartNode!.id]) - const currentData = canvas.getGraphData() - - const { dataToGraphData } = await import('@falkordb/canvas') - const graphNode = dataToGraphData({ - nodes: [{ - color: chartNode.color, - id: chartNode.id, - labels: [chartNode.category], - visible: chartNode.visible, - data: { - ...chartNode.data, - isPath: chartNode.isPath, - isPathSelected: chartNode.isPathSelected, - } - }], links: [] - }).nodes[0] - - if (graphNode) { - currentData.nodes.push(graphNode) - } - - canvas.setGraphData(currentData) - + canvas.setGraphData(convertToCanvasData(graph.Elements)) setTimeout(() => { canvas.zoomToFit(4, (n: GraphNode) => n.id === chartNode!.id) @@ -265,22 +241,7 @@ export default function App() { chartNode.visible = true graph.visibleLinks(true, [chartNode!.id]) - const currentData = canvas.getGraphData() - - const graphNode = currentData.nodes.find(n => n.id === chartNode!.id) - if (graphNode) { - graphNode.visible = true - } - - currentData.links.forEach(canvasLink => { - const appLink = graph.LinksMap.get(canvasLink.id) - - if (appLink) { - canvasLink.visible = appLink.visible - } - }) - - canvas.setGraphData(currentData) + canvas.setGraphData(convertToCanvasData(graph.Elements)) } setTimeout(() => { @@ -306,27 +267,18 @@ export default function App() { graph.visibleLinks(show) - const currentData = canvas.getGraphData(); - - currentData.nodes.forEach(canvasNode => { - const appNode = graph.NodesMap.get(canvasNode.id); - - if (appNode) { - canvasNode.visible = appNode.visible; - } - }); - - currentData.links.forEach(canvasLink => { - const appLink = graph.LinksMap.get(canvasLink.id); - - if (appLink) { - canvasLink.visible = appLink.visible; - } - }); - - canvas.setGraphData(currentData); + // setGraphData doesn't update visible for existing nodes — mutate canvas nodes directly + const canvasData = canvas.getGraphData() + canvasData.nodes.forEach((canvasNode: { id: number, visible: boolean }) => { + const appNode = graph.NodesMap.get(canvasNode.id) + if (appNode) canvasNode.visible = appNode.visible + }) + canvasData.links.forEach((canvasLink: { id: number, visible: boolean }) => { + const appLink = graph.LinksMap.get(canvasLink.id) + if (appLink) canvasLink.visible = appLink.visible + }) + canvas.refresh() - setCooldownTicks(cooldownTicks === undefined ? undefined : -1); setHasHiddenElements(graph.getElements().some(element => !element.visible)); } @@ -538,8 +490,10 @@ export default function App() { handleSearchSubmit={(node) => handleSearchSubmit(node, desktopChartRef)} searchNode={searchNode} setSearchNode={setSearchNode} - cooldownTicks={cooldownTicks} - setCooldownTicks={setCooldownTicks} + animation={animation} + setAnimation={setAnimation} + manualDimmed={manualDimmed} + setManualDimmed={setManualDimmed} onCategoryClick={(name, show) => onCategoryClick(name, show, desktopChartRef)} handleDownloadImage={handleDownloadImage} zoomedNodes={zoomedNodes} @@ -574,7 +528,6 @@ export default function App() { setIsPathResponse={setIsPathResponse} paths={paths} setPaths={setPaths} - setCooldownTicks={setCooldownTicks} /> @@ -665,8 +618,10 @@ export default function App() { handleSearchSubmit={(node) => handleSearchSubmit(node, mobileChartRef)} setSearchNode={setSearchNode} searchNode={searchNode} - cooldownTicks={cooldownTicks} - setCooldownTicks={setCooldownTicks} + animation={animation} + setAnimation={setAnimation} + manualDimmed={manualDimmed} + setManualDimmed={setManualDimmed} onCategoryClick={(name, show) => onCategoryClick(name, show, mobileChartRef)} handleDownloadImage={handleDownloadImage} zoomedNodes={zoomedNodes} @@ -705,7 +660,6 @@ export default function App() { setChatOpen={setChatOpen} paths={paths} setPaths={setPaths} - setCooldownTicks={setCooldownTicks} /> @@ -720,11 +674,10 @@ export default function App() { - onCategoryClick(name, show, mobileChartRef)} /> -
- -

Take Screenshot

-
+ {/* Options controls inside the drawer, below search */} + diff --git a/app/src/components/ForceGraph.tsx b/app/src/components/ForceGraph.tsx index e7a52e54..93c09c10 100644 --- a/app/src/components/ForceGraph.tsx +++ b/app/src/components/ForceGraph.tsx @@ -1,11 +1,12 @@ -import { useCallback, useEffect, useState } from "react" +import { createElement, useCallback, useEffect, useRef, useState } from "react" import type { Data, GraphLink, GraphNode } from "@falkordb/canvas" import { GraphRef, PATH_COLOR } from "@/lib/utils" import { GraphData, Link, Node } from "./model" interface Props { id: "desktop" | "mobile" + graphId: string data: GraphData canvasRef: GraphRef onNodeClick: (node: Node, event: MouseEvent) => void @@ -16,30 +17,34 @@ interface Props { onLinkHover: (link: Link | null) => void onLinkRightClick: (link: Link, event: MouseEvent) => void isLinkSelected: (link: GraphLink) => boolean + isNodeDimmed: (node: GraphNode) => boolean + isLinkDimmed: (link: GraphLink) => boolean + dimmed: boolean + linkLineDash: (link: any) => number[] onBackgroundClick: (event: MouseEvent) => void onBackgroundRightClick: (event: MouseEvent) => void - nodeCanvasObject: (node: GraphNode, ctx: CanvasRenderingContext2D) => void - nodePointerAreaPaint: (node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => void - linkLineDash: (link: any) => number[] onZoom: () => void onEngineStop: () => void - cooldownTicks: number | undefined + onNodeDragEnd?: () => void backgroundColor?: string foregroundColor?: string } -const convertToCanvasData = (graphData: GraphData): Data => ({ - nodes: graphData.nodes.filter(n => n.visible).map(({ id, category, color, visible, isPath, isPathSelected, data }) => ({ + +export const convertToCanvasData = (graphData: GraphData): Data => ({ + nodes: graphData.nodes.map(({ id, category, color, visible, expand, isPath, isPathSelected, data }) => ({ id, labels: [category], color, + borderColor: (isPath || isPathSelected) ? PATH_COLOR : undefined, visible, + expand, data: { ...data, isPath, isPathSelected } })), - links: graphData.links.filter(l => l.visible).map(({ id, label, color, visible, source, target, isPath, isPathSelected, data }) => ({ + links: graphData.links.map(({ id, label, color, visible, source, target, isPath, isPathSelected, data }) => ({ id, relationship: label, - color: isPath ? PATH_COLOR : color, + color: (isPath || isPathSelected) ? PATH_COLOR : color, visible, source, target, @@ -49,6 +54,7 @@ const convertToCanvasData = (graphData: GraphData): Data => ({ export default function ForceGraph({ id, + graphId, data, canvasRef, onNodeClick, @@ -59,14 +65,15 @@ export default function ForceGraph({ onLinkHover, onLinkRightClick, isLinkSelected, + isNodeDimmed, + isLinkDimmed, + dimmed, + linkLineDash, onBackgroundClick, onBackgroundRightClick, onZoom, onEngineStop, - nodeCanvasObject, - nodePointerAreaPaint, - linkLineDash, - cooldownTicks, + onNodeDragEnd, backgroundColor = "#FFFFFF", foregroundColor = "#000000" }: Props) { @@ -82,10 +89,10 @@ export default function ForceGraph({ useEffect(() => { const canvas = canvasRef.current - if (!canvas) return + if (!canvas || !canvasLoaded) return (window as any)[id === "desktop" ? "graphDesktop" : "graphMobile"] = () => canvas.getGraphData(); - }, [canvasRef, id]) + }, [canvasRef, id, canvasLoaded]) // Update canvas colors useEffect(() => { @@ -94,13 +101,6 @@ export default function ForceGraph({ canvasRef.current.setForegroundColor(foregroundColor) }, [canvasRef, backgroundColor, foregroundColor, canvasLoaded]) - // Update cooldown ticks - useEffect(() => { - if (!canvasRef.current || !canvasLoaded) return - - canvasRef.current.setCooldownTicks(cooldownTicks === -1 ? undefined : cooldownTicks) - }, [canvasRef, cooldownTicks, canvasLoaded]) - // Map node click handler const handleNodeClick = useCallback((node: GraphNode, event: MouseEvent) => { const originalNode = data.nodes.find(n => n.id === node.id) @@ -113,9 +113,9 @@ export default function ForceGraph({ onNodeHover(null) return } - + const originalNode = data.nodes.find(n => n.id === node.id) - + if (originalNode) onNodeHover(originalNode) }, [onNodeHover, data.nodes]) @@ -137,9 +137,9 @@ export default function ForceGraph({ onLinkHover(null) return } - + const originalLink = data.links.find(l => l.id === link.id) - + if (originalLink) onLinkHover(originalLink) }, [onLinkHover, data.links]) @@ -158,23 +158,25 @@ export default function ForceGraph({ useEffect(() => { if (!canvasRef.current || !canvasLoaded) return canvasRef.current.setConfig({ - autoStopOnSettle: false, - // nodes will display node.data.captionsKeys in the canvas - captionsKeys: ["name", "title"], - onNodeClick: handleNodeClick, - onNodeRightClick: handleNodeRightClick, - onNodeHover: handleNodeHover, + captionsKeys: [["name", true], ["title", true]], isNodeSelected: isNodeSelected, - onLinkClick: handleLinkClick, - onLinkRightClick: handleLinkRightClick, - onLinkHover: handleLinkHover, isLinkSelected: isLinkSelected, - onBackgroundClick, - onBackgroundRightClick, - onEngineStop: handleEngineStop, - node: { nodeCanvasObject, nodePointerAreaPaint }, + isNodeDimmed: isNodeDimmed, + isLinkDimmed: isLinkDimmed, linkLineDash, - onZoom + eventHandlers: { + onNodeClick: handleNodeClick, + onNodeRightClick: handleNodeRightClick, + onNodeHover: handleNodeHover, + onLinkClick: handleLinkClick, + onLinkRightClick: handleLinkRightClick, + onLinkHover: handleLinkHover, + onBackgroundClick, + onBackgroundRightClick, + onEngineStop: handleEngineStop, + onNodeDragEnd, + onZoom + } }) }, [ handleNodeClick, @@ -185,27 +187,43 @@ export default function ForceGraph({ handleLinkHover, isNodeSelected, isLinkSelected, + isNodeDimmed, + isLinkDimmed, onBackgroundClick, onBackgroundRightClick, handleEngineStop, - nodeCanvasObject, - nodePointerAreaPaint, + onNodeDragEnd, linkLineDash, onZoom, canvasRef, canvasLoaded ]) + // Sync dimmed state to canvas (like browser: separate effect, not via setConfig) + useEffect(() => { + if (!canvasRef.current || !canvasLoaded) return + canvasRef.current.setDimmed(dimmed) + }, [dimmed, canvasRef, canvasLoaded]) + + // Track the last graphId that triggered a full setData + const lastGraphIdRef = useRef(undefined) + // Update canvas data useEffect(() => { const canvas = canvasRef.current if (!canvas || !canvasLoaded) return const canvasData = convertToCanvasData(data) - canvas.setData(canvasData) - }, [canvasRef, data, canvasLoaded]) - return ( - - ) + if (graphId !== lastGraphIdRef.current) { + // New graph identity: full replacement (resets positions, runs simulation) + lastGraphIdRef.current = graphId + canvas.setData(canvasData) + } else { + // Same graph: incremental update (preserves node positions) + canvas.setGraphData(canvasData) + } + }, [canvasRef, graphId, data, canvasLoaded]) + + return createElement("falkordb-canvas", { ref: canvasRef, "node-mode": "replace" }) } diff --git a/app/src/components/chat.tsx b/app/src/components/chat.tsx index 0ecc49c2..b1060dd1 100644 --- a/app/src/components/chat.tsx +++ b/app/src/components/chat.tsx @@ -1,7 +1,7 @@ import { toast } from "@/components/ui/use-toast"; import { Dispatch, FormEvent, SetStateAction, useEffect, useRef, useState } from "react"; import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Loader2, Undo2 } from "lucide-react"; -import { Message, MessageTypes, Path, PathData, PATH_COLOR } from "@/lib/utils"; +import { Message, MessageTypes, Path, PathData, PATH_COLOR, createMessage } from "@/lib/utils"; import Input from "./Input"; import { Graph, GraphData, Node } from "./model"; import { cn, GraphRef } from "@/lib/utils"; @@ -10,9 +10,10 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/compon import { Button } from "@/components/ui/button"; const AUTH_HEADERS: HeadersInit = import.meta.env.VITE_SECRET_TOKEN - ? { 'Authorization': `Bearer ${import.meta.env.VITE_SECRET_TOKEN}` } - : {}; -import { dataToGraphData, GraphLink, GraphNode } from "@falkordb/canvas"; + ? { 'Authorization': `Bearer ${import.meta.env.VITE_SECRET_TOKEN}` } + : {}; +import { GraphNode } from "@falkordb/canvas"; +import { convertToCanvasData } from "./ForceGraph"; interface Props { repo: string @@ -29,7 +30,6 @@ interface Props { setQuery: Dispatch> selectedPath: PathData | undefined setSelectedPath: Dispatch> - setCooldownTicks: Dispatch> setChatOpen?: Dispatch> paths: PathData[] setPaths: Dispatch> @@ -48,34 +48,35 @@ type RemoveLastPathResult = { insertIndex: number } -const RemoveLastPath = (messages: Message[]): RemoveLastPathResult => { - // Find the last Path message so we know where the user was in the conversation +const RemoveLastPath = (messages: Message[], includeQuery = false): RemoveLastPathResult => { + // Find the last Path marker (the pending "select start/end" state) const index = messages.findLastIndex((m) => m.type === MessageTypes.Path) if (index === -1) { return { messages, insertIndex: messages.length } } - const groupStart = Math.max(0, index - 2) - const hasMessagesAfter = index < messages.length - 1 - const cleaned = [...messages.slice(0, groupStart), ...messages.slice(index + 1)] - - // Recurse to strip any remaining Path groups - const { messages: finalMessages } = RemoveLastPath(cleaned) + // The Path marker is always preceded by Response("Please select..."). + // Optionally also remove the Query("Create a path") before it. + let groupStart = index; + if (index > 0 && messages[index - 1].type === MessageTypes.Response) { + groupStart = index - 1; + if (includeQuery && groupStart > 0 && messages[groupStart - 1].type === MessageTypes.Query) { + groupStart = groupStart - 1; + } + } - // If there were messages after the Path group, inject the answer there; - // otherwise just append at the end - const insertIndex = hasMessagesAfter ? groupStart : finalMessages.length + const cleaned = [...messages.slice(0, groupStart), ...messages.slice(index + 1)] - return { messages: finalMessages, insertIndex } + return { messages: cleaned, insertIndex: groupStart } } -export function Chat({ messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setCooldownTicks, canvasRef, paths, setPaths }: Props) { +export function Chat({ messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, canvasRef, paths, setPaths }: Props) { const [sugOpen, setSugOpen] = useState(false); // A reference to the chat container to allow scrolling to the bottom - const containerRef: React.RefObject = useRef(null); + const containerRef = useRef(null); const isSendMessage = messages.some(m => m.type === MessageTypes.Pending) || (messages.some(m => m.text === "Please select a starting point and the end point. Select or press relevant item on the graph") && !messages.some(m => m.type === MessageTypes.Path)) @@ -109,12 +110,54 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set setPaths([]) }, [isPathResponse]) + // Update selected path highlighting when selectedPathId changes + useEffect(() => { + if (!selectedPathId || !isPathResponse || paths.length === 0) return + + const canvas = canvasRef.current + if (!canvas) return + + // Find which path contains the selected link + const selectedPath = paths.find(p => p.links.some(l => l.id === selectedPathId)) + if (!selectedPath) return + + // Clear all path selections first + graph.Elements.nodes.forEach((node: any) => { + if (node.isPathSelected) node.isPathSelected = false + }) + graph.Elements.links.forEach((link: any) => { + if (link.isPathSelected) link.isPathSelected = false + }) + + // Mark selected path elements + selectedPath.nodes.forEach(n => { + const node = graph.Elements.nodes.find(gn => gn.id === n.id) + if (node) node.isPathSelected = true + }) + selectedPath.links.forEach(l => { + const link = graph.Elements.links.find(gl => gl.id === l.id) + if (link) link.isPathSelected = true + }) + + canvas.setGraphData(convertToCanvasData(graph.Elements)) + + // Zoom to fit selected path nodes + const selectedNodeIds = new Set(selectedPath.nodes.map((n: Node) => n.id)) + const timeout = setTimeout(() => { + canvas.zoomToFit(1, (n: GraphNode) => selectedNodeIds.has(n.id)) + }, 100) + + return () => { + clearTimeout(timeout) + } + }, [selectedPathId, isPathResponse, paths, graph, canvasRef]) + const handleSetSelectedPath = (p: PathData) => { const canvas = canvasRef.current if (!canvas) return - // Sets for the new path + setIsPathResponse(true) const pNodeIds = new Set(p.nodes.map((n: Node) => n.id)) const pLinkIds = new Set(p.links.map((l: any) => l.id)) const pIds = new Set([...pNodeIds, ...pLinkIds]) @@ -180,56 +223,11 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set }) } - const currentData = canvas.getGraphData(); - - // --- Unset canvas data for previous path elements no longer in the new path --- - if (selectedPath) { - currentData.nodes.forEach((n: any) => { - if (prevNodeIds.has(n.id) && !pNodeIds.has(n.id)) { - if (isPathResponse) { - // keep isPath + PATH_COLOR border, just deselect - n.data.isPathSelected = false - } else { - n.data.isPath = false - n.data.isPathSelected = false - } - } - }) - currentData.links.forEach((l: any) => { - if (prevLinkIds.has(l.id) && !pLinkIds.has(l.id)) { - if (isPathResponse) { - // keep color + dashed path line, just deselect - l.data.isPathSelected = false - } else { - l.data.isPathSelected = false - l.color = "#999999" - } - } - }) - } - - // --- Set canvas data for new path elements --- - currentData.nodes.forEach((n: any) => { - if (pNodeIds.has(n.id)) { - if (n.id === firstNodeId || n.id === lastNodeId) { - n.data.isPathSelected = true; - } else { - n.data.isPath = true; - } - } - }); - currentData.links.forEach((l: any) => { - if (pLinkIds.has(l.id)) { - l.data.isPathSelected = true; - l.color = PATH_COLOR; - } - }); - - canvas.setGraphData(currentData) + canvas.setGraphData(convertToCanvasData(graph.Elements)) setTimeout(() => { - canvas.zoomToFit(2, (n: GraphNode) => pNodeIds.has(n.id)); - }, 0) + canvas.zoomToFit(1, (n: GraphNode) => pNodeIds.has(n.id)); + }, 300) setChatOpen && setChatOpen(false) } @@ -263,7 +261,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set setQuery("") - setMessages((messages) => [...messages, { text: q, type: MessageTypes.Query }, { type: MessageTypes.Pending }]); + setMessages((messages) => [...messages, createMessage({ text: q, type: MessageTypes.Query }), createMessage({ type: MessageTypes.Pending })]); const result = await fetch(`/api/chat`, { method: 'POST', @@ -277,7 +275,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set if (!result.ok) { setMessages((prev) => { prev = [...prev.slice(0, -1)]; - return [...prev, { type: MessageTypes.Response, text: "Sorry but I couldn't answer your question, please try rephrasing." }]; + return [...prev, createMessage({ type: MessageTypes.Response, text: "Sorry but I couldn't answer your question, please try rephrasing." })]; }); return } @@ -286,7 +284,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set setMessages((prev) => { prev = prev.slice(0, -1); - return [...prev, { text: json.response, type: MessageTypes.Response }]; + return [...prev, createMessage({ text: json.response, type: MessageTypes.Response })]; }); } @@ -300,17 +298,17 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set if (!path?.start?.id || !path.end?.id) return - const pathMessage = [{ + const pathMessage = [createMessage({ type: MessageTypes.Response, text: "Please select a starting point and the end point. Select or press relevant item on the graph" - }, { type: MessageTypes.Path }] + }), createMessage({ type: MessageTypes.Path })] setPath(undefined) let insertIndex = 0 setMessages((prev) => { const { messages, insertIndex: idx } = RemoveLastPath(prev) insertIndex = idx - const pending: Message = { type: MessageTypes.Pending } + const pending: Message = createMessage({ type: MessageTypes.Pending }) return [...messages.slice(0, insertIndex), pending, ...messages.slice(insertIndex)] }) @@ -367,82 +365,46 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set setPaths(formattedPaths) setMessages((prev) => [ ...prev.slice(0, insertIndex), - { type: MessageTypes.PathResponse, paths: formattedPaths, graphName: graph.Id }, + createMessage({ type: MessageTypes.PathResponse, paths: formattedPaths, graphName: graph.Id }), ...prev.slice(insertIndex + 1), ]); setIsPathResponse(true) - const currentData = canvas.getGraphData(); - - const nodesMap = new Map(currentData.nodes.map(n => [n.id, n])) - const linksMap = new Map(currentData.links.map(l => [l.id, l])) + // Clear previous path markings before applying new ones + graph.Elements.nodes.forEach((node: any) => { + node.isPath = false; + node.isPathSelected = false; + }); + graph.Elements.links.forEach((link: any) => { + link.isPath = false; + link.isPathSelected = false; + link.color = "#999999"; + }); + // Mark path elements on the model formattedPaths.flatMap(p => p.nodes).forEach(n => { - const node = nodesMap.get(n.id); + const node = graph.Elements.nodes.find(gn => gn.id === n.id); if (node) { - node.data.isPath = true; + node.isPath = true; } }); formattedPaths.flatMap(p => p.links).forEach(l => { - const link = linksMap.get(l.id); - + const link = graph.Elements.links.find(gl => gl.id === l.id); if (link) { - link.data.isPath = true; + link.isPath = true; link.color = PATH_COLOR; } }); - // Filter for only new elements - const newDataElements = { - nodes: elements.nodes.filter(n => !nodesMap.has(n.id)) - .map(({ category, color, data, id, isPath, isPathSelected, visible }) => ({ - color, - id, - labels: [category], - data: { - ...data, - isPath, - isPathSelected - }, - visible, - })), - links: elements.links.filter(l => !linksMap.has(l.id)) - .map(({ color, id, source, target, data, isPath, isPathSelected, visible, label }) => ({ - color: isPath ? PATH_COLOR : color, - id, - source, - target, - data: { - ...data, - isPath, - isPathSelected - }, - visible, - relationship: label, - })) - } - - // Convert only new data to GraphData format - const newGraphData = dataToGraphData( - newDataElements, - undefined, - new Map(currentData.nodes.map(n => [n.id, n])) - ) - - // Merge with existing data - canvasRef.current?.setGraphData({ - nodes: [...currentData.nodes, ...newGraphData.nodes], - links: [...currentData.links, ...newGraphData.links] - }) - - if (elements.nodes.length !== 0 || elements.links.length !== 0) { - setCooldownTicks(-1) - } + // Update the canvas from the model + const pathCanvas = canvasRef.current + pathCanvas?.setGraphData(convertToCanvasData(graph.Elements)) + // Zoom to fit all path nodes (same as selected path zoom but for all paths) + const allPathNodeIds = new Set(formattedPaths.flatMap(p => p.nodes).map((n: Node) => n.id)) setTimeout(() => { - const nodesMap = new Map(formattedPaths.flatMap(p => p.nodes.map((n: Node) => [n.id, n]))) - canvas.zoomToFit(2, (n: GraphNode) => formattedPaths.some(p => nodesMap.has(n.id))); - }, 0) + pathCanvas?.zoomToFit(1, (n: GraphNode) => allPathNodeIds.has(n.id)) + }, 100) } const getTip = (className?: string) => @@ -457,9 +419,9 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set setSugOpen(false) setMessages(prev => { - const { messages, insertIndex } = RemoveLastPath(prev) - const queryMsg: Message = { type: MessageTypes.Query, text: "Create a path" } - return [...messages.slice(0, insertIndex), queryMsg, ...messages.slice(insertIndex)] + const { messages } = RemoveLastPath(prev, true) + const queryMsg: Message = createMessage({ type: MessageTypes.Query, text: "Create a path" }) + return [...messages, queryMsg] }) if (isPathResponse) { @@ -469,24 +431,18 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set e.isPathSelected = false }) - const currentData = canvas.getGraphData(); - - [...currentData.nodes, ...currentData.links].forEach(element => { - element.data.isPath = false - element.data.isPathSelected = false - - if ("source" in element) { - element.color = "#999999" - } + // Reset link colors on the model + graph.Elements.links.forEach(link => { + link.color = "#999999" }) - canvas.setGraphData(currentData) + canvas.setGraphData(convertToCanvasData(graph.Elements)) } - setMessages(prev => [...prev, { + setMessages(prev => [...prev, createMessage({ type: MessageTypes.Response, text: "Please select a starting point and the end point. Select or press relevant item on the graph" - }, { type: MessageTypes.Path }]) + }), createMessage({ type: MessageTypes.Path })]) setPath({}) }} > @@ -513,7 +469,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set const getMessage = (message: Message, index?: number) => { switch (message.type) { case MessageTypes.Query: return ( -
+

You

@@ -522,13 +478,12 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
) case MessageTypes.Response: return ( -
+

Answer

) case MessageTypes.Text: return ( -

{message.text}

+

{message.text}

) case MessageTypes.Path: { return ( -
+
+
+
+ +

Answer

+
{ message.paths && message.paths.map((p, i: number) => ( @@ -577,7 +536,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set className={cn( "flex text-wrap border p-2 gap-2 rounded-md", p.nodes.length === selectedPath?.nodes.length && - selectedPath?.nodes.every(node => p?.nodes.some((n) => n.id === node.id)) && + selectedPath?.nodes.every((node, i) => p?.nodes[i]?.id === node.id) && "border-[#ffde21] bg-[#ffde2133]", message.graphName !== graph.Id && "opacity-50 bg-secondary" )} @@ -592,7 +551,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set return; } - if (selectedPath?.nodes.every(node => p?.nodes.some((n) => n.id === node.id)) && selectedPath.nodes.length === p.nodes.length) return + if (selectedPath?.nodes.every((node, i) => p?.nodes[i]?.id === node.id) && selectedPath.nodes.length === p.nodes.length) return if (!isPathResponse) { setIsPathResponse(undefined) @@ -615,7 +574,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
) default: return ( -
+
Thinking... diff --git a/app/src/components/code-graph.tsx b/app/src/components/code-graph.tsx index c0b2a083..0cf108a5 100644 --- a/app/src/components/code-graph.tsx +++ b/app/src/components/code-graph.tsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; -import { Graph, GraphData, Node, Link } from "./model"; +import { Graph, GraphData, Link, Node } from "./model"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; import { Download, GitFork, Search, X } from "lucide-react"; @@ -7,20 +7,18 @@ import { Button } from "@/components/ui/button"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; import { toast } from '@/components/ui/use-toast'; -import { Path, PATH_COLOR } from "@/lib/utils"; +import { Path, RepoOption } from "@/lib/utils"; import Input from './Input'; // import CommitList from './commitList'; import { Checkbox } from '@/components/ui/checkbox'; import type { Position } from "./graphView"; -import { prepareArg } from '../utils'; import { GraphRef } from "@/lib/utils"; -import { dataToGraphData } from "@falkordb/canvas"; -import type { Node as CanvasNode, Link as CanvasLink, GraphData as CanvasData } from "@falkordb/canvas"; import GraphView from "./graphView"; +import { convertToCanvasData } from "./ForceGraph"; const AUTH_HEADERS: HeadersInit = import.meta.env.VITE_SECRET_TOKEN - ? { 'Authorization': `Bearer ${import.meta.env.VITE_SECRET_TOKEN}` } - : {}; + ? { 'Authorization': `Bearer ${import.meta.env.VITE_SECRET_TOKEN}` } + : {}; interface Props { id: "desktop" | "mobile" @@ -29,8 +27,8 @@ interface Props { setData: Dispatch>, onFetchGraph: (graphName: string) => Promise, onFetchNode: (nodeIds: number[]) => Promise, - options: string[] - setOptions: Dispatch> + options: RepoOption[] + setOptions: Dispatch> isShowPath: boolean setPath: Dispatch> canvasRef: GraphRef @@ -42,8 +40,10 @@ interface Props { handleSearchSubmit: (node: any) => void searchNode: any setSearchNode: Dispatch> - cooldownTicks: number | undefined - setCooldownTicks: Dispatch> + animation: boolean + setAnimation: (animation: boolean) => void + manualDimmed: boolean + setManualDimmed: (dimmed: boolean) => void onCategoryClick: (name: string, show: boolean) => void handleDownloadImage: () => void zoomedNodes: Node[] @@ -72,8 +72,10 @@ export function CodeGraph({ handleSearchSubmit, searchNode, setSearchNode, - cooldownTicks, - setCooldownTicks, + animation, + setAnimation, + manualDimmed, + setManualDimmed, onCategoryClick, handleDownloadImage, zoomedNodes, @@ -83,8 +85,7 @@ export function CodeGraph({ }: Props) { const [url, setURL] = useState(""); - const [selectedObj, setSelectedObj] = useState(); - const [selectedObjects, setSelectedObjects] = useState([]); + const [selectedObjects, setSelectedObjects] = useState<(Node | Link)[]>([]); const [position, setPosition] = useState(); const [graphName, setGraphName] = useState(""); const [commits, setCommits] = useState([]); @@ -110,9 +111,15 @@ export function CodeGraph({ useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Delete') { - if (selectedObjects.length === 0 && (!selectedObj || "source" in selectedObj)) return - - handleRemove([...selectedObjects.map(obj => obj.id), selectedObj?.id].filter(id => id !== undefined), "nodes"); + if (selectedObjects.length === 0) return + + const selectedNodeIds = selectedObjects + .filter((obj): obj is Node => 'category' in obj) + .map((obj) => obj.id) + + if (selectedNodeIds.length === 0) return + + handleRemove(selectedNodeIds, "nodes"); } }; @@ -121,7 +128,7 @@ export function CodeGraph({ return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [selectedObjects, selectedObj]); + }, [selectedObjects]); async function fetchCount() { const result = await fetch(`/api/repo_info`, { @@ -193,20 +200,20 @@ export function CodeGraph({ if (nodes.length === 0) return; const expandedNodes: Node[] = [] - const deleteIdsMap = new Set() graph.Elements = { nodes: graph.Elements.nodes.filter(node => { if (!node.collapsed) return true - const isTarget = graph.Elements.links.some(link => link.target === node.id && nodes.some(n => n.id === link.source)); + // Check if this collapsed node is directly part of any link from deleted nodes + const isConnected = graph.Elements.links.some(link => + (link.target === node.id && nodes.some(n => n.id === link.source)) || + (link.source === node.id && nodes.some(n => n.id === link.target)) + ); - if (!isTarget) return true + if (!isConnected) return true - deleteIdsMap.add(node.id) - const deleted = graph.NodesMap.delete(Number(node.id)) - - if (deleted && node.expand) { + if (graph.NodesMap.delete(Number(node.id)) && node.expand) { expandedNodes.push(node) } @@ -215,15 +222,14 @@ export function CodeGraph({ links: graph.Elements.links } - deleteNeighbors(expandedNodes)?.forEach(id => deleteIdsMap.add(id)) - - graph.removeLinks() + deleteNeighbors(expandedNodes) - return deleteIdsMap + graph.removeLinks(nodes.map(n => n.id)) } const handleExpand = async (nodes: Node[], expand: boolean) => { if (expand) { + // DON'T set expand state until after successful fetch const elements = await onFetchNode(nodes.map(n => n.id)) if (elements.nodes.length === 0) { @@ -231,85 +237,36 @@ export function CodeGraph({ title: `No neighbors found`, description: `No neighbors found`, }) + // Don't set expand if no neighbors found - no glow effect return } - const currentData = canvasRef.current?.getGraphData() - - if (!currentData) return - - // Get existing IDs - const existingNodeIds = new Set(currentData.nodes.map(n => n.id)) - const existingLinkIds = new Set(currentData.links.map(l => l.id)) - - // Filter for only new elements - const newDataElements = { - nodes: elements.nodes.filter(n => !existingNodeIds.has(n.id)) - .map(({ category, color, data, id, isPath, isPathSelected, visible }) => ({ - color: isPath ? PATH_COLOR : color, - id, - labels: [category], - data: { - ...data, - isPath, - isPathSelected - }, - visible, - } as CanvasNode)), - links: elements.links.filter(l => !existingLinkIds.has(l.id)) - .map(({ color, id, source, target, data, isPath, isPathSelected, visible, label }) => ({ - color: isPath ? PATH_COLOR : color, - id, - source, - target, - data: { - ...data, - isPath, - isPathSelected - }, - visible, - relationship: label, - } as CanvasLink)) - } - - // Convert only new data to GraphData format - const newGraphData = dataToGraphData( - newDataElements, - undefined, - new Map(currentData.nodes.map(n => [n.id, n])) - ) - - // Merge with existing data - canvasRef.current?.setGraphData({ - nodes: [...currentData.nodes, ...newGraphData.nodes], - links: [...currentData.links, ...newGraphData.links] + // Only NOW set expand state after successful fetch - glow animation triggers here + nodes.forEach((node) => { + node.expand = true }) - setCooldownTicks(-1) + // Update the model with new elements (graph.Elements is already updated by onFetchNode) + // Convert the full model to canvas data and update + canvasRef.current?.setGraphData(convertToCanvasData(graph.Elements)) + setData({ ...graph.Elements }) } else { const deleteNodes = nodes.filter(n => n.expand) if (deleteNodes.length > 0) { - const deleteIdsMap = deleteNeighbors(deleteNodes); - - if (!deleteIdsMap || deleteIdsMap.size === 0) return - - const currentData = canvasRef.current?.getGraphData() + deleteNeighbors(deleteNodes) + } - if (currentData) { - currentData.nodes = currentData.nodes.filter(node => !deleteIdsMap.has(Number(node.id))) - currentData.links = currentData.links.filter(link => !deleteIdsMap.has(Number(link.source.id)) && !deleteIdsMap.has(Number(link.target.id))) + // Set expand state AFTER deleteNeighbors so the filter above works + nodes.forEach((node) => { + node.expand = false + }) - canvasRef.current?.setGraphData(currentData) - setCooldownTicks(-1) - } - } + // Convert the updated model to canvas data + canvasRef.current?.setGraphData(convertToCanvasData(graph.Elements)) + setData({ ...graph.Elements }) } - nodes.forEach((node) => { - node.expand = expand - }) - - setSelectedObj(undefined) + setSelectedObjects([]) } const handleRemove = (ids: number[], type: "nodes" | "links") => { @@ -322,28 +279,26 @@ export function CodeGraph({ element.visible = false }) - const currentData = canvas.getGraphData() - - currentData[type].forEach(element => { - if (!ids.includes(Number(element.id))) return - element.visible = false - }) - if (type === "nodes") { - currentData.links.forEach((link) => { - if (ids.includes(link.source.id) || ids.includes(link.target.id)) { - link.visible = false - } - }) + graph.visibleLinks(false, ids) } - canvas.setGraphData(currentData) - graph.visibleLinks(false, ids) + // setGraphData doesn't update visible for existing nodes — mutate canvas nodes directly + const canvasData = canvas.getGraphData() + canvasData.nodes.forEach(canvasNode => { + const appNode = graph.NodesMap.get(canvasNode.id) + if (appNode) canvasNode.visible = appNode.visible + }) + canvasData.links.forEach(canvasLink => { + const appLink = graph.LinksMap.get(canvasLink.id) + if (appLink) canvasLink.visible = appLink.visible + }) + canvas.refresh() + + setData({ ...graph.Elements }) setHasHiddenElements(true) - setSelectedObj(undefined) setSelectedObjects([]) - setCooldownTicks(-1) } return ( @@ -387,25 +342,25 @@ export function CodeGraph({ if (!canvas) return - graph.getElements().forEach((element) => { - element.isPath = false - element.isPathSelected = false + // Clear path flags from graph model + graph.Elements.nodes.forEach((node) => { + node.isPath = false + node.isPathSelected = false }) - const currentData = canvas.getGraphData(); - - [...currentData.nodes, ...currentData.links].forEach(element => { - element.data.isPath = false - element.data.isPathSelected = false - - if ("source" in element) { - element.color = "#999999" - } + graph.Elements.links.forEach((link) => { + link.isPath = false + link.isPathSelected = false + link.color = "#999999" // Reset link color }) - canvas.setGraphData(currentData) + // Update React state to trigger re-render + setData({ ...graph.Elements }) + + // Update canvas + canvas.setGraphData(convertToCanvasData(graph.Elements)) + canvas.zoomToFit() setIsPathResponse(false) - setCooldownTicks(-1) }} > @@ -428,15 +383,12 @@ export function CodeGraph({ element.visible = true }); - const currentData = canvas.getGraphData(); - - [...currentData.nodes, ...currentData.links].forEach(element => { - element.visible = true - }); - - canvas.setGraphData(currentData); + const canvasData = canvas.getGraphData(); + canvasData.nodes.forEach((n: { visible: boolean }) => { n.visible = true; }); + canvasData.links.forEach((l: { visible: boolean }) => { l.visible = true; }); + canvas.refresh(); + setData({ ...graph.Elements }); setHasHiddenElements(false); - setCooldownTicks(-1); }} > @@ -446,11 +398,11 @@ export function CodeGraph({
{ setPath(path) - setSelectedObj(undefined) + setSelectedObjects([]) }} handleRemove={handleRemove} position={position} @@ -464,9 +416,7 @@ export function CodeGraph({ setData={setData} graph={graph} chartRef={canvasRef} - selectedObj={selectedObj} selectedObjects={selectedObjects} - setSelectedObj={setSelectedObj} setSelectedObjects={setSelectedObjects} setPosition={setPosition} handleExpand={handleExpand} @@ -475,10 +425,9 @@ export function CodeGraph({ isPathResponse={isPathResponse} selectedPathId={selectedPathId} setSelectedPathId={setSelectedPathId} - cooldownTicks={cooldownTicks} - setCooldownTicks={setCooldownTicks} setZoomedNodes={setZoomedNodes} zoomedNodes={zoomedNodes} + manualDimmed={manualDimmed} />
@@ -510,8 +459,11 @@ export function CodeGraph({ className="gap-4" canvasRef={canvasRef} handleDownloadImage={handleDownloadImage} - setCooldownTicks={setCooldownTicks} - cooldownTicks={cooldownTicks} + animation={animation} + setAnimation={setAnimation} + manualDimmed={manualDimmed} + setManualDimmed={setManualDimmed} + selectedObjects={selectedObjects} />
diff --git a/app/src/components/combobox.tsx b/app/src/components/combobox.tsx index 93c6720c..820acd64 100644 --- a/app/src/components/combobox.tsx +++ b/app/src/components/combobox.tsx @@ -1,14 +1,15 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; import { useEffect, useState } from "react"; +import { RepoOption, repoLabel, toRepoOption } from "@/lib/utils"; const AUTH_HEADERS: HeadersInit = import.meta.env.VITE_SECRET_TOKEN ? { 'Authorization': `Bearer ${import.meta.env.VITE_SECRET_TOKEN}` } : {}; interface Props { - options: string[] - setOptions: (options: string[]) => void + options: RepoOption[] + setOptions: (options: RepoOption[]) => void selectedValue: string onSelectedValue: (value: string) => Promise @@ -37,7 +38,8 @@ export default function Combobox({ options, setOptions, selectedValue, onSelecte } const json = await result.json() - setOptions(json.repositories) + const repositories: unknown[] = Array.isArray(json.repositories) ? json.repositories : [] + setOptions(repositories.map(toRepoOption)) } useEffect(() => { @@ -65,8 +67,8 @@ export default function Combobox({ options, setOptions, selectedValue, onSelecte { options.length !== 0 && options.map((option) => ( - - {option} + + {repoLabel(option)} )) } diff --git a/app/src/components/dataPanel.tsx b/app/src/components/dataPanel.tsx index 4cf67120..441574a4 100644 --- a/app/src/components/dataPanel.tsx +++ b/app/src/components/dataPanel.tsx @@ -1,5 +1,4 @@ -import { Dispatch, SetStateAction } from "react"; -import { JSONTree } from 'react-json-tree'; +import { Dispatch, SetStateAction, type ReactNode } from "react"; import { Link, Node } from "./model"; import { Copy, SquareArrowOutUpRight, X } from "lucide-react"; import SyntaxHighlighter from 'react-syntax-highlighter'; @@ -32,6 +31,77 @@ const excludedProperties = [ "fy", ] +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function renderSourceBlock(value: string) { + return ( + + {value} + + ) +} + +function renderJsonValue(value: unknown, path: string[]): ReactNode { + const key = path[path.length - 1] + + if (key === "src" && typeof value === "string") { + return renderSourceBlock(value) + } + + if (Array.isArray(value)) { + return ( +
+ Array({value.length}) +
+ {value.map((item, index) => ( +
+ [{index}] +
+ {renderJsonValue(item, [...path, String(index)])} +
+
+ ))} +
+
+ ) + } + + if (isPlainObject(value)) { + const entries = Object.entries(value).filter(([nestedKey]) => !excludedProperties.includes(nestedKey)) + + return ( +
+ Object({entries.length}) +
+ {entries.map(([nestedKey, nestedValue]) => ( +
+ {nestedKey}: +
+ {renderJsonValue(nestedValue, [...path, nestedKey])} +
+
+ ))} +
+
+ ) + } + + return {String(value)} +} + export default function DataPanel({ obj, setObj, url }: Props) { if (!obj) return null; @@ -50,76 +120,17 @@ export default function DataPanel({ obj, setObj, url }: Props) {
- { - object.map(([key, value]) => ( -
-

{key}:

- { - key === "src" ? - - {value} - - : typeof value === "object" && value !== null ? - !excludedProperties.includes(k)))} - theme={{ - base00: 'transparent', // background - base01: '#000000', - base02: '#CE9178', - base03: '#CE9178', // open values - base04: '#CE9178', - base05: '#CE9178', - base06: '#CE9178', - base07: '#CE9178', - base08: '#CE9178', - base09: '#b5cea8', // numbers - base0A: '#CE9178', - base0B: '#CE9178', // close values - base0C: '#CE9178', - base0D: '#99E4E5', // * keys - base0E: '#ae81ff', - base0F: '#cc6633' - }} - valueRenderer={(_valueAsString, value, keyPath) => { - if (keyPath === "src") { - return - {value as string} - - } - return {value as string} - }} - /> - : {value} - } + {object.map(([key, value]) => ( +
+

{key}:

+
+ {renderJsonValue(value, [key])}
- )) - } +
+ ))}
- { - "category" in obj && + {"category" in obj && ( <>
diff --git a/app/src/components/elementMenu.tsx b/app/src/components/elementMenu.tsx index e7d5764c..70c14d85 100644 --- a/app/src/components/elementMenu.tsx +++ b/app/src/components/elementMenu.tsx @@ -9,19 +9,21 @@ import { GraphNode } from "@falkordb/canvas"; interface Props { obj: Node | Link | undefined; - objects: Node[]; + objects: (Node | Link)[]; setPath: Dispatch>; handleRemove: (ids: number[], type: "nodes" | "links") => void; position: Position | undefined; url: string; handleExpand: (nodes: Node[], expand: boolean) => void; - parentRef: RefObject; + parentRef: RefObject; } export default function ElementMenu({ obj, objects, setPath, handleRemove, position, url, handleExpand, parentRef }: Props) { const [currentObj, setCurrentObj] = useState(); const [containerWidth, setContainerWidth] = useState(0); + const [containerHeight, setContainerHeight] = useState(0); + const nodeObjects = objects.filter((o): o is Node => "category" in o) useEffect(() => { setCurrentObj(undefined) @@ -29,33 +31,62 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit if (!obj || !position) return null - const objURL = "category" in obj && obj.category === "File" - ? `${url}/tree/master/${obj.data.path}/${obj.data.name}` - : `${url}/tree/master/${obj.data.path}#L${obj.data.src_start}-L${obj.data.src_end + 1}` + const nodeURL = "category" in obj + ? obj.category === "File" + ? `${url}/tree/master/${obj.data.path}/${obj.data.name}` + : `${url}/tree/master/${obj.data.path}#L${obj.data.src_start}-L${obj.data.src_end + 1}` + : undefined + + const parentRect = parentRef?.current?.getBoundingClientRect() + const parentW = parentRef?.current?.clientWidth || 0 + const parentH = parentRef?.current?.clientHeight || 0 + + const edgeMargin = 8 // fixed margin from container edges + const gap = 12 // fixed gap between node edge and menu — constant at every zoom level + + const zoom = position.zoom ?? 1 + const nodeRadius = 9 * zoom // node visual radius in screen px (NODE_SIZE=9 world units) + + // Convert viewport coordinates to parent-relative coordinates + const relX = position.x - (parentRect?.left || 0) + const relY = position.y - (parentRect?.top || 0) + + // X axis: center on click, clamp to container edges + const rawLeft = relX - containerWidth / 2 + const left = Math.max(edgeMargin, Math.min(rawLeft, parentW - containerWidth - edgeMargin)) + + // Y axis: always place below at (node_center + node_radius + fixed_gap). + // Clamp to canvas edges — no flipping. If the node is larger than the canvas + // the menu stays at the bottom edge, which may overlap the node. + const rawTop = relY + nodeRadius + gap + const top = Math.max(edgeMargin, Math.min(rawTop, parentH - containerHeight - edgeMargin)) return ( <>
{ if (!ref) return - setContainerWidth(ref.clientWidth) + const { clientWidth, clientHeight } = ref + if (clientWidth !== containerWidth) { + setContainerWidth(clientWidth) + } + if (clientHeight !== containerHeight) { + setContainerHeight(clientHeight) + } }} - className="absolute z-10 bg-popover text-popover-foreground rounded-lg shadow-lg flex divide-x divide-border" + className="absolute z-[15] bg-popover text-popover-foreground rounded-lg shadow-lg flex divide-x divide-border" id="elementMenu" - style={{ - left: Math.max(8, Math.min(position.x - containerWidth / 2, (parentRef?.current?.clientWidth || 0) - containerWidth - 8)), - top: Math.max(8, Math.min(position.y - 153, (parentRef?.current?.clientHeight || 0) - containerWidth - 8)), - }} + style={{ left, top }} > { - objects.some(o => o.id === obj.id) && objects.length > 1 ? + "category" in obj && nodeObjects.some(o => o.id === obj.id) && nodeObjects.length > 1 ? <> { - objects.length === 2 && + nodeObjects.length === 2 && @@ -63,19 +94,19 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit @@ -123,11 +154,11 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit <> { - window.open(objURL, '_blank'); + if (nodeURL) window.open(nodeURL, '_blank'); }} > @@ -164,7 +195,7 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit ) diff --git a/app/src/components/graphView.tsx b/app/src/components/graphView.tsx index 3a08e27f..0311f71b 100644 --- a/app/src/components/graphView.tsx +++ b/app/src/components/graphView.tsx @@ -1,7 +1,7 @@ import { Graph, GraphData, Link, Node } from './model'; -import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; -import { Path, PATH_COLOR } from '@/lib/utils'; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Path } from '@/lib/utils'; import { Fullscreen } from 'lucide-react'; import { GraphRef } from '@/lib/utils'; import ForceGraph from './ForceGraph'; @@ -11,6 +11,7 @@ import { useTheme } from './theme-provider'; export interface Position { x: number, y: number, + zoom?: number, } interface Props { @@ -19,10 +20,8 @@ interface Props { graph: Graph chartRef: GraphRef id: "desktop" | "mobile" - selectedObj: Node | Link | undefined - setSelectedObj: Dispatch> - selectedObjects: Node[] - setSelectedObjects: Dispatch> + selectedObjects: (Node | Link)[] + setSelectedObjects: Dispatch> setPosition: Dispatch> handleExpand: (nodes: Node[], expand: boolean) => void isShowPath: boolean @@ -30,30 +29,23 @@ interface Props { isPathResponse: boolean | undefined selectedPathId: number | undefined setSelectedPathId: (selectedPathId: number) => void - cooldownTicks: number | undefined - setCooldownTicks: Dispatch> setZoomedNodes: Dispatch> zoomedNodes: Node[] + manualDimmed: boolean } -const NODE_SIZE = 6; -const PADDING = 2; const LIGHT_CANVAS_BACKGROUND = '#FFFFFF'; const DARK_CANVAS_BACKGROUND = '#1A1A1A'; const LIGHT_CANVAS_FOREGROUND = '#000000'; const DARK_CANVAS_FOREGROUND = '#F5F5F5'; -const LIGHT_DIMMED_NODE_FILL = '#E5E5E5'; -const DARK_DIMMED_NODE_FILL = '#525252'; -const LIGHT_DIMMED_NODE_STROKE = 'gray'; -const DARK_DIMMED_NODE_STROKE = '#A3A3A3'; + +const DOUBLE_CLICK_MS = 300; export default function GraphView({ data, graph, chartRef: canvasRef, id, - selectedObj, - setSelectedObj, selectedObjects, setSelectedObjects, setPosition, @@ -63,21 +55,19 @@ export default function GraphView({ isPathResponse, selectedPathId, setSelectedPathId, - cooldownTicks, - setCooldownTicks, zoomedNodes, - setZoomedNodes + setZoomedNodes, + manualDimmed }: Props) { - const lastClick = useRef<{ date: Date, name: string }>({ date: new Date(), name: "" }) + const lastClick = useRef<{ date: number, id: number }>({ date: 0, id: 0 }) + const isCenteringRef = useRef(false) const [screenSize, setScreenSize] = useState(0) const [hoverElement, setHoverElement] = useState() const { resolvedTheme } = useTheme() const isDark = resolvedTheme === 'dark' const canvasBackgroundColor = isDark ? DARK_CANVAS_BACKGROUND : LIGHT_CANVAS_BACKGROUND const canvasForegroundColor = isDark ? DARK_CANVAS_FOREGROUND : LIGHT_CANVAS_FOREGROUND - const dimmedNodeFillColor = isDark ? DARK_DIMMED_NODE_FILL : LIGHT_DIMMED_NODE_FILL - const dimmedNodeStrokeColor = isDark ? DARK_DIMMED_NODE_STROKE : LIGHT_DIMMED_NODE_STROKE useEffect(() => { const handleResize = () => { @@ -94,10 +84,9 @@ export default function GraphView({ }, []) const unsetSelectedObjects = useCallback((evt?: MouseEvent) => { - if (evt?.ctrlKey || (!selectedObj && selectedObjects.length === 0)) return - setSelectedObj(undefined) + if (evt?.ctrlKey || selectedObjects.length === 0) return setSelectedObjects([]) - }, [selectedObj, selectedObjects, setSelectedObj, setSelectedObjects]) + }, [selectedObjects, setSelectedObjects]) const handleRightClick = useCallback((element: Node | Link, evt: MouseEvent) => { if (evt.ctrlKey && "category" in element) { @@ -105,15 +94,70 @@ export default function GraphView({ setSelectedObjects(selectedObjects.filter(obj => obj.id !== element.id)) return } else { - setSelectedObjects([...selectedObjects, element as Node]) + setSelectedObjects([...selectedObjects, element]) } } else { - setSelectedObjects([]) + setSelectedObjects([element]) } - setSelectedObj(element) - setPosition({ x: evt.clientX, y: evt.clientY }) - }, [selectedObjects, setSelectedObjects, setSelectedObj, setPosition]) + // Center on node or link midpoint when focus mode is ON, then show menu + if ((manualDimmed || isPathResponse) && canvasRef.current) { + // Clear any existing menu immediately so it doesn't flash at old position + setPosition(undefined) + + const graphData = canvasRef.current.getGraphData() + let cx: number | undefined + let cy: number | undefined + + if ("category" in element) { + const graphNode = graphData?.nodes.find(n => n.id === element.id) + cx = graphNode?.x + cy = graphNode?.y + } else { + const src = graphData?.nodes.find(n => n.id === (element as Link).source) + const tgt = graphData?.nodes.find(n => n.id === (element as Link).target) + if (src?.x !== undefined && tgt?.x !== undefined) { + cx = ((src.x ?? 0) + (tgt.x ?? 0)) / 2 + cy = ((src.y ?? 0) + (tgt.y ?? 0)) / 2 + } + } + + if (cx !== undefined && cy !== undefined) { + isCenteringRef.current = true + canvasRef.current.centerAt(cx, cy, 300) + // Show menu after animation fully settles (400ms > 300ms animation) + setTimeout(() => { + isCenteringRef.current = false + const canvasBounds = canvasRef.current?.getBoundingClientRect() + if (canvasBounds) { + setPosition({ + x: canvasBounds.left + canvasBounds.width / 2, + y: canvasBounds.top + canvasBounds.height / 2, + zoom: canvasRef.current?.getZoom() ?? 1 + }) + } + }, 400) + return + } + } + // Focus mode OFF: use the node's actual screen-center so the gap in + // elementMenu is always the same fixed value regardless of where on the + // node the user clicked. + if ("category" in element && canvasRef.current) { + const gd = canvasRef.current.getGraphData() + const gn = gd?.nodes.find(n => n.id === element.id) + const vp = canvasRef.current.getViewport() + const cb = canvasRef.current.getBoundingClientRect() + if (gn?.x !== undefined && gn?.y !== undefined && vp && cb) { + const z = vp.zoom + const sx = (gn.x - vp.centerX) * z + cb.width / 2 + const sy = (gn.y - vp.centerY) * z + cb.height / 2 + setPosition({ x: cb.left + sx, y: cb.top + sy, zoom: z }) + return + } + } + setPosition({ x: evt.clientX, y: evt.clientY, zoom: canvasRef.current?.getZoom() ?? 1 }) + }, [selectedObjects, setSelectedObjects, setPosition, canvasRef, manualDimmed, isPathResponse]) const handleLinkClick = (link: Link, evt: MouseEvent) => { unsetSelectedObjects(evt) @@ -132,26 +176,26 @@ export default function GraphView({ const isNodeSelected = useCallback((node: GraphNode) => { if (isPathResponse) { return node.data.isPathSelected - } else { - return selectedObjects.some(obj => "category" in obj && obj.id === node.id) || (selectedObj && "category" in selectedObj && selectedObj?.id === node.id) || (hoverElement && ('category' in hoverElement) && hoverElement.id === node.id) } - }, [isPathResponse, selectedObjects, selectedObj, hoverElement]) + // Highlight selected and hovered nodes - dimming handled separately by isNodeDimmed + return selectedObjects.some(obj => "category" in obj && obj.id === node.id) || (hoverElement && ('category' in hoverElement) && hoverElement.id === node.id) + }, [isPathResponse, selectedObjects, hoverElement]) const isLinkSelected = useCallback((link: GraphLink) => { if (isPathResponse) { return link.data.isPathSelected - } else { - return selectedObjects.some(obj => "source" in obj && obj.id === link.id) || (selectedObj && "source" in selectedObj && selectedObj?.id === link.id) || (hoverElement && 'source' in hoverElement && hoverElement.id === link.id) } - }, [isPathResponse, selectedObjects, selectedObj, hoverElement]) + // Highlight selected and hovered links - dimming handled separately by isLinkDimmed + return selectedObjects.some(obj => "source" in obj && obj.id === link.id) || (hoverElement && 'source' in hoverElement && hoverElement.id === link.id) + }, [isPathResponse, selectedObjects, hoverElement]) const handleNodeClick = useCallback(async (node: Node) => { - const now = new Date() - const { date, name } = lastClick.current - - const isDoubleClick = now.getTime() - date.getTime() < 1000 && name === node.data.name - lastClick.current = { date: now, name: node.data.name } + const now = Date.now() + const { date, id } = lastClick.current + const isDoubleClick = now - date < DOUBLE_CLICK_MS && id === node.id + lastClick.current = isDoubleClick ? { date: 0, id: 0 } : { date: now, id: node.id } + if (isDoubleClick) { handleExpand([node], !node.expand) } else if (isShowPath) { @@ -171,107 +215,115 @@ export default function GraphView({ canvasRef.current?.zoomToFit(zoomedNodes.length === 1 ? 4 : 1, (n: GraphNode) => zoomedNodes.some(node => node.id === n.id)) setZoomedNodes([]) } + }, [zoomedNodes, canvasRef]) - if (cooldownTicks !== -1) return - - setCooldownTicks(0) - }, [zoomedNodes, cooldownTicks, canvasRef]) - - const nodeCanvasObject = useCallback((node: GraphNode, ctx: CanvasRenderingContext2D) => { - if (node.x === undefined || node.y === undefined) { - node.x = 0; - node.y = 0; + const activeElements = useMemo(() => { + const elements: (Node | Link)[] = [...selectedObjects] + if (hoverElement && !elements.includes(hoverElement)) { + elements.push(hoverElement) + } + return elements + }, [selectedObjects, hoverElement]) + + const selectedNodeIds = useMemo(() => { + const ids = new Set() + for (const el of activeElements) { + if (!('source' in el)) { + ids.add(el.id) + } + } + return ids + }, [activeElements]) + + const activeEndpointIds = useMemo(() => { + const ids = new Set() + for (const el of activeElements) { + if ('source' in el) { + ids.add(el.source as number) + ids.add(el.target as number) + } } + return ids + }, [activeElements]) - const isHovered = !!hoverElement && !('source' in hoverElement) && hoverElement.id === node.id - const isSelected = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id + const adjacentToSelectedNodeIds = useMemo(() => { + const ids = new Set() + if (selectedNodeIds.size === 0) return ids - if (isPathResponse) { - if (node.data.isPathSelected) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 1.5 - } else if (node.data.isPath) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 1 - } else { - ctx.fillStyle = dimmedNodeFillColor; - ctx.strokeStyle = dimmedNodeStrokeColor; - ctx.lineWidth = 1 + for (const link of data.links) { + if (selectedNodeIds.has(link.source)) { + ids.add(link.target) } - } else if (isPathResponse === undefined) { - if (node.data.isPathSelected) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 1.5 - } else if (node.data.isPath) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 1 - } else { - ctx.fillStyle = node.color; - ctx.strokeStyle = canvasForegroundColor; - ctx.lineWidth = isSelected || isHovered ? 1.5 : 1 + if (selectedNodeIds.has(link.target)) { + ids.add(link.source) } - } else { - ctx.fillStyle = node.color; - ctx.strokeStyle = canvasForegroundColor; - ctx.lineWidth = isSelected || isHovered ? 1.5 : 1 } - ctx.beginPath(); - ctx.arc(node.x, node.y, NODE_SIZE + ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); - ctx.fill(); - - ctx.fillStyle = canvasForegroundColor; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = '2px Arial'; - let name = node.data.name || ""; - const textWidth = ctx.measureText(name).width; - const ellipsis = '...'; - const ellipsisWidth = ctx.measureText(ellipsis).width; - const nodeSize = (NODE_SIZE + ctx.lineWidth / 2) * 2 - PADDING; - - // truncate text if it's too long - if (textWidth > nodeSize) { - while (name.length > 0 && ctx.measureText(name).width + ellipsisWidth > nodeSize) { - name = name.slice(0, -1); - } - name += ellipsis; + return ids + }, [selectedNodeIds, data.links]) + + const isNodeDimmed = useCallback((node: GraphNode) => { + if (isPathResponse) { + return !node.data.isPath && !node.data.isPathSelected } + // Only apply dimming when focus mode is ON (manualDimmed=true) + // When manualDimmed=false, nothing should be dimmed regardless of selection + if (!manualDimmed) { + return false + } + + if (activeElements.length === 0) return false + + const allActiveIds = new Set([...selectedNodeIds, ...activeEndpointIds]) + if (allActiveIds.size === 0) return false + if (allActiveIds.has(node.id)) return false - // add label - ctx.fillText(name, node.x, node.y); + return !adjacentToSelectedNodeIds.has(node.id) }, [ - selectedObj, - selectedObjects, isPathResponse, - hoverElement, - dimmedNodeFillColor, - dimmedNodeStrokeColor, - canvasForegroundColor, + manualDimmed, + activeElements, + selectedNodeIds, + activeEndpointIds, + adjacentToSelectedNodeIds, ]) - const nodePointerAreaPaint = useCallback((node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => { - if (node.x === undefined || node.y === undefined) { - node.x = 0; - node.y = 0; + const isLinkDimmed = useCallback((link: GraphLink) => { + if (isPathResponse) { + return !link.data.isPath && !link.data.isPathSelected } - - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(node.x, node.y, NODE_SIZE + 2 + ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.fill(); - }, []) + // Only apply dimming when focus mode is ON (manualDimmed=true) + // When manualDimmed=false, nothing should be dimmed regardless of selection + if (!manualDimmed) { + return false + } + + if (activeElements.length === 0) return false + + // Don't dim the link itself if it's selected/hovered + if (hoverElement && 'source' in hoverElement && hoverElement.id === link.id) return false + if (selectedObjects.some(obj => 'source' in obj && obj.id === link.id)) return false + + if (selectedNodeIds.size === 0) return true + + // Link is undimmed if one endpoint is a directly selected node + const srcId = typeof link.source === 'object' ? (link.source as any).id : link.source + const tgtId = typeof link.target === 'object' ? (link.target as any).id : link.target + + if (selectedNodeIds.has(srcId) || selectedNodeIds.has(tgtId)) return false + + return true + }, [isPathResponse, manualDimmed, selectedObjects, hoverElement, activeElements, selectedNodeIds]) const linkLineDash = useCallback((link: GraphLink) => { if (link.data.isPath && !link.data.isPathSelected) return [5, 5] return [] }, []) + const handleNodeDragEnd = useCallback(() => { + setPosition(undefined) + }, [setPosition]) + const mobileBreakpointRaw = Number(import.meta.env.VITE_MOBILE_BREAKPOINT) const mobileBreakpoint = Number.isFinite(mobileBreakpointRaw) ? mobileBreakpointRaw : 0 const isDesktop = screenSize > mobileBreakpoint @@ -285,24 +337,26 @@ export default function GraphView({
handleNodeClick(node) : (node: Node, evt: MouseEvent) => handleRightClick(node, evt)} onNodeHover={handleNodeHover} onNodeRightClick={handleRightClick} isNodeSelected={isNodeSelected} - onLinkClick={isDesktop && isPathResponse ? handleLinkClick : handleRightClick} + onLinkClick={isDesktop ? handleLinkClick : handleRightClick} onLinkHover={handleLinkHover} onLinkRightClick={handleRightClick} isLinkSelected={isLinkSelected} + isNodeDimmed={isNodeDimmed} + isLinkDimmed={isLinkDimmed} + dimmed={isPathResponse === true || manualDimmed} + linkLineDash={linkLineDash} onBackgroundClick={unsetSelectedObjects} onBackgroundRightClick={unsetSelectedObjects} - onZoom={() => unsetSelectedObjects()} + onZoom={() => { if (!isCenteringRef.current) unsetSelectedObjects() }} onEngineStop={handleEngineStop} - nodeCanvasObject={nodeCanvasObject} - nodePointerAreaPaint={nodePointerAreaPaint} - linkLineDash={linkLineDash} - cooldownTicks={cooldownTicks} + onNodeDragEnd={handleNodeDragEnd} backgroundColor={canvasBackgroundColor} foregroundColor={canvasForegroundColor} /> diff --git a/app/src/components/model.ts b/app/src/components/model.ts index 2fe40608..91380b48 100644 --- a/app/src/components/model.ts +++ b/app/src/components/model.ts @@ -34,6 +34,7 @@ export interface Link { target: number, label: string, visible: boolean, + collapsed: boolean, isPathSelected: boolean, isPath: boolean, color: string, @@ -246,6 +247,7 @@ export class Graph { target: edgeData.dest_node, label: edgeData.relation, visible: true, + collapsed, color: "#999999", isPathSelected: false, isPath: !!path, @@ -260,16 +262,20 @@ export class Graph { return newElements } - public removeLinks() { - this.elements = { - nodes: this.elements.nodes, - links: this.elements.links.map(link => { - if (this.nodesMap.get(link.source) && this.nodesMap.get(link.target)) { - return link - } + public removeLinks(ids: number[] = []) { + const idsSet = ids.length !== 0 ? new Set(ids) : null + + this.elements.links = this.elements.links.filter(link => { + const isConnectedToIds = idsSet !== null && (idsSet.has(link.source) || idsSet.has(link.target)) + const hasEndpoints = this.nodesMap.get(link.source) && this.nodesMap.get(link.target) + const shouldKeep = Boolean(hasEndpoints) && !(link.collapsed && isConnectedToIds) + + if (!shouldKeep) { this.linksMap.delete(link.id) - }).filter(link => link !== undefined) - } + } + + return shouldKeep + }) } public visibleLinks(visible: boolean, ids?: number[]) { diff --git a/app/src/components/toolbar.tsx b/app/src/components/toolbar.tsx index b4d11258..93687e78 100644 --- a/app/src/components/toolbar.tsx +++ b/app/src/components/toolbar.tsx @@ -1,71 +1,263 @@ -import { Download, Fullscreen, ZoomIn, ZoomOut } from "lucide-react"; +import { useState } from "react"; +import { ChevronDown, Circle, Download, Fullscreen, Pause, Pin, PinOff, Play, Telescope, ZoomIn, ZoomOut } from "lucide-react"; +import type { LayoutMode, HierarchyDirection, RadialDirection } from "@falkordb/canvas"; import { cn } from "@/lib/utils" import { GraphRef } from "@/lib/utils"; import { Switch } from "@/components/ui/switch"; +import type { Node, Link } from "./model"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; + +const LAYOUTS: { value: LayoutMode; label: string }[] = [ + { value: 'force', label: 'Force' }, + { value: 'tree', label: 'Tree' }, + { value: 'radial', label: 'Radial' }, +]; + +const HIERARCHY_DIRECTIONS: { value: HierarchyDirection; label: string }[] = [ + { value: 'td', label: 'Top → Down' }, + { value: 'bu', label: 'Bottom → Up' }, + { value: 'lr', label: 'Left → Right' }, + { value: 'rl', label: 'Right → Left' }, +]; + +const RADIAL_DIRECTIONS: { value: RadialDirection; label: string }[] = [ + { value: 'out', label: 'Outward' }, + { value: 'in', label: 'Inward' }, +]; + +interface ZoomControlsProps { + canvasRef: GraphRef + className?: string + selectedObjects?: (Node | Link)[] +} + +export function ZoomControls({ canvasRef, className, selectedObjects }: ZoomControlsProps) { + const handleZoomClick = (changeFactor: number) => { + const canvas = canvasRef.current + if (!canvas) return + if (selectedObjects && selectedObjects.length > 0) { + const graphData = canvas.getGraphData() + const selectedNodeIds = new Set() + for (const el of selectedObjects) { + if ('source' in el) { + selectedNodeIds.add(el.source as number) + selectedNodeIds.add(el.target as number) + } else { + selectedNodeIds.add(el.id) + } + } + const focusedNodes = graphData?.nodes.filter(n => selectedNodeIds.has(n.id)) ?? [] + if (focusedNodes.length > 0) { + const cx = focusedNodes.reduce((s, n) => s + (n.x ?? 0), 0) / focusedNodes.length + const cy = focusedNodes.reduce((s, n) => s + (n.y ?? 0), 0) / focusedNodes.length + canvas.centerAt(cx, cy, 300) + } + } + canvas.zoom(canvas.getZoom() * changeFactor) + } + + return ( +
+ + + +
+ ) +} interface Props { canvasRef: GraphRef className?: string handleDownloadImage?: () => void - setCooldownTicks: (ticks?: 0) => void - cooldownTicks: number | undefined + animation: boolean + setAnimation: (animation: boolean) => void + manualDimmed: boolean + setManualDimmed: (dimmed: boolean) => void + selectedObjects?: (Node | Link)[] + /** Hide the three zoom buttons, show everything else. */ + hideZoom?: boolean } -export function Toolbar({ canvasRef, className, handleDownloadImage, setCooldownTicks, cooldownTicks }: Props) { +export function Toolbar({ canvasRef, className, handleDownloadImage, animation, setAnimation, manualDimmed, setManualDimmed, selectedObjects, hideZoom: hideZoom }: Props) { - const handleZoomClick = (changefactor: number) => { - const canvas = canvasRef.current + const [layout, setLayout] = useState('force'); + const [treeDirection, setTreeDirection] = useState('td'); + const [radialDirection, setRadialDirection] = useState('out'); + const [pinned, setPinned] = useState(false); + + const handleAnimationToggle = (checked: boolean) => { + setAnimation(checked); + canvasRef.current?.setAnimation(checked); + } - if (canvas) { - canvas.zoom(canvas.getZoom() * changefactor) + const handleDimToggle = (checked: boolean) => { + setManualDimmed(checked); + canvasRef.current?.setDimmed(checked); + } + + const handlePinToggle = () => { + const next = !pinned; + setPinned(next); + canvasRef.current?.setPinOnDragEnd(next); + } + + const handleLayoutChange = (value: string) => { + const mode = value as LayoutMode; + setLayout(mode); + + if (mode === 'tree') { + canvasRef.current?.setLayoutOptions({ tree: { direction: treeDirection } }); + } else if (mode === 'radial') { + canvasRef.current?.setLayoutOptions({ radial: { direction: radialDirection } }); + } + + canvasRef.current?.setLayout(mode); + + // Non-force layouts auto-pin and auto-disable animation + const nextPinned = mode !== 'force'; + setPinned(nextPinned); + canvasRef.current?.setPinOnDragEnd(nextPinned); + + // If switching to non-force layout, disable animation + if (mode !== 'force' && animation) { + setAnimation(false); + canvasRef.current?.setAnimation(false); } } - const handleCenterClick = () => { - const canvas = canvasRef.current + const handleDirectionChange = (value: string, targetLayout?: string) => { + const effectiveLayout = targetLayout || layout; - if (canvas) { - canvas.zoomToFit() + if (effectiveLayout === 'tree') { + const direction = value as HierarchyDirection; + setTreeDirection(direction); + canvasRef.current?.setLayoutOptions({ tree: { direction } }); + } else if (effectiveLayout === 'radial') { + const direction = value as RadialDirection; + setRadialDirection(direction); + canvasRef.current?.setLayoutOptions({ radial: { direction } }); } } + const animationDisabled = pinned || layout !== 'force'; + return ( -
+
+ {animation ? : } { - setCooldownTicks(cooldownTicks !== 0 ? 0 : undefined) - }} + className="pointer-events-auto data-[state=unchecked]:bg-border" + checked={animation} + disabled={animationDisabled} + onCheckedChange={handleAnimationToggle} + aria-label={animation ? "Pause animation" : "Play animation"} + /> + + - - - +
+ + + + + + + Force + + + + {layout === 'tree' && ( + + + + )} + Tree + + + {HIERARCHY_DIRECTIONS.map(d => ( + { + if (layout !== 'tree') handleLayoutChange('tree'); + handleDirectionChange(d.value, 'tree'); + }} + > + {layout === 'tree' && treeDirection === d.value && ( + + + + )} + {d.label} + + ))} + + + + + {layout === 'radial' && ( + + + + )} + Radial + + + {RADIAL_DIRECTIONS.map(d => ( + { + if (layout !== 'radial') handleLayoutChange('radial'); + handleDirectionChange(d.value, 'radial'); + }} + > + {layout === 'radial' && radialDirection === d.value && ( + + + + )} + {d.label} + + ))} + + + + +
+ {!hideZoom && } + {handleDownloadImage && ( + + )}
) } \ No newline at end of file diff --git a/app/src/lib/utils.ts b/app/src/lib/utils.ts index 73cf2c9c..0a1f8e03 100644 --- a/app/src/lib/utils.ts +++ b/app/src/lib/utils.ts @@ -8,6 +8,37 @@ export type PathData = { links: any[] } +export const DEFAULT_BRANCH = "_default" + +// A single entry returned by /api/list_repos: `graph` is the underlying +// FalkorDB graph name to query, `project`/`branch` are used for display. +export type RepoOption = { + project: string + branch: string + graph: string +} + +// Normalizes a /api/list_repos entry into a RepoOption. Older/production +// backends may still return plain repo name strings instead of +// {project, branch, graph} objects — handle both so the UI never renders +// "undefined (undefined)" or crashes when pointed at a mismatched backend. +export function toRepoOption(entry: unknown): RepoOption { + if (typeof entry === "string") { + return { project: entry, branch: DEFAULT_BRANCH, graph: entry } + } + const repo = entry as Partial | null | undefined + const graph = repo?.graph ?? repo?.project ?? "" + return { + project: repo?.project ?? graph, + branch: repo?.branch ?? DEFAULT_BRANCH, + graph, + } +} + +export function repoLabel(repo: RepoOption): string { + return repo.branch === DEFAULT_BRANCH ? repo.project : `${repo.project} (${repo.branch})` +} + export type PathNode = { id?: number name?: string @@ -27,7 +58,13 @@ export enum MessageTypes { Text, } +export function createMessage(msg: Omit): Message { + const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}` + return { ...msg, id } +} + export interface Message { + id: string; type: MessageTypes; text?: string; paths?: { nodes: any[], links: any[] }[]; diff --git a/e2e/config/testData.ts b/e2e/config/testData.ts index 3c4c3785..e8bf166e 100644 --- a/e2e/config/testData.ts +++ b/e2e/config/testData.ts @@ -26,7 +26,7 @@ export const nodes: { nodeName: string; }[] = [ { nodeName: "ask"} ]; -export const categories: string[] = ['File', 'Class', 'Function']; +export const categories: string[] = ['File', 'Function', 'Class']; export const graphs: { graphName: string; }[] = [ { graphName: "GraphRAG-SDK" }, diff --git a/e2e/infra/ui/browserWrapper.ts b/e2e/infra/ui/browserWrapper.ts index 77fa5d07..0108092d 100644 --- a/e2e/infra/ui/browserWrapper.ts +++ b/e2e/infra/ui/browserWrapper.ts @@ -15,6 +15,7 @@ export default class BrowserWrapper { } this.context = await this.browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'], + viewport: { width: 1920, height: 1080 }, }); if (!this.page) { this.page = await this.context.newPage(); diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index bd2f4249..3efab4e4 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -71,7 +71,7 @@ export default class CodeGraph extends BasePage { } private get selectGraphInComboBoxByName(): (graph: string) => Locator { - return (graph: string) => this.page.locator(`//div[@role='presentation']//div//span[contains(text(), '${graph}')]`); + return (graph: string) => this.page.locator(`//div[@role='presentation']//div//span[contains(text(), '${graph}')]`).first(); } private get selectGraphInComboBoxById(): (graph: string) => Locator { @@ -218,6 +218,10 @@ export default class CodeGraph extends BasePage { return (checkbox: string) => this.scopedLocator(`(//button[@role='checkbox'])[${checkbox}]`); } + private get categoryCheckbox(): (category: string) => Locator { + return (category: string) => this.scopedLocator(`//p[text()='${category}']/preceding-sibling::button[@role='checkbox']`); + } + private get clearGraphBtn(): Locator { return this.container.getByRole('button', { name: 'Reset Graph' }); } @@ -550,6 +554,10 @@ export default class CodeGraph extends BasePage { await interactWhenVisible(this.codeGraphCheckbox(checkbox), (el) => el.click(), `Checkbox ${checkbox}`); } + async selectCategoryCheckbox(category: string): Promise { + await interactWhenVisible(this.categoryCheckbox(category), (el) => el.click(), `Category checkbox ${category}`); + } + async clickOnClearGraphBtn(): Promise { await this.page.mouse.click(10, 10); await interactWhenVisible(this.clearGraphBtn, (el) => el.click(), 'Clear Graph button'); @@ -613,8 +621,19 @@ export default class CodeGraph extends BasePage { } async getMetricsPanelInfo(): Promise<{ nodes: string, edges: string }> { - const nodes = await this.canvasMetricsPanel("1").innerHTML(); - const edges = await this.canvasMetricsPanel("3").innerHTML(); + // The metrics panel is populated by an async /api/repo_info fetch + // that fires after graph selection resolves, so it can briefly read + // "0" right after selectGraph(). Poll until it settles on a + // non-zero value (or give up and return whatever is last read). + let nodes = "0"; + let edges = "0"; + for (let attempt = 0; attempt < 10; attempt++) { + nodes = await this.canvasMetricsPanel("1").innerHTML(); + edges = await this.canvasMetricsPanel("3").innerHTML(); + // innerHTML is e.g. "495 Nodes" / "806 Edges" — parse the number + if (parseInt(nodes, 10) > 0 && parseInt(edges, 10) > 0) break; + await this.page.waitForTimeout(500); + } return { nodes, edges } } @@ -714,7 +733,7 @@ export default class CodeGraph extends BasePage { async getCanvasScaling(): Promise<{ scaleX: number; scaleY: number }> { await this.waitForCanvasAnimationToEnd(); - const zoom = await this.canvasHost.evaluate((canvas: { getZoom?: () => number }) => { + const zoom = await this.canvasHost.evaluate((canvas: any) => { return typeof canvas.getZoom === "function" ? canvas.getZoom() : 1; }); return { scaleX: zoom, scaleY: zoom }; @@ -740,7 +759,13 @@ export default class CodeGraph extends BasePage { await this.page.mouse.move(centerX, centerY); await this.page.waitForTimeout(500); await this.page.mouse.click(centerX, centerY, { button: 'right' }); - if (await this.elementMenu.isVisible()) { + // In focus mode the app centers the clicked element first and only + // shows the menu ~400ms after the pan animation, so wait for it. + const menuAppeared = await this.elementMenu + .waitFor({ state: 'visible', timeout: 3000 }) + .then(() => true) + .catch(() => false); + if (menuAppeared) { return; } await this.page.waitForTimeout(1000); @@ -804,10 +829,7 @@ export default class CodeGraph extends BasePage { const startTime = Date.now(); while (Date.now() - startTime < timeout) { - const { cooldown, zoom } = await this.canvasHost.evaluate((canvas: { - getGraph?: () => { cooldownTicks?: () => number } | undefined; - getZoom?: () => number; - }) => { + const { cooldown, zoom } = await this.canvasHost.evaluate((canvas: any) => { const graph = typeof canvas.getGraph === "function" ? canvas.getGraph() : undefined; return { cooldown: typeof graph?.cooldownTicks === "function" ? graph.cooldownTicks() : null, @@ -857,4 +879,49 @@ export default class CodeGraph extends BasePage { }; }, this.activeGraphGetterName); } + + /* Element Menu specific tests */ + async isElementMenuVisible(): Promise { + await this.page.waitForTimeout(500); + return await this.elementMenu.isVisible(); + } + + async getElementMenuBoundingBox(): Promise { + const isVisible = await this.isElementMenuVisible(); + if (!isVisible) throw new Error("Element menu is not visible!"); + return await this.elementMenu.boundingBox(); + } + + async hasElementMenuButton(buttonTitle: string): Promise { + const isVisible = await this.isElementMenuVisible(); + if (!isVisible) throw new Error("Element menu is not visible!"); + const button = this.elementMenuButton(buttonTitle); + return await button.isVisible(); + } + + async rightClickAtNode(x: number, y: number): Promise { + await this.waitForCanvasAnimationToEnd(); + const boundingBox = await this.canvasElement.boundingBox(); + if (!boundingBox) throw new Error("Canvas bounding box not found"); + + // Move to node position and right-click + await this.page.mouse.move(x, y); + await this.page.waitForTimeout(300); + await this.page.mouse.click(x, y, { button: 'right' }); + + // Wait for element menu to appear. In focus mode the app first centers + // the clicked node (300ms pan) and shows the menu ~400ms later, so we + // must actually wait for visibility rather than checking immediately. + const isMenuVisible = await this.elementMenu + .waitFor({ state: 'visible', timeout: 5000 }) + .then(() => true) + .catch(() => false); + if (!isMenuVisible) { + throw new Error(`Element menu not visible after right-clicking at (${x}, ${y})`); + } + } + + async getCanvasBoundingBox(): Promise { + return await this.canvasElement.boundingBox(); + } } diff --git a/e2e/seed_test_data.py b/e2e/seed_test_data.py index a6fa19ac..bd209022 100644 --- a/e2e/seed_test_data.py +++ b/e2e/seed_test_data.py @@ -98,11 +98,12 @@ def main(): getattr(graphrag_sdk, "__version__", "?"), sdk_path, ) - Project( + sdk_project = Project( name="GraphRAG-SDK", path=sdk_path, url="https://github.com/FalkorDB/GraphRAG-SDK", - ).analyze_sources() + ) + sdk_project.analyze_sources() for url in REPOS: logger.info("Seeding %s ...", url) @@ -110,8 +111,12 @@ def main(): proj.analyze_sources() logger.info("Done seeding %s", url) - ensure_calls_edges("GraphRAG-SDK") - ensure_search_term_variety("GraphRAG-SDK") + # Use the actual composed graph name (e.g. "code:GraphRAG-SDK:_default") + # that analyze_sources() wrote into -- not the bare project name, which + # would silently create/write a separate, unused graph. + graph_name = sdk_project.graph.name + ensure_calls_edges(graph_name) + ensure_search_term_variety(graph_name) logger.info("All test data seeded successfully.") diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index ef86597c..1427a7f4 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -91,12 +91,11 @@ test.describe("Canvas tests", () => { expect(targetNodeForUpdateGraph.visible).toBe(true); }); - categories.forEach((category, index) => { - const checkboxIndex = index + 1; + categories.forEach((category) => { test(`Verify that unchecking the ${category} checkbox hides ${category} nodes on the canvas`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPHRAG_SDK); - await codeGraph.selectCodeGraphCheckbox(checkboxIndex.toString()); + await codeGraph.selectCategoryCheckbox(category); const result = await codeGraph.getGraphNodes(); const findItem = result.find((item: { category: string; }) => item.category === category); expect(findItem.visible).toBeFalsy(); @@ -249,5 +248,91 @@ test.describe("Canvas tests", () => { expect(await codeGraph.isNodeToolTipVisible(node.nodeName)).toBe(true); }) }) - + + // --- Tests covering the convertToCanvasData visibility fix --- + // Previously, invisible nodes were filtered out before being sent to the canvas, + // which caused them to be fully removed (losing their positions). Now all nodes + // are sent with their `visible` property so the canvas preserves their positions. + + test(`Validate hidden node remains in canvas data with visible=false after remove`, async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + await codeGraph.fillSearchBar(nodes[0].nodeName); + await codeGraph.selectSearchBarOptionBtn("1"); + await codeGraph.waitForCanvasAnimationToEnd(); + const initialGraph = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(initialGraph, nodes[0].nodeName); + expect(targetNode).toBeDefined(); + + // Hide the node via the element menu + await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); + await codeGraph.clickOnRemoveNodeViaElementMenu(); + + const updatedGraph = await codeGraph.getGraphNodes(); + const hiddenNode = findNodeByName(updatedGraph, nodes[0].nodeName); + + // Node must still be present in canvas data (not removed), just invisible + expect(hiddenNode).toBeDefined(); + expect(hiddenNode.visible).toBe(false); + }); + + test(`Validate node position is preserved after hide and unhide for ${nodes[0].nodeName}`, async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + await codeGraph.fillSearchBar(nodes[0].nodeName); + await codeGraph.selectSearchBarOptionBtn("1"); + await codeGraph.waitForCanvasAnimationToEnd(); + + // Capture position before hiding + const initialGraph = await codeGraph.getGraphNodes(); + const initialNode = findNodeByName(initialGraph, nodes[0].nodeName); + expect(initialNode).toBeDefined(); + const { x: xBefore, y: yBefore } = initialNode; + + // Hide the node + await codeGraph.nodeClick(initialNode.screenX, initialNode.screenY); + await codeGraph.clickOnRemoveNodeViaElementMenu(); + + // Unhide all nodes — no simulation reruns, so positions must be preserved + await codeGraph.clickOnUnhideNodesBtn(); + + const updatedGraph = await codeGraph.getGraphNodes(); + const restoredNode = findNodeByName(updatedGraph, nodes[0].nodeName); + expect(restoredNode).toBeDefined(); + expect(restoredNode.visible).toBe(true); + + // Position tolerance of 1 unit accounts for floating-point serialisation + expect(Math.abs(restoredNode.x - xBefore)).toBeLessThan(1); + expect(Math.abs(restoredNode.y - yBefore)).toBeLessThan(1); + }); + + test(`Validate category node positions are preserved after toggling visibility for ${categories[0]}`, async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + await codeGraph.waitForCanvasAnimationToEnd(); + + // Capture positions of the first visible category node before hiding + const initialGraph = await codeGraph.getGraphNodes(); + const categoryNodesBefore = initialGraph.filter((n: any) => n.category === categories[0]); + expect(categoryNodesBefore.length).toBeGreaterThan(0); + const sampleNode = categoryNodesBefore[0]; + const { x: xBefore, y: yBefore } = sampleNode; + + // Hide the category + await codeGraph.selectCategoryCheckbox(categories[0]); + + // Show the category again — canvas must not re-simulate; positions are kept + await codeGraph.selectCategoryCheckbox(categories[0]); + + const updatedGraph = await codeGraph.getGraphNodes(); + const restoredNode = updatedGraph.find((n: any) => n.id === sampleNode.id); + expect(restoredNode).toBeDefined(); + expect(restoredNode.visible).toBe(true); + + expect(Math.abs(restoredNode.x - xBefore)).toBeLessThan(1); + expect(Math.abs(restoredNode.y - yBefore)).toBeLessThan(1); + }); + }); diff --git a/e2e/tests/elementMenu.spec.ts b/e2e/tests/elementMenu.spec.ts new file mode 100644 index 00000000..675fe46d --- /dev/null +++ b/e2e/tests/elementMenu.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from "@playwright/test"; +import BrowserWrapper from "../infra/ui/browserWrapper"; +import CodeGraph from "../logic/POM/codeGraph"; +import urls from "../config/urls.json"; +import { GRAPHRAG_SDK } from "../config/constants"; +import { findNodeByName } from "../logic/utils"; +import { nodes } from "../config/testData"; + +test.describe("Element Menu and Right-click Menu tests", () => { + let browser: BrowserWrapper; + + test.beforeAll(async () => { + browser = new BrowserWrapper(); + }); + + test.afterAll(async () => { + await browser.closeBrowser(); + }); + + test("Verify element menu appears on right-click at canvas center", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + // Focus a known node first so it is centered on the canvas — the layout + // is not deterministic, so a blind center click may hit empty background. + await codeGraph.fillSearchBar(nodes[0].nodeName); + await codeGraph.selectSearchBarOptionBtn("1"); + await codeGraph.waitForCanvasAnimationToEnd(); + await codeGraph.rightClickAtCanvasCenter(); + const isMenuVisible = await codeGraph.isElementMenuVisible(); + expect(isMenuVisible).toBe(true); + }); + + test("Verify element menu has consistent positioning gap from node", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + // Get a node's position + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(graphData, nodes[0].nodeName); + expect(targetNode).toBeDefined(); + + // Right-click on the node to trigger element menu + await codeGraph.rightClickAtNode(targetNode.screenX, targetNode.screenY); + + // Verify menu is visible + const isMenuVisible = await codeGraph.isElementMenuVisible(); + expect(isMenuVisible).toBe(true); + + // Get menu position and verify it appears below the node (not overlapping) + const menuBox = await codeGraph.getElementMenuBoundingBox(); + expect(menuBox).toBeDefined(); + expect(menuBox.y).toBeGreaterThan(targetNode.screenY); + }); + + test("Verify element menu contains 'View Node' button", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + // Focus a known node so the canvas-center right-click hits it reliably. + await codeGraph.fillSearchBar(nodes[0].nodeName); + await codeGraph.selectSearchBarOptionBtn("1"); + await codeGraph.waitForCanvasAnimationToEnd(); + await codeGraph.rightClickAtCanvasCenter(); + + const hasViewNodeBtn = await codeGraph.hasElementMenuButton("View Node"); + expect(hasViewNodeBtn).toBe(true); + }); + + test("Verify element menu 'View Node' button opens node details panel", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + // Get a node and right-click on it + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(graphData, nodes[0].nodeName); + expect(targetNode).toBeDefined(); + + await codeGraph.rightClickAtNode(targetNode.screenX, targetNode.screenY); + await codeGraph.clickOnViewNode(); + + expect(await codeGraph.isNodeDetailsPanel()).toBe(true); + }); + + test("Verify element menu 'Remove' button removes node/link from canvas", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + // Remove a specific node to keep the assertion deterministic. + const initialGraphData = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(initialGraphData, nodes[0].nodeName); + expect(targetNode).toBeDefined(); + + await codeGraph.rightClickAtNode(targetNode.screenX, targetNode.screenY); + await codeGraph.clickOnRemoveNodeViaElementMenu(); + + // Hidden nodes remain in graph data; verify visibility rather than length. + await codeGraph.waitForCanvasAnimationToEnd(); + const updatedGraphData = await codeGraph.getGraphNodes(); + const updatedTargetNode = findNodeByName(updatedGraphData, nodes[0].nodeName); + expect(updatedTargetNode).toBeDefined(); + expect(updatedTargetNode.visible).toBe(false); + }); + + test("Verify element menu positioning respects canvas boundaries", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + // Focus a known node so the canvas-center right-click hits it reliably. + await codeGraph.fillSearchBar(nodes[0].nodeName); + await codeGraph.selectSearchBarOptionBtn("1"); + await codeGraph.waitForCanvasAnimationToEnd(); + // Right-click at canvas center + await codeGraph.rightClickAtCanvasCenter(); + + // Get menu bounding box and canvas bounds + const menuBox = await codeGraph.getElementMenuBoundingBox(); + const canvasBox = await codeGraph.getCanvasBoundingBox(); + + // Verify menu stays within canvas bounds + expect(menuBox.x).toBeGreaterThanOrEqual(canvasBox.x); + expect(menuBox.y + menuBox.height).toBeLessThanOrEqual(canvasBox.y + canvasBox.height); + }); +}); diff --git a/e2e/tests/toolbar.spec.ts b/e2e/tests/toolbar.spec.ts new file mode 100644 index 00000000..2b1f4405 --- /dev/null +++ b/e2e/tests/toolbar.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from "@playwright/test"; +import BrowserWrapper from "../infra/ui/browserWrapper"; +import CodeGraph from "../logic/POM/codeGraph"; +import urls from "../config/urls.json"; +import { GRAPHRAG_SDK } from "../config/constants"; + +test.describe("Toolbar and Zoom Controls tests", () => { + let browser: BrowserWrapper; + + test.beforeAll(async () => { + browser = new BrowserWrapper(); + }); + + test.afterAll(async () => { + await browser.closeBrowser(); + }); + + test("Verify zoom in increases canvas scale", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + const initialScale = await codeGraph.getCanvasScaling(); + await codeGraph.clickZoomIn(); + const newScale = await codeGraph.getCanvasScaling(); + + expect(newScale.scaleX).toBeGreaterThan(initialScale.scaleX); + expect(newScale.scaleY).toBeGreaterThan(initialScale.scaleY); + }); + + test("Verify zoom out decreases canvas scale", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + const initialScale = await codeGraph.getCanvasScaling(); + await codeGraph.clickZoomOut(); + const newScale = await codeGraph.getCanvasScaling(); + + expect(newScale.scaleX).toBeLessThan(initialScale.scaleX); + expect(newScale.scaleY).toBeLessThan(initialScale.scaleY); + }); + + test("Verify center button resets zoom to fit", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + // Zoom in first + await codeGraph.clickZoomIn(); + await codeGraph.clickZoomIn(); + await codeGraph.getCanvasScaling(); + + // Click center to reset + await codeGraph.clickCenter(); + const centeredScale = await codeGraph.getCanvasScaling(); + + // Scale should have changed (either increased or decreased depending on implementation) + expect(centeredScale.scaleX).toBeDefined(); + expect(centeredScale.scaleY).toBeDefined(); + }); + + test("Verify zoom controls remain functional after multiple interactions", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + const scales: number[] = []; + + // Perform multiple zoom operations + for (let i = 0; i < 3; i++) { + await codeGraph.clickZoomIn(); + const scale = await codeGraph.getCanvasScaling(); + scales.push(scale.scaleX); + } + + // Verify each zoom in increased the scale + for (let i = 1; i < scales.length; i++) { + expect(scales[i]).toBeGreaterThan(scales[i - 1]); + } + }); + + test("Verify zoom controls work after graph change", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + const initialScale = await codeGraph.getCanvasScaling(); + await codeGraph.clickZoomIn(); + const zoomedScale = await codeGraph.getCanvasScaling(); + + expect(zoomedScale.scaleX).toBeGreaterThan(initialScale.scaleX); + }); + + test("Verify continuous zoom maintains functionality", async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await codeGraph.selectGraph(GRAPHRAG_SDK); + + // Zoom in, then out, should return to similar scale + const initialScale = await codeGraph.getCanvasScaling(); + + await codeGraph.clickZoomIn(); + await codeGraph.clickZoomIn(); + await codeGraph.clickZoomOut(); + await codeGraph.clickZoomOut(); + + const finalScale = await codeGraph.getCanvasScaling(); + + // Should be approximately the same (within 5% tolerance for floating point operations) + expect(Math.abs(finalScale.scaleX - initialScale.scaleX) / initialScale.scaleX).toBeLessThan(0.05); + }); +});