commit 5c07425d1d7ae4f8aeea734089c057ce9483cfe8 Author: Eduard Duran Date: Tue Feb 10 23:59:52 2026 +0100 Initial commit: Sudoku SPA React + Vite sudoku app with 5 difficulty levels, dark mode, celebration effects, localStorage save, and deploy script. Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..7a21d46 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === Configuració === +REMOTE_USER="root" +REMOTE_HOST="ontanem.net" +REMOTE_DIR="/var/www/simple-sudoku" +SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[✓]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[✗]${NC} $1"; exit 1; } + +# === Comprovacions prèvies === +echo "" +echo "🚀 Deploy Sudoku → ${SSH_TARGET}:${REMOTE_DIR}" +echo "─────────────────────────────────────────────" + +# Verificar que estem al directori del projecte +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +if [ ! -f "package.json" ]; then + error "No s'ha trobat package.json. Executa l'script des del directori del projecte." +fi + +# Verificar connexió SSH +info "Comprovant connexió SSH..." +if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$SSH_TARGET" "echo ok" &>/dev/null; then + error "No s'ha pogut connectar a ${SSH_TARGET}. Verifica la configuració SSH." +fi + +# === Build === +info "Instal·lant dependències..." +npm ci --silent + +info "Generant build de producció..." +npx vite build --emptyOutDir + +if [ ! -d "dist" ]; then + error "El build ha fallat: no s'ha creat el directori dist/" +fi + +# === Deploy === +info "Creant directori remot si no existeix..." +ssh "$SSH_TARGET" "mkdir -p ${REMOTE_DIR}" + +info "Pujant fitxers amb rsync..." +rsync -azP --delete \ + dist/ \ + "${SSH_TARGET}:${REMOTE_DIR}/" + +# === Verificació === +info "Verificant fitxers al servidor..." +REMOTE_INDEX=$(ssh "$SSH_TARGET" "test -f ${REMOTE_DIR}/index.html && echo 'ok' || echo 'fail'") + +if [ "$REMOTE_INDEX" = "ok" ]; then + echo "" + info "Deploy completat correctament! ✨" + echo "" + echo " Fitxers desplegats a: ${SSH_TARGET}:${REMOTE_DIR}/" + echo "" +else + error "Alguna cosa ha anat malament: index.html no trobat al servidor." +fi diff --git a/index.html b/index.html new file mode 100644 index 0000000..a5a875e --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Sudoku + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..97f2b93 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1796 @@ +{ + "name": "simple-sudoku", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simple-sudoku", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..85c4985 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "simple-sudoku", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.0" + } +} diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..09a07a0 --- /dev/null +++ b/src/App.css @@ -0,0 +1,843 @@ +/* === Reset & Base === */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; + background: #f8f9fa; + color: #212529; + min-height: 100vh; + -webkit-tap-highlight-color: transparent; +} + +/* === App Layout === */ +.app { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding: 16px; +} + +.app-header { + margin-bottom: 12px; +} + +.app-header h1 { + font-size: 28px; + font-weight: 700; + color: #343a40; + letter-spacing: -0.5px; +} + +.app-main { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + width: 100%; + max-width: 460px; +} + +/* === ScoreBoard === */ +.score-board { + display: flex; + justify-content: space-between; + width: 100%; + padding: 10px 16px; + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.score-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.score-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #868e96; + font-weight: 600; +} + +.score-value { + font-size: 16px; + font-weight: 600; + color: #343a40; +} + +.errors-count { + color: #e53935; +} + +.timer { + font-variant-numeric: tabular-nums; +} + +/* === Board === */ +.board { + display: grid; + grid-template-columns: repeat(9, 1fr); + grid-template-rows: repeat(9, 1fr); + width: 100%; + aspect-ratio: 1; + max-width: 460px; + background: #343a40; + gap: 1px; + padding: 2px; + border-radius: 8px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + user-select: none; +} + +/* Thick borders for 3x3 boxes */ +.board .cell:nth-child(9n + 1) { margin-left: 1px; } +.board .cell:nth-child(9n + 4) { margin-left: 1px; } +.board .cell:nth-child(9n + 7) { margin-left: 1px; } +.board .cell:nth-child(9n) { margin-right: 1px; } + +.board .cell:nth-child(-n + 9) { margin-top: 1px; } +.board .cell:nth-child(n + 28):nth-child(-n + 36) { margin-top: 1px; } +.board .cell:nth-child(n + 55):nth-child(-n + 63) { margin-top: 1px; } +.board .cell:nth-child(n + 73) { margin-bottom: 1px; } + +/* === Cell === */ +.cell { + display: flex; + align-items: center; + justify-content: center; + background: #fff; + cursor: pointer; + position: relative; + transition: background-color 0.1s ease; +} + +.cell-value { + font-size: clamp(20px, 5.5vw, 32px); + font-weight: 500; + line-height: 1; + color: #1976d2; +} + +.cell.given .cell-value { + color: #343a40; + font-weight: 700; +} + +.cell.error .cell-value { + color: #e53935; +} + +/* Cell highlighting */ +.cell.selected { + background: #bbdefb; +} + +.cell.highlighted-row, +.cell.highlighted-col, +.cell.highlighted-box { + background: #e8eef4; +} + +.cell.same-number { + background: #c8e6f9; +} + +.cell.same-number.highlighted-row, +.cell.same-number.highlighted-col, +.cell.same-number.highlighted-box { + background: #b3d9f5; +} + +.cell.error { + background: #ffebee; +} + +/* Celebration animation */ +.cell.celebrating::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + animation: celebrateGlow 1.4s ease-in-out forwards; + pointer-events: none; +} + +.cell.celebrating .cell-value { + animation: celebrateText 1.4s ease-in-out; +} + +@keyframes celebrateGlow { + 0% { + background: rgba(255, 213, 79, 0); + } + 12% { + background: rgba(255, 213, 79, 0.45); + } + 24% { + background: rgba(255, 193, 7, 0.35); + } + 36% { + background: rgba(255, 213, 79, 0.25); + } + 55% { + background: rgba(255, 224, 130, 0.15); + } + 80% { + background: rgba(255, 236, 179, 0.05); + } + 100% { + background: rgba(255, 236, 179, 0); + } +} + +@keyframes celebrateText { + 0% { + transform: scale(1); + } + 12% { + color: #f57f17; + transform: scale(1.25); + } + 24% { + color: #ff8f00; + transform: scale(0.9); + } + 36% { + color: #ffa000; + transform: scale(1.1); + } + 55% { + color: #ffb300; + transform: scale(1); + } + 80% { + transform: scale(1); + } + 100% { + transform: scale(1); + } +} + +/* Notes grid */ +.cell-notes { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + width: 100%; + height: 100%; + padding: 1px; +} + +.note { + display: flex; + align-items: center; + justify-content: center; + font-size: clamp(9px, 2vw, 11px); + color: #868e96; + line-height: 1; +} + +/* === NumberPad === */ +.number-pad { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; +} + +.number-buttons { + display: grid; + grid-template-columns: repeat(9, 1fr); + gap: 6px; +} + +.num-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1px; + padding: 8px 0; + border: none; + border-radius: 8px; + background: #fff; + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + transition: all 0.15s ease; +} + +.num-btn:hover:not(:disabled) { + background: #e3f2fd; + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); +} + +.num-btn:active:not(:disabled) { + transform: translateY(0); +} + +.num-btn:disabled { + opacity: 0.35; + cursor: default; +} + +.num-btn-value { + font-size: clamp(16px, 4vw, 22px); + font-weight: 600; + color: #1976d2; + line-height: 1; +} + +.num-btn-count { + font-size: 9px; + color: #adb5bd; + line-height: 1; +} + +.pad-actions { + display: flex; + gap: 8px; +} + +.action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px; + border: none; + border-radius: 8px; + background: #fff; + color: #495057; + font-size: 14px; + font-weight: 500; + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + transition: all 0.15s ease; +} + +.action-btn:hover { + background: #f1f3f5; +} + +.action-btn.active { + background: #1976d2; + color: #fff; + box-shadow: 0 2px 8px rgba(25, 118, 210, 0.3); +} + +/* === GameControls === */ +.game-controls { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; +} + +.controls-row { + display: flex; + gap: 8px; +} + +.controls-row + .controls-row { + margin-top: 4px; +} + +.difficulty-select { + flex: 1; + padding: 10px 12px; + border: 1px solid #dee2e6; + border-radius: 8px; + background: #fff; + font-size: 14px; + font-family: inherit; + color: #495057; + cursor: pointer; + appearance: auto; +} + +.difficulty-select:focus { + outline: none; + border-color: #1976d2; + box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15); +} + +/* === Toggle Switch === */ +.checkbox-label { + display: flex; + align-items: center; + gap: 10px; + margin: 4px 0; + padding: 8px 14px; + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + font-size: 14px; + font-weight: 500; + color: #495057; + cursor: pointer; + user-select: none; + transition: all 0.15s ease; +} + +.checkbox-label:hover { + background: #f1f3f5; +} + +.checkbox-label input[type="checkbox"] { + display: none; +} + +.toggle-track { + position: relative; + width: 36px; + height: 20px; + background: #ced4da; + border-radius: 10px; + transition: background 0.2s ease; + flex-shrink: 0; +} + +.toggle-track::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + transition: transform 0.2s ease; +} + +.checkbox-label input:checked + .toggle-track { + background: #1976d2; +} + +.checkbox-label input:checked + .toggle-track::after { + transform: translateX(16px); +} + +/* === Buttons === */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px 16px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn:disabled { + opacity: 0.4; + cursor: default; +} + +.btn-primary { + flex: 1; + background: #1976d2; + color: #fff; + box-shadow: 0 2px 6px rgba(25, 118, 210, 0.25); +} + +.btn-primary:hover:not(:disabled) { + background: #1565c0; + box-shadow: 0 3px 8px rgba(25, 118, 210, 0.35); +} + +.btn-secondary { + flex: 1; + background: #fff; + color: #495057; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); +} + +.btn-secondary:hover:not(:disabled) { + background: #f1f3f5; +} + +.btn-icon { + flex: 0; + padding: 10px; +} + +.btn-large { + padding: 14px 28px; + font-size: 16px; +} + +.btn-block { + width: 100%; +} + +/* === Confetti === */ +.confetti-canvas { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + z-index: 200; + pointer-events: none; +} + +/* === WinModal === */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + animation: fadeIn 0.3s ease; + padding: 16px; +} + +.modal { + background: #fff; + border-radius: 16px; + padding: 32px; + text-align: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); + max-width: 340px; + width: 100%; + animation: slideUp 0.3s ease; +} + +.modal h2 { + font-size: 24px; + color: #2e7d32; + margin-bottom: 4px; +} + +.modal p { + color: #868e96; + margin-bottom: 20px; +} + +.modal-stats { + display: flex; + justify-content: space-around; + margin-bottom: 24px; + padding: 16px 0; + border-top: 1px solid #f1f3f5; + border-bottom: 1px solid #f1f3f5; +} + +.modal-stat { + display: flex; + flex-direction: column; + gap: 4px; +} + +.modal-stat-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #868e96; + font-weight: 600; +} + +.modal-stat-value { + font-size: 18px; + font-weight: 700; + color: #343a40; +} + +.modal-actions { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.modal-actions .btn { + flex: 1; +} + +.settings-list { + display: flex; + flex-direction: column; + gap: 10px; + margin: 20px 0; +} + +.settings-list .checkbox-label { + margin: 0; +} + +/* === Animations === */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* === Responsive === */ +@media (max-width: 480px) { + .app { + padding: 10px 8px; + } + + .app-header h1 { + font-size: 22px; + } + + .app-main { + gap: 10px; + } + + .score-board { + padding: 8px 12px; + } + + .number-buttons { + gap: 4px; + } + + .num-btn { + padding: 6px 0; + border-radius: 6px; + } + + .pad-actions { + gap: 6px; + } + + .action-btn { + padding: 8px; + font-size: 13px; + } + + .controls-row { + gap: 6px; + } + + .btn { + padding: 8px 12px; + font-size: 13px; + } +} + +@media (min-width: 768px) { + .app { + padding: 32px; + } + + .app-header { + margin-bottom: 20px; + } + + .app-header h1 { + font-size: 32px; + } +} + +/* === Dark Mode === */ +.app.dark { + background: #1a1a2e; + color: #e0e0e0; +} + +.app.dark .app-header h1 { + color: #e0e0e0; +} + +.app.dark .score-board { + background: #16213e; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.app.dark .score-label { + color: #8a8a9a; +} + +.app.dark .score-value { + color: #e0e0e0; +} + +.app.dark .board { + background: #4a4a6a; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4); +} + +.app.dark .cell { + background: #1a1a2e; +} + +.app.dark .cell-value { + color: #64b5f6; +} + +.app.dark .cell.given .cell-value { + color: #e0e0e0; +} + +.app.dark .cell.error .cell-value { + color: #ef5350; +} + +.app.dark .cell.selected { + background: #1a3a5c; +} + +.app.dark .cell.highlighted-row, +.app.dark .cell.highlighted-col, +.app.dark .cell.highlighted-box { + background: #16213e; +} + +.app.dark .cell.same-number { + background: #1a3050; +} + +.app.dark .cell.same-number.highlighted-row, +.app.dark .cell.same-number.highlighted-col, +.app.dark .cell.same-number.highlighted-box { + background: #1a2a45; +} + +.app.dark .cell.error { + background: #3e1a1a; +} + +.app.dark .note { + color: #6a6a7a; +} + +.app.dark .num-btn { + background: #16213e; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.app.dark .num-btn:hover:not(:disabled) { + background: #1a3a5c; +} + +.app.dark .num-btn-value { + color: #64b5f6; +} + +.app.dark .num-btn-count { + color: #5a5a6a; +} + +.app.dark .btn-secondary { + background: #16213e; + color: #c0c0d0; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.app.dark .btn-secondary:hover:not(:disabled) { + background: #1a2a45; +} + +.app.dark .btn-primary { + background: #1565c0; + box-shadow: 0 2px 6px rgba(21, 101, 192, 0.4); +} + +.app.dark .btn-primary:hover:not(:disabled) { + background: #1976d2; +} + +.app.dark .difficulty-select { + background: #16213e; + border-color: #2a2a4a; + color: #c0c0d0; +} + +.app.dark .difficulty-select:focus { + border-color: #64b5f6; + box-shadow: 0 0 0 3px rgba(100, 181, 246, 0.15); +} + +.app.dark .action-btn { + background: #16213e; + color: #c0c0d0; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.app.dark .action-btn:hover { + background: #1a2a45; +} + +.app.dark .action-btn.active { + background: #1565c0; + color: #fff; +} + +.app.dark .modal-overlay { + background: rgba(0, 0, 0, 0.6); +} + +.app.dark .modal { + background: #16213e; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +} + +.app.dark .modal h2 { + color: #e0e0e0; +} + +.app.dark .modal p { + color: #8a8a9a; +} + +.app.dark .modal-stats { + border-color: #2a2a4a; +} + +.app.dark .modal-stat-label { + color: #8a8a9a; +} + +.app.dark .modal-stat-value { + color: #e0e0e0; +} + +.app.dark .checkbox-label { + background: #1a1a2e; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); + color: #c0c0d0; +} + +.app.dark .checkbox-label:hover { + background: #1a2a45; +} + +.app.dark .toggle-track { + background: #3a3a5a; +} + +.app.dark .settings-list .checkbox-label { + background: #1a1a2e; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..e31ddcf --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react' +import Board from './components/Board' +import NumberPad from './components/NumberPad' +import GameControls from './components/GameControls' +import ScoreBoard from './components/ScoreBoard' +import WinModal from './components/WinModal' +import Confetti from './components/Confetti' +import SettingsModal from './components/SettingsModal' +import { useGame } from './hooks/useGame' + +function App() { + const { + state, + numberCounts, + newGame, + selectCell, + setNumber, + clearCell, + toggleNotes, + toggleShowErrors, + toggleShowEffects, + toggleDarkMode, + undo, + redo, + } = useGame() + + const [showSettings, setShowSettings] = useState(false) + + useEffect(() => { + document.body.style.background = state.darkMode ? '#1a1a2e' : '#f8f9fa' + }, [state.darkMode]) + + return ( +
+
+

