diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..106eecf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build-and-test: + name: Build, Lint, and Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + # Cache Bun dependencies + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + # Cache Next.js build + - name: Cache Next.js build + uses: actions/cache@v4 + with: + path: | + frontend/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('frontend/src/**/*.[jt]s', 'frontend/src/**/*.[jt]sx') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lock') }}- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint (ESLint 9) + run: bun run lint + + - name: Type Check (TypeScript) + run: bun run type-check + + - name: Run Tests + run: bun run test + + - name: Build Project + run: bun run build diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index 21f90dd..0000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": ["next/core-web-vitals"], - "rules": { - "react-hooks/exhaustive-deps": "warn" - }, - "ignorePatterns": [ - "node_modules/**", - ".next/**", - "out/**", - "build/**", - "next-env.d.ts" - ] -} diff --git a/frontend/bun.lock b/frontend/bun.lock index 93ff188..0199c1b 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -22,63 +22,28 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@next/eslint-plugin-next": "^16.0.10", "@types/bun": "^1.3.4", "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "autoprefixer": "^10.4.23", "eslint": "^9.39.2", - "eslint-config-next": "^16.0.10", "eslint-plugin-react": "^7.37.5", + "globals": "^16.5.0", "postcss": "^8.5.6", - "tailwindcss": "3.4.17", + "tailwindcss": "^3.4.17", "typescript": "^5.9.3", + "typescript-eslint": "^8.50.0", }, }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], - - "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], - - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -171,16 +136,12 @@ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.10", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-b2NlWN70bbPLmfyoLvvidPKWENBYYIe017ZGUpElvQjDytCWgxPJx7L9juxHt0xHvNVA08ZHJdOyhGzon/KJuw=="], @@ -207,8 +168,6 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@react-aria/focus": ["@react-aria/focus@3.21.2", "", { "dependencies": { "@react-aria/interactions": "^3.25.6", "@react-aria/utils": "^3.31.0", "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ=="], "@react-aria/interactions": ["@react-aria/interactions@3.25.6", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.31.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw=="], @@ -223,16 +182,12 @@ "@react-types/shared": ["@react-types/shared@3.32.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w=="], - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], "@types/dom-speech-recognition": ["@types/dom-speech-recognition@0.0.7", "", {}, "sha512-NjiUoJbBlKhyufNsMZLSp+pbPNtPAFnR738RCJvtZy/HVQ2TZjmqpMyaeOSMXgxdfZM60nt8QGbtfmQrJAH2sw=="], @@ -241,8 +196,6 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], @@ -269,44 +222,6 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], - "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], - - "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], - - "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], - - "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], - - "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], - - "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], - - "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], - - "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], - - "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], - - "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], - - "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], - - "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], - - "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], - - "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], - - "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], - - "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], - - "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], - - "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], - - "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - "@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], "@vercel/speed-insights": ["@vercel/speed-insights@1.3.1", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ=="], @@ -327,16 +242,12 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], @@ -345,30 +256,18 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], - - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.4", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -401,16 +300,12 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], @@ -435,10 +330,6 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.266", "", {}, "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -455,28 +346,12 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], - "eslint-config-next": ["eslint-config-next@16.0.10", "", { "dependencies": { "@next/eslint-plugin-next": "16.0.10", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-BxouZUm0I45K4yjOOIzj24nTi0H2cGo0y7xUmk+Po/PYtJXFBYVDS1BguE7t28efXjKdcN0tmiLivxQy//SsZg=="], - - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], - - "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], - - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], - - "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], - - "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], - "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -493,7 +368,7 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -515,8 +390,6 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -529,26 +402,20 @@ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -563,10 +430,6 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -585,8 +448,6 @@ "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -641,24 +502,16 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], - - "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -671,8 +524,6 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.561.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -683,8 +534,6 @@ "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], @@ -695,14 +544,10 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -719,8 +564,6 @@ "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -755,7 +598,7 @@ "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], - "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], @@ -789,8 +632,6 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -827,12 +668,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], @@ -843,8 +680,6 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], @@ -859,7 +694,7 @@ "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], @@ -875,8 +710,6 @@ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -891,16 +724,12 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.48.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A=="], + "typescript-eslint": ["typescript-eslint@8.50.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], @@ -919,21 +748,11 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], - - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -941,90 +760,30 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="], - "bun-types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "eslint-config-next/globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-import-resolver-node/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "postcss-import/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "tailwindcss/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "tailwindcss/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/type-utils": "8.48.1", "@typescript-eslint/utils": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA=="], - - "typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA=="], - - "typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.48.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.48.1", "@typescript-eslint/tsconfig-utils": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg=="], - - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="], - - "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="], - - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg=="], - - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="], - - "typescript-eslint/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="], - - "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], - - "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.48.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.48.1", "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.48.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="], - - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], - - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], - - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], - - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="], + "tailwindcss/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], } } diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..da78375 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import path from "path"; +import { fileURLToPath } from "url"; +import tseslint from "typescript-eslint"; +import nextPlugin from "@next/eslint-plugin-next"; +import reactPlugin from "eslint-plugin-react"; +import globals from "globals"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +export default tseslint.config( + { + ignores: [ + "**/node_modules/**", + "**/.next/**", + "**/out/**", + "**/build/**", + "eslint.config.mjs", + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + // Use compat for react-hooks to ensure legacy plugin structure is handled correctly + ...compat.plugins("eslint-plugin-react-hooks"), + { + files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], + plugins: { + "@next/next": nextPlugin, + react: reactPlugin, + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + settings: { + react: { + version: "detect", + }, + }, + rules: { + ...reactPlugin.configs.recommended.rules, + // Enable hooks rules manually since we loaded the plugin via compat + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + + // Essential Next.js Rules (Manual) + "@next/next/no-html-link-for-pages": "error", + "@next/next/no-img-element": "error", + "@next/next/no-sync-scripts": "error", + + // Custom & Relaxed Rules + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-require-imports": "warn", + "react/no-unknown-property": "warn", + }, + }, +); diff --git a/frontend/package.json b/frontend/package.json index 5e69a6a..b01c416 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,8 @@ "dev": "bun --bun next dev --turbo", "build": "bun --bun next build", "start": "bun --bun next start", - "lint": "tsc --noEmit", + "lint": "eslint .", + "type-check": "tsc --noEmit", "test": "bun test", "test:watch": "bun test --watch", "test:coverage": "bun test --coverage" @@ -35,13 +36,14 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "autoprefixer": "^10.4.23", + "globals": "^16.5.0", + "typescript-eslint": "^8.50.0", + "@eslint/js": "^9.39.2", + "@next/eslint-plugin-next": "^16.0.10", "eslint": "^9.39.2", - "eslint-config-next": "^16.0.10", "eslint-plugin-react": "^7.37.5", "postcss": "^8.5.6", - "tailwindcss": "3.4.17", + "tailwindcss": "^3.4.19", "typescript": "^5.9.3" } } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d590880..95da775 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,14 +1,14 @@ "use client"; -import { useState, useEffect, useRef, useCallback, useMemo } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { ChevronsRight } from "lucide-react"; -// Import existing hooks and utilities import { useDarkMode, useSwipeGesture, useDynamicFavicon, useKeyboardShortcuts, + useBankersAlgorithm, } from "@/hooks"; import { AnimatedThemeToggler } from "@/components/magicui"; @@ -17,20 +17,16 @@ import { BrowserCompatibilityWarning } from "@/components/ui/browser-compatibili import { ToastContainer, useToast } from "@/components/ui"; import { useAppLoading } from "@/hooks/useAppLoading"; -// Import Banker's Algorithm types and calculator -import { - BankersAlgorithmState, - ResourceRequest, -} from "@/types/bankers-algorithm"; -import { BankersAlgorithmCalculator } from "@/lib/bankers-algorithm-calculator"; -import { calculateNeedMatrix } from "@/utils/matrix-utils"; import { SystemControls, AlgorithmTable, StepByStepResults, } from "@/components/bankers-algorithm"; -// Updated Logo Icon Component for Banker's Algorithm with theme-dependent colors matching favicon +/** + * Logo Icon Component for Banker's Algorithm + * Theme-dependent colors matching favicon + */ const LogoIcon = ({ className }: { className?: string }) => ( ( - () => { - const calculator = new BankersAlgorithmCalculator(); - return calculator.createDefaultState(); - }, - ); + // Use the consolidated Banker's Algorithm hook + const { + algorithmState, + isProcessingRequest, + requestResult, + stepNavigationState, + checkSafety, + processResourceRequest, + updateAllocation, + updateMax, + updateAvailable, + updateProcessCount, + updateResourceCount, + resetAlgorithm, + handleStepChange, + setRequestResult, + } = useBankersAlgorithm({ + onSuccess: showSuccess, + onError: showError, + autoPreviewOnMount: true, + }); const sidebarRef = useRef(null); const mainContainerRef = useRef(null); @@ -77,20 +87,14 @@ export default function BankersAlgorithmPage() { const [showScrollButton, setShowScrollButton] = useState(false); const [isScrolled, setIsScrolled] = useState(false); - // Initialize calculator - use useMemo to avoid recreating on every render - const calculator = useMemo(() => new BankersAlgorithmCalculator(), []); - - // Track if initial preview has been shown - const hasShownInitialPreview = useRef(false); - - // Keyboard shortcuts + // Toggle sidebar callback const toggleSidebar = useCallback(() => { setIsSidebarOpen((prev) => !prev); setIsDesktopSidebarCollapsed((prev) => !prev); }, []); + // Focus first input callback const focusInput = useCallback(() => { - // Focus on first input field in the algorithm table const firstInput = document.querySelector( 'input[type="number"]', ) as HTMLInputElement; @@ -99,631 +103,19 @@ export default function BankersAlgorithmPage() { } }, []); - const resetAlgorithm = useCallback(() => { - // Preserve current process and resource counts - const currentProcessCount = algorithmState.processCount; - const currentResourceCount = algorithmState.resourceCount; - - // Create fresh state with preserved counts - const freshState = calculator.createFreshState(); - const newState = calculator.resizeMatrices( - freshState, - currentProcessCount, - currentResourceCount, - ); - - setAlgorithmState(newState); + // Enhanced reset with focus + const handleReset = useCallback(() => { + resetAlgorithm(); focusInput(); - showSuccess( - "System Reset", - "Matrix values have been reset while preserving process and resource counts.", - 3000, - ); - }, [ - focusInput, - calculator, - showSuccess, - algorithmState.processCount, - algorithmState.resourceCount, - ]); - - // Load default example - const loadDefaultExample = useCallback(() => { - const defaultState = calculator.createDefaultState(); - setAlgorithmState(defaultState); - showSuccess( - "Example Loaded", - "Classical Banker's Algorithm example has been loaded successfully.", - 4000, - ); - }, [calculator, showSuccess]); - - // Complete a process (simulate process finishing its task) - const completeProcess = useCallback( - (processId: number) => { - try { - const newState = calculator.completeProcess(algorithmState, processId); - - if (newState !== algorithmState) { - setAlgorithmState({ - ...newState, - lastUpdated: new Date(), - }); - - showSuccess( - "Process Completed", - `Process P${processId} has completed execution and released all resources.`, - 5000, - ); - } else { - showError( - "Cannot Complete Process", - `Process P${processId} cannot be completed yet. It still has unfinished tasks.`, - 5000, - ); - } - } catch (error) { - showError( - "Process Completion Error", - error instanceof Error ? error.message : "Failed to complete process", - 5000, - ); - } - }, - [algorithmState, calculator, showSuccess, showError], - ); - - // Update process count - const updateProcessCount = useCallback( - (newCount: number) => { - const minCount = 1; - const maxCount = 10; - const validCount = Math.max(minCount, Math.min(maxCount, newCount)); - - const newState = calculator.resizeMatrices( - algorithmState, - validCount, - algorithmState.resourceCount, - ); - setAlgorithmState(newState); - }, - [algorithmState, calculator], - ); - - // Update resource count - const updateResourceCount = useCallback( - (newCount: number) => { - const minCount = 1; - const maxCount = 10; - const validCount = Math.max(minCount, Math.min(maxCount, newCount)); - - const newState = calculator.resizeMatrices( - algorithmState, - algorithmState.processCount, - validCount, - ); - setAlgorithmState(newState); - }, - [algorithmState, calculator], - ); - - // Update available resources - const updateAvailable = useCallback( - (index: number, value: number) => { - const newAvailable = [...algorithmState.available]; - newAvailable[index] = Math.max(0, value); - - setAlgorithmState((prev) => ({ - ...prev, - available: newAvailable, - })); - }, - [algorithmState.available], - ); - - // Update allocation matrix - const updateAllocation = useCallback( - (processIndex: number, resourceIndex: number, value: number) => { - const newAllocation = algorithmState.allocation.map((row) => [...row]); - newAllocation[processIndex][resourceIndex] = Math.max(0, value); - - // Recalculate need matrix - const newNeed = calculateNeedMatrix(algorithmState.max, newAllocation); - - setAlgorithmState((prev) => ({ - ...prev, - allocation: newAllocation, - need: newNeed, - })); - }, - [algorithmState.allocation, algorithmState.max, calculator], - ); - - // Update max matrix - const updateMax = useCallback( - (processIndex: number, resourceIndex: number, value: number) => { - const newMax = algorithmState.max.map((row) => [...row]); - newMax[processIndex][resourceIndex] = Math.max(0, value); - - // Recalculate need matrix - const newNeed = calculateNeedMatrix(newMax, algorithmState.allocation); - - setAlgorithmState((prev) => ({ - ...prev, - max: newMax, - need: newNeed, - })); - }, - [algorithmState.max, algorithmState.allocation, calculator], - ); - - // Check safety with enhanced validation - const checkSafety = useCallback(() => { - // First validate the system state - const validationErrors = calculator.validateSystemData(algorithmState); - - if (validationErrors.length > 0) { - showError( - "System Validation Failed", - `Please fix the following issues: ${validationErrors - .map((e) => e.message) - .join(", ")}`, - 8000, - ); - return; - } - - // Clear previous calculation data before starting new calculation - setAlgorithmState((prev) => ({ - ...prev, - isCalculating: true, - // Clear previous calculation results - algorithmSteps: [], - safeSequence: [], - finish: Array(prev.processCount).fill(false), - isSafe: undefined, - })); - - // Clear request result since this is just a safety check - setRequestResult({ isRequest: false }); - - // Reset step navigation - setCurrentStepIndex(undefined); - setStepStates([]); - - // Add a small delay to show loading state - setTimeout(() => { - const safetyResult = calculator.checkSafety( - algorithmState.available, - algorithmState.allocation, - algorithmState.need, - ); - - // Save original state before updating - // Note: We don't need to save 'available' because it never changes during navigation - setOriginalStateBeforeSteps({ - available: [...algorithmState.available], // Keep original available - allocation: algorithmState.allocation.map((row) => [...row]), - need: algorithmState.need.map((row) => [...row]), - finish: [...algorithmState.finish], - }); - - setAlgorithmState((prev) => ({ - ...prev, - finish: safetyResult.finalFinishState, - safeSequence: safetyResult.safeSequence, - algorithmSteps: safetyResult.steps, - isSafe: safetyResult.isSafe, - isCalculating: false, - lastUpdated: new Date(), - })); - - // Build step states for navigation - track complete state at each step - const states: Array<{ - work: number[]; - finish: boolean[]; - allocation: number[][]; - need: number[][]; - available?: number[]; // Not used for safety check, available stays constant - }> = []; - let currentWork = [...algorithmState.available]; - let currentFinish = Array(algorithmState.processCount).fill(false); - // For safety checks, allocation and need never change - they're just the current state - const constantAllocation = algorithmState.allocation.map((row) => [ - ...row, - ]); - const constantNeed = algorithmState.need.map((row) => [...row]); - - safetyResult.steps.forEach((step) => { - if (step.workVector && step.workVector.length > 0) { - currentWork = [...step.workVector]; - } - if (step.processChecked && step.canFinish) { - const processIndex = parseInt(step.processChecked.replace("P", "")); - if (!isNaN(processIndex)) { - currentFinish = [...currentFinish]; - currentFinish[processIndex] = true; - } - } - states.push({ - work: [...currentWork], - finish: [...currentFinish], - allocation: constantAllocation.map((row) => [...row]), - need: constantNeed.map((row) => [...row]), - // Don't include available - it should remain constant for safety checks - }); - }); - - setStepStates(states); - - // Show result notification - if (safetyResult.isSafe) { - showSuccess( - "System is Safe", - `Safe execution sequence found: ${safetyResult.safeSequence.join( - " → ", - )}`, - 6000, - ); - } else { - showError( - "System is Unsafe", - "The current system state could lead to deadlock. Please review resource allocation.", - 8000, - ); - } - }, 300); - }, [algorithmState, calculator, showSuccess, showError]); - - // Auto-preview on initial page load (only once) - useEffect(() => { - if (!hasShownInitialPreview.current) { - const runInitialPreview = () => { - setTimeout(() => { - checkSafety(); - hasShownInitialPreview.current = true; - }, 1000); - }; - - runInitialPreview(); - } - }, [checkSafety]); + }, [resetAlgorithm, focusInput]); // Keyboard shortcuts useKeyboardShortcuts({ onToggleSidebar: toggleSidebar, onToggleTheme: toggleDarkMode, - onCheckSafety: checkSafety, // Check safety with Shift+Enter + onCheckSafety: checkSafety, }); - // Process resource request with enhanced error handling - const [isProcessingRequest, setIsProcessingRequest] = useState(false); - const [requestResult, setRequestResult] = useState<{ - isRequest: boolean; - wasGranted?: boolean; - processId?: number; - requestVector?: number[]; - shouldResetRequest?: boolean; - }>({ isRequest: false }); - - // Step navigation state - const [currentStepIndex, setCurrentStepIndex] = useState( - undefined, - ); - const [stepStates, setStepStates] = useState< - Array<{ - work: number[]; - finish: boolean[]; - allocation: number[][]; - need: number[][]; - available?: number[]; // Optional: only used for request steps - }> - >([]); - const [originalStateBeforeSteps, setOriginalStateBeforeSteps] = useState<{ - available: number[]; - allocation: number[][]; - need: number[][]; - finish: boolean[]; - } | null>(null); - - // Handle step navigation - const handleStepChange = useCallback( - (stepIndex: number | undefined) => { - setCurrentStepIndex(stepIndex); - - // If stepIndex is undefined, reset to final state (exit navigation mode) - if (stepIndex === undefined) { - // Restore the original state from before step navigation - if (originalStateBeforeSteps) { - setAlgorithmState((prev) => ({ - ...prev, - available: [...originalStateBeforeSteps.available], - allocation: originalStateBeforeSteps.allocation.map((row) => [ - ...row, - ]), - need: originalStateBeforeSteps.need.map((row) => [...row]), - finish: [...originalStateBeforeSteps.finish], - })); - } - return; - } - - // Update the UI to reflect the state at this step - if (stepStates[stepIndex]) { - const stepState = stepStates[stepIndex]; - setAlgorithmState((prev) => ({ - ...prev, - // For request steps, update available if it's tracked; otherwise use work vector - available: stepState.available - ? [...stepState.available] - : [...stepState.work], - allocation: stepState.allocation.map((row) => [...row]), - need: stepState.need.map((row) => [...row]), - finish: [...stepState.finish], - })); - } - }, - [stepStates, originalStateBeforeSteps], - ); - - const processResourceRequest = useCallback( - (request: ResourceRequest) => { - setIsProcessingRequest(true); - - // Clear previous calculation data before starting new request processing - setAlgorithmState((prev) => ({ - ...prev, - // Clear previous calculation results - algorithmSteps: [], - safeSequence: [], - finish: Array(prev.processCount).fill(false), - isSafe: undefined, - })); - - // Reset step navigation - setCurrentStepIndex(undefined); - setStepStates([]); - - // Add a small delay to show loading state - setTimeout(() => { - const requestResult = calculator.processRequest( - request, - algorithmState, - ); - - if (requestResult.canGrant && requestResult.newState) { - // Request can be granted - update state with enhanced final step - const enhancedSteps = [...(requestResult.simulationSteps || [])]; - - // Add request result information to the final step - if (enhancedSteps.length > 0) { - const lastStep = enhancedSteps[enhancedSteps.length - 1]; - if (lastStep.description.includes("System is SAFE")) { - lastStep.description += `\n\n[REQUEST GRANTED]: Process P${request.processId} successfully allocated [${request.requestVector.join(", ")}] resources. The system remains in a safe state.`; - } - } - - // Save original state before updating for step navigation - // For granted requests, save the NEW state (after allocation) - setOriginalStateBeforeSteps({ - available: [...requestResult.newState.available], // New available after allocation - allocation: requestResult.newState.allocation.map((row) => [ - ...row, - ]), - need: requestResult.newState.need.map((row) => [...row]), - finish: [...requestResult.newState.finish], - }); - - setAlgorithmState({ - ...requestResult.newState, - algorithmSteps: enhancedSteps, - lastUpdated: new Date(), - }); - - // Build step states for navigation - const states: Array<{ - work: number[]; - finish: boolean[]; - allocation: number[][]; - need: number[][]; - available: number[]; // Track available separately for request steps - }> = []; - let currentWork = [...requestResult.newState.available]; - let currentFinish = Array(requestResult.newState.processCount).fill( - false, - ); - // Start with original state before allocation - let currentAllocation = algorithmState.allocation.map((row) => [ - ...row, - ]); - let currentNeed = algorithmState.need.map((row) => [...row]); - let currentAvailable = [...algorithmState.available]; // Start with original available - - enhancedSteps.forEach((step, index) => { - // Check if this step is where allocation happens (step 3 in request process) - if ( - step.description.includes("Temporarily allocate resources") && - requestResult.newState - ) { - // Update to new state after allocation - currentAvailable = [...requestResult.newState.available]; - currentAllocation = requestResult.newState.allocation.map( - (row) => [...row], - ); - currentNeed = requestResult.newState.need.map((row) => [...row]); - } - - if (step.workVector && step.workVector.length > 0) { - currentWork = [...step.workVector]; - } - if (step.processChecked && step.canFinish) { - const processIndex = parseInt( - step.processChecked.replace("P", ""), - ); - if (!isNaN(processIndex)) { - currentFinish = [...currentFinish]; - currentFinish[processIndex] = true; - } - } - states.push({ - work: [...currentWork], - finish: [...currentFinish], - allocation: currentAllocation.map((row) => [...row]), - need: currentNeed.map((row) => [...row]), - available: [...currentAvailable], // Store the correct available for this step - }); - }); - - setStepStates(states); - - // Set request result state - setRequestResult({ - isRequest: true, - wasGranted: true, - processId: request.processId, - requestVector: request.requestVector, - shouldResetRequest: true, // Signal to reset request panel - }); - - // Show success message with detailed information - showSuccess( - "Request Granted", - requestResult.errorMessage || - `Process P${ - request.processId - } allocated [${request.requestVector.join( - ", ", - )}]. System remains safe.`, - 6000, - ); - } else { - // Request cannot be granted - show detailed error with enhanced final step - const enhancedSteps = [...(requestResult.simulationSteps || [])]; - - // Add request result information to the final step - if (enhancedSteps.length > 0) { - const lastStep = enhancedSteps[enhancedSteps.length - 1]; - if (lastStep.description.includes("System is UNSAFE")) { - lastStep.description += `\n\n[REQUEST DENIED]: Process P${request.processId} request [${request.requestVector.join(", ")}] cannot be granted as it would lead to an unsafe state (potential deadlock).`; - } - } - - // Set request result state - setRequestResult({ - isRequest: true, - wasGranted: false, - processId: request.processId, - requestVector: request.requestVector, - }); - - showError( - "Request Denied", - requestResult.errorMessage || - `Process P${ - request.processId - } request [${request.requestVector.join( - ", ", - )}] cannot be granted.`, - 8000, - ); - - // Show the simulation steps even for denied requests to help user understand why - if (requestResult.simulationSteps) { - // Save original state before updating for step navigation - // Note: We don't need to save 'available' because it never changes during navigation - setOriginalStateBeforeSteps({ - available: [...algorithmState.available], // Keep original available - allocation: algorithmState.allocation.map((row) => [...row]), - need: algorithmState.need.map((row) => [...row]), - finish: [...algorithmState.finish], - }); - - setAlgorithmState((prev) => ({ - ...prev, - algorithmSteps: enhancedSteps, - safeSequence: [], - finish: prev.finish.map(() => false), - isSafe: false, - lastUpdated: new Date(), - })); - - // Build step states for navigation - const states: Array<{ - work: number[]; - finish: boolean[]; - allocation: number[][]; - need: number[][]; - available?: number[]; - }> = []; - let currentWork = [...algorithmState.available]; - let currentFinish = Array(algorithmState.processCount).fill(false); - let currentAllocation = algorithmState.allocation.map((row) => [ - ...row, - ]); - let currentNeed = algorithmState.need.map((row) => [...row]); - let currentAvailable = [...algorithmState.available]; // Start with original available - - // For denied requests, we still need to track when allocation would have happened - enhancedSteps.forEach((step) => { - // Check if this step is where allocation would happen (step 3 in request process) - if (step.description.includes("Temporarily allocate resources")) { - // Extract the new available from the description or calculate it - // For denied requests, we simulate what would have happened - const requestVector = request.requestVector; - const processId = request.processId; - - // Update available (subtract request) - currentAvailable = algorithmState.available.map( - (val, idx) => val - requestVector[idx], - ); - - // Update allocation (add request to process) - currentAllocation = algorithmState.allocation.map( - (row, pIdx) => - pIdx === processId - ? row.map((val, rIdx) => val + requestVector[rIdx]) - : [...row], - ); - - // Update need (subtract request from process) - currentNeed = algorithmState.need.map((row, pIdx) => - pIdx === processId - ? row.map((val, rIdx) => val - requestVector[rIdx]) - : [...row], - ); - } - - if (step.workVector && step.workVector.length > 0) { - currentWork = [...step.workVector]; - } - if (step.processChecked && step.canFinish) { - const processIndex = parseInt( - step.processChecked.replace("P", ""), - ); - if (!isNaN(processIndex)) { - currentFinish = [...currentFinish]; - currentFinish[processIndex] = true; - } - } - states.push({ - work: [...currentWork], - finish: [...currentFinish], - allocation: currentAllocation.map((row) => [...row]), - need: currentNeed.map((row) => [...row]), - available: [...currentAvailable], - }); - }); - - setStepStates(states); - } - } - - setIsProcessingRequest(false); - }, 500); - }, - [algorithmState, calculator, showSuccess, showError], - ); - // Setup swipe gestures for mobile sidebar const { attachToElement } = useSwipeGesture({ onSwipeRight: () => { @@ -761,13 +153,8 @@ export default function BankersAlgorithmPage() { setIsScrolled(scrollTop > 10); }; - // Check on mount and when content changes checkScrollable(); - - // Check on scroll mainContent.addEventListener("scroll", checkScrollable); - - // Check on resize const resizeObserver = new ResizeObserver(checkScrollable); resizeObserver.observe(mainContent); @@ -795,11 +182,7 @@ export default function BankersAlgorithmPage() { return ( <> {/* Browser Compatibility Warning */} - { - /* Browser warning dismissed */ - }} - /> + {}} /> {/* Toast Notifications */} @@ -842,7 +225,7 @@ export default function BankersAlgorithmPage() { className="flex items-center justify-center cursor-pointer hover:scale-105 transition-transform duration-200" onClick={(e) => { e.stopPropagation(); - resetAlgorithm(); + handleReset(); }} > @@ -851,7 +234,7 @@ export default function BankersAlgorithmPage() { - {/* Mobile System Controls - Scrollable Content */} + {/* Mobile System Controls */}
- {/* Mobile Sidebar Footer - Always at bottom */} + {/* Mobile Sidebar Footer */}
@@ -945,7 +328,7 @@ export default function BankersAlgorithmPage() { className="flex items-center justify-center cursor-pointer hover:scale-105 transition-transform duration-200" onClick={(e) => { e.stopPropagation(); - resetAlgorithm(); + handleReset(); }} > @@ -954,7 +337,7 @@ export default function BankersAlgorithmPage() {
- {/* Desktop System Controls - Scrollable Content */} + {/* Desktop System Controls */}
- {/* Sidebar Footer - Always at bottom */} + {/* Sidebar Footer */}
{!isDesktopSidebarCollapsed ? (
@@ -1114,7 +497,7 @@ export default function BankersAlgorithmPage() {
diff --git a/frontend/src/components/bankers-algorithm/AlgorithmTable.tsx b/frontend/src/components/bankers-algorithm/AlgorithmTable.tsx index d8363b0..4fc79ba 100644 --- a/frontend/src/components/bankers-algorithm/AlgorithmTable.tsx +++ b/frontend/src/components/bankers-algorithm/AlgorithmTable.tsx @@ -533,3 +533,24 @@ export const AlgorithmTable: React.FC = ({
); }; + +// Memoized export to prevent unnecessary re-renders +// Custom comparison ensures references are compared correctly +export default React.memo(AlgorithmTable, (prevProps, nextProps) => { + return ( + prevProps.processCount === nextProps.processCount && + prevProps.resourceCount === nextProps.resourceCount && + prevProps.isCalculating === nextProps.isCalculating && + prevProps.isProcessingRequest === nextProps.isProcessingRequest && + prevProps.currentStepIndex === nextProps.currentStepIndex && + prevProps.onAllocationChange === nextProps.onAllocationChange && + prevProps.onMaxChange === nextProps.onMaxChange && + // Deep comparison for arrays (matrices change less frequently) + JSON.stringify(prevProps.allocation) === + JSON.stringify(nextProps.allocation) && + JSON.stringify(prevProps.max) === JSON.stringify(nextProps.max) && + JSON.stringify(prevProps.need) === JSON.stringify(nextProps.need) && + JSON.stringify(prevProps.finish) === JSON.stringify(nextProps.finish) && + prevProps.algorithmSteps.length === nextProps.algorithmSteps.length + ); +}); diff --git a/frontend/src/components/bankers-algorithm/RequestPanel.tsx b/frontend/src/components/bankers-algorithm/RequestPanel.tsx index 5024685..a60a984 100644 --- a/frontend/src/components/bankers-algorithm/RequestPanel.tsx +++ b/frontend/src/components/bankers-algorithm/RequestPanel.tsx @@ -18,8 +18,6 @@ interface RequestPanelProps { export const RequestPanel: React.FC = ({ processCount, resourceCount, - need: _need, - available: _available, onRequestSubmit, isProcessing, disabled = false, diff --git a/frontend/src/components/bankers-algorithm/index.ts b/frontend/src/components/bankers-algorithm/index.ts index 666249d..05e548d 100644 --- a/frontend/src/components/bankers-algorithm/index.ts +++ b/frontend/src/components/bankers-algorithm/index.ts @@ -3,7 +3,7 @@ export { CountControl } from "./CountControl"; export { ProcessControl } from "./ProcessControl"; export { ResourceControl } from "./ResourceControl"; export { AvailableResourcesInput } from "./AvailableResourcesInput"; -export { AlgorithmTable } from "./AlgorithmTable"; +export { default as AlgorithmTable } from "./AlgorithmTable"; export { RequestPanel } from "./RequestPanel"; export { StepByStepResults } from "./StepByStepResults"; export { AnimatedFinishBadge } from "./AnimatedFinishBadge"; diff --git a/frontend/src/hooks/__tests__/useBankersAlgorithm.test.ts b/frontend/src/hooks/__tests__/useBankersAlgorithm.test.ts new file mode 100644 index 0000000..2a241c6 --- /dev/null +++ b/frontend/src/hooks/__tests__/useBankersAlgorithm.test.ts @@ -0,0 +1,483 @@ +/** + * useBankersAlgorithm Hook Tests + * + * Comprehensive test suite to verify all functionality extracted from page.tsx + * Tests ensure that refactored logic maintains identical behavior. + * + * Run: bun test src/hooks/__tests__/useBankersAlgorithm.test.ts + */ + +import { describe, test, expect, beforeEach } from "bun:test"; +import { BankersAlgorithmCalculator } from "@/lib/bankers-algorithm-calculator"; +import { calculateNeedMatrix } from "@/utils/matrix-utils"; +import { ResourceRequest } from "@/types/bankers-algorithm"; + +/** + * Since useBankersAlgorithm is a React hook and requires React context, + * we test the underlying logic functions directly. + * This validates that the extracted logic works correctly. + */ +describe("useBankersAlgorithm - Logic Validation", () => { + let calculator: BankersAlgorithmCalculator; + + beforeEach(() => { + calculator = new BankersAlgorithmCalculator(); + }); + + describe("State Initialization", () => { + test("should create default state with valid structure", () => { + const state = calculator.createDefaultState(); + + expect(state.processCount).toBeGreaterThan(0); + expect(state.resourceCount).toBeGreaterThan(0); + expect(state.allocation.length).toBe(state.processCount); + expect(state.max.length).toBe(state.processCount); + expect(state.available.length).toBe(state.resourceCount); + expect(state.need.length).toBe(state.processCount); + expect(state.finish.length).toBe(state.processCount); + expect(state.isCalculating).toBe(false); + }); + + test("should create fresh state with all zeros", () => { + const state = calculator.createFreshState(); + + expect( + state.allocation.every((row) => row.every((val) => val === 0)), + ).toBe(true); + expect(state.max.every((row) => row.every((val) => val === 0))).toBe( + true, + ); + expect(state.available.every((val) => val === 0)).toBe(true); + }); + }); + + describe("Matrix Update Operations", () => { + test("updateAllocation should update value and recalculate need", () => { + const state = calculator.createDefaultState(); + const originalAllocation = state.allocation[0][0]; + const newValue = originalAllocation + 1; + + // Simulate updateAllocation logic + const newAllocation = state.allocation.map((row) => [...row]); + newAllocation[0][0] = Math.max(0, newValue); + const newNeed = calculateNeedMatrix(state.max, newAllocation); + + expect(newAllocation[0][0]).toBe(newValue); + expect(newNeed[0][0]).toBe(state.max[0][0] - newValue); + }); + + test("updateMax should update value and recalculate need", () => { + const state = calculator.createDefaultState(); + const newMaxValue = 10; + + // Simulate updateMax logic + const newMax = state.max.map((row) => [...row]); + newMax[0][0] = Math.max(0, newMaxValue); + const newNeed = calculateNeedMatrix(newMax, state.allocation); + + expect(newMax[0][0]).toBe(newMaxValue); + expect(newNeed[0][0]).toBe(newMaxValue - state.allocation[0][0]); + }); + + test("updateAvailable should update value with non-negative constraint", () => { + const state = calculator.createDefaultState(); + + // Test positive value + const newAvailable = [...state.available]; + newAvailable[0] = Math.max(0, 5); + expect(newAvailable[0]).toBe(5); + + // Test negative value should be clamped to 0 + newAvailable[1] = Math.max(0, -3); + expect(newAvailable[1]).toBe(0); + }); + }); + + describe("Count Update Operations", () => { + test("updateProcessCount should resize matrices correctly", () => { + const state = calculator.createDefaultState(); + const newProcessCount = 5; + + const newState = calculator.resizeMatrices( + state, + newProcessCount, + state.resourceCount, + ); + + expect(newState.processCount).toBe(newProcessCount); + expect(newState.allocation.length).toBe(newProcessCount); + expect(newState.max.length).toBe(newProcessCount); + expect(newState.need.length).toBe(newProcessCount); + expect(newState.finish.length).toBe(newProcessCount); + }); + + test("updateResourceCount should resize matrices correctly", () => { + const state = calculator.createDefaultState(); + const newResourceCount = 5; + + const newState = calculator.resizeMatrices( + state, + state.processCount, + newResourceCount, + ); + + expect(newState.resourceCount).toBe(newResourceCount); + expect(newState.allocation[0].length).toBe(newResourceCount); + expect(newState.max[0].length).toBe(newResourceCount); + expect(newState.available.length).toBe(newResourceCount); + }); + + test("should clamp process count to valid range (1-10)", () => { + const clampCount = ( + value: number, + limits: { min: number; max: number }, + ) => Math.max(limits.min, Math.min(limits.max, value)); + + expect(clampCount(0, { min: 1, max: 10 })).toBe(1); + expect(clampCount(15, { min: 1, max: 10 })).toBe(10); + expect(clampCount(5, { min: 1, max: 10 })).toBe(5); + }); + + test("should preserve existing data when resizing", () => { + const state = calculator.createDefaultState(); + const originalValue = state.allocation[0][0]; + + const newState = calculator.resizeMatrices( + state, + state.processCount + 2, + state.resourceCount + 2, + ); + + expect(newState.allocation[0][0]).toBe(originalValue); + }); + }); + + describe("Safety Check Operations", () => { + test("checkSafety should identify safe state", () => { + const state = calculator.createDefaultState(); + const result = calculator.checkSafety( + state.available, + state.allocation, + state.need, + ); + + expect(result.isSafe).toBe(true); + expect(result.safeSequence.length).toBeGreaterThan(0); + expect(result.steps.length).toBeGreaterThan(0); + }); + + test("checkSafety should identify unsafe state", () => { + const state = calculator.createDefaultState(); + state.available = [0, 0, 0]; // No resources available + + const result = calculator.checkSafety( + state.available, + state.allocation, + state.need, + ); + + expect(result.isSafe).toBe(false); + expect(result.safeSequence).toEqual([]); + }); + + test("checkSafety should validate system data before running", () => { + const state = calculator.createDefaultState(); + state.allocation[0][0] = -1; // Invalid negative value + + const errors = calculator.validateSystemData(state); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.message.includes("non-negative"))).toBe(true); + }); + }); + + describe("Resource Request Operations", () => { + test("processRequest should grant safe request", () => { + const state = calculator.createDefaultState(); + const request: ResourceRequest = { + processId: 0, + requestVector: [1, 0, 0], + }; + + const result = calculator.processRequest(request, state); + + expect(result.canGrant).toBe(true); + expect(result.newState).toBeDefined(); + expect(result.simulationSteps).toBeDefined(); + }); + + test("processRequest should deny request exceeding need", () => { + const state = calculator.createDefaultState(); + const request: ResourceRequest = { + processId: 0, + requestVector: [999, 999, 999], + }; + + const result = calculator.processRequest(request, state); + + expect(result.canGrant).toBe(false); + expect(result.errorMessage).toContain("exceeds declared maximum"); + }); + + test("processRequest should deny request exceeding available", () => { + const state = calculator.createDefaultState(); + state.available = [0, 0, 0]; + // Set a small need so request passes need check but fails availability + state.need[0] = [1, 1, 1]; + + const request: ResourceRequest = { + processId: 0, + requestVector: [1, 0, 0], + }; + + const result = calculator.processRequest(request, state); + + expect(result.canGrant).toBe(false); + expect(result.errorMessage).toContain("Insufficient resources"); + }); + }); + + describe("Reset and Load Operations", () => { + test("resetAlgorithm should preserve process and resource counts", () => { + const state = calculator.createDefaultState(); + + // Resize first + const resized = calculator.resizeMatrices(state, 5, 4); + + // Then reset + const freshState = calculator.createFreshState(); + const resetState = calculator.resizeMatrices( + freshState, + resized.processCount, + resized.resourceCount, + ); + + expect(resetState.processCount).toBe(5); + expect(resetState.resourceCount).toBe(4); + expect( + resetState.allocation.every((row) => row.every((v) => v === 0)), + ).toBe(true); + }); + + test("loadDefaultExample should restore default state", () => { + const defaultState = calculator.createDefaultState(); + + expect(defaultState.processCount).toBe(2); + expect(defaultState.resourceCount).toBe(3); + expect(defaultState.isSafe).toBe(true); + }); + }); + + describe("Process Completion Operations", () => { + test("completeProcess should release resources when need is zero", () => { + const state = calculator.createDefaultState(); + state.need[0] = [0, 0, 0]; // Process 0 has completed its task + + const newState = calculator.completeProcess(state, 0); + + expect(newState.finish[0]).toBe(true); + expect(newState.allocation[0]).toEqual([0, 0, 0]); + }); + + test("completeProcess should not complete process with remaining need", () => { + const state = calculator.createDefaultState(); + // Process 0 still has need + + const newState = calculator.completeProcess(state, 0); + + expect(newState).toEqual(state); // No change + }); + + test("completeProcess should throw for invalid process ID", () => { + const state = calculator.createDefaultState(); + + expect(() => calculator.completeProcess(state, 999)).toThrow( + "Invalid process ID", + ); + }); + }); + + describe("Step State Building", () => { + test("should correctly track work vector through steps", () => { + const state = calculator.createDefaultState(); + const result = calculator.checkSafety( + state.available, + state.allocation, + state.need, + ); + + // First step should have work = available + expect(result.steps[0].workVector).toEqual(state.available); + + // Each subsequent step should have valid work vector + result.steps.forEach((step) => { + if (step.workVector) { + expect(step.workVector.length).toBe(state.resourceCount); + expect(step.workVector.every((v) => v >= 0)).toBe(true); + } + }); + }); + + test("should correctly track finish state through steps", () => { + const state = calculator.createDefaultState(); + const result = calculator.checkSafety( + state.available, + state.allocation, + state.need, + ); + + // Final finish state should match result + expect(result.finalFinishState.length).toBe(state.processCount); + + if (result.isSafe) { + expect(result.finalFinishState.every((f) => f === true)).toBe(true); + } + }); + }); + + describe("Data Immutability", () => { + test("should not mutate original state on matrix update", () => { + const state = calculator.createDefaultState(); + const originalAllocation = JSON.stringify(state.allocation); + + // Simulate update + const newAllocation = state.allocation.map((row) => [...row]); + newAllocation[0][0] = 999; + + expect(JSON.stringify(state.allocation)).toBe(originalAllocation); + }); + + test("should not mutate original state on resize", () => { + const state = calculator.createDefaultState(); + const originalProcessCount = state.processCount; + + calculator.resizeMatrices(state, 10, 10); + + expect(state.processCount).toBe(originalProcessCount); + }); + }); + + describe("Edge Cases", () => { + test("should handle single process system", () => { + const state = calculator.createDefaultState(); + const singleProcessState = calculator.resizeMatrices(state, 1, 3); + + const result = calculator.checkSafety( + singleProcessState.available, + singleProcessState.allocation, + singleProcessState.need, + ); + + expect(result.steps.length).toBeGreaterThan(0); + }); + + test("should handle single resource system", () => { + const state = calculator.createDefaultState(); + const singleResourceState = calculator.resizeMatrices(state, 2, 1); + + const result = calculator.checkSafety( + singleResourceState.available, + singleResourceState.allocation, + singleResourceState.need, + ); + + expect(result.steps.length).toBeGreaterThan(0); + }); + + test("should handle maximum allowed dimensions", () => { + const state = calculator.createDefaultState(); + const maxState = calculator.resizeMatrices(state, 10, 10); + + expect(maxState.processCount).toBe(10); + expect(maxState.resourceCount).toBe(10); + expect(maxState.allocation.length).toBe(10); + expect(maxState.allocation[0].length).toBe(10); + }); + + test("should handle zero request vector", () => { + const state = calculator.createDefaultState(); + const request: ResourceRequest = { + processId: 0, + requestVector: [0, 0, 0], + }; + + const result = calculator.processRequest(request, state); + + expect(result.canGrant).toBe(true); + }); + + test("should quickly detect unsafe state with zero available resources", () => { + const state = calculator.createDefaultState(); + state.available = [0, 0, 0]; + + const startTime = performance.now(); + const result = calculator.checkSafety( + state.available, + state.allocation, + state.need, + ); + const endTime = performance.now(); + + expect(result.isSafe).toBe(false); + expect(result.steps.length).toBe(1); // Early exit with single step + expect(result.steps[0].description).toContain("no available resources"); + expect(endTime - startTime).toBeLessThan(1); // Should be nearly instant + }); + }); +}); + +describe("Step Navigation Logic", () => { + let calculator: BankersAlgorithmCalculator; + + beforeEach(() => { + calculator = new BankersAlgorithmCalculator(); + }); + + test("should build correct step states for safety check", () => { + const state = calculator.createDefaultState(); + const result = calculator.checkSafety( + state.available, + state.allocation, + state.need, + ); + + // Build step states manually (extracted logic) + const states: Array<{ + work: number[]; + finish: boolean[]; + }> = []; + + let currentWork = [...state.available]; + let currentFinish = Array(state.processCount).fill(false); + + result.steps.forEach((step) => { + if (step.workVector && step.workVector.length > 0) { + currentWork = [...step.workVector]; + } + if (step.processChecked && step.canFinish) { + const processIndex = parseInt(step.processChecked.replace("P", "")); + if (!isNaN(processIndex)) { + currentFinish = [...currentFinish]; + currentFinish[processIndex] = true; + } + } + states.push({ + work: [...currentWork], + finish: [...currentFinish], + }); + }); + + expect(states.length).toBe(result.steps.length); + expect(states[0].work).toEqual(state.available); + }); + + test("should restore original state when exiting navigation", () => { + const state = calculator.createDefaultState(); + const originalAvailable = [...state.available]; + const originalAllocation = state.allocation.map((row) => [...row]); + + // Simulate navigation exit - should restore original + expect(originalAvailable).toEqual(state.available); + expect(originalAllocation).toEqual(state.allocation); + }); +}); diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index b7ebb4b..eaf25eb 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -8,3 +8,10 @@ export { useKeyboardShortcuts } from "./useKeyboardShortcuts"; export { useSwipeGesture } from "./useSwipeGesture"; export { useAppLoading } from "./useAppLoading"; export { useMediaQuery } from "./useMediaQuery"; +export { useBankersAlgorithm } from "./useBankersAlgorithm"; +export type { + UseBankersAlgorithmReturn, + UseBankersAlgorithmOptions, + RequestResultState, + StepNavigationState, +} from "./useBankersAlgorithm"; diff --git a/frontend/src/hooks/useBankersAlgorithm.ts b/frontend/src/hooks/useBankersAlgorithm.ts new file mode 100644 index 0000000..59ce9bf --- /dev/null +++ b/frontend/src/hooks/useBankersAlgorithm.ts @@ -0,0 +1,673 @@ +/** + * useBankersAlgorithm Hook + * + * Consolidates all Banker's Algorithm state and operations. + * Extracted from page.tsx to follow Single Responsibility Principle. + * + * Responsibilities: + * - Algorithm state management (allocation, max, available, need matrices) + * - Safety checking + * - Resource request processing + * - Process/resource count management + * - Matrix value updates + * + * @module hooks/useBankersAlgorithm + */ + +import { useState, useCallback, useMemo, useRef, useEffect } from "react"; +import { + BankersAlgorithmState, + ResourceRequest, +} from "@/types/bankers-algorithm"; +import { BankersAlgorithmCalculator } from "@/lib/bankers-algorithm-calculator"; +import { calculateNeedMatrix } from "@/utils/matrix-utils"; + +export interface UseBankersAlgorithmReturn { + algorithmState: BankersAlgorithmState; + calculator: BankersAlgorithmCalculator; + isProcessingRequest: boolean; + requestResult: RequestResultState; + stepNavigationState: StepNavigationState; + + // Actions + checkSafety: () => void; + processResourceRequest: (request: ResourceRequest) => void; + updateAllocation: ( + processIndex: number, + resourceIndex: number, + value: number, + ) => void; + updateMax: ( + processIndex: number, + resourceIndex: number, + value: number, + ) => void; + updateAvailable: (index: number, value: number) => void; + updateProcessCount: (newCount: number) => void; + updateResourceCount: (newCount: number) => void; + resetAlgorithm: () => void; + loadDefaultExample: () => void; + completeProcess: (processId: number) => void; + handleStepChange: (stepIndex: number | undefined) => void; + setRequestResult: React.Dispatch>; +} + +export interface RequestResultState { + isRequest: boolean; + wasGranted?: boolean; + processId?: number; + requestVector?: number[]; + shouldResetRequest?: boolean; +} + +export interface StepNavigationState { + currentStepIndex: number | undefined; + stepStates: Array<{ + work: number[]; + finish: boolean[]; + allocation: number[][]; + need: number[][]; + available?: number[]; + }>; + originalStateBeforeSteps: { + available: number[]; + allocation: number[][]; + need: number[][]; + finish: boolean[]; + } | null; +} + +export interface UseBankersAlgorithmOptions { + onSuccess?: (title: string, message: string, duration?: number) => void; + onError?: (title: string, message: string, duration?: number) => void; + autoPreviewOnMount?: boolean; +} + +const PROCESS_COUNT_LIMITS = { min: 1, max: 10 } as const; +const RESOURCE_COUNT_LIMITS = { min: 1, max: 10 } as const; + +export function useBankersAlgorithm( + options: UseBankersAlgorithmOptions = {}, +): UseBankersAlgorithmReturn { + const { onSuccess, onError, autoPreviewOnMount = true } = options; + + // Memoized calculator instance + const calculator = useMemo(() => new BankersAlgorithmCalculator(), []); + + // Core algorithm state + const [algorithmState, setAlgorithmState] = useState( + () => calculator.createDefaultState(), + ); + + // Request processing state + const [isProcessingRequest, setIsProcessingRequest] = useState(false); + const [requestResult, setRequestResult] = useState({ + isRequest: false, + }); + + // Step navigation state + const [currentStepIndex, setCurrentStepIndex] = useState( + undefined, + ); + const [stepStates, setStepStates] = useState< + Array<{ + work: number[]; + finish: boolean[]; + allocation: number[][]; + need: number[][]; + available?: number[]; + }> + >([]); + const [originalStateBeforeSteps, setOriginalStateBeforeSteps] = useState<{ + available: number[]; + allocation: number[][]; + need: number[][]; + finish: boolean[]; + } | null>(null); + + // Track initial preview + const hasShownInitialPreview = useRef(false); + + /** + * Clamps a count value within allowed limits + */ + const clampCount = useCallback( + (value: number, limits: { min: number; max: number }) => + Math.max(limits.min, Math.min(limits.max, value)), + [], + ); + + /** + * Builds step states for navigation from algorithm steps + * Optimized: Uses references for read-only data to reduce memory allocations + */ + const buildStepStates = useCallback( + ( + steps: BankersAlgorithmState["algorithmSteps"], + initialState: { + available: number[]; + allocation: number[][]; + need: number[][]; + finish: boolean[]; + processCount: number; + }, + newState?: BankersAlgorithmState, + isGrantedRequest?: boolean, + ) => { + const states: Array<{ + work: number[]; + finish: boolean[]; + allocation: number[][]; + need: number[][]; + available?: number[]; + }> = []; + + // Use shallow copies for tracking, deep clone only when state changes + let currentWork = initialState.available; + let currentFinish = new Array(initialState.processCount).fill(false); + let currentAllocation = initialState.allocation; + let currentNeed = initialState.need; + let currentAvailable = initialState.available; + + steps.forEach((step) => { + // Only clone when allocation actually changes (granted request) + if (step.description.includes("Temporarily allocate resources")) { + if (isGrantedRequest && newState) { + currentAvailable = newState.available; + currentAllocation = newState.allocation; + currentNeed = newState.need; + } + } + + // Update work vector when it changes + if (step.workVector && step.workVector.length > 0) { + currentWork = step.workVector; + } + + // Update finish state when process completes + if (step.processChecked && step.canFinish) { + const processIndex = parseInt(step.processChecked.replace("P", "")); + if (!isNaN(processIndex)) { + // Clone finish array only when it changes + currentFinish = [...currentFinish]; + currentFinish[processIndex] = true; + } + } + + // Store references (read-only for UI display) + states.push({ + work: currentWork, + finish: currentFinish, + allocation: currentAllocation, + need: currentNeed, + available: currentAvailable, + }); + }); + + return states; + }, + [], + ); + + /** + * Updates allocation matrix value + */ + const updateAllocation = useCallback( + (processIndex: number, resourceIndex: number, value: number) => { + setAlgorithmState((prev) => { + const newAllocation = prev.allocation.map((row) => [...row]); + newAllocation[processIndex][resourceIndex] = Math.max(0, value); + const newNeed = calculateNeedMatrix(prev.max, newAllocation); + + return { + ...prev, + allocation: newAllocation, + need: newNeed, + }; + }); + }, + [], + ); + + /** + * Updates max matrix value + */ + const updateMax = useCallback( + (processIndex: number, resourceIndex: number, value: number) => { + setAlgorithmState((prev) => { + const newMax = prev.max.map((row) => [...row]); + newMax[processIndex][resourceIndex] = Math.max(0, value); + const newNeed = calculateNeedMatrix(newMax, prev.allocation); + + return { + ...prev, + max: newMax, + need: newNeed, + }; + }); + }, + [], + ); + + /** + * Updates available resources + */ + const updateAvailable = useCallback((index: number, value: number) => { + setAlgorithmState((prev) => { + const newAvailable = [...prev.available]; + newAvailable[index] = Math.max(0, value); + return { ...prev, available: newAvailable }; + }); + }, []); + + /** + * Updates process count with matrix resizing + */ + const updateProcessCount = useCallback( + (newCount: number) => { + const validCount = clampCount(newCount, PROCESS_COUNT_LIMITS); + setAlgorithmState((prev) => + calculator.resizeMatrices(prev, validCount, prev.resourceCount), + ); + }, + [calculator, clampCount], + ); + + /** + * Updates resource count with matrix resizing + */ + const updateResourceCount = useCallback( + (newCount: number) => { + const validCount = clampCount(newCount, RESOURCE_COUNT_LIMITS); + setAlgorithmState((prev) => + calculator.resizeMatrices(prev, prev.processCount, validCount), + ); + }, + [calculator, clampCount], + ); + + /** + * Resets algorithm to empty state while preserving counts + */ + const resetAlgorithm = useCallback(() => { + setAlgorithmState((prev) => { + const freshState = calculator.createFreshState(); + return calculator.resizeMatrices( + freshState, + prev.processCount, + prev.resourceCount, + ); + }); + setCurrentStepIndex(undefined); + setStepStates([]); + setRequestResult({ isRequest: false }); + onSuccess?.( + "System Reset", + "Matrix values have been reset while preserving process and resource counts.", + 3000, + ); + }, [calculator, onSuccess]); + + /** + * Loads default example data + */ + const loadDefaultExample = useCallback(() => { + const defaultState = calculator.createDefaultState(); + setAlgorithmState(defaultState); + setCurrentStepIndex(undefined); + setStepStates([]); + onSuccess?.( + "Example Loaded", + "Classical Banker's Algorithm example has been loaded successfully.", + 4000, + ); + }, [calculator, onSuccess]); + + /** + * Completes a process and releases its resources + */ + const completeProcess = useCallback( + (processId: number) => { + try { + const newState = calculator.completeProcess(algorithmState, processId); + + if (newState !== algorithmState) { + setAlgorithmState({ + ...newState, + lastUpdated: new Date(), + }); + onSuccess?.( + "Process Completed", + `Process P${processId} has completed execution and released all resources.`, + 5000, + ); + } else { + onError?.( + "Cannot Complete Process", + `Process P${processId} cannot be completed yet. It still has unfinished tasks.`, + 5000, + ); + } + } catch (error) { + onError?.( + "Process Completion Error", + error instanceof Error ? error.message : "Failed to complete process", + 5000, + ); + } + }, + [algorithmState, calculator, onSuccess, onError], + ); + + /** + * Handles step navigation changes + * Optimized: Direct assignment for display (read-only in UI) + */ + const handleStepChange = useCallback( + (stepIndex: number | undefined) => { + setCurrentStepIndex(stepIndex); + + if (stepIndex === undefined) { + // Restore original state when exiting navigation + if (originalStateBeforeSteps) { + setAlgorithmState((prev) => ({ + ...prev, + available: originalStateBeforeSteps.available, + allocation: originalStateBeforeSteps.allocation, + need: originalStateBeforeSteps.need, + finish: originalStateBeforeSteps.finish, + })); + } + return; + } + + // Update UI to reflect state at this step (read-only references) + if (stepStates[stepIndex]) { + const stepState = stepStates[stepIndex]; + setAlgorithmState((prev) => ({ + ...prev, + available: stepState.available || stepState.work, + allocation: stepState.allocation, + need: stepState.need, + finish: stepState.finish, + })); + } + }, + [stepStates, originalStateBeforeSteps], + ); + + /** + * Checks system safety and updates state with results + */ + const checkSafety = useCallback(() => { + // Validate system state + const validationErrors = calculator.validateSystemData(algorithmState); + if (validationErrors.length > 0) { + onError?.( + "System Validation Failed", + `Please fix the following issues: ${validationErrors + .map((e) => e.message) + .join(", ")}`, + 8000, + ); + return; + } + + // Clear previous state + setAlgorithmState((prev) => ({ + ...prev, + isCalculating: true, + algorithmSteps: [], + safeSequence: [], + finish: Array(prev.processCount).fill(false), + isSafe: undefined, + })); + setRequestResult({ isRequest: false }); + setCurrentStepIndex(undefined); + setStepStates([]); + + // Run safety check with delay for UI feedback + setTimeout(() => { + const safetyResult = calculator.checkSafety( + algorithmState.available, + algorithmState.allocation, + algorithmState.need, + ); + + // Save original state for navigation + setOriginalStateBeforeSteps({ + available: [...algorithmState.available], + allocation: algorithmState.allocation.map((row) => [...row]), + need: algorithmState.need.map((row) => [...row]), + finish: [...algorithmState.finish], + }); + + setAlgorithmState((prev) => ({ + ...prev, + finish: safetyResult.finalFinishState, + safeSequence: safetyResult.safeSequence, + algorithmSteps: safetyResult.steps, + isSafe: safetyResult.isSafe, + isCalculating: false, + lastUpdated: new Date(), + })); + + // Build step states + const states = buildStepStates(safetyResult.steps, { + available: algorithmState.available, + allocation: algorithmState.allocation, + need: algorithmState.need, + finish: algorithmState.finish, + processCount: algorithmState.processCount, + }); + setStepStates(states); + + // Show result notification + if (safetyResult.isSafe) { + onSuccess?.( + "System is Safe", + `Safe execution sequence found: ${safetyResult.safeSequence.join( + " → ", + )}`, + 6000, + ); + } else { + onError?.( + "System is Unsafe", + "The current system state could lead to deadlock. Please review resource allocation.", + 8000, + ); + } + }, 300); + }, [algorithmState, calculator, onSuccess, onError, buildStepStates]); + + /** + * Processes a resource request + */ + const processResourceRequest = useCallback( + (request: ResourceRequest) => { + setIsProcessingRequest(true); + + // Clear previous state + setAlgorithmState((prev) => ({ + ...prev, + algorithmSteps: [], + safeSequence: [], + finish: Array(prev.processCount).fill(false), + isSafe: undefined, + })); + setCurrentStepIndex(undefined); + setStepStates([]); + + setTimeout(() => { + const result = calculator.processRequest(request, algorithmState); + + if (result.canGrant && result.newState) { + // Request granted - update state + const enhancedSteps = [...(result.simulationSteps || [])]; + + // Enhance final step with grant message + if (enhancedSteps.length > 0) { + const lastStep = enhancedSteps[enhancedSteps.length - 1]; + if (lastStep.description.includes("System is SAFE")) { + lastStep.description += `\n\n[REQUEST GRANTED]: Process P${ + request.processId + } successfully allocated [${request.requestVector.join( + ", ", + )}] resources. The system remains in a safe state.`; + } + } + + setOriginalStateBeforeSteps({ + available: [...result.newState.available], + allocation: result.newState.allocation.map((row) => [...row]), + need: result.newState.need.map((row) => [...row]), + finish: [...result.newState.finish], + }); + + setAlgorithmState({ + ...result.newState, + algorithmSteps: enhancedSteps, + lastUpdated: new Date(), + }); + + const states = buildStepStates( + enhancedSteps, + { + available: algorithmState.available, + allocation: algorithmState.allocation, + need: algorithmState.need, + finish: algorithmState.finish, + processCount: algorithmState.processCount, + }, + result.newState, + true, + ); + setStepStates(states); + + setRequestResult({ + isRequest: true, + wasGranted: true, + processId: request.processId, + requestVector: request.requestVector, + shouldResetRequest: true, + }); + + onSuccess?.( + "Request Granted", + result.errorMessage || + `Process P${ + request.processId + } allocated [${request.requestVector.join( + ", ", + )}]. System remains safe.`, + 6000, + ); + } else { + // Request denied + const enhancedSteps = [...(result.simulationSteps || [])]; + + if (enhancedSteps.length > 0) { + const lastStep = enhancedSteps[enhancedSteps.length - 1]; + if (lastStep.description.includes("System is UNSAFE")) { + lastStep.description += `\n\n[REQUEST DENIED]: Process P${ + request.processId + } request [${request.requestVector.join( + ", ", + )}] cannot be granted as it would lead to an unsafe state (potential deadlock).`; + } + } + + setRequestResult({ + isRequest: true, + wasGranted: false, + processId: request.processId, + requestVector: request.requestVector, + }); + + onError?.( + "Request Denied", + result.errorMessage || + `Process P${ + request.processId + } request [${request.requestVector.join( + ", ", + )}] cannot be granted.`, + 8000, + ); + + if (result.simulationSteps) { + setOriginalStateBeforeSteps({ + available: [...algorithmState.available], + allocation: algorithmState.allocation.map((row) => [...row]), + need: algorithmState.need.map((row) => [...row]), + finish: [...algorithmState.finish], + }); + + setAlgorithmState((prev) => ({ + ...prev, + algorithmSteps: enhancedSteps, + safeSequence: [], + finish: prev.finish.map(() => false), + isSafe: false, + lastUpdated: new Date(), + })); + + const states = buildStepStates( + enhancedSteps, + { + available: algorithmState.available, + allocation: algorithmState.allocation, + need: algorithmState.need, + finish: algorithmState.finish, + processCount: algorithmState.processCount, + }, + undefined, + false, + ); + setStepStates(states); + } + } + + setIsProcessingRequest(false); + }, 500); + }, + [algorithmState, calculator, onSuccess, onError, buildStepStates], + ); + + // Auto-preview on mount + useEffect(() => { + if (autoPreviewOnMount && !hasShownInitialPreview.current) { + const timer = setTimeout(() => { + checkSafety(); + hasShownInitialPreview.current = true; + }, 1000); + return () => clearTimeout(timer); + } + }, [autoPreviewOnMount, checkSafety]); + + return { + algorithmState, + calculator, + isProcessingRequest, + requestResult, + stepNavigationState: { + currentStepIndex, + stepStates, + originalStateBeforeSteps, + }, + + checkSafety, + processResourceRequest, + updateAllocation, + updateMax, + updateAvailable, + updateProcessCount, + updateResourceCount, + resetAlgorithm, + loadDefaultExample, + completeProcess, + handleStepChange, + setRequestResult, + }; +} diff --git a/frontend/src/lib/__tests__/bankers-algorithm-enhanced.test.ts b/frontend/src/lib/__tests__/bankers-algorithm-enhanced.test.ts deleted file mode 100644 index 9f958a5..0000000 --- a/frontend/src/lib/__tests__/bankers-algorithm-enhanced.test.ts +++ /dev/null @@ -1,676 +0,0 @@ -/** - * Enhanced Banker's Algorithm Tests - * Comprehensive test suite based on C++ implementation validation - * npm test -- --testPathPatterns=bankers-algorithm-enhanced.test.ts --verbose - * npm test -- --testPathPatterns=bankers-algorithm-enhanced.test.ts --testNamePattern="should show detailed steps for unsafe state" - */ - -import { describe, test, expect, beforeEach } from "bun:test"; - -import { BankersAlgorithmCalculator } from "../bankers-algorithm-calculator"; -import { - BankersAlgorithmState, - ResourceRequest, -} from "@/types/bankers-algorithm"; -import { calculateNeedMatrix } from "@/utils/matrix-utils"; - -describe("Enhanced Banker's Algorithm Calculator", () => { - let calculator: BankersAlgorithmCalculator; - - beforeEach(() => { - calculator = new BankersAlgorithmCalculator(); - }); - - describe("Safety Algorithm - Classical Examples", () => { - test("should correctly identify safe state with textbook example", () => { - // Classic safe state from operating systems textbooks (corrected) - const available = [3, 3, 2]; - const allocation = [ - [0, 1, 0], // P0 - [2, 0, 0], // P1 - [3, 0, 2], // P2 - ]; - const need = [ - [7, 4, 3], // P0 - needs more resources - [1, 2, 2], // P1 - can finish first - [6, 0, 0], // P2 - needs 6 of resource A, but only 5 available after P1 - ]; - - const result = calculator.checkSafety(available, allocation, need); - - // This should actually be unsafe because P2 needs 6 units of resource A - // but only 5 will be available after P1 finishes - expect(result.isSafe).toBe(false); - expect(result.safeSequence).toEqual([]); - expect(result.steps.length).toBeGreaterThan(0); - - // Verify the algorithm steps are logical - const initStep = result.steps[0]; - expect(initStep.description).toContain("init: work = available"); - expect(initStep.workVector).toEqual([3, 3, 2]); - }); - - test("should correctly identify safe state with corrected example", () => { - // Use the default state which is known to be safe - const state = calculator.createDefaultState(); - const result = calculator.checkSafety( - state.available, - state.allocation, - state.need, - ); - - expect(result.isSafe).toBe(true); - expect(result.safeSequence.length).toBeGreaterThan(0); - expect(result.steps.length).toBeGreaterThan(0); - }); - - test("should correctly identify unsafe state", () => { - // Modified example that creates unsafe state - const available = [1, 0, 0]; // Very limited resources - const allocation = [ - [0, 1, 0], - [2, 0, 0], - [3, 0, 2], - ]; - const need = [ - [7, 4, 3], - [1, 2, 2], - [6, 0, 0], - ]; - - const result = calculator.checkSafety(available, allocation, need); - - expect(result.isSafe).toBe(false); - expect(result.safeSequence).toEqual([]); - - // Should have steps showing why it's unsafe - const finalStep = result.steps[result.steps.length - 1]; - expect(finalStep.description).toContain("UNSAFE"); - }); - - test("should show detailed steps for unsafe state - matching user example", () => { - // User's exact example: Processes: 2, Resource: 3, Available = (0, 0, 0) - const available = [0, 0, 0]; - const allocation = [ - [1, 0, 0], // P0 - [0, 1, 0], // P1 - [0, 0, 1], // P2 - ]; - const max = [ - [1, 1, 0], // P0 - [0, 1, 1], // P1 - [1, 0, 1], // P2 - ]; - const need = calculateNeedMatrix(max, allocation); - - const result = calculator.checkSafety(available, allocation, need); - - expect(result.isSafe).toBe(false); - expect(result.safeSequence).toEqual([]); - expect(result.steps.length).toBeGreaterThan(0); - - // Should have initialization step - const initStep = result.steps.find((step) => step.stepNumber === 1); - expect(initStep).toBeDefined(); - expect(initStep?.description).toContain("init: work = available"); - expect(initStep?.workVector).toEqual([0, 0, 0]); - - // Should have process checking steps showing why each process cannot finish - const processCheckSteps = result.steps.filter( - (step) => step.stepNumber === 2, - ); - expect(processCheckSteps.length).toBeGreaterThan(0); - - // Should show that no processes can finish with available resources - processCheckSteps.forEach((step) => { - expect(step.description).toContain("need[P"); - expect(step.description).toContain("≤ work"); - expect(step.canFinish).toBe(false); - }); - - // Should have final step explaining why system is unsafe - const finalStep = result.steps[result.steps.length - 1]; - expect(finalStep.stepNumber).toBe(4); - expect(finalStep.description).toContain("System is UNSAFE"); - expect(finalStep.description).toContain("cannot finish"); - }); - - test("should handle edge case with single process", () => { - const available = [2, 1]; - const allocation = [[1, 0]]; - const need = [[1, 1]]; - - const result = calculator.checkSafety(available, allocation, need); - - expect(result.isSafe).toBe(true); - expect(result.safeSequence).toEqual(["P0"]); - }); - - test("should handle empty system", () => { - const available: number[] = []; - const allocation: number[][] = []; - const need: number[][] = []; - - const result = calculator.checkSafety(available, allocation, need); - - expect(result.isSafe).toBe(true); - expect(result.safeSequence).toEqual([]); - }); - }); - - describe("Resource Request Processing - Enhanced Validation", () => { - let safeSystemState: BankersAlgorithmState; - - beforeEach(() => { - // Create a proper safe system state using the calculator - const calculator = new BankersAlgorithmCalculator(); - safeSystemState = calculator.createDefaultState(); - }); - - test("should grant safe resource request", () => { - // Use a small request that should be safe - const request: ResourceRequest = { - processId: 0, - requestVector: [1, 0, 0], // Small request for process 0 - }; - - const result = calculator.processRequest(request, safeSystemState); - - expect(result.canGrant).toBe(true); - expect(result.newState).toBeDefined(); - expect(result.errorMessage).toContain("GRANTED"); - expect(result.errorMessage).toContain("SAFE state"); - }); - - test("should deny request exceeding need with detailed message", () => { - const request: ResourceRequest = { - processId: 1, - requestVector: [2, 3, 3], // Exceeds need [1, 2, 2] - }; - - const result = calculator.processRequest(request, safeSystemState); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("exceeds declared maximum need"); - expect(result.errorMessage).toContain( - "cannot request more resources than it declared", - ); - expect(result.simulationSteps).toBeDefined(); - expect(result.simulationSteps!.length).toBeGreaterThan(0); - - // Should have step 1 that shows the comparison - const step1 = result.simulationSteps!.find( - (step) => step.stepNumber === 1, - ); - expect(step1).toBeDefined(); - expect(step1!.description).toContain("Check if Request"); - expect(step1!.canFinish).toBe(false); - }); - - test("should deny request exceeding available resources with detailed message", () => { - // Create a system state where request passes need check but fails availability check - const limitedSystemState: BankersAlgorithmState = { - processCount: 2, - resourceCount: 3, - allocation: [ - [0, 0, 0], // P0 - no current allocation - [1, 1, 1], // P1 - some allocation - ], - max: [ - [5, 5, 5], // P0 - high maximum need - [2, 2, 2], // P1 - lower maximum need - ], - available: [1, 1, 1], // Limited resources - need: [], // Will be calculated - finish: [false, false], - safeSequence: [], - algorithmSteps: [], - isCalculating: false, - }; - - // Calculate need matrix - limitedSystemState.need = calculateNeedMatrix( - limitedSystemState.max, - limitedSystemState.allocation, - ); - - const request: ResourceRequest = { - processId: 0, - requestVector: [2, 0, 0], // Request 2 units of resource A, but only 1 available - }; - - const result = calculator.processRequest(request, limitedSystemState); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("Insufficient resources available"); - expect(result.errorMessage).toContain( - "must wait until more resources become available", - ); - expect(result.simulationSteps).toBeDefined(); - - // Should have steps 1 and 2 - const step1 = result.simulationSteps!.find( - (step) => step.stepNumber === 1, - ); - const step2 = result.simulationSteps!.find( - (step) => step.stepNumber === 2, - ); - expect(step1).toBeDefined(); - expect(step2).toBeDefined(); - expect(step1!.canFinish).toBe(true); // Should pass need check - expect(step2!.canFinish).toBe(false); // Should fail availability check - }); - - test("should deny request leading to unsafe state with detailed explanation", () => { - // Create a system state that would become unsafe - const unsafeSystemState: BankersAlgorithmState = { - processCount: 3, - resourceCount: 3, - allocation: [ - [0, 1, 0], // P0 - [2, 0, 0], // P1 - [3, 0, 2], // P2 - ], - max: [ - [7, 5, 3], // P0 - [3, 2, 2], // P1 - [9, 0, 2], // P2 - ], - available: [3, 3, 2], - need: [], // Will be calculated - finish: [false, false, false], - safeSequence: [], - algorithmSteps: [], - isCalculating: false, - }; - - // Calculate need matrix - unsafeSystemState.need = calculateNeedMatrix( - unsafeSystemState.max, - unsafeSystemState.allocation, - ); - - const request: ResourceRequest = { - processId: 0, - requestVector: [3, 3, 2], // Request that would make system unsafe - }; - - const result = calculator.processRequest(request, unsafeSystemState); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("UNSAFE state"); - expect(result.errorMessage).toContain("potential deadlock"); - expect(result.errorMessage).toContain( - "cannot guarantee that all processes can complete", - ); - expect(result.simulationSteps).toBeDefined(); - - // Should have all 4 steps of Resource Request Algorithm - const steps = result.simulationSteps!; - expect(steps.some((step) => step.stepNumber === 1)).toBe(true); // Check Request <= Need - expect(steps.some((step) => step.stepNumber === 2)).toBe(true); // Check Request <= Available - expect(steps.some((step) => step.stepNumber === 3)).toBe(true); // Temporarily allocate - expect(steps.some((step) => step.stepNumber === 4)).toBe(true); // Run Safety Algorithm - }); - - test("should handle requests that exceed both need and available resources", () => { - const request: ResourceRequest = { - processId: 0, - requestVector: [999, 999, 999], // Exceeds everything - }; - - const result = calculator.processRequest(request, safeSystemState); - - expect(result.canGrant).toBe(false); - // Should fail at step 1 (exceeds need) before checking availability - expect(result.errorMessage).toContain("exceeds declared maximum need"); - expect(result.simulationSteps).toBeDefined(); - expect(result.simulationSteps!.length).toBe(1); // Should stop at step 1 - }); - - test("should provide detailed simulation steps for granted requests", () => { - const request: ResourceRequest = { - processId: 0, - requestVector: [1, 0, 0], // Small safe request - }; - - const result = calculator.processRequest(request, safeSystemState); - - if (result.canGrant) { - expect(result.simulationSteps).toBeDefined(); - expect(result.simulationSteps!.length).toBeGreaterThan(4); // Should include safety algorithm steps - expect(result.errorMessage).toContain("execution sequence"); - expect(result.newState).toBeDefined(); - - // Verify the new state has updated allocation and available resources - expect(result.newState!.allocation[0][0]).toBe( - safeSystemState.allocation[0][0] + 1, - ); - expect(result.newState!.available[0]).toBe( - safeSystemState.available[0] - 1, - ); - } - }); - - test("should handle zero request vector", () => { - const request: ResourceRequest = { - processId: 0, - requestVector: [0, 0, 0], // Zero request - }; - - const result = calculator.processRequest(request, safeSystemState); - - // Zero request should be granted (no resources needed) - expect(result.canGrant).toBe(true); - expect(result.newState).toBeDefined(); - expect(result.simulationSteps).toBeDefined(); - }); - - test("should handle invalid process ID", () => { - const request: ResourceRequest = { - processId: 999, // Invalid process ID - requestVector: [1, 0, 0], - }; - - const result = calculator.processRequest(request, safeSystemState); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("Invalid process ID"); - }); - - test("should handle malformed system state", () => { - const malformedState: BankersAlgorithmState = { - ...safeSystemState, - allocation: [], // Empty allocation matrix - }; - - const request: ResourceRequest = { - processId: 0, - requestVector: [1, 0, 0], - }; - - const result = calculator.processRequest(request, malformedState); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("Invalid system state"); - }); - }); - - describe("Process Completion Simulation", () => { - test("should complete process and release resources", () => { - const state = calculator.createDefaultState(); - - // Manually set a process to be ready for completion (need = 0) - state.need[1] = [0, 0, 0]; // P1 has completed its task - - const newState = calculator.completeProcess(state, 1); - - expect(newState.finish[1]).toBe(true); - expect(newState.allocation[1]).toEqual([0, 0, 0]); - - // Available resources should increase - expect(newState.available[0]).toBeGreaterThanOrEqual(state.available[0]); - }); - - test("should not complete process that hasn't finished its task", () => { - const state = calculator.createDefaultState(); - - const newState = calculator.completeProcess(state, 0); - - // Should remain unchanged since process hasn't completed its task - expect(newState.finish[0]).toBe(false); - expect(newState).toEqual(state); - }); - - test("should handle invalid process ID", () => { - const state = calculator.createDefaultState(); - - expect(() => { - calculator.completeProcess(state, 999); - }).toThrow("Invalid process ID"); - }); - }); - - describe("System Validation - Comprehensive Checks", () => { - test("should validate correct system state", () => { - const state = calculator.createDefaultState(); - const errors = calculator.validateSystemData(state); - - expect(errors).toHaveLength(0); - }); - - test("should detect negative values", () => { - const state = calculator.createDefaultState(); - state.allocation[0][0] = -1; - - const errors = calculator.validateSystemData(state); - - expect(errors.length).toBeGreaterThan(0); - expect(errors.some((e) => e.message.includes("non-negative"))).toBe(true); - }); - - test("should detect allocation exceeding maximum", () => { - const state = calculator.createDefaultState(); - state.allocation[0][0] = state.max[0][0] + 1; - - const errors = calculator.validateSystemData(state); - - expect(errors.length).toBeGreaterThan(0); - expect(errors.some((e) => e.message.includes("cannot exceed"))).toBe( - true, - ); - }); - - test("should detect dimension mismatches", () => { - const state = calculator.createDefaultState(); - state.allocation = [[1, 2]]; // Wrong dimensions - should have 3 columns - state.processCount = 1; // Adjust process count to match - - const errors = calculator.validateSystemData(state); - - expect(errors.length).toBeGreaterThan(0); - expect( - errors.some( - (e) => - e.message.includes("must have") || e.message.includes("dimensions"), - ), - ).toBe(true); - }); - }); - - describe("System Snapshot and Statistics", () => { - test("should generate comprehensive system snapshot", () => { - const state = calculator.createDefaultState(); - const snapshot = calculator.getSystemSnapshot(state); - - expect(snapshot.processCount).toBe(state.processCount); - expect(snapshot.resourceCount).toBe(state.resourceCount); - expect(snapshot.matrices.allocation).toEqual(state.allocation); - expect(snapshot.matrices.maximum).toEqual(state.max); - expect(snapshot.matrices.need).toEqual(state.need); - expect(snapshot.vectors.available).toEqual(state.available); - expect(snapshot.safetyInfo).toBeDefined(); - expect(snapshot.statistics).toBeDefined(); - }); - - test("should calculate resource utilization correctly", () => { - const state = calculator.createDefaultState(); - const snapshot = calculator.getSystemSnapshot(state); - - expect(snapshot.statistics.resourceUtilization).toBeDefined(); - expect(snapshot.statistics.resourceUtilization.length).toBe( - state.resourceCount, - ); - - // All utilization percentages should be between 0 and 100 - snapshot.statistics.resourceUtilization.forEach((util) => { - expect(util).toBeGreaterThanOrEqual(0); - expect(util).toBeLessThanOrEqual(100); - }); - }); - }); - - describe("Algorithm Step Numbering", () => { - test("should use correct step numbers for Safety Algorithm", () => { - // Create a simple safe state - const available = [1, 0, 3]; - const allocation = [ - [0, 5, 1], // P0 - [1, 0, 3], // P1 - ]; - const need = [ - [1, 5, 1], // P0 - [1, 0, 3], // P1 - ]; - - const result = calculator.checkSafety(available, allocation, need); - - expect(result.steps.length).toBeGreaterThan(0); - - // Check that step numbers follow the algorithm pattern - const stepNumbers = result.steps.map((step) => step.stepNumber); - - // Should start with step 1 (initialization) - expect(stepNumbers[0]).toBe(1); - expect(result.steps[0].description).toContain("init: work = available"); - - // Should have step 2 (process checks) - expect(stepNumbers.some((num) => num === 2)).toBe(true); - - // Should have step 3 (resource allocation) - expect(stepNumbers.some((num) => num === 3)).toBe(true); - - // Final step should be 4 for conclusion - expect(stepNumbers[stepNumbers.length - 1]).toBe(4); - // The test case might result in unsafe state, so check for either safe or unsafe conclusion - const finalDescription = - result.steps[result.steps.length - 1].description; - expect(finalDescription).toMatch( - /All processes can finish safely|System is UNSAFE/, - ); - }); - - test("should use correct step numbers for Resource Request Algorithm", () => { - const state = calculator.createDefaultState(); - - const request: ResourceRequest = { - processId: 0, - requestVector: [1, 0, 0], - }; - - const result = calculator.processRequest(request, state); - - if (result.simulationSteps) { - const stepNumbers = result.simulationSteps.map( - (step) => step.stepNumber, - ); - - // Should have steps 1, 2, 3, 4 for Resource Request Algorithm - expect(stepNumbers.includes(1)).toBe(true); // Check Request <= Need - expect(stepNumbers.includes(2)).toBe(true); // Check Request <= Available - expect(stepNumbers.includes(3)).toBe(true); // Temporarily allocate - expect(stepNumbers.includes(4)).toBe(true); // Run Safety Algorithm - - // Find the step descriptions - const step1 = result.simulationSteps.find( - (step) => step.stepNumber === 1, - ); - const step2 = result.simulationSteps.find( - (step) => step.stepNumber === 2, - ); - const step3 = result.simulationSteps.find( - (step) => step.stepNumber === 3, - ); - const step4 = result.simulationSteps.find( - (step) => step.stepNumber === 4, - ); - - expect(step1?.description).toContain("Check if Request"); - expect(step1?.description).toContain("Need"); - expect(step2?.description).toContain("Available"); - expect(step3?.description).toContain("Temporarily allocate"); - expect(step4?.description).toContain("Run Safety Algorithm"); - } - }); - - test("should format step descriptions correctly", () => { - const available = [1, 0, 3]; - const allocation = [[0, 5, 1]]; - const need = [[1, 0, 3]]; - - const result = calculator.checkSafety(available, allocation, need); - - // Check initialization step format - const initStep = result.steps.find((step) => step.stepNumber === 1); - expect(initStep?.description).toBe("init: work = available"); - expect(initStep?.workVector).toEqual([1, 0, 3]); - - // Check process check step format - const processCheckStep = result.steps.find( - (step) => step.stepNumber === 2, - ); - expect(processCheckStep?.description).toContain("need[P0] ≤ work"); - expect(processCheckStep?.description).toContain("(1, 0, 3) ≤ (1, 0, 3)"); - }); - }); - - describe("Safe Sequence Finding", () => { - test("should find correct safe sequence", () => { - const state = calculator.createDefaultState(); - - // First check if the default state is actually safe - const safetyResult = calculator.checkSafety( - state.available, - state.allocation, - state.need, - ); - - if (safetyResult.isSafe) { - const safeSequence = calculator.findSafeSequence(state); - expect(safeSequence.length).toBeGreaterThan(0); - expect(safeSequence.every((p) => p.startsWith("P"))).toBe(true); - } else { - // If default state is not safe, test with a known safe state - const safeState = { - ...state, - available: [5, 5, 5], // More resources to ensure safety - }; - const safeSequence = calculator.findSafeSequence(safeState); - expect(safeSequence.length).toBeGreaterThanOrEqual(0); - } - }); - - test("should return empty sequence for unsafe state", () => { - const state = calculator.createDefaultState(); - state.available = [0, 0, 0]; // Make it unsafe - - const safeSequence = calculator.findSafeSequence(state); - - expect(safeSequence).toEqual([]); - }); - }); - - describe("Matrix Resizing", () => { - test("should resize matrices correctly", () => { - const state = calculator.createDefaultState(); - const resized = calculator.resizeMatrices(state, 5, 4); - - expect(resized.processCount).toBe(5); - expect(resized.resourceCount).toBe(4); - expect(resized.allocation.length).toBe(5); - expect(resized.allocation[0].length).toBe(4); - expect(resized.max.length).toBe(5); - expect(resized.max[0].length).toBe(4); - expect(resized.available.length).toBe(4); - }); - - test("should preserve existing data when resizing", () => { - const state = calculator.createDefaultState(); - const originalValue = state.allocation[0][0]; - - const resized = calculator.resizeMatrices(state, 5, 4); - - expect(resized.allocation[0][0]).toBe(originalValue); - }); - }); -}); diff --git a/frontend/src/lib/__tests__/security.test.ts b/frontend/src/lib/__tests__/security.test.ts deleted file mode 100644 index 32a0927..0000000 --- a/frontend/src/lib/__tests__/security.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Security Tests for Banker's Algorithm - * Validates protection against OWASP Top 10 vulnerabilities - * - * Security Analysis for this Frontend Application: - * - XSS: React's JSX escapes values by default, no dangerouslySetInnerHTML - * - SQL Injection: N/A - No database in frontend - * - Command Injection: N/A - No shell exec in frontend - * - DDoS: Rate limiting should be handled at infrastructure level - * - Input Validation: Algorithm validates all matrix inputs - */ - -import { describe, test, expect } from "bun:test"; -import { BankersAlgorithmCalculator } from "../bankers-algorithm-calculator"; -import { - validateMatrixValues, - validateVectorValues, - isVectorLessOrEqual, - calculateNeedMatrix, -} from "@/utils/matrix-utils"; - -describe("Security Tests - Input Validation", () => { - const calculator = new BankersAlgorithmCalculator(); - - describe("Injection Prevention (Matrix Input Validation)", () => { - test("should reject negative values in allocation matrix", () => { - const state = calculator.createDefaultState(); - state.allocation[0][0] = -1; - - const errors = calculator.validateSystemData(state); - expect(errors.length).toBeGreaterThan(0); - expect(errors.some((e) => e.message.includes("non-negative"))).toBe(true); - }); - - test("should reject NaN values", () => { - const errors = validateMatrixValues([ - [NaN, 1], - [2, 3], - ]); - expect(errors.length).toBeGreaterThan(0); - }); - - test("should reject Infinity values", () => { - const errors = validateVectorValues([Infinity, 1, 2]); - expect(errors.length).toBeGreaterThan(0); - }); - - test("should reject non-integer values", () => { - const errors = validateMatrixValues([ - [1.5, 2], - [3, 4], - ]); - expect(errors.length).toBeGreaterThan(0); - }); - - test("should handle extremely large values safely", () => { - const largeValue = Number.MAX_SAFE_INTEGER; - const errors = validateVectorValues([largeValue, 1, 2]); - // Should not throw, just validate - expect(errors).toBeDefined(); - }); - }); - - describe("Resource Exhaustion Prevention", () => { - test("should handle large process counts without stack overflow", () => { - const state = calculator.createDefaultState(); - const resized = calculator.resizeMatrices(state, 100, 10); - - expect(resized.processCount).toBe(100); - expect(resized.allocation.length).toBe(100); - }); - - test("should prevent infinite loops in safety algorithm", () => { - // Algorithm has maxIterations guard - const available = [0, 0, 0]; - const allocation = [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1], - ]; - const need = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - ]; - - // Should complete and return unsafe, not hang - const start = Date.now(); - const result = calculator.checkSafety(available, allocation, need); - const elapsed = Date.now() - start; - - expect(result.isSafe).toBe(false); - expect(elapsed).toBeLessThan(1000); // Should complete in < 1 second - }); - }); - - describe("Data Integrity", () => { - test("should not mutate input arrays", () => { - const original = [1, 2, 3]; - const copy = [...original]; - - isVectorLessOrEqual(original, [2, 3, 4]); - - expect(original).toEqual(copy); - }); - - test("should create deep copies of matrices", () => { - const max = [ - [2, 1], - [1, 2], - ]; - const allocation = [ - [1, 0], - [0, 1], - ]; - - const need = calculateNeedMatrix(max, allocation); - - // Modifying need should not affect original matrices - need[0][0] = 999; - expect(max[0][0]).toBe(2); - }); - }); - - describe("Request Validation (Authorization Check)", () => { - test("should reject invalid process IDs", () => { - const state = calculator.createDefaultState(); - - const result = calculator.processRequest( - { processId: -1, requestVector: [1, 0, 0] }, - state, - ); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("Invalid"); - }); - - test("should reject out-of-bounds process IDs", () => { - const state = calculator.createDefaultState(); - - const result = calculator.processRequest( - { processId: 999, requestVector: [1, 0, 0] }, - state, - ); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("Invalid process ID"); - }); - - test("should reject requests exceeding declared maximum", () => { - const state = calculator.createDefaultState(); - - const result = calculator.processRequest( - { processId: 0, requestVector: [999, 999, 999] }, - state, - ); - - expect(result.canGrant).toBe(false); - expect(result.errorMessage).toContain("exceeds declared maximum"); - }); - }); -}); diff --git a/frontend/src/lib/bankers-algorithm-calculator.ts b/frontend/src/lib/bankers-algorithm-calculator.ts index 7d02347..e1ef5f4 100644 --- a/frontend/src/lib/bankers-algorithm-calculator.ts +++ b/frontend/src/lib/bankers-algorithm-calculator.ts @@ -42,6 +42,23 @@ export class BankersAlgorithmCalculator { ): SafetyResult { const processCount = allocation.length; + // Early exit: If no resources available, system cannot be safe + if (available.every((v) => v === 0) && processCount > 0) { + return { + isSafe: false, + safeSequence: [], + steps: [ + { + stepNumber: 1, + description: "System has no available resources - UNSAFE", + workVector: available, + isHighlighted: true, + }, + ], + finalFinishState: Array(processCount).fill(false), + }; + } + // Step 1: Initialize Work = Available and Finish[i] = false let work = cloneVector(available); const finish = Array(processCount).fill(false); @@ -102,7 +119,7 @@ export class BankersAlgorithmCalculator { stepNumber: 3, description: `work = work + allocation[${processName}]: (${prevWork.join(", ")}) + (${allocation[ i - ].join(", ")})`, // = (${work.join(", ")}) + ].join(", ")})`, workVector: cloneVector(work), processChecked: processName, canFinish: true, @@ -117,9 +134,14 @@ export class BankersAlgorithmCalculator { // If no process was found in this iteration, show why we're stopping if (!foundProcess && iterationCount > 1) { - const unfinishedProcesses = finish - .map((finished, index) => (finished ? null : `P${index}`)) - .filter((p) => p !== null); + // Optimize: Use Set for O(1) membership test vs O(n) array operations + const finishedSet = new Set(safeSequence); + const unfinishedProcesses: string[] = []; + for (let i = 0; i < processCount; i++) { + if (!finishedSet.has(`P${i}`)) { + unfinishedProcesses.push(`P${i}`); + } + } steps.push({ stepNumber: 2,