diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ea85931 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,44 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: ["refactor-antigravity"] # O el nombre de la rama donde estés trabajando + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Run Tests + run: npm test -- --run + + - name: Build + run: npm run build + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './dist' # Vite guarda aquí el resultado final + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index b21eb2e..449ddfb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# VisualCryptoLab -A visual laboratory to learn cryptography +# VisualCryptoLab 🧪 + +An interactive tool to visualize and experiment with cryptographic tools + +## 🚀 Live Deployment +This project uses **CI/CD** via GitHub Actions. Every `push` to the branch automatically builds and deploys the latest version to GitHub Pages. + +🔗 **Live Demo:** [https://visualcryptolab.github.io/vcryptolab/](https://visualcryptolab.github.io/vcryptolab/) + +## 🛠️ Local Development + +To run this project locally on your machine, follow these steps: + +1. **Clone the repository:** + ```bash + git clone [https://github.com/visualcryptolab/vcryptolab.git](https://github.com/visualcryptolab/vcryptolab.git) + cd vcryptolab \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0eb6414..ef8641b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,8 @@ }, "devDependencies": { "@eslint/js": "^9.36.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.1", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.4", @@ -27,11 +29,27 @@ "eslint-plugin-react-refresh": "^0.4.22", "gh-pages": "^6.3.0", "globals": "^16.4.0", + "jsdom": "^27.4.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.18", - "vite": "^7.1.7" + "vite": "^7.1.7", + "vitest": "^4.0.17" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -45,6 +63,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -279,6 +352,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -327,6 +410,141 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", + "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -926,6 +1144,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.8.0.tgz", + "integrity": "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@exodus/crypto": "^1.0.0-rc.4" + }, + "peerDependenciesMeta": { + "@exodus/crypto": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1512,6 +1748,97 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1557,6 +1884,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -1810,6 +2148,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1877,6 +2222,117 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", + "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.17", + "@vitest/utils": "4.0.17", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", + "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.17", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", + "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", + "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", + "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.17", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz", + "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz", + "integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.17", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1900,6 +2356,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1994,9 +2460,19 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", @@ -2004,6 +2480,16 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2075,6 +2561,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2187,6 +2683,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2332,6 +2838,27 @@ "utrie": "^1.0.2" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2345,6 +2872,32 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2457,6 +3010,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2475,6 +3042,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2482,6 +3056,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2509,6 +3093,14 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2537,6 +3129,26 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -2770,6 +3382,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2780,6 +3402,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3207,6 +3839,19 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -3220,6 +3865,34 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3257,6 +3930,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3329,6 +4012,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3381,6 +4071,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3545,6 +4275,27 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3561,6 +4312,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3598,6 +4356,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3712,6 +4480,17 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3792,6 +4571,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3853,6 +4645,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4128,6 +4927,55 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4293,6 +5141,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4401,6 +5273,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4440,6 +5325,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4473,6 +5365,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4577,6 +5483,19 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4662,6 +5581,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", @@ -4732,6 +5658,23 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4749,6 +5692,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4762,6 +5735,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -4956,6 +5955,131 @@ } } }, + "node_modules/vitest": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz", + "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.17", + "@vitest/mocker": "4.0.17", + "@vitest/pretty-format": "4.0.17", + "@vitest/runner": "4.0.17", + "@vitest/snapshot": "4.0.17", + "@vitest/spy": "4.0.17", + "@vitest/utils": "4.0.17", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.17", + "@vitest/browser-preview": "4.0.17", + "@vitest/browser-webdriverio": "4.0.17", + "@vitest/ui": "4.0.17", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4972,6 +6096,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5077,6 +6218,45 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 92c98f4..8af3add 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", + "test": "vitest run", "preview": "vite preview", "predeploy": "npm run build", "deploy": "gh-pages -d dist" @@ -23,6 +24,8 @@ }, "devDependencies": { "@eslint/js": "^9.36.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.1", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.4", @@ -32,8 +35,10 @@ "eslint-plugin-react-refresh": "^0.4.22", "gh-pages": "^6.3.0", "globals": "^16.4.0", + "jsdom": "^27.4.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.18", - "vite": "^7.1.7" + "vite": "^7.1.7", + "vitest": "^4.0.17" } -} +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index cfde89b..bc72a32 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,1653 +1,151 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; -import { LayoutGrid, Cpu, Key, Zap, Settings, Lock, Unlock, Hash, Clipboard, X, ArrowLeft, ArrowRight, Download, Upload, Camera, ChevronDown, ChevronUp, CheckCheck, Fingerprint, Signature, ZoomIn, ZoomOut, Info, Split } from 'lucide-react'; +import { + NODE_DEFINITIONS, + PROJECT_SCHEMA_VERSION, + INITIAL_NODES, + INITIAL_CONNECTIONS, + NODE_DIMENSIONS +} from './constants/appConstants'; +import { globalStyles } from './styles/globalStyles'; +import { migrateProjectData, downloadFile } from './utils/projectUtils'; +import { getLinePath } from './utils/canvasUtils'; +import { recalculateGraph } from './utils/graphUtils'; +import { getOutputFormat } from './utils/cryptoUtils'; +import Toolbar from './components/Toolbar'; +import DraggableBox from './components/DraggableBox'; +import StatusNotification from './components/ui/StatusNotification'; -// --- Global Configuration --- -const PROJECT_SCHEMA_VERSION = '1.2'; - -// --- CSS Styles --- -const globalStyles = ` - @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); - - html, body, #root { - height: 100%; - margin: 0; - padding: 0; - font-family: 'Inter', sans-serif; - } - - @keyframes animate-pulse-slow { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } - } - .animate-pulse-slow { - animation: animate-pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - } - - .connection-line-visible { - stroke: #059669; /* Emerald 600 */ - fill: none; - pointer-events: none; - } - .connection-hitbox { - stroke: transparent; - fill: none; - cursor: pointer; - pointer-events: stroke; - } - .connection-hitbox:hover { - stroke: rgba(248, 113, 129, 0.5); - } - - /* Custom scrollbar for panels */ - ::-webkit-scrollbar { - width: 6px; - height: 6px; - } - ::-webkit-scrollbar-track { - background: #f1f1f1; - } - ::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 3px; - } - ::-webkit-scrollbar-thumb:hover { - background: #a8a8a8; - } -`; - -// --- Custom Icons --- - -function XORIcon(props) { - return ( - - - - - - ); -} - -function BitShiftIcon(props) { - return ( - - - - - - ); -} - -// --- Constants & Maps --- - -const BORDER_CLASSES = { - blue: 'border-blue-600', red: 'border-red-600', orange: 'border-orange-600', cyan: 'border-cyan-600', pink: 'border-pink-500', - teal: 'border-teal-600', gray: 'border-gray-600', lime: 'border-lime-600', indigo: 'border-indigo-600', - purple: 'border-purple-600', maroon: 'border-red-800', rose: 'border-pink-700', amber: 'border-amber-500', - yellow: 'border-yellow-400', fuchsia: 'border-fuchsia-600', green: 'border-green-600', -}; - -const HOVER_BORDER_CLASSES = { - blue: 'hover:border-blue-500', red: 'hover:border-red-500', orange: 'hover:border-orange-500', cyan: 'hover:border-cyan-500', pink: 'hover:border-pink-500', - teal: 'hover:border-teal-500', gray: 'hover:border-gray-500', lime: 'hover:border-lime-500', indigo: 'hover:border-indigo-500', - purple: 'hover:border-purple-500', maroon: 'hover:border-red-700', rose: 'hover:border-pink-600', amber: 'hover:border-amber-400', - yellow: 'hover:border-yellow-300', fuchsia: 'hover:border-fuchsia-500', green: 'hover:border-green-500', -}; - -const TEXT_ICON_CLASSES = { - blue: 'text-blue-600', red: 'text-red-600', orange: 'text-orange-600', cyan: 'text-cyan-600', pink: 'text-pink-500', - teal: 'text-teal-600', gray: 'text-gray-600', lime: 'text-lime-600', indigo: 'text-indigo-600', - purple: 'text-purple-600', maroon: 'text-red-800', rose: 'text-pink-700', amber: 'text-amber-500', - yellow: 'text-yellow-400', fuchsia: 'text-fuchsia-600', green: 'text-green-600', -}; - -const HOVER_BORDER_TOOLBAR_CLASSES = { - blue: 'hover:border-blue-400', red: 'hover:border-red-400', orange: 'hover:border-orange-400', cyan: 'hover:border-cyan-400', - pink: 'hover:border-pink-400', teal: 'hover:border-teal-400', gray: 'hover:border-gray-400', lime: 'hover:border-lime-400', - indigo: 'hover:border-indigo-400', purple: 'hover:border-purple-400', maroon: 'hover:border-red-600', rose: 'hover:border-pink-600', - amber: 'hover:border-amber-400', yellow: 'hover:border-yellow-300', fuchsia: 'hover:border-fuchsia-400', green: 'hover:border-green-400', -}; - -const PORT_SIZE = 4; -const INPUT_PORT_COLOR = 'bg-stone-500'; -const OPTIONAL_PORT_COLOR = 'bg-gray-400'; -const OUTPUT_PORT_COLOR = 'bg-emerald-500'; -const PUBLIC_KEY_COLOR = 'bg-lime-500'; -const PRIVATE_KEY_COLOR = 'bg-red-800'; -const SIGNATURE_COLOR = 'bg-fuchsia-500'; - -const HASH_ALGORITHMS = ['SHA-256', 'SHA-512']; -const SYM_ALGORITHMS = ['AES-GCM']; -const ASYM_ALGORITHMS = ['RSA-OAEP']; -const ALL_FORMATS = ['Text (UTF-8)', 'Base64', 'Hexadecimal', 'Binary', 'Decimal']; - -const NODE_DEFINITIONS = { - DATA_INPUT: { label: 'Data Input', color: 'blue', icon: LayoutGrid, inputPorts: [], outputPorts: [{ name: 'Data Output', type: 'data', keyField: 'dataOutput' }] }, - OUTPUT_VIEWER: { label: 'Output Viewer', color: 'red', icon: Zap, inputPorts: [{ name: 'Data Input', type: 'data', mandatory: true, id: 'data' }], outputPorts: [{ name: 'Viewer Data Output', type: 'data', keyField: 'dataOutput' }] }, - HASH_FN: { label: 'Hash Function', color: 'gray', icon: Hash, inputPorts: [{ name: 'Data Input', type: 'data', mandatory: true, id: 'data' }], outputPorts: [{ name: 'Hash Output', type: 'data', keyField: 'dataOutput' }] }, - XOR_OP: { label: 'XOR Operation', color: 'lime', icon: XORIcon, inputPorts: [{ name: 'Input A', type: 'data', mandatory: true, id: 'dataA' }, { name: 'Input B', type: 'data', mandatory: true, id: 'dataB' }], outputPorts: [{ name: 'Result', type: 'data', keyField: 'dataOutput' }] }, - SHIFT_OP: { label: 'Bit Shift', color: 'indigo', icon: BitShiftIcon, inputPorts: [{ name: 'Data Input', type: 'data', mandatory: true, id: 'data' }], outputPorts: [{ name: 'Result', type: 'data', keyField: 'dataOutput' }] }, - DATA_SPLIT: { label: 'Data Split', color: 'green', icon: Split, inputPorts: [{ name: 'Data Input', type: 'data', mandatory: true, id: 'data' }], outputPorts: [{ name: 'Chunk 1', type: 'data', keyField: 'chunk1' }, { name: 'Chunk 2', type: 'data', keyField: 'chunk2' }] }, - DATA_CONCAT: { label: 'Data Concatenate', color: 'teal', icon: Cpu, inputPorts: [{ name: 'Data A', type: 'data', mandatory: true, id: 'dataA' }, { name: 'Data B', type: 'data', mandatory: true, id: 'dataB' }], outputPorts: [{ name: 'Concatenated Output', type: 'data', keyField: 'dataOutput' }] }, - CAESAR_CIPHER: { label: 'Caesar Cipher', color: 'amber', icon: Lock, inputPorts: [{ name: 'Plaintext', type: 'data', mandatory: true, id: 'plaintext' }], outputPorts: [{ name: 'Ciphertext', type: 'data', keyField: 'dataOutput' }] }, - VIGENERE_CIPHER: { label: 'Vigenère Cipher', color: 'yellow', icon: Lock, inputPorts: [{ name: 'Plaintext/Ciphertext', type: 'data', mandatory: true, id: 'data' }], outputPorts: [{ name: 'Result', type: 'data', keyField: 'dataOutput' }] }, - KEY_GEN: { label: 'Sym Key Generator', color: 'orange', icon: Key, inputPorts: [], outputPorts: [{ name: 'Key Output (AES)', type: 'key', keyField: 'dataOutput' }] }, - SIMPLE_RSA_KEY_GEN: { label: 'Simple RSA PrivKey Gen', color: 'purple', icon: Key, inputPorts: [], outputPorts: [{ name: 'Private Key (d)', type: 'private', keyField: 'dataOutputPrivate' }] }, - SIMPLE_RSA_PUBKEY_GEN: { label: 'Simple RSA PubKey Gen', color: 'lime', icon: Unlock, inputPorts: [{ name: 'Private Key Source', type: 'private', mandatory: false, id: 'keySource' }], outputPorts: [{ name: 'Public Key (n, e)', type: 'public', keyField: 'dataOutputPublic' }] }, - SIMPLE_RSA_ENC: { label: 'Simple RSA Encrypt', color: 'maroon', icon: Lock, inputPorts: [{ name: 'Message (m)', type: 'data', mandatory: true, id: 'message' }, { name: 'Public Key (n, e)', type: 'public', mandatory: true, id: 'publicKey' }], outputPorts: [{ name: 'Ciphertext (c)', type: 'data', keyField: 'dataOutput' }] }, - SIMPLE_RSA_DEC: { label: 'Simple RSA Decrypt', color: 'rose', icon: Unlock, inputPorts: [{ name: 'Ciphertext (c)', type: 'data', mandatory: true, id: 'cipher' }, { name: 'Private Key (d)', type: 'private', mandatory: true, id: 'privateKey' }], outputPorts: [{ name: 'Plaintext (m)', type: 'data', keyField: 'dataOutput' }] }, - SIMPLE_RSA_SIGN: { label: 'Simple RSA Sign', color: 'fuchsia', icon: Signature, inputPorts: [{ name: 'Message (m)', type: 'data', mandatory: true, id: 'message' }, { name: 'Private Key (d)', type: 'private', mandatory: true, id: 'privateKey' }], outputPorts: [{ name: 'Signature (s)', type: 'data', keyField: 'dataOutput' }] }, - SIMPLE_RSA_VERIFY: { label: 'Simple RSA Verify', color: 'fuchsia', icon: CheckCheck, inputPorts: [{ name: 'Message (m)', type: 'data', mandatory: true, id: 'message' }, { name: 'Signature (s)', type: 'data', mandatory: true, id: 'signature' }, { name: 'Public Key (n, e)', type: 'public', mandatory: true, id: 'publicKey' }], outputPorts: [{ name: 'Verification Result', type: 'data', keyField: 'dataOutput' }] }, - SYM_ENC: { label: 'Sym Encrypt', color: 'red', icon: Lock, inputPorts: [{ name: 'Data Input', type: 'data', mandatory: true, id: 'data' }, { name: 'Key Input', type: 'key', mandatory: true, id: 'key' }], outputPorts: [{ name: 'Ciphertext', type: 'data', keyField: 'dataOutput' }] }, - SYM_DEC: { label: 'Sym Decrypt', color: 'pink', icon: Unlock, inputPorts: [{ name: 'Cipher Input', type: 'data', mandatory: true, id: 'cipher' }, { name: 'Key Input', type: 'key', mandatory: true, id: 'key' }], outputPorts: [{ name: 'Plaintext', type: 'data', keyField: 'dataOutput' }] }, - ASYM_ENC: { label: 'Asym Encrypt', color: 'cyan', icon: Lock, inputPorts: [{ name: 'Data Input', type: 'data', mandatory: true, id: 'data' }, { name: 'Public Key', type: 'public', mandatory: true, id: 'publicKey' }], outputPorts: [{ name: 'Ciphertext', type: 'data', keyField: 'dataOutput' }] }, - ASYM_DEC: { label: 'Asym Decrypt', color: 'teal', icon: Unlock, inputPorts: [{ name: 'Cipher Input', type: 'data', mandatory: true, id: 'cipher' }, { name: 'Private Key', type: 'private', mandatory: true, id: 'privateKey' }], outputPorts: [{ name: 'Plaintext', type: 'data', keyField: 'dataOutput' }] }, -}; - -const ORDERED_NODE_GROUPS = [ - { name: 'CORE TOOLS', types: ['DATA_INPUT', 'OUTPUT_VIEWER', 'HASH_FN', 'XOR_OP', 'SHIFT_OP', 'DATA_SPLIT', 'DATA_CONCAT'] }, - { name: 'CLASSIC CIPHERS', types: ['CAESAR_CIPHER', 'VIGENERE_CIPHER'] }, - { name: 'SIMPLE RSA', types: ['SIMPLE_RSA_KEY_GEN', 'SIMPLE_RSA_PUBKEY_GEN', 'SIMPLE_RSA_ENC', 'SIMPLE_RSA_DEC', 'SIMPLE_RSA_SIGN', 'SIMPLE_RSA_VERIFY'] }, - { name: 'SYMMETRIC CRYPTO (AES)', types: ['KEY_GEN', 'SYM_ENC', 'SYM_DEC'] }, -]; - -const INITIAL_NODES = []; -const INITIAL_CONNECTIONS = []; -const NODE_DIMENSIONS = { initialWidth: 300, initialHeight: 280, minWidth: 250, minHeight: 250 }; - -// --- Logic & Helpers --- - -const modPow = (base, exponent, modulus) => { - if (modulus === BigInt(1)) return BigInt(0); - let result = BigInt(1); - base = base % modulus; - while (exponent > BigInt(0)) { - if (exponent % BigInt(2) === BigInt(1)) { - result = (result * base) % modulus; - } - exponent = exponent >> BigInt(1); - base = (base * base) % modulus; - } - return result; -}; - -const gcd = (a, b) => { - while (b) { - [a, b] = [b, a % b]; - } - return a; -}; - -const modInverse = (a, m) => { - let m0 = m; - let x0 = BigInt(0); - let x1 = BigInt(1); - if (m === BigInt(1)) return BigInt(0); - while (a > BigInt(1)) { - let q = a / m; - let t = m; - m = a % m; - a = t; - t = x0; - x0 = x1 - q * x0; - x1 = t; - } - if (x1 < BigInt(0)) { - x1 += m0; - } - return x1; -}; - -const DEMO_PRIMES = [167, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283]; - -const generateSmallPrimes = () => { - let p = 0; - let q = 0; - while (p === q) { - p = DEMO_PRIMES[Math.floor(Math.random() * DEMO_PRIMES.length)]; - q = DEMO_PRIMES[Math.floor(Math.random() * DEMO_PRIMES.length)]; - } - return { p: BigInt(p), q: BigInt(q) }; -}; - -const generateSmallE = (phiN) => { - let e = BigInt(0); - do { - e = BigInt(Math.floor(Math.random() * (Number(phiN) - 3)) + 2); - } while (gcd(e, phiN) !== BigInt(1)); - return e; -}; - -const caesarEncrypt = (inputData, inputFormat, k) => { - if (inputFormat !== 'Text (UTF-8)') { - return { output: `ERROR: Caesar Cipher requires Text (UTF-8) input. Received: ${inputFormat}`, format: inputFormat }; - } - let ciphertext = ''; - const shift = (k % 26 + 26) % 26; - const plaintext = inputData; - for (let i = 0; i < plaintext.length; i++) { - const char = plaintext[i]; - const charCode = char.charCodeAt(0); - if (charCode >= 65 && charCode <= 90) { - const encryptedCode = ((charCode - 65 + shift) % 26) + 65; - ciphertext += String.fromCharCode(encryptedCode); - } else if (charCode >= 97 && charCode <= 122) { - const encryptedCode = ((charCode - 97 + shift) % 26) + 97; - ciphertext += String.fromCharCode(encryptedCode); - } else { - ciphertext += char; - } - } - return { output: ciphertext, format: 'Text (UTF-8)' }; -}; - -const vigenereEncryptDecrypt = (inputData, keyWord, mode = 'ENCRYPT') => { - if (!keyWord || keyWord.length === 0) return { output: "ERROR: Keyword cannot be empty.", format: 'Text (UTF-8)' }; - if (inputData.startsWith('ERROR')) return { output: inputData, format: 'Text (UTF-8)' }; - - let result = ''; - let keyIndex = 0; - const plaintext = inputData; - const alphabetSize = 26; - - for (let i = 0; i < plaintext.length; i++) { - const char = plaintext[i]; - const charCode = char.charCodeAt(0); - if ((charCode >= 65 && charCode <= 90) || (charCode >= 97 && charCode <= 122)) { - const keyChar = keyWord[keyIndex % keyWord.length]; - let keyShift = keyChar.toUpperCase().charCodeAt(0) - 65; - let base = (charCode >= 65 && charCode <= 90) ? 65 : 97; - let charOffset = charCode - base; - let encryptedOffset; - if (mode === 'ENCRYPT') { - encryptedOffset = (charOffset + keyShift) % alphabetSize; - } else { - encryptedOffset = (charOffset - keyShift + alphabetSize) % alphabetSize; - } - result += String.fromCharCode(encryptedOffset + base); - keyIndex++; - } else { - result += char; - } - } - return { output: result, format: 'Text (UTF-8)' }; -}; - -const arrayBufferToBase64 = (buffer) => { - const bytes = new Uint8Array(buffer); - let binary = ''; - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); -}; - -const base64ToArrayBuffer = (base64) => { - const binary_string = atob(base64); - const len = binary_string.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - return bytes.buffer; -}; - -const arrayBufferToBigIntString = (buffer) => { - const hex = arrayBufferToHex(buffer); - if (hex.length === 0) return '0'; - try { - return BigInt(`0x${hex}`).toString(10); - } catch (e) { - return `ERROR: Data too large for BigInt conversion (${buffer.byteLength} bytes).`; - } -}; - -const arrayBufferToHexBig = (buffer) => { - return arrayBufferToHex(buffer).toUpperCase(); -}; - -const arrayBufferToBinaryBig = (buffer) => { - const byteArray = new Uint8Array(buffer); - let binary = ''; - for (const byte of byteArray) { - binary += byte.toString(2).padStart(8, '0'); - } - return binary; -}; - -const arrayBufferToHex = (buffer) => { - const byteArray = new Uint8Array(buffer); - return Array.from(byteArray).map(byte => byte.toString(16).padStart(2, '0')).join(''); -}; - -const arrayBufferToBinary = (buffer) => { - const byteArray = new Uint8Array(buffer); - return Array.from(byteArray).map(byte => byte.toString(2).padStart(8, '0')).join(' '); -}; - -const hexToArrayBuffer = (hex) => { - const cleanedHex = hex.replace(/\s/g, ''); - if (cleanedHex.length === 0) return new ArrayBuffer(0); - const paddedHex = cleanedHex.length % 2 !== 0 ? '0' + cleanedHex : cleanedHex; - const len = paddedHex.length / 2; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = parseInt(paddedHex.substring(i * 2, i * 2 + 2), 16); - } - return bytes.buffer; -}; - -const convertToUint8Array = (dataStr, sourceFormat) => { - if (!dataStr) return new Uint8Array(0); - try { - if (sourceFormat === 'Text (UTF-8)') return new TextEncoder().encode(dataStr); - if (sourceFormat === 'Base64') return new Uint8Array(base64ToArrayBuffer(dataStr)); - if (sourceFormat === 'Hexadecimal') { - const cleanedHex = dataStr.replace(/\s/g, ''); - return new Uint8Array(hexToArrayBuffer(cleanedHex)); - } - if (sourceFormat === 'Binary') { - const binaryArray = dataStr.replace(/\s+/g, '').match(/.{1,8}/g) || []; - const validBytes = binaryArray.map(s => parseInt(s, 2)).filter(b => !isNaN(b)); - return new Uint8Array(validBytes); - } - if (sourceFormat === 'Decimal') { - const decimalArray = dataStr.split(/\s+/).map(s => parseInt(s, 10)); - const validBytes = decimalArray.filter(b => !isNaN(b) && b >= 0 && b >= 255); - return new Uint8Array(validBytes); - } - return new TextEncoder().encode(dataStr); - } catch (e) { - console.error(`Conversion to Uint8Array failed for format ${sourceFormat}:`, e); - return new Uint8Array(0); - } -}; - -const convertDataFormat = (dataStr, sourceFormat, targetFormat, toSingleNumber = false) => { - if (!dataStr) return ''; - if (sourceFormat === targetFormat || dataStr.startsWith('ERROR')) return dataStr; - let buffer; - try { - if (sourceFormat === 'Text (UTF-8)') buffer = new TextEncoder().encode(dataStr).buffer; - else if (sourceFormat === 'Base64') buffer = base64ToArrayBuffer(dataStr); - else if (sourceFormat === 'Hexadecimal') buffer = hexToArrayBuffer(dataStr.replace(/\s/g, '')); - else if (sourceFormat === 'Binary') { - const binaryArray = dataStr.replace(/\s+/g, '').match(/.{1,8}/g) || []; - const validBytes = binaryArray.map(s => parseInt(s, 2)).filter(b => !isNaN(b) && b >= 0 && b <= 255); - buffer = new Uint8Array(validBytes).buffer; - } else if (sourceFormat === 'Decimal') { - const decimalArray = dataStr.split(/\s+/).map(s => parseInt(s, 10)); - const validBytes = decimalArray.filter(b => !isNaN(b) && b >= 0 && b <= 255); - buffer = new Uint8Array(validBytes).buffer; - } else buffer = new TextEncoder().encode(dataStr).buffer; - } catch (e) { return `DECODING ERROR: Failed source format (${sourceFormat}).`; } - - try { - if (toSingleNumber) { - if (targetFormat === 'Decimal') return arrayBufferToBigIntString(buffer); - if (targetFormat === 'Hexadecimal') return arrayBufferToHexBig(buffer); - if (targetFormat === 'Binary') return arrayBufferToBinaryBig(buffer); - } - if (targetFormat === 'Text (UTF-8)') return new TextDecoder().decode(buffer); - if (targetFormat === 'Base64') return arrayBufferToBase64(buffer); - if (targetFormat === 'Hexadecimal') return arrayBufferToHex(buffer).toUpperCase().match(/.{1,2}/g)?.join(' ') || ''; - if (targetFormat === 'Binary') return arrayBufferToBinary(buffer); - if (targetFormat === 'Decimal') return Array.from(new Uint8Array(buffer)).join(' '); - return `ERROR: Unsupported target format (${targetFormat})`; - } catch (e) { return `ENCODING ERROR: Failed conversion to ${targetFormat}.`; } -}; - -const getOutputFormat = (nodeType) => { - switch (nodeType) { - case 'DATA_INPUT': case 'CAESAR_CIPHER': case 'VIGENERE_CIPHER': return 'Text (UTF-8)'; - case 'KEY_GEN': case 'SYM_ENC': case 'DATA_SPLIT': case 'DATA_CONCAT': return 'Binary'; - case 'ASYM_ENC': case 'SIMPLE_RSA_KEY_GEN': case 'RSA_KEY_GEN': case 'SIMPLE_RSA_PUBKEY_GEN': return 'Base64'; - case 'HASH_FN': return 'Hexadecimal'; - case 'SYM_DEC': case 'ASYM_DEC': return 'Text (UTF-8)'; - case 'SIMPLE_RSA_ENC': case 'SIMPLE_RSA_DEC': case 'SIMPLE_RSA_SIGN': return 'Decimal'; - case 'SIMPLE_RSA_VERIFY': return 'Text (UTF-8)'; - default: return 'Text (UTF-8)'; - } -} - -const performBitwiseXor = (dataAStr, formatA, dataBStr, formatB) => { - if (!dataAStr || !dataBStr || dataAStr.startsWith('ERROR') || dataBStr.startsWith('ERROR')) { - return { output: "ERROR: Missing one or both inputs or inputs failed conversion.", format: formatA }; - } - if (formatA !== formatB || !['Binary', 'Hexadecimal'].includes(formatA)) { - const bytesA = convertToUint8Array(dataAStr, formatA); - const bytesB = convertToUint8Array(dataBStr, formatB); - const combinedBytes = performRawXor(bytesA, bytesB); - const finalFormat = formatA === 'N/A' || formatA === 'Decimal' ? 'Base64' : formatA; - const output = convertDataFormat(arrayBufferToBase64(combinedBytes.buffer), 'Base64', finalFormat); - return { output: output, format: finalFormat }; - } - const cleanA = dataAStr.replace(/\s/g, ''); - const cleanB = dataBStr.replace(/\s/g, ''); - const targetLength = Math.max(cleanA.length, cleanB.length); - const paddedA = cleanA.padStart(targetLength, '0'); - const paddedB = cleanB.padStart(targetLength, '0'); - let bigIntA, bigIntB; - try { - if (formatA === 'Binary') { - bigIntA = BigInt(`0b${paddedA}`); - bigIntB = BigInt(`0b${paddedB}`); - } else if (formatA === 'Hexadecimal') { - bigIntA = BigInt(`0x${paddedA}`); - bigIntB = BigInt(`0x${paddedB}`); - } - } catch (e) { return { output: "ERROR: Data too large for BigInt XOR or invalid numerical input.", format: formatA }; } - const resultBigInt = bigIntA ^ bigIntB; - let resultStr; - if (formatA === 'Binary') resultStr = bigIntToString(resultBigInt, 'Binary', targetLength); - else resultStr = bigIntToString(resultBigInt, 'Hexadecimal', targetLength, true); - return { output: resultStr, format: formatA }; -}; +const App = () => { + const [nodes, setNodes] = useState(INITIAL_NODES); + const [connections, setConnections] = useState(INITIAL_CONNECTIONS); + const [connectingPort, setConnectingPort] = useState(null); + const [statusMessage, setStatusMessage] = useState(null); + const [scale, setScale] = useState(1); + const canvasRef = useRef(null); -const performRawXor = (bytesA, bytesB) => { - const len = Math.min(bytesA.length, bytesB.length); - const result = new Uint8Array(len); - for (let i = 0; i < len; i++) result[i] = bytesA[i] ^ bytesB[i]; - return result; -}; + const clearStatusMessage = useCallback(() => setStatusMessage(null), []); -const stringToBigInt = (dataStr, format) => { - if (!dataStr) return null; - if (dataStr.includes(' ') && format !== 'Text (UTF-8)' && format !== 'Base64') return null; - const cleanedStr = dataStr.replace(/\s/g, ''); - try { - if (format === 'Decimal') { if (!/^\d+$/.test(cleanedStr)) return null; return BigInt(cleanedStr); } - if (format === 'Hexadecimal') { if (!/^[0-9a-fA-F]+$/.test(cleanedStr)) return null; return BigInt(`0x${cleanedStr}`); } - if (format === 'Binary') { - if (!/^[01]+$/.test(cleanedStr)) return null; - const paddedBinary = cleanedStr.padStart(Math.ceil(cleanedStr.length / 4) * 4, '0'); - return BigInt(`0b${paddedBinary}`); - } - } catch (e) { return null; } - return null; -}; + useEffect(() => { setNodes(prevNodes => recalculateGraph(prevNodes, connections, null, setNodes)); }, [connections]); -const bigIntToString = (bigIntValue, format, originalLength = 0, isHexLength = false) => { - if (bigIntValue === null) return 'N/A'; - switch (format) { - case 'Decimal': return bigIntValue.toString(10); - case 'Hexadecimal': - let hexString = bigIntValue.toString(16).toUpperCase(); - if (originalLength > 0) { - const hexLength = isHexLength ? originalLength : Math.ceil(originalLength / 4); - hexString = hexString.padStart(hexLength, '0'); - if (hexString.length > hexLength) hexString = hexString.substring(hexString.length - hexLength); - } - return hexString; - case 'Binary': - let binaryString = bigIntValue.toString(2); - if (originalLength > 0) { - binaryString = binaryString.padStart(originalLength, '0'); - if (binaryString.length > originalLength) binaryString = binaryString.substring(binaryString.length - originalLength); + const updateNodeContent = useCallback((id, field, value) => { + setNodes(prevNodes => { + const nextNodes = prevNodes.map(node => node.id === id ? { ...node, [field]: value } : node); + return recalculateGraph(nextNodes, connections, id, setNodes); + }); + }, [connections]); + + const setPosition = useCallback((id, newPos) => setNodes(prev => prev.map(n => n.id === id ? { ...n, position: newPos } : n)), []); + const handleNodeResize = useCallback((id, w, h) => setNodes(prev => prev.map(n => n.id === id ? { ...n, width: Math.max(NODE_DIMENSIONS.minWidth, w), height: Math.max(NODE_DIMENSIONS.minHeight, h) } : n)), []); + + const addNode = useCallback((type, label, color) => { + const newId = `${type}_${Date.now()}`; + const def = NODE_DEFINITIONS[type]; + const initialContent = { dataOutput: '', isProcessing: false, outputFormat: getOutputFormat(type), width: (['SHIFT_OP', 'XOR_OP', 'DATA_SPLIT', 'DATA_CONCAT'].includes(type) ? 300 : NODE_DIMENSIONS.initialWidth), height: (['SHIFT_OP', 'XOR_OP', 'DATA_SPLIT', 'DATA_CONCAT'].includes(type) ? 300 : NODE_DIMENSIONS.initialHeight) }; + const cv = canvasRef.current; + let x = ((cv?.clientWidth || 800) / 2) - 150 + (Math.random() * 200 - 100); + let y = ((cv?.clientHeight || 600) / 2) - 140 + (Math.random() * 200 - 100); + // Explicitly handle initial values for specific node types + if (type === 'DATA_INPUT') { initialContent.content = ''; initialContent.format = 'Binary'; } + else if (type === 'OUTPUT_VIEWER') { initialContent.isConversionExpanded = false; initialContent.convertedFormat = 'Base64'; } + else if (type === 'CAESAR_CIPHER') initialContent.shiftKey = 3; + else if (type === 'VIGENERE_CIPHER') { initialContent.keyword = 'HELLO'; initialContent.vigenereMode = 'ENCRYPT'; } + else if (type === 'SIMPLE_RSA_KEY_GEN') { initialContent.generateKey = true; initialContent.modulusLength = 0; } + else if (type === 'SHIFT_OP') { initialContent.shiftType = 'Left'; initialContent.shiftAmount = 1; } + + setNodes(prev => [...prev, { id: newId, label: def.label, position: { x: Math.max(20, x), y: Math.max(20, y) }, type, color, ...initialContent }]); + }, []); + + const handleDeleteNode = useCallback((id) => { + setNodes(prev => prev.filter(n => n.id !== id)); + setConnections(prev => prev.filter(c => c.source !== id && c.target !== id)); + }, []); + + const handleConnectStart = useCallback((nodeId, portIndex, outputType) => setConnectingPort({ sourceId: nodeId, sourcePortIndex: portIndex, outputType }), []); + const handleConnectEnd = useCallback((targetId, targetPortId) => { + if (connectingPort && targetId && connectingPort.sourceId !== targetId) { + const { sourceId, sourcePortIndex } = connectingPort; + const targetNode = nodes.find(n => n.id === targetId); + const targetNodeDef = NODE_DEFINITIONS[targetNode?.type]; + if (targetNodeDef && targetNodeDef.inputPorts.some(p => p.id === targetPortId)) { + setConnections(prev => [...prev, { source: sourceId, sourcePortIndex, target: targetId, targetPortId }]); } - return binaryString; - default: return bigIntValue.toString(10); - } -}; - -const performBitShiftOperation = (dataStr, shiftType, shiftAmount, inputFormat) => { - let shiftDescription = `Arithmetic/Logical ${shiftType} Shift (${shiftAmount} bits)`; - if (!dataStr) return { output: "ERROR: Missing data input.", description: shiftDescription }; - if (inputFormat === 'Text (UTF-8)' || inputFormat === 'Base64') return { output: `ERROR: Bit Shift requires input data to be a single number (Decimal, Hexadecimal, or Binary). Received: ${inputFormat}.`, description: shiftDescription }; - const cleanedStr = dataStr.replace(/\s/g, ''); - const bigIntData = stringToBigInt(cleanedStr, inputFormat); - if (bigIntData === null) return { output: `ERROR: Data must represent a single, contiguous number in ${inputFormat} format. Spaces are not allowed.`, description: shiftDescription }; - const amount = BigInt(Math.max(0, parseInt(shiftAmount) || 0)); - let resultBigInt; - let bitLength = 0; - const isRotational = inputFormat === 'Binary' || inputFormat === 'Hexadecimal'; - if (isRotational) { - if (inputFormat === 'Binary') bitLength = cleanedStr.length; - else if (inputFormat === 'Hexadecimal') bitLength = cleanedStr.length * 4; - } - const amountMod = amount % BigInt(bitLength || 1); - try { - if (isRotational && bitLength > 0) { - const L = BigInt(bitLength); - const data = bigIntData; - if (shiftType === 'Left') { - const shiftedLeft = data << amountMod; - const shiftedRight = data >> (L - amountMod); - const mask = (BigInt(1) << L) - BigInt(1); - resultBigInt = (shiftedLeft | shiftedRight) & mask; - shiftDescription = `Rotational Left Shift (ROL) (${shiftAmount} bits)`; - } else if (shiftType === 'Right') { - const shiftedRight = data >> amountMod; - const shiftedLeft = data << (L - amountMod); - const mask = (BigInt(1) << L) - BigInt(1); - resultBigInt = (shiftedRight | shiftedLeft) & mask; - shiftDescription = `Rotational Right Shift (ROR) (${shiftAmount} bits)`; - } - } else { - if (shiftType === 'Left') resultBigInt = bigIntData << amount; - else resultBigInt = bigIntData >> amount; } - } catch (error) { return { output: `ERROR: Bit Shift calculation failed. ${error.message}`, description: shiftDescription }; } - const finalLength = isRotational ? bitLength : 0; - return { output: bigIntToString(resultBigInt, inputFormat, finalLength, inputFormat === 'Hexadecimal'), description: shiftDescription }; -}; - -const splitDataIntoChunks = (dataStr, format) => { - if (!dataStr || dataStr.startsWith('ERROR')) { - const error = dataStr || 'Missing data input.'; - return { chunk1: `ERROR: ${error}`, chunk2: `ERROR: ${error}`, outputFormat: format }; - } - - let cleanData = dataStr.replace(/\s/g, ''); - let representation, splitUnit; - if (format === 'Text (UTF-8)' || format === 'Base64') { representation = cleanData; splitUnit = 'char'; } - else if (format === 'Hexadecimal') { representation = cleanData; splitUnit = 'hex'; } - else if (format === 'Decimal') return { chunk1: `ERROR: Cannot split a single Decimal number.`, chunk2: `ERROR: Cannot split a single Decimal number.`, outputFormat: 'Text (UTF-8)' }; - else { representation = cleanData; splitUnit = 'bin'; } - const length = representation.length; - const midPoint = Math.ceil(length / 2); - const chunk1 = representation.substring(0, midPoint); - const chunk2 = representation.substring(midPoint); - const formatChunk = (chunk, originalFormat) => { - if (originalFormat === 'Hexadecimal' && splitUnit === 'hex') return chunk.match(/.{1,2}/g)?.join(' ')?.trim() || chunk; - if (originalFormat === 'Binary' && splitUnit === 'bin') return chunk.match(/.{1,8}/g)?.join(' ')?.trim() || chunk; - return chunk; - }; - return { chunk1: formatChunk(chunk1, format), chunk2: formatChunk(chunk2, format), outputFormat: format }; -}; - -const concatenateData = (dataAStr, formatA, dataBStr, formatB, interpretAsText = false) => { - if (!dataAStr || dataAStr.startsWith('ERROR')) return { output: dataBStr || "ERROR: Missing data input A and B.", format: formatB || 'Binary' }; - if (!dataBStr || dataBStr.startsWith('ERROR')) return { output: dataAStr, format: formatA || 'Binary' }; - - // Strict text concatenation if checkbox is enabled - if (interpretAsText) { - return { output: dataAStr + dataBStr, format: 'Text (UTF-8)' }; - } - - // STRICT TYPE CHECKING - if (formatA !== formatB) { - return { - output: `ERROR: Type Mismatch. Input A is ${formatA} and Input B is ${formatB}. Inputs must be of the same type.`, - format: 'Text (UTF-8)' + setConnectingPort(null); + }, [connectingPort, nodes]); + + const handleRemoveConnection = useCallback((s, t, sIdx, tId) => setConnections(prev => prev.filter(c => !(c.source === s && c.target === t && c.sourcePortIndex === sIdx && c.targetPortId === tId))), []); + + const handleZoomIn = useCallback(() => setScale(prev => Math.min(prev + 0.1, 2)), []); + const handleZoomOut = useCallback(() => setScale(prev => Math.max(prev - 0.1, 0.5)), []); + + const handleDownloadProject = useCallback(() => { + const projectData = { schemaVersion: PROJECT_SCHEMA_VERSION, nodes: nodes, connections: connections }; + downloadFile(JSON.stringify(projectData, null, 2), `visual_crypto_project_v${PROJECT_SCHEMA_VERSION}.json`, 'application/json'); + setStatusMessage({ type: 'success', message: 'Project exported successfully!' }); + setTimeout(clearStatusMessage, 3000); + }, [nodes, connections, clearStatusMessage]); + + const handleUploadProject = useCallback((fileInput) => { + clearStatusMessage(); + const file = fileInput.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + try { + const projectData = JSON.parse(e.target.result); + if (!projectData || !Array.isArray(projectData.nodes)) throw new Error("Invalid JSON"); + const { migratedData, wasMigrated } = migrateProjectData(projectData); + setNodes(migratedData.nodes); + setConnections(migratedData.connections); + setStatusMessage({ type: 'success', message: wasMigrated ? 'Project migrated & loaded!' : 'Project loaded successfully!' }); + } catch (error) { setStatusMessage({ type: 'error', message: 'Import failed: Invalid file.' }); } + setTimeout(clearStatusMessage, 3000); }; - } - - const cleanA = dataAStr.replace(/\s/g, ''); - const cleanB = dataBStr.replace(/\s/g, ''); - - if (formatA === 'Binary') return { output: cleanA + cleanB, format: 'Binary' }; - - if (formatA === 'Hexadecimal') { - // Concatenate without spaces and ensure lower case to match visual expectation from user - return { output: (cleanA + cleanB).toLowerCase(), format: 'Hexadecimal' }; - } - - if (formatA === 'Text (UTF-8)') { - return { output: dataAStr + dataBStr, format: 'Text (UTF-8)' }; - } - - try { - const bytesA = convertToUint8Array(dataAStr, formatA); - const bytesB = convertToUint8Array(dataBStr, formatB); - const combinedBytes = new Uint8Array(bytesA.length + bytesB.length); - combinedBytes.set(bytesA, 0); - combinedBytes.set(bytesB, bytesA.length); - const output = convertDataFormat(arrayBufferToBase64(combinedBytes.buffer), 'Base64', formatA); - return { output, format: formatA }; - } catch (e) { return { output: `ERROR: Concatenation failed. Check data formats.`, format: formatA }; } -}; - -// MODIFIED: calculateHash now takes format to convert input correctly -const calculateHash = async (str, format, algorithm) => { - if (!str) return 'Missing data input.'; - if (!HASH_ALGORITHMS.includes(algorithm)) return `ERROR: Algorithm not supported (${algorithm}).`; - try { - // Convert based on the actual format (e.g. 'Hexadecimal' -> bytes) instead of always assuming text - const data = convertToUint8Array(str, format); - const hashBuffer = await crypto.subtle.digest(algorithm.toUpperCase(), data); - return arrayBufferToHex(hashBuffer); - } catch (error) { return `ERROR: Calculation failed with ${algorithm}.`; } -}; - -const generateSymmetricKey = async (algorithm) => { - try { - const key = await crypto.subtle.generateKey({ name: algorithm, length: 256 }, true, ["encrypt", "decrypt"]); - const rawKey = await crypto.subtle.exportKey('raw', key); - return { keyObject: key, keyBase64: arrayBufferToBase64(rawKey) }; - } catch (error) { return { keyObject: null, keyBase64: `ERROR: Key generation failed. ${error.message}` }; } -}; - -const generateAsymmetricKeyPair = async (algorithm, modulusLength, publicExponentDecimal) => { - let publicExponentArray = new Uint8Array([0x01, 0x00, 0x01]); - const exponentValue = publicExponentDecimal || 65537; - try { - const keyPair = await crypto.subtle.generateKey( - { name: algorithm, modulusLength: modulusLength, publicExponent: publicExponentArray, hash: { name: "SHA-256" } }, - true, ["encrypt", "decrypt", "wrapKey", "unwrapKey"] - ); - const publicKey = await crypto.subtle.exportKey('spki', keyPair.publicKey); - const privateKey = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey); - const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); - return { - publicKey: arrayBufferToBase64(publicKey), - privateKey: arrayBufferToBase64(privateKey), - keyPairObject: keyPair, - rsaParameters: { n: privateKeyJwk.n, e: privateKeyJwk.e, d: privateKeyJwk.d, p: privateKeyJwk.p, q: privateKeyJwk.q } + reader.readAsText(file); + fileInput.value = ''; + }, [clearStatusMessage]); + + const connectionPaths = useMemo(() => { + let maxX = 0; let maxY = 0; const padding = 50; + nodes.forEach(node => { maxX = Math.max(maxX, node.position.x + node.width); maxY = Math.max(maxY, node.position.y + node.height); }); + const svgWidth = Math.max(maxX + padding, (canvasRef.current?.clientWidth || 0) / scale); + const svgHeight = Math.max(maxY + padding, (canvasRef.current?.clientHeight || 0) / scale); + return { + size: { width: svgWidth, height: svgHeight }, + paths: connections.map(conn => { + const sN = nodes.find(n => n.id === conn.source); + const tN = nodes.find(n => n.id === conn.target); + return (sN && tN) ? { path: getLinePath(sN, tN, conn), source: conn.source, target: conn.target, sourcePortIndex: conn.sourcePortIndex, targetPortId: conn.targetPortId } : null; + }).filter(Boolean) }; - } catch (error) { return { publicKey: `ERROR: ${error.message}`, privateKey: `ERROR: ${error.message}`, keyPairObject: null, rsaParameters: {} }; } -}; - -const asymmetricEncrypt = async (dataStr, base64PublicKey, algorithm) => { - if (!dataStr) return 'Missing Data Input.'; - if (!base64PublicKey || typeof base64PublicKey !== 'string') return 'Missing or invalid Public Key Input.'; - try { - const keyBuffer = base64ToArrayBuffer(base64PublicKey); - const publicKey = await crypto.subtle.importKey('spki', keyBuffer, { name: algorithm, hash: "SHA-256" }, true, ['encrypt']); - const encoder = new TextEncoder(); - const encryptedBuffer = await crypto.subtle.encrypt({ name: algorithm }, publicKey, encoder.encode(dataStr)); - return arrayBufferToBase64(encryptedBuffer); - } catch (error) { return `ERROR: Asymmetric Encryption failed. ${error.message}`; } -}; - -const asymmetricDecrypt = async (base64Ciphertext, base64PrivateKey, algorithm) => { - if (!base64Ciphertext) return 'Missing Ciphertext Input.'; - if (!base64PrivateKey || typeof base64PrivateKey !== 'string') return 'Missing or invalid Private Key Input.'; - try { - const keyBuffer = base64ToArrayBuffer(base64PrivateKey); - const privateKey = await crypto.subtle.importKey('pkcs8', keyBuffer, { name: algorithm, hash: "SHA-256" }, true, ['decrypt']); - const decryptedBuffer = await crypto.subtle.decrypt({ name: algorithm }, privateKey, base64ToArrayBuffer(base64Ciphertext)); - return new TextDecoder().decode(decryptedBuffer); - } catch (error) { return `ERROR: Asymmetric Decryption failed. ${error.message}`; } -}; - -const symmetricEncrypt = async (dataStr, base64Key, algorithm) => { - if (!dataStr) return 'Missing Data Input.'; - if (!base64Key || typeof base64Key !== 'string') return 'Missing or invalid Key Input.'; - try { - const key = await crypto.subtle.importKey('raw', base64ToArrayBuffer(base64Key), { name: algorithm, length: 256 }, true, ['encrypt', 'decrypt']); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encryptedBuffer = await crypto.subtle.encrypt({ name: algorithm, iv: iv }, key, new TextEncoder().encode(dataStr)); - const fullCipher = new Uint8Array(iv.byteLength + encryptedBuffer.byteLength); - fullCipher.set(new Uint8Array(iv), 0); - fullCipher.set(new Uint8Array(encryptedBuffer), iv.byteLength); - return arrayBufferToBase64(fullCipher.buffer); - } catch (error) { return `ERROR: Encryption failed. ${error.message}`; } -}; - -const symmetricDecrypt = async (base64Ciphertext, base64Key, algorithm) => { - if (!base64Ciphertext) return 'Missing Ciphertext Input.'; - if (!base64Key || typeof base64Key !== 'string') return 'Missing or invalid Key Input.'; - try { - const key = await crypto.subtle.importKey('raw', base64ToArrayBuffer(base64Key), { name: algorithm, length: 256 }, true, ['encrypt', 'decrypt']); - const fullCipherBuffer = base64ToArrayBuffer(base64Ciphertext); - if (fullCipherBuffer.byteLength < 12) throw new Error('Ciphertext is too short.'); - const iv = fullCipherBuffer.slice(0, 12); - const ciphertext = fullCipherBuffer.slice(12); - const decryptedBuffer = await crypto.subtle.decrypt({ name: algorithm, iv: new Uint8Array(iv) }, key, ciphertext); - return new TextDecoder().decode(decryptedBuffer); - } catch (error) { return `ERROR: Decryption failed. ${error.message}.`; } -}; + }, [connections, nodes, scale]); -const isContentCompatible = (content, targetFormat) => { - const cleanedContent = content.replace(/\s+/g, ''); - if (!cleanedContent) return true; - if (targetFormat === 'Text (UTF-8)') return true; - if (targetFormat === 'Binary') return /^[01]*$/.test(cleanedContent); - if (targetFormat === 'Decimal') return /^\d*$/.test(cleanedContent); - if (targetFormat === 'Hexadecimal') return /^[0-9a-fA-F]*$/.test(cleanedContent); - if (targetFormat === 'Base64') return /^[A-Za-z0-9+/=]*$/.test(cleanedContent); - return true; -}; - -const getLinePath = (sourceNode, targetNode, connection) => { - const sourceDef = NODE_DEFINITIONS[sourceNode.type]; - const targetDef = NODE_DEFINITIONS[targetNode.type]; - const getVerticalPosition = (nodeDef, index, isInput, nodeHeight) => { - const numPorts = isInput ? nodeDef.inputPorts.length : nodeDef.outputPorts.length; - const step = nodeHeight / (numPorts + 1); - return (index + 1) * step; - }; - const sourceVerticalPos = getVerticalPosition(sourceDef, connection.sourcePortIndex, false, sourceNode.height); - const targetPortIndex = targetDef.inputPorts.findIndex(p => p.id === connection.targetPortId); - const targetVerticalPos = getVerticalPosition(targetDef, targetPortIndex, true, targetNode.height); - const p1 = { x: sourceNode.position.x + sourceNode.width, y: sourceNode.position.y + sourceVerticalPos }; - const p2 = { x: targetNode.position.x, y: targetNode.position.y + targetVerticalPos }; - const midX = (p1.x + p2.x) / 2; - return `M${p1.x} ${p1.y} C${midX} ${p1.y}, ${midX} ${p2.y}, ${p2.x} ${p2.y}`; -}; - -const migrateProjectData = (projectData) => { - const currentVersion = PROJECT_SCHEMA_VERSION; - const importedVersion = projectData.schemaVersion || '1.0'; - if (importedVersion === currentVersion) return { migratedData: projectData, wasMigrated: false }; - let migratedData = { ...projectData }; - let wasMigrated = false; - if (importedVersion < '1.1') { - wasMigrated = true; - migratedData.nodes = migratedData.nodes.map(node => { - const newNode = { ...node }; - if (newNode.type === 'DATA_INPUT' && newNode.format && newNode.outputFormat === 'Text (UTF-8)') { - if (['Binary', 'Hexadecimal', 'Decimal'].includes(newNode.format)) newNode.outputFormat = newNode.format; - } - if (!newNode.width || newNode.width < NODE_DIMENSIONS.minWidth) newNode.width = NODE_DIMENSIONS.initialWidth; - if (!newNode.height || newNode.height < NODE_DIMENSIONS.minHeight) { - if (newNode.type === 'XOR_OP' || newNode.type === 'SHIFT_OP' || newNode.type === 'DATA_SPLIT' || newNode.type === 'DATA_CONCAT') newNode.height = 300; - else newNode.height = NODE_DIMENSIONS.initialHeight; - } - if (newNode.type === 'XOR_OP') { delete newNode.shiftType; delete newNode.shiftAmount; delete newNode.shiftDescription; } - return newNode; - }); - } - migratedData.schemaVersion = currentVersion; - return { migratedData, wasMigrated }; -}; - -// --- Sub-Components --- + const handleCanvasClick = useCallback(() => { if (connectingPort) handleConnectEnd(null); }, [connectingPort, handleConnectEnd]); -const Port = React.memo(({ nodeId, type, isConnecting, onStart, onEnd, title, isMandatory, portId, portIndex, outputType, nodes }) => { - let interactionClasses = ""; - let clickHandler = () => {}; - let portColor = OUTPUT_PORT_COLOR; - if (outputType === 'public' || outputType === 'private') { - portColor = outputType === 'public' ? PUBLIC_KEY_COLOR : PRIVATE_KEY_COLOR; - } else if (type === 'input') { - portColor = isMandatory ? INPUT_PORT_COLOR : OPTIONAL_PORT_COLOR; - } - if (type === 'output' && outputType === 'key') portColor = TEXT_ICON_CLASSES['orange'].replace('text', 'bg'); - if (type === 'output' && outputType === 'signature') portColor = SIGNATURE_COLOR.replace('border', 'bg'); - - if (type === 'output') { - clickHandler = (e) => { e.stopPropagation(); onStart(nodeId, portIndex, outputType); }; - interactionClasses = isConnecting?.sourceId === nodeId ? 'ring-4 ring-emerald-300 animate-pulse' : 'hover:ring-4 hover:ring-emerald-300 transition duration-150'; - } else if (type === 'input') { - const targetNode = nodes.find(n => n.id === nodeId); - const targetNodeDef = NODE_DEFINITIONS[targetNode?.type]; - const inputPortDef = targetNodeDef.inputPorts.find(p => p.id === portId); - const inputPortType = inputPortDef?.type; - const isTargetCandidate = isConnecting && isConnecting.sourceId !== nodeId && isConnecting.outputType === inputPortType; - if (isTargetCandidate) { - clickHandler = (e) => { e.stopPropagation(); onEnd(nodeId, portId); }; - interactionClasses = 'ring-4 ring-yellow-300 cursor-pointer animate-pulse-slow'; - } else { - interactionClasses = 'hover:ring-4 hover:ring-stone-300 transition duration-150'; - clickHandler = (e) => { e.stopPropagation(); }; - } - } - const stopPropagation = (e) => e.stopPropagation(); return ( -
- ); -}); - -const DraggableBox = ({ node, setPosition, canvasRef, handleConnectStart, handleConnectEnd, connectingPort, updateNodeContent, connections, handleDeleteNode, nodes, scale, handleResize }) => { - const { id, label, position, type, color, content, format, dataOutput, dataOutputPublic, dataOutputPrivate, isProcessing, hashAlgorithm, keyAlgorithm, symAlgorithm, modulusLength, publicExponent, sourceFormat, rawInputData, p, q, e, d, n, phiN, shiftKey, keyword, vigenereMode, dStatus, n_pub, e_pub, isReadOnly, width, height, keyBase64, shiftDescription, chunk1, chunk2, convertedData, convertedFormat, isConversionExpanded, interpretAsText } = node; - const definition = NODE_DEFINITIONS[type]; - const [isDragging, setIsDragging] = useState(false); - const [isResizing, setIsResizing] = useState(false); - const boxRef = useRef(null); - const offset = useRef({ x: 0, y: 0 }); - const resizeOffset = useRef({ x: 0, y: 0 }); - const [copyStatus, setCopyStatus] = useState('Copy'); - - const isDataInput = type === 'DATA_INPUT'; - const isOutputViewer = type === 'OUTPUT_VIEWER'; - const isHashFn = type === 'HASH_FN'; - const isKeyGen = type === 'KEY_GEN'; - const isSimpleRSAKeyGen = type === 'SIMPLE_RSA_KEY_GEN'; - const isSimpleRSAPubKeyGen = type === 'SIMPLE_RSA_PUBKEY_GEN'; - const isRSAKeyGen = type === 'RSA_KEY_GEN'; - const isSimpleRSAEnc = type === 'SIMPLE_RSA_ENC'; - const isSimpleRSADec = type === 'SIMPLE_RSA_DEC'; - const isSimpleRSASign = type === 'SIMPLE_RSA_SIGN'; - const isSimpleRSAVerify = type === 'SIMPLE_RSA_VERIFY'; - const isSymEnc = type === 'SYM_ENC'; - const isSymDec = type === 'SYM_DEC'; - const isAsymEnc = type === 'ASYM_ENC'; - const isAsymDec = type === 'ASYM_DEC'; - const isBitShift = type === 'SHIFT_OP'; - const isCaesarCipher = type === 'CAESAR_CIPHER'; - const isVigenereCipher = type === 'VIGENERE_CIPHER'; - const isDataSplit = type === 'DATA_SPLIT'; - const isDataConcat = type === 'DATA_CONCAT'; - - const FORMATS = ALL_FORMATS; - const isPortSource = connectingPort?.sourceId === id; - const iconTextColorClass = TEXT_ICON_CLASSES[color] || 'text-gray-600'; - let specificClasses = ''; - if (isPortSource) specificClasses = `border-emerald-500 ring-4 ring-emerald-300 cursor-pointer animate-pulse transition duration-200`; - else specificClasses = `${BORDER_CLASSES[color]} ${HOVER_BORDER_CLASSES[color]} ${isDragging ? 'cursor-grabbing' : 'cursor-pointer hover:border-blue-500'}`; - if (isProcessing) specificClasses = `border-yellow-500 ring-4 ring-yellow-300 animate-pulse transition duration-200`; - - let requiredMinHeight = NODE_DIMENSIONS.minHeight; - if (isOutputViewer) requiredMinHeight = isConversionExpanded ? 280 : 250; - if (isBitShift || type === 'XOR_OP' || isDataSplit || isDataConcat) requiredMinHeight = 300; - const effectiveMinHeight = requiredMinHeight; - const baseClasses = `h-auto flex flex-col justify-start items-center p-3 bg-white shadow-xl rounded-xl border-4 transition duration-150 ease-in-out hover:shadow-2xl absolute select-none z-10`; - const boxStyle = { left: `${position.x}px`, top: `${position.y}px`, width: `${width}px`, minHeight: `${effectiveMinHeight}px`, height: `${height}px` }; - const contentHeightExcludingHeader = height - 50; - - const handleDragStart = useCallback((e) => { - if (connectingPort || isResizing) return; - const interactiveTags = ['TEXTAREA', 'SELECT', 'OPTION', 'BUTTON', 'INPUT']; - if (e.target.tagName === 'DIV' && e.target.classList.contains('w-4') && e.target.classList.contains('h-4')) return; - if (interactiveTags.includes(e.target.tagName)) return; - const clientX = e.clientX || (e.touches?.[0]?.clientX ?? 0); - const clientY = e.clientY || (e.touches?.[0]?.clientY ?? 0); - const canvas = canvasRef.current; - if (boxRef.current && canvas) { - const canvasRect = canvas.getBoundingClientRect(); - const unscaledMouseX = (clientX - canvasRect.left) / scale; - const unscaledMouseY = (clientY - canvasRect.y) / scale; - offset.current = { x: unscaledMouseX - position.x, y: unscaledMouseY - position.y }; - setIsDragging(true); - e.preventDefault(); - } - }, [canvasRef, position.x, position.y, connectingPort, isResizing, scale]); - - const handleDragMove = useCallback((e) => { - if (!isDragging) return; - const canvas = canvasRef.current; - if (!canvas) return; - const clientX = e.clientX || (e.touches?.[0]?.clientX ?? 0); - const clientY = e.clientY || (e.touches?.[0]?.clientY ?? 0); - const canvasRect = canvas.getBoundingClientRect(); - const unscaledMouseX = (clientX - canvasRect.left) / scale; - const unscaledMouseY = (clientY - canvasRect.y) / scale; - let newX = unscaledMouseX - offset.current.x; - let newY = unscaledMouseY - offset.current.y; - newX = Math.max(0, newX); - newY = Math.max(0, newY); - setPosition(id, { x: newX, y: newY }); - }, [isDragging, id, setPosition, canvasRef, scale]); - - const handleDragEnd = useCallback(() => setIsDragging(false), []); - - const handleResizeStart = useCallback((e) => { - e.stopPropagation(); setIsResizing(true); - const clientX = e.clientX || (e.touches?.[0]?.clientX ?? 0); - const clientY = e.clientY || (e.touches?.[0]?.clientY ?? 0); - const canvas = canvasRef.current.getBoundingClientRect(); - const unscaledMouseX = (clientX - canvas.left) / scale; - const unscaledMouseY = (clientY - canvas.y) / scale; - resizeOffset.current = { x: unscaledMouseX - (node.position.x + node.width), y: unscaledMouseY - (node.position.y + node.height) }; - }, [node.position.x, node.position.y, node.width, node.height, scale, canvasRef]); - - const handleResizeMove = useCallback((e) => { - if (!isResizing) return; - const canvas = canvasRef.current; - if (!canvas) return; - const clientX = e.clientX || (e.touches?.[0]?.clientX ?? 0); - const clientY = e.clientY || (e.touches?.[0]?.clientY ?? 0); - const canvasRect = canvas.getBoundingClientRect(); - const unscaledMouseX = (clientX - canvasRect.left) / scale; - const unscaledMouseY = (clientY - canvasRect.y) / scale; - let newWidth = unscaledMouseX - node.position.x - resizeOffset.current.x; - let newHeight = unscaledMouseY - node.position.y - resizeOffset.current.y; - handleResize(id, newWidth, newHeight); - e.preventDefault(); - }, [isResizing, id, handleResize, node.position.x, node.position.y, scale]); - - const handleResizeEnd = useCallback(() => setIsResizing(false), []); - - useEffect(() => { - const globalHandleMove = (e) => { if (isDragging) handleDragMove(e); else if (isResizing) handleResizeMove(e); }; - const globalHandleUp = (e) => { if (isDragging) handleDragEnd(e); else if (isResizing) handleResizeEnd(e); }; - if (isDragging || isResizing) { - document.addEventListener('mousemove', globalHandleMove); - document.addEventListener('mouseup', globalHandleUp); - document.addEventListener('touchmove', globalHandleMove, { passive: false }); - document.addEventListener('touchend', globalHandleUp); - } - return () => { - document.removeEventListener('mousemove', globalHandleMove); - document.removeEventListener('mouseup', globalHandleUp); - document.removeEventListener('touchmove', globalHandleMove); - document.removeEventListener('touchend', globalHandleUp); - }; - }, [isDragging, isResizing, handleDragMove, handleDragEnd, handleResizeMove, handleResizeEnd]); - - const handleBoxClick = useCallback((e) => { - if (isDragging || isResizing) return; - if (connectingPort) handleConnectEnd(null); - e.stopPropagation(); - }, [connectingPort, handleConnectEnd, isDragging, isResizing]); - - const handleCopyToClipboard = useCallback((e, textToCopy) => { - e.stopPropagation(); - if (!textToCopy || textToCopy.startsWith('ERROR')) return; - try { - const tempTextArea = document.createElement('textarea'); - tempTextArea.value = textToCopy; - tempTextArea.style.position = 'fixed'; - tempTextArea.style.left = '-9999px'; - document.body.appendChild(tempTextArea); - tempTextArea.select(); - document.execCommand('copy'); - document.body.removeChild(tempTextArea); - setCopyStatus('Copied!'); - setTimeout(() => setCopyStatus('Copy'), 1500); - } catch (err) { console.error('Failed to copy text:', err); setCopyStatus('Error'); setTimeout(() => setCopyStatus('Copy'), 2000); } - }, [setCopyStatus]); - - const renderInputPorts = () => { - if (!definition.inputPorts || definition.inputPorts.length === 0) return null; - const step = height / (definition.inputPorts.length + 1); - return definition.inputPorts.map((portDef, index) => { - const topPosition = (index + 1) * step; - const isInputConnected = connections.some(c => c.target === id && c.targetPortId === portDef.id); - return ( -
- -
- ); - }); - }; - - const renderOutputPorts = () => { - if (!definition.outputPorts || definition.outputPorts.length === 0) return null; - const step = height / (definition.outputPorts.length + 1); - return definition.outputPorts.map((portDef, index) => { - const topPosition = (index + 1) * step; - return ( -
- -
- ); - }); - }; - - return ( -
-
e.stopPropagation()} title="Resize"> -
-
- - {renderInputPorts()} {renderOutputPorts()} -
-
- {definition.icon && ()} - {label} - {isCaesarCipher && k = {node.shiftKey || 0}} - {isVigenereCipher && Keyword: {node.keyword || 'None'}} - {isSimpleRSASign && Signing (m^d mod n)} - {isSimpleRSAVerify && Verifying (s^e mod n)} - {isSimpleRSAEnc && Encryption: (c = m^e mod n)} - {isSimpleRSADec && Decryption: (m = c^d mod n)} - {isHashFn && ( -
- ALGORITHM - -
- )} - {isKeyGen && ({keyAlgorithm})} - {isSimpleRSAKeyGen && ({modulusLength} bits)} - {type === 'XOR_OP' && ({isProcessing ? 'Processing' : 'Bitwise XOR'})} - {isBitShift && ({isProcessing ? 'Processing' : (shiftDescription || 'Bit Shift')})} - {isSimpleRSAPubKeyGen && Public Key Output} - {isDataSplit && Split by: Character/Hex/Bit} - {isDataConcat && Concatenation: Data A + Data B} -
- - {isDataInput && ( -
-