diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml
deleted file mode 100644
index 368a8401..00000000
--- a/.github/workflows/lint_test_build.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-name: Lint, tests and build
-
-on:
- push:
- branches-ignore: ["main"]
-
-jobs:
- lint:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Set up Node.js
- uses: actions/setup-node@v3
- with:
- node-version: "22"
-
- - name: Install dependencies
- run: npm install
-
- - name: Lint
- run: npm run lint:check
-
- tests:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Set up Node.js
- uses: actions/setup-node@v3
- with:
- node-version: "22"
-
- - name: Install dependencies
- run: npm install
-
- - name: Tests
- run: npm run test
-
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Set up Node.js
- uses: actions/setup-node@v3
- with:
- node-version: "22"
-
- - name: Install dependencies
- run: npm install
-
- - name: Build
- run: npm run build
diff --git a/.github/workflows/lint_test_compile.yml b/.github/workflows/lint_test_compile.yml
new file mode 100644
index 00000000..cd0d3e4f
--- /dev/null
+++ b/.github/workflows/lint_test_compile.yml
@@ -0,0 +1,54 @@
+name: Lint, tests and build
+
+on:
+ push:
+ branches-ignore: ["main"]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2
+ with:
+ deno-version: "2.x"
+
+ - name: Deno check
+ run: deno check
+
+ - name: Deno lint
+ run: deno lint
+
+ - name: Deno fmt (check)
+ run: deno fmt --check
+
+ tests:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: denoland/setup-deno@v2
+ with:
+ deno-version: "2.x"
+
+ - name: Tests
+ run: deno task test
+
+ compile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: denoland/setup-deno@v2
+ with:
+ deno-version: "2.x"
+
+ - name: Compile
+ run: deno task compile:all
diff --git a/deno.json b/deno.json
index 3bf27b13..2eaa1708 100644
--- a/deno.json
+++ b/deno.json
@@ -7,9 +7,11 @@
"build:app": "deno task --config packages/app/deno.json build",
"compile:cli": "deno task --config packages/cli/deno.json compile",
"compile": "deno task --config packages/app/deno.json build && deno task --config packages/cli/deno.json compile",
- "compile:all": "deno task --config packages/app/deno.json build && deno task --config packages/cli/deno.json compile:linux && deno task --config packages/cli/deno.json compile:macos && deno task --config packages/cli/deno.json compile:windows",
- "test": "deno test",
- "lint": "deno lint",
- "fmt": "deno fmt"
+ "compile:all": "deno task --config packages/app/deno.json build && deno task --config packages/cli/deno.json 'compile-*'",
+ "test": "deno run -A npm:vitest",
+ "lint": "deno fmt && deno check &&deno lint --fix"
+ },
+ "imports": {
+ "vitest": "npm:vitest@^3.1.3"
}
}
diff --git a/deno.lock b/deno.lock
index f6ececb2..ba61a904 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1,37 +1,53 @@
{
"version": "5",
"specifiers": {
- "npm:@deno/vite-plugin@^1.0.4": "1.0.4_vite@6.3.5__picomatch@4.0.2",
- "npm:@inquirer/prompts@^7.5.0": "7.5.0",
+ "npm:@deno/vite-plugin@^1.0.4": "1.0.4_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0",
+ "npm:@inquirer/prompts@^7.5.0": "7.5.0_@types+node@22.12.0",
+ "npm:@radix-ui/react-dialog@^1.1.13": "1.1.13_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-dropdown-menu@^2.1.14": "2.1.14_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-label@^2.1.6": "2.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-scroll-area@^1.2.8": "1.2.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-separator@^1.1.6": "1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-slider@^1.3.4": "1.3.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-slot@^1.2.2": "1.2.2_@types+react@19.1.3_react@19.1.0",
+ "npm:@radix-ui/react-toast@^1.2.13": "1.2.13_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:@radix-ui/react-tooltip@^1.2.6": "1.2.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
"npm:@radix-ui/themes@^3.2.1": "3.2.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3",
- "npm:@tailwindcss/vite@^4.1.5": "4.1.5_vite@6.3.5__picomatch@4.0.2",
+ "npm:@tailwindcss/vite@^4.1.5": "4.1.5_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0",
"npm:@types/cytoscape-fcose@^2.2.4": "2.2.4",
"npm:@types/express@^5.0.1": "5.0.1",
+ "npm:@types/node@*": "22.12.0",
"npm:@types/react-dom@^19.1.3": "19.1.3_@types+react@19.1.3",
"npm:@types/react-router@^5.1.20": "5.1.20",
"npm:@types/react@^19.1.3": "19.1.3",
"npm:@types/yargs@^17.0.33": "17.0.33",
- "npm:@vitejs/plugin-react@^4.4.1": "4.4.1_vite@6.3.5__picomatch@4.0.2_@babel+core@7.27.1",
- "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.3",
+ "npm:@vitejs/plugin-react@^4.4.1": "4.4.1_vite@6.3.5__picomatch@4.0.2_@babel+core@7.27.1_@types+node@22.12.0",
+ "npm:class-variance-authority@~0.7.1": "0.7.1",
+ "npm:clsx@^2.1.1": "2.1.1",
"npm:cytoscape-fcose@^2.2.0": "2.2.0_cytoscape@3.31.1",
"npm:cytoscape@3.31.1": "3.31.1",
"npm:express@^5.1.0": "5.1.0",
+ "npm:find-unused-exports@*": "7.1.1",
"npm:glob@^11.0.2": "11.0.2",
"npm:http-proxy-middleware@^3.0.5": "3.0.5",
+ "npm:lucide-react@0.508": "0.508.0_react@19.1.0",
"npm:react-dom@^19.1.0": "19.1.0_react@19.1.0",
- "npm:react-icons@5.5.0": "5.5.0_react@19.1.0",
- "npm:react-resizable-panels@2.1.8": "2.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "npm:react-resizable-panels@^3.0.1": "3.0.1_react@19.1.0_react-dom@19.1.0__react@19.1.0",
"npm:react-router@^7.5.3": "7.5.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
- "npm:react-toastify@^11.0.5": "11.0.5_react@19.1.0_react-dom@19.1.0__react@19.1.0",
"npm:react@^19.1.0": "19.1.0",
+ "npm:shadcn@latest": "2.5.0_@babel+core@7.27.1_@types+node@22.12.0",
+ "npm:tailwind-merge@^3.2.0": "3.2.0",
"npm:tailwindcss@^4.1.5": "4.1.5",
"npm:tree-sitter-c-sharp@~0.23.1": "0.23.1_tree-sitter@0.22.4",
"npm:tree-sitter-python@~0.23.6": "0.23.6_tree-sitter@0.22.4",
"npm:tree-sitter@~0.22.4": "0.22.4",
+ "npm:tw-animate-css@^1.2.9": "1.2.9",
"npm:uuid@^11.1.0": "11.1.0",
- "npm:vite@*": "6.3.5_picomatch@4.0.2",
- "npm:vite@^6.3.5": "6.3.5_picomatch@4.0.2",
- "npm:vitest@^3.1.3": "3.1.3_vite@6.3.5__picomatch@4.0.2",
+ "npm:vite@*": "6.3.5_picomatch@4.0.2_@types+node@22.12.0",
+ "npm:vite@^6.3.5": "6.3.5_picomatch@4.0.2_@types+node@22.12.0",
+ "npm:vitest@*": "3.1.3_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0",
+ "npm:vitest@3.1.3": "3.1.3_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0",
+ "npm:vitest@^3.1.3": "3.1.3_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0",
"npm:yargs@^17.7.2": "17.7.2",
"npm:zod@^3.24.4": "3.24.4"
},
@@ -43,6 +59,10 @@
"@jridgewell/trace-mapping"
]
},
+ "@antfu/ni@23.3.1": {
+ "integrity": "sha512-C90iyzm/jLV7Lomv2UzwWUzRv9WZr1oRsFRKsX5HjQL4EXrbi9H/RtBkjCP+NF+ABZXUKpAa4F1dkoTaea4zHg==",
+ "bin": true
+ },
"@babel/code-frame@7.27.1": {
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dependencies": [
@@ -84,6 +104,12 @@
"jsesc"
]
},
+ "@babel/helper-annotate-as-pure@7.27.1": {
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dependencies": [
+ "@babel/types"
+ ]
+ },
"@babel/helper-compilation-targets@7.27.2": {
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dependencies": [
@@ -94,6 +120,26 @@
"semver"
]
},
+ "@babel/helper-create-class-features-plugin@7.27.1_@babel+core@7.27.1": {
+ "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/helper-annotate-as-pure",
+ "@babel/helper-member-expression-to-functions",
+ "@babel/helper-optimise-call-expression",
+ "@babel/helper-replace-supers",
+ "@babel/helper-skip-transparent-expression-wrappers",
+ "@babel/traverse",
+ "semver"
+ ]
+ },
+ "@babel/helper-member-expression-to-functions@7.27.1": {
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dependencies": [
+ "@babel/traverse",
+ "@babel/types"
+ ]
+ },
"@babel/helper-module-imports@7.27.1": {
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dependencies": [
@@ -110,9 +156,31 @@
"@babel/traverse"
]
},
+ "@babel/helper-optimise-call-expression@7.27.1": {
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dependencies": [
+ "@babel/types"
+ ]
+ },
"@babel/helper-plugin-utils@7.27.1": {
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="
},
+ "@babel/helper-replace-supers@7.27.1_@babel+core@7.27.1": {
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/helper-member-expression-to-functions",
+ "@babel/helper-optimise-call-expression",
+ "@babel/traverse"
+ ]
+ },
+ "@babel/helper-skip-transparent-expression-wrappers@7.27.1": {
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dependencies": [
+ "@babel/traverse",
+ "@babel/types"
+ ]
+ },
"@babel/helper-string-parser@7.27.1": {
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
},
@@ -136,6 +204,13 @@
],
"bin": true
},
+ "@babel/plugin-syntax-typescript@7.27.1_@babel+core@7.27.1": {
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/helper-plugin-utils"
+ ]
+ },
"@babel/plugin-transform-react-jsx-self@7.27.1_@babel+core@7.27.1": {
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dependencies": [
@@ -150,6 +225,17 @@
"@babel/helper-plugin-utils"
]
},
+ "@babel/plugin-transform-typescript@7.27.1_@babel+core@7.27.1": {
+ "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/helper-annotate-as-pure",
+ "@babel/helper-create-class-features-plugin",
+ "@babel/helper-plugin-utils",
+ "@babel/helper-skip-transparent-expression-wrappers",
+ "@babel/plugin-syntax-typescript"
+ ]
+ },
"@babel/template@7.27.2": {
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dependencies": [
@@ -177,10 +263,35 @@
"@babel/helper-validator-identifier"
]
},
+ "@bundled-es-modules/cookie@2.0.1": {
+ "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==",
+ "dependencies": [
+ "cookie@0.7.2"
+ ]
+ },
+ "@bundled-es-modules/statuses@1.0.1": {
+ "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==",
+ "dependencies": [
+ "statuses"
+ ]
+ },
+ "@bundled-es-modules/tough-cookie@0.1.6": {
+ "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==",
+ "dependencies": [
+ "@types/tough-cookie",
+ "tough-cookie"
+ ]
+ },
"@deno/vite-plugin@1.0.4_vite@6.3.5__picomatch@4.0.2": {
"integrity": "sha512-xg8YT8Wn2sGXSnJgiGTpBGX1Dov0c6fd1rAp8VsfrCUtyBRRWzwVMAnd3fQ4yq8h7LSVvJUxEFN4U421k/DQLA==",
"dependencies": [
- "vite"
+ "vite@6.3.5_picomatch@4.0.2"
+ ]
+ },
+ "@deno/vite-plugin@1.0.4_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0": {
+ "integrity": "sha512-xg8YT8Wn2sGXSnJgiGTpBGX1Dov0c6fd1rAp8VsfrCUtyBRRWzwVMAnd3fQ4yq8h7LSVvJUxEFN4U421k/DQLA==",
+ "dependencies": [
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0"
]
},
"@emnapi/core@1.4.3": {
@@ -351,50 +462,119 @@
"@floating-ui/utils@0.2.9": {
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
},
+ "@import-maps/resolve@2.0.0": {
+ "integrity": "sha512-RwzRTpmrrS6Q1ZhQExwuxJGK1Wqhv4stt+OF2JzS+uawewpwNyU7EJL1WpBex7aDiiGLs4FsXGkfUBdYuX7xiQ=="
+ },
"@inquirer/checkbox@4.1.5": {
"integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==",
"dependencies": [
- "@inquirer/core",
+ "@inquirer/core@10.1.10",
"@inquirer/figures",
- "@inquirer/type",
+ "@inquirer/type@3.0.6",
"ansi-escapes",
"yoctocolors-cjs"
]
},
+ "@inquirer/checkbox@4.1.5_@types+node@22.12.0": {
+ "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/figures",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
+ "ansi-escapes",
+ "yoctocolors-cjs"
+ ],
+ "optionalPeers": [
+ "@types/node"
+ ]
+ },
"@inquirer/confirm@5.1.9": {
"integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type"
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6"
+ ]
+ },
+ "@inquirer/confirm@5.1.9_@types+node@22.12.0": {
+ "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/core@10.1.10": {
"integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==",
"dependencies": [
"@inquirer/figures",
- "@inquirer/type",
+ "@inquirer/type@3.0.6",
+ "ansi-escapes",
+ "cli-width",
+ "mute-stream",
+ "signal-exit@4.1.0",
+ "wrap-ansi@6.2.0",
+ "yoctocolors-cjs"
+ ]
+ },
+ "@inquirer/core@10.1.10_@types+node@22.12.0": {
+ "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==",
+ "dependencies": [
+ "@inquirer/figures",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
"ansi-escapes",
"cli-width",
"mute-stream",
- "signal-exit",
+ "signal-exit@4.1.0",
"wrap-ansi@6.2.0",
"yoctocolors-cjs"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/editor@4.2.10": {
"integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type",
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6",
+ "external-editor"
+ ]
+ },
+ "@inquirer/editor@4.2.10_@types+node@22.12.0": {
+ "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
"external-editor"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/expand@4.0.12": {
"integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type",
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6",
+ "yoctocolors-cjs"
+ ]
+ },
+ "@inquirer/expand@4.0.12_@types+node@22.12.0": {
+ "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
"yoctocolors-cjs"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/figures@1.0.11": {
@@ -403,70 +583,171 @@
"@inquirer/input@4.1.9": {
"integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type"
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6"
+ ]
+ },
+ "@inquirer/input@4.1.9_@types+node@22.12.0": {
+ "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/number@3.0.12": {
"integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type"
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6"
+ ]
+ },
+ "@inquirer/number@3.0.12_@types+node@22.12.0": {
+ "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/password@4.0.12": {
"integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type",
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6",
"ansi-escapes"
]
},
+ "@inquirer/password@4.0.12_@types+node@22.12.0": {
+ "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
+ "ansi-escapes"
+ ],
+ "optionalPeers": [
+ "@types/node"
+ ]
+ },
"@inquirer/prompts@7.5.0": {
"integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==",
"dependencies": [
- "@inquirer/checkbox",
- "@inquirer/confirm",
- "@inquirer/editor",
- "@inquirer/expand",
- "@inquirer/input",
- "@inquirer/number",
- "@inquirer/password",
- "@inquirer/rawlist",
- "@inquirer/search",
- "@inquirer/select"
+ "@inquirer/checkbox@4.1.5",
+ "@inquirer/confirm@5.1.9",
+ "@inquirer/editor@4.2.10",
+ "@inquirer/expand@4.0.12",
+ "@inquirer/input@4.1.9",
+ "@inquirer/number@3.0.12",
+ "@inquirer/password@4.0.12",
+ "@inquirer/rawlist@4.1.0",
+ "@inquirer/search@3.0.12",
+ "@inquirer/select@4.2.0"
+ ]
+ },
+ "@inquirer/prompts@7.5.0_@types+node@22.12.0": {
+ "integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==",
+ "dependencies": [
+ "@inquirer/checkbox@4.1.5_@types+node@22.12.0",
+ "@inquirer/confirm@5.1.9_@types+node@22.12.0",
+ "@inquirer/editor@4.2.10_@types+node@22.12.0",
+ "@inquirer/expand@4.0.12_@types+node@22.12.0",
+ "@inquirer/input@4.1.9_@types+node@22.12.0",
+ "@inquirer/number@3.0.12_@types+node@22.12.0",
+ "@inquirer/password@4.0.12_@types+node@22.12.0",
+ "@inquirer/rawlist@4.1.0_@types+node@22.12.0",
+ "@inquirer/search@3.0.12_@types+node@22.12.0",
+ "@inquirer/select@4.2.0_@types+node@22.12.0",
+ "@types/node"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/rawlist@4.1.0": {
"integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==",
"dependencies": [
- "@inquirer/core",
- "@inquirer/type",
+ "@inquirer/core@10.1.10",
+ "@inquirer/type@3.0.6",
"yoctocolors-cjs"
]
},
+ "@inquirer/rawlist@4.1.0_@types+node@22.12.0": {
+ "integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
+ "yoctocolors-cjs"
+ ],
+ "optionalPeers": [
+ "@types/node"
+ ]
+ },
"@inquirer/search@3.0.12": {
"integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==",
"dependencies": [
- "@inquirer/core",
+ "@inquirer/core@10.1.10",
+ "@inquirer/figures",
+ "@inquirer/type@3.0.6",
+ "yoctocolors-cjs"
+ ]
+ },
+ "@inquirer/search@3.0.12_@types+node@22.12.0": {
+ "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
"@inquirer/figures",
- "@inquirer/type",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
"yoctocolors-cjs"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/select@4.2.0": {
"integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==",
"dependencies": [
- "@inquirer/core",
+ "@inquirer/core@10.1.10",
+ "@inquirer/figures",
+ "@inquirer/type@3.0.6",
+ "ansi-escapes",
+ "yoctocolors-cjs"
+ ]
+ },
+ "@inquirer/select@4.2.0_@types+node@22.12.0": {
+ "integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==",
+ "dependencies": [
+ "@inquirer/core@10.1.10_@types+node@22.12.0",
"@inquirer/figures",
- "@inquirer/type",
+ "@inquirer/type@3.0.6_@types+node@22.12.0",
+ "@types/node",
"ansi-escapes",
"yoctocolors-cjs"
+ ],
+ "optionalPeers": [
+ "@types/node"
]
},
"@inquirer/type@3.0.6": {
"integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA=="
},
+ "@inquirer/type@3.0.6_@types+node@22.12.0": {
+ "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==",
+ "dependencies": [
+ "@types/node"
+ ],
+ "optionalPeers": [
+ "@types/node"
+ ]
+ },
"@isaacs/cliui@8.0.2": {
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dependencies": [
@@ -502,6 +783,17 @@
"@jridgewell/sourcemap-codec"
]
},
+ "@mswjs/interceptors@0.37.6": {
+ "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==",
+ "dependencies": [
+ "@open-draft/deferred-promise",
+ "@open-draft/logger",
+ "@open-draft/until",
+ "is-node-process",
+ "outvariant",
+ "strict-event-emitter"
+ ]
+ },
"@napi-rs/wasm-runtime@0.2.9": {
"integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==",
"dependencies": [
@@ -510,6 +802,36 @@
"@tybys/wasm-util"
]
},
+ "@nodelib/fs.scandir@2.1.5": {
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": [
+ "@nodelib/fs.stat",
+ "run-parallel"
+ ]
+ },
+ "@nodelib/fs.stat@2.0.5": {
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+ },
+ "@nodelib/fs.walk@1.2.8": {
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": [
+ "@nodelib/fs.scandir",
+ "fastq"
+ ]
+ },
+ "@open-draft/deferred-promise@2.2.0": {
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="
+ },
+ "@open-draft/logger@0.3.0": {
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dependencies": [
+ "is-node-process",
+ "outvariant"
+ ]
+ },
+ "@open-draft/until@2.1.0": {
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
+ },
"@radix-ui/colors@3.0.0": {
"integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg=="
},
@@ -611,6 +933,20 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-arrow@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-arrow@1.1.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==",
"dependencies": [
@@ -757,6 +1093,23 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-collection@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-slot@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-collection@1.1.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==",
"dependencies": [
@@ -785,6 +1138,16 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-compose-refs@1.1.2_react@19.1.0": {
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"dependencies": [
@@ -833,6 +1196,16 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-context@1.1.2_react@19.1.0": {
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"dependencies": [
@@ -849,6 +1222,33 @@
"@types/react"
]
},
+ "@radix-ui/react-dialog@1.1.13_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-dismissable-layer@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-focus-guards@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-focus-scope@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-portal@1.1.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-presence@1.1.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-slot@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "aria-hidden",
+ "react",
+ "react-dom",
+ "react-remove-scroll@2.6.3_@types+react@19.1.3_react@19.1.0"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-dialog@1.1.13_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==",
"dependencies": [
@@ -897,6 +1297,16 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-direction@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-direction@1.1.1_react@19.1.0": {
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"dependencies": [
@@ -913,6 +1323,24 @@
"@types/react"
]
},
+ "@radix-ui/react-dismissable-layer@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-escape-keydown@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-dismissable-layer@1.1.9_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
"dependencies": [
@@ -943,6 +1371,26 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-dropdown-menu@2.1.14_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-menu@2.1.14_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-dropdown-menu@2.1.14_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==",
"dependencies": [
@@ -977,6 +1425,16 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-focus-guards@1.1.2_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-focus-guards@1.1.2_react@19.1.0": {
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
"dependencies": [
@@ -993,6 +1451,22 @@
"@types/react"
]
},
+ "@radix-ui/react-focus-scope@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-focus-scope@1.1.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==",
"dependencies": [
@@ -1089,6 +1563,17 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "dependencies": [
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-id@1.1.1_react@19.1.0": {
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"dependencies": [
@@ -1107,6 +1592,20 @@
"@types/react"
]
},
+ "@radix-ui/react-label@2.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-label@2.1.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==",
"dependencies": [
@@ -1129,6 +1628,37 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-menu@2.1.14_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-collection@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-direction@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-dismissable-layer@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-focus-guards@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-focus-scope@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-popper@1.2.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-portal@1.1.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-presence@1.1.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-roving-focus@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-slot@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "aria-hidden",
+ "react",
+ "react-dom",
+ "react-remove-scroll@2.6.3_@types+react@19.1.3_react@19.1.0"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-menu@2.1.14_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==",
"dependencies": [
@@ -1403,6 +1933,29 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-popper@1.2.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==",
+ "dependencies": [
+ "@floating-ui/react-dom",
+ "@radix-ui/react-arrow@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-rect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-size@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/rect",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-popper@1.2.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==",
"dependencies": [
@@ -1443,6 +1996,21 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-portal@1.1.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-portal@1.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==",
"dependencies": [
@@ -1467,6 +2035,21 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-presence@1.1.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-presence@1.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"dependencies": [
@@ -1491,6 +2074,20 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
+ "dependencies": [
+ "@radix-ui/react-slot@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-primitive@2.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
"dependencies": [
@@ -1577,6 +2174,28 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-roving-focus@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-collection@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-direction@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-roving-focus@1.1.9_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==",
"dependencies": [
@@ -1615,6 +2234,28 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-scroll-area@1.2.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-K5h1RkYA6M0Sn61BV5LQs686zqBsSC0sGzL4/Gw4mNnjzrQcGSc6YXfC6CRFNaGydSdv5+M8cb0eNsOGo0OXtQ==",
+ "dependencies": [
+ "@radix-ui/number",
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-direction@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-presence@1.1.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-scroll-area@1.2.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-K5h1RkYA6M0Sn61BV5LQs686zqBsSC0sGzL4/Gw4mNnjzrQcGSc6YXfC6CRFNaGydSdv5+M8cb0eNsOGo0OXtQ==",
"dependencies": [
@@ -1715,6 +2356,20 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-separator@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-separator@1.1.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==",
"dependencies": [
@@ -1737,6 +2392,30 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-slider@1.3.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-Cp6hEmQtRJFci285vkdIJ+HCDLTRDk+25VhFwa1fcubywjMUE3PynBgtN5RLudOgSCYMlT4jizCXdmV+8J7Y2w==",
+ "dependencies": [
+ "@radix-ui/number",
+ "@radix-ui/primitive",
+ "@radix-ui/react-collection@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-direction@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-previous@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-size@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-slider@1.3.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-Cp6hEmQtRJFci285vkdIJ+HCDLTRDk+25VhFwa1fcubywjMUE3PynBgtN5RLudOgSCYMlT4jizCXdmV+8J7Y2w==",
"dependencies": [
@@ -1775,8 +2454,19 @@
"react-dom"
],
"optionalPeers": [
- "@types/react",
- "@types/react-dom"
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-slot@1.2.2_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
]
},
"@radix-ui/react-slot@1.2.2_react@19.1.0": {
@@ -1867,6 +2557,31 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-toast@1.2.13_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-collection@1.1.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-dismissable-layer@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-portal@1.1.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-presence@1.1.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-visually-hidden@1.2.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-toast@1.2.13_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ==",
"dependencies": [
@@ -2005,6 +2720,31 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-tooltip@1.2.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-dismissable-layer@1.1.9_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-popper@1.2.6_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-portal@1.1.8_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-presence@1.1.4_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@radix-ui/react-slot@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-visually-hidden@1.2.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-tooltip@1.2.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==",
"dependencies": [
@@ -2049,6 +2789,16 @@
"@types/react-dom"
]
},
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-callback-ref@1.1.1_react@19.1.0": {
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"dependencies": [
@@ -2065,6 +2815,18 @@
"@types/react"
]
},
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "dependencies": [
+ "@radix-ui/react-use-effect-event@0.0.2_@types+react@19.1.3_react@19.1.0",
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-controllable-state@1.2.2_react@19.1.0": {
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"dependencies": [
@@ -2085,6 +2847,17 @@
"@types/react"
]
},
+ "@radix-ui/react-use-effect-event@0.0.2_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "dependencies": [
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-effect-event@0.0.2_react@19.1.0": {
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"dependencies": [
@@ -2103,6 +2876,17 @@
"@types/react"
]
},
+ "@radix-ui/react-use-escape-keydown@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "dependencies": [
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-escape-keydown@1.1.1_react@19.1.0": {
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"dependencies": [
@@ -2139,6 +2923,16 @@
"@types/react"
]
},
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-layout-effect@1.1.1_react@19.1.0": {
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"dependencies": [
@@ -2155,6 +2949,16 @@
"@types/react"
]
},
+ "@radix-ui/react-use-previous@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-previous@1.1.1_react@19.1.0": {
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"dependencies": [
@@ -2171,6 +2975,17 @@
"@types/react"
]
},
+ "@radix-ui/react-use-rect@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "dependencies": [
+ "@radix-ui/rect",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-rect@1.1.1_react@19.1.0": {
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"dependencies": [
@@ -2189,6 +3004,17 @@
"@types/react"
]
},
+ "@radix-ui/react-use-size@1.1.1_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "dependencies": [
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.3_react@19.1.0",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"@radix-ui/react-use-size@1.1.1_react@19.1.0": {
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"dependencies": [
@@ -2207,6 +3033,20 @@
"@types/react"
]
},
+ "@radix-ui/react-visually-hidden@1.2.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.2_@types+react@19.1.3_@types+react-dom@19.1.3__@types+react@19.1.3_react@19.1.0_react-dom@19.1.0__react@19.1.0",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
"@radix-ui/react-visually-hidden@1.2.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==",
"dependencies": [
@@ -2360,6 +3200,9 @@
"os": ["win32"],
"cpu": ["x64"]
},
+ "@sindresorhus/merge-streams@2.3.0": {
+ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="
+ },
"@tailwindcss/node@4.1.5": {
"integrity": "sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==",
"dependencies": [
@@ -2459,7 +3302,25 @@
"@tailwindcss/node",
"@tailwindcss/oxide",
"tailwindcss",
- "vite"
+ "vite@6.3.5_picomatch@4.0.2"
+ ]
+ },
+ "@tailwindcss/vite@4.1.5_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0": {
+ "integrity": "sha512-FE1stRoqdHSb7RxesMfCXE8icwI1W6zGE/512ae3ZDrpkQYTTYeSyUJPRCjZd8CwVAhpDUbi1YR8pcZioFJQ/w==",
+ "dependencies": [
+ "@tailwindcss/node",
+ "@tailwindcss/oxide",
+ "tailwindcss",
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0"
+ ]
+ },
+ "@ts-morph/common@0.19.0": {
+ "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==",
+ "dependencies": [
+ "fast-glob",
+ "minimatch@7.4.6",
+ "mkdirp",
+ "path-browserify"
]
},
"@tybys/wasm-util@0.9.0": {
@@ -2510,6 +3371,9 @@
"@types/node"
]
},
+ "@types/cookie@0.6.0": {
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
+ },
"@types/cytoscape-fcose@2.2.4": {
"integrity": "sha512-QwWtnT8HI9h+DHhG5krGc1ZY0Ex+cn85MvX96ZNAjSxuXiZDnjIZW/ypVkvvubTjIY4rSdkJY1D/Nsn8NDpmAw==",
"dependencies": [
@@ -2600,6 +3464,12 @@
"@types/send"
]
},
+ "@types/statuses@2.0.5": {
+ "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A=="
+ },
+ "@types/tough-cookie@4.0.5": {
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="
+ },
"@types/yargs-parser@21.0.3": {
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="
},
@@ -2617,7 +3487,18 @@
"@babel/plugin-transform-react-jsx-source",
"@types/babel__core",
"react-refresh",
- "vite"
+ "vite@6.3.5_picomatch@4.0.2"
+ ]
+ },
+ "@vitejs/plugin-react@4.4.1_vite@6.3.5__picomatch@4.0.2_@babel+core@7.27.1_@types+node@22.12.0": {
+ "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/plugin-transform-react-jsx-self",
+ "@babel/plugin-transform-react-jsx-source",
+ "@types/babel__core",
+ "react-refresh",
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0"
]
},
"@vitest/expect@3.1.3": {
@@ -2635,10 +3516,22 @@
"@vitest/spy",
"estree-walker",
"magic-string",
- "vite"
+ "vite@6.3.5_picomatch@4.0.2"
+ ],
+ "optionalPeers": [
+ "vite@6.3.5_picomatch@4.0.2"
+ ]
+ },
+ "@vitest/mocker@3.1.3_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0": {
+ "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==",
+ "dependencies": [
+ "@vitest/spy",
+ "estree-walker",
+ "magic-string",
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0"
],
"optionalPeers": [
- "vite"
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0"
]
},
"@vitest/pretty-format@3.1.3": {
@@ -2683,10 +3576,13 @@
"negotiator"
]
},
+ "agent-base@7.1.3": {
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
+ },
"ansi-escapes@4.3.2": {
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dependencies": [
- "type-fest"
+ "type-fest@0.21.3"
]
},
"ansi-regex@5.0.1": {
@@ -2704,6 +3600,12 @@
"ansi-styles@6.2.1": {
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="
},
+ "arg@5.0.2": {
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+ },
+ "argparse@2.0.1": {
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
"aria-hidden@1.2.4": {
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
"dependencies": [
@@ -2713,22 +3615,26 @@
"assertion-error@2.0.1": {
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="
},
- "autoprefixer@10.4.21_postcss@8.5.3": {
- "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "ast-types@0.16.1": {
+ "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==",
"dependencies": [
- "browserslist",
- "caniuse-lite",
- "fraction.js",
- "normalize-range",
- "picocolors",
- "postcss",
- "postcss-value-parser"
- ],
- "bin": true
+ "tslib"
+ ]
},
"balanced-match@1.0.2": {
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "base64-js@1.5.1": {
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+ },
+ "bl@5.1.0": {
+ "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+ "dependencies": [
+ "buffer",
+ "inherits",
+ "readable-stream"
+ ]
+ },
"body-parser@2.2.0": {
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"dependencies": [
@@ -2765,6 +3671,13 @@
],
"bin": true
},
+ "buffer@6.0.3": {
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dependencies": [
+ "base64-js",
+ "ieee754"
+ ]
+ },
"bytes@3.1.2": {
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
@@ -2785,6 +3698,9 @@
"get-intrinsic"
]
},
+ "callsites@3.1.0": {
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+ },
"caniuse-lite@1.0.30001717": {
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="
},
@@ -2798,15 +3714,33 @@
"pathval"
]
},
+ "chalk@5.4.1": {
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="
+ },
"chardet@0.7.0": {
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"check-error@2.1.1": {
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="
},
+ "class-variance-authority@0.7.1": {
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "dependencies": [
+ "clsx"
+ ]
+ },
"classnames@2.5.1": {
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
+ "cli-cursor@4.0.0": {
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "dependencies": [
+ "restore-cursor"
+ ]
+ },
+ "cli-spinners@2.9.2": {
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="
+ },
"cli-width@4.1.0": {
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="
},
@@ -2818,9 +3752,15 @@
"wrap-ansi@7.0.0"
]
},
+ "clone@1.0.4": {
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="
+ },
"clsx@2.1.1": {
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
},
+ "code-block-writer@12.0.0": {
+ "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="
+ },
"color-convert@2.0.1": {
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": [
@@ -2830,6 +3770,9 @@
"color-name@1.1.4": {
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
+ "commander@10.0.1": {
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="
+ },
"content-disposition@1.0.0": {
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"dependencies": [
@@ -2857,10 +3800,19 @@
"layout-base"
]
},
+ "cosmiconfig@8.3.6": {
+ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+ "dependencies": [
+ "import-fresh",
+ "js-yaml",
+ "parse-json",
+ "path-type@4.0.0"
+ ]
+ },
"cross-spawn@7.0.6": {
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": [
- "path-key",
+ "path-key@3.1.1",
"shebang-command",
"which"
]
@@ -2878,6 +3830,9 @@
"cytoscape@3.31.1": {
"integrity": "sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw=="
},
+ "data-uri-to-buffer@4.0.1": {
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
+ },
"debug@4.4.0": {
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": [
@@ -2887,6 +3842,15 @@
"deep-eql@5.0.2": {
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="
},
+ "deepmerge@4.3.1": {
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
+ },
+ "defaults@1.0.4": {
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dependencies": [
+ "clone"
+ ]
+ },
"depd@2.0.0": {
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
@@ -2896,6 +3860,9 @@
"detect-node-es@1.1.0": {
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
+ "diff@5.2.0": {
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="
+ },
"dunder-proto@1.0.1": {
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": [
@@ -2929,6 +3896,12 @@
"tapable"
]
},
+ "error-ex@1.3.2": {
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dependencies": [
+ "is-arrayish"
+ ]
+ },
"es-define-property@1.0.1": {
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
@@ -2982,6 +3955,10 @@
"escape-html@1.0.3": {
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
+ "esprima@4.0.1": {
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "bin": true
+ },
"estree-walker@3.0.3": {
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dependencies": [
@@ -2994,6 +3971,20 @@
"eventemitter3@4.0.7": {
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
+ "execa@7.2.0": {
+ "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+ "dependencies": [
+ "cross-spawn",
+ "get-stream",
+ "human-signals",
+ "is-stream",
+ "merge-stream",
+ "npm-run-path",
+ "onetime@6.0.0",
+ "signal-exit@3.0.7",
+ "strip-final-newline"
+ ]
+ },
"expect-type@1.2.1": {
"integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="
},
@@ -3037,6 +4028,22 @@
"tmp"
]
},
+ "fast-glob@3.3.3": {
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dependencies": [
+ "@nodelib/fs.stat",
+ "@nodelib/fs.walk",
+ "glob-parent",
+ "merge2",
+ "micromatch"
+ ]
+ },
+ "fastq@1.19.1": {
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dependencies": [
+ "reusify"
+ ]
+ },
"fdir@6.4.4_picomatch@4.0.2": {
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dependencies": [
@@ -3046,6 +4053,13 @@
"picomatch@4.0.2"
]
},
+ "fetch-blob@3.2.0": {
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "dependencies": [
+ "node-domexception",
+ "web-streams-polyfill"
+ ]
+ },
"fill-range@7.1.1": {
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": [
@@ -3063,6 +4077,19 @@
"statuses"
]
},
+ "find-unused-exports@7.1.1": {
+ "integrity": "sha512-vr8llp+VmD3/+SvjPoGzo/SyzJGeYFBSjOaQex2hvAomfzB4ke9BEUXc6lR+CjG5xd+SSvWylFhhdCctGzeV0g==",
+ "dependencies": [
+ "@babel/core",
+ "@import-maps/resolve",
+ "@types/babel__core",
+ "@types/node",
+ "arg",
+ "globby",
+ "kleur@4.1.5"
+ ],
+ "bin": true
+ },
"follow-redirects@1.15.9": {
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
@@ -3070,18 +4097,29 @@
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dependencies": [
"cross-spawn",
- "signal-exit"
+ "signal-exit@4.1.0"
+ ]
+ },
+ "formdata-polyfill@4.0.10": {
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "dependencies": [
+ "fetch-blob"
]
},
"forwarded@0.2.0": {
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
- "fraction.js@4.3.7": {
- "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="
- },
"fresh@2.0.0": {
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="
},
+ "fs-extra@11.3.0": {
+ "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
+ "dependencies": [
+ "graceful-fs",
+ "jsonfile",
+ "universalify@2.0.1"
+ ]
+ },
"fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"os": ["darwin"],
@@ -3114,6 +4152,9 @@
"get-nonce@1.0.1": {
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
},
+ "get-own-enumerable-keys@1.0.0": {
+ "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="
+ },
"get-proto@1.0.1": {
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": [
@@ -3121,12 +4162,21 @@
"es-object-atoms"
]
},
+ "get-stream@6.0.1": {
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
+ },
+ "glob-parent@5.1.2": {
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": [
+ "is-glob"
+ ]
+ },
"glob@11.0.2": {
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
"dependencies": [
"foreground-child",
"jackspeak",
- "minimatch",
+ "minimatch@10.0.1",
"minipass",
"package-json-from-dist",
"path-scurry"
@@ -3136,12 +4186,26 @@
"globals@11.12.0": {
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
+ "globby@14.1.0": {
+ "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+ "dependencies": [
+ "@sindresorhus/merge-streams",
+ "fast-glob",
+ "ignore",
+ "path-type@6.0.0",
+ "slash",
+ "unicorn-magic"
+ ]
+ },
"gopd@1.2.0": {
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"graceful-fs@4.2.11": {
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
+ "graphql@16.11.0": {
+ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="
+ },
"has-symbols@1.1.0": {
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
@@ -3151,6 +4215,9 @@
"function-bind"
]
},
+ "headers-polyfill@4.0.3": {
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="
+ },
"http-errors@2.0.0": {
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": [
@@ -3180,6 +4247,16 @@
"requires-port"
]
},
+ "https-proxy-agent@6.2.1": {
+ "integrity": "sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA==",
+ "dependencies": [
+ "agent-base",
+ "debug"
+ ]
+ },
+ "human-signals@4.3.1": {
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="
+ },
"iconv-lite@0.4.24": {
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": [
@@ -3192,12 +4269,28 @@
"safer-buffer"
]
},
+ "ieee754@1.2.1": {
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+ },
+ "ignore@7.0.4": {
+ "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="
+ },
+ "import-fresh@3.3.1": {
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dependencies": [
+ "parent-module",
+ "resolve-from"
+ ]
+ },
"inherits@2.0.4": {
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipaddr.js@1.9.1": {
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
+ "is-arrayish@0.2.1": {
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+ },
"is-extglob@2.1.1": {
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
},
@@ -3210,15 +4303,33 @@
"is-extglob"
]
},
+ "is-interactive@2.0.0": {
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="
+ },
+ "is-node-process@1.2.0": {
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="
+ },
"is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
+ "is-obj@3.0.0": {
+ "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="
+ },
"is-plain-object@5.0.0": {
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"is-promise@4.0.0": {
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
},
+ "is-regexp@3.1.0": {
+ "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="
+ },
+ "is-stream@3.0.0": {
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="
+ },
+ "is-unicode-supported@1.3.0": {
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="
+ },
"isexe@2.0.0": {
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
@@ -3235,14 +4346,39 @@
"js-tokens@4.0.0": {
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
+ "js-yaml@4.1.0": {
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dependencies": [
+ "argparse"
+ ],
+ "bin": true
+ },
"jsesc@3.1.0": {
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"bin": true
},
+ "json-parse-even-better-errors@2.3.1": {
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
"json5@2.2.3": {
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": true
},
+ "jsonfile@6.1.0": {
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dependencies": [
+ "universalify@2.0.1"
+ ],
+ "optionalDependencies": [
+ "graceful-fs"
+ ]
+ },
+ "kleur@3.0.3": {
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="
+ },
+ "kleur@4.1.5": {
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="
+ },
"layout-base@2.0.1": {
"integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
},
@@ -3314,6 +4450,16 @@
"lightningcss-win32-x64-msvc"
]
},
+ "lines-and-columns@1.2.4": {
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "log-symbols@5.1.0": {
+ "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
+ "dependencies": [
+ "chalk",
+ "is-unicode-supported"
+ ]
+ },
"loupe@3.1.3": {
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="
},
@@ -3326,6 +4472,12 @@
"yallist"
]
},
+ "lucide-react@0.508.0_react@19.1.0": {
+ "integrity": "sha512-gcP16PnexqtOFrTtv98kVsGzTfnbPekzZiQfByi2S89xfk7E/4uKE1USZqccIp58v42LqkO7MuwpCqshwSrJCg==",
+ "dependencies": [
+ "react"
+ ]
+ },
"magic-string@0.30.17": {
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dependencies": [
@@ -3341,6 +4493,12 @@
"merge-descriptors@2.0.0": {
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="
},
+ "merge-stream@2.0.0": {
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+ },
+ "merge2@1.4.1": {
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
+ },
"micromatch@4.0.8": {
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": [
@@ -3357,18 +4515,87 @@
"mime-db"
]
},
+ "mimic-fn@2.1.0": {
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
+ },
+ "mimic-fn@4.0.0": {
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="
+ },
"minimatch@10.0.1": {
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dependencies": [
"brace-expansion"
]
},
+ "minimatch@7.4.6": {
+ "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==",
+ "dependencies": [
+ "brace-expansion"
+ ]
+ },
+ "minimist@1.2.8": {
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
+ },
"minipass@7.1.2": {
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
},
+ "mkdirp@2.1.6": {
+ "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==",
+ "bin": true
+ },
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
+ "msw@2.7.6": {
+ "integrity": "sha512-P+rwn43ktxN8ghcl8q+hSAUlEi0PbJpDhGmDkw4zeUnRj3hBCVynWD+dTu38yLYKCE9ZF1OYcvpy7CTBRcqkZA==",
+ "dependencies": [
+ "@bundled-es-modules/cookie",
+ "@bundled-es-modules/statuses",
+ "@bundled-es-modules/tough-cookie",
+ "@inquirer/confirm@5.1.9",
+ "@mswjs/interceptors",
+ "@open-draft/deferred-promise",
+ "@open-draft/until",
+ "@types/cookie",
+ "@types/statuses",
+ "graphql",
+ "headers-polyfill",
+ "is-node-process",
+ "outvariant",
+ "path-to-regexp@6.3.0",
+ "picocolors",
+ "strict-event-emitter",
+ "type-fest@4.41.0",
+ "yargs"
+ ],
+ "scripts": true,
+ "bin": true
+ },
+ "msw@2.7.6_@types+node@22.12.0": {
+ "integrity": "sha512-P+rwn43ktxN8ghcl8q+hSAUlEi0PbJpDhGmDkw4zeUnRj3hBCVynWD+dTu38yLYKCE9ZF1OYcvpy7CTBRcqkZA==",
+ "dependencies": [
+ "@bundled-es-modules/cookie",
+ "@bundled-es-modules/statuses",
+ "@bundled-es-modules/tough-cookie",
+ "@inquirer/confirm@5.1.9_@types+node@22.12.0",
+ "@mswjs/interceptors",
+ "@open-draft/deferred-promise",
+ "@open-draft/until",
+ "@types/cookie",
+ "@types/statuses",
+ "graphql",
+ "headers-polyfill",
+ "is-node-process",
+ "outvariant",
+ "path-to-regexp@6.3.0",
+ "picocolors",
+ "strict-event-emitter",
+ "type-fest@4.41.0",
+ "yargs"
+ ],
+ "scripts": true,
+ "bin": true
+ },
"mute-stream@2.0.0": {
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="
},
@@ -3382,6 +4609,18 @@
"node-addon-api@8.3.1": {
"integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA=="
},
+ "node-domexception@1.0.0": {
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": true
+ },
+ "node-fetch@3.3.2": {
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "dependencies": [
+ "data-uri-to-buffer",
+ "fetch-blob",
+ "formdata-polyfill"
+ ]
+ },
"node-gyp-build@4.8.4": {
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"bin": true
@@ -3389,8 +4628,11 @@
"node-releases@2.0.19": {
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
},
- "normalize-range@0.1.2": {
- "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="
+ "npm-run-path@5.3.0": {
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dependencies": [
+ "path-key@4.0.0"
+ ]
},
"object-inspect@1.13.4": {
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
@@ -3407,18 +4649,68 @@
"wrappy"
]
},
+ "onetime@5.1.2": {
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dependencies": [
+ "mimic-fn@2.1.0"
+ ]
+ },
+ "onetime@6.0.0": {
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dependencies": [
+ "mimic-fn@4.0.0"
+ ]
+ },
+ "ora@6.3.1": {
+ "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==",
+ "dependencies": [
+ "chalk",
+ "cli-cursor",
+ "cli-spinners",
+ "is-interactive",
+ "is-unicode-supported",
+ "log-symbols",
+ "stdin-discarder",
+ "strip-ansi@7.1.0",
+ "wcwidth"
+ ]
+ },
"os-tmpdir@1.0.2": {
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="
},
+ "outvariant@1.4.3": {
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="
+ },
"package-json-from-dist@1.0.1": {
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
},
+ "parent-module@1.0.1": {
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dependencies": [
+ "callsites"
+ ]
+ },
+ "parse-json@5.2.0": {
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dependencies": [
+ "@babel/code-frame",
+ "error-ex",
+ "json-parse-even-better-errors",
+ "lines-and-columns"
+ ]
+ },
"parseurl@1.3.3": {
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
+ "path-browserify@1.0.1": {
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
+ },
"path-key@3.1.1": {
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
},
+ "path-key@4.0.0": {
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="
+ },
"path-scurry@2.0.0": {
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dependencies": [
@@ -3426,9 +4718,18 @@
"minipass"
]
},
+ "path-to-regexp@6.3.0": {
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
+ },
"path-to-regexp@8.2.0": {
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="
},
+ "path-type@4.0.0": {
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
+ },
+ "path-type@6.0.0": {
+ "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="
+ },
"pathe@2.0.3": {
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
},
@@ -3444,9 +4745,6 @@
"picomatch@4.0.2": {
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
},
- "postcss-value-parser@4.2.0": {
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
- },
"postcss@8.5.3": {
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dependencies": [
@@ -3455,6 +4753,13 @@
"source-map-js"
]
},
+ "prompts@2.4.2": {
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dependencies": [
+ "kleur@3.0.3",
+ "sisteransi"
+ ]
+ },
"proxy-addr@2.0.7": {
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": [
@@ -3462,12 +4767,27 @@
"ipaddr.js"
]
},
+ "psl@1.15.0": {
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dependencies": [
+ "punycode"
+ ]
+ },
+ "punycode@2.3.1": {
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
+ },
"qs@6.14.0": {
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dependencies": [
"side-channel"
]
},
+ "querystringify@2.2.0": {
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+ },
+ "queue-microtask@1.2.3": {
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
+ },
"radix-ui@1.4.1_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
"integrity": "sha512-xG1aeAgvAiVglxHXMpHyk7RqLGnc8VnDUZvzpE8rZ8GAhuGeNm/+7YbIwCV+rKKRpsSgxdnvfUObQidK2EnTzw==",
"dependencies": [
@@ -3617,15 +4937,21 @@
"scheduler"
]
},
- "react-icons@5.5.0_react@19.1.0": {
- "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
- "dependencies": [
- "react"
- ]
- },
"react-refresh@0.17.0": {
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="
},
+ "react-remove-scroll-bar@2.3.8_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "react-style-singleton@2.2.3_@types+react@19.1.3_react@19.1.0",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"react-remove-scroll-bar@2.3.8_react@19.1.0": {
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"dependencies": [
@@ -3646,6 +4972,21 @@
"@types/react"
]
},
+ "react-remove-scroll@2.6.3_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "react-remove-scroll-bar@2.3.8_@types+react@19.1.3_react@19.1.0",
+ "react-style-singleton@2.2.3_@types+react@19.1.3_react@19.1.0",
+ "tslib",
+ "use-callback-ref@1.3.3_@types+react@19.1.3_react@19.1.0",
+ "use-sidecar@1.1.3_@types+react@19.1.3_react@19.1.0"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"react-remove-scroll@2.6.3_react@19.1.0": {
"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
"dependencies": [
@@ -3672,8 +5013,8 @@
"@types/react"
]
},
- "react-resizable-panels@2.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
- "integrity": "sha512-oDvD0sw34Ecx00cQFLiRJpAE2fCgNLBr8DMrBzkrsaUiLpAycIQoY3eAWfMblDql3pTIMZ60wJ/P89RO1htM2w==",
+ "react-resizable-panels@3.0.1_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
+ "integrity": "sha512-6ruCEyw0iqXRcXEktPQn1HL553DNhrdLisCyEdSpzhkmo9bPqZxskJZ+aGeFqJ1qPvIWxuAiag82kvLSb2JZTQ==",
"dependencies": [
"react",
"react-dom"
@@ -3692,6 +5033,18 @@
"react-dom"
]
},
+ "react-style-singleton@2.2.3_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "dependencies": [
+ "@types/react",
+ "get-nonce",
+ "react",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"react-style-singleton@2.2.3_react@19.1.0": {
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"dependencies": [
@@ -3712,16 +5065,26 @@
"@types/react"
]
},
- "react-toastify@11.0.5_react@19.1.0_react-dom@19.1.0__react@19.1.0": {
- "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
+ "react@19.1.0": {
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="
+ },
+ "readable-stream@3.6.2": {
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": [
- "clsx",
- "react",
- "react-dom"
+ "inherits",
+ "string_decoder",
+ "util-deprecate"
]
},
- "react@19.1.0": {
- "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="
+ "recast@0.23.11": {
+ "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==",
+ "dependencies": [
+ "ast-types",
+ "esprima",
+ "source-map",
+ "tiny-invariant",
+ "tslib"
+ ]
},
"require-directory@2.1.1": {
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
@@ -3729,6 +5092,19 @@
"requires-port@1.0.0": {
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "resolve-from@4.0.0": {
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+ },
+ "restore-cursor@4.0.0": {
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "dependencies": [
+ "onetime@5.1.2",
+ "signal-exit@3.0.7"
+ ]
+ },
+ "reusify@1.1.0": {
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="
+ },
"rollup@4.40.2": {
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
"dependencies": [
@@ -3766,7 +5142,13 @@
"depd",
"is-promise",
"parseurl",
- "path-to-regexp"
+ "path-to-regexp@8.2.0"
+ ]
+ },
+ "run-parallel@1.2.0": {
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dependencies": [
+ "queue-microtask"
]
},
"safe-buffer@5.2.1": {
@@ -3813,6 +5195,64 @@
"setprototypeof@1.2.0": {
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "shadcn@2.5.0_@babel+core@7.27.1": {
+ "integrity": "sha512-kqbTpDWIYE8spytMiJFoKyzOTHb+DzNLjMXzz+ifDFTBgTPv6gIydmirTZwwfFb23XvenrTSVHsZHUjGsllcHw==",
+ "dependencies": [
+ "@antfu/ni",
+ "@babel/core",
+ "@babel/parser",
+ "@babel/plugin-transform-typescript",
+ "commander",
+ "cosmiconfig",
+ "deepmerge",
+ "diff",
+ "execa",
+ "fast-glob",
+ "fs-extra",
+ "https-proxy-agent",
+ "kleur@4.1.5",
+ "msw@2.7.6",
+ "node-fetch",
+ "ora",
+ "postcss",
+ "prompts",
+ "recast",
+ "stringify-object",
+ "ts-morph",
+ "tsconfig-paths",
+ "zod"
+ ],
+ "bin": true
+ },
+ "shadcn@2.5.0_@babel+core@7.27.1_@types+node@22.12.0": {
+ "integrity": "sha512-kqbTpDWIYE8spytMiJFoKyzOTHb+DzNLjMXzz+ifDFTBgTPv6gIydmirTZwwfFb23XvenrTSVHsZHUjGsllcHw==",
+ "dependencies": [
+ "@antfu/ni",
+ "@babel/core",
+ "@babel/parser",
+ "@babel/plugin-transform-typescript",
+ "commander",
+ "cosmiconfig",
+ "deepmerge",
+ "diff",
+ "execa",
+ "fast-glob",
+ "fs-extra",
+ "https-proxy-agent",
+ "kleur@4.1.5",
+ "msw@2.7.6_@types+node@22.12.0",
+ "node-fetch",
+ "ora",
+ "postcss",
+ "prompts",
+ "recast",
+ "stringify-object",
+ "ts-morph",
+ "tsconfig-paths",
+ "zod"
+ ],
+ "bin": true
+ },
"shebang-command@2.0.0": {
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": [
@@ -3861,12 +5301,24 @@
"siginfo@2.0.0": {
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="
},
+ "signal-exit@3.0.7": {
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
"signal-exit@4.1.0": {
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
},
+ "sisteransi@1.0.5": {
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
+ },
+ "slash@5.1.0": {
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="
+ },
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
+ "source-map@0.6.1": {
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
"stackback@0.0.2": {
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="
},
@@ -3876,6 +5328,15 @@
"std-env@3.9.0": {
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="
},
+ "stdin-discarder@0.1.0": {
+ "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
+ "dependencies": [
+ "bl"
+ ]
+ },
+ "strict-event-emitter@0.5.1": {
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="
+ },
"string-width@4.2.3": {
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": [
@@ -3892,6 +5353,20 @@
"strip-ansi@7.1.0"
]
},
+ "string_decoder@1.3.0": {
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": [
+ "safe-buffer"
+ ]
+ },
+ "stringify-object@5.0.0": {
+ "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==",
+ "dependencies": [
+ "get-own-enumerable-keys",
+ "is-obj",
+ "is-regexp"
+ ]
+ },
"strip-ansi@6.0.1": {
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": [
@@ -3904,12 +5379,24 @@
"ansi-regex@6.1.0"
]
},
+ "strip-bom@3.0.0": {
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="
+ },
+ "strip-final-newline@3.0.0": {
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="
+ },
+ "tailwind-merge@3.2.0": {
+ "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA=="
+ },
"tailwindcss@4.1.5": {
"integrity": "sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA=="
},
"tapable@2.2.1": {
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
},
+ "tiny-invariant@1.3.3": {
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
+ },
"tinybench@2.9.0": {
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="
},
@@ -3947,6 +5434,15 @@
"toidentifier@1.0.1": {
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
+ "tough-cookie@4.1.4": {
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dependencies": [
+ "psl",
+ "punycode",
+ "universalify@0.2.0",
+ "url-parse"
+ ]
+ },
"tree-sitter-c-sharp@0.23.1_tree-sitter@0.22.4": {
"integrity": "sha512-9zZ4FlcTRWWfRf6f4PgGhG8saPls6qOOt75tDfX7un9vQZJmARjPrAC6yBNCX2T/VKcCjIDbgq0evFaB3iGhQw==",
"dependencies": [
@@ -3979,15 +5475,36 @@
],
"scripts": true
},
+ "ts-morph@18.0.0": {
+ "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==",
+ "dependencies": [
+ "@ts-morph/common",
+ "code-block-writer"
+ ]
+ },
+ "tsconfig-paths@4.2.0": {
+ "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+ "dependencies": [
+ "json5",
+ "minimist",
+ "strip-bom"
+ ]
+ },
"tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"turbo-stream@2.4.0": {
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
},
+ "tw-animate-css@1.2.9": {
+ "integrity": "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg=="
+ },
"type-fest@0.21.3": {
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
},
+ "type-fest@4.41.0": {
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="
+ },
"type-is@2.0.1": {
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"dependencies": [
@@ -3999,6 +5516,15 @@
"undici-types@6.20.0": {
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
+ "unicorn-magic@0.3.0": {
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="
+ },
+ "universalify@0.2.0": {
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="
+ },
+ "universalify@2.0.1": {
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="
+ },
"unpipe@1.0.0": {
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
@@ -4011,6 +5537,24 @@
],
"bin": true
},
+ "url-parse@1.5.10": {
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dependencies": [
+ "querystringify",
+ "requires-port"
+ ]
+ },
+ "use-callback-ref@1.3.3_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"use-callback-ref@1.3.3_react@19.1.0": {
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"dependencies": [
@@ -4029,6 +5573,18 @@
"@types/react"
]
},
+ "use-sidecar@1.1.3_@types+react@19.1.3_react@19.1.0": {
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "dependencies": [
+ "@types/react",
+ "detect-node-es",
+ "react",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"use-sidecar@1.1.3_react@19.1.0": {
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"dependencies": [
@@ -4055,6 +5611,9 @@
"react"
]
},
+ "util-deprecate@1.0.2": {
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
"uuid@11.1.0": {
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"bin": true
@@ -4069,7 +5628,18 @@
"debug",
"es-module-lexer",
"pathe",
- "vite"
+ "vite@6.3.5_picomatch@4.0.2"
+ ],
+ "bin": true
+ },
+ "vite-node@3.1.3_@types+node@22.12.0": {
+ "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==",
+ "dependencies": [
+ "cac",
+ "debug",
+ "es-module-lexer",
+ "pathe",
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0"
],
"bin": true
},
@@ -4088,11 +5658,58 @@
],
"bin": true
},
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0": {
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+ "dependencies": [
+ "@types/node",
+ "esbuild",
+ "fdir",
+ "picomatch@4.0.2",
+ "postcss",
+ "rollup",
+ "tinyglobby"
+ ],
+ "optionalDependencies": [
+ "fsevents"
+ ],
+ "optionalPeers": [
+ "@types/node"
+ ],
+ "bin": true
+ },
"vitest@3.1.3_vite@6.3.5__picomatch@4.0.2": {
"integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
"dependencies": [
"@vitest/expect",
- "@vitest/mocker",
+ "@vitest/mocker@3.1.3_vite@6.3.5__picomatch@4.0.2",
+ "@vitest/pretty-format",
+ "@vitest/runner",
+ "@vitest/snapshot",
+ "@vitest/spy",
+ "@vitest/utils",
+ "chai",
+ "debug",
+ "expect-type",
+ "magic-string",
+ "pathe",
+ "std-env",
+ "tinybench",
+ "tinyexec",
+ "tinyglobby",
+ "tinypool",
+ "tinyrainbow",
+ "vite@6.3.5_picomatch@4.0.2",
+ "vite-node@3.1.3",
+ "why-is-node-running"
+ ],
+ "bin": true
+ },
+ "vitest@3.1.3_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0": {
+ "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
+ "dependencies": [
+ "@types/node",
+ "@vitest/expect",
+ "@vitest/mocker@3.1.3_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0",
"@vitest/pretty-format",
"@vitest/runner",
"@vitest/snapshot",
@@ -4109,12 +5726,24 @@
"tinyglobby",
"tinypool",
"tinyrainbow",
- "vite",
- "vite-node",
+ "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0",
+ "vite-node@3.1.3_@types+node@22.12.0",
"why-is-node-running"
],
+ "optionalPeers": [
+ "@types/node"
+ ],
"bin": true
},
+ "wcwidth@1.0.1": {
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dependencies": [
+ "defaults"
+ ]
+ },
+ "web-streams-polyfill@3.3.3": {
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
+ },
"which@2.0.2": {
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": [
@@ -4186,10 +5815,22 @@
}
},
"workspace": {
+ "dependencies": [
+ "npm:vitest@^3.1.3"
+ ],
"members": {
"packages/app": {
"dependencies": [
"npm:@deno/vite-plugin@^1.0.4",
+ "npm:@radix-ui/react-dialog@^1.1.13",
+ "npm:@radix-ui/react-dropdown-menu@^2.1.14",
+ "npm:@radix-ui/react-label@^2.1.6",
+ "npm:@radix-ui/react-scroll-area@^1.2.8",
+ "npm:@radix-ui/react-separator@^1.1.6",
+ "npm:@radix-ui/react-slider@^1.3.4",
+ "npm:@radix-ui/react-slot@^1.2.2",
+ "npm:@radix-ui/react-toast@^1.2.13",
+ "npm:@radix-ui/react-tooltip@^1.2.6",
"npm:@radix-ui/themes@^3.2.1",
"npm:@tailwindcss/vite@^4.1.5",
"npm:@types/cytoscape-fcose@^2.2.4",
@@ -4197,16 +5838,18 @@
"npm:@types/react-router@^5.1.20",
"npm:@types/react@^19.1.3",
"npm:@vitejs/plugin-react@^4.4.1",
- "npm:autoprefixer@^10.4.21",
+ "npm:class-variance-authority@~0.7.1",
+ "npm:clsx@^2.1.1",
"npm:cytoscape-fcose@^2.2.0",
"npm:cytoscape@3.31.1",
+ "npm:lucide-react@0.508",
"npm:react-dom@^19.1.0",
- "npm:react-icons@5.5.0",
- "npm:react-resizable-panels@2.1.8",
+ "npm:react-resizable-panels@^3.0.1",
"npm:react-router@^7.5.3",
- "npm:react-toastify@^11.0.5",
"npm:react@^19.1.0",
+ "npm:tailwind-merge@^3.2.0",
"npm:tailwindcss@^4.1.5",
+ "npm:tw-animate-css@^1.2.9",
"npm:vite@^6.3.5"
]
},
@@ -4222,7 +5865,6 @@
"npm:tree-sitter-python@~0.23.6",
"npm:tree-sitter@~0.22.4",
"npm:uuid@^11.1.0",
- "npm:vitest@^3.1.3",
"npm:yargs@^17.7.2",
"npm:zod@^3.24.4"
]
diff --git a/packages/app/components.json b/packages/app/components.json
new file mode 100644
index 00000000..285033d3
--- /dev/null
+++ b/packages/app/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/styles/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/packages/app/deno.json b/packages/app/deno.json
index 23d155d5..c964995b 100644
--- a/packages/app/deno.json
+++ b/packages/app/deno.json
@@ -5,6 +5,15 @@
"imports": {
"@napi/shared": "../shared/src/index.ts",
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4",
+ "@radix-ui/react-dialog": "npm:@radix-ui/react-dialog@^1.1.13",
+ "@radix-ui/react-dropdown-menu": "npm:@radix-ui/react-dropdown-menu@^2.1.14",
+ "@radix-ui/react-label": "npm:@radix-ui/react-label@^2.1.6",
+ "@radix-ui/react-scroll-area": "npm:@radix-ui/react-scroll-area@^1.2.8",
+ "@radix-ui/react-separator": "npm:@radix-ui/react-separator@^1.1.6",
+ "@radix-ui/react-slider": "npm:@radix-ui/react-slider@^1.3.4",
+ "@radix-ui/react-slot": "npm:@radix-ui/react-slot@^1.2.2",
+ "@radix-ui/react-toast": "npm:@radix-ui/react-toast@^1.2.13",
+ "@radix-ui/react-tooltip": "npm:@radix-ui/react-tooltip@^1.2.6",
"@radix-ui/themes": "npm:@radix-ui/themes@^3.2.1",
"@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.5",
"@types/cytoscape-fcose": "npm:@types/cytoscape-fcose@^2.2.4",
@@ -12,16 +21,18 @@
"@types/react-dom": "npm:@types/react-dom@^19.1.3",
"@types/react-router": "npm:@types/react-router@^5.1.20",
"@vitejs/plugin-react": "npm:@vitejs/plugin-react@^4.4.1",
- "autoprefixer": "npm:autoprefixer@^10.4.21",
+ "class-variance-authority": "npm:class-variance-authority@^0.7.1",
+ "clsx": "npm:clsx@^2.1.1",
"cytoscape-fcose": "npm:cytoscape-fcose@^2.2.0",
+ "lucide-react": "npm:lucide-react@^0.508.0",
"react": "npm:react@^19.1.0",
"react-dom": "npm:react-dom@^19.1.0",
- "react-icons": "npm:react-icons@5.5.0",
"react-router": "npm:react-router@^7.5.3",
- "react-toastify": "npm:react-toastify@^11.0.5",
- "react-resizable-panels": "npm:react-resizable-panels@2.1.8",
+ "react-resizable-panels": "npm:react-resizable-panels@^3.0.1",
"cytoscape": "npm:cytoscape@3.31.1",
+ "tailwind-merge": "npm:tailwind-merge@^3.2.0",
"tailwindcss": "npm:tailwindcss@^4.1.5",
+ "tw-animate-css": "npm:tw-animate-css@^1.2.9",
"vite": "npm:vite@^6.3.5"
},
"tasks": {
diff --git a/packages/app/index.html b/packages/app/index.html
index be954c35..723f8983 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -3,6 +3,7 @@
+
NanoAPI
diff --git a/packages/app/src/components/Cytoscape/ControlExtensions/FiltersExtension.tsx b/packages/app/src/components/Cytoscape/ControlExtensions/FiltersExtension.tsx
deleted file mode 100644
index 227cb303..00000000
--- a/packages/app/src/components/Cytoscape/ControlExtensions/FiltersExtension.tsx
+++ /dev/null
@@ -1,301 +0,0 @@
-import { type MouseEvent, useEffect, useRef, useState } from "react";
-import { useParams } from "react-router";
-import { Button, Checkbox, DropdownMenu } from "@radix-ui/themes";
-import { LuChevronUp } from "react-icons/lu";
-import type { Core } from "cytoscape";
-import { toast } from "react-toastify";
-import { MdFilterAlt } from "react-icons/md";
-
-interface FiltersType {
- showExternal: boolean;
- showInternal: boolean;
- showVariables: boolean;
- showFunctions: boolean;
- showClasses: boolean;
-}
-
-export default function FiltersExtension(props: {
- busy: boolean;
- cy: Core;
- onLayout: () => void;
-}) {
- const initialized = useRef(false);
- const [filters, setFilters] = useState({
- showExternal: true,
- showInternal: true,
- showVariables: true,
- showFunctions: true,
- showClasses: true,
- });
- const { file } = useParams();
-
- function checkFiltersSet() {
- if (!filters) return false;
-
- // For boolean filters, assuming they take the form "show X"
- // then we need to return false if they are set to their default value
- if (!filters.showExternal) {
- return true;
- }
-
- if (!filters.showInternal) {
- return true;
- }
-
- if (!filters.showVariables) {
- return true;
- }
-
- if (!filters.showFunctions) {
- return true;
- }
-
- if (!filters.showClasses) {
- return true;
- }
-
- return false;
- }
-
- useEffect(() => {
- if (!props.cy) return;
-
- const INITIAL_NODE_LIMIT = 50;
- const nodeCount = props.cy.nodes().length;
-
- // Handle strict mode for dev
- if (!initialized.current) {
- initialized.current = true;
- return;
- }
-
- if (nodeCount > INITIAL_NODE_LIMIT) {
- toast.info(
- `There are more than ${INITIAL_NODE_LIMIT} elements on screen. We recommend using the filters in the control bar to get a better view of the graph.`,
- );
- }
- }, []);
-
- // Show and hide external nodes and edges
- useEffect(() => {
- if (!props.cy) return;
-
- // Grab only the external nodes
- const nodes = props.cy.nodes().filter((node) => {
- return node.data("customData").isExternal;
- });
-
- if (filters.showExternal) {
- nodes.removeClass("hidden");
- } else {
- nodes.addClass("hidden");
- }
- }, [filters.showExternal]);
-
- // Show and hide internal nodes and edges
- useEffect(() => {
- if (!props.cy) return;
-
- // Grab only the internal nodes
- const nodes = props.cy.nodes().filter((node) => {
- return (
- !node.data("customData").isExternal &&
- node.data("customData").fileName !== file
- );
- });
- if (filters.showInternal) {
- nodes.removeClass("hidden");
- } else {
- nodes.addClass("hidden");
- }
- }, [filters.showInternal]);
-
- // Show and hide variables
- useEffect(() => {
- if (!props.cy) return;
-
- // Grab only the variable nodes
- const nodes = props.cy.nodes().filter((node) => {
- return (
- node.data("customData").symbolType === "variable" &&
- node.data("customData").fileName === file
- );
- });
- if (filters.showVariables) {
- nodes.removeClass("hidden");
- } else {
- nodes.addClass("hidden");
- }
- }, [filters.showVariables]);
-
- // Show and hide functions
- useEffect(() => {
- if (!props.cy) return;
-
- // Grab only the function nodes
- const nodes = props.cy.nodes().filter((node) => {
- return (
- node.data("customData").symbolType === "function" &&
- node.data("customData").fileName === file
- );
- });
- if (filters.showFunctions) {
- nodes.removeClass("hidden");
- } else {
- nodes.addClass("hidden");
- }
- }, [filters.showFunctions]);
-
- // Show and hide classes
- useEffect(() => {
- if (!props.cy) return;
-
- // Grab only the class nodes
- const nodes = props.cy.nodes().filter((node) => {
- return (
- node.data("customData").symbolType === "class" &&
- node.data("customData").fileName === file
- );
- });
-
- if (filters.showClasses) {
- nodes.removeClass("hidden");
- } else {
- nodes.addClass("hidden");
- }
- }, [filters.showClasses]);
-
- return (
-
-
- props.onLayout()}
- >
-
-
-
-
-
- {/* Add filter options here */}
- Supporting files
- {
- e.preventDefault();
- setFilters({ ...filters, showExternal: !filters.showExternal });
- }}
- className="flex justify-between"
- >
- Show external
-
- setFilters({ ...filters, showExternal: Boolean(checked) })}
- />
-
- {
- e.preventDefault();
- setFilters({ ...filters, showInternal: !filters.showInternal });
- }}
- className="flex justify-between"
- >
- Show internal
-
- setFilters({ ...filters, showInternal: Boolean(checked) })}
- />
-
-
- Main file
- {
- e.preventDefault();
- setFilters({ ...filters, showVariables: !filters.showVariables });
- }}
- className="flex justify-between"
- >
- Show variables
-
- setFilters({ ...filters, showVariables: Boolean(checked) })}
- />
-
- {
- e.preventDefault();
- setFilters({ ...filters, showFunctions: !filters.showFunctions });
- }}
- className="flex justify-between"
- >
- Show functions
-
- setFilters({ ...filters, showFunctions: Boolean(checked) })}
- />
-
- {
- e.preventDefault();
- setFilters({ ...filters, showClasses: !filters.showClasses });
- }}
- className="flex justify-between"
- >
- Show classes
-
- setFilters({ ...filters, showClasses: Boolean(checked) })}
- />
-
-
- {
- setFilters({
- showExternal: true,
- showInternal: true,
- showVariables: true,
- showFunctions: true,
- showClasses: true,
- });
- }}
- className="mx-3"
- >
- Clear filters
-
-
-
- );
-}
diff --git a/packages/app/src/components/Cytoscape/ControlExtensions/GraphDepthExtension.tsx b/packages/app/src/components/Cytoscape/ControlExtensions/GraphDepthExtension.tsx
deleted file mode 100644
index 0abb5ce2..00000000
--- a/packages/app/src/components/Cytoscape/ControlExtensions/GraphDepthExtension.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { useState } from "react";
-import type { Core } from "cytoscape";
-import { Button, DropdownMenu, Slider, TextField } from "@radix-ui/themes";
-import { MdTune } from "react-icons/md";
-import { LuChevronUp } from "react-icons/lu";
-
-export default function GraphDepthExtension(props: {
- busy: boolean;
- cy: Core;
- dependencyState: {
- depth: number;
- setDepth: (depth: number) => void;
- };
- dependentState: {
- depth: number;
- setDepth: (depth: number) => void;
- };
-}) {
- const { dependencyState, dependentState } = props;
- const [tempDependencyDepth, setTempDependencyDepth] = useState(
- dependencyState.depth,
- );
- const [tempDependentDepth, setTempDependentDepth] = useState(
- dependentState.depth,
- );
-
- function checkFiltersSet() {
- if (!props.cy) return false;
-
- if (
- tempDependencyDepth > 1 ||
- tempDependentDepth > 1 ||
- tempDependencyDepth < 1 ||
- tempDependentDepth < 1
- ) {
- return true;
- }
-
- return false;
- }
-
- function applyChanges() {
- if (tempDependencyDepth !== dependencyState.depth) {
- dependencyState.setDepth(tempDependencyDepth);
- }
- if (tempDependentDepth !== dependentState.depth) {
- dependentState.setDepth(tempDependentDepth);
- }
- }
-
- return (
-
-
-
-
-
-
-
-
- Dependency depth
-
- {
- setTempDependencyDepth(value[0]);
- }}
- className="grow"
- />
- {
- const parsedValue = parseInt(e.target.value, 10);
- if (!isNaN(parsedValue) && parsedValue >= 0) {
- setTempDependencyDepth(parsedValue);
- }
- }}
- onKeyUp={(e) => {
- if (e.key === "Up" || e.key === "ArrowUp") {
- setTempDependencyDepth(tempDependencyDepth + 1);
- } else if (e.key === "Down" || e.key === "ArrowDown") {
- setTempDependencyDepth(
- tempDependencyDepth === 0 ? 0 : tempDependencyDepth - 1,
- );
- }
- }}
- className="max-w-10"
- />
-
-
- Dependent depth
-
- {
- setTempDependentDepth(value[0]);
- }}
- className="grow"
- />
- {
- const parsedValue = parseInt(e.target.value, 10);
- if (!isNaN(parsedValue) && parsedValue >= 0) {
- setTempDependentDepth(parsedValue);
- }
- }}
- onKeyUp={(e) => {
- if (e.key === "Up" || e.key === "ArrowUp") {
- setTempDependentDepth(tempDependentDepth + 1);
- } else if (e.key === "Down" || e.key === "ArrowDown") {
- setTempDependentDepth(
- tempDependentDepth === 0 ? 0 : tempDependentDepth - 1,
- );
- }
- }}
- className="max-w-10"
- />
-
- applyChanges()}
- className="mx-3 mt-2"
- >
- Apply
-
-
- {
- setTempDependencyDepth(1);
- dependencyState.setDepth(1);
- setTempDependentDepth(1);
- dependentState.setDepth(1);
- }}
- className="mx-3"
- >
- Reset depth
-
-
-
- );
-}
diff --git a/packages/app/src/components/Cytoscape/ControlExtensions/MetricsExtension.tsx b/packages/app/src/components/Cytoscape/ControlExtensions/MetricsExtension.tsx
deleted file mode 100644
index 7debbe64..00000000
--- a/packages/app/src/components/Cytoscape/ControlExtensions/MetricsExtension.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Button, DropdownMenu } from "@radix-ui/themes";
-import { LuChevronUp } from "react-icons/lu";
-import {
- type Metric,
- metricCharacterCount,
- metricCodeCharacterCount,
- metricCodeLineCount,
- metricCyclomaticComplexity,
- metricDependencyCount,
- metricDependentCount,
- metricLinesCount,
-} from "@napi/shared";
-
-// Extension for the controls in the project view
-export default function MetricsExtension(props: {
- busy: boolean;
- metricState: {
- metric: Metric | undefined;
- setMetric: (metric: Metric | undefined) => void;
- };
-}) {
- const metric = props.metricState.metric;
-
- function getMetricLabel(metric: Metric | undefined) {
- if (metric === metricLinesCount) {
- return "Lines";
- }
- if (metric === metricCodeLineCount) {
- return "Code Lines";
- }
- if (metric === metricCharacterCount) {
- return "Chars";
- }
- if (metric === metricCodeCharacterCount) {
- return "Code Chars";
- }
- if (metric === metricDependencyCount) {
- return "Dependencies";
- }
- if (metric === metricDependentCount) {
- return "Dependents";
- }
- if (metric === metricCyclomaticComplexity) {
- return "Complexity";
- } else {
- return "No Metric";
- }
- }
-
- return (
-
-
-
- {getMetricLabel(metric)}
-
-
-
-
- props.metricState?.setMetric?.(undefined)}
- disabled={props.busy}
- >
- No Metric
-
- {(
- [
- { metric: metricLinesCount, label: "Total Lines" },
- { metric: metricCodeLineCount, label: "Code Lines" },
- { metric: metricCharacterCount, label: "Total Characters" },
- { metric: metricCodeCharacterCount, label: "Code Characters" },
- { metric: metricDependencyCount, label: "Dependencies Count" },
- { metric: metricDependentCount, label: "Dependents Count" },
- {
- metric: metricCyclomaticComplexity,
- label: "Cyclomatic Complexity",
- },
- ] as { metric: Metric; label: string }[]
- ).map(({ metric, label }) => (
- props.metricState?.setMetric?.(metric)}
- >
- {label}
-
- ))}
-
-
- );
-}
diff --git a/packages/app/src/components/Cytoscape/Controls.tsx b/packages/app/src/components/Cytoscape/Controls.tsx
deleted file mode 100644
index 1cff5af1..00000000
--- a/packages/app/src/components/Cytoscape/Controls.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import type { ReactNode } from "react";
-import { Button, Tooltip } from "@radix-ui/themes";
-import type { Core } from "cytoscape";
-import {
- MdFilterCenterFocus,
- MdOutlineAccountTree,
- MdOutlineZoomIn,
- MdOutlineZoomOut,
-} from "react-icons/md";
-
-export default function Controls(props: {
- busy: boolean;
- cy: Core;
- onLayout: () => void;
- showFilters?: boolean;
- children?: ReactNode;
-}) {
- function handleFit() {
- const elements = props.cy.elements();
- const padding = 10;
- props.cy.center(elements).fit(elements, padding);
- }
-
- function handleZoom(zoom: number) {
- const level = props.cy.zoom() * zoom;
- const x = props.cy.width() / 2;
- const y = props.cy.height() / 2;
- const renderedPosition = { x, y };
-
- props.cy.zoom({
- level,
- renderedPosition,
- });
- }
-
- return (
-
-
-
-
-
-
-
-
-
- props.onLayout()}
- >
-
-
-
-
- handleZoom(0.9)}
- >
-
-
-
-
- handleZoom(1.1)}
- >
-
-
-
- {/* Used to pass extensions into the controls */}
- {props.children}
-
-
-
- );
-}
diff --git a/packages/app/src/components/Cytoscape/Skeleton.tsx b/packages/app/src/components/Cytoscape/Skeleton.tsx
deleted file mode 100644
index 11bdddd8..00000000
--- a/packages/app/src/components/Cytoscape/Skeleton.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Skeleton } from "@radix-ui/themes";
-
-export function CytoscapeSkeleton() {
- return (
-
- );
-}
diff --git a/packages/app/src/components/Cytoscape/contextMenu/FileContextMenu.tsx b/packages/app/src/components/Cytoscape/contextMenu/FileContextMenu.tsx
deleted file mode 100644
index b3c11f20..00000000
--- a/packages/app/src/components/Cytoscape/contextMenu/FileContextMenu.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Link } from "react-router";
-import { DropdownMenu } from "@radix-ui/themes";
-import {
- LuFolderSearch2,
- LuGitGraph,
- LuPanelRightOpen,
- LuSearchCode,
-} from "react-icons/lu";
-import type { FileDependencyManifest } from "@napi/shared";
-
-export default function FileContextMenu(props: {
- position: { x: number; y: number };
- fileDependencyManifest: FileDependencyManifest;
- open: boolean;
- showInSidebar: (filename: string) => void;
- onOpenChange: (open: boolean) => void;
- setDetailsPaneOpen: (open: boolean) => void;
- setExtractionNodes: (
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) => void;
-}) {
- function handleOnExtract() {
- props.setExtractionNodes(
- props.fileDependencyManifest.filePath,
- Object.values(props.fileDependencyManifest.symbols).map(
- (symbol) => symbol.id,
- ),
- "add",
- );
- }
-
- return (
- props.onOpenChange(false)} // Optional auto-close
- >
-
-
-
-
-
-
-
- {props.fileDependencyManifest.filePath.split("/").pop()}
-
-
- props.setDetailsPaneOpen(true)}
- >
-
- Show details
-
-
-
-
-
- Inspect symbols
-
-
-
-
- props.showInSidebar(
- props.fileDependencyManifest.filePath.split("/").pop() || "",
- )}
- >
-
- Show file
-
-
-
-
- handleOnExtract()}
- >
-
- Extract all symbols
-
-
-
-
-
-
- );
-}
diff --git a/packages/app/src/components/Cytoscape/contextMenu/SymbolContextMenu.tsx b/packages/app/src/components/Cytoscape/contextMenu/SymbolContextMenu.tsx
deleted file mode 100644
index d82fdbef..00000000
--- a/packages/app/src/components/Cytoscape/contextMenu/SymbolContextMenu.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Link, useParams } from "react-router";
-import { DropdownMenu } from "@radix-ui/themes";
-import { LuGitGraph, LuPanelRightOpen, LuSearchCode } from "react-icons/lu";
-import type { FileDependencyManifest } from "@napi/shared";
-
-export default function SymbolContextMenu(props: {
- position: { x: number; y: number };
- fileDependencyManifest: FileDependencyManifest;
- symbolId: string;
- open: boolean;
- onOpenChange: (open: boolean) => void;
- setDetailsPaneOpen: (open: boolean) => void;
- setExtractionNodes: (
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) => void;
-}) {
- const { file, instance } = useParams();
-
- const isSymbolLevelView = file && instance;
-
- function handleOnExtract() {
- props.setExtractionNodes(
- props.fileDependencyManifest.filePath,
- [props.symbolId],
- "add",
- );
- }
-
- return (
- props.onOpenChange(false)} // Optional auto-close
- >
-
-
-
-
-
-
-
-
- {props.fileDependencyManifest.symbols[props.symbolId].id}
-
-
- {/* TODO: There is something wrong with the details pane on the symbol-level view */}
- {/* Fix it, and then remove the following */}
- {!isSymbolLevelView && (
- <>
-
- props.setDetailsPaneOpen(true)}
- >
-
- Details
-
-
-
-
-
- Inspect symbol
-
-
-
- >
- )}
-
- handleOnExtract()}
- >
-
- Extract symbol
-
-
-
-
-
-
- );
-}
diff --git a/packages/app/src/components/DisplayNameWithTootip.tsx b/packages/app/src/components/DisplayNameWithTootip.tsx
new file mode 100644
index 00000000..d6e974e8
--- /dev/null
+++ b/packages/app/src/components/DisplayNameWithTootip.tsx
@@ -0,0 +1,32 @@
+import { Tooltip, TooltipContent, TooltipTrigger } from "./shadcn/Tooltip.tsx";
+
+export default function DisplayNameWithTooltip(props: {
+ name: string;
+ maxChar?: number;
+ truncateBefore?: boolean;
+}) {
+ const maxChar = props.maxChar || 30;
+
+ if (props.name.length > maxChar) {
+ let displayedName: string;
+
+ if (props.truncateBefore) {
+ displayedName = "..." + props.name.slice(0, maxChar);
+ } else {
+ displayedName = props.name.slice(0, maxChar) + "...";
+ }
+
+ return (
+
+
+ {displayedName}
+
+
+ {props.name}
+
+
+ );
+ } else {
+ return {props.name} ;
+ }
+}
diff --git a/packages/app/src/components/FileDetailsPane.tsx b/packages/app/src/components/FileDetailsPane.tsx
deleted file mode 100644
index dab3ff54..00000000
--- a/packages/app/src/components/FileDetailsPane.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-import { Link } from "react-router";
-import type { ReactNode } from "react";
-import { Button, Callout, ScrollArea, Separator, Text } from "@radix-ui/themes";
-import {
- LuCircleX,
- LuCode,
- LuFileText,
- LuSearchCode,
- LuTriangle,
- LuX,
-} from "react-icons/lu";
-import {
- type FileAuditManifest,
- type FileDependencyManifest,
- metricCharacterCount,
- metricCodeCharacterCount,
- metricCodeLineCount,
- metricCyclomaticComplexity,
- metricDependencyCount,
- metricDependentCount,
- metricLinesCount,
-} from "@napi/shared";
-
-// Subcomponent for section headings
-function SectionHeading({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-// Metric item component for consistent display
-function MetricItem({
- label,
- value,
- alert,
-}: {
- label: string;
- value: number | string;
- alert?: { message: { long: string } };
-}) {
- return (
-
-
-
- {label}:
-
-
- {value}
- {alert && }
-
-
- {alert && (
-
-
-
-
- {alert.message.long}
-
- )}
-
- );
-}
-
-// Alert badge component
-function AlertBadge({ count }: { count: number }) {
- return (
-
- 0 ? "text-amber-500" : "text-gray-400"}`}
- />
- 0 ? "text-amber-500" : "text-gray-400"}>
- {count}
-
-
- );
-}
-
-// Symbol metrics and alerts component
-function SymbolSection({
- symbol,
- fileDependencyManifest,
-}: {
- symbol: {
- id: string;
- alerts: Record;
- };
- fileDependencyManifest: FileDependencyManifest;
-}) {
- const symbolData = fileDependencyManifest.symbols[symbol.id];
- const alerts = Object.values(symbol.alerts);
- const alertsByMetric = alerts.reduce(
- (acc, alert) => {
- if (alert.metric) {
- acc[alert.metric] = alert;
- }
- return acc;
- },
- {} as Record,
- );
-
- return (
-
-
-
- {symbolData.type}: {symbol.id}
-
-
-
-
-
- {symbolData.metrics &&
- Object.entries(symbolData.metrics).map(([metricKey, value]) => (
-
- ))}
-
-
- );
-}
-
-export default function FileDetailsPane(props: {
- fileDependencyManifest: FileDependencyManifest;
- fileAuditManifest: FileAuditManifest;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const { fileDependencyManifest, fileAuditManifest, open, setOpen } = props;
- const fileName = fileDependencyManifest.filePath.split("/").pop() || "";
- const fileAlerts = Object.values(fileAuditManifest.alerts) as {
- message: { long: string };
- metric: string;
- }[];
-
- // Organize alerts by their metric for file metrics
- const alertsByMetric = fileAlerts.reduce(
- (acc, alert) => {
- if (alert.metric) {
- acc[alert.metric] = alert;
- }
- return acc;
- },
- {} as Record,
- );
-
- return (
-
-
- {/* Header */}
-
-
- {fileName}
-
- setOpen(false)}
- className="text-xl text-gray-light hover:text-black dark:text-gray-dark dark:hover:text-white"
- >
-
-
-
-
-
- {/* File Metrics with Alerts */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Symbols with their metrics and alerts */}
-
- Symbols
-
-
- {/* Total symbols count */}
-
-
-
-
- {Object.entries(
- Object.values(fileDependencyManifest.symbols).reduce(
- (acc, symbol) => {
- acc[symbol.type] = (acc[symbol.type] || 0) + 1;
- return acc;
- },
- {} as Record,
- ),
- ).map(([type, count]) => (
-
- {type}: {count}
-
- ))}
-
-
-
-
- {/* Individual symbols with their metrics and alerts */}
-
- {Object.values(fileAuditManifest.symbols).map((symbol) => (
-
- ))}
-
-
- {/* Action Buttons */}
-
-
-
-
- Inspect file interactions
-
-
-
-
-
- );
-}
diff --git a/packages/app/src/components/FileExplorer/FileExplorer.tsx b/packages/app/src/components/FileExplorer/FileExplorer.tsx
deleted file mode 100644
index 91f19162..00000000
--- a/packages/app/src/components/FileExplorer/FileExplorer.tsx
+++ /dev/null
@@ -1,832 +0,0 @@
-import {
- Button,
- Checkbox,
- IconButton,
- ScrollArea,
- TextField,
- Tooltip,
-} from "@radix-ui/themes";
-import { useEffect, useRef, useState } from "react";
-import { Link } from "react-router";
-import {
- type ImperativePanelHandle,
- Panel,
- PanelGroup,
- PanelResizeHandle,
-} from "react-resizable-panels";
-import type { ExtractionNode } from "@napi/shared";
-import { FileExplorerSkeleton } from "./Skeleton.tsx";
-import { LuSearchCode, LuSearchSlash, LuX } from "react-icons/lu";
-import languageIcon from "./languageIcons.tsx";
-import {
- MdDragHandle,
- MdOutlineKeyboardArrowDown,
- MdOutlineKeyboardArrowLeft,
- MdOutlineKeyboardArrowRight,
- MdOutlineRemoveRedEye,
- MdSearch,
- MdSubdirectoryArrowRight,
- MdVerticalAlignBottom,
- MdVerticalAlignTop,
-} from "react-icons/md";
-import { runExtraction } from "../../service/api/index.ts";
-import { toast } from "react-toastify";
-
-interface TreeData {
- id: string;
- level: number;
- name: string;
- matchesSearch: boolean;
- children?: TreeData[]; // Only for non leaf nodes
- isSymbol?: boolean; // Only for leaf nodes
-}
-
-export interface FileExplorerFile {
- path: string;
- symbols: string[];
-}
-
-export default function FileExplorer(props: {
- busy: boolean;
- files: FileExplorerFile[];
- isOpen: boolean;
- setIsOpen: (open: boolean) => void;
- search: string;
- setIsSearch: (search: string) => void;
- highlightedNodeId: string | null;
- setHighlightedNodeId: (node: string | null) => void;
- extractionState: {
- extractionNodes: Record;
- updateExtractionNodes: (
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) => void;
- };
-}) {
- const [treeData, setTreeData] = useState([]);
- const [extractionPanelSize, setExtractionPanelSize] = useState(0);
- const extractionPanelRef = useRef(null);
-
- function nodeMatchesSearch(name: string, symbols: string[]): boolean {
- return (
- !props.search ||
- name.toLowerCase().includes(props.search.toLowerCase()) ||
- symbols.some((symbol) =>
- symbol.toLowerCase().includes(props.search.toLowerCase())
- )
- );
- }
-
- function addFileToTree(
- currentLevel: TreeData[],
- segments: string[],
- cumulativePath: string,
- symbols: string[],
- ): boolean {
- const [segment, ...remainingSegments] = segments;
- const nodeId = cumulativePath ? `${cumulativePath}/${segment}` : segment;
- let existingNode = currentLevel.find((node) => node.name === segment);
-
- if (!existingNode) {
- existingNode = {
- id: nodeId,
- level: 0,
- name: segment,
- matchesSearch: false,
- children: [],
- };
- currentLevel.push(existingNode);
- }
-
- const matchesCurrentNode = nodeMatchesSearch(segment, symbols);
-
- if (remainingSegments.length > 0) {
- const childMatches = addFileToTree(
- existingNode.children as TreeData[],
- remainingSegments,
- nodeId,
- symbols,
- );
- existingNode.matchesSearch = existingNode.matchesSearch || childMatches;
- } else {
- existingNode.matchesSearch = existingNode.matchesSearch ||
- matchesCurrentNode;
-
- // ADD SYMBOLS AS CHILDREN HERE
- if (symbols.length > 0) {
- existingNode.children = symbols.map((symbol) => ({
- id: `${nodeId}#${symbol}`,
- level: existingNode.level + 1,
- name: symbol,
- matchesSearch: !props.search ||
- symbol.toLowerCase().includes(props.search.toLowerCase()),
- isSymbol: true,
- }));
-
- // Update matchesSearch if symbol matches
- existingNode.matchesSearch = existingNode.matchesSearch ||
- existingNode.children.some((child) => child.matchesSearch);
- }
- }
-
- return existingNode.matchesSearch;
- }
-
- function buildTreeData(files: FileExplorerFile[]): TreeData[] {
- let rootNodes: TreeData[] = [];
-
- files.forEach((file) => {
- // Filter out elements that don't match the search
- // on either the filename or the symbols
- if (!nodeMatchesSearch(file.path, file.symbols)) {
- return;
- }
-
- const segments = file.path.split("/");
-
- // Add the file to the tree structure
- addFileToTree(rootNodes, segments, "", file.symbols);
- });
-
- rootNodes = rootNodes.map(flattenNode);
- rootNodes = setLevels(rootNodes);
-
- return rootNodes;
- }
-
- // Recursive helper that flattens a single node
- function flattenNode(node: TreeData): TreeData {
- // Recursively flatten all children
- if (node.children) {
- node.children = node.children.map(flattenNode);
- }
-
- // As long as the node has exactly one child, merge them
- while (
- node.children &&
- node.children.length === 1 &&
- !node.children[0].isSymbol
- ) {
- const [child] = node.children;
-
- // Merge child's name into parent
- node.name = `${node.name}/${child.name}`;
- // Update the parent's id to child's id (which is already the full path)
- node.id = child.id;
- // Optionally inherit the child's level if needed
- node.level = child.level;
- // Replace parent's children with child's children
- node.children = child.children;
-
- // Continue merging in case the new "flattened" node also has exactly one child
- }
-
- return node;
- }
-
- function setLevels(nodes: TreeData[], currentLevel = 0): TreeData[] {
- return nodes.map((node) => {
- // Assign the current level
- node.level = currentLevel;
-
- // Recursively set levels for children
- if (node.children && node.children.length > 0) {
- node.children = setLevels(node.children, currentLevel + 1);
- }
-
- return node;
- });
- }
-
- function handleResize(size: number) {
- setExtractionPanelSize(size);
- }
-
- function handleExpandAndCollapse() {
- if (extractionPanelRef.current) {
- if (extractionPanelSize > 0) {
- extractionPanelRef.current.resize(0);
- setExtractionPanelSize(0);
- } else {
- extractionPanelRef.current.resize(50);
- setExtractionPanelSize(50);
- }
- }
- }
-
- useEffect(() => {
- // Use setTimeout to ensure the panel is resized after the DOM has updated
- // IF WE DON'T DO THIS, the panel will not resize correctly when forcing
- // the sidebar to open if it was closed.
- // This is a side effect of the way we do the sidebar and state management
- setTimeout(() => {
- if (extractionPanelRef.current) {
- extractionPanelRef.current.resize(50);
- }
- }, 50);
- }, [props.extractionState.extractionNodes]);
-
- useEffect(() => {
- setTreeData(buildTreeData(props.files));
- }, [props.files, props.search]);
-
- return (
-
-
-
-
- {props.isOpen && (
-
-
- NanoAPI
-
- )}
-
props.setIsOpen(!props.isOpen)}
- variant="ghost"
- color="violet"
- radius="full"
- >
- {props.isOpen
- ?
- : }
-
-
-
-
- {props.busy ? : (
- <>
- props.setIsSearch(e.target.value)}
- className={`min-h-8 transition-all duration-300 overflow-hidden ${
- !props.isOpen && "w-0"
- }`}
- >
-
-
-
- {props.search.length > 0 && (
-
- props.setIsSearch("")}
- >
-
-
-
- )}
-
-
-
-
- >
- )}
-
-
-
- {props.isOpen && (
- <>
-
- {extractionPanelSize > 0
- ? (
-
handleExpandAndCollapse()}
- className="text-xl text-gray-light dark:text-gray-dark"
- />
- )
- : (
- handleExpandAndCollapse()}
- className="text-xl text-gray-light dark:text-gray-dark"
- />
- )}
-
- Symbol Extraction
-
-
-
-
- >
- )}
-
- );
-}
-
-function ListElement(props: {
- nodes: TreeData[];
- search: string;
- hlNodeId: string | null;
- setHLNodeId: (node: string | null) => void;
-}) {
- return (
-
- {props.nodes.map((node) => {
- return (
-
- );
- })}
-
- );
-}
-
-function NodeElement(props: {
- node: TreeData;
- search: string;
- hlNodeId: string | null;
- setHLNodeId: (node: string | null) => void;
-}) {
- const [isOpen, setIsOpen] = useState(false);
-
- const shouldAutoExpand = props.search.length > 0 && props.node.matchesSearch;
- const isHighlighted = props.hlNodeId === props.node.id;
-
- function toggleHighlight(id: string) {
- if (isHighlighted) {
- props.setHLNodeId(null);
- } else {
- props.setHLNodeId(id);
- }
- }
-
- useEffect(() => {
- setIsOpen(shouldAutoExpand);
- }, [shouldAutoExpand]);
-
- function handleToggle() {
- setIsOpen((value) => !value);
- }
-
- return (
-
-
- {props.node.children &&
- props.node.children.length > 0 &&
- !props.node.children[0].isSymbol
- ? (
-
-
- {isOpen
- ? (
-
- )
- : (
-
- )}
-
-
-
- )
- : (
- <>
- {!props.node.isSymbol
- ? (
-
-
-
-
- {languageIcon(
- props.node.name.split(".").pop() || "txt",
- )}
-
-
-
-
-
-
- toggleHighlight(props.node.id)}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- )
- : (
-
-
-
-
-
-
-
-
- encodeURIComponent(id))
- .join("/")
- }`}
- >
-
-
-
-
-
-
- )}
- >
- )}
-
- {isOpen && props.node.children && (
-
- )}
-
- );
-}
-
-function DisplayedPath({
- node,
- search = "",
-}: {
- node: TreeData;
- search: string;
-}) {
- const maxPathLength = Math.max(25 - 2 * node.level, 5);
- let foundInSearch = false;
- if (search.length > 2) {
- foundInSearch = node.name.toLowerCase().includes(search.toLowerCase());
- }
-
- if (node.name.length > maxPathLength) {
- return (
-
-
- {`...${node.name.slice(-maxPathLength)}`}
-
-
- );
- }
-
- return (
-
- {node.name}
-
- );
-}
-
-enum EditMode {
- NONE = "none", // Nothing has been edited
- EDITING = "editing", // The user is currently editing
- CANCELLED = "cancelled", // The user has cancelled the edit
- COMPLETED = "completed", // The user has completed the edit
-}
-
-function ExtractionPanel(props: {
- extractionNodes: Record;
- updateExtractionNodes: (
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) => void;
-}) {
- const [editMode, setEditMode] = useState(EditMode.NONE);
- const [extractionLoading, setExtractionLoading] = useState(false);
-
- async function runExtractionViaAPI() {
- setExtractionLoading(true);
-
- const extractionNodes = Object.values(props.extractionNodes);
- const response = await runExtraction(extractionNodes);
-
- if (response && response.success) {
- toast.success(
- "Extraction completed successfully. You'll find the extracted files in the output directory.",
- );
- } else {
- toast.error("Extraction failed. Please check the logs for more details.");
- }
-
- setExtractionLoading(false);
- }
-
- return (
-
-
- {Object.keys(props.extractionNodes).map((key) => {
- const node = props.extractionNodes[key];
- return (
-
- );
- })}
-
-
- {editMode !== EditMode.EDITING && (
- <>
-
- {
- setEditMode(EditMode.EDITING);
- }}
- >
- Edit
-
-
-
-
- Extract
-
-
- >
- )}
- {editMode === EditMode.EDITING && (
- <>
- {
- setEditMode(EditMode.CANCELLED);
- }}
- >
- Cancel
-
- setEditMode(EditMode.COMPLETED)}
- >
- Update
-
- >
- )}
-
-
- );
-}
-
-function ExtractionElement(props: {
- node: ExtractionNode;
- updateExtractionNodes: (
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) => void;
- editMode?: EditMode;
-}) {
- type CheckedState = boolean | "indeterminate";
-
- const symbolMap = new Map();
- props.node.symbols.forEach((symbol) => {
- symbolMap.set(symbol, true);
- });
-
- const [fileChecked, setFileChecked] = useState(true);
- const [checkedSymbols, setCheckedSymbols] = useState(symbolMap);
- const maxPathLength = 20;
-
- function getDisplayedPath(name: string) {
- if (name.length > maxPathLength) {
- return (
-
-
- {`...${
- name.slice(
- -maxPathLength,
- )
- }`}
-
-
- );
- }
- return {name}
;
- }
-
- function resolveFileCheckedState(symbolMap: Map) {
- const allChecked = props.node.symbols.every((symbol) => {
- return symbolMap.get(symbol) === true;
- });
- const allUnchecked = props.node.symbols.every((symbol) => {
- return symbolMap.get(symbol) === false;
- });
- if (allChecked) {
- return true;
- }
- if (allUnchecked) {
- return false;
- }
- return "indeterminate";
- }
-
- function handleSymbolCheck(symbol: string, checked: boolean) {
- setCheckedSymbols((prev) => {
- const newMap = new Map(prev);
- newMap.set(symbol, checked);
- setFileChecked(resolveFileCheckedState(newMap));
- return newMap;
- });
- }
-
- useEffect(() => {
- if (props.editMode === EditMode.COMPLETED) {
- // Check if any of the symbols are unchecked, and remove them from the extraction
- const uncheckedSymbols = Array.from(checkedSymbols.entries())
- .filter(([, checked]) => !checked)
- .map(([symbol]) => symbol);
- if (uncheckedSymbols.length > 0) {
- props.updateExtractionNodes(
- props.node.filePath,
- uncheckedSymbols,
- "remove",
- );
- }
-
- // If a file is unchecked, remove all symbols from the extraction
- if (fileChecked === false) {
- props.updateExtractionNodes(
- props.node.filePath,
- props.node.symbols,
- "remove",
- );
- }
-
- //Finally, update the checkedSymbols to remove the unchecked symbols
- setCheckedSymbols((prev) => {
- const newMap = new Map();
- props.node.symbols.forEach((symbol) => {
- if (prev.get(symbol) !== false) {
- newMap.set(symbol, true);
- }
- });
- return newMap;
- });
- setFileChecked(true);
- }
-
- if (props.editMode === EditMode.CANCELLED) {
- // Reset the checked state to the original state
- // by setting everything to true
- setFileChecked(true);
- setCheckedSymbols(() => {
- const newMap = new Map();
- props.node.symbols.forEach((symbol) => {
- newMap.set(symbol, true);
- });
- return newMap;
- });
- }
- }, [props.editMode]);
-
- return (
-
-
-
- {languageIcon(props.node.filePath.split(".").pop() || "txt")}
- {getDisplayedPath(props.node.filePath)}
-
- {props.editMode === EditMode.EDITING && (
-
-
- {
- setFileChecked(Boolean(checked));
- for (const symbol of props.node.symbols) {
- if (checked) {
- checkedSymbols.set(symbol, true);
- } else {
- checkedSymbols.set(symbol, false);
- }
- }
- }}
- >
-
-
-
- )}
-
- {props.node.symbols.map((symbol) => (
-
-
-
- {getDisplayedPath(symbol)}
-
- {props.editMode === EditMode.EDITING && (
-
-
- {
- handleSymbolCheck(symbol, Boolean(checked));
- }}
- >
-
-
-
- )}
-
- ))}
-
- );
-}
diff --git a/packages/app/src/components/FileExplorer/Skeleton.tsx b/packages/app/src/components/FileExplorer/Skeleton.tsx
deleted file mode 100644
index ecaba2d5..00000000
--- a/packages/app/src/components/FileExplorer/Skeleton.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Skeleton } from "@radix-ui/themes";
-
-export function FileExplorerSkeleton() {
- return (
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/app/src/components/FileExplorer/languageIcons.tsx b/packages/app/src/components/FileExplorer/languageIcons.tsx
deleted file mode 100644
index 16e531d8..00000000
--- a/packages/app/src/components/FileExplorer/languageIcons.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { LuFileCode } from "react-icons/lu";
-import { DiPython } from "react-icons/di";
-import { TbBrandCSharp } from "react-icons/tb";
-import type { JSX } from "react";
-
-const languageIconMap: Record = {
- py: (
-
- ),
- cs: (
-
- ),
- fallback: (
-
- ),
-};
-
-export default function languageIcon(fileExtension: string) {
- const icon = languageIconMap[fileExtension] || languageIconMap.fallback;
- return icon;
-}
diff --git a/packages/app/src/components/SymbolDetailsPane.tsx b/packages/app/src/components/SymbolDetailsPane.tsx
deleted file mode 100644
index ddccbc93..00000000
--- a/packages/app/src/components/SymbolDetailsPane.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-import type { ReactNode } from "react";
-import { Link } from "react-router";
-import { Button, Callout, ScrollArea, Separator, Text } from "@radix-ui/themes";
-import {
- LuCircleX,
- LuCode,
- LuFileText,
- LuSearchCode,
- LuTriangle,
- LuX,
-} from "react-icons/lu";
-import {
- type FileAuditManifest,
- type FileDependencyManifest,
- metricCharacterCount,
- metricCodeCharacterCount,
- metricCodeLineCount,
- metricCyclomaticComplexity,
- metricDependencyCount,
- metricDependentCount,
- metricLinesCount,
-} from "@napi/shared";
-
-// Subcomponent for section headings
-function SectionHeading({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-// Metric item component for consistent display
-function MetricItem({
- label,
- value,
- alert,
-}: {
- label: string;
- value: number | string;
- alert?: { message: { long: string } };
-}) {
- return (
-
-
-
- {label}:
-
-
- {value}
- {alert && }
-
-
- {alert && (
-
-
-
-
- {alert.message.long}
-
- )}
-
- );
-}
-
-// Alert badge component
-function AlertBadge({ count }: { count: number }) {
- return (
-
- 0 ? "text-amber-500" : "text-gray-400"}`}
- />
- 0 ? "text-amber-500" : "text-gray-400"}>
- {count}
-
-
- );
-}
-
-export default function SymbolDetailsPane(props: {
- fileDependencyManifest: FileDependencyManifest;
- fileAuditManifest: FileAuditManifest;
- symbolId: string;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const { fileDependencyManifest, fileAuditManifest, symbolId, open, setOpen } =
- props;
- const fileName = fileDependencyManifest.filePath.split("/").pop() || "";
- const symbolData = fileDependencyManifest.symbols[symbolId];
- const symbolName = symbolData?.id || symbolId;
- const symbolType = symbolData?.type || "";
-
- // Get alerts related to the current symbol only
- const symbolAlertsObj = fileAuditManifest.symbols[symbolId]?.alerts || {};
- const symbolAlerts = Object.values(symbolAlertsObj) as {
- message: { long: string };
- metric: string;
- }[];
-
- // Organize alerts by their metric for symbol metrics
- const symbolAlertsByMetric = symbolAlerts.reduce(
- (acc, alert) => {
- if (alert.metric) {
- acc[alert.metric] = alert;
- }
- return acc;
- },
- {} as Record,
- );
-
- const fileAlerts = Object.values(fileAuditManifest.alerts) as {
- message: { long: string };
- metric: string;
- }[];
-
- // Organize alerts by their metric for file metrics
- const alertsByMetric = fileAlerts.reduce(
- (acc, alert) => {
- if (alert.metric) {
- acc[alert.metric] = alert;
- }
- return acc;
- },
- {} as Record,
- );
-
- return (
-
-
- {/* Header */}
-
-
- {fileName}
-
- setOpen(false)}
- className="text-xl text-gray-light hover:text-black dark:text-gray-dark dark:hover:text-white"
- >
-
-
-
-
-
-
- {/* File Metrics with Alerts */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Current Symbol with its metrics and alerts */}
-
- Symbol
-
-
- {symbolData && fileAuditManifest.symbols[symbolId] && (
-
-
-
- {symbolType}: {symbolName}
-
-
-
-
-
- {symbolData.metrics &&
- Object.entries(symbolData.metrics).map(([metricKey, value]) => (
-
- ))}
-
-
- )}
-
- {/* Action Buttons */}
-
-
-
-
- Inspect symbol interactions
-
-
-
-
-
- );
-}
diff --git a/packages/app/src/components/contextMenu/FileContextMenu.tsx b/packages/app/src/components/contextMenu/FileContextMenu.tsx
new file mode 100644
index 00000000..a9c013fa
--- /dev/null
+++ b/packages/app/src/components/contextMenu/FileContextMenu.tsx
@@ -0,0 +1,74 @@
+import { Link } from "react-router";
+import type { FileDependencyManifest } from "@napi/shared";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "../shadcn/Dropdownmenu.tsx";
+import { PanelRight, SearchCode } from "lucide-react";
+import DisplayNameWithTooltip from "../DisplayNameWithTootip.tsx";
+
+export default function FileContextMenu(props: {
+ context: {
+ position: { x: number; y: number };
+ fileDependencyManifest: FileDependencyManifest;
+ } | undefined;
+ onClose: () => void;
+ onOpenDetails: (filePath: string) => void;
+}) {
+ return (
+
+
props.onClose()}
+ >
+
+
+
+
+
+
+
+
+ props.context?.fileDependencyManifest &&
+ props.onOpenDetails(
+ props.context?.fileDependencyManifest.filePath,
+ )}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/app/src/components/contextMenu/SymbolContextMenu.tsx b/packages/app/src/components/contextMenu/SymbolContextMenu.tsx
new file mode 100644
index 00000000..e3277ddb
--- /dev/null
+++ b/packages/app/src/components/contextMenu/SymbolContextMenu.tsx
@@ -0,0 +1,84 @@
+import { Link } from "react-router";
+import type {
+ FileDependencyManifest,
+ SymbolDependencyManifest,
+} from "@napi/shared";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "../shadcn/Dropdownmenu.tsx";
+import { PanelRight, SearchCode } from "lucide-react";
+import DisplayNameWithTooltip from "../DisplayNameWithTootip.tsx";
+
+export default function SymbolContextMenu(props: {
+ context: {
+ position: { x: number; y: number };
+ fileDependencyManifest: FileDependencyManifest;
+ symbolDependencyManifest: SymbolDependencyManifest;
+ } | undefined;
+ onClose: () => void;
+ onOpenDetails: (filePath: string, symbolId: string) => void;
+}) {
+ return (
+
+
props.onClose()}
+ >
+
+
+
+
+
+
+
+
+ props.context?.fileDependencyManifest &&
+ props.onOpenDetails(
+ props.context?.fileDependencyManifest.filePath,
+ props.context?.symbolDependencyManifest.id,
+ )}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/app/src/components/controls/ControlExtensions/FiltersExtension.tsx b/packages/app/src/components/controls/ControlExtensions/FiltersExtension.tsx
new file mode 100644
index 00000000..f68f9cce
--- /dev/null
+++ b/packages/app/src/components/controls/ControlExtensions/FiltersExtension.tsx
@@ -0,0 +1,150 @@
+import { type MouseEvent, useEffect, useState } from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "../../shadcn/Tooltip.tsx";
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "../../shadcn/Dropdownmenu.tsx";
+import { Button } from "../../shadcn/Button.tsx";
+import { Funnel } from "lucide-react";
+
+export default function FiltersExtension(props: {
+ busy: boolean;
+ currentFileName?: string;
+ onFilterChange: (
+ showExternal: boolean,
+ showVariables: boolean,
+ showFunctions: boolean,
+ showClasses: boolean,
+ showStructs: boolean,
+ showEnums: boolean,
+ showInterfaces: boolean,
+ showRecords: boolean,
+ showDelegates: boolean,
+ ) => void;
+}) {
+ const [showExternal, setShowExternal] = useState(true);
+ const [showVariables, setShowVariables] = useState(true);
+ const [showFunctions, setShowFunctions] = useState(true);
+ const [showClasses, setShowClasses] = useState(true);
+ const [showStructs, setShowStructs] = useState(true);
+ const [showEnums, setShowEnums] = useState(true);
+ const [showInterfaces, setShowInterfaces] = useState(true);
+ const [showRecords, setShowRecords] = useState(true);
+ const [showDelegates, setShowDelegates] = useState(true);
+
+ useEffect(() => {
+ props.onFilterChange(
+ showExternal,
+ showVariables,
+ showFunctions,
+ showClasses,
+ showStructs,
+ showEnums,
+ showInterfaces,
+ showRecords,
+ showDelegates,
+ );
+ }, [
+ showExternal,
+ showVariables,
+ showFunctions,
+ showClasses,
+ showStructs,
+ showEnums,
+ showInterfaces,
+ showRecords,
+ showDelegates,
+ ]);
+
+ function handleFilterClick(
+ e: MouseEvent,
+ set: React.Dispatch>,
+ ) {
+ e.preventDefault();
+ set((prev) => !prev);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Filters
+
+
+ {[
+ {
+ label: "Show external",
+ checked: showExternal,
+ set: setShowExternal,
+ },
+ {
+ label: "Show variables",
+ checked: showVariables,
+ set: setShowVariables,
+ },
+ {
+ label: "Show functions",
+ checked: showFunctions,
+ set: setShowFunctions,
+ },
+ {
+ label: "Show classes",
+ checked: showClasses,
+ set: setShowClasses,
+ },
+ {
+ label: "Show structs",
+ checked: showStructs,
+ set: setShowStructs,
+ },
+ { label: "Show enums", checked: showEnums, set: setShowEnums },
+ {
+ label: "Show interfaces",
+ checked: showInterfaces,
+ set: setShowInterfaces,
+ },
+ {
+ label: "Show records",
+ checked: showRecords,
+ set: setShowRecords,
+ },
+ {
+ label: "Show delegates",
+ checked: showDelegates,
+ set: setShowDelegates,
+ },
+ ].map(({ label, checked, set }) => (
+ handleFilterClick(e, set)}
+ >
+ {label}
+
+ ))}
+
+
+
+ Hide or show specific elements in the graph.
+
+
+ );
+}
diff --git a/packages/app/src/components/controls/ControlExtensions/GraphDepthExtension.tsx b/packages/app/src/components/controls/ControlExtensions/GraphDepthExtension.tsx
new file mode 100644
index 00000000..19a71bd5
--- /dev/null
+++ b/packages/app/src/components/controls/ControlExtensions/GraphDepthExtension.tsx
@@ -0,0 +1,128 @@
+import { useState } from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "../../shadcn/Tooltip.tsx";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+} from "../../shadcn/Dropdownmenu.tsx";
+import { Button } from "../../shadcn/Button.tsx";
+import { Settings2 } from "lucide-react";
+import { Slider } from "../../shadcn/Slider.tsx";
+import { Input } from "../../shadcn/Input.tsx";
+import { Label } from "../../shadcn/Label.tsx";
+
+export default function GraphDepthExtension(props: {
+ busy: boolean;
+ dependencyState: {
+ depth: number;
+ setDepth: (depth: number) => void;
+ };
+ dependentState: {
+ depth: number;
+ setDepth: (depth: number) => void;
+ };
+}) {
+ const { dependencyState, dependentState } = props;
+ const [tempDependencyDepth, setTempDependencyDepth] = useState(
+ dependencyState.depth,
+ );
+ const [tempDependentDepth, setTempDependentDepth] = useState(
+ dependentState.depth,
+ );
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (tempDependencyDepth !== dependencyState.depth) {
+ dependencyState.setDepth(tempDependencyDepth);
+ }
+ if (tempDependentDepth !== dependentState.depth) {
+ dependentState.setDepth(tempDependentDepth);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Set the depth of the dependencies shown on the graph.
+
+
+ );
+}
diff --git a/packages/app/src/components/controls/ControlExtensions/MetricsExtension.tsx b/packages/app/src/components/controls/ControlExtensions/MetricsExtension.tsx
new file mode 100644
index 00000000..5fa2d339
--- /dev/null
+++ b/packages/app/src/components/controls/ControlExtensions/MetricsExtension.tsx
@@ -0,0 +1,98 @@
+import {
+ type Metric,
+ metricCharacterCount,
+ metricCodeCharacterCount,
+ metricCodeLineCount,
+ metricCyclomaticComplexity,
+ metricDependencyCount,
+ metricDependentCount,
+ metricLinesCount,
+} from "@napi/shared";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "../../shadcn/Tooltip.tsx";
+import { Button } from "../../shadcn/Button.tsx";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "../../shadcn/Dropdownmenu.tsx";
+
+// Extension for the controls in the project view
+export default function MetricsExtension(props: {
+ busy: boolean;
+ metricState: {
+ metric: Metric | undefined;
+ setMetric: (metric: Metric | undefined) => void;
+ };
+}) {
+ const metric = props.metricState.metric;
+
+ function getMetricLabel(metric: Metric | undefined) {
+ if (metric === metricLinesCount) {
+ return "Lines";
+ }
+ if (metric === metricCodeLineCount) {
+ return "Code Lines";
+ }
+ if (metric === metricCharacterCount) {
+ return "Chars";
+ }
+ if (metric === metricCodeCharacterCount) {
+ return "Code Chars";
+ }
+ if (metric === metricDependencyCount) {
+ return "Dependencies";
+ }
+ if (metric === metricDependentCount) {
+ return "Dependents";
+ }
+ if (metric === metricCyclomaticComplexity) {
+ return "Complexity";
+ } else {
+ return "None";
+ }
+ }
+
+ return (
+
+
+
+
+
+ {getMetricLabel(metric)}
+
+
+
+
+ {([
+ { metric: undefined, label: "No Metric" },
+ { metric: metricLinesCount, label: "Lines" },
+ { metric: metricCodeLineCount, label: "Code Lines" },
+ { metric: metricCharacterCount, label: "Total Characters" },
+ { metric: metricCodeCharacterCount, label: "Code Characters" },
+ { metric: metricDependencyCount, label: "Dependencies" },
+ { metric: metricDependentCount, label: "Dependents" },
+ { metric: metricCyclomaticComplexity, label: "Complexity" },
+ ] as { metric: Metric | undefined; label: string }[]).map((val) => (
+ props.metricState?.setMetric?.(val.metric)}
+ >
+ {val.label}
+
+ ))}
+
+
+
+ Select a metric to display on the graph
+
+
+ );
+}
diff --git a/packages/app/src/components/controls/Controls.tsx b/packages/app/src/components/controls/Controls.tsx
new file mode 100644
index 00000000..ffdbbea0
--- /dev/null
+++ b/packages/app/src/components/controls/Controls.tsx
@@ -0,0 +1,102 @@
+import type { ReactNode } from "react";
+import { Button } from "../shadcn/Button.tsx";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../shadcn/Tooltip.tsx";
+import type { Core } from "cytoscape";
+import { Focus, Network, ZoomIn, ZoomOut } from "lucide-react";
+
+export default function Controls(props: {
+ busy: boolean;
+ cy: Core | undefined;
+ onLayout: () => void;
+ showFilters?: boolean;
+ children?: ReactNode;
+}) {
+ function handleFit() {
+ if (!props.cy) return;
+
+ const elements = props.cy.elements();
+ const padding = 10;
+ props.cy.center(elements).fit(elements, padding);
+ }
+
+ function handleZoom(zoom: number) {
+ if (!props.cy) return;
+
+ const level = props.cy.zoom() * zoom;
+ const x = props.cy.width() / 2;
+ const y = props.cy.height() / 2;
+ const renderedPosition = { x, y };
+
+ props.cy.zoom({
+ level,
+ renderedPosition,
+ });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Fit to screen
+
+
+
+
+ props.onLayout()}
+ >
+
+
+
+
+ Reset layout
+
+
+
+
+ handleZoom(0.9)}
+ >
+
+
+
+
+ Zoom out
+
+
+
+
+ handleZoom(1.1)}
+ >
+
+
+
+
+ Zoom in
+
+
+ {/* Used to pass extensions into the controls */}
+ {props.children}
+
+ );
+}
diff --git a/packages/app/src/components/detailsPanes/FileDetailsPane.tsx b/packages/app/src/components/detailsPanes/FileDetailsPane.tsx
new file mode 100644
index 00000000..9fa24ec2
--- /dev/null
+++ b/packages/app/src/components/detailsPanes/FileDetailsPane.tsx
@@ -0,0 +1,199 @@
+import type { FileAuditManifest, FileDependencyManifest } from "@napi/shared";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from "../shadcn/Sheet.tsx";
+import { Card, CardContent, CardHeader, CardTitle } from "../shadcn/Card.tsx";
+import { Code, File, Pickaxe, SearchCode } from "lucide-react";
+import { ScrollArea } from "../shadcn/Scrollarea.tsx";
+import { Button } from "../shadcn/Button.tsx";
+import { Link } from "react-router";
+import DisplayNameWithTooltip from "../DisplayNameWithTootip.tsx";
+import Metrics from "./metrics.tsx";
+import AlertBadge from "./alertBadge.tsx";
+
+export default function FileDetailsPane(props: {
+ context: {
+ fileDependencyManifest: FileDependencyManifest;
+ fileAuditManifest: FileAuditManifest;
+ } | undefined;
+ onClose: () => void;
+ onAddSymbolsForExtraction: (filePath: string, symbolIds: string[]) => void;
+}) {
+ function markAllSymbolsForExtraction() {
+ if (!props.context?.fileDependencyManifest) {
+ return;
+ }
+
+ props.onAddSymbolsForExtraction(
+ props.context?.fileDependencyManifest.filePath,
+ Object.keys(props.context.fileDependencyManifest.symbols),
+ );
+ }
+
+ function markSymbolForExtraction(symbolId: string) {
+ if (!props.context?.fileDependencyManifest) {
+ return;
+ }
+
+ props.onAddSymbolsForExtraction(
+ props.context.fileDependencyManifest.filePath,
+ [symbolId],
+ );
+ }
+
+ return (
+
+
props.onClose()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ View graph for this file
+
+
+
+
+ Mark all symbols for extraction
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Symbols ({Object.keys(
+ props.context?.fileDependencyManifest.symbols || {},
+ ).length || 0})
+
+
+
+
+
+
+ {Object.entries(
+ Object.values(
+ props.context?.fileDependencyManifest.symbols || [],
+ ).reduce((acc, symbol) => {
+ acc[symbol.type] = (acc[symbol.type] || 0) + 1;
+ return acc;
+ }, {} as Record
),
+ ).map(([type, count]) => (
+
+ ))}
+
+
+
+ {Object.values(
+ props.context?.fileDependencyManifest.symbols || {},
+ ).map((symbol) => (
+
+
+
+
+
+
+
+
+
+
+
+ View graph for this symbol
+
+
+ markSymbolForExtraction(symbol.id)}
+ variant="secondary"
+ size="sm"
+ >
+
+ Mark for extraction
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/packages/app/src/components/detailsPanes/SymbolDetailsPane.tsx b/packages/app/src/components/detailsPanes/SymbolDetailsPane.tsx
new file mode 100644
index 00000000..235156d9
--- /dev/null
+++ b/packages/app/src/components/detailsPanes/SymbolDetailsPane.tsx
@@ -0,0 +1,171 @@
+import type {
+ FileAuditManifest,
+ FileDependencyManifest,
+ SymbolAuditManifest,
+ SymbolDependencyManifest,
+} from "@napi/shared";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from "../shadcn/Sheet.tsx";
+import { Card, CardContent, CardHeader, CardTitle } from "../shadcn/Card.tsx";
+import { Code, File, Pickaxe, SearchCode } from "lucide-react";
+import { ScrollArea } from "../shadcn/Scrollarea.tsx";
+import { Button } from "../shadcn/Button.tsx";
+import { Link } from "react-router";
+import DisplayNameWithTooltip from "../DisplayNameWithTootip.tsx";
+import Metrics from "./metrics.tsx";
+import AlertBadge from "./alertBadge.tsx";
+
+export default function SymbolDetailsPane(props: {
+ context: {
+ fileDependencyManifest: FileDependencyManifest;
+ symbolDependencyManifest: SymbolDependencyManifest;
+ fileAuditManifest: FileAuditManifest;
+ symbolAuditManifest: SymbolAuditManifest;
+ } | undefined;
+ onClose: () => void;
+ onAddSymbolsForExtraction: (filePath: string, symbolIds: string[]) => void;
+}) {
+ function markSymbolForExtraction(symbolId: string) {
+ if (!props.context?.fileDependencyManifest) {
+ return;
+ }
+
+ props.onAddSymbolsForExtraction(
+ props.context.fileDependencyManifest.filePath,
+ [symbolId],
+ );
+ }
+
+ return (
+
+
props.onClose()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View graph for this symbol
+
+
+
+ props.context?.symbolDependencyManifest.id &&
+ markSymbolForExtraction(
+ props.context?.symbolDependencyManifest.id,
+ )}
+ variant="secondary"
+ size="sm"
+ >
+
+ Mark symbol for extraction
+
+
+
+
+ View graph for this file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/app/src/components/detailsPanes/alertBadge.tsx b/packages/app/src/components/detailsPanes/alertBadge.tsx
new file mode 100644
index 00000000..fcab2042
--- /dev/null
+++ b/packages/app/src/components/detailsPanes/alertBadge.tsx
@@ -0,0 +1,16 @@
+import { Check, TriangleAlert } from "lucide-react";
+
+export default function AlertBadge(props: { count: number }) {
+ return (
+
+ {props.count > 0
+ ? (
+ <>
+
+ {props.count}
+ >
+ )
+ : }
+
+ );
+}
diff --git a/packages/app/src/components/detailsPanes/metrics.tsx b/packages/app/src/components/detailsPanes/metrics.tsx
new file mode 100644
index 00000000..e1a44e35
--- /dev/null
+++ b/packages/app/src/components/detailsPanes/metrics.tsx
@@ -0,0 +1,69 @@
+import {
+ type FileAuditManifest,
+ type FileDependencyManifest,
+ metricCharacterCount,
+ metricCodeCharacterCount,
+ metricCodeLineCount,
+ metricCyclomaticComplexity,
+ metricDependencyCount,
+ metricDependentCount,
+ metricLinesCount,
+ type SymbolAuditManifest,
+ type SymbolDependencyManifest,
+} from "@napi/shared";
+import { Alert, AlertDescription } from "../shadcn/Alert.tsx";
+
+export default function Metrics(props: {
+ dependencyManifest:
+ | FileDependencyManifest
+ | SymbolDependencyManifest
+ | undefined;
+ auditManifest: FileAuditManifest | SymbolAuditManifest | undefined;
+}) {
+ function metricToHumanString(metric: string) {
+ switch (metric) {
+ case metricLinesCount:
+ return "Lines";
+ case metricCodeLineCount:
+ return "Code Lines";
+ case metricCharacterCount:
+ return "Characters";
+ case metricCodeCharacterCount:
+ return "Code Characters";
+ case metricDependencyCount:
+ return "Dependencies";
+ case metricDependentCount:
+ return "Dependents";
+ case metricCyclomaticComplexity:
+ return "Cyclomatic Complexity";
+ default:
+ return metric;
+ }
+ }
+
+ return (
+
+ {Object.entries(props.dependencyManifest?.metrics || {}).map((
+ [key, value],
+ ) => (
+
+
+
+ {metricToHumanString(key)}
+
+
+ {value}
+
+
+ {(props.auditManifest?.alerts || {})?.[key] && (
+
+
+ {props.auditManifest?.alerts?.[key]?.message?.long}
+
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/packages/app/src/components/shadcn/Alert.tsx b/packages/app/src/components/shadcn/Alert.tsx
new file mode 100644
index 00000000..f80fec6d
--- /dev/null
+++ b/packages/app/src/components/shadcn/Alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "../../lib/utils.ts";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertDescription, AlertTitle };
diff --git a/packages/app/src/components/shadcn/Breadcrumb.tsx b/packages/app/src/components/shadcn/Breadcrumb.tsx
new file mode 100644
index 00000000..05f4a7f6
--- /dev/null
+++ b/packages/app/src/components/shadcn/Breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "../../lib/utils.ts";
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = "Breadcrumb";
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbList.displayName = "BreadcrumbList";
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbItem.displayName = "BreadcrumbItem";
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+
+ );
+});
+BreadcrumbLink.displayName = "BreadcrumbLink";
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbPage.displayName = "BreadcrumbPage";
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+
+export {
+ Breadcrumb,
+ BreadcrumbEllipsis,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+};
diff --git a/packages/app/src/components/shadcn/Button.tsx b/packages/app/src/components/shadcn/Button.tsx
new file mode 100644
index 00000000..e6687a33
--- /dev/null
+++ b/packages/app/src/components/shadcn/Button.tsx
@@ -0,0 +1,58 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "../../lib/utils.ts";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends
+ React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/packages/app/src/components/shadcn/Card.tsx b/packages/app/src/components/shadcn/Card.tsx
new file mode 100644
index 00000000..a24eaad5
--- /dev/null
+++ b/packages/app/src/components/shadcn/Card.tsx
@@ -0,0 +1,83 @@
+import * as React from "react";
+
+import { cn } from "../../lib/utils.ts";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+};
diff --git a/packages/app/src/components/shadcn/Dropdownmenu.tsx b/packages/app/src/components/shadcn/Dropdownmenu.tsx
new file mode 100644
index 00000000..7e539898
--- /dev/null
+++ b/packages/app/src/components/shadcn/Dropdownmenu.tsx
@@ -0,0 +1,201 @@
+"use client";
+
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+
+import { cn } from "../../lib/utils.ts";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+ svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+};
diff --git a/packages/app/src/components/shadcn/Input.tsx b/packages/app/src/components/shadcn/Input.tsx
new file mode 100644
index 00000000..a7a053d5
--- /dev/null
+++ b/packages/app/src/components/shadcn/Input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+import { cn } from "../../lib/utils.ts";
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/packages/app/src/components/shadcn/Label.tsx b/packages/app/src/components/shadcn/Label.tsx
new file mode 100644
index 00000000..c7fd301a
--- /dev/null
+++ b/packages/app/src/components/shadcn/Label.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "../../lib/utils.ts";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ & React.ComponentPropsWithoutRef
+ & VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/packages/app/src/components/shadcn/README.md b/packages/app/src/components/shadcn/README.md
new file mode 100644
index 00000000..49d8ff28
--- /dev/null
+++ b/packages/app/src/components/shadcn/README.md
@@ -0,0 +1,18 @@
+# Shadcn UI Components
+
+These components are imported from [shadcn/ui](https://ui.shadcn.com/) and
+should not be modified directly.
+
+## Usage
+
+These components are part of our design system and follow the shadcn/ui
+implementation pattern. They are carefully styled and configured to work with
+our application's theme.
+
+## Important Notes
+
+Modification of these files can have consequenses on other shadcn/ui components.
+Make sure to double check before mofifying these
+
+For more information on how to use or customize these components, refer to the
+[shadcn/ui documentation](https://ui.shadcn.com/docs).
diff --git a/packages/app/src/components/shadcn/Resizable.tsx b/packages/app/src/components/shadcn/Resizable.tsx
new file mode 100644
index 00000000..0da61fec
--- /dev/null
+++ b/packages/app/src/components/shadcn/Resizable.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { GripVertical } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "../../lib/utils.ts";
+
+const ResizablePanelGroup = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+);
+
+const ResizablePanel = ResizablePrimitive.Panel;
+
+const ResizableHandle = ({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean;
+}) => (
+ div]:rotate-90",
+ className,
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+);
+
+export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
diff --git a/packages/app/src/components/shadcn/Scrollarea.tsx b/packages/app/src/components/shadcn/Scrollarea.tsx
new file mode 100644
index 00000000..4364d158
--- /dev/null
+++ b/packages/app/src/components/shadcn/Scrollarea.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+
+import { cn } from "../../lib/utils.ts";
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/packages/app/src/components/shadcn/Separator.tsx b/packages/app/src/components/shadcn/Separator.tsx
new file mode 100644
index 00000000..13268e40
--- /dev/null
+++ b/packages/app/src/components/shadcn/Separator.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "../../lib/utils.ts";
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref,
+ ) => (
+
+ ),
+);
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/packages/app/src/components/shadcn/Sheet.tsx b/packages/app/src/components/shadcn/Sheet.tsx
new file mode 100644
index 00000000..05103d46
--- /dev/null
+++ b/packages/app/src/components/shadcn/Sheet.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+
+import { cn } from "../../lib/utils.ts";
+
+const Sheet = SheetPrimitive.Root;
+
+const SheetTrigger = SheetPrimitive.Trigger;
+
+const SheetClose = SheetPrimitive.Close;
+
+const SheetPortal = SheetPrimitive.Portal;
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top:
+ "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left:
+ "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ },
+);
+
+interface SheetContentProps
+ extends
+ React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+
+
+ Close
+
+ {children}
+
+
+));
+SheetContent.displayName = SheetPrimitive.Content.displayName;
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = "SheetHeader";
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetFooter.displayName = "SheetFooter";
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetClose,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetOverlay,
+ SheetPortal,
+ SheetTitle,
+ SheetTrigger,
+};
diff --git a/packages/app/src/components/shadcn/Sidebar.tsx b/packages/app/src/components/shadcn/Sidebar.tsx
new file mode 100644
index 00000000..3e40a9be
--- /dev/null
+++ b/packages/app/src/components/shadcn/Sidebar.tsx
@@ -0,0 +1,776 @@
+"use client";
+
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeft } from "lucide-react";
+
+import { useIsMobile } from "./hooks/use-mobile.tsx";
+import { cn } from "../../lib/utils.ts";
+import { Button } from "./Button.tsx";
+import { Input } from "./Input.tsx";
+import { Separator } from "./Separator.tsx";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "./Sheet.tsx";
+import { Skeleton } from "./Skeleton.tsx";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "./Tooltip.tsx";
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext(null);
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.");
+ }
+
+ return context;
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value;
+ if (setOpenProp) {
+ setOpenProp(openState);
+ } else {
+ _setOpen(openState);
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie =
+ `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open],
+ );
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
+
+ globalThis.addEventListener("keydown", handleKeyDown);
+ return () => globalThis.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed";
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ ],
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ },
+);
+SidebarProvider.displayName = "SidebarProvider";
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ );
+ },
+);
+Sidebar.displayName = "Sidebar";
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+ {
+ onClick?.(event);
+ toggleSidebar();
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ );
+});
+SidebarTrigger.displayName = "SidebarTrigger";
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+SidebarRail.displayName = "SidebarRail";
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInset.displayName = "SidebarInset";
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInput.displayName = "SidebarInput";
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarHeader.displayName = "SidebarHeader";
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarFooter.displayName = "SidebarFooter";
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarSeparator.displayName = "SidebarSeparator";
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarContent.displayName = "SidebarContent";
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarGroup.displayName = "SidebarGroup";
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupLabel.displayName = "SidebarGroupLabel";
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupAction.displayName = "SidebarGroupAction";
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarGroupContent.displayName = "SidebarGroupContent";
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenu.displayName = "SidebarMenu";
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuItem.displayName = "SidebarMenuItem";
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+ },
+);
+SidebarMenuButton.displayName = "SidebarMenuButton";
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuAction.displayName = "SidebarMenuAction";
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuBadge.displayName = "SidebarMenuBadge";
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ );
+});
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuSub.displayName = "SidebarMenuSub";
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => );
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/packages/app/src/components/shadcn/Skeleton.tsx b/packages/app/src/components/shadcn/Skeleton.tsx
new file mode 100644
index 00000000..20cc560d
--- /dev/null
+++ b/packages/app/src/components/shadcn/Skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "../../lib/utils.ts";
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/packages/app/src/components/shadcn/Slider.tsx b/packages/app/src/components/shadcn/Slider.tsx
new file mode 100644
index 00000000..556abc10
--- /dev/null
+++ b/packages/app/src/components/shadcn/Slider.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import * as SliderPrimitive from "@radix-ui/react-slider";
+
+import { cn } from "../../lib/utils.ts";
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+));
+Slider.displayName = SliderPrimitive.Root.displayName;
+
+export { Slider };
diff --git a/packages/app/src/components/shadcn/Toast.tsx b/packages/app/src/components/shadcn/Toast.tsx
new file mode 100644
index 00000000..87861ed9
--- /dev/null
+++ b/packages/app/src/components/shadcn/Toast.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import * as React from "react";
+import * as ToastPrimitives from "@radix-ui/react-toast";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+
+import { cn } from "../../lib/utils.ts";
+
+const ToastProvider = ToastPrimitives.Provider;
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ & React.ComponentPropsWithoutRef
+ & VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ );
+});
+Toast.displayName = ToastPrimitives.Root.displayName;
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastAction.displayName = ToastPrimitives.Action.displayName;
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+ToastClose.displayName = ToastPrimitives.Close.displayName;
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastTitle.displayName = ToastPrimitives.Title.displayName;
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastDescription.displayName = ToastPrimitives.Description.displayName;
+
+type ToastProps = React.ComponentPropsWithoutRef;
+
+type ToastActionElement = React.ReactElement;
+
+export {
+ Toast,
+ ToastAction,
+ type ToastActionElement,
+ ToastClose,
+ ToastDescription,
+ type ToastProps,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+};
diff --git a/packages/app/src/components/shadcn/Toaster.tsx b/packages/app/src/components/shadcn/Toaster.tsx
new file mode 100644
index 00000000..39d06d71
--- /dev/null
+++ b/packages/app/src/components/shadcn/Toaster.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { useToast } from "./hooks/use-toast.tsx";
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from "./Toast.tsx";
+
+export function Toaster() {
+ const { toasts } = useToast();
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title} }
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/packages/app/src/components/shadcn/Tooltip.tsx b/packages/app/src/components/shadcn/Tooltip.tsx
new file mode 100644
index 00000000..7e2d3805
--- /dev/null
+++ b/packages/app/src/components/shadcn/Tooltip.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "../../lib/utils.ts";
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/packages/app/src/components/shadcn/hooks/use-mobile.tsx b/packages/app/src/components/shadcn/hooks/use-mobile.tsx
new file mode 100644
index 00000000..0606ed40
--- /dev/null
+++ b/packages/app/src/components/shadcn/hooks/use-mobile.tsx
@@ -0,0 +1,23 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(
+ undefined,
+ );
+
+ React.useEffect(() => {
+ const mql = globalThis.matchMedia(
+ `(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
+ );
+ const onChange = () => {
+ setIsMobile(globalThis.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(globalThis.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/packages/app/src/components/shadcn/hooks/use-toast.tsx b/packages/app/src/components/shadcn/hooks/use-toast.tsx
new file mode 100644
index 00000000..266573c8
--- /dev/null
+++ b/packages/app/src/components/shadcn/hooks/use-toast.tsx
@@ -0,0 +1,191 @@
+"use client";
+
+// Inspired by react-hot-toast library
+import * as React from "react";
+
+import type { ToastActionElement, ToastProps } from "../Toast.tsx";
+
+const TOAST_LIMIT = 1;
+const TOAST_REMOVE_DELAY = 1000000;
+
+type ToasterToast = ToastProps & {
+ id: string;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ action?: ToastActionElement;
+};
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const;
+
+let count = 0;
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
+ return count.toString();
+}
+
+type ActionType = typeof actionTypes;
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"];
+ toast: ToasterToast;
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"];
+ toast: Partial;
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"];
+ toastId?: ToasterToast["id"];
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"];
+ toastId?: ToasterToast["id"];
+ };
+
+interface State {
+ toasts: ToasterToast[];
+}
+
+const toastTimeouts = new Map>();
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return;
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId);
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ });
+ }, TOAST_REMOVE_DELAY);
+
+ toastTimeouts.set(toastId, timeout);
+};
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ };
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ };
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action;
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId);
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id);
+ });
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ };
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ };
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ };
+ }
+};
+
+const listeners: Array<(state: State) => void> = [];
+
+let memoryState: State = { toasts: [] };
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action);
+ listeners.forEach((listener) => {
+ listener(memoryState);
+ });
+}
+
+type Toast = Omit;
+
+function toast({ ...props }: Toast) {
+ const id = genId();
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ });
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss();
+ },
+ },
+ });
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ };
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState);
+
+ React.useEffect(() => {
+ listeners.push(setState);
+ return () => {
+ const index = listeners.indexOf(setState);
+ if (index > -1) {
+ listeners.splice(index, 1);
+ }
+ };
+ }, [state]);
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ };
+}
+
+export { toast, useToast };
diff --git a/packages/app/src/contexts/ThemeContext.tsx b/packages/app/src/contexts/ThemeContext.tsx
deleted file mode 100644
index a0384a14..00000000
--- a/packages/app/src/contexts/ThemeContext.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { createContext, type ReactNode, useState } from "react";
-
-export const lightTheme = "light";
-export const darkTheme = "dark";
-export type Theme = typeof lightTheme | typeof darkTheme;
-
-export const ThemeContext = createContext<{
- theme: Theme;
- changeTheme: (newTheme: Theme) => void;
-}>({
- theme: lightTheme,
- changeTheme: () => {},
-});
-
-export function ThemeProvider({ children }: { children: ReactNode }) {
- function getModeFromLocalStorage() {
- if (
- localStorage.theme === darkTheme ||
- (!("theme" in localStorage) &&
- globalThis.matchMedia("(prefers-color-scheme: dark)").matches)
- ) {
- localStorage.theme = darkTheme;
- document.documentElement.classList.add(darkTheme);
- return darkTheme;
- }
- return lightTheme;
- }
-
- const [theme, setTheme] = useState(getModeFromLocalStorage());
-
- function changeTheme(newTheme: Theme) {
- localStorage.theme = newTheme;
- setTheme(newTheme);
- if (newTheme === lightTheme) {
- document.documentElement.classList.remove(darkTheme);
- } else {
- document.documentElement.classList.add(darkTheme);
- }
- }
-
- return (
-
- {children}
-
- );
-}
diff --git a/packages/app/src/contexts/ThemeProvider.tsx b/packages/app/src/contexts/ThemeProvider.tsx
new file mode 100644
index 00000000..cfdfb539
--- /dev/null
+++ b/packages/app/src/contexts/ThemeProvider.tsx
@@ -0,0 +1,69 @@
+import { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "dark" | "light";
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+function getSystemTheme() {
+ const systemTheme = globalThis.matchMedia("(prefers-color-scheme: dark)");
+ return systemTheme.matches ? "dark" : "light";
+}
+
+const initialState: ThemeProviderState = {
+ theme: getSystemTheme(),
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = getSystemTheme(),
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
+ );
+
+ useEffect(() => {
+ const root = globalThis.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+
+ return context;
+};
diff --git a/packages/app/src/helpers/css/index.ts b/packages/app/src/helpers/css/index.ts
deleted file mode 100644
index 7c081980..00000000
--- a/packages/app/src/helpers/css/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export function getCssValue(name: string) {
- const root = document.documentElement;
- const computedStyle = globalThis.getComputedStyle(root);
- return computedStyle.getPropertyValue(name);
-}
diff --git a/packages/app/src/helpers/cytoscape/elements/file.ts b/packages/app/src/helpers/cytoscape/elements/file.ts
new file mode 100644
index 00000000..b92b5ad0
--- /dev/null
+++ b/packages/app/src/helpers/cytoscape/elements/file.ts
@@ -0,0 +1,303 @@
+import type {
+ AuditManifest,
+ DependencyManifest,
+ FileAuditManifest,
+ FileDependencyManifest,
+ metricCharacterCount,
+ metricCodeCharacterCount,
+ metricCodeLineCount,
+ metricCyclomaticComplexity,
+ metricDependencyCount,
+ metricDependentCount,
+ metricLinesCount,
+ SymbolDependencyManifest,
+ SymbolType,
+} from "@napi/shared";
+import {
+ getCollapsedSymbolNodeLabel,
+ getExpandedSymbolNodeLabel,
+ getNodeWidthAndHeightFromLabel,
+} from "../label/index.ts";
+import { getMetricsSeverityForNode } from "../metrics/index.ts";
+import type { SymbolNapiNodeData } from "./types.ts";
+import type { EdgeDefinition, NodeDefinition } from "cytoscape";
+
+export function computeNodeId(fileId: string, symbolId: string) {
+ return `${fileId}:${symbolId}`;
+}
+
+function createNodeData(params: {
+ id: string;
+ fileName: string;
+ symbolName: string;
+ symbolType: string;
+ isExternal: boolean;
+ metricsSeverity: {
+ [metricLinesCount]: number;
+ [metricCodeLineCount]: number;
+ [metricCodeCharacterCount]: number;
+ [metricCharacterCount]: number;
+ [metricDependencyCount]: number;
+ [metricDependentCount]: number;
+ [metricCyclomaticComplexity]: number;
+ };
+ expandedLabel: string;
+ collapsedLabel: string;
+}): SymbolNapiNodeData {
+ // Calculate dimensions for expanded and collapsed views
+ const { width: expandedWidth, height: expandedHeight } =
+ getNodeWidthAndHeightFromLabel(
+ params.expandedLabel,
+ );
+
+ const { width: collapsedWidth, height: collapsedHeight } =
+ getNodeWidthAndHeightFromLabel(
+ params.collapsedLabel,
+ );
+
+ // Create the node data structure
+ return {
+ id: params.id,
+ position: { x: 0, y: 0 },
+ fileName: params.fileName,
+ isExternal: params.isExternal,
+ symbolName: params.symbolName,
+ symbolType: params.symbolType,
+ metricsSeverity: params.metricsSeverity,
+ expanded: {
+ label: params.expandedLabel,
+ width: expandedWidth,
+ height: expandedHeight,
+ },
+ collapsed: {
+ label: params.collapsedLabel,
+ width: collapsedWidth,
+ height: collapsedHeight,
+ },
+ };
+}
+
+interface CustomNodeDefinition extends NodeDefinition {
+ data: SymbolNapiNodeData & object;
+}
+
+function processDependencies(
+ symbol: SymbolDependencyManifest,
+ symbolNodeId: string,
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+ nodes: CustomNodeDefinition[],
+ edges: EdgeDefinition[],
+) {
+ Object.values(symbol.dependencies).forEach((dep) => {
+ let depDependencyManifest: FileDependencyManifest | undefined;
+ let depAuditManifest: FileAuditManifest | undefined;
+
+ if (!dep.isExternal) {
+ depDependencyManifest = dependencyManifest[dep.id];
+ depAuditManifest = auditManifest[dep.id];
+ }
+
+ // For each symbol this depends on, create an edge
+ Object.keys(dep.symbols).forEach((depSymbolName) => {
+ const depSymbolNodeId = computeNodeId(dep.id, depSymbolName);
+
+ // Check if node already exists
+ const existingNode = nodes.find(
+ (node) => node.data.id === depSymbolNodeId,
+ );
+
+ if (!existingNode) {
+ let depSymbolType: SymbolType | "unknown" = "unknown";
+ if (depDependencyManifest) {
+ depSymbolType = depDependencyManifest.symbols[depSymbolName].type;
+ }
+
+ // Create label for dependency nodes
+ const expandedLabel = getExpandedSymbolNodeLabel({
+ currentFileId: dep.id,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ symbolAuditManifest: depAuditManifest?.symbols[depSymbolName],
+ });
+
+ const collapsedLabel = getCollapsedSymbolNodeLabel({
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ });
+
+ const metricsSeverity = getMetricsSeverityForNode(
+ depAuditManifest?.symbols[depSymbolName],
+ );
+
+ const nodeData = createNodeData({
+ id: depSymbolNodeId,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ isExternal: dep.isExternal,
+ metricsSeverity,
+ expandedLabel,
+ collapsedLabel,
+ });
+
+ nodes.push({ data: nodeData });
+ }
+
+ // Add the edge
+ const edgeId = `${depSymbolNodeId}->${symbolNodeId}`;
+ edges.push({
+ data: {
+ id: edgeId,
+ source: depSymbolNodeId,
+ target: symbolNodeId,
+ },
+ });
+ });
+ });
+}
+
+function processDependents(
+ symbol: SymbolDependencyManifest,
+ symbolNodeId: string,
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+ nodes: CustomNodeDefinition[],
+ edges: EdgeDefinition[],
+) {
+ Object.values(symbol.dependents).forEach((dep) => {
+ const depDependencyManifest = dependencyManifest[dep.id];
+ const depAuditManifest = auditManifest[dep.id];
+
+ Object.keys(dep.symbols).forEach((depSymbolName) => {
+ const depSymbolNodeId = computeNodeId(dep.id, depSymbolName);
+
+ // Check if node already exists
+ const existingNode = nodes.find(
+ (node) => node.data.id === depSymbolNodeId,
+ );
+
+ if (!existingNode) {
+ const depSymbolType = depDependencyManifest.symbols[depSymbolName].type;
+
+ // Create label for dependent nodes
+ const expandedLabel = getExpandedSymbolNodeLabel({
+ currentFileId: dep.id,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ symbolAuditManifest: depAuditManifest?.symbols[depSymbolName],
+ });
+
+ const metricsSeverity = getMetricsSeverityForNode(
+ depAuditManifest?.symbols[depSymbolName],
+ );
+
+ const isExternal = !dependencyManifest[dep.id];
+
+ const nodeData = createNodeData({
+ id: depSymbolNodeId,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ isExternal,
+ metricsSeverity,
+ expandedLabel,
+ collapsedLabel: depSymbolName,
+ });
+
+ nodes.push({ data: nodeData });
+ }
+
+ // Add the edge
+ const edgeId = `${symbolNodeId}->${depSymbolNodeId}`;
+ edges.push({
+ data: {
+ id: edgeId,
+ source: symbolNodeId,
+ target: depSymbolNodeId,
+ },
+ });
+ });
+ });
+}
+
+export function getSymbolElementsInFile(
+ fileId: string,
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+) {
+ const fileManifest = dependencyManifest[fileId];
+ if (!fileManifest) {
+ console.error(`File manifest not found for ${fileId}`);
+ return [];
+ }
+
+ const fileAuditManifest = auditManifest[fileId];
+ const nodes: CustomNodeDefinition[] = [];
+ const edges: EdgeDefinition[] = [];
+
+ // First pass: Create nodes for each symbol in the file
+ Object.values(fileManifest.symbols).forEach((symbol) => {
+ const symbolAuditManifest = fileAuditManifest.symbols[symbol.id];
+ const symbolNodeId = computeNodeId(fileId, symbol.id);
+
+ // Create labels for the symbol
+ const expandedLabel = getExpandedSymbolNodeLabel({
+ currentFileId: fileId,
+ fileName: fileId,
+ symbolName: symbol.id,
+ symbolType: symbol.type,
+ symbolAuditManifest,
+ });
+ const collapsedLabel = getCollapsedSymbolNodeLabel({
+ symbolName: symbol.id,
+ symbolType: symbol.type,
+ });
+
+ const metricsSeverity = getMetricsSeverityForNode(symbolAuditManifest);
+
+ const isExternal = !dependencyManifest[fileId];
+
+ const nodeData = createNodeData({
+ id: symbolNodeId,
+ fileName: fileId,
+ symbolName: symbol.id,
+ symbolType: symbol.type,
+ isExternal,
+ metricsSeverity,
+ expandedLabel,
+ collapsedLabel,
+ });
+
+ nodes.push({ data: nodeData });
+ });
+
+ // Second pass: Create nodes and edges for dependencies and dependents
+ Object.values(fileManifest.symbols).forEach((symbol) => {
+ const symbolNodeId = computeNodeId(fileId, symbol.id);
+
+ // Process dependencies
+ processDependencies(
+ symbol,
+ symbolNodeId,
+ dependencyManifest,
+ auditManifest,
+ nodes,
+ edges,
+ );
+
+ // Process dependents
+ processDependents(
+ symbol,
+ symbolNodeId,
+ dependencyManifest,
+ auditManifest,
+ nodes,
+ edges,
+ );
+ });
+
+ return [...nodes, ...edges];
+}
diff --git a/packages/app/src/helpers/cytoscape/elements/project.ts b/packages/app/src/helpers/cytoscape/elements/project.ts
new file mode 100644
index 00000000..e41b2ba1
--- /dev/null
+++ b/packages/app/src/helpers/cytoscape/elements/project.ts
@@ -0,0 +1,94 @@
+import type {
+ EdgeDefinition,
+ ElementDefinition,
+ NodeDefinition,
+} from "cytoscape";
+import type { AuditManifest, DependencyManifest } from "@napi/shared";
+import {
+ getCollapsedFileNodeLabel,
+ getExpandedFileNodeLabel,
+ getNodeWidthAndHeightFromLabel,
+} from "../label/index.ts";
+import { getMetricsSeverityForNode } from "../metrics/index.ts";
+import type { FileNapiNodeData } from "./types.ts";
+
+export function getFileElementsInProject(
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+): ElementDefinition[] {
+ interface CustomNodeDefinition extends NodeDefinition {
+ data: FileNapiNodeData & object;
+ }
+
+ const nodes: CustomNodeDefinition[] = [];
+ const edges: EdgeDefinition[] = [];
+
+ Object.values(dependencyManifest).forEach((fileDependencyManifest) => {
+ const fileAuditManifest = auditManifest[fileDependencyManifest.id];
+
+ const expandedLabel = getExpandedFileNodeLabel({
+ fileName: fileDependencyManifest.id,
+ fileAuditManifest,
+ });
+ const { width: expandedWitdh, height: expandedHeight } =
+ getNodeWidthAndHeightFromLabel(
+ expandedLabel,
+ );
+
+ const collapsedLabel = getCollapsedFileNodeLabel({
+ fileName: fileDependencyManifest.id,
+ fileAuditManifest,
+ });
+ const { width: collapsedWidth, height: collapsedHeight } =
+ getNodeWidthAndHeightFromLabel(
+ collapsedLabel,
+ );
+
+ const metricsColors = getMetricsSeverityForNode(fileAuditManifest);
+
+ const nodeElement: CustomNodeDefinition = {
+ data: {
+ id: fileDependencyManifest.id,
+ // initial node position - will be updated by layout
+ position: { x: 0, y: 0 },
+ fileName: fileDependencyManifest.id,
+ isExternal: false,
+ metricsSeverity: metricsColors,
+ expanded: {
+ label: expandedLabel,
+ width: expandedWitdh,
+ height: expandedHeight,
+ },
+ collapsed: {
+ label: collapsedLabel,
+ width: collapsedWidth,
+ height: collapsedHeight,
+ },
+ },
+ };
+
+ nodes.push(nodeElement);
+
+ for (
+ const fileDependency of Object.values(fileDependencyManifest.dependencies)
+ ) {
+ if (fileDependency.isExternal) {
+ continue;
+ }
+
+ if (fileDependency.id === fileDependencyManifest.id) {
+ // ignore self-references
+ continue;
+ }
+
+ edges.push({
+ data: {
+ source: fileDependencyManifest.id,
+ target: fileDependency.id,
+ },
+ });
+ }
+ });
+
+ return [...nodes, ...edges];
+}
diff --git a/packages/app/src/helpers/cytoscape/elements/symbol.ts b/packages/app/src/helpers/cytoscape/elements/symbol.ts
new file mode 100644
index 00000000..698d18df
--- /dev/null
+++ b/packages/app/src/helpers/cytoscape/elements/symbol.ts
@@ -0,0 +1,346 @@
+import type {
+ AuditManifest,
+ DependencyManifest,
+ FileAuditManifest,
+ FileDependencyManifest,
+ metricCharacterCount,
+ metricCodeCharacterCount,
+ metricCodeLineCount,
+ metricCyclomaticComplexity,
+ metricDependencyCount,
+ metricDependentCount,
+ metricLinesCount,
+ SymbolDependencyManifest,
+ SymbolType,
+} from "@napi/shared";
+import {
+ getCollapsedSymbolNodeLabel,
+ getExpandedSymbolNodeLabel,
+ getNodeWidthAndHeightFromLabel,
+} from "../label/index.ts";
+import { getMetricsSeverityForNode } from "../metrics/index.ts";
+import type { SymbolNapiNodeData } from "./types.ts";
+import type { EdgeDefinition, NodeDefinition } from "cytoscape";
+import { computeNodeId } from "./file.ts";
+
+function createNodeData(params: {
+ id: string;
+ fileName: string;
+ symbolName: string;
+ symbolType: string;
+ isExternal: boolean;
+ metricsSeverity: {
+ [metricLinesCount]: number;
+ [metricCodeLineCount]: number;
+ [metricCodeCharacterCount]: number;
+ [metricCharacterCount]: number;
+ [metricDependencyCount]: number;
+ [metricDependentCount]: number;
+ [metricCyclomaticComplexity]: number;
+ };
+ expandedLabel: string;
+ collapsedLabel: string;
+}): SymbolNapiNodeData {
+ // Calculate dimensions for expanded and collapsed views
+ const { width: expandedWidth, height: expandedHeight } =
+ getNodeWidthAndHeightFromLabel(
+ params.expandedLabel,
+ );
+
+ const { width: collapsedWidth, height: collapsedHeight } =
+ getNodeWidthAndHeightFromLabel(
+ params.collapsedLabel,
+ );
+
+ // Create the node data structure
+ return {
+ id: params.id,
+ position: { x: 0, y: 0 },
+ fileName: params.fileName,
+ isExternal: params.isExternal,
+ symbolName: params.symbolName,
+ symbolType: params.symbolType,
+ metricsSeverity: params.metricsSeverity,
+ expanded: {
+ label: params.expandedLabel,
+ width: expandedWidth,
+ height: expandedHeight,
+ },
+ collapsed: {
+ label: params.collapsedLabel,
+ width: collapsedWidth,
+ height: collapsedHeight,
+ },
+ };
+}
+
+interface CustomNodeDefinition extends NodeDefinition {
+ data: SymbolNapiNodeData & object;
+}
+
+function traverseSymbolGraph(
+ symbol: SymbolDependencyManifest,
+ symbolNodeId: string,
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+ nodeMap: Map,
+ edgeMap: Map,
+ currentDepth: number,
+ maxDepsDepth: number,
+ maxDependentsDepth: number,
+ visited: Set,
+) {
+ // Process dependencies if we haven't reached max depth
+ if (currentDepth < maxDepsDepth) {
+ Object.values(symbol.dependencies).forEach((dep) => {
+ let depDependencyManifest: FileDependencyManifest | undefined;
+ let depAuditManifest: FileAuditManifest | undefined;
+
+ if (!dep.isExternal) {
+ depDependencyManifest = dependencyManifest[dep.id];
+ depAuditManifest = auditManifest[dep.id];
+ }
+
+ Object.keys(dep.symbols).forEach((depSymbolName) => {
+ const depSymbolNodeId = computeNodeId(dep.id, depSymbolName);
+
+ // Skip if already visited
+ if (visited.has(depSymbolNodeId)) {
+ // Add edge even if node was already visited
+ const edgeId = `${depSymbolNodeId}->${symbolNodeId}`;
+ edgeMap.set(edgeId, {
+ data: {
+ id: edgeId,
+ source: depSymbolNodeId,
+ target: symbolNodeId,
+ },
+ });
+ return;
+ }
+ visited.add(depSymbolNodeId);
+
+ let depSymbolType: SymbolType | "unknown" = "unknown";
+ if (depDependencyManifest) {
+ depSymbolType = depDependencyManifest.symbols[depSymbolName].type;
+ }
+
+ const expandedLabel = getExpandedSymbolNodeLabel({
+ currentFileId: dep.id,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ symbolAuditManifest: depAuditManifest?.symbols[depSymbolName],
+ });
+
+ const collapsedLabel = getCollapsedSymbolNodeLabel({
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ });
+
+ const metricsSeverity = getMetricsSeverityForNode(
+ depAuditManifest?.symbols[depSymbolName],
+ );
+
+ const nodeData = createNodeData({
+ id: depSymbolNodeId,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ isExternal: dep.isExternal,
+ metricsSeverity,
+ expandedLabel,
+ collapsedLabel,
+ });
+
+ nodeMap.set(depSymbolNodeId, { data: nodeData });
+
+ // Add the edge
+ const edgeId = `${depSymbolNodeId}->${symbolNodeId}`;
+ edgeMap.set(edgeId, {
+ data: {
+ id: edgeId,
+ source: depSymbolNodeId,
+ target: symbolNodeId,
+ },
+ });
+
+ // Recursively process dependencies if not external
+ if (!dep.isExternal && depDependencyManifest) {
+ traverseSymbolGraph(
+ depDependencyManifest.symbols[depSymbolName],
+ depSymbolNodeId,
+ dependencyManifest,
+ auditManifest,
+ nodeMap,
+ edgeMap,
+ currentDepth + 1,
+ maxDepsDepth,
+ maxDependentsDepth,
+ visited,
+ );
+ }
+ });
+ });
+ }
+
+ // Process dependents if we haven't reached max depth
+ if (currentDepth < maxDependentsDepth) {
+ Object.values(symbol.dependents).forEach((dep) => {
+ const depDependencyManifest = dependencyManifest[dep.id];
+ const depAuditManifest = auditManifest[dep.id];
+
+ Object.keys(dep.symbols).forEach((depSymbolName) => {
+ const depSymbolNodeId = computeNodeId(dep.id, depSymbolName);
+
+ // Skip if already visited
+ if (visited.has(depSymbolNodeId)) {
+ // Add edge even if node was already visited
+ const edgeId = `${symbolNodeId}->${depSymbolNodeId}`;
+ edgeMap.set(edgeId, {
+ data: {
+ id: edgeId,
+ source: symbolNodeId,
+ target: depSymbolNodeId,
+ },
+ });
+ return;
+ }
+ visited.add(depSymbolNodeId);
+
+ const depSymbolType = depDependencyManifest.symbols[depSymbolName].type;
+
+ const expandedLabel = getExpandedSymbolNodeLabel({
+ currentFileId: dep.id,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ symbolAuditManifest: depAuditManifest?.symbols[depSymbolName],
+ });
+
+ const metricsSeverity = getMetricsSeverityForNode(
+ depAuditManifest?.symbols[depSymbolName],
+ );
+
+ const isExternal = !dependencyManifest[dep.id];
+
+ const nodeData = createNodeData({
+ id: depSymbolNodeId,
+ fileName: dep.id,
+ symbolName: depSymbolName,
+ symbolType: depSymbolType,
+ isExternal,
+ metricsSeverity,
+ expandedLabel,
+ collapsedLabel: depSymbolName,
+ });
+
+ nodeMap.set(depSymbolNodeId, { data: nodeData });
+
+ // Add the edge
+ const edgeId = `${symbolNodeId}->${depSymbolNodeId}`;
+ edgeMap.set(edgeId, {
+ data: {
+ id: edgeId,
+ source: symbolNodeId,
+ target: depSymbolNodeId,
+ },
+ });
+
+ // Recursively process dependents
+ if (depDependencyManifest) {
+ traverseSymbolGraph(
+ depDependencyManifest.symbols[depSymbolName],
+ depSymbolNodeId,
+ dependencyManifest,
+ auditManifest,
+ nodeMap,
+ edgeMap,
+ currentDepth + 1,
+ maxDepsDepth,
+ maxDependentsDepth,
+ visited,
+ );
+ }
+ });
+ });
+ }
+}
+
+export function getSymbolElementsForSymbol(
+ fileName: string,
+ symbolId: string,
+ dependencyDepth: number,
+ dependentDepth: number,
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+) {
+ const fileManifest = dependencyManifest[fileName];
+ if (!fileManifest) {
+ console.error(`File manifest not found for ${fileName}`);
+ return [];
+ }
+ const symbolManifest = fileManifest.symbols[symbolId];
+ if (!symbolManifest) {
+ console.error(`Symbol manifest not found for ${symbolId}`);
+ return [];
+ }
+
+ const fileAuditManifest = auditManifest[fileName];
+ const symbolAuditManifest = fileAuditManifest.symbols[symbolId];
+
+ const nodeMap: Map = new Map();
+ const edgeMap: Map = new Map();
+ const visited = new Set();
+
+ const symbolNodeId = computeNodeId(fileName, symbolManifest.id);
+ visited.add(symbolNodeId);
+
+ // Create labels for the symbol
+ const expandedLabel = getExpandedSymbolNodeLabel({
+ currentFileId: fileName,
+ fileName,
+ symbolName: symbolManifest.id,
+ symbolType: symbolManifest.type,
+ symbolAuditManifest,
+ });
+ const collapsedLabel = getCollapsedSymbolNodeLabel({
+ symbolName: symbolManifest.id,
+ symbolType: symbolManifest.type,
+ });
+
+ const metricsSeverity = getMetricsSeverityForNode(symbolAuditManifest);
+
+ const isExternal = !dependencyManifest[fileName];
+
+ const nodeData = createNodeData({
+ id: symbolNodeId,
+ fileName,
+ symbolName: symbolManifest.id,
+ symbolType: symbolManifest.type,
+ isExternal,
+ metricsSeverity,
+ expandedLabel,
+ collapsedLabel,
+ });
+
+ nodeMap.set(symbolNodeId, { data: nodeData });
+
+ // Use the recursive function to process dependencies and dependents
+ traverseSymbolGraph(
+ symbolManifest,
+ symbolNodeId,
+ dependencyManifest,
+ auditManifest,
+ nodeMap,
+ edgeMap,
+ 0,
+ dependencyDepth,
+ dependentDepth,
+ visited,
+ );
+
+ const nodes = Array.from(nodeMap.values());
+ const edges = Array.from(edgeMap.values());
+
+ return [...nodes, ...edges];
+}
diff --git a/packages/app/src/helpers/cytoscape/elements/types.ts b/packages/app/src/helpers/cytoscape/elements/types.ts
new file mode 100644
index 00000000..a0383aa1
--- /dev/null
+++ b/packages/app/src/helpers/cytoscape/elements/types.ts
@@ -0,0 +1,45 @@
+import type {
+ metricCharacterCount,
+ metricCodeCharacterCount,
+ metricCodeLineCount,
+ metricCyclomaticComplexity,
+ metricDependencyCount,
+ metricDependentCount,
+ metricLinesCount,
+} from "@napi/shared";
+
+export interface NapiNodeData {
+ id: string;
+ position: { x: number; y: number };
+ metricsSeverity: {
+ [metricLinesCount]: number;
+ [metricCodeLineCount]: number;
+ [metricCodeCharacterCount]: number;
+ [metricCharacterCount]: number;
+ [metricDependencyCount]: number;
+ [metricDependentCount]: number;
+ [metricCyclomaticComplexity]: number;
+ };
+ expanded: {
+ label: string;
+ width: number;
+ height: number;
+ };
+ collapsed: {
+ label: string;
+ width: number;
+ height: number;
+ };
+}
+
+export interface FileNapiNodeData extends NapiNodeData {
+ fileName: string;
+ isExternal: boolean;
+}
+
+export interface SymbolNapiNodeData extends NapiNodeData {
+ fileName: string;
+ isExternal: boolean;
+ symbolName: string;
+ symbolType: string;
+}
diff --git a/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/index.ts b/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/index.ts
index a3bdf2e2..df09b119 100644
--- a/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/index.ts
+++ b/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/index.ts
@@ -1,50 +1,29 @@
import {
type AuditManifest,
classSymbolType,
+ delegateSymbolType,
type DependencyManifest,
enumSymbolType,
- type FileAuditManifest,
- type FileDependencyManifest,
functionSymbolType,
+ interfaceSymbolType,
type Metric,
- type metricCharacterCount,
- type metricCodeCharacterCount,
- type metricCodeLineCount,
- type metricCyclomaticComplexity,
- type metricDependencyCount,
- type metricDependentCount,
- type metricLinesCount,
+ recordSymbolType,
structSymbolType,
- type SymbolDependencyManifest,
- type SymbolType,
variableSymbolType,
} from "@napi/shared";
import type {
Collection,
Core,
+ EdgeSingular,
EventObjectNode,
NodeSingular,
- StylesheetJson,
} from "cytoscape";
import fcose from "cytoscape-fcose";
-import {
- edgeTypeDependency,
- edgeTypeDependent,
- type NapiEdgeData,
- type NapiNodeData,
-} from "./types.ts";
+import type { SymbolNapiNodeData } from "../elements/types.ts";
import cytoscape from "cytoscape";
-import {
- getCollapsedSymbolNodeLabel,
- getExpandedSymbolNodeLabel,
- getNodeWidthAndHeightFromLabel,
-} from "../label/index.ts";
-import {
- getMetricLevelColor,
- getMetricsSeverityForNode,
-} from "../metrics/index.ts";
import { mainLayout } from "../layout/index.ts";
-import { getCssValue } from "../../css/index.ts";
+import { getCytoscapeStylesheet } from "../styles/index.ts";
+import { computeNodeId, getSymbolElementsInFile } from "../elements/file.ts";
/**
* FileDependencyVisualizer creates an interactive graph of symbol dependencies within a file.
@@ -77,15 +56,14 @@ export class FileDependencyVisualizer {
private targetMetric: Metric | undefined;
/** Currently selected node in the graph */
private selectedNodeId: string | undefined;
- /** Currently highlighted node in the graph */
- private highlightedNodeId: string | undefined;
/** Callback functions triggered by graph interactions */
private externalCallbacks: {
onAfterNodeClick: () => void;
- onAfterNodeDblClick: (data: NapiNodeData) => void;
+ onAfterNodeDblClick: (filePath: string, symbolId: string) => void;
onAfterNodeRightClick: (data: {
position: { x: number; y: number };
- data: NapiNodeData;
+ filePath: string;
+ symbolId: string;
}) => void;
};
@@ -100,9 +78,10 @@ export class FileDependencyVisualizer {
onAfterNodeClick?: () => void;
onAfterNodeRightClick?: (data: {
position: { x: number; y: number };
- data: NapiNodeData;
+ filePath: string;
+ symbolId: string;
}) => void;
- onAfterNodeDblClick?: (data: NapiNodeData) => void;
+ onAfterNodeDblClick?: (filePath: string, symbolId: string) => void;
},
) {
this.fileId = fileId;
@@ -125,23 +104,36 @@ export class FileDependencyVisualizer {
onAfterNodeRightClick: mergedOptions.onAfterNodeRightClick,
};
+ this.theme = mergedOptions.theme;
+
cytoscape.use(fcose);
this.cy = cytoscape();
this.cy.mount(container);
- this.theme = mergedOptions.theme;
- const elements = this.getElementsFromManifestos(
- fileId,
- dependencyManifest,
- auditManifest,
- );
- this.cy.add(elements);
+ this.cy.batch(() => {
+ const elements = getSymbolElementsInFile(
+ fileId,
+ dependencyManifest,
+ auditManifest,
+ );
+ this.cy.add(elements);
- this.cy.style(this.getCyStyleSheet(this.theme));
+ const currentFileNode = this.cy.nodes().filter(
+ `node[fileName="${this.fileId}"]`,
+ );
+ currentFileNode.addClass("collapsed");
+ currentFileNode.addClass("symbol");
- this.layoutGraph(this.cy);
+ const stylesheet = getCytoscapeStylesheet(
+ this.targetMetric,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
- this.createEventListeners();
+ this.layoutGraph(this.cy);
+
+ this.createEventListeners();
+ });
}
/**
@@ -151,7 +143,11 @@ export class FileDependencyVisualizer {
*/
public updateTheme(theme: "light" | "dark") {
this.theme = theme;
- this.cy.style(this.getCyStyleSheet(this.theme));
+ const stylesheet = getCytoscapeStylesheet(
+ this.targetMetric,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
}
/**
@@ -159,17 +155,26 @@ export class FileDependencyVisualizer {
*
* @param nodeId - The ID of the node to highlight
*/
- public highlightNode(nodeId: string) {
- this.highlightedNodeId = nodeId;
- this.cy.style(this.getCyStyleSheet(this.theme));
+ public highlightNode(ref: { filePath: string; symbolId?: string }) {
+ if (ref.symbolId) {
+ const nodeId = computeNodeId(ref.filePath, ref.symbolId);
+
+ const highlightedNode = this.cy.nodes(`node[id="${nodeId}"]`);
+ const allElements = this.cy.elements();
+ const otherElements = allElements.difference(highlightedNode);
+
+ otherElements.removeClass("highlighted");
+ highlightedNode.addClass("highlighted");
+ }
}
/**
* Unhighlights all nodes in the graph
*/
public unhighlightNodes() {
- this.highlightedNodeId = undefined;
- this.cy.style(this.getCyStyleSheet(this.theme));
+ const allElements = this.cy.elements();
+
+ allElements.removeClass("highlighted");
}
/**
@@ -179,7 +184,60 @@ export class FileDependencyVisualizer {
*/
public setTargetMetric(metric: Metric | undefined) {
this.targetMetric = metric;
- this.cy.style(this.getCyStyleSheet(this.theme));
+
+ const stylesheet = getCytoscapeStylesheet(
+ this.targetMetric,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
+ }
+
+ public filterNodes(
+ showExternal: boolean,
+ showVariables: boolean,
+ showFunctions: boolean,
+ showClasses: boolean,
+ showStructs: boolean,
+ showEnums: boolean,
+ showInterfaces: boolean,
+ showRecords: boolean,
+ showDelegates: boolean,
+ ) {
+ const nodesToHide = this.cy.nodes().filter((node: NodeSingular) => {
+ const data = node.data() as SymbolNapiNodeData;
+ if (data.fileName === this.fileId) {
+ // never hide symbols from the current file
+ return false;
+ }
+
+ if (!showExternal && data.isExternal) {
+ return true;
+ }
+
+ const symbolTypeFilters = {
+ [variableSymbolType]: showVariables,
+ [functionSymbolType]: showFunctions,
+ [classSymbolType]: showClasses,
+ [structSymbolType]: showStructs,
+ [enumSymbolType]: showEnums,
+ [interfaceSymbolType]: showInterfaces,
+ [recordSymbolType]: showRecords,
+ [delegateSymbolType]: showDelegates,
+ };
+ for (const [symbolType, show] of Object.entries(symbolTypeFilters)) {
+ if (!show && data.symbolType === symbolType) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ const elementsToHide = nodesToHide.connectedEdges().union(nodesToHide);
+ const otherElements = this.cy.elements().difference(elementsToHide);
+
+ elementsToHide.addClass("hidden");
+ otherElements.removeClass("hidden");
}
/**
@@ -191,109 +249,106 @@ export class FileDependencyVisualizer {
private createEventListeners() {
this.cy.on("onetap", "node", (evt: EventObjectNode) => {
const nodeId = evt.target.id();
- const nodeData = evt.target.data() as NapiNodeData;
- const isCurrentFileNode = nodeData.customData.fileName === this.fileId;
-
- // If clicking on a node that's already selected, deselect it
+ const nodeData = evt.target.data() as SymbolNapiNodeData;
const isAlreadySelected = this.selectedNodeId === nodeId;
+ this.selectedNodeId = nodeId;
- if (isAlreadySelected) {
- // Deselect the node
- this.cy
- .elements()
- .removeClass([
- "background",
- "selected",
- "connected",
- "dependency",
- "dependent",
- "highlighted",
- ]);
- this.selectedNodeId = undefined;
- this.cy.style(this.getCyStyleSheet(this.theme));
- this.externalCallbacks.onAfterNodeClick();
+ const isCurrentFile = nodeData.fileName === this.fileId;
+ if (!isCurrentFile) {
+ // Only allow selection of nodes within the current file
return;
}
- this.selectedNodeId = nodeId;
- const selectedNode = evt.target;
-
- // For nodes in the current file, expand and show neighbors
- if (isCurrentFileNode) {
- const allElements = this.cy.elements();
- const connectedNodes = selectedNode
- .closedNeighborhood()
- .nodes()
- .difference(selectedNode);
-
- const dependentEdges = selectedNode
- .connectedEdges()
- .filter((edge) => edge.source().id() === this.selectedNodeId);
-
- const dependencyEdges = selectedNode
- .connectedEdges()
- .filter((edge) => edge.target().id() === this.selectedNodeId);
-
- const focusedElements = selectedNode.closedNeighborhood();
- const backgroundElements = allElements.difference(focusedElements);
-
- this.cy.batch(() => {
- // remove all, clean state
- allElements.removeClass([
- "background",
- "selected",
- "connected",
- "dependency",
- "dependent",
- "highlighted",
- ]);
-
- // add relevant classes
+ const allElements = this.cy.elements();
+
+ const selectedNode = this.cy.nodes(`node[id="${this.selectedNodeId}"]`);
+
+ const connectedNodes = selectedNode
+ .closedNeighborhood()
+ .nodes()
+ .difference(selectedNode);
+
+ const dependentEdges = selectedNode
+ .connectedEdges()
+ .filter(
+ (edge: EdgeSingular) => edge.source().id() === this.selectedNodeId,
+ );
+
+ const dependencyEdges = selectedNode
+ .connectedEdges()
+ .filter(
+ (edge: EdgeSingular) => edge.target().id() === this.selectedNodeId,
+ );
+
+ const focusedElements = selectedNode.closedNeighborhood();
+
+ const backgroundElements = allElements.difference(focusedElements);
+
+ this.cy.batch(() => {
+ // remove all, clean state
+ allElements.removeClass([
+ "symbol",
+ "collapsed",
+ "expanded",
+ "selected",
+ "background",
+ "dependency",
+ "dependent",
+ ]);
+
+ if (!isAlreadySelected) {
backgroundElements.addClass("background");
- connectedNodes.addClass("connected");
+
+ connectedNodes.addClass("collapsed");
+ connectedNodes.addClass("symbol");
+
+ selectedNode.addClass("expanded");
+ selectedNode.addClass("selected");
+ selectedNode.addClass("symbol");
+
dependencyEdges.addClass("dependency");
dependentEdges.addClass("dependent");
- selectedNode.addClass("selected");
- // layout the closed neighborhood
focusedElements.layout(this.layout).run();
- });
+ } else {
+ this.selectedNodeId = undefined;
- this.cy.style(this.getCyStyleSheet(this.theme));
- this.externalCallbacks.onAfterNodeClick();
- return;
- }
-
- // For nodes not in current file, just expand the node without affecting other nodes
- this.cy.batch(() => {
- // Just mark the clicked node as selected
- selectedNode.addClass("selected");
+ const fileNodes = this.cy.nodes().filter(
+ `node[fileName="${this.fileId}"]`,
+ );
+ fileNodes.addClass("collapsed");
+ fileNodes.addClass("symbol");
+ }
});
- this.cy.style(this.getCyStyleSheet(this.theme));
+
this.externalCallbacks.onAfterNodeClick();
});
this.cy.on("dbltap", "node", (evt: EventObjectNode) => {
const node = evt.target;
- const data = node.data() as NapiNodeData;
+ const data = node.data() as SymbolNapiNodeData;
// If the node is external, ignore it
- if (data.customData.isExternal) return;
+ if (data.isExternal) return;
- this.externalCallbacks.onAfterNodeDblClick(data);
+ this.externalCallbacks.onAfterNodeDblClick(
+ data.fileName,
+ data.symbolName,
+ );
});
this.cy.on("cxttap", "node", (evt: EventObjectNode) => {
const node = evt.target;
- const data = node.data() as NapiNodeData;
+ const data = node.data() as SymbolNapiNodeData;
// If the node is external, ignore it
- if (data.customData.isExternal) return;
+ if (data.isExternal) return;
const { x, y } = node.renderedPosition();
this.externalCallbacks.onAfterNodeRightClick({
position: { x, y },
- data,
+ filePath: data.fileName,
+ symbolId: data.symbolName,
});
});
}
@@ -307,444 +362,4 @@ export class FileDependencyVisualizer {
const collectionToLayout = collection || this.cy.nodes();
collectionToLayout.layout(this.layout).run();
}
-
- private computeNodeId(fileId: string, symbolId: string) {
- return `${fileId}:${symbolId}`;
- }
-
- /**
- * Creates node data with all required properties
- */
- private createNodeData(params: {
- id: string;
- fileName: string;
- symbolName: string;
- symbolType: string;
- isExternal: boolean;
- metricsSeverity: {
- [metricLinesCount]: number;
- [metricCodeLineCount]: number;
- [metricCodeCharacterCount]: number;
- [metricCharacterCount]: number;
- [metricDependencyCount]: number;
- [metricDependentCount]: number;
- [metricCyclomaticComplexity]: number;
- };
- expandedLabel: string;
- collapsedLabel: string;
- }): NapiNodeData {
- // Calculate dimensions for expanded and collapsed views
- const { width: expandedWidth, height: expandedHeight } =
- getNodeWidthAndHeightFromLabel(
- params.expandedLabel,
- );
-
- const { width: collapsedWidth, height: collapsedHeight } =
- getNodeWidthAndHeightFromLabel(
- params.collapsedLabel,
- );
-
- // Create the node data structure
- return {
- id: params.id,
- position: { x: 0, y: 0 },
- customData: {
- fileName: params.fileName,
- symbolName: params.symbolName,
- symbolType: params.symbolType,
- isExternal: params.isExternal,
- metricsSeverity: params.metricsSeverity,
- expanded: {
- label: params.expandedLabel,
- width: expandedWidth,
- height: expandedHeight,
- },
- collapsed: {
- label: params.collapsedLabel,
- width: collapsedWidth,
- height: collapsedHeight,
- },
- },
- };
- }
-
- /**
- * Processes dependency and audit manifests to create graph elements (nodes and edges)
- * for a specific file, showing all symbols and their dependencies
- *
- * @param fileId - The ID of the file to visualize
- * @param dependencyManifest - Object containing dependency information
- * @param auditManifest - Object containing audit information
- * @returns Array of element definitions for Cytoscape
- */
- private getElementsFromManifestos(
- fileId: string,
- dependencyManifest: DependencyManifest,
- auditManifest: AuditManifest,
- ) {
- // Get the file manifest for the current file
- const fileManifest = dependencyManifest[fileId];
- if (!fileManifest) {
- console.error(`File manifest not found for ${fileId}`);
- return [];
- }
-
- const fileAuditManifest = auditManifest[fileId];
- const nodes: { data: NapiNodeData }[] = [];
- const edges: { data: NapiEdgeData }[] = [];
-
- // First pass: Create nodes for each symbol in the file
- Object.values(fileManifest.symbols).forEach((symbol) => {
- const symbolAuditManifest = fileAuditManifest.symbols[symbol.id];
- const symbolNodeId = this.computeNodeId(fileId, symbol.id);
-
- // Create labels for the symbol
- const expandedLabel = getExpandedSymbolNodeLabel({
- currentFileId: fileId,
- fileName: fileId,
- symbolName: symbol.id,
- symbolType: symbol.type,
- symbolAuditManifest,
- });
- const collapsedLabel = getCollapsedSymbolNodeLabel({
- symbolName: symbol.id,
- });
-
- const metricsSeverity = getMetricsSeverityForNode(symbolAuditManifest);
-
- const nodeData = this.createNodeData({
- id: symbolNodeId,
- fileName: fileId,
- symbolName: symbol.id,
- symbolType: symbol.type,
- isExternal: false,
- metricsSeverity,
- expandedLabel,
- collapsedLabel,
- });
-
- nodes.push({ data: nodeData });
- });
-
- // Second pass: Create nodes and edges for dependencies and dependents
- Object.values(fileManifest.symbols).forEach((symbol) => {
- const symbolNodeId = this.computeNodeId(fileId, symbol.id);
-
- // Process dependencies
- this.processDependencies(
- symbol,
- symbolNodeId,
- dependencyManifest,
- auditManifest,
- nodes,
- edges,
- );
-
- // Process dependents
- this.processDependents(
- symbol,
- symbolNodeId,
- dependencyManifest,
- auditManifest,
- nodes,
- edges,
- );
- });
-
- return [...nodes, ...edges];
- }
-
- /**
- * Process dependencies of a symbol and create corresponding nodes and edges
- */
- private processDependencies(
- symbol: SymbolDependencyManifest,
- symbolNodeId: string,
- dependencyManifest: DependencyManifest,
- auditManifest: AuditManifest,
- nodes: { data: NapiNodeData }[],
- edges: { data: NapiEdgeData }[],
- ) {
- Object.values(symbol.dependencies).forEach((dep) => {
- let depDependencyManifest: FileDependencyManifest | undefined;
- let depAuditManifest: FileAuditManifest | undefined;
-
- if (!dep.isExternal) {
- depDependencyManifest = dependencyManifest[dep.id];
- depAuditManifest = auditManifest[dep.id];
- }
-
- // For each symbol this depends on, create an edge
- Object.keys(dep.symbols).forEach((depSymbolName) => {
- const depSymbolNodeId = this.computeNodeId(dep.id, depSymbolName);
-
- // Check if node already exists
- const existingNode = nodes.find(
- (node) => node.data.id === depSymbolNodeId,
- );
-
- if (!existingNode) {
- let depSymbolType: SymbolType | "unknown" = "unknown";
- if (depDependencyManifest) {
- depSymbolType = depDependencyManifest.symbols[depSymbolName].type;
- }
-
- // Create label for dependency nodes
- const expandedLabel = getExpandedSymbolNodeLabel({
- currentFileId: dep.id,
- fileName: dep.id,
- symbolName: depSymbolName,
- symbolType: depSymbolType,
- symbolAuditManifest: depAuditManifest?.symbols[depSymbolName],
- });
-
- const metricsSeverity = getMetricsSeverityForNode(
- depAuditManifest?.symbols[depSymbolName],
- );
-
- const nodeData = this.createNodeData({
- id: depSymbolNodeId,
- fileName: dep.id,
- symbolName: depSymbolName,
- symbolType: depSymbolType,
- isExternal: dep.isExternal,
- metricsSeverity,
- expandedLabel,
- collapsedLabel: depSymbolName,
- });
-
- nodes.push({ data: nodeData });
- }
-
- // Add the edge
- const edgeId = `${depSymbolNodeId}->${symbolNodeId}`;
- edges.push({
- data: {
- id: edgeId,
- source: depSymbolNodeId,
- target: symbolNodeId,
- customData: {
- type: edgeTypeDependency,
- },
- },
- });
- });
- });
- }
-
- /**
- * Process dependents of a symbol and create corresponding nodes and edges
- */
- private processDependents(
- symbol: SymbolDependencyManifest,
- symbolNodeId: string,
- dependencyManifest: DependencyManifest,
- auditManifest: AuditManifest,
- nodes: { data: NapiNodeData }[],
- edges: { data: NapiEdgeData }[],
- ) {
- Object.values(symbol.dependents).forEach((dep) => {
- const depDependencyManifest = dependencyManifest[dep.id];
- const depAuditManifest = auditManifest[dep.id];
-
- Object.keys(dep.symbols).forEach((depSymbolName) => {
- const depSymbolNodeId = this.computeNodeId(dep.id, depSymbolName);
-
- // Check if node already exists
- const existingNode = nodes.find(
- (node) => node.data.id === depSymbolNodeId,
- );
-
- if (!existingNode) {
- const depSymbolType =
- depDependencyManifest.symbols[depSymbolName].type;
-
- // Create label for dependent nodes
- const expandedLabel = getExpandedSymbolNodeLabel({
- currentFileId: dep.id,
- fileName: dep.id,
- symbolName: depSymbolName,
- symbolType: depSymbolType,
- symbolAuditManifest: depAuditManifest?.symbols[depSymbolName],
- });
-
- const metricsSeverity = getMetricsSeverityForNode(
- depAuditManifest?.symbols[depSymbolName],
- );
-
- const nodeData = this.createNodeData({
- id: depSymbolNodeId,
- fileName: dep.id,
- symbolName: depSymbolName,
- symbolType: depSymbolType,
- isExternal: false,
- metricsSeverity,
- expandedLabel,
- collapsedLabel: depSymbolName,
- });
-
- nodes.push({ data: nodeData });
- }
-
- // Add the edge
- const edgeId = `${symbolNodeId}->${depSymbolNodeId}`;
- edges.push({
- data: {
- id: edgeId,
- source: symbolNodeId,
- target: depSymbolNodeId,
- customData: {
- type: edgeTypeDependent,
- },
- },
- });
- });
- });
- }
-
- /**
- * Generates the stylesheet for Cytoscape graph visualization based on theme
- *
- * @param theme - The current theme (light or dark)
- * @returns StylesheetJson for Cytoscape
- */
- private getCyStyleSheet(theme: "light" | "dark"): StylesheetJson {
- return [
- {
- selector: "node",
- style: {
- label: "data(customData.collapsed.label)",
- "text-wrap": "wrap",
- color: getCssValue(`--color-text-${theme}`),
- "border-width": 4,
- "border-color": (node: NodeSingular) => {
- const data = node.data() as NapiNodeData;
- if (data.customData.isExternal) {
- return getCssValue(`--color-border-${theme}`);
- } else if (data.customData.fileName !== this.fileId) {
- return getCssValue(`--color-secondary-${theme}`);
- }
-
- if (this.targetMetric) {
- return getMetricLevelColor(
- this.theme,
- data.customData.metricsSeverity[this.targetMetric],
- );
- }
-
- return getCssValue(`--color-primary-${theme}`);
- },
- "background-color": (node: NodeSingular) => {
- const data = node.data() as NapiNodeData;
-
- if (data.customData.isExternal) {
- return getCssValue(`--color-border-${theme}`);
- }
-
- if (data.customData.fileName !== this.fileId) {
- return getCssValue(`--color-secondary-${theme}`);
- }
-
- if (this.targetMetric) {
- return getMetricLevelColor(
- this.theme,
- data.customData.metricsSeverity[this.targetMetric],
- );
- }
-
- return getCssValue(`--color-primary-${theme}`);
- },
-
- shape: (node: NodeSingular) => {
- const symbolTypeToShape = {
- [classSymbolType]: "hexagon",
- [functionSymbolType]: "ellipse",
- [variableSymbolType]: "diamond",
- [structSymbolType]: "hexagon",
- [enumSymbolType]: "triangle",
- };
-
- const fallbackShape = "octagon";
-
- const data = node.data() as NapiNodeData;
-
- return (
- symbolTypeToShape[
- data.customData.symbolType as keyof typeof symbolTypeToShape
- ] ||
- fallbackShape
- );
- },
- "background-opacity": 0.2,
- width: "data(customData.collapsed.width)",
- height: "data(customData.collapsed.height)",
- "text-valign": "center",
- "text-halign": "center",
- },
- },
- {
- selector: "node.background",
- style: {
- opacity: 0.2,
- },
- },
- {
- selector: "node.selected",
- style: {
- label: "data(customData.expanded.label)",
- "border-width": 5,
- "z-index": 2000,
- width: "data(customData.expanded.width)",
- height: "data(customData.expanded.height)",
- },
- },
- {
- selector: "node.connected",
- style: {
- "border-width": 5,
- "z-index": 1000,
- width: "data(customData.collapsed.width)",
- height: "data(customData.collapsed.height)",
- },
- },
- {
- selector: "edge",
- style: {
- width: 2,
- "line-color": getCssValue(`--color-text-${theme}`),
- "line-opacity": 1,
- "target-arrow-color": getCssValue(`--color-text-${theme}`),
- "target-arrow-shape": "triangle",
- "curve-style": "straight",
- "arrow-scale": 1,
- },
- },
- {
- selector: `edge[customData.type = '${edgeTypeDependency}']`,
- style: {
- "line-color": getCssValue(`--color-primary-${theme}`),
- "target-arrow-color": getCssValue(`--color-primary-${theme}`),
- },
- },
- {
- selector: `edge[customData.type = '${edgeTypeDependent}']`,
- style: {
- "line-color": getCssValue(`--color-secondary-${theme}`),
- "target-arrow-color": getCssValue(`--color-secondary-${theme}`),
- },
- },
- {
- selector: "edge.background",
- style: {
- "line-opacity": 0.1,
- },
- },
- {
- selector: ".hidden",
- style: {
- display: "none",
- },
- },
- ] as StylesheetJson;
- }
}
diff --git a/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/types.ts b/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/types.ts
deleted file mode 100644
index 6fdddd46..00000000
--- a/packages/app/src/helpers/cytoscape/fileDependencyVisualizer/types.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type {
- metricCharacterCount,
- metricCodeCharacterCount,
- metricCodeLineCount,
- metricCyclomaticComplexity,
- metricDependencyCount,
- metricDependentCount,
- metricLinesCount,
-} from "@napi/shared";
-
-export interface NapiNodeData {
- id: string;
- position: { x: number; y: number };
- customData: {
- fileName: string;
- symbolName: string;
- symbolType: string;
- isExternal: boolean;
- metricsSeverity: {
- [metricLinesCount]: number;
- [metricCodeLineCount]: number;
- [metricCodeCharacterCount]: number;
- [metricCharacterCount]: number;
- [metricDependencyCount]: number;
- [metricDependentCount]: number;
- [metricCyclomaticComplexity]: number;
- };
- expanded: {
- label: string;
- width: number;
- height: number;
- };
- collapsed: {
- label: string;
- width: number;
- height: number;
- };
- };
-}
-
-export const edgeTypeDependency = "dependency";
-export const edgeTypeDependent = "dependent";
-
-export interface NapiEdgeData {
- id: string;
- source: string;
- target: string;
- customData: {
- type: typeof edgeTypeDependency | typeof edgeTypeDependent;
- };
-}
diff --git a/packages/app/src/helpers/cytoscape/label/index.ts b/packages/app/src/helpers/cytoscape/label/index.ts
index b80fde75..516e13d7 100644
--- a/packages/app/src/helpers/cytoscape/label/index.ts
+++ b/packages/app/src/helpers/cytoscape/label/index.ts
@@ -113,8 +113,11 @@ export function getExpandedFileNodeLabel(data: {
* @param data - Object containing symbol name
* @returns Symbol name as the collapsed label
*/
-export function getCollapsedSymbolNodeLabel(data: { symbolName: string }) {
- return data.symbolName;
+export function getCollapsedSymbolNodeLabel(data: {
+ symbolName: string;
+ symbolType: string;
+}) {
+ return `${data.symbolName} (${data.symbolType})`;
}
/**
diff --git a/packages/app/src/helpers/cytoscape/metrics/index.ts b/packages/app/src/helpers/cytoscape/metrics/index.ts
index d515415c..ab4efe7e 100644
--- a/packages/app/src/helpers/cytoscape/metrics/index.ts
+++ b/packages/app/src/helpers/cytoscape/metrics/index.ts
@@ -12,42 +12,6 @@ import {
import type { Metric } from "@napi/shared";
-/**
- * Returns a color for a specific metric severity level based on theme.
- *
- * Maps severity levels (0-5) to appropriate colors in either light or dark theme.
- * Level 0 represents no issues (green), while level 5 represents critical issues (red).
- * Colors are optimized for visibility in each theme mode.
- *
- * @param theme - The current UI theme ("light" or "dark")
- * @param level - The severity level of the metric (0-5)
- * @returns The hex color code for the specified severity level
- */
-export function getMetricLevelColor(
- theme: "light" | "dark",
- level: number,
-): string {
- const levelToColor = theme === "light"
- ? {
- 0: "#22c55e", // green - no issues
- 1: "#eab308", // yellow - minor issues
- 2: "#f97316", // orange - moderate issues
- 3: "#d97706", // amber - significant issues
- 4: "#991b1b", // dark red - severe issues
- 5: "#ef4444", // red - critical issues
- }
- : {
- 0: "#4ade80", // lighter green for dark theme - no issues
- 1: "#facc15", // brighter yellow for dark theme - minor issues
- 2: "#fb923c", // lighter orange for dark theme - moderate issues
- 3: "#fbbf24", // brighter amber for dark theme - significant issues
- 4: "#b91c1c", // slightly brighter dark red for dark theme - severe issues
- 5: "#f87171", // lighter red for dark theme - critical issues
- };
-
- return levelToColor[level as keyof typeof levelToColor] || levelToColor[5];
-}
-
/**
* Extracts metric severity levels from an audit manifest for visualization.
*
diff --git a/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/index.ts b/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/index.ts
index 6231336c..c7144374 100644
--- a/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/index.ts
+++ b/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/index.ts
@@ -1,28 +1,16 @@
import cytoscape, {
type Collection,
- type EdgeDefinition,
type EdgeSingular,
- type ElementDefinition,
type EventObjectNode,
- type NodeDefinition,
- type NodeSingular,
- type StylesheetJson,
} from "cytoscape";
import type { Core } from "cytoscape";
import fcose from "cytoscape-fcose";
import type { AuditManifest, DependencyManifest, Metric } from "@napi/shared";
-import {
- getCollapsedFileNodeLabel,
- getExpandedFileNodeLabel,
- getNodeWidthAndHeightFromLabel,
-} from "../label/index.ts";
-import type { NapiNodeData } from "./types.ts";
-import {
- getMetricLevelColor,
- getMetricsSeverityForNode,
-} from "../metrics/index.ts";
+import type { NapiNodeData } from "../elements/types.ts";
import { mainLayout } from "../layout/index.ts";
-import { getCssValue } from "../../css/index.ts";
+import { getCytoscapeStylesheet } from "../styles/index.ts";
+import { getFileElementsInProject } from "../elements/project.ts";
+
/**
* ProjectDependencyVisualizer creates an interactive graph of project-level dependencies.
*
@@ -52,15 +40,13 @@ export class ProjectDependencyVisualizer {
private targetMetric: Metric | undefined;
/** Currently selected node in the graph */
private selectedNodeId: string | undefined;
- /** Currently highlighted node in the graph */
- private highlightedNodeId: string | undefined;
/** Callback functions triggered by graph interactions */
private externalCallbacks: {
onAfterNodeClick: () => void;
- onAfterNodeDblClick: (data: NapiNodeData) => void;
+ onAfterNodeDblClick: (filePath: string) => void;
onAfterNodeRightClick: (data: {
position: { x: number; y: number };
- id: string;
+ filePath: string;
}) => void;
};
@@ -82,9 +68,9 @@ export class ProjectDependencyVisualizer {
onAfterNodeClick?: () => void;
onAfterNodeRightClick?: (data: {
position: { x: number; y: number };
- id: string;
+ filePath: string;
}) => void;
- onAfterNodeDblClick?: (data: NapiNodeData) => void;
+ onAfterNodeDblClick?: (filePath: string) => void;
},
) {
const defaultOptions = {
@@ -110,13 +96,17 @@ export class ProjectDependencyVisualizer {
this.cy.mount(container);
this.theme = mergedOptions.theme;
- const elements = this.getElementsFromManifestos(
+ const elements = getFileElementsInProject(
dependencyManifest,
auditManifest,
);
this.cy.add(elements);
- this.cy.style(this.getCyStyleSheet(this.theme));
+ const stylesheet = getCytoscapeStylesheet(
+ this.targetMetric,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
this.layoutGraph(this.cy);
@@ -130,7 +120,11 @@ export class ProjectDependencyVisualizer {
*/
public updateTheme(theme: "light" | "dark") {
this.theme = theme;
- this.cy.style(this.getCyStyleSheet(this.theme));
+ const stylesheet = getCytoscapeStylesheet(
+ this.targetMetric,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
}
/**
@@ -138,17 +132,24 @@ export class ProjectDependencyVisualizer {
*
* @param nodeId - The ID of the node to highlight
*/
- public highlightNode(nodeId: string) {
- this.highlightedNodeId = nodeId;
- this.cy.style(this.getCyStyleSheet(this.theme));
+ public highlightNode(ref: { filePath: string; symbolId?: string }) {
+ const nodeId = ref.filePath;
+
+ const highlightedNode = this.cy.nodes(`node[id="${nodeId}"]`);
+ const allElements = this.cy.elements();
+ const otherElements = allElements.difference(highlightedNode);
+
+ otherElements.removeClass("highlighted");
+ highlightedNode.addClass("highlighted");
}
/**
* Unhighlights all nodes in the graph
*/
public unhighlightNodes() {
- this.highlightedNodeId = undefined;
- this.cy.style(this.getCyStyleSheet(this.theme));
+ const allElements = this.cy.elements();
+
+ allElements.removeClass("highlighted");
}
/**
@@ -159,9 +160,9 @@ export class ProjectDependencyVisualizer {
*/
private createEventListeners() {
this.cy.on("onetap", "node", (evt: EventObjectNode) => {
- const isAlreadySelected = this.selectedNodeId === evt.target.id();
-
- this.selectedNodeId = evt.target.id();
+ const nodeId = evt.target.id();
+ const isAlreadySelected = this.selectedNodeId === nodeId;
+ this.selectedNodeId = nodeId;
const allElements = this.cy.elements();
@@ -191,29 +192,33 @@ export class ProjectDependencyVisualizer {
this.cy.batch(() => {
// remove all, clean state
allElements.removeClass([
- "background",
+ "file",
+ "collapsed",
+ "expanded",
"selected",
- "connected",
"dependency",
"dependent",
- "highlighted",
+ "background",
]);
if (!isAlreadySelected) {
- // add relevant classes
backgroundElements.addClass("background");
- connectedNodes.addClass("connected");
+
+ connectedNodes.addClass("collapsed");
+ connectedNodes.addClass("file");
+
+ selectedNode.addClass("expanded");
+ selectedNode.addClass("selected");
+ selectedNode.addClass("file");
+
dependencyEdges.addClass("dependency");
dependentEdges.addClass("dependent");
- selectedNode.addClass("selected");
// layout the closed neighborhood
focusedElements.layout(this.layout).run();
} else {
this.selectedNodeId = undefined;
}
-
- this.cy.style(this.getCyStyleSheet(this.theme));
});
this.externalCallbacks.onAfterNodeClick();
@@ -222,7 +227,7 @@ export class ProjectDependencyVisualizer {
this.cy.on("dbltap", "node", (evt: EventObjectNode) => {
const node = evt.target;
const data = node.data() as NapiNodeData;
- this.externalCallbacks.onAfterNodeDblClick(data);
+ this.externalCallbacks.onAfterNodeDblClick(data.id);
});
this.cy.on("cxttap", "node", (evt: EventObjectNode) => {
@@ -231,7 +236,7 @@ export class ProjectDependencyVisualizer {
this.externalCallbacks.onAfterNodeRightClick({
position: { x, y },
- id: node.id(),
+ filePath: node.id(),
});
});
}
@@ -254,252 +259,10 @@ export class ProjectDependencyVisualizer {
public setTargetMetric(metric: Metric | undefined) {
this.targetMetric = metric;
- this.cy.style(this.getCyStyleSheet(this.theme));
- }
-
- /**
- * Processes dependency and audit manifests to create graph elements (nodes and edges)
- *
- * @param dependencyManifest - Object containing dependency information for project files
- * @param auditManifest - Object containing audit information for project files
- * @returns Array of element definitions for Cytoscape
- */
- private getElementsFromManifestos(
- dependencyManifest: DependencyManifest,
- auditManifest: AuditManifest,
- ): ElementDefinition[] {
- const nodes = this.createNodes(dependencyManifest, auditManifest);
- const edges = this.createEdges(dependencyManifest);
-
- return [...nodes, ...edges];
- }
-
- /**
- * Generates the stylesheet for Cytoscape graph visualization based on theme
- *
- * Includes styles for:
- * - Base node and edge appearance
- * - Selected nodes and their connections
- * - Background elements
- * - Dependency/dependent relationship highlighting
- *
- * @param theme - The current theme (light or dark)
- * @returns StylesheetJson for Cytoscape
- */
- private getCyStyleSheet(theme: "light" | "dark") {
- return [
- {
- selector: "node",
- style: {
- "text-wrap": "wrap",
- color: getCssValue(`--color-text-${theme}`),
- "border-width": (node: NodeSingular) => {
- return this.highlightedNodeId === node.id() ? 10 : 6;
- },
- "border-color": (node: NodeSingular) => {
- if (this.highlightedNodeId === node.id()) {
- return "yellow";
- }
-
- if (this.targetMetric) {
- return getMetricLevelColor(
- this.theme,
- node.data().customData.metricsSeverity[this.targetMetric],
- );
- }
- return getCssValue(`--color-primary-${theme}`);
- },
- "background-color": (node: NodeSingular) => {
- const data = node.data() as NapiNodeData;
- if (this.targetMetric) {
- return getMetricLevelColor(
- this.theme,
- data.customData.metricsSeverity[this.targetMetric],
- );
- }
- return getCssValue(`--color-primary-${theme}`);
- },
- "background-opacity": 0.4,
- shape: "circle",
- "text-valign": "center",
- "text-halign": "center",
- },
- },
- {
- selector: "node.background",
- style: {
- opacity: 0.2,
- },
- },
- {
- selector: "node.selected",
- style: {
- label: "data(customData.expanded.label)",
- "background-color": getCssValue(`--color-background-${theme}`),
- "border-color": getCssValue(`--color-primary-${theme}`),
- "z-index": 2000,
- shape: "roundrectangle",
- width: "data(customData.expanded.width)",
- height: "data(customData.expanded.height)",
- },
- },
- {
- selector: "node.connected",
- style: {
- label: "data(customData.collapsed.label)",
- "background-color": getCssValue(`--color-background-${theme}`),
- "z-index": 1000,
- shape: "roundrectangle",
- width: "data(customData.collapsed.width)",
- height: "data(customData.collapsed.height)",
- },
- },
- {
- selector: "edge",
- style: {
- width: 1,
- "line-color": getCssValue(`--color-primary-${theme}`),
- "target-arrow-color": getCssValue(`--color-primary-${theme}`),
- "target-arrow-shape": "triangle",
- "curve-style": "straight",
- },
- },
- {
- selector: "edge.dependency",
- style: {
- width: 2,
- "line-color": getCssValue(`--color-primary-${theme}`),
- "target-arrow-color": getCssValue(`--color-primary-${theme}`),
- },
- },
- {
- selector: "edge.dependent",
- style: {
- width: 2,
- "line-color": getCssValue(`--color-secondary-${theme}`),
- "target-arrow-color": getCssValue(`--color-secondary-${theme}`),
- },
- },
- {
- selector: "edge.background",
- style: {
- "line-opacity": 0.1,
- },
- },
- {
- selector: ".hidden",
- style: {
- display: "none",
- },
- },
- ] as StylesheetJson;
- }
-
- /**
- * Creates node elements for the Cytoscape graph with proper styling and metrics
- *
- * Each node contains:
- * - File identification
- * - Metric data (LOC, character count, etc.)
- * - Audit information (errors/warnings)
- * - Visual properties for expanded/collapsed states
- *
- * @param dependencyManifest - Object containing dependency information for project files
- * @param auditManifest - Object containing audit information for project files
- * @returns Array of node definitions for Cytoscape
- */
- private createNodes(
- dependencyManifest: DependencyManifest,
- auditManifest: AuditManifest,
- ) {
- interface CustomNodeDefinition extends NodeDefinition {
- data: NapiNodeData & object;
- }
-
- const nodes: CustomNodeDefinition[] = [];
-
- Object.values(dependencyManifest).forEach((fileDependencyManifest) => {
- const fileAuditManifest = auditManifest[fileDependencyManifest.id];
-
- const expandedLabel = getExpandedFileNodeLabel({
- fileName: fileDependencyManifest.id,
- fileAuditManifest,
- });
- const { width: expandedWitdh, height: expandedHeight } =
- getNodeWidthAndHeightFromLabel(
- expandedLabel,
- );
-
- const collapsedLabel = getCollapsedFileNodeLabel({
- fileName: fileDependencyManifest.id,
- fileAuditManifest,
- });
- const { width: collapsedWidth, height: collapsedHeight } =
- getNodeWidthAndHeightFromLabel(
- collapsedLabel,
- );
-
- const metricsColors = getMetricsSeverityForNode(fileAuditManifest);
-
- const nodeElement: CustomNodeDefinition = {
- data: {
- id: fileDependencyManifest.id,
- // initial node position - will be updated by layout
- position: { x: 0, y: 0 },
- customData: {
- fileName: fileDependencyManifest.id,
- metricsSeverity: metricsColors,
- expanded: {
- label: expandedLabel,
- width: expandedWitdh,
- height: expandedHeight,
- },
- collapsed: {
- label: collapsedLabel,
- width: collapsedWidth,
- height: collapsedHeight,
- },
- },
- },
- };
-
- nodes.push(nodeElement);
- });
-
- return nodes;
- }
-
- /**
- * Creates edge elements representing dependencies between files
- *
- * Filters out self-references and external dependencies to focus on
- * internal project structure.
- *
- * @param dependencyManifest - Object containing dependency information for project files
- * @returns Array of edge definitions for Cytoscape
- */
- private createEdges(dependencyManifest: DependencyManifest) {
- const edges: EdgeDefinition[] = [];
-
- Object.values(dependencyManifest).forEach((fileManifest) => {
- for (const dependency of Object.values(fileManifest.dependencies)) {
- if (dependency.isExternal) {
- continue;
- }
-
- if (dependency.id === fileManifest.id) {
- continue;
- }
-
- edges.push({
- data: {
- source: dependency.id,
- target: fileManifest.id,
- },
- });
- }
- });
-
- return edges;
+ const stylesheet = getCytoscapeStylesheet(
+ this.targetMetric,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
}
}
diff --git a/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/types.ts b/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/types.ts
deleted file mode 100644
index b17d1e38..00000000
--- a/packages/app/src/helpers/cytoscape/projectDependencyVisualizer/types.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type {
- metricCharacterCount,
- metricCodeCharacterCount,
- metricCodeLineCount,
- metricCyclomaticComplexity,
- metricDependencyCount,
- metricDependentCount,
- metricLinesCount,
-} from "@napi/shared";
-
-export interface NapiNodeData {
- id: string;
- position: { x: number; y: number };
- customData: {
- fileName: string;
- metricsSeverity: {
- [metricLinesCount]: number;
- [metricCodeLineCount]: number;
- [metricCodeCharacterCount]: number;
- [metricCharacterCount]: number;
- [metricDependencyCount]: number;
- [metricDependentCount]: number;
- [metricCyclomaticComplexity]: number;
- };
- expanded: {
- label: string;
- width: number;
- height: number;
- };
- collapsed: {
- label: string;
- width: number;
- height: number;
- };
- };
-}
diff --git a/packages/app/src/helpers/cytoscape/styles/index.ts b/packages/app/src/helpers/cytoscape/styles/index.ts
new file mode 100644
index 00000000..aacfbf2a
--- /dev/null
+++ b/packages/app/src/helpers/cytoscape/styles/index.ts
@@ -0,0 +1,261 @@
+import type { NodeSingular, StylesheetJson } from "cytoscape";
+import {
+ classSymbolType,
+ delegateSymbolType,
+ enumSymbolType,
+ functionSymbolType,
+ interfaceSymbolType,
+ type Metric,
+ recordSymbolType,
+ structSymbolType,
+ variableSymbolType,
+} from "@napi/shared";
+import type { NapiNodeData, SymbolNapiNodeData } from "../elements/types.ts";
+
+interface CytoscapeStyles {
+ node: {
+ colors: {
+ text: {
+ default: string;
+ };
+ background: {
+ default: string;
+ severity: {
+ 0: string;
+ 1: string;
+ 2: string;
+ 3: string;
+ 4: string;
+ 5: string;
+ };
+ };
+ border: {
+ default: string;
+ highlighted: string;
+ selected: string;
+ };
+ };
+ width: {
+ default: number;
+ highlighted: number;
+ };
+ };
+ edge: {
+ colors: {
+ default: string;
+ dependency: string;
+ dependent: string;
+ };
+ width: {
+ default: number;
+ highlighted: number;
+ };
+ };
+}
+
+function getSeverityColor(styles: CytoscapeStyles, level: number) {
+ const severityLevels = styles.node.colors.background.severity;
+ const targetColor = level in severityLevels
+ ? severityLevels[level as keyof typeof severityLevels]
+ : undefined;
+
+ return targetColor || styles.node.colors.background.default;
+}
+
+function getCytoscapeStyles(theme: "light" | "dark" = "light") {
+ return {
+ node: {
+ colors: {
+ text: {
+ default: theme === "light" ? "#1a1a1a" : "#ffffff",
+ },
+ background: {
+ default: theme === "light" ? "#ffffff" : "#1a1a1a",
+ severity: {
+ 0: theme === "light" ? "#16a34a" : "#4ade80",
+ 1: theme === "light" ? "#65a30d" : "#a3e635",
+ 2: theme === "light" ? "#ca8a04" : "#facc15",
+ 3: theme === "light" ? "#d97706" : "#fbbf24",
+ 4: theme === "light" ? "#ea580c" : "#fb923c",
+ 5: theme === "light" ? "#dc2626" : "#f87171",
+ },
+ },
+ border: {
+ default: theme === "light" ? "#1a1a1a" : "#ffffff",
+ highlighted: theme === "light" ? "#6366f1" : "#818cf8",
+ selected: theme === "light" ? "#059669" : "#10b981",
+ },
+ },
+ width: {
+ default: 5,
+ highlighted: 10,
+ },
+ },
+ edge: {
+ colors: {
+ default: theme === "light" ? "#1a1a1a" : "#ffffff",
+ dependency: theme === "light" ? "#0284c7" : "#38bdf8",
+ dependent: theme === "light" ? "#9333ea" : "#a78bfa",
+ },
+ width: {
+ default: 1,
+ highlighted: 3,
+ },
+ },
+ } as CytoscapeStyles;
+}
+
+export function getCytoscapeStylesheet(
+ targetMetric: Metric | undefined,
+ theme: "light" | "dark" = "light",
+) {
+ const styles = getCytoscapeStyles(theme);
+
+ const stylesheet = [
+ // Node specific styles
+ {
+ selector: "node",
+ style: {
+ "text-wrap": "wrap",
+ color: styles.node.colors.text.default,
+ "border-width": styles.node.width.default,
+ "border-color": styles.node.colors.border.default,
+ "background-color": (node: NodeSingular) => {
+ const data = node.data() as NapiNodeData;
+ if (targetMetric) {
+ return getSeverityColor(styles, data.metricsSeverity[targetMetric]);
+ }
+ return styles.node.colors.background.default;
+ },
+ shape: "ellipse",
+ "text-valign": "center",
+ "text-halign": "center",
+ },
+ },
+ {
+ selector: "node.file",
+ style: {
+ shape: "roundrectangle",
+ },
+ },
+ {
+ selector: "node.symbol",
+ "border-color": (node: NodeSingular) => {
+ const data = node.data() as SymbolNapiNodeData;
+ if (data.isExternal) {
+ return styles.node.colors.border.default;
+ }
+ return styles.node.colors.border.default;
+ },
+ style: {
+ shape: (node: NodeSingular) => {
+ const data = node.data() as SymbolNapiNodeData;
+
+ if (data.isExternal) {
+ return "octagon";
+ }
+
+ if (data.symbolType === classSymbolType) {
+ return "hexagon";
+ }
+ if (data.symbolType === functionSymbolType) {
+ return "triangle";
+ }
+ if (data.symbolType === variableSymbolType) {
+ return "ellipse";
+ }
+ if (data.symbolType === structSymbolType) {
+ return "hexagon";
+ }
+ if (data.symbolType === enumSymbolType) {
+ return "hexagon";
+ }
+ if (data.symbolType === interfaceSymbolType) {
+ return "hexagon";
+ }
+ if (data.symbolType === recordSymbolType) {
+ return "hexagon";
+ }
+ if (data.symbolType === delegateSymbolType) {
+ return "hexagon";
+ }
+ },
+ },
+ },
+ {
+ selector: "node.collapsed",
+ style: {
+ label: "data(collapsed.label)",
+ width: "data(collapsed.width)",
+ height: "data(collapsed.height)",
+ "z-index": 1000,
+ },
+ },
+ {
+ selector: "node.expanded",
+ style: {
+ label: "data(expanded.label)",
+ width: "data(expanded.width)",
+ height: "data(expanded.height)",
+ "z-index": 2000,
+ },
+ },
+ {
+ selector: "node.highlighted",
+ style: {
+ "border-width": styles.node.width.highlighted,
+ "border-color": styles.node.colors.border.highlighted,
+ },
+ },
+ {
+ selector: "node.selected",
+ style: {
+ "border-color": styles.node.colors.border.selected,
+ },
+ },
+
+ // Edge specific styles
+ {
+ selector: "edge",
+ style: {
+ width: styles.edge.width.default,
+ "line-color": styles.edge.colors.default,
+ "target-arrow-color": styles.edge.colors.default,
+ "target-arrow-shape": "triangle",
+ "curve-style": "straight",
+ },
+ },
+ {
+ selector: "edge.dependency",
+ style: {
+ width: styles.edge.width.highlighted,
+ "line-color": styles.edge.colors.dependency,
+ "target-arrow-color": styles.edge.colors.dependency,
+ },
+ },
+ {
+ selector: "edge.dependent",
+ style: {
+ width: styles.edge.width.highlighted,
+ "line-color": styles.edge.colors.dependent,
+ "target-arrow-color": styles.edge.colors.dependent,
+ },
+ },
+
+ // All elements styles
+ {
+ selector: ".background",
+ style: {
+ "opacity": 0.1,
+ },
+ },
+ {
+ selector: ".hidden",
+ style: {
+ "opacity": 0,
+ },
+ },
+ ] as StylesheetJson;
+
+ return stylesheet;
+}
diff --git a/packages/app/src/helpers/cytoscape/symbolDependencyVisualizer/index.ts b/packages/app/src/helpers/cytoscape/symbolDependencyVisualizer/index.ts
new file mode 100644
index 00000000..57469bb5
--- /dev/null
+++ b/packages/app/src/helpers/cytoscape/symbolDependencyVisualizer/index.ts
@@ -0,0 +1,294 @@
+import {
+ type AuditManifest,
+ classSymbolType,
+ type DependencyManifest,
+ enumSymbolType,
+ functionSymbolType,
+ type Metric,
+ structSymbolType,
+ variableSymbolType,
+} from "@napi/shared";
+import type {
+ Collection,
+ Core,
+ EventObjectNode,
+ NodeSingular,
+} from "cytoscape";
+import fcose from "cytoscape-fcose";
+import type { SymbolNapiNodeData } from "../elements/types.ts";
+import cytoscape from "cytoscape";
+import { mainLayout } from "../layout/index.ts";
+import { getCytoscapeStylesheet } from "../styles/index.ts";
+import { computeNodeId } from "../elements/file.ts";
+import { getSymbolElementsForSymbol } from "../elements/symbol.ts";
+
+/**
+ * FileDependencyVisualizer creates an interactive graph of symbol dependencies within a file.
+ *
+ * This visualization provides a detailed view of internal file structure where:
+ * - Nodes represent symbols (functions, classes, variables) within the file
+ * - Edges represent dependencies between symbols
+ * - Node shapes indicate symbol types (hexagon for classes, ellipse for functions, etc.)
+ * - Colors indicate metrics severity for each symbol
+ *
+ * Key features:
+ * - Focus on a single file's internal structure and external dependencies
+ * - Different node shapes for different symbol types (classes, functions, variables)
+ * - Theme-aware visualization with optimized colors for light/dark modes
+ * - Selectable metric visualization for symbol-level analysis
+ * - Interactive node selection with focus on direct dependencies
+ * - Visual distinction between internal and external symbols
+ * - Comprehensive display of audit information for each symbol
+ *
+ * The visualization is designed to help developers understand code organization
+ * at the symbol level, identify complex relationships, and analyze internal
+ * dependencies within files.
+ */
+export class SymbolDependencyVisualizer {
+ public cy: Core;
+ private theme: "light" | "dark";
+ private layout = mainLayout;
+ private filePath: string;
+ private symbolId: string;
+ /** Currently selected node in the graph */
+ private selectedNodeId: string | undefined;
+ /** Callback functions triggered by graph interactions */
+ private externalCallbacks: {
+ onAfterNodeClick: () => void;
+ onAfterNodeDblClick: (filePath: string, symbolId: string) => void;
+ onAfterNodeRightClick: (data: {
+ position: { x: number; y: number };
+ filePath: string;
+ symbolId: string;
+ }) => void;
+ };
+
+ constructor(
+ container: HTMLElement,
+ filePath: string,
+ symbolId: string,
+ dependencyDepth: number,
+ dependentDepth: number,
+ dependencyManifest: DependencyManifest,
+ auditManifest: AuditManifest,
+ options?: {
+ theme?: "light" | "dark";
+ defaultMetric?: Metric | undefined;
+ onAfterNodeClick?: () => void;
+ onAfterNodeRightClick?: (data: {
+ position: { x: number; y: number };
+ filePath: string;
+ symbolId: string;
+ }) => void;
+ onAfterNodeDblClick?: (filePath: string, symbolId: string) => void;
+ },
+ ) {
+ this.filePath = filePath;
+ this.symbolId = symbolId;
+
+ const defaultOptions = {
+ onAfterNodeClick: () => {},
+ onAfterNodeDblClick: () => {},
+ onAfterNodeRightClick: () => {},
+ theme: "light" as const,
+ defaultMetric: undefined,
+ };
+
+ const mergedOptions = { ...defaultOptions, ...options };
+
+ this.externalCallbacks = {
+ onAfterNodeClick: mergedOptions.onAfterNodeClick,
+ onAfterNodeDblClick: mergedOptions.onAfterNodeDblClick,
+ onAfterNodeRightClick: mergedOptions.onAfterNodeRightClick,
+ };
+
+ this.theme = mergedOptions.theme;
+
+ cytoscape.use(fcose);
+ this.cy = cytoscape();
+ this.cy.mount(container);
+
+ this.cy.batch(() => {
+ const elements = getSymbolElementsForSymbol(
+ this.filePath,
+ this.symbolId,
+ dependencyDepth,
+ dependentDepth,
+ dependencyManifest,
+ auditManifest,
+ );
+ this.cy.add(elements);
+
+ const allNodes = this.cy.nodes();
+ const selectedNode = this.cy.nodes().filter(
+ (node) => {
+ const data = node.data() as SymbolNapiNodeData;
+ return data.fileName === this.filePath &&
+ data.symbolName === this.symbolId;
+ },
+ );
+ selectedNode.addClass("selected");
+ selectedNode.addClass("expanded");
+
+ allNodes.addClass("symbol");
+
+ const stylesheet = getCytoscapeStylesheet(
+ undefined,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
+
+ this.layoutGraph(this.cy);
+
+ this.createEventListeners();
+ });
+ }
+
+ /**
+ * Updates the theme of the visualization between light and dark mode
+ *
+ * @param theme - The theme to switch to ("light" or "dark")
+ */
+ public updateTheme(theme: "light" | "dark") {
+ this.theme = theme;
+ const stylesheet = getCytoscapeStylesheet(
+ undefined,
+ this.theme,
+ );
+ this.cy.style(stylesheet);
+ }
+
+ /**
+ * Highlights a specific node in the graph
+ *
+ * @param nodeId - The ID of the node to highlight
+ */
+ public highlightNode(ref: { filePath: string; symbolId?: string }) {
+ if (ref.symbolId) {
+ const nodeId = computeNodeId(ref.filePath, ref.symbolId);
+
+ const highlightedNode = this.cy.nodes(`node[id="${nodeId}"]`);
+ const allElements = this.cy.elements();
+ const otherElements = allElements.difference(highlightedNode);
+
+ otherElements.removeClass("highlighted");
+ highlightedNode.addClass("highlighted");
+ }
+ }
+
+ /**
+ * Unhighlights all nodes in the graph
+ */
+ public unhighlightNodes() {
+ const allElements = this.cy.elements();
+
+ allElements.removeClass("highlighted");
+ }
+
+ public filterNodes(
+ showExternal: boolean,
+ showVariables: boolean,
+ showFunctions: boolean,
+ showClasses: boolean,
+ showStructs: boolean,
+ showEnums: boolean,
+ ) {
+ const nodesToHide = this.cy.nodes().filter((node: NodeSingular) => {
+ const data = node.data() as SymbolNapiNodeData;
+ if (
+ data.fileName === this.filePath && data.symbolName === this.symbolId
+ ) {
+ // never hide the current symbol
+ return false;
+ }
+
+ if (!showExternal && data.isExternal) {
+ return true;
+ }
+ if (!showVariables && data.symbolType === variableSymbolType) {
+ return true;
+ }
+ if (!showFunctions && data.symbolType === functionSymbolType) {
+ return true;
+ }
+ if (!showClasses && data.symbolType === classSymbolType) {
+ return true;
+ }
+ if (!showStructs && data.symbolType === structSymbolType) {
+ return true;
+ }
+ if (!showEnums && data.symbolType === enumSymbolType) {
+ return true;
+ }
+ return false;
+ });
+
+ const elementsToHide = nodesToHide.connectedEdges().union(nodesToHide);
+ const otherElements = this.cy.elements().difference(elementsToHide);
+
+ elementsToHide.addClass("hidden");
+ otherElements.removeClass("hidden");
+ }
+
+ /**
+ * Sets up event listeners for node interactions:
+ * - Click: Selects a node and highlights its connections
+ * - Double-click: Triggers the external double-click callback
+ * - Right-click: Opens context menu via the external callback
+ */
+ private createEventListeners() {
+ this.cy.on("onetap", "node", (evt: EventObjectNode) => {
+ const nodeId = evt.target.id();
+ this.selectedNodeId = nodeId;
+
+ const selectedNode = this.cy.nodes(`node[id="${nodeId}"]`);
+ const isAlreadyExpanded = selectedNode[0].hasClass("expanded");
+
+ if (isAlreadyExpanded) {
+ selectedNode.removeClass("expanded");
+ } else {
+ selectedNode.addClass("expanded");
+ }
+
+ this.externalCallbacks.onAfterNodeClick();
+ });
+
+ this.cy.on("dbltap", "node", (evt: EventObjectNode) => {
+ const node = evt.target;
+ const data = node.data() as SymbolNapiNodeData;
+
+ // If the node is external, ignore it
+ if (data.isExternal) return;
+
+ this.externalCallbacks.onAfterNodeDblClick(
+ data.fileName,
+ data.id,
+ );
+ });
+
+ this.cy.on("cxttap", "node", (evt: EventObjectNode) => {
+ const node = evt.target;
+ const data = node.data() as SymbolNapiNodeData;
+
+ // If the node is external, ignore it
+ if (data.isExternal) return;
+
+ const { x, y } = node.renderedPosition();
+ this.externalCallbacks.onAfterNodeRightClick({
+ position: { x, y },
+ filePath: data.fileName,
+ symbolId: data.symbolName,
+ });
+ });
+ }
+
+ /**
+ * Applies the graph layout algorithm to position nodes optimally
+ *
+ * @param collection - The collection of elements to layout (defaults to all nodes)
+ */
+ public layoutGraph(collection: Collection | Core) {
+ const collectionToLayout = collection || this.cy.nodes();
+ collectionToLayout.layout(this.layout).run();
+ }
+}
diff --git a/packages/app/src/helpers/cytoscape/views/auditFile.ts b/packages/app/src/helpers/cytoscape/views/auditFile.ts
deleted file mode 100644
index e4faf362..00000000
--- a/packages/app/src/helpers/cytoscape/views/auditFile.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import type { ElementDefinition, StylesheetJson } from "cytoscape";
-import type { FcoseLayoutOptions } from "cytoscape-fcose";
-import type { Theme } from "../../../contexts/ThemeContext.tsx";
-import { getCssValue } from "../../css/index.ts";
-
-export interface NodeElementDefinition extends ElementDefinition {
- data: {
- id: string;
- label: string;
- position: { x: number; y: number };
- parent?: string;
- isExpanded: boolean;
- type: "file" | "instance";
- isCurrentFile: boolean;
- isExternal: boolean;
- customData: {
- fileName: string;
- instance?: {
- name: string;
- type?: string;
- };
- errorMessages: string[];
- warningMessages: string[];
- } & object;
- };
-}
-
-export interface EdgeElementDefinition extends ElementDefinition {
- data: {
- source: string;
- target: string;
- type: "dependency" | "dependent";
- };
-}
-
-export function getCyStyle(theme: Theme) {
- return [
- // filenode general style
- {
- selector: "node[type = 'file']",
- style: {
- label: "data(label)",
- "text-wrap": "wrap",
- "background-color": getCssValue(`--color-background-${theme}`),
- "border-width": 2,
- "border-color": getCssValue(`--color-border-${theme}`),
- opacity: 0.8,
- "text-valign": "top",
- "text-halign": "center",
- shape: "roundrectangle",
- },
- },
- // current filenode
- {
- selector: "node[type = 'file'][isCurrentFile]",
- style: {
- color: getCssValue(`--color-primary-${theme}`),
- },
- },
- // external filenode
- {
- selector: "node[type = 'file'][!isCurrentFile][isExternal]",
- style: {
- color: getCssValue(`--color-gray-${theme}`),
- },
- },
- // file node non external
- {
- selector: "node[type = 'file'][!isCurrentFile][!isExternal]",
- style: {
- color: getCssValue(`--color-secondary-${theme}`),
- },
- },
- // instancenode general style
- {
- selector: "node[type = 'instance']",
- style: {
- label: "data(label)",
- "text-wrap": "wrap",
- "text-valign": "center",
- "text-halign": "center",
- color: getCssValue(`--color-text-${theme}`),
- shape: "roundrectangle",
- width: "data(customData.nodeWidth)",
- },
- },
- // current instancenode
- {
- selector: "node[type = 'instance'][isCurrentFile]",
- style: {
- "background-color": getCssValue(`--color-primary-${theme}`),
- "background-opacity": 0.2,
- "border-color": getCssValue(`--color-primary-${theme}`),
- "border-width": 2,
- },
- },
- // external instancenode
- {
- selector: "node[type = 'instance'][!isCurrentFile][isExternal]",
- style: {
- "background-color": getCssValue(`--color-gray-${theme}`),
- "background-opacity": 0.2,
- "border-color": getCssValue(`--color-gray-${theme}`),
- "border-width": 2,
- },
- },
- // instance node non external
- {
- selector: "node[type = 'instance'][!isCurrentFile][!isExternal]",
- style: {
- "background-color": getCssValue(`--color-secondary-${theme}`),
- "background-opacity": 0.2,
- "border-color": getCssValue(`--color-secondary-${theme}`),
- "border-width": 2,
- },
- },
- // dependency edge
- {
- selector: "edge[type = 'dependency']",
- style: {
- width: 2,
- "line-color": getCssValue(`--color-primary-${theme}`),
- "target-arrow-color": getCssValue(`--color-primary-${theme}`),
- "target-arrow-shape": "triangle",
- "curve-style": "bezier",
- },
- },
- // dependent edge
- {
- selector: "edge[type = 'dependent']",
- style: {
- width: 2,
- "line-color": getCssValue(`--color-secondary-${theme}`),
- "target-arrow-color": getCssValue(`--color-secondary-${theme}`),
- "target-arrow-shape": "triangle",
- "curve-style": "bezier",
- },
- },
- // hide elements
- {
- selector: ".hidden",
- style: {
- display: "none",
- },
- },
- ] as StylesheetJson;
-}
-
-export const layout = {
- name: "fcose",
- quality: "proof",
- nodeRepulsion: 1000,
- idealEdgeLength: 200,
- gravity: 0.1,
- gravityCompound: 1000,
- packComponents: true,
- nodeDimensionsIncludeLabels: true,
-} as FcoseLayoutOptions;
-
-const errorChar = "❗";
-const warningChar = "⚠️";
-const successChar = "🎉";
-
-export function getNodeLabel(data: {
- isExpanded: boolean;
- isExternal: boolean;
- type: "file" | "instance";
- fileName: string;
- instance?: {
- name: string;
- type?: string;
- };
- errorMessages: string[];
- warningMessages: string[];
-}) {
- let label = "";
- if (data.isExpanded) {
- if (data.type === "file") {
- label = data.fileName;
- if (data.isExternal) {
- label += "\n(External reference)";
- }
- } else if (data.type === "instance" && data.instance) {
- label = `Name: ${data.instance.name}`;
- if (data.instance.type) {
- label += `\nType: ${data.instance.type}`;
- }
- }
-
- if (!data.isExternal) {
- if (data.errorMessages.length > 0 || data.warningMessages.length > 0) {
- data.errorMessages.forEach((message) => {
- label += `\n${errorChar} ${message}`;
- });
- data.warningMessages.forEach((message) => {
- label += `\n${warningChar} ${message}`;
- });
- } else {
- label += `\n${successChar} No issues found`;
- }
- }
-
- return label;
- }
-
- if (data.type === "file") {
- label = data.fileName;
- if (data.isExternal) {
- label += " (External)";
- }
- } else if (data.type === "instance" && data.instance) {
- label = data.instance.name;
- }
-
- if (!data.isExternal) {
- if (data.errorMessages.length > 0) {
- label += `\n${errorChar}(${data.errorMessages.length})`;
- } else if (data.warningMessages.length > 0) {
- label += `\n${warningChar}(${data.warningMessages.length})`;
- }
- }
-
- return label;
-}
diff --git a/packages/app/src/helpers/cytoscape/views/auditInstance.ts b/packages/app/src/helpers/cytoscape/views/auditInstance.ts
deleted file mode 100644
index cca599c8..00000000
--- a/packages/app/src/helpers/cytoscape/views/auditInstance.ts
+++ /dev/null
@@ -1,260 +0,0 @@
-import type { DependencyManifest, FileDependencyManifest } from "@napi/shared";
-import type { ElementDefinition } from "cytoscape";
-import { type EdgeElementDefinition, getNodeLabel } from "./auditFile.ts";
-import { getNodeWidthAndHeightFromLabel } from "../label/index.ts";
-
-interface NodeElementDefinition extends ElementDefinition {
- data: {
- id: string;
- label: string;
- position: { x: number; y: number };
- parent?: string;
- isExpanded: boolean;
- type: "file" | "instance";
- isCurrentFile: boolean;
- isExternal: boolean;
- customData: {
- fileName: string;
- instance?: {
- name: string;
- type?: string;
- };
- errorMessages: string[];
- warningMessages: string[];
- nodeWidth: number;
- } & object;
- };
-}
-
-type NodeMap = Record<
- string,
- {
- element: NodeElementDefinition;
- children: NodeMap;
- }
->;
-
-function getNodeId(filePath: string, symbolName: string): string {
- const joinChar = "|";
- return `${filePath}${joinChar}${symbolName}`;
-}
-
-function createNodeElement(params: {
- fileId: string;
- filePath: string;
- symbolName: string;
- symbolType?: string;
- isExternal: boolean;
- isCurrentFile: boolean;
- errorMessages?: string[];
- warningMessages?: string[];
-}): NodeElementDefinition {
- const {
- fileId,
- filePath,
- symbolName,
- symbolType,
- isExternal,
- isCurrentFile,
- errorMessages = [],
- warningMessages = [],
- } = params;
-
- const id = getNodeId(fileId, symbolName);
- const label = getNodeLabel({
- isExpanded: false,
- isExternal,
- type: "instance",
- fileName: filePath,
- instance: { name: symbolName, type: symbolType },
- errorMessages,
- warningMessages,
- });
-
- const { width } = getNodeWidthAndHeightFromLabel(label);
-
- return {
- data: {
- id,
- label,
- position: { x: 0, y: 0 },
- isExpanded: false,
- type: "instance",
- isCurrentFile,
- isExternal,
- parent: fileId,
- customData: {
- fileName: filePath,
- instance: { name: symbolName, type: symbolType },
- errorMessages,
- warningMessages,
- nodeWidth: width,
- },
- },
- };
-}
-
-function traverseGraphAdaptive(
- manifest: DependencyManifest,
- file: FileDependencyManifest,
- symbolName: string,
- currentDepth: number,
- maxDepsDepth: number,
- maxDependentsDepth: number,
- visited: Set,
-): {
- localNodeMap: NodeMap;
- localEdges: EdgeElementDefinition[];
-} {
- const nodeMap: NodeMap = {};
- const edges: EdgeElementDefinition[] = [];
-
- const currentSymbol = file.symbols[symbolName];
- const currentId = getNodeId(file.id, symbolName);
-
- // Traverse dependencies
- if (currentDepth < maxDepsDepth) {
- for (const depFileId in currentSymbol.dependencies) {
- const depInfo = currentSymbol.dependencies[depFileId];
- const depFile = manifest[depFileId];
-
- for (const depSymbolName in depInfo.symbols) {
- const depId = getNodeId(depFileId, depSymbolName);
- if (visited.has(depId)) continue;
- visited.add(depId);
-
- const depSymbol = depFile?.symbols?.[depSymbolName];
- const depNode = createNodeElement({
- fileId: depFileId,
- filePath: depFile?.filePath ?? depFileId,
- symbolName: depSymbolName,
- symbolType: depSymbol?.type,
- isExternal: depInfo.isExternal,
- isCurrentFile: false,
- });
-
- nodeMap[depId] = { element: depNode, children: {} };
- edges.push({
- data: { source: depId, target: currentId, type: "dependency" },
- });
-
- if (!depInfo.isExternal && depFile) {
- const sub = traverseGraphAdaptive(
- manifest,
- depFile,
- depSymbolName,
- currentDepth + 1,
- maxDepsDepth,
- maxDependentsDepth,
- visited,
- );
- Object.assign(nodeMap, sub.localNodeMap);
- edges.push(...sub.localEdges);
- }
- }
- }
- }
-
- // Traverse dependents
- if (currentDepth < maxDependentsDepth) {
- for (const depFileId in currentSymbol.dependents) {
- const depInfo = currentSymbol.dependents[depFileId];
- const depFile = manifest[depFileId];
-
- for (const depSymbolName in depInfo.symbols) {
- const depId = getNodeId(depFileId, depSymbolName);
- if (visited.has(depId)) continue;
- visited.add(depId);
-
- const depSymbol = depFile?.symbols?.[depSymbolName];
- const depNode = createNodeElement({
- fileId: depFileId,
- filePath: depFile?.filePath ?? depFileId,
- symbolName: depSymbolName,
- symbolType: depSymbol?.type,
- isExternal: false,
- isCurrentFile: false,
- });
-
- nodeMap[depId] = { element: depNode, children: {} };
- edges.push({
- data: { source: currentId, target: depId, type: "dependent" },
- });
-
- if (depFile) {
- const sub = traverseGraphAdaptive(
- manifest,
- depFile,
- depSymbolName,
- currentDepth + 1,
- maxDepsDepth,
- maxDependentsDepth,
- visited,
- );
- Object.assign(nodeMap, sub.localNodeMap);
- edges.push(...sub.localEdges);
- }
- }
- }
- }
-
- return { localNodeMap: nodeMap, localEdges: edges };
-}
-
-export function getInstanceCyElements(
- dependencyManifest: DependencyManifest,
- currentFilePath: string,
- currentSymbolName: string,
- dependencyDepth = 1,
- dependentDepth = 1,
-) {
- const currentFile = dependencyManifest[currentFilePath];
- if (!currentFile) throw new Error(`File not found: ${currentFilePath}`);
-
- const currentSymbol = currentFile.symbols[currentSymbolName];
- if (!currentSymbol) throw new Error(`Symbol not found: ${currentSymbolName}`);
-
- const currentId = getNodeId(currentFile.id, currentSymbolName);
- const nodeMap: NodeMap = {};
- const edges: EdgeElementDefinition[] = [];
-
- const currentSymbolElement = createNodeElement({
- fileId: currentFile.id,
- filePath: currentFilePath,
- symbolName: currentSymbolName,
- symbolType: currentSymbol.type,
- isExternal: false,
- isCurrentFile: true,
- });
-
- nodeMap[currentId] = { element: currentSymbolElement, children: {} };
-
- const visited = new Set([currentId]);
-
- const results = traverseGraphAdaptive(
- dependencyManifest,
- currentFile,
- currentSymbolName,
- 0,
- dependencyDepth,
- dependentDepth,
- visited,
- );
-
- Object.assign(nodeMap, results.localNodeMap);
- edges.push(...results.localEdges);
-
- const traverse = (
- nodeMap: NodeMap,
- nodeElements: NodeElementDefinition[] = [],
- ): NodeElementDefinition[] => {
- for (const node of Object.values(nodeMap)) {
- nodeElements.push(node.element);
- traverse(node.children, nodeElements);
- }
- return nodeElements;
- };
-
- const nodeElements = traverse(nodeMap);
- return [...nodeElements, ...edges];
-}
diff --git a/packages/app/src/helpers/index.ts b/packages/app/src/helpers/index.ts
deleted file mode 100644
index 77938727..00000000
--- a/packages/app/src/helpers/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const defaultMaxPathLength = 25;
-
-export function getDisplayedPath(
- path: string,
- maxPathLength: number = defaultMaxPathLength,
-) {
- if (path.length > maxPathLength) {
- return `...${path.slice(-maxPathLength)}`;
- }
-
- return path;
-}
diff --git a/packages/app/src/hooks/index.ts b/packages/app/src/hooks/index.ts
deleted file mode 100644
index 2878cb11..00000000
--- a/packages/app/src/hooks/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useMatches } from "react-router";
-import type { RouteHandle, ViewNames } from "./types.ts";
-
-export function useCurrentViewName(): ViewNames {
- const matches = useMatches();
- if (!matches || matches.length === 0) {
- throw new Error("No matches found");
- }
-
- const currentMatch = matches[matches.length - 1];
- if (!currentMatch) {
- throw new Error("No current match found");
- }
-
- const handle = currentMatch.handle as RouteHandle;
-
- return handle.viewName;
-}
diff --git a/packages/app/src/hooks/types.ts b/packages/app/src/hooks/types.ts
deleted file mode 100644
index da2b616d..00000000
--- a/packages/app/src/hooks/types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export enum ViewNames {
- PROJECT = "project",
- FILE = "file",
- INSTANCE = "instance",
- NOT_FOUND = "not_found",
-}
-
-// Needed because react-router doesn't type UIMatch correctly
-export interface RouteHandle {
- viewName: ViewNames;
-}
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
deleted file mode 100644
index 7097e638..00000000
--- a/packages/app/src/index.css
+++ /dev/null
@@ -1,46 +0,0 @@
-@import "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap";
-
-@import "react-toastify/dist/ReactToastify.css";
-@import "@radix-ui/themes/styles.css";
-@import "tailwindcss";
-
-@custom-variant dark (&:where(.dark, .dark *));
-
-@theme {
- /* Fonts */
- --font-family: "Plus Jakarta Sans", sans-serif;
-
- /* Colors */
- --color-background-dark: #0B0A32;
- --color-background-light: #F9FAFB;
-
- --color-secondaryBackground-dark: #15143D;
- --color-secondaryBackground-light: #E5E7EB;
-
- --color-surface-dark: #3A397C;
- --color-surface-light: #FFFFFF;
-
- --color-secondarySurface-dark: #25235C;
- --color-secondarySurface-light: #F0F1F3;
-
- --color-border-dark: #3A397C;
- --color-border-light: #BDBDBD;
-
- --color-text-dark: #FFFFFF;
- --color-text-light: #333333;
-
- --color-gray-dark: #B4B4C9;
- --color-gray-light: #6B7280;
-
- --color-primary-dark: #695AF0;
- --color-primary-light: #9F99FF;
-
- --color-secondary-dark: #E43F8C;
- --color-secondary-light: #F1729E;
-
- --color-focus-dark: #695AF0;
- --color-focus-light: #7B74FF;
-
- --color-highlight-dark: #FFB800;
- --color-highlight-light: #FFB800;
-}
diff --git a/packages/app/src/layout/GraphLayout.tsx b/packages/app/src/layout/GraphLayout.tsx
deleted file mode 100644
index 0802d1fe..00000000
--- a/packages/app/src/layout/GraphLayout.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { type ReactNode, useContext } from "react";
-import { Button } from "@radix-ui/themes";
-import {
- darkTheme,
- lightTheme,
- ThemeContext,
-} from "../contexts/ThemeContext.tsx";
-import { Link, useParams } from "react-router";
-import { MdDarkMode, MdKeyboardArrowRight, MdLightMode } from "react-icons/md";
-import { useCurrentViewName } from "../hooks/index.ts";
-import { ViewNames } from "../hooks/types.ts";
-
-export default function GraphLayout(props: {
- sideBarSlot?: ReactNode;
- graphSlot: ReactNode;
-}) {
- const themeContext = useContext(ThemeContext);
- const { file, instance } = useParams();
-
- const currentView = useCurrentViewName();
-
- return (
-
- {props.sideBarSlot && (
-
- {props.sideBarSlot}
-
- )}
-
-
-
-
- {
- /* */
- }
- Project
- {/* */}
-
- {[ViewNames.PROJECT, ViewNames.FILE].includes(currentView) &&
- file && (
- <>
-
-
- File:
-
-
- {file}
-
- >
- )}
- {currentView === ViewNames.INSTANCE && file && instance && (
- <>
-
-
- Symbol:
-
-
- {instance}
-
- >
- )}
-
-
- themeContext.changeTheme(lightTheme)}
- className={`p-2.5 rounded-md ${
- themeContext.theme === lightTheme
- ? "bg-secondarySurface-light"
- : ""
- }`}
- >
-
-
- themeContext.changeTheme(darkTheme)}
- className={`p-2.5 rounded-md ${
- themeContext.theme === darkTheme
- ? "bg-secondarySurface-dark"
- : ""
- }`}
- >
-
-
-
-
-
- {props.graphSlot}
-
-
-
- );
-}
diff --git a/packages/app/src/lib/utils.ts b/packages/app/src/lib/utils.ts
new file mode 100644
index 00000000..365058ce
--- /dev/null
+++ b/packages/app/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/packages/app/src/main.tsx b/packages/app/src/main.tsx
index 33e834f7..5a5526e1 100644
--- a/packages/app/src/main.tsx
+++ b/packages/app/src/main.tsx
@@ -1,17 +1,14 @@
///
-import { StrictMode, useContext } from "react";
+import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
-import { Theme } from "@radix-ui/themes";
-import "./index.css";
import { createBrowserRouter, Navigate, RouterProvider } from "react-router";
-import { ToastContainer } from "react-toastify";
-import { ThemeContext, ThemeProvider } from "./contexts/ThemeContext.tsx";
import BaseAuditPage from "./pages/audit/base.tsx";
import AuditPage from "./pages/audit/index.tsx";
import AuditFilePage from "./pages/audit/file/index.tsx";
import AuditInstancePage from "./pages/audit/file/instance/index.tsx";
-import { ViewNames } from "./hooks/types.ts";
+import { Toaster } from "./components/shadcn/Toaster.tsx";
+import { ThemeProvider } from "./contexts/ThemeProvider.tsx";
const router = createBrowserRouter([
{
@@ -25,17 +22,14 @@ const router = createBrowserRouter([
{
path: "/audit",
element: ,
- handle: { viewName: ViewNames.PROJECT },
},
{
path: "/audit/:file",
element: ,
- handle: { viewName: ViewNames.FILE },
},
{
path: "/audit/:file/:instance",
element: ,
- handle: { viewName: ViewNames.INSTANCE },
},
],
},
@@ -47,23 +41,11 @@ if (!rootElement) {
throw new Error("Root element not found");
}
-function Main() {
- const themeContext = useContext(ThemeContext);
-
- return (
-
-
-
-
-
-
- );
-}
-
ReactDOM.createRoot(rootElement).render(
-
+
+
,
);
diff --git a/packages/app/src/pages/audit/base.tsx b/packages/app/src/pages/audit/base.tsx
index 2b22c56f..2b49fc75 100644
--- a/packages/app/src/pages/audit/base.tsx
+++ b/packages/app/src/pages/audit/base.tsx
@@ -1,107 +1,138 @@
-import { useEffect, useRef, useState } from "react";
-import { Outlet } from "react-router";
-import { toast } from "react-toastify";
+import { useEffect, useState } from "react";
+import { Link, Outlet, useParams } from "react-router";
import {
getAuditManifest,
getDependencyManifest,
+ runExtraction,
} from "../../service/api/index.ts";
-import GraphLayout from "../../layout/GraphLayout.tsx";
-import FileExplorer, {
- type FileExplorerFile,
-} from "../../components/FileExplorer/FileExplorer.tsx";
import type {
AuditManifest,
DependencyManifest,
- ExtractionNode,
+ SymbolsToExtract,
} from "@napi/shared";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarProvider,
+ SidebarRail,
+ SidebarTrigger,
+} from "../../components/shadcn/Sidebar.tsx";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbSeparator,
+} from "../../components/shadcn/Breadcrumb.tsx";
+import { Button } from "../../components/shadcn/Button.tsx";
+import { Skeleton } from "../../components/shadcn/Skeleton.tsx";
+import { Input } from "../../components/shadcn/Input.tsx";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "../../components/shadcn/Tooltip.tsx";
+import {
+ ChevronDown,
+ ChevronRight,
+ CircleMinus,
+ Code,
+ File,
+ Loader,
+ Moon,
+ Pickaxe,
+ ScanEye,
+ SearchCode,
+ Sun,
+} from "lucide-react";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "../../components/shadcn/Resizable.tsx";
+import { ScrollArea, ScrollBar } from "../../components/shadcn/Scrollarea.tsx";
+import { useToast } from "../../components/shadcn/hooks/use-toast.tsx";
+import { useTheme } from "../../contexts/ThemeProvider.tsx";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "../../components/shadcn/Card.tsx";
+import DisplayNameWithTooltip from "../../components/DisplayNameWithTootip.tsx";
export interface AuditContext {
busy: boolean;
dependencyManifest: DependencyManifest;
auditManifest: AuditManifest;
- highlightedNodeId: string | null;
- extractionNodes: Record;
- actions: {
- setHighlightedNodeId: (nodeId: string | null) => void;
- showInSidebar: (filename: string) => void;
- updateExtractionNodes: (
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) => void;
- };
+ highlightedCytoscapeRef: {
+ filePath: string;
+ symbolId: string | undefined;
+ } | undefined;
+ onAddSymbolsForExtraction: (
+ filePath: string,
+ symbolIds: string[],
+ ) => void;
}
export default function BaseAuditPage() {
- const initialized = useRef(false);
+ const { theme, setTheme } = useTheme();
- const [busy, setBusy] = useState(true);
- const [sidebarOpen, setSidebarOpen] = useState(false);
- const [sidebarSearch, setSidebarSearch] = useState("");
+ const { file, instance } = useParams();
- const [files, setFiles] = useState([]);
+ const { toast } = useToast();
+
+ const [busy, setBusy] = useState(true);
const [auditManifest, setAuditManifest] = useState({});
const [dependencyManifest, setDependencyManifest] = useState<
DependencyManifest
>({});
- const [highlightedNodeId, setHighlightedNodeId] = useState(
- null,
- );
- const [extractionNodes, setExtractionNodes] = useState<
- Record
- >({});
-
- function showInSidebar(filename: string) {
- setSidebarOpen(true);
- setSidebarSearch(filename);
- }
+ const [highlightedCytoscapeRef, setHighlightedCytoscapeRef] = useState<
+ {
+ filePath: string;
+ symbolId: string | undefined;
+ } | undefined
+ >(undefined);
- function updateExtractionNodes(
- filePath: string,
- symbols: string[],
- action: "add" | "remove",
- ) {
- // When doing anything here, we need to make sure the sidebar is open
- setSidebarOpen(true);
-
- if (filePath in extractionNodes) {
- const existingSymbols = extractionNodes[filePath].symbols;
- const newSymbols = action === "add"
- ? [...new Set([...existingSymbols, ...symbols])]
- : existingSymbols.filter((symbol) => !symbols.includes(symbol));
-
- setExtractionNodes((prev) => {
- // Ensure react forces a re-render
- const newExtractionNodes = { ...prev };
-
- // Remove a file if no symbols are left
- // This is to prevent the file from being shown in the sidebar
- if (newSymbols.length === 0) {
- delete newExtractionNodes[filePath];
- return newExtractionNodes;
- }
+ const [symbolsToExtract, setSymbolsToExtract] = useState(
+ [],
+ );
- newExtractionNodes[filePath] = {
- filePath: filePath,
- symbols: newSymbols,
- };
- return newExtractionNodes;
+ async function extractSymbols() {
+ setBusy(true);
+ const extractionToast = toast({
+ title: "Extracting symbols",
+ description: "This may take a while...",
+ });
+ try {
+ await runExtraction(symbolsToExtract);
+ extractionToast.update({
+ id: extractionToast.id,
+ description: "Symbols extracted successfully",
});
- } else {
- setExtractionNodes((prev) => {
- // Ensure react forces a re-render
- const newExtractionNodes = { ...prev };
- newExtractionNodes[filePath] = { filePath: filePath, symbols };
- return newExtractionNodes;
+ } catch (_error) {
+ extractionToast.update({
+ id: extractionToast.id,
+ description: "Failed to extract symbols",
+ variant: "destructive",
});
+ } finally {
+ setBusy(false);
}
}
useEffect(() => {
async function handleOnLoad() {
setBusy(true);
+ const allPromiseToast = toast({
+ title: "Loading manifests",
+ description: "This may take a while...",
+ });
try {
const dependencyManifestPromise = getDependencyManifest();
const auditManifestPromise = getAuditManifest();
@@ -111,68 +142,638 @@ export default function BaseAuditPage() {
auditManifestPromise,
]);
- toast.promise(allPromise, {
- success: "Successfully loaded project overview",
- error: "Failed to load project overview",
- pending: "Loading project overview...",
- });
-
const [dependencyManifest, auditManifest] = await allPromise;
setDependencyManifest(dependencyManifest);
setAuditManifest(auditManifest);
- const paths = Object.values(dependencyManifest).map((fileManifest) => ({
- path: fileManifest.filePath,
- symbols: Object.keys(fileManifest.symbols),
- }));
-
- setFiles(paths);
+ allPromiseToast.update({
+ id: allPromiseToast.id,
+ description: "Manifests loaded successfully",
+ });
+ } catch (_error) {
+ allPromiseToast.update({
+ id: allPromiseToast.id,
+ description: "Failed to load manifests",
+ variant: "destructive",
+ });
} finally {
setBusy(false);
}
}
- if (!initialized.current) {
- initialized.current = true;
- handleOnLoad();
- }
+ handleOnLoad();
}, []);
return (
-
+
+ {
+ if (!node.fileId) return;
+ const newRef = {
+ filePath: node.fileId,
+ symbolId: node.symbolId,
+ };
+ // If the new ref is the same as the current ref, we un set it (unhighlight)
+ if (
+ highlightedCytoscapeRef?.filePath === newRef.filePath &&
+ highlightedCytoscapeRef?.symbolId === newRef.symbolId
+ ) {
+ setHighlightedCytoscapeRef(undefined);
+ } else {
+ setHighlightedCytoscapeRef(newRef);
+ }
+ }}
+ toDetails={(node: ExplorerNodeData) => {
+ if (node.symbolId && node.fileId) {
+ return `/audit/${encodeURIComponent(node.fileId)}/${
+ encodeURIComponent(node.symbolId)
+ }`;
+ } else if (node.fileId) {
+ return `/audit/${encodeURIComponent(node.fileId)}`;
+ } else {
+ return "/audit";
+ }
+ }}
+ symbolsToExtract={symbolsToExtract}
+ onUpdateSymbolsToExtract={setSymbolsToExtract}
+ onExtractSymbols={extractSymbols}
+ />
+
+
+
+
+ "/audit"}
+ fileId={file}
+ toFileIdLink={(fileId) => `/audit/${encodeURIComponent(fileId)}`}
+ instanceId={instance}
+ toInstanceIdLink={(fileId, instanceId) =>
+ `/audit/${encodeURIComponent(fileId)}/${
+ encodeURIComponent(instanceId)
+ }`}
+ />
+
+
setTheme(theme === "light" ? "dark" : "light")}
+ className="mr-2"
+ >
+ {theme === "light" ? : }
+
+
+
+ {
+ const newSymbolsToExtract = [...symbolsToExtract];
+ for (const symbolId of symbolIds) {
+ // Check if there's an existing entry for this file
+ const existingIndex = newSymbolsToExtract.findIndex(
+ (s) => s.filePath === filePath,
+ );
+
+ if (existingIndex === -1) {
+ // No existing entry for this file, create a new one
+ newSymbolsToExtract.push({ filePath, symbols: [symbolId] });
+ } else {
+ // File exists, check if symbol is already included
+ if (
+ !newSymbolsToExtract[existingIndex].symbols.includes(
+ symbolId,
+ )
+ ) {
+ newSymbolsToExtract[existingIndex].symbols.push(symbolId);
+ }
+ }
+ }
+ setSymbolsToExtract(newSymbolsToExtract);
+ },
+ } as AuditContext}
+ />
+
+
+
+ );
+}
+
+function BreadcrumbNav(props: {
+ toProjectLink: () => string;
+ fileId: string | undefined;
+ toFileIdLink: (fileId: string) => string;
+ instanceId: string | undefined;
+ toInstanceIdLink: (fileId: string, instanceId: string) => string;
+}) {
+ return (
+
+
+
+
+ Project
+
+
+ {props.fileId && (
+ <>
+
+
+
+
+ {props.fileId}
+
+
+
+ {props.instanceId && (
+ <>
+
+
+
+
+ {props.instanceId}
+
+
+
+ >
+ )}
+ >
+ )}
+
+
+ );
+}
+
+interface ExplorerNodeData {
+ id: string;
+ displayName: string;
+ fileId?: string;
+ symbolId?: string;
+ children: Map;
+}
+
+function FileExplorerSidebar(props: {
+ busy: boolean;
+ dependencyManifest: DependencyManifest;
+ auditManifest: AuditManifest;
+ onHighlightInCytoscape: (node: ExplorerNodeData) => void;
+ toDetails: (node: ExplorerNodeData) => string;
+ symbolsToExtract: SymbolsToExtract;
+ onUpdateSymbolsToExtract: (symbolsToExtract: SymbolsToExtract) => void;
+ onExtractSymbols: () => void;
+}) {
+ const [search, setSearch] = useState("");
+
+ const [explorerTree, setExplorerTree] = useState();
+
+ // Build the explorer tree when the dependency manifest changes
+ useEffect(() => {
+ const tree = buildExplorerTree(props.dependencyManifest, search);
+ setExplorerTree(tree);
+ }, [props.dependencyManifest, search]);
+
+ function buildExplorerTree(
+ dependencyManifest: DependencyManifest,
+ search: string,
+ ): ExplorerNodeData | undefined {
+ const getExplorerNodeId = (filePath: string, instanceId?: string) => {
+ if (instanceId) {
+ return `${filePath}#${instanceId}`;
}
- graphSlot={
-
+ return filePath;
+ };
+
+ const root: ExplorerNodeData = {
+ id: "root",
+ displayName: "Project",
+ children: new Map(),
+ };
+
+ // Filter function to check if a string matches the search term
+ const matchesSearch = (text: string): boolean => {
+ if (!search) return true;
+ return text.toLowerCase().includes(search.toLowerCase());
+ };
+
+ // Track if any nodes match the search to avoid empty results
+ let hasMatchingNodes = false;
+
+ for (const fileDependencyManifest of Object.values(dependencyManifest)) {
+ const filePath = fileDependencyManifest.filePath;
+ const fileName = filePath.split("/").pop() || "";
+ const fileMatchesSearch = matchesSearch(fileName);
+
+ // Check if any symbols match the search
+ const matchingSymbols = Object.keys(fileDependencyManifest.symbols)
+ .filter(
+ (symbolId) => matchesSearch(symbolId),
+ );
+
+ // Skip this file if neither the file nor any symbols match the search
+ if (search && !fileMatchesSearch && matchingSymbols.length === 0) {
+ continue;
}
- />
+
+ hasMatchingNodes = true;
+
+ const parts = filePath.split("/");
+ let currentNode: ExplorerNodeData = root;
+ for (const part of parts) {
+ const id = getExplorerNodeId(part);
+ if (!currentNode.children.has(id)) {
+ currentNode.children.set(id, {
+ id: id,
+ displayName: part,
+ children: new Map(),
+ });
+ }
+ currentNode = currentNode.children.get(id)!;
+ }
+ currentNode.fileId = getExplorerNodeId(filePath);
+
+ // Only add symbols that match the search or if no search is provided
+ for (const instanceId of Object.keys(fileDependencyManifest.symbols)) {
+ if (!search || matchesSearch(instanceId)) {
+ const id = getExplorerNodeId(filePath, instanceId);
+ currentNode.children.set(id, {
+ id: id,
+ displayName: instanceId,
+ fileId: filePath,
+ symbolId: instanceId,
+ children: new Map(),
+ });
+ }
+ }
+ }
+
+ // If no nodes match the search, return an empty tree
+ if (search && !hasMatchingNodes) {
+ return undefined;
+ }
+
+ const flattenTree = (node: ExplorerNodeData): ExplorerNodeData => {
+ // First recursively flatten all children
+ if (node.children.size > 0) {
+ const flattenedChildren = new Map();
+ for (const [id, child] of node.children) {
+ const flattenedChild = flattenTree(child);
+ flattenedChildren.set(id, flattenedChild);
+ }
+ node.children = flattenedChildren;
+ }
+
+ // Then check if this node has exactly one child that can be merged
+ while (node.children.size === 1) {
+ const childEntry = Array.from(node.children.entries())[0];
+ const child = childEntry[1];
+
+ // Skip if the child has symbols (is a leaf node)
+ if (child.fileId) {
+ break;
+ }
+
+ // Merge child's name into parent
+ node.displayName = `${node.displayName}/${child.displayName}`;
+ // Update the parent's id to child's id
+ node.id = child.id;
+ // Replace parent's children with child's children
+ node.children = child.children;
+ }
+
+ return node;
+ };
+
+ // Flatten nodes that have only one child
+ return flattenTree(root);
+ }
+
+ function removeSymbolsFromExtraction(filePath: string, symbolIds: string[]) {
+ const newSymbolsToExtract = props.symbolsToExtract.map(
+ (symbolToExtract) => {
+ if (symbolToExtract.filePath === filePath) {
+ // Only filter out the specific symbol IDs from this file's symbols array
+ return {
+ ...symbolToExtract,
+ symbols: symbolToExtract.symbols.filter(
+ (symbolId) => !symbolIds.includes(symbolId),
+ ),
+ };
+ }
+ return symbolToExtract;
+ },
+ ).filter((symbolToExtract) => symbolToExtract.symbols.length > 0); // Remove entries with no symbols left
+
+ props.onUpdateSymbolsToExtract(newSymbolsToExtract);
+ }
+
+ return (
+
+
+
+
+ NanoAPI
+
+
+
+
+ {props.busy
+ ? (
+
+ {Array.from({ length: 10 }).map((_, index) => (
+
+ ))}
+
+ )
+ : (
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ />
+
+
+
+ Search for a file or symbol.
+
+ The search will find partial matches in both symbol
+ names and file paths.
+
+
+
+
+
+ {!explorerTree
+ ? (
+
+ No Matching files found
+
+ )
+ : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ Symbol Extraction
+
+
+ {props.symbolsToExtract.length === 0
+ ? (
+
+ No symbols marked for extraction yet
+
+ )
+ : (
+
+ {props.symbolsToExtract.map((symbolToExtract) => (
+
+
+
+
+
+
+
+
+ removeSymbolsFromExtraction(
+ symbolToExtract.filePath,
+ symbolToExtract.symbols,
+ )}
+ >
+
+
+
+
+
+
+ {symbolToExtract.symbols.map((symbol) => (
+
+
+
+
+
+
+ removeSymbolsFromExtraction(
+ symbolToExtract.filePath,
+ [symbol],
+ )}
+ >
+
+
+
+ ))}
+
+
+
+ ))}
+
+ {props.busy
+ ?
+ : }
+ Extract symbols
+
+
+ )}
+
+
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+function ExplorerNode(props: {
+ node: ExplorerNodeData;
+ level: number;
+ onHighlightInCytoscape: (node: ExplorerNodeData) => void;
+ toDetails: (node: ExplorerNodeData) => string;
+}) {
+ const [showChildren, setShowChildren] = useState(false);
+
+ const type: "folder" | "file" | "symbol" = props.node.symbolId
+ ? "symbol"
+ : props.node.fileId
+ ? "file"
+ : "folder";
+
+ return (
+
+ {(() => {
+ switch (type) {
+ case "folder":
+ return (
+
setShowChildren(!showChildren)}
+ className="w-full justify-start"
+ >
+ {showChildren ? : }
+
+
+ );
+ case "file":
+ return (
+
+ setShowChildren(!showChildren)}
+ className="w-full justify-start"
+ >
+
+
+
+
+
+ props.onHighlightInCytoscape(props.node)}
+ >
+
+
+
+
+ Highlight in graph
+
+
+
+
+
+
+
+
+
+
+
+ View graph for this file
+
+
+
+ );
+ case "symbol":
+ return (
+
+
+
+
+
+
+
+
+ props.onHighlightInCytoscape(props.node)}
+ >
+
+
+
+
+ Highlight in graph
+
+
+
+
+
+
+
+
+
+
+
+ View graph for this symbol
+
+
+
+
+ );
+ }
+ })()}
+ {showChildren &&
+ Array.from(props.node.children.values()).map((child) => (
+
+ ))}
+
);
}
diff --git a/packages/app/src/pages/audit/file/index.tsx b/packages/app/src/pages/audit/file/index.tsx
index e79b7a84..c03f604f 100644
--- a/packages/app/src/pages/audit/file/index.tsx
+++ b/packages/app/src/pages/audit/file/index.tsx
@@ -1,28 +1,36 @@
-import { useContext, useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import {
useNavigate,
useOutletContext,
useParams,
useSearchParams,
} from "react-router";
-import Controls from "../../../components/Cytoscape/Controls.tsx";
-import { ThemeContext } from "../../../contexts/ThemeContext.tsx";
+import Controls from "../../../components/controls/Controls.tsx";
import type { AuditContext } from "../base.tsx";
-import { CytoscapeSkeleton } from "../../../components/Cytoscape/Skeleton.tsx";
import { FileDependencyVisualizer } from "../../../helpers/cytoscape/fileDependencyVisualizer/index.ts";
-import type { NapiNodeData } from "../../../helpers/cytoscape/fileDependencyVisualizer/types.ts";
-import type { Metric } from "@napi/shared";
-import SymbolContextMenu from "../../../components/Cytoscape/contextMenu/SymbolContextMenu.tsx";
-import SymbolDetailsPane from "../../../components/SymbolDetailsPane.tsx";
-import FiltersExtension from "../../../components/Cytoscape/ControlExtensions/FiltersExtension.tsx";
-import MetricsExtension from "../../../components/Cytoscape/ControlExtensions/MetricsExtension.tsx";
+import type {
+ FileAuditManifest,
+ FileDependencyManifest,
+ Metric,
+ SymbolAuditManifest,
+ SymbolDependencyManifest,
+} from "@napi/shared";
+import MetricsExtension from "../../../components/controls/ControlExtensions/MetricsExtension.tsx";
+import { useTheme } from "../../../contexts/ThemeProvider.tsx";
+import FiltersExtension from "../../../components/controls/ControlExtensions/FiltersExtension.tsx";
+import SymbolContextMenu from "../../../components/contextMenu/SymbolContextMenu.tsx";
+import SymbolDetailsPane from "../../../components/detailsPanes/SymbolDetailsPane.tsx";
export default function AuditFilePage() {
const navigate = useNavigate();
+
+ const { theme } = useTheme();
+
const params = useParams<{ file: string }>();
+
const [searchParams, setSearchParams] = useSearchParams();
+
const context = useOutletContext();
- const themeContext = useContext(ThemeContext);
const containerRef = useRef(null);
const [busy, setBusy] = useState(true);
@@ -45,66 +53,71 @@ export default function AuditFilePage() {
setMetric(metric);
}
- const [contextMenuOpen, setContextMenuOpen] = useState(false);
- const [contextMenuPosition, setContextMenuPosition] = useState({
- x: 0,
- y: 0,
- });
- const [contextMenuSymbolId, setContextMenuSymbolId] = useState<
- string | undefined
+ const [contextMenu, setContextMenu] = useState<
+ {
+ position: { x: number; y: number };
+ fileDependencyManifest: FileDependencyManifest;
+ symbolDependencyManifest: SymbolDependencyManifest;
+ } | undefined
>(undefined);
- const [detailsPaneSymbolId, setDetailsPaneSymbolId] = useState<
- string | undefined
+ const [detailsPane, setDetailsPane] = useState<
+ {
+ fileDependencyManifest: FileDependencyManifest;
+ symbolDependencyManifest: SymbolDependencyManifest;
+ fileAuditManifest: FileAuditManifest;
+ symbolAuditManifest: SymbolAuditManifest;
+ } | undefined
>(undefined);
- const [detailsPaneOpen, setDetailsPaneOpen] = useState(false);
-
// On mount useEffect
useEffect(() => {
- if (
- !params.file ||
- context.busy ||
- !context.dependencyManifest ||
- !context.auditManifest
- ) {
+ setBusy(true);
+
+ if (!params.file) {
return;
}
- setBusy(true);
const fileDependencyVisualizer = new FileDependencyVisualizer(
containerRef.current as HTMLElement,
params.file,
context.dependencyManifest,
context.auditManifest,
{
- theme: themeContext.theme,
+ theme: theme,
defaultMetric: metric,
onAfterNodeRightClick: (value: {
position: { x: number; y: number };
- data: NapiNodeData;
+ filePath: string;
+ symbolId: string;
}) => {
- if (value.data.customData.fileName !== params.file) {
- // ignore clicks on nodes from other files
- return;
- }
- setContextMenuPosition(value.position);
- setContextMenuOpen(true);
- setContextMenuSymbolId(value.data.customData.symbolName);
+ const fileDependencyManifest =
+ context.dependencyManifest[value.filePath];
+ const symbolDependencyManifest =
+ fileDependencyManifest.symbols[value.symbolId];
+ setContextMenu({
+ position: value.position,
+ fileDependencyManifest,
+ symbolDependencyManifest,
+ });
},
- onAfterNodeDblClick: (data: NapiNodeData) => {
- navigate(
- `/audit/${
- encodeURIComponent(
- data.customData.fileName,
- )
- }/${encodeURIComponent(data.customData.symbolName)}`,
+ onAfterNodeDblClick: (filePath: string, symbolId: string) => {
+ const urlEncodedFileName = encodeURIComponent(
+ filePath,
);
+ const urlEncodedSymbolId = encodeURIComponent(
+ symbolId,
+ );
+ const urlEncodedSymbolName =
+ `/audit/${urlEncodedFileName}/${urlEncodedSymbolId}`;
+
+ navigate(urlEncodedSymbolName);
},
},
);
setFileVisualizer(fileDependencyVisualizer);
+
setBusy(false);
// Cleanup on unmount
@@ -124,20 +137,46 @@ export default function AuditFilePage() {
// Hook to update highlight node in the graph
useEffect(() => {
if (fileVisualizer) {
- if (context.highlightedNodeId) {
- fileVisualizer.highlightNode(context.highlightedNodeId);
+ if (context.highlightedCytoscapeRef) {
+ fileVisualizer.highlightNode(context.highlightedCytoscapeRef);
} else {
fileVisualizer.unhighlightNodes();
}
}
- }, [context.highlightedNodeId]);
+ }, [context.highlightedCytoscapeRef]);
// Hook to update the theme in the graph
useEffect(() => {
if (fileVisualizer) {
- fileVisualizer.updateTheme(themeContext.theme);
+ fileVisualizer.updateTheme(theme);
}
- }, [themeContext.theme]);
+ }, [theme]);
+
+ function handleFilterChange(
+ showExternal: boolean,
+ showVariables: boolean,
+ showFunctions: boolean,
+ showClasses: boolean,
+ showStructs: boolean,
+ showEnums: boolean,
+ showInterfaces: boolean,
+ showRecords: boolean,
+ showDelegates: boolean,
+ ) {
+ if (fileVisualizer) {
+ fileVisualizer.filterNodes(
+ showExternal,
+ showVariables,
+ showFunctions,
+ showClasses,
+ showStructs,
+ showEnums,
+ showInterfaces,
+ showRecords,
+ showDelegates,
+ );
+ }
+ }
return (
@@ -146,18 +185,16 @@ export default function AuditFilePage() {
{/* Otherwise, Cytoscape will not render correctly */}
- {(context.busy || busy || !fileVisualizer) &&
}
-
- {fileVisualizer && (
+
fileVisualizer.layoutGraph(fileVisualizer.cy)}
+ cy={fileVisualizer?.cy}
+ onLayout={() => fileVisualizer?.layoutGraph(fileVisualizer.cy)}
>
fileVisualizer.layoutGraph(fileVisualizer.cy)}
+ currentFileName={params.file}
+ onFilterChange={handleFilterChange}
/>
- )}
-
- {contextMenuSymbolId && (
- {
- setDetailsPaneOpen(open);
- if (open) {
- setDetailsPaneSymbolId(contextMenuSymbolId);
- }
- }}
- setExtractionNodes={context.actions.updateExtractionNodes}
- />
- )}
-
- {detailsPaneSymbolId && (
-
- )}
+
+
+
setContextMenu(undefined)}
+ onOpenDetails={(filePath, symbolId) => {
+ const fileDependencyManifest = context.dependencyManifest[filePath];
+ const symbolDependencyManifest =
+ fileDependencyManifest.symbols[symbolId];
+ const fileAuditManifest = context.auditManifest[filePath];
+ const symbolAuditManifest = fileAuditManifest.symbols[symbolId];
+ setDetailsPane({
+ fileDependencyManifest,
+ symbolDependencyManifest,
+ fileAuditManifest,
+ symbolAuditManifest,
+ });
+ }}
+ />
+
+ setDetailsPane(undefined)}
+ onAddSymbolsForExtraction={(filePath, symbolIds) => {
+ context.onAddSymbolsForExtraction(filePath, symbolIds);
+ }}
+ />
);
}
diff --git a/packages/app/src/pages/audit/file/instance/index.tsx b/packages/app/src/pages/audit/file/instance/index.tsx
index 6e66df8c..288d84e8 100644
--- a/packages/app/src/pages/audit/file/instance/index.tsx
+++ b/packages/app/src/pages/audit/file/instance/index.tsx
@@ -1,256 +1,210 @@
-import { useContext, useEffect, useRef, useState } from "react";
-import cytoscape, { type Core } from "cytoscape";
-import Controls from "../../../../components/Cytoscape/Controls.tsx";
-import GraphDepthExtension from "../../../../components/Cytoscape/ControlExtensions/GraphDepthExtension.tsx";
-import SymbolContextMenu from "../../../../components/Cytoscape/contextMenu/SymbolContextMenu.tsx";
-import { useNavigate, useOutletContext, useParams } from "react-router";
-import fcose from "cytoscape-fcose";
-import { ThemeContext } from "../../../../contexts/ThemeContext.tsx";
+import { useEffect, useRef, useState } from "react";
+import Controls from "../../../../components/controls/Controls.tsx";
+import GraphDepthExtension from "../../../../components/controls/ControlExtensions/GraphDepthExtension.tsx";
+import SymbolContextMenu from "../../../../components/contextMenu/SymbolContextMenu.tsx";
import {
- getCyStyle,
- getNodeLabel,
- layout,
- type NodeElementDefinition,
-} from "../../../../helpers/cytoscape/views/auditFile.ts";
-import { getInstanceCyElements } from "../../../../helpers/cytoscape/views/auditInstance.ts";
-import { CytoscapeSkeleton } from "../../../../components/Cytoscape/Skeleton.tsx";
+ useNavigate,
+ useOutletContext,
+ useParams,
+ useSearchParams,
+} from "react-router";
import type { AuditContext } from "../../base.tsx";
-import SymbolDetailsPane from "../../../../components/SymbolDetailsPane.tsx";
-
-const DEFAULT_DEPENDENCY_DEPTH = 1;
-const DEFAULT_DEPENDENT_DEPTH = 1;
+import SymbolDetailsPane from "../../../../components/detailsPanes/SymbolDetailsPane.tsx";
+import { useTheme } from "../../../../contexts/ThemeProvider.tsx";
+import type {
+ FileAuditManifest,
+ FileDependencyManifest,
+ SymbolAuditManifest,
+ SymbolDependencyManifest,
+} from "@napi/shared";
+import { SymbolDependencyVisualizer } from "../../../../helpers/cytoscape/symbolDependencyVisualizer/index.ts";
export default function AuditInstancePage() {
const navigate = useNavigate();
+
+ const { theme } = useTheme();
+
const params = useParams<{ file: string; instance: string }>();
- const context = useOutletContext();
- const themeContext = useContext(ThemeContext);
- const containerRef = useRef(null);
- const [cyInstance, setCyInstance] = useState(undefined);
- const [dependencyDepth, setDependencyDepth] = useState(1);
- const [dependentDepth, setDependentDepth] = useState(1);
- const [depLoading, setDepLoading] = useState(false);
+ const [searchParams, setSearchParams] = useSearchParams();
- const [contextMenuOpen, setContextMenuOpen] = useState(false);
- const [contextMenuPosition, setContextMenuPosition] = useState({
- x: 0,
- y: 0,
- });
- const [contextMenuSymbolId, setContextMenuSymbolId] = useState<
- string | undefined
- >(undefined);
+ const context = useOutletContext();
- const [detailsPaneSymbolId, setDetailsPaneSymbolId] = useState<
- string | undefined
+ const containerRef = useRef(null);
+ const [busy, setBusy] = useState(true);
+ const [symbolVisualizer, setSymbolVisualizer] = useState<
+ SymbolDependencyVisualizer | undefined
>(undefined);
- const [detailsPaneOpen, setDetailsPaneOpen] = useState(false);
+ const dependencyDepthFromUrl =
+ (searchParams.get("dependencyDepth") || undefined) as number | undefined;
+ const dependentDepthFromUrl =
+ (searchParams.get("dependentDepth") || undefined) as number | undefined;
- // Initialize and cleanup Cytoscape
- useEffect(() => {
- if (context.busy) return;
+ const [dependencyDepth, setDependencyDepth] = useState(
+ dependencyDepthFromUrl || 3,
+ );
+ const [dependentDepth, setDependentDepth] = useState(
+ dependentDepthFromUrl || 0,
+ );
- if (cyInstance) {
- cyInstance.destroy();
- setCyInstance(undefined);
- }
+ function handleDependencyDepthChange(depth: number) {
+ setSearchParams({ dependencyDepth: depth.toString() });
+ setDependencyDepth(depth);
+ // TODO do something with the symbolVisualizer
+ }
- const cy = initializeCytoscape();
- setCyInstance(cy);
+ function handleDependentDepthChange(depth: number) {
+ setSearchParams({ dependentDepth: depth.toString() });
+ setDependentDepth(depth);
+ // TODO do something with the symbolVisualizer
+ }
- return () => {
- cy.destroy();
- setCyInstance(undefined);
- };
- }, [context.busy, params.file]);
+ const [contextMenu, setContextMenu] = useState<
+ {
+ position: { x: number; y: number };
+ fileDependencyManifest: FileDependencyManifest;
+ symbolDependencyManifest: SymbolDependencyManifest;
+ } | undefined
+ >(undefined);
- // Update style when theme changes
- useEffect(() => {
- cyInstance?.style(getCyStyle(themeContext.theme));
- }, [themeContext.changeTheme]);
+ const [detailsPane, setDetailsPane] = useState<
+ {
+ fileDependencyManifest: FileDependencyManifest;
+ symbolDependencyManifest: SymbolDependencyManifest;
+ fileAuditManifest: FileAuditManifest;
+ symbolAuditManifest: SymbolAuditManifest;
+ } | undefined
+ >(undefined); // Hook to update highlight node in the graph
- // Update elements when dependency depth or dependent depth changes
+ // On mount useEffect
useEffect(() => {
- if (cyInstance) {
- // Update loading state
- setDepLoading(true);
+ setBusy(true);
- // This function lets us wait for the next animation frame before updating the elements
- // This allows us to show the button loading state before blocking
- // the main thread with the cytoscape operations
- setTimeout(() => {
- // Remove existing elements
- cyInstance.elements().remove();
-
- // Add new elements
- const elements = getInstanceCyElements(
- context.dependencyManifest,
- params.file as string,
- params.instance as string,
- dependencyDepth,
- dependentDepth,
- );
- cyInstance.add(elements);
-
- cyInstance.one("layoutstop", () => {
- setDepLoading(false);
- });
-
- cyInstance.layout(layout).run();
- }, 50);
+ if (!params.file || !params.instance) {
+ return;
}
- }, [dependencyDepth, dependentDepth]);
- function initializeCytoscape() {
- cytoscape.use(fcose);
- const cy = cytoscape();
- cy.mount(containerRef.current as Element);
-
- // Apply style
- cy.style(getCyStyle(themeContext.theme));
-
- // Add elements
- const elements = getInstanceCyElements(
+ const symbolVisualizer = new SymbolDependencyVisualizer(
+ containerRef.current as HTMLElement,
+ params.file,
+ params.instance,
+ dependencyDepth,
+ dependentDepth,
context.dependencyManifest,
- params.file as string,
- params.instance as string,
- DEFAULT_DEPENDENCY_DEPTH,
- DEFAULT_DEPENDENT_DEPTH,
+ context.auditManifest,
+ {
+ theme: theme,
+ onAfterNodeRightClick: (value: {
+ position: { x: number; y: number };
+ filePath: string;
+ symbolId: string;
+ }) => {
+ const fileDependencyManifest =
+ context.dependencyManifest[value.filePath];
+ const symbolDependencyManifest =
+ fileDependencyManifest.symbols[value.symbolId];
+ setContextMenu({
+ position: value.position,
+ fileDependencyManifest,
+ symbolDependencyManifest,
+ });
+ },
+ onAfterNodeDblClick: (filePath: string, symbolId: string) => {
+ const urlEncodedFileName = encodeURIComponent(
+ filePath,
+ );
+ const urlEncodedSymbolId = encodeURIComponent(
+ symbolId,
+ );
+ const urlEncodedSymbolName =
+ `/audit/${urlEncodedFileName}/${urlEncodedSymbolId}`;
+
+ navigate(urlEncodedSymbolName);
+ },
+ },
);
- cy.add(elements);
- // Apply layout
- cy.layout(layout).run();
+ setSymbolVisualizer(symbolVisualizer);
- // Add event listeners
- addEventListeners(cy);
+ setBusy(false);
- return cy;
- }
-
- function addEventListeners(cy: Core) {
- // Toggle node expansion on tap
- cy.on("tap", "node", (evt) => {
- const node = evt.target;
- const data = node.data() as NodeElementDefinition["data"];
-
- const isExpanded = !data.isExpanded;
-
- const label = getNodeLabel({
- isExpanded,
- isExternal: data.isExternal,
- type: data.type,
- fileName: data.customData.fileName,
- instance: data.customData.instance,
- errorMessages: data.customData.errorMessages,
- warningMessages: data.customData.warningMessages,
- });
-
- node.data({ label, isExpanded });
- });
-
- // Navigate to file/instance on double tap
- cy.on("dbltap", "node", (evt) => {
- const node = evt.target;
- const data = node.data() as NodeElementDefinition["data"];
-
- if (data.isExternal) return;
-
- const urlEncodedFileName = encodeURIComponent(data.customData.fileName);
- let url = `/audit/${urlEncodedFileName}`;
-
- if (data.type === "instance" && data.customData.instance) {
- const urlEncodedInstance = encodeURIComponent(
- data.customData.instance.name,
- );
- url += `/${urlEncodedInstance}`;
- }
-
- navigate(url);
- });
-
- // Show context menu on right click
- cy.on("cxttap", "node", (evt) => {
- const node = evt.target;
- const data = node.data() as NodeElementDefinition["data"];
+ // Cleanup on unmount
+ return () => {
+ symbolVisualizer?.cy.destroy();
+ setSymbolVisualizer(undefined);
+ };
+ }, [
+ context.dependencyManifest,
+ context.auditManifest,
+ params.file,
+ params.instance,
+ dependencyDepth,
+ dependentDepth,
+ ]);
- if (data.customData.fileName !== params.file) {
- // ignore clicks on nodes from other files
- return;
+ useEffect(() => {
+ if (symbolVisualizer) {
+ if (context.highlightedCytoscapeRef) {
+ symbolVisualizer.highlightNode(context.highlightedCytoscapeRef);
+ } else {
+ symbolVisualizer.unhighlightNodes();
}
-
- setContextMenuPosition(node.renderedPosition());
- setContextMenuOpen(true);
- setContextMenuSymbolId(data.customData.instance?.name);
- });
-
- return cy;
- }
-
- function handleLayout() {
- cyInstance?.makeLayout(layout).run();
- }
+ }
+ }, [context.highlightedCytoscapeRef]);
return (
-
-
- {context.busy || !cyInstance
- ?
- : (
-
- {/* TODO: Fix data shape for instane-level view and then uncomment */}
- {
- /* */
- }
-
-
- )}
-
- {contextMenuSymbolId && (
-
{
- setDetailsPaneOpen(open);
- if (open) {
- setDetailsPaneSymbolId(contextMenuSymbolId);
- }
- }}
- setExtractionNodes={context.actions.updateExtractionNodes}
- />
- )}
-
- {detailsPaneSymbolId && (
-
- )}
+ {/* This is the container for Cytoscape */}
+ {/* It is important to set the width and height to 100% */}
+ {/* Otherwise, Cytoscape will not render correctly */}
+
+
+
+ symbolVisualizer?.layoutGraph(symbolVisualizer.cy)}
+ >
+
+
+
+
+ setContextMenu(undefined)}
+ onOpenDetails={(filePath, symbolId) => {
+ const fileDependencyManifest = context.dependencyManifest[filePath];
+ const symbolDependencyManifest =
+ fileDependencyManifest.symbols[symbolId];
+ const fileAuditManifest = context.auditManifest[filePath];
+ const symbolAuditManifest = fileAuditManifest.symbols[symbolId];
+ setDetailsPane({
+ fileDependencyManifest,
+ symbolDependencyManifest,
+ fileAuditManifest,
+ symbolAuditManifest,
+ });
+ }}
+ />
+
+ setDetailsPane(undefined)}
+ onAddSymbolsForExtraction={(filePath, symbolIds) => {
+ context.onAddSymbolsForExtraction(filePath, symbolIds);
+ }}
+ />
);
}
diff --git a/packages/app/src/pages/audit/index.tsx b/packages/app/src/pages/audit/index.tsx
index 8c3d9128..384fb560 100644
--- a/packages/app/src/pages/audit/index.tsx
+++ b/packages/app/src/pages/audit/index.tsx
@@ -1,23 +1,26 @@
-import { useContext, useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { useNavigate, useOutletContext, useSearchParams } from "react-router";
-import Controls from "../../components/Cytoscape/Controls.tsx";
-import MetricsExtension from "../../components/Cytoscape/ControlExtensions/MetricsExtension.tsx";
-import { CytoscapeSkeleton } from "../../components/Cytoscape/Skeleton.tsx";
-import FileContextMenu from "../../components/Cytoscape/contextMenu/FileContextMenu.tsx";
-import { ThemeContext } from "../../contexts/ThemeContext.tsx";
+import Controls from "../../components/controls/Controls.tsx";
+import MetricsExtension from "../../components/controls/ControlExtensions/MetricsExtension.tsx";
+import FileContextMenu from "../../components/contextMenu/FileContextMenu.tsx";
import type { AuditContext } from "./base.tsx";
-import FileDetailsPane from "../../components/FileDetailsPane.tsx";
+import FileDetailsPane from "../../components/detailsPanes/FileDetailsPane.tsx";
import { ProjectDependencyVisualizer } from "../../helpers/cytoscape/projectDependencyVisualizer/index.ts";
-import type { NapiNodeData } from "../../helpers/cytoscape/projectDependencyVisualizer/types.ts";
-import type { Metric } from "@napi/shared";
+import type {
+ FileAuditManifest,
+ FileDependencyManifest,
+ Metric,
+} from "@napi/shared";
+import { useTheme } from "../../contexts/ThemeProvider.tsx";
export default function AuditPage() {
const navigate = useNavigate();
+ const { theme } = useTheme();
+
const [searchParams, setSearchParams] = useSearchParams();
- const context = useOutletContext();
- const themeContext = useContext(ThemeContext);
+ const context = useOutletContext();
const containerRef = useRef(null);
const [busy, setBusy] = useState(true);
@@ -40,21 +43,20 @@ export default function AuditPage() {
setMetric(metric);
}
- const [contextMenuOpen, setContextMenuOpen] = useState(false);
- const [contextMenuPosition, setContextMenuPosition] = useState({
- x: 0,
- y: 0,
- });
- const [contextMenuNodeId, setContextMenuNodeId] = useState<
- string | undefined
+ const [contextMenu, setContextMenu] = useState<
+ {
+ position: { x: number; y: number };
+ fileDependencyManifest: FileDependencyManifest;
+ } | undefined
>(undefined);
- const [detailsPaneNodeId, setDetailsPaneNodeId] = useState<
- string | undefined
+ const [detailsPane, setDetailsPane] = useState<
+ {
+ fileDependencyManifest: FileDependencyManifest;
+ fileAuditManifest: FileAuditManifest;
+ } | undefined
>(undefined);
- const [detailsPaneOpen, setDetailsPaneOpen] = useState(false);
-
// On mount useEffect
useEffect(() => {
setBusy(true);
@@ -63,19 +65,20 @@ export default function AuditPage() {
context.dependencyManifest,
context.auditManifest,
{
- theme: themeContext.theme,
+ theme,
defaultMetric: metric,
onAfterNodeRightClick: (value: {
position: { x: number; y: number };
- id: string;
+ filePath: string;
}) => {
- setContextMenuPosition(value.position);
- setContextMenuOpen(true);
- setContextMenuNodeId(value.id);
+ setContextMenu({
+ position: value.position,
+ fileDependencyManifest: context.dependencyManifest[value.filePath],
+ });
},
- onAfterNodeDblClick: (data: NapiNodeData) => {
+ onAfterNodeDblClick: (filePath: string) => {
const urlEncodedFileName = encodeURIComponent(
- data.customData.fileName,
+ filePath,
);
navigate(`/audit/${urlEncodedFileName}`);
},
@@ -103,20 +106,20 @@ export default function AuditPage() {
// Hook to update highlight node in the graph
useEffect(() => {
if (projectVisualizer) {
- if (context.highlightedNodeId) {
- projectVisualizer.highlightNode(context.highlightedNodeId);
+ if (context.highlightedCytoscapeRef) {
+ projectVisualizer.highlightNode(context.highlightedCytoscapeRef);
} else {
projectVisualizer.unhighlightNodes();
}
}
- }, [context.highlightedNodeId]);
+ }, [context.highlightedCytoscapeRef]);
// Hook to update the theme in the graph
useEffect(() => {
if (projectVisualizer) {
- projectVisualizer.updateTheme(themeContext.theme);
+ projectVisualizer.updateTheme(theme);
}
- }, [themeContext.theme]);
+ }, [theme]);
return (
@@ -125,13 +128,11 @@ export default function AuditPage() {
{/* Otherwise, Cytoscape will not render correctly */}
- {(context.busy || busy || !projectVisualizer) &&
}
-
- {projectVisualizer && (
+
projectVisualizer.layoutGraph(projectVisualizer.cy)}
+ cy={projectVisualizer?.cy}
+ onLayout={() => projectVisualizer?.layoutGraph(projectVisualizer.cy)}
>
- )}
-
- {contextMenuNodeId && (
- {
- setDetailsPaneOpen(open);
- if (open) {
- setDetailsPaneNodeId(contextMenuNodeId);
- }
- }}
- setExtractionNodes={context.actions.updateExtractionNodes}
- />
- )}
-
- {detailsPaneNodeId && (
-
- )}
+
+
+
setContextMenu(undefined)}
+ onOpenDetails={(filePath) => {
+ setDetailsPane({
+ fileDependencyManifest: context.dependencyManifest[filePath],
+ fileAuditManifest: context.auditManifest[filePath],
+ });
+ }}
+ />
+
+ setDetailsPane(undefined)}
+ onAddSymbolsForExtraction={(filePath, symbolIds) => {
+ context.onAddSymbolsForExtraction(filePath, symbolIds);
+ }}
+ />
);
}
diff --git a/packages/app/src/service/api/index.ts b/packages/app/src/service/api/index.ts
index 09b09235..dc015293 100644
--- a/packages/app/src/service/api/index.ts
+++ b/packages/app/src/service/api/index.ts
@@ -1,7 +1,7 @@
import type {
AuditManifest,
DependencyManifest,
- ExtractionNode,
+ SymbolsToExtract,
} from "@napi/shared";
export async function getDependencyManifest() {
@@ -33,14 +33,14 @@ export async function getAuditManifest() {
}
export async function runExtraction(
- extractionNodes: ExtractionNode[],
+ symbolsToExtract: SymbolsToExtract,
): Promise<{ success: boolean }> {
const response = await fetch("/api/extractSymbol/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
- body: JSON.stringify(extractionNodes),
+ body: JSON.stringify(symbolsToExtract),
});
if (!response.ok || response.status !== 200) {
diff --git a/packages/app/src/styles/global.css b/packages/app/src/styles/global.css
new file mode 100644
index 00000000..a28618e3
--- /dev/null
+++ b/packages/app/src/styles/global.css
@@ -0,0 +1,148 @@
+/* IMPORTANT: This file is used by shadcn/ui to generate the theme. Do not modify this file directly. */
+
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ /* was same as destructive here before. I changed it. */
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.85 0.3 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+
+ :root {
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+
+ .dark {
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
diff --git a/packages/app/src/styles/index.css b/packages/app/src/styles/index.css
new file mode 100644
index 00000000..e529f24e
--- /dev/null
+++ b/packages/app/src/styles/index.css
@@ -0,0 +1 @@
+@import "./global.css";
diff --git a/packages/app/tailwind.config.js b/packages/app/tailwind.config.js
deleted file mode 100644
index 0519ecba..00000000
--- a/packages/app/tailwind.config.js
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts
index 66ecef25..72af33f4 100644
--- a/packages/app/vite.config.ts
+++ b/packages/app/vite.config.ts
@@ -3,9 +3,6 @@ import react from "@vitejs/plugin-react";
import deno from "@deno/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
-import "react";
-import "react-dom";
-
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
diff --git a/packages/cli/deno.json b/packages/cli/deno.json
index 9ae3ebee..c0967c1d 100644
--- a/packages/cli/deno.json
+++ b/packages/cli/deno.json
@@ -3,8 +3,8 @@
"version": "1.0.7",
"exports": "./src/index.ts",
"imports": {
- "@inquirer/prompts": "npm:@inquirer/prompts@^7.5.0",
"@napi/shared": "../shared/src/index.ts",
+ "@inquirer/prompts": "npm:@inquirer/prompts@^7.5.0",
"@types/express": "npm:@types/express@^5.0.1",
"@types/yargs": "npm:@types/yargs@^17.0.33",
"express": "npm:express@^5.1.0",
@@ -14,16 +14,15 @@
"tree-sitter-c-sharp": "npm:tree-sitter-c-sharp@^0.23.1",
"tree-sitter-python": "npm:tree-sitter-python@^0.23.6",
"uuid": "npm:uuid@^11.1.0",
- "vitest": "npm:vitest@^3.1.3",
"yargs": "npm:yargs@^17.7.2",
"zod": "npm:zod@^3.24.4"
},
"tasks": {
"dev": "NODE_ENV=development deno run --allow-all --watch src/index.ts",
"compile": "deno compile --allow-all --include=../app/dist --output=dist/napi src/index.ts",
- "compile:linux": "deno compile --allow-all --include=../app/dist --output=dist/napi.linux --target=x86_64-unknown-linux-gnu src/index.ts",
- "compile:macos": "deno compile --allow-all --include=../app/dist --output=dist/napi.macos --target=x86_64-apple-darwin src/index.ts",
- "compile:windows": "deno compile --allow-all --include=../app/dist --output=dist/napi.exe --target=x86_64-pc-windows-msvc src/index.ts",
+ "compile-linux": "deno compile --allow-all --include=../app/dist --output=dist/napi.linux --target=x86_64-unknown-linux-gnu src/index.ts",
+ "compile-macos": "deno compile --allow-all --include=../app/dist --output=dist/napi.macos --target=x86_64-apple-darwin src/index.ts",
+ "compile-windows": "deno compile --allow-all --include=../app/dist --output=dist/napi.exe --target=x86_64-pc-windows-msvc src/index.ts",
"test": "deno test --allow-all"
}
}
diff --git a/packages/cli/src/cli/handlers/init/index.ts b/packages/cli/src/cli/handlers/init/index.ts
index b553962c..77e10ab6 100644
--- a/packages/cli/src/cli/handlers/init/index.ts
+++ b/packages/cli/src/cli/handlers/init/index.ts
@@ -74,6 +74,3 @@ export default {
builder: {},
handler,
};
-
-// Export for testing
-export { generateConfig };
diff --git a/packages/cli/src/cli/helpers/server.ts b/packages/cli/src/cli/helpers/server.ts
index d7efa99d..4ca3d8d1 100644
--- a/packages/cli/src/cli/helpers/server.ts
+++ b/packages/cli/src/cli/helpers/server.ts
@@ -52,10 +52,9 @@ export async function runServer(
`Running in development mode, proxying all traffic to the app to ${targetServiceUrl}`,
);
app.use(createProxyMiddleware({
- target: targetServiceUrl,
- changeOrigin: true,
- }),
- );
+ target: targetServiceUrl,
+ changeOrigin: true,
+ }));
} else {
app.use(express.static(app_dist));
}
diff --git a/packages/cli/src/config/globalConfig.ts b/packages/cli/src/config/globalConfig.ts
index 75f1b475..cebef1ec 100644
--- a/packages/cli/src/config/globalConfig.ts
+++ b/packages/cli/src/config/globalConfig.ts
@@ -5,7 +5,7 @@ import { v4 } from "uuid";
import { z } from "zod";
import process from "node:process";
-export const globalConfigSchema = z.object({
+const globalConfigSchema = z.object({
userId: z.string(),
});
@@ -78,8 +78,3 @@ export function getOrCreateGlobalConfig() {
return config;
}
-
-export function updateConfig(newConfig: z.infer) {
- const configPath = getConfigPath();
- fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
-}
diff --git a/packages/cli/src/config/localConfig.ts b/packages/cli/src/config/localConfig.ts
index dc3925be..63860762 100644
--- a/packages/cli/src/config/localConfig.ts
+++ b/packages/cli/src/config/localConfig.ts
@@ -58,7 +58,7 @@ export const localConfigSchema = z.object({
.optional(),
});
-export const napiConfigFileName = ".napirc";
+const napiConfigFileName = ".napirc";
export function getConfigFromWorkDir(workdir: string) {
const napircPath = path.join(workdir, napiConfigFileName);
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index a7bcb2ca..6c5aad34 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -11,5 +11,13 @@ if (process.env.NODE_ENV !== "development") {
initCli();
-export const entryPointDirname = path.resolve(import.meta.dirname as string);
-export const app_dist = path.join(entryPointDirname, "..", "..", "app", "dist");
+const entryPointDirname: string = path.resolve(
+ import.meta.dirname as string,
+);
+export const app_dist: string = path.join(
+ entryPointDirname,
+ "..",
+ "..",
+ "app",
+ "dist",
+);
diff --git a/packages/cli/src/languagePlugins/csharp/testFiles/index.ts b/packages/cli/src/languagePlugins/csharp/testFiles/index.ts
index 3710a2da..0527e4c2 100644
--- a/packages/cli/src/languagePlugins/csharp/testFiles/index.ts
+++ b/packages/cli/src/languagePlugins/csharp/testFiles/index.ts
@@ -3,7 +3,10 @@ import path from "node:path";
import type Parser from "tree-sitter";
import { csharpParser } from "../../../helpers/treeSitter/parsers.ts";
-export const csharpFilesFolder = path.join(__dirname, "csharpFiles");
+export const csharpFilesFolder = path.join(
+ import.meta.dirname as string,
+ "csharpFiles",
+);
const csharpFilesMap = new Map<
string,
diff --git a/packages/shared/src/extraction/types.ts b/packages/shared/src/extraction/types.ts
index 6de77da2..e25a0267 100644
--- a/packages/shared/src/extraction/types.ts
+++ b/packages/shared/src/extraction/types.ts
@@ -1,4 +1,4 @@
-export interface ExtractionNode {
+export type SymbolsToExtract = {
filePath: string;
symbols: string[];
-}
+}[];
diff --git a/packages/shared/src/manifest/dependencyManifest/types.ts b/packages/shared/src/manifest/dependencyManifest/types.ts
index da94887b..8501a69d 100644
--- a/packages/shared/src/manifest/dependencyManifest/types.ts
+++ b/packages/shared/src/manifest/dependencyManifest/types.ts
@@ -7,6 +7,15 @@ export const structSymbolType = "struct";
/** Identifies the "enum" instance type. */
export const enumSymbolType = "enum";
+/** Identifies the "interface" instance type. */
+export const interfaceSymbolType = "interface";
+
+/** Identifies the "record" instance type. */
+export const recordSymbolType = "record";
+
+/** Identifies the "delegate" instance type. */
+export const delegateSymbolType = "delegate";
+
/** Identifies the "function" instance type. */
export const functionSymbolType = "function";
@@ -18,13 +27,19 @@ export const variableSymbolType = "variable";
* - struct
* - enum
* - function
- * - variable */
+ * - variable
+ * - interface
+ * - record
+ * - delegate */
export type SymbolType =
| typeof classSymbolType
| typeof functionSymbolType
| typeof variableSymbolType
| typeof structSymbolType
- | typeof enumSymbolType;
+ | typeof enumSymbolType
+ | typeof interfaceSymbolType
+ | typeof recordSymbolType
+ | typeof delegateSymbolType;
export const metricLinesCount = "linesCount";
export const metricCodeLineCount = "codeLineCount";
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..6d6d1784
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,11 @@
+import { resolve } from "node:path";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ // TODO this is a workarround, should remove vitest and use Deno.test() instead
+ "@napi/shared": resolve("./packages/shared/src/index.ts"),
+ },
+ },
+});