Sudoku

+
+ +
+ + + + + + + 0} + canRedo={state.future.length > 0} + onOpenSettings={() => setShowSettings(true)} + /> +
+ + + {state.isComplete && ( + newGame(state.difficulty)} + /> + )} + + {showSettings && ( + setShowSettings(false)} + /> + )} +
+ ) +} + +export default App diff --git a/src/components/Board.jsx b/src/components/Board.jsx new file mode 100644 index 0000000..4cee104 --- /dev/null +++ b/src/components/Board.jsx @@ -0,0 +1,55 @@ +import React from 'react' +import Cell from './Cell' +import { checkValue } from '../utils/sudoku' + +function Board({ board, puzzle, solution, notes, selected, showErrors, celebrating, selectCell }) { + const selectedValue = selected && board[selected.row][selected.col] + + return ( +
+ {board.map((row, r) => + row.map((value, c) => { + const isGiven = puzzle[r][c] !== 0 + const isSelected = selected && selected.row === r && selected.col === c + const isHighlightedRow = selected && selected.row === r && !isSelected + const isHighlightedCol = selected && selected.col === c && !isSelected + const isHighlightedBox = + selected && + !isSelected && + Math.floor(selected.row / 3) === Math.floor(r / 3) && + Math.floor(selected.col / 3) === Math.floor(c / 3) + const isSameNumber = + !isSelected && + selectedValue !== 0 && + value !== 0 && + value === selectedValue + const isError = + showErrors && + !isGiven && + value !== 0 && + !checkValue(solution, r, c, value) + const isCelebrating = celebrating.has(`${r},${c}`) + + return ( + selectCell(r, c)} + /> + ) + }) + )} +
+ ) +} + +export default React.memo(Board) diff --git a/src/components/Cell.jsx b/src/components/Cell.jsx new file mode 100644 index 0000000..b2f051b --- /dev/null +++ b/src/components/Cell.jsx @@ -0,0 +1,29 @@ +import React from 'react' + +function Cell({ value, notes, isGiven, isSelected, isHighlightedRow, isHighlightedCol, isHighlightedBox, isSameNumber, isError, isCelebrating, onClick }) { + const classes = ['cell'] + if (isGiven) classes.push('given') + if (isSelected) classes.push('selected') + if (isHighlightedRow) classes.push('highlighted-row') + if (isHighlightedCol) classes.push('highlighted-col') + if (isHighlightedBox) classes.push('highlighted-box') + if (isSameNumber) classes.push('same-number') + if (isError) classes.push('error') + if (isCelebrating) classes.push('celebrating') + + return ( +
+ {value !== 0 ? ( + {value} + ) : notes && notes.size > 0 ? ( +
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => ( + {notes.has(n) ? n : ''} + ))} +
+ ) : null} +
+ ) +} + +export default React.memo(Cell) diff --git a/src/components/Confetti.jsx b/src/components/Confetti.jsx new file mode 100644 index 0000000..02da5ce --- /dev/null +++ b/src/components/Confetti.jsx @@ -0,0 +1,87 @@ +import React, { useEffect, useRef } from 'react' + +const COLORS = ['#ffd700', '#ff6b6b', '#48dbfb', '#ff9ff3', '#54a0ff', '#5f27cd', '#01a3a4', '#f368e0', '#ff9f43', '#2ed573'] +const PARTICLE_COUNT = 150 +const DURATION = 4000 + +function Confetti({ active }) { + const canvasRef = useRef(null) + + useEffect(() => { + if (!active) return + + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext('2d') + let animationId + let startTime + + function resize() { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + resize() + window.addEventListener('resize', resize) + + const particles = Array.from({ length: PARTICLE_COUNT }, () => ({ + x: Math.random() * canvas.width, + y: -20 - Math.random() * canvas.height * 0.5, + w: 4 + Math.random() * 6, + h: 6 + Math.random() * 10, + color: COLORS[Math.floor(Math.random() * COLORS.length)], + vx: (Math.random() - 0.5) * 4, + vy: 1.5 + Math.random() * 3, + rotation: Math.random() * Math.PI * 2, + rotationSpeed: (Math.random() - 0.5) * 0.15, + wobble: Math.random() * Math.PI * 2, + wobbleSpeed: 0.03 + Math.random() * 0.05, + })) + + function animate(time) { + if (!startTime) startTime = time + const elapsed = time - startTime + + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Fade out in the last second + const fadeStart = DURATION - 1000 + const globalAlpha = elapsed > fadeStart ? 1 - (elapsed - fadeStart) / 1000 : 1 + + for (const p of particles) { + p.x += p.vx + Math.sin(p.wobble) * 0.5 + p.y += p.vy + p.rotation += p.rotationSpeed + p.wobble += p.wobbleSpeed + p.vy += 0.02 // gravity + + ctx.save() + ctx.globalAlpha = globalAlpha + ctx.translate(p.x, p.y) + ctx.rotate(p.rotation) + ctx.fillStyle = p.color + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h) + ctx.restore() + } + + if (elapsed < DURATION) { + animationId = requestAnimationFrame(animate) + } else { + ctx.clearRect(0, 0, canvas.width, canvas.height) + } + } + + animationId = requestAnimationFrame(animate) + + return () => { + cancelAnimationFrame(animationId) + window.removeEventListener('resize', resize) + } + }, [active]) + + if (!active) return null + + return +} + +export default Confetti diff --git a/src/components/GameControls.jsx b/src/components/GameControls.jsx new file mode 100644 index 0000000..979a2d1 --- /dev/null +++ b/src/components/GameControls.jsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react' + +const DIFFICULTIES = [ + { value: 'molt-facil', label: 'Molt fàcil' }, + { value: 'facil', label: 'Fàcil' }, + { value: 'mitja', label: 'Mitjà' }, + { value: 'dificil', label: 'Difícil' }, + { value: 'expert', label: 'Expert' }, +] + +function GameControls({ currentDifficulty, onNewGame, onClear, onUndo, onRedo, canUndo, canRedo, onOpenSettings }) { + const [difficulty, setDifficulty] = useState(currentDifficulty) + const [showConfirm, setShowConfirm] = useState(false) + + function handleNewGame() { + setShowConfirm(false) + onNewGame(difficulty) + } + + return ( +
+
+ + +
+
+ + + + +
+ + {showConfirm && ( +
setShowConfirm(false)}> +
e.stopPropagation()}> +

Nova partida?

+

Es perdrà el progrés de la partida actual.

+
+ + +
+
+
+ )} +
+ ) +} + +export default React.memo(GameControls) diff --git a/src/components/NumberPad.jsx b/src/components/NumberPad.jsx new file mode 100644 index 0000000..6fcc756 --- /dev/null +++ b/src/components/NumberPad.jsx @@ -0,0 +1,27 @@ +import React from 'react' + +function NumberPad({ onNumber, numberCounts }) { + return ( +
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => { + const remaining = 9 - numberCounts[n] + const disabled = remaining <= 0 + return ( + + ) + })} +
+
+ ) +} + +export default React.memo(NumberPad) diff --git a/src/components/ScoreBoard.jsx b/src/components/ScoreBoard.jsx new file mode 100644 index 0000000..66bb397 --- /dev/null +++ b/src/components/ScoreBoard.jsx @@ -0,0 +1,33 @@ +import React from 'react' +import Timer from './Timer' + +const DIFFICULTY_LABELS = { + 'molt-facil': 'Molt fàcil', + 'facil': 'Fàcil', + 'mitja': 'Mitjà', + 'dificil': 'Difícil', + 'expert': 'Expert', +} + +function ScoreBoard({ difficulty, errors, timer, showErrors }) { + return ( +
+
+ Dificultat + {DIFFICULTY_LABELS[difficulty]} +
+ {showErrors && ( +
+ Errors + {errors} +
+ )} +
+ Temps + +
+
+ ) +} + +export default React.memo(ScoreBoard) diff --git a/src/components/SettingsModal.jsx b/src/components/SettingsModal.jsx new file mode 100644 index 0000000..ba46d02 --- /dev/null +++ b/src/components/SettingsModal.jsx @@ -0,0 +1,45 @@ +import React from 'react' + +function SettingsModal({ showErrors, showEffects, darkMode, onToggleShowErrors, onToggleShowEffects, onToggleDarkMode, onClose }) { + return ( +
+
e.stopPropagation()}> +

Configuració

+
+ + + +
+ +
+
+ ) +} + +export default SettingsModal diff --git a/src/components/Timer.jsx b/src/components/Timer.jsx new file mode 100644 index 0000000..6025adc --- /dev/null +++ b/src/components/Timer.jsx @@ -0,0 +1,13 @@ +import React from 'react' + +function Timer({ seconds }) { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return ( + + {String(mins).padStart(2, '0')}:{String(secs).padStart(2, '0')} + + ) +} + +export default React.memo(Timer) diff --git a/src/components/WinModal.jsx b/src/components/WinModal.jsx new file mode 100644 index 0000000..f6b6118 --- /dev/null +++ b/src/components/WinModal.jsx @@ -0,0 +1,40 @@ +import React from 'react' +import Timer from './Timer' + +const DIFFICULTY_LABELS = { + 'molt-facil': 'Molt fàcil', + 'facil': 'Fàcil', + 'mitja': 'Mitjà', + 'dificil': 'Difícil', + 'expert': 'Expert', +} + +function WinModal({ timer, errors, difficulty, onNewGame }) { + return ( +
+
+

Felicitats!

+

Has completat el Sudoku!

+
+
+ Dificultat + {DIFFICULTY_LABELS[difficulty]} +
+
+ Temps + +
+
+ Errors + {errors} +
+
+ +
+
+ ) +} + +export default WinModal diff --git a/src/hooks/useGame.js b/src/hooks/useGame.js new file mode 100644 index 0000000..4eb0da9 --- /dev/null +++ b/src/hooks/useGame.js @@ -0,0 +1,476 @@ +import { useReducer, useEffect, useCallback, useRef } from 'react' +import { generatePuzzle, checkValue, isBoardComplete } from '../utils/sudoku' + +const STORAGE_KEY = 'sudoku-save' + +function cloneBoard(board) { + return board.map(row => [...row]) +} + +function cloneNotes(notes) { + return notes.map(row => row.map(cell => new Set(cell))) +} + +function serializeNotes(notes) { + return notes.map(row => row.map(cell => [...cell])) +} + +function deserializeNotes(notes) { + return notes.map(row => row.map(cell => new Set(cell))) +} + +function createInitialNotes() { + return Array.from({ length: 9 }, () => + Array.from({ length: 9 }, () => new Set()) + ) +} + +function saveState(state) { + try { + const toSave = { + puzzle: state.puzzle, + solution: state.solution, + board: state.board, + notes: serializeNotes(state.notes), + difficulty: state.difficulty, + errors: state.errors, + timer: state.timer, + isComplete: state.isComplete, + notesMode: state.notesMode, + showErrors: state.showErrors, + showEffects: state.showEffects, + darkMode: state.darkMode, + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)) + } catch { + // ignore storage errors + } +} + +function loadSavedState() { + try { + const saved = localStorage.getItem(STORAGE_KEY) + if (!saved) return null + const parsed = JSON.parse(saved) + return { + ...parsed, + notes: deserializeNotes(parsed.notes), + selected: null, + showErrors: parsed.showErrors || false, + showEffects: parsed.showEffects !== undefined ? parsed.showEffects : true, + darkMode: parsed.darkMode || false, + isRunning: !parsed.isComplete, + celebrating: new Set(), + history: [], + future: [], + } + } catch { + return null + } +} + +function findCompletedGroupKeys(board, solution) { + const keys = new Set() + + for (let r = 0; r < 9; r++) { + let complete = true + for (let c = 0; c < 9; c++) { + if (board[r][c] !== solution[r][c]) { complete = false; break } + } + if (complete) keys.add(`r${r}`) + } + + for (let c = 0; c < 9; c++) { + let complete = true + for (let r = 0; r < 9; r++) { + if (board[r][c] !== solution[r][c]) { complete = false; break } + } + if (complete) keys.add(`c${c}`) + } + + for (let br = 0; br < 3; br++) { + for (let bc = 0; bc < 3; bc++) { + let complete = true + for (let r = br * 3; r < br * 3 + 3; r++) { + for (let c = bc * 3; c < bc * 3 + 3; c++) { + if (board[r][c] !== solution[r][c]) { complete = false; break } + } + if (!complete) break + } + if (complete) keys.add(`b${br}${bc}`) + } + } + + return keys +} + +function cellsFromGroupKeys(keys) { + const cells = new Set() + for (const key of keys) { + if (key[0] === 'r') { + const r = +key[1] + for (let c = 0; c < 9; c++) cells.add(`${r},${c}`) + } else if (key[0] === 'c') { + const c = +key[1] + for (let r = 0; r < 9; r++) cells.add(`${r},${c}`) + } else { + const br = +key[1], bc = +key[2] + for (let r = br * 3; r < br * 3 + 3; r++) + for (let c = bc * 3; c < bc * 3 + 3; c++) cells.add(`${r},${c}`) + } + } + return cells +} + +function createNewGameState(difficulty) { + const { puzzle, solution } = generatePuzzle(difficulty) + return { + puzzle, + solution, + board: cloneBoard(puzzle), + notes: createInitialNotes(), + selected: null, + difficulty, + errors: 0, + timer: 0, + isRunning: true, + isComplete: false, + notesMode: false, + showErrors: false, + showEffects: true, + darkMode: false, + celebrating: new Set(), + history: [], + future: [], + } +} + +function pushHistory(state) { + return { + history: [...state.history, { board: cloneBoard(state.board), notes: cloneNotes(state.notes) }], + future: [], + } +} + +function reducer(state, action) { + switch (action.type) { + case 'NEW_GAME': { + return createNewGameState(action.difficulty) + } + + case 'LOAD_GAME': { + const saved = loadSavedState() + if (saved) return saved + return state + } + + case 'SELECT_CELL': { + return { ...state, selected: { row: action.row, col: action.col } } + } + + case 'SET_NUMBER': { + if (!state.selected || state.isComplete) return state + const { row, col } = state.selected + if (state.puzzle[row][col] !== 0) return state + + const num = action.number + const histUpdate = pushHistory(state) + const newBoard = cloneBoard(state.board) + const newNotes = cloneNotes(state.notes) + const currentValue = state.board[row][col] + const hasNotes = state.notes[row][col].size > 0 + + // Cell already has notes → toggle number in notes + if (hasNotes || state.notesMode) { + if (newNotes[row][col].has(num)) { + newNotes[row][col].delete(num) + } else { + newNotes[row][col].add(num) + } + newBoard[row][col] = 0 + return { ...state, ...histUpdate, board: newBoard, notes: newNotes } + } + + // Cell has a big number + if (currentValue !== 0) { + if (currentValue === num) { + // Same number again → convert to note + newBoard[row][col] = 0 + newNotes[row][col] = new Set([num]) + } else { + // Different number → both become notes + newBoard[row][col] = 0 + newNotes[row][col] = new Set([currentValue, num]) + } + return { ...state, ...histUpdate, board: newBoard, notes: newNotes } + } + + // Empty cell → place as big number + newBoard[row][col] = num + const isCorrect = checkValue(state.solution, row, col, num) + const newErrors = (!isCorrect && state.showErrors) ? state.errors + 1 : state.errors + const complete = isCorrect && isBoardComplete(newBoard, state.solution) + + // Detect newly completed groups + const prevKeys = findCompletedGroupKeys(state.board, state.solution) + const newKeys = findCompletedGroupKeys(newBoard, state.solution) + const freshKeys = new Set() + for (const key of newKeys) { + if (!prevKeys.has(key)) freshKeys.add(key) + } + const celebrating = cellsFromGroupKeys(freshKeys) + + return { + ...state, + ...histUpdate, + board: newBoard, + notes: newNotes, + errors: newErrors, + isComplete: complete, + isRunning: !complete, + celebrating, + } + } + + case 'CLEAR_CELL': { + if (!state.selected || state.isComplete) return state + const { row, col } = state.selected + if (state.puzzle[row][col] !== 0) return state + if (state.board[row][col] === 0 && state.notes[row][col].size === 0) return state + + const histUpdate = pushHistory(state) + const newBoard = cloneBoard(state.board) + const newNotes = cloneNotes(state.notes) + newBoard[row][col] = 0 + newNotes[row][col] = new Set() + + return { ...state, ...histUpdate, board: newBoard, notes: newNotes } + } + + case 'TOGGLE_NOTES': { + return { ...state, notesMode: !state.notesMode } + } + + case 'TOGGLE_SHOW_ERRORS': { + return { ...state, showErrors: !state.showErrors } + } + + case 'TOGGLE_SHOW_EFFECTS': { + return { ...state, showEffects: !state.showEffects } + } + + case 'TOGGLE_DARK_MODE': { + return { ...state, darkMode: !state.darkMode } + } + + case 'CLEAR_CELEBRATING': { + return { ...state, celebrating: new Set() } + } + + case 'UNDO': { + if (state.history.length === 0) return state + const prev = state.history[state.history.length - 1] + return { + ...state, + board: prev.board, + notes: prev.notes, + history: state.history.slice(0, -1), + future: [...state.future, { board: cloneBoard(state.board), notes: cloneNotes(state.notes) }], + } + } + + case 'REDO': { + if (state.future.length === 0) return state + const next = state.future[state.future.length - 1] + return { + ...state, + board: next.board, + notes: next.notes, + future: state.future.slice(0, -1), + history: [...state.history, { board: cloneBoard(state.board), notes: cloneNotes(state.notes) }], + } + } + + case 'TICK': { + return { ...state, timer: state.timer + 1 } + } + + case 'MOVE_SELECTION': { + if (!state.selected) return { ...state, selected: { row: 0, col: 0 } } + const { row, col } = state.selected + let newRow = row + (action.dr || 0) + let newCol = col + (action.dc || 0) + newRow = Math.max(0, Math.min(8, newRow)) + newCol = Math.max(0, Math.min(8, newCol)) + return { ...state, selected: { row: newRow, col: newCol } } + } + + default: + return state + } +} + +export function useGame() { + const [state, dispatch] = useReducer(reducer, null, () => { + const saved = loadSavedState() + if (saved) return saved + return createNewGameState('mitja') + }) + + // Auto-save to localStorage on every state change + useEffect(() => { + saveState(state) + }, [state]) + + // Clear celebration after animation + useEffect(() => { + if (state.celebrating.size > 0) { + const timeout = setTimeout(() => dispatch({ type: 'CLEAR_CELEBRATING' }), 1500) + return () => clearTimeout(timeout) + } + }, [state.celebrating]) + + const timerRef = useRef(null) + + useEffect(() => { + if (state.isRunning) { + timerRef.current = setInterval(() => dispatch({ type: 'TICK' }), 1000) + } + return () => clearInterval(timerRef.current) + }, [state.isRunning]) + + const newGame = useCallback((difficulty) => { + dispatch({ type: 'NEW_GAME', difficulty }) + }, []) + + const selectCell = useCallback((row, col) => { + dispatch({ type: 'SELECT_CELL', row, col }) + }, []) + + const setNumber = useCallback((number) => { + dispatch({ type: 'SET_NUMBER', number }) + }, []) + + const clearCell = useCallback(() => { + dispatch({ type: 'CLEAR_CELL' }) + }, []) + + const toggleNotes = useCallback(() => { + dispatch({ type: 'TOGGLE_NOTES' }) + }, []) + + const toggleShowErrors = useCallback(() => { + dispatch({ type: 'TOGGLE_SHOW_ERRORS' }) + }, []) + + const toggleShowEffects = useCallback(() => { + dispatch({ type: 'TOGGLE_SHOW_EFFECTS' }) + }, []) + + const toggleDarkMode = useCallback(() => { + dispatch({ type: 'TOGGLE_DARK_MODE' }) + }, []) + + const undo = useCallback(() => { + dispatch({ type: 'UNDO' }) + }, []) + + const redo = useCallback(() => { + dispatch({ type: 'REDO' }) + }, []) + + const moveSelection = useCallback((dr, dc) => { + dispatch({ type: 'MOVE_SELECTION', dr, dc }) + }, []) + + // Keyboard support + useEffect(() => { + function handleKeyDown(e) { + const num = parseInt(e.key) + if (num >= 1 && num <= 9) { + e.preventDefault() + setNumber(num) + return + } + switch (e.key) { + case 'Backspace': + case 'Delete': + e.preventDefault() + clearCell() + break + case 'ArrowUp': + e.preventDefault() + moveSelection(-1, 0) + break + case 'ArrowDown': + e.preventDefault() + moveSelection(1, 0) + break + case 'ArrowLeft': + e.preventDefault() + moveSelection(0, -1) + break + case 'ArrowRight': + e.preventDefault() + moveSelection(0, 1) + break + case 'n': + case 'N': + e.preventDefault() + toggleNotes() + break + case 'z': + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + if (e.shiftKey) redo() + else undo() + } + break + case 'y': + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + redo() + } + break + } + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [setNumber, clearCell, moveSelection, toggleNotes, undo, redo]) + + // Count how many of each number are placed + const numberCounts = Array(10).fill(0) + for (let r = 0; r < 9; r++) { + for (let c = 0; c < 9; c++) { + if (state.board[r][c] !== 0) { + numberCounts[state.board[r][c]]++ + } + } + } + + return { + state, + numberCounts, + newGame, + selectCell, + setNumber, + clearCell, + toggleNotes, + toggleShowErrors, + toggleShowEffects, + toggleDarkMode, + undo, + redo, + } +} + +export function hasSavedGame() { + try { + const saved = localStorage.getItem(STORAGE_KEY) + if (!saved) return false + const parsed = JSON.parse(saved) + return !parsed.isComplete + } catch { + return false + } +} diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..2d73cf6 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './App.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +) diff --git a/src/utils/sudoku.js b/src/utils/sudoku.js new file mode 100644 index 0000000..a7b3cea --- /dev/null +++ b/src/utils/sudoku.js @@ -0,0 +1,129 @@ +const EMPTY = 0 + +function shuffle(arr) { + const a = [...arr] + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[a[i], a[j]] = [a[j], a[i]] + } + return a +} + +function createEmptyGrid() { + return Array.from({ length: 9 }, () => Array(9).fill(EMPTY)) +} + +function isValid(grid, row, col, num) { + for (let i = 0; i < 9; i++) { + if (grid[row][i] === num) return false + if (grid[i][col] === num) return false + } + const boxRow = Math.floor(row / 3) * 3 + const boxCol = Math.floor(col / 3) * 3 + for (let r = boxRow; r < boxRow + 3; r++) { + for (let c = boxCol; c < boxCol + 3; c++) { + if (grid[r][c] === num) return false + } + } + return true +} + +function fillGrid(grid) { + for (let row = 0; row < 9; row++) { + for (let col = 0; col < 9; col++) { + if (grid[row][col] === EMPTY) { + const candidates = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9]) + for (const num of candidates) { + if (isValid(grid, row, col, num)) { + grid[row][col] = num + if (fillGrid(grid)) return true + grid[row][col] = EMPTY + } + } + return false + } + } + } + return true +} + +function countSolutions(grid, limit = 2) { + let count = 0 + + function solve(g) { + if (count >= limit) return + for (let row = 0; row < 9; row++) { + for (let col = 0; col < 9; col++) { + if (g[row][col] === EMPTY) { + for (let num = 1; num <= 9; num++) { + if (isValid(g, row, col, num)) { + g[row][col] = num + solve(g) + g[row][col] = EMPTY + } + if (count >= limit) return + } + return + } + } + } + count++ + } + + solve(grid) + return count +} + +function cloneGrid(grid) { + return grid.map(row => [...row]) +} + +const BLANKS_BY_DIFFICULTY = { + 'molt-facil': 30, + 'facil': 40, + 'mitja': 46, + 'dificil': 52, + 'expert': 58, +} + +export function generatePuzzle(difficulty = 'mitja') { + const solution = createEmptyGrid() + fillGrid(solution) + + const puzzle = cloneGrid(solution) + const target = BLANKS_BY_DIFFICULTY[difficulty] || 46 + + const positions = shuffle( + Array.from({ length: 81 }, (_, i) => [Math.floor(i / 9), i % 9]) + ) + + let removed = 0 + for (const [row, col] of positions) { + if (removed >= target) break + + const backup = puzzle[row][col] + puzzle[row][col] = EMPTY + + const testGrid = cloneGrid(puzzle) + if (countSolutions(testGrid, 2) !== 1) { + puzzle[row][col] = backup + } else { + removed++ + } + } + + return { puzzle, solution } +} + +export function checkValue(solution, row, col, value) { + return solution[row][col] === value +} + +export function isBoardComplete(board, solution) { + for (let r = 0; r < 9; r++) { + for (let c = 0; c < 9; c++) { + if (board[r][c] !== solution[r][c]) return false + } + } + return true +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..54592a1 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + base: '/sudoku/', +})