commit 3ce44d7aac921c01931dc0f6dc63a6c3e24e7c8f Author: david.martinez Date: Sat Jul 25 01:35:36 2026 -0600 Ruleta actvidad completa en React diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..1255078 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b89d61 --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# 🎡 Ruleta Interactiva — SCORM 1.2 + +Actividad e-learning interactiva basada en una ruleta con preguntas de cultura general, construida con React, TypeScript, Tailwind CSS y empaquetada como contenido SCORM 1.2. + +--- + +## 🛠️ Stack Tecnológico + +| Tecnología | Versión | Uso | +|---|---|---| +| React | 19 | UI y manejo de estado | +| TypeScript | 5 | Tipado estático | +| Tailwind CSS | 4 | Estilos utilitarios | +| Vite | 6 | Bundler y dev server | +| Howler.js | 2 | Manejo de audio | +| scorm-again | 3 | Simulador SCORM en desarrollo | + +--- + +## 📁 Estructura del Proyecto + +``` +src/ +├── audio/ +│ ├── ExtendedHowl.ts # Clase que extiende Howl con eventos de timeupdate (250ms) +│ ├── AudioController.ts # Controlador global de audio + sonidos precargados +│ └── audios.ts # Constantes con rutas de archivos de audio +│ +├── components/ +│ ├── ui/ +│ │ └── Button.tsx # Botón reutilizable (variantes: primary, secondary, outline, danger) +│ ├── layout/ +│ │ ├── Header.tsx # Cabecera de la aplicación +│ │ └── Footer.tsx # Pie de página +│ ├── screens/ +│ │ ├── Cover.tsx # Pantalla de portada con introducción y botón comenzar +│ │ ├── Game.tsx # Pantalla principal del juego +│ │ └── Result.tsx # Pantalla de cierre (3 estados) +│ └── game/ +│ ├── Ruleta/ +│ │ └── RuletaSVG.tsx # Ruleta SVG con 4 secciones de colores +│ ├── QuestionModal.tsx # Modal de pregunta con temporizador y feedback +│ └── ResultModal.tsx # Modal interno (reservado) +│ +├── data/ +│ └── preguntas.json # 4 categorías × 8 preguntas + banco genérico +│ +├── hooks/ +│ ├── useAudio.ts # Hook para consumir el AudioController +│ ├── useIntentos.ts # Manejo de intentos con persistencia en sessionStorage +│ ├── useRuleta.ts # Lógica completa del juego +│ ├── useSCORM.ts # Hook para inicializar y usar SCORM 1.2 +│ └── useScale.ts # Escalado adaptativo 1920×1080 +│ +├── layouts/ +│ └── MainLayout.tsx # Template principal con escalado adaptativo +│ +├── scorm/ +│ └── scorm.ts # Funciones SCORM 1.2 con fallback a sessionStorage +│ +├── types/ +│ └── index.ts # Interfaces TypeScript (Pregunta, Categoria, PreguntasData) +│ +├── App.tsx # Manejo de pantallas y flujo de intentos +└── main.tsx # Entry point con simulador SCORM en desarrollo + +public/ +├── audios/ +│ ├── click.mp3 # Sonido de clic en botones +│ ├── wheel-spin.mp3 # Sonido al girar la ruleta +│ ├── feedback-correct.mpeg # Sonido de respuesta correcta +│ └── feedback-incorrect.mpeg # Sonido de respuesta incorrecta +├── img/ +│ ├── ruleta.png # Imagen de ruleta (respaldo) +│ └── indicador.png # Imagen de indicador (respaldo) +└── imsmanifest.xml # Manifiesto SCORM 1.2 requerido por el LMS +``` + +--- + +## 🎮 Flujo de la Actividad + +``` +Portada (Cover) + │ + │ clic en "Comenzar" → consume 1 intento + sonido click + ▼ +Juego (Game) + │ + │ clic en la ruleta → gira + sonido wheel-spin (4s) + ▼ +Detecta sección (4 categorías × 90°) + │ + ▼ +Modal de Pregunta (temporizador 30s) + ├── Respuesta correcta → sonido correct → feedback → círculo de progreso + ├── Respuesta incorrecta → sonido incorrect → feedback → círculo de progreso + └── Tiempo agotado → cuenta como incorrecta → feedback + │ + │ 10 preguntas respondidas + ▼ +Pantalla de Resultado + ├── ≥ 8 correctas → ¡Felicidades! (sin botón) + ├── < 8 correctas + intentos disponibles → "Casi" + botón Reintentar + └── Sin intentos → Mensaje final (persiste al recargar) +``` + +--- + +## 🎯 Categorías de Preguntas + +| # | Categoría | Color | Preguntas | +|---|---|---|---| +| 1 | 🏛️ Historia | Morado `#7C3AED` | 8 | +| 2 | 🌍 Geografía | Verde `#059669` | 8 | +| 3 | 🔬 Ciencia | Rojo `#DC2626` | 8 | +| 4 | 🎨 Arte y Literatura | Naranja `#D97706` | 8 | + +> Las preguntas se seleccionan aleatoriamente sin repetir dentro de cada categoría. +> Si se agotan las preguntas de una categoría, se usa el **banco genérico** (5 preguntas). + +--- + +## 🏆 Criterios de Aprobación + +| Criterio | Valor | +|---|---| +| Preguntas por actividad | 10 | +| Mínimo para aprobar | 8 correctas | +| Intentos totales | 3 | +| Tiempo por pregunta | 30 segundos | +| Tiempo agotado | Cuenta como incorrecta | + +--- + +## 🔄 Sistema de Intentos + +- El intento se consume al hacer clic en **"Comenzar"** +- Se persiste en `sessionStorage`, por lo que recargar la página **no** recupera el intento +- Al agotar los 3 intentos, la pantalla de cierre final persiste aunque se recargue + +--- + +## 📐 Escalado Adaptativo + +El hook `useScale` escala toda la actividad a una resolución base de **1920×1080** manteniendo proporciones en cualquier pantalla: + +```ts +const BASE_WIDTH = 1920 +const BASE_HEIGHT = 1080 +const scale = Math.min(window.innerWidth / BASE_WIDTH, window.innerHeight / BASE_HEIGHT) +``` + +> Solo orientación horizontal. No aplica diseño para pantallas verticales. + +--- + +## 🔊 Audio + +Los audios se precargan al iniciar la aplicación mediante `AudioController`: + +```ts +sounds.click?.play() // Botones de navegación +sounds.wheelSpin?.play() // Al girar la ruleta +sounds.correct?.play() // Respuesta correcta +sounds.incorrect?.play() // Respuesta incorrecta +``` + +--- + +## 📡 SCORM 1.2 + +El módulo SCORM (`src/scorm/scorm.ts`) implementa todas las funciones estándar con **fallback automático a `sessionStorage`** cuando no hay LMS disponible: + +| Función | CMI Key | +|---|---| +| `initSCORM()` | Inicializa y establece status `incomplete` | +| `getStudentName()` | `cmi.core.student_name` | +| `getLessonStatus()` / `setLessonStatus()` | `cmi.core.lesson_status` | +| `getLessonLocation()` / `setLessonLocation()` | `cmi.core.lesson_location` | +| `getScore()` / `setScore()` | `cmi.core.score.raw/max/min` | +| `getSuspendData()` / `setSuspendData()` | `cmi.suspend_data` | +| `getScormData()` / `setScormData()` | Cualquier clave CMI | +| `finishSCORM()` | Calcula `session_time` en HH:MM:SS y cierra | +| `completeLesson()` | Establece status `completed` | + +> En desarrollo, `scorm-again` simula el `window.API` del LMS automáticamente. + +--- + +## 🧩 Componente Button + +```tsx + + + + +``` + +--- + +## 🚀 Comandos + +```bash +# Instalar dependencias +npm install + +# Desarrollo +npm run dev + +# Build para producción +npm run build + +# Verificar TypeScript +npx tsc --noEmit +``` + +--- + +## 📦 Empaquetado SCORM + +Para publicar en un LMS (Moodle, Cornerstone, etc.): + +1. Ejecutar `npm run build` +2. Abrir la carpeta `dist/` — el `imsmanifest.xml` se copia automáticamente +3. Seleccionar **todo** el contenido de `dist/` y comprimir en un `.zip` +4. Subir el `.zip` al LMS como paquete SCORM 1.2 + +> El `masteryscore` está configurado en **80** en el `imsmanifest.xml`. + +--- + +## 📋 Pendientes + +- [ ] Reemplazar categorías y preguntas de cultura general por el contenido real del cliente +- [ ] Integrar `setScore` y `completeLesson` con SCORM al finalizar la actividad +- [ ] Pruebas en LMS real (Moodle) +- [ ] Definir diseño visual final (colores, logo, imágenes del cliente) diff --git a/Ruleta.code-workspace b/Ruleta.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/Ruleta.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..eb1a7ed --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + ruleta + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d53b2b5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2384 @@ +{ + "name": "ruleta", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ruleta", + "version": "0.0.0", + "dependencies": { + "howler": "^2.2.4", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "scorm-again": "^3.1.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/howler": "^2.2.13", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.74.0.tgz", + "integrity": "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.74.0.tgz", + "integrity": "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.74.0.tgz", + "integrity": "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.74.0.tgz", + "integrity": "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.74.0.tgz", + "integrity": "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.74.0.tgz", + "integrity": "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.74.0.tgz", + "integrity": "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.74.0.tgz", + "integrity": "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.74.0.tgz", + "integrity": "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.74.0.tgz", + "integrity": "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.74.0.tgz", + "integrity": "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.74.0.tgz", + "integrity": "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.74.0.tgz", + "integrity": "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.74.0.tgz", + "integrity": "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.74.0.tgz", + "integrity": "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.74.0.tgz", + "integrity": "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.74.0.tgz", + "integrity": "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.74.0.tgz", + "integrity": "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.74.0.tgz", + "integrity": "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/howler": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.13.tgz", + "integrity": "sha512-40+EBjqIHHrC4VShlz/7i0lBUsE3QkgzZinQQji74Hd8sBkJZUBaT7LWFLK6rcabsDOOQpoMbEJvtaFQwxOu/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/howler": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/oxlint": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.74.0.tgz", + "integrity": "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.74.0", + "@oxlint/binding-android-arm64": "1.74.0", + "@oxlint/binding-darwin-arm64": "1.74.0", + "@oxlint/binding-darwin-x64": "1.74.0", + "@oxlint/binding-freebsd-x64": "1.74.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", + "@oxlint/binding-linux-arm-musleabihf": "1.74.0", + "@oxlint/binding-linux-arm64-gnu": "1.74.0", + "@oxlint/binding-linux-arm64-musl": "1.74.0", + "@oxlint/binding-linux-ppc64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-musl": "1.74.0", + "@oxlint/binding-linux-s390x-gnu": "1.74.0", + "@oxlint/binding-linux-x64-gnu": "1.74.0", + "@oxlint/binding-linux-x64-musl": "1.74.0", + "@oxlint/binding-openharmony-arm64": "1.74.0", + "@oxlint/binding-win32-arm64-msvc": "1.74.0", + "@oxlint/binding-win32-ia32-msvc": "1.74.0", + "@oxlint/binding-win32-x64-msvc": "1.74.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.24.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scorm-again": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/scorm-again/-/scorm-again-3.1.1.tgz", + "integrity": "sha512-WoZkssQoMJnZE5jzTXgxOeAAlgp5P9kjeT0ha506poljdfJ+t9vhQwQjmGTWEiHwDFVxNI3lpDuFrXA90/QcVw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "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/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..322e4c9 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "ruleta", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "howler": "^2.2.4", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "scorm-again": "^3.1.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/howler": "^2.2.13", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.1.1" + } +} diff --git a/public/audios/click.mp3 b/public/audios/click.mp3 new file mode 100644 index 0000000..62e0a38 Binary files /dev/null and b/public/audios/click.mp3 differ diff --git a/public/audios/feedback-correct.mpeg b/public/audios/feedback-correct.mpeg new file mode 100644 index 0000000..c8436d8 Binary files /dev/null and b/public/audios/feedback-correct.mpeg differ diff --git a/public/audios/feedback-incorrect.mpeg b/public/audios/feedback-incorrect.mpeg new file mode 100644 index 0000000..ac22ed3 Binary files /dev/null and b/public/audios/feedback-incorrect.mpeg differ diff --git a/public/audios/wheel-spin.mp3 b/public/audios/wheel-spin.mp3 new file mode 100644 index 0000000..1a3fa02 Binary files /dev/null and b/public/audios/wheel-spin.mp3 differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/indicador.png b/public/img/indicador.png new file mode 100644 index 0000000..4529887 Binary files /dev/null and b/public/img/indicador.png differ diff --git a/public/img/pr.png b/public/img/pr.png new file mode 100644 index 0000000..b4d237e Binary files /dev/null and b/public/img/pr.png differ diff --git a/public/img/progress.png b/public/img/progress.png new file mode 100644 index 0000000..3fe9a2d Binary files /dev/null and b/public/img/progress.png differ diff --git a/public/img/ruleta.png b/public/img/ruleta.png new file mode 100644 index 0000000..e03d743 Binary files /dev/null and b/public/img/ruleta.png differ diff --git a/public/imsmanifest.xml b/public/imsmanifest.xml new file mode 100644 index 0000000..f29ba18 --- /dev/null +++ b/public/imsmanifest.xml @@ -0,0 +1,30 @@ + + + + + ADL SCORM + 1.2 + + + + + Ruleta Interactiva + + Ruleta Interactiva + 80 + + + + + + + + + + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..425a19c --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react' +import MainLayout from './layouts/MainLayout' +import Cover from './components/screens/Cover' +import Game from './components/screens/Game' +import Result from './components/screens/Result' +import { consumirIntento, intentosAgotados } from './hooks/useIntentos' + +type Screen = 'cover' | 'game' | 'result' + +interface ResultState { + aprobado: boolean + sinIntentos: boolean +} + +export default function App() { + const [screen, setScreen] = useState(() => + intentosAgotados() ? 'result' : 'cover' + ) + const [resultState, setResultState] = useState({ + aprobado: false, + sinIntentos: intentosAgotados(), + }) + + function handleStart() { + consumirIntento() + setScreen('game') + } + + function handleGameEnd(aprobado: boolean) { + setResultState({ aprobado, sinIntentos: intentosAgotados() }) + setScreen('result') + } + + function handleReintentar() { + setScreen('cover') + } + + return ( + + {screen === 'cover' && } + {screen === 'game' && } + {screen === 'result' && ( + + )} + + ) +} diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/audio/AudioController.ts b/src/audio/AudioController.ts new file mode 100644 index 0000000..d6692cd --- /dev/null +++ b/src/audio/AudioController.ts @@ -0,0 +1,80 @@ +import { Howler } from 'howler' +import { ExtendedHowl, createSound, stopAllSounds } from './ExtendedHowl' +import { AUDIOS } from './audios' + +export class AudioController { + private audioElement: ExtendedHowl | null = null + private isMuted = false + + load(url: string): void { + this.audioElement?.stop() + this.audioElement = createSound(url) + } + + setHowl(howl: ExtendedHowl): void { + this.audioElement?.stop() + this.audioElement = howl + } + + stopAllAndPlay(howl: ExtendedHowl): void { + stopAllSounds() + this.setHowl(howl) + this.play() + } + + play(): void { + this.audioElement?.play() + } + + pause(): void { + this.audioElement?.pause() + } + + stop(): void { + this.audioElement?.stop() + } + + toggle(): void { + if (!this.audioElement) return + this.audioElement.playing() ? this.pause() : this.play() + } + + toggleMute(): void { + this.isMuted = !this.isMuted + Howler.mute(this.isMuted) + } + + isPlaying(): boolean { + return this.audioElement?.playing() ?? false + } + + getMuted(): boolean { + return this.isMuted + } + + getDuration(): number { + return (this.audioElement?.duration() as number) ?? 0 + } + + onTimeUpdate(cb: (currentTime: number) => void): void { + this.audioElement?.onTimeUpdate(cb) + } + + offTimeUpdate(cb: (currentTime: number) => void): void { + this.audioElement?.offTimeUpdate(cb) + } + + on(event: string, cb: () => void): void { + this.audioElement?.on(event, cb) + } +} + +export const audioController = new AudioController() + +// Sonidos precargados listos para usar +export const sounds = { + click: createSound(AUDIOS.click), + wheelSpin: createSound(AUDIOS.wheelSpin), + correct: createSound(AUDIOS.correct), + incorrect: createSound(AUDIOS.incorrect), +} diff --git a/src/audio/ExtendedHowl.ts b/src/audio/ExtendedHowl.ts new file mode 100644 index 0000000..1774ac5 --- /dev/null +++ b/src/audio/ExtendedHowl.ts @@ -0,0 +1,69 @@ +import { Howl, Howler } from 'howler' + +type TimeUpdateCallback = (currentTime: number) => void + +declare module 'howler' { + interface HowlerGlobal { + _howls: Howl[] + } +} + +export class ExtendedHowl extends Howl { + private _timeupdateListeners: TimeUpdateCallback[] = [] + private _interval: ReturnType | null = null + + constructor(options: ConstructorParameters[0]) { + super(options) + this._startTimeUpdate() + } + + private _startTimeUpdate(): void { + this._interval = setInterval(() => { + if (this.playing()) this._emitTimeUpdate(this.seek() as number) + }, 250) + } + + private _emitTimeUpdate(currentTime: number): void { + this._timeupdateListeners.forEach((cb) => cb(currentTime)) + } + + onTimeUpdate(cb: TimeUpdateCallback): void { + this._timeupdateListeners.push(cb) + } + + offTimeUpdate(cb: TimeUpdateCallback): void { + this._timeupdateListeners = this._timeupdateListeners.filter((l) => l !== cb) + } + + play(id?: number): number { + const result = super.play(id as number) + if (!this._interval) this._startTimeUpdate() + return result + } + + pause(id?: number): this { + super.pause(id) + if (this._interval) { + clearInterval(this._interval) + this._interval = null + } + return this + } + + stop(id?: number): this { + super.stop(id) + if (this._interval) { + clearInterval(this._interval) + this._interval = null + } + return this + } +} + +export function createSound(audioUrl?: string): ExtendedHowl | null { + return audioUrl ? new ExtendedHowl({ src: [audioUrl] }) : null +} + +export function stopAllSounds(): void { + Howler._howls?.forEach((sound: Howl) => sound.stop()) +} diff --git a/src/audio/audios.ts b/src/audio/audios.ts new file mode 100644 index 0000000..28cde97 --- /dev/null +++ b/src/audio/audios.ts @@ -0,0 +1,6 @@ +export const AUDIOS = { + click: '/audios/click.mp3', + wheelSpin: '/audios/wheel-spin.mp3', + correct: '/audios/feedback-correct.mpeg', + incorrect: '/audios/feedback-incorrect.mpeg', +} as const diff --git a/src/components/game/QuestionModal.tsx b/src/components/game/QuestionModal.tsx new file mode 100644 index 0000000..1f8a6e4 --- /dev/null +++ b/src/components/game/QuestionModal.tsx @@ -0,0 +1,124 @@ +import { useEffect, useRef, useState } from 'react' +import type { Pregunta, Categoria } from '../../types' + +interface QuestionModalProps { + pregunta: Pregunta + categoria: Categoria + mostrarFeedback: boolean + feedbackCorrecto: boolean + onResponder: (indice: number | null) => void + onCerrar: () => void +} + +const LETRAS = ['A', 'B', 'C', 'D'] +const TIEMPO_LIMITE = 30 + +export default function QuestionModal({ pregunta, categoria, mostrarFeedback, feedbackCorrecto, onResponder, onCerrar }: QuestionModalProps) { + const [tiempo, setTiempo] = useState(TIEMPO_LIMITE) + const intervalRef = useRef | null>(null) + const tiempoAgotadoRef = useRef(false) + + useEffect(() => { + if (mostrarFeedback) { + if (intervalRef.current) clearInterval(intervalRef.current) + return + } + + setTiempo(TIEMPO_LIMITE) + tiempoAgotadoRef.current = false + intervalRef.current = setInterval(() => { + setTiempo((prev) => { + if (prev <= 1) { + clearInterval(intervalRef.current!) + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => { if (intervalRef.current) clearInterval(intervalRef.current) } + }, [pregunta.id, mostrarFeedback]) + + useEffect(() => { + if (tiempo === 0 && !mostrarFeedback && !tiempoAgotadoRef.current) { + tiempoAgotadoRef.current = true + onResponder(null) + } + }, [tiempo, mostrarFeedback]) + + const porcentaje = (tiempo / TIEMPO_LIMITE) * 100 + const colorBarra = porcentaje > 50 ? '#22c55e' : porcentaje > 25 ? '#f97316' : '#ef4444' + + return ( +
+
+ + {!mostrarFeedback && ( + <> + {/* Header con categoría */} +
+ {categoria.emoji} +

{categoria.nombre}

+

{pregunta.pregunta}

+
+ + {/* Temporizador */} +
+
+ Tiempo restante + {tiempo}s +
+
+
+
+
+ + {/* Opciones */} +
+ {pregunta.opciones.map((opcion, i) => ( + + ))} +
+ + )} + + {/* Feedback */} + {mostrarFeedback && ( +
+ {feedbackCorrecto ? '🎉' : '❌'} +

+ {feedbackCorrecto ? '¡Correcto!' : '¡Incorrecto!'} +

+ {feedbackCorrecto + ?

{pregunta.retroalimentacion}

+ :

{tiempo === 0 ? 'Se agotó el tiempo.' : 'Respuesta incorrecta.'}

+ } + +
+ )} + +
+
+ ) +} diff --git a/src/components/game/ResultModal.tsx b/src/components/game/ResultModal.tsx new file mode 100644 index 0000000..8daf628 --- /dev/null +++ b/src/components/game/ResultModal.tsx @@ -0,0 +1,23 @@ +interface ResultModalProps { + totalPuntos: number + onCerrar: () => void +} + +export default function ResultModal({ totalPuntos, onCerrar }: ResultModalProps) { + return ( +
+
+ 🏆 +

¡Bien hecho!

+

Has concluido la actividad.

+

{totalPuntos} pts

+ +
+
+ ) +} diff --git a/src/components/game/Ruleta/RuletaSVG.tsx b/src/components/game/Ruleta/RuletaSVG.tsx new file mode 100644 index 0000000..49566ab --- /dev/null +++ b/src/components/game/Ruleta/RuletaSVG.tsx @@ -0,0 +1,99 @@ +import type { Categoria } from '../../../types' + +interface RuletaSVGProps { + categorias: Categoria[] + rotacion: number + isSpinning: boolean + onClick: () => void +} + +export default function RuletaSVG({ categorias, rotacion, isSpinning, onClick }: RuletaSVGProps) { + const cx = 200 + const cy = 200 + const r = 180 + const total = categorias.length + const angleStep = 360 / total + + function polarToCartesian(angle: number, radius: number) { + const rad = (angle - 90) * (Math.PI / 180) + return { + x: cx + radius * Math.cos(rad), + y: cy + radius * Math.sin(rad), + } + } + + function sectorPath(startAngle: number, endAngle: number) { + const start = polarToCartesian(startAngle, r) + const end = polarToCartesian(endAngle, r) + const largeArc = endAngle - startAngle > 180 ? 1 : 0 + return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y} Z` + } + + return ( +
+ + {/* Indicador */} +
+
+
+ + {/* Ruleta SVG */} + + {categorias.map((cat, i) => { + const startAngle = i * angleStep + const endAngle = startAngle + angleStep + const midAngle = startAngle + angleStep / 2 + const textPos = polarToCartesian(midAngle, r * 0.62) + + return ( + + {/* Sector */} + + {/* Emoji */} + + {cat.emoji} + + {/* Nombre */} + + {cat.nombre} + + + ) + })} + + {/* Centro */} + + + +
+ ) +} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx new file mode 100644 index 0000000..a787d16 --- /dev/null +++ b/src/components/layout/Footer.tsx @@ -0,0 +1,9 @@ +export default function Footer() { + return ( +
+
+ © {new Date().getFullYear()} Actividad Interactiva — Todos los derechos reservados +
+
+ ) +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..67ce4e7 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,10 @@ +export default function Header() { + return ( +
+
+ 🎡 Ruleta + Actividad Interactiva +
+
+ ) +} diff --git a/src/components/screens/Cover.tsx b/src/components/screens/Cover.tsx new file mode 100644 index 0000000..53c1421 --- /dev/null +++ b/src/components/screens/Cover.tsx @@ -0,0 +1,46 @@ +import hero from '../../assets/hero.png' +import Button from '../ui/Button' +import { useAudio } from '../../hooks/useAudio' + +interface CoverProps { + onStart: () => void +} + +export default function Cover({ onStart }: CoverProps) { + const { sounds } = useAudio() + + function handleStart() { + sounds.click?.play() + onStart() + } + return ( +
+ + {/* Texto izquierdo */} +
+

+ ¡Bienvenido a la Ruleta Interactiva! +

+

+ Pon a prueba tus conocimientos girando la ruleta. + Responde cada pregunta correctamente y acumula el mayor puntaje posible. +

+
+ +
+
+ + {/* Imagen derecha */} +
+ Ruleta Interactiva +
+ +
+ ) +} diff --git a/src/components/screens/Game.tsx b/src/components/screens/Game.tsx new file mode 100644 index 0000000..943222f --- /dev/null +++ b/src/components/screens/Game.tsx @@ -0,0 +1,72 @@ +import { useEffect } from 'react' +import { useRuleta } from '../../hooks/useRuleta' +import { sounds } from '../../audio/AudioController' +import QuestionModal from '../game/QuestionModal' +import RuletaSVG from '../game/Ruleta/RuletaSVG' + +interface GameProps { + onEnd: (aprobado: boolean) => void +} + +export default function Game({ onEnd }: GameProps) { + const { estado, categorias, girar, responder, cerrarFeedback, MAX_PREGUNTAS } = useRuleta() + + useEffect(() => { + if (estado.completado) onEnd(estado.aprobado) + }, [estado.completado]) + + function handleGirar() { + sounds.click?.play() + girar() + } + + return ( +
+ + {/* Instrucciones */} +
+

¿Cómo jugar?

+

+ Haz clic en la ruleta para girarla y responde correctamente para ganar puntos. +

+
+ +
+ + {/* Progreso de preguntas */} +
+ {Array.from({ length: MAX_PREGUNTAS }).map((_, i) => ( +
+ ))} +
+ + {/* Ruleta SVG */} + + +
+ + {/* Modal pregunta */} + {estado.preguntaActual && !estado.completado && ( + + )} + +
+ ) +} diff --git a/src/components/screens/Result.tsx b/src/components/screens/Result.tsx new file mode 100644 index 0000000..acb0601 --- /dev/null +++ b/src/components/screens/Result.tsx @@ -0,0 +1,52 @@ +interface ResultProps { + aprobado: boolean + sinIntentos: boolean + onReintentar: () => void +} + +export default function Result({ aprobado, sinIntentos, onReintentar }: ResultProps) { + return ( +
+ + {/* Aprobado */} + {aprobado && ( + <> + 🏆 +

¡Felicidades!

+

+ Has completado la actividad exitosamente. ¡Excelente desempeño! +

+ + )} + + {/* Reprobado con intentos disponibles */} + {!aprobado && !sinIntentos && ( + <> + 💪 +

¡Casi lo logras!

+

+ Aún te falta un poco. Vuelve a intentarlo, ¡tú puedes! +

+ + + )} + + {/* Sin intentos */} + {sinIntentos && ( + <> + 😔 +

Intentos agotados

+

+ Has utilizado todos tus intentos disponibles. Te recomendamos repasar el contenido del curso antes de volver a intentarlo. +

+ + )} + +
+ ) +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..91f56cb --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react' + +type Variant = 'primary' | 'secondary' | 'danger' | 'outline' +type Size = 'sm' | 'md' | 'lg' + +interface ButtonProps { + children: ReactNode + variant?: Variant + size?: Size + disabled?: boolean + onClick?: () => void + type?: 'button' | 'submit' | 'reset' + className?: string +} + +const variants: Record = { + primary: 'bg-purple-600 text-white hover:bg-purple-700 active:bg-purple-800', + secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 active:bg-gray-400', + danger: 'bg-red-500 text-white hover:bg-red-600 active:bg-red-700', + outline: 'border-2 border-purple-600 text-purple-600 hover:bg-purple-50 active:bg-purple-100', +} + +const sizes: Record = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-5 py-2.5 text-base', + lg: 'px-7 py-3.5 text-lg', +} + +export default function Button({ + children, + variant = 'primary', + size = 'md', + disabled = false, + onClick, + type = 'button', + className = '', +}: ButtonProps) { + return ( + + ) +} diff --git a/src/data/preguntas.json b/src/data/preguntas.json new file mode 100644 index 0000000..e37b50f --- /dev/null +++ b/src/data/preguntas.json @@ -0,0 +1,297 @@ +{ + "categorias": [ + { + "id": 0, + "nombre": "Historia", + "color": "#7C3AED", + "emoji": "🏛️", + "preguntas": [ + { + "id": 1, + "pregunta": "¿En qué año cayó el Muro de Berlín?", + "opciones": ["1987", "1989", "1991", "1993"], + "correcta": 1, + "retroalimentacion": "El Muro de Berlín cayó el 9 de noviembre de 1989, marcando el fin de la Guerra Fría." + }, + { + "id": 2, + "pregunta": "¿Quién fue el primer presidente de los Estados Unidos?", + "opciones": ["Abraham Lincoln", "Thomas Jefferson", "George Washington", "Benjamin Franklin"], + "correcta": 2, + "retroalimentacion": "George Washington fue el primer presidente de EE.UU., gobernando de 1789 a 1797." + }, + { + "id": 3, + "pregunta": "¿En qué año comenzó la Primera Guerra Mundial?", + "opciones": ["1912", "1914", "1916", "1918"], + "correcta": 1, + "retroalimentacion": "La Primera Guerra Mundial comenzó en 1914 tras el asesinato del archiduque Francisco Fernando." + }, + { + "id": 4, + "pregunta": "¿Qué civilización construyó las pirámides de Giza?", + "opciones": ["Griega", "Romana", "Egipcia", "Mesopotámica"], + "correcta": 2, + "retroalimentacion": "Las pirámides de Giza fueron construidas por los antiguos egipcios hace más de 4,500 años." + }, + { + "id": 5, + "pregunta": "¿En qué año llegó Cristóbal Colón a América?", + "opciones": ["1488", "1490", "1492", "1495"], + "correcta": 2, + "retroalimentacion": "Cristóbal Colón llegó a América el 12 de octubre de 1492." + }, + { + "id": 6, + "pregunta": "¿Quién fue Napoleón Bonaparte?", + "opciones": ["Rey de España", "Emperador de Francia", "Presidente de Italia", "General inglés"], + "correcta": 1, + "retroalimentacion": "Napoleón Bonaparte fue Emperador de Francia entre 1804 y 1815." + }, + { + "id": 7, + "pregunta": "¿En qué país se originó la Revolución Industrial?", + "opciones": ["Francia", "Alemania", "Estados Unidos", "Inglaterra"], + "correcta": 3, + "retroalimentacion": "La Revolución Industrial comenzó en Inglaterra a mediados del siglo XVIII." + }, + { + "id": 8, + "pregunta": "¿Cuál fue el primer país en conceder el voto a la mujer?", + "opciones": ["Francia", "Nueva Zelanda", "Estados Unidos", "Suecia"], + "correcta": 1, + "retroalimentacion": "Nueva Zelanda fue el primer país en conceder el voto a la mujer en 1893." + } + ] + }, + { + "id": 1, + "nombre": "Geografía", + "color": "#059669", + "emoji": "🌍", + "preguntas": [ + { + "id": 9, + "pregunta": "¿Cuál es el río más largo del mundo?", + "opciones": ["Amazonas", "Nilo", "Yangtsé", "Misisipi"], + "correcta": 1, + "retroalimentacion": "El río Nilo, con aproximadamente 6,650 km, es considerado el río más largo del mundo." + }, + { + "id": 10, + "pregunta": "¿Cuál es el país más grande del mundo por superficie?", + "opciones": ["China", "Canadá", "Estados Unidos", "Rusia"], + "correcta": 3, + "retroalimentacion": "Rusia es el país más grande del mundo con 17.1 millones de km²." + }, + { + "id": 11, + "pregunta": "¿En qué continente se encuentra el desierto del Sahara?", + "opciones": ["Asia", "América", "África", "Australia"], + "correcta": 2, + "retroalimentacion": "El Sahara se encuentra en África y es el desierto cálido más grande del mundo." + }, + { + "id": 12, + "pregunta": "¿Cuál es la capital de Australia?", + "opciones": ["Sídney", "Melbourne", "Canberra", "Brisbane"], + "correcta": 2, + "retroalimentacion": "Canberra es la capital de Australia, no Sídney como muchos creen." + }, + { + "id": 13, + "pregunta": "¿Cuántos océanos hay en el planeta?", + "opciones": ["3", "4", "5", "6"], + "correcta": 2, + "retroalimentacion": "Hay 5 océanos: Pacífico, Atlántico, Índico, Ártico y Antártico." + }, + { + "id": 14, + "pregunta": "¿Cuál es el volcán activo más alto del mundo?", + "opciones": ["Etna", "Kilimanjaro", "Ojos del Salado", "Cotopaxi"], + "correcta": 3, + "retroalimentacion": "El Cotopaxi en Ecuador es uno de los volcanes activos más altos del mundo con 5,897 metros." + }, + { + "id": 15, + "pregunta": "¿En qué país se encuentra la Torre de Pisa?", + "opciones": ["España", "Francia", "Italia", "Portugal"], + "correcta": 2, + "retroalimentacion": "La Torre de Pisa se encuentra en la ciudad de Pisa, Italia." + }, + { + "id": 16, + "pregunta": "¿Cuál es el país más pequeño del mundo?", + "opciones": ["Mónaco", "San Marino", "Liechtenstein", "Ciudad del Vaticano"], + "correcta": 3, + "retroalimentacion": "Ciudad del Vaticano es el país más pequeño del mundo con apenas 0.44 km²." + } + ] + }, + { + "id": 2, + "nombre": "Ciencia", + "color": "#DC2626", + "emoji": "🔬", + "preguntas": [ + { + "id": 17, + "pregunta": "¿Cuál es el elemento más abundante en el universo?", + "opciones": ["Oxígeno", "Carbono", "Hidrógeno", "Helio"], + "correcta": 2, + "retroalimentacion": "El hidrógeno es el elemento más abundante del universo, constituyendo el 75% de su masa." + }, + { + "id": 18, + "pregunta": "¿A qué velocidad viaja la luz en el vacío?", + "opciones": ["200,000 km/s", "300,000 km/s", "400,000 km/s", "150,000 km/s"], + "correcta": 1, + "retroalimentacion": "La luz viaja a aproximadamente 299,792 km/s en el vacío." + }, + { + "id": 19, + "pregunta": "¿Cuántos huesos tiene el cuerpo humano adulto?", + "opciones": ["196", "206", "216", "226"], + "correcta": 1, + "retroalimentacion": "El cuerpo humano adulto tiene 206 huesos." + }, + { + "id": 20, + "pregunta": "¿Cuál es el planeta más grande del sistema solar?", + "opciones": ["Saturno", "Neptuno", "Júpiter", "Urano"], + "correcta": 2, + "retroalimentacion": "Júpiter es el planeta más grande del sistema solar." + }, + { + "id": 21, + "pregunta": "¿Qué gas es el más abundante en la atmósfera terrestre?", + "opciones": ["Oxígeno", "Dióxido de carbono", "Nitrógeno", "Argón"], + "correcta": 2, + "retroalimentacion": "El nitrógeno constituye aproximadamente el 78% de la atmósfera terrestre." + }, + { + "id": 22, + "pregunta": "¿Cuántos cromosomas tiene una célula humana normal?", + "opciones": ["23", "44", "46", "48"], + "correcta": 2, + "retroalimentacion": "Las células humanas normales tienen 46 cromosomas, organizados en 23 pares." + }, + { + "id": 23, + "pregunta": "¿Quién formuló la teoría de la relatividad?", + "opciones": ["Isaac Newton", "Albert Einstein", "Nikola Tesla", "Stephen Hawking"], + "correcta": 1, + "retroalimentacion": "Albert Einstein formuló la teoría de la relatividad especial en 1905 y la general en 1915." + }, + { + "id": 24, + "pregunta": "¿Cuál es el símbolo químico del oro?", + "opciones": ["Go", "Ag", "Au", "Or"], + "correcta": 2, + "retroalimentacion": "El símbolo químico del oro es Au, del latín Aurum." + } + ] + }, + { + "id": 3, + "nombre": "Arte y Literatura", + "color": "#D97706", + "emoji": "🎨", + "preguntas": [ + { + "id": 25, + "pregunta": "¿Quién escribió 'Cien años de soledad'?", + "opciones": ["Mario Vargas Llosa", "Pablo Neruda", "Gabriel García Márquez", "Jorge Luis Borges"], + "correcta": 2, + "retroalimentacion": "Gabriel García Márquez escribió Cien años de soledad en 1967, obra cumbre del realismo mágico." + }, + { + "id": 26, + "pregunta": "¿Quién pintó 'La noche estrellada'?", + "opciones": ["Pablo Picasso", "Claude Monet", "Salvador Dalí", "Vincent van Gogh"], + "correcta": 3, + "retroalimentacion": "La noche estrellada fue pintada por Vincent van Gogh en 1889." + }, + { + "id": 27, + "pregunta": "¿En qué país nació Mozart?", + "opciones": ["Alemania", "Austria", "Italia", "Francia"], + "correcta": 1, + "retroalimentacion": "Wolfgang Amadeus Mozart nació en Salzburgo, Austria, en 1756." + }, + { + "id": 28, + "pregunta": "¿Quién escribió 'Don Quijote de la Mancha'?", + "opciones": ["Lope de Vega", "Miguel de Cervantes", "Francisco de Quevedo", "Garcilaso de la Vega"], + "correcta": 1, + "retroalimentacion": "Don Quijote de la Mancha fue escrita por Miguel de Cervantes y publicada en 1605." + }, + { + "id": 29, + "pregunta": "¿Quién compuso la Quinta Sinfonía?", + "opciones": ["Mozart", "Bach", "Beethoven", "Chopin"], + "correcta": 2, + "retroalimentacion": "La Quinta Sinfonía fue compuesta por Ludwig van Beethoven entre 1804 y 1808." + }, + { + "id": 30, + "pregunta": "¿En qué museo se encuentra la Mona Lisa?", + "opciones": ["Museo del Prado", "Museo Británico", "Museo del Louvre", "Museo Uffizi"], + "correcta": 2, + "retroalimentacion": "La Mona Lisa se encuentra en el Museo del Louvre en París, Francia." + }, + { + "id": 31, + "pregunta": "¿Quién escribió 'Romeo y Julieta'?", + "opciones": ["Charles Dickens", "William Shakespeare", "Jane Austen", "Victor Hugo"], + "correcta": 1, + "retroalimentacion": "Romeo y Julieta fue escrita por William Shakespeare alrededor de 1595." + }, + { + "id": 32, + "pregunta": "¿Qué movimiento artístico lideró Salvador Dalí?", + "opciones": ["Cubismo", "Impresionismo", "Surrealismo", "Expresionismo"], + "correcta": 2, + "retroalimentacion": "Salvador Dalí fue uno de los principales representantes del Surrealismo." + } + ] + } + ], + "bancoGenerico": [ + { + "id": 101, + "pregunta": "¿Cuál es el océano más grande del mundo?", + "opciones": ["Atlántico", "Índico", "Pacífico", "Ártico"], + "correcta": 2, + "retroalimentacion": "El océano Pacífico es el más grande, cubriendo más de 165 millones de km²." + }, + { + "id": 102, + "pregunta": "¿Cuántos planetas tiene el sistema solar?", + "opciones": ["7", "8", "9", "10"], + "correcta": 1, + "retroalimentacion": "El sistema solar tiene 8 planetas desde que Plutón fue reclasificado en 2006." + }, + { + "id": 103, + "pregunta": "¿Cuál es el animal terrestre más rápido?", + "opciones": ["León", "Guepardo", "Antílope", "Caballo"], + "correcta": 1, + "retroalimentacion": "El guepardo puede alcanzar velocidades de hasta 120 km/h, siendo el animal terrestre más rápido." + }, + { + "id": 104, + "pregunta": "¿En qué año se fundó la ONU?", + "opciones": ["1943", "1945", "1947", "1950"], + "correcta": 1, + "retroalimentacion": "La ONU fue fundada el 24 de octubre de 1945 tras el fin de la Segunda Guerra Mundial." + }, + { + "id": 105, + "pregunta": "¿Cuál es el idioma más hablado en el mundo?", + "opciones": ["Inglés", "Español", "Mandarín", "Hindi"], + "correcta": 2, + "retroalimentacion": "El mandarín es el idioma con más hablantes nativos, con más de 900 millones." + } + ] +} diff --git a/src/hooks/useAudio.ts b/src/hooks/useAudio.ts new file mode 100644 index 0000000..b6de53b --- /dev/null +++ b/src/hooks/useAudio.ts @@ -0,0 +1,23 @@ +import { useEffect, useCallback } from 'react' +import { audioController, sounds } from '../audio/AudioController' +import { createSound, stopAllSounds } from '../audio/ExtendedHowl' + +export function useAudio() { + useEffect(() => { + return () => audioController.stop() + }, []) + + const load = useCallback((url: string) => audioController.load(url), []) + const play = useCallback(() => audioController.play(), []) + const pause = useCallback(() => audioController.pause(), []) + const stop = useCallback(() => audioController.stop(), []) + const toggle = useCallback(() => audioController.toggle(), []) + const toggleMute = useCallback(() => audioController.toggleMute(), []) + const stopAll = useCallback(() => stopAllSounds(), []) + const playNew = useCallback((url: string) => { + const howl = createSound(url) + if (howl) audioController.stopAllAndPlay(howl) + }, []) + + return { load, play, pause, stop, toggle, toggleMute, stopAll, playNew, sounds, audioController } +} diff --git a/src/hooks/useIntentos.ts b/src/hooks/useIntentos.ts new file mode 100644 index 0000000..4262c47 --- /dev/null +++ b/src/hooks/useIntentos.ts @@ -0,0 +1,31 @@ +const KEY_INTENTOS = 'ruleta_intentos_usados' +const KEY_AGOTADOS = 'ruleta_intentos_agotados' +const MAX_INTENTOS = 3 + +function getIntentosUsados(): number { + return parseInt(sessionStorage.getItem(KEY_INTENTOS) ?? '0', 10) +} + +function setIntentosUsados(n: number): void { + sessionStorage.setItem(KEY_INTENTOS, String(n)) + if (n >= MAX_INTENTOS) { + sessionStorage.setItem(KEY_AGOTADOS, 'true') + } +} + +export function intentosAgotados(): boolean { + return sessionStorage.getItem(KEY_AGOTADOS) === 'true' +} + +export function intentosRestantes(): number { + return Math.max(0, MAX_INTENTOS - getIntentosUsados()) +} + +export function consumirIntento(): void { + setIntentosUsados(getIntentosUsados() + 1) +} + +export function resetIntentos(): void { + sessionStorage.removeItem(KEY_INTENTOS) + sessionStorage.removeItem(KEY_AGOTADOS) +} diff --git a/src/hooks/useRuleta.ts b/src/hooks/useRuleta.ts new file mode 100644 index 0000000..a98ba70 --- /dev/null +++ b/src/hooks/useRuleta.ts @@ -0,0 +1,134 @@ +import { useState, useRef } from 'react' +import type { Pregunta, Categoria, PreguntasData } from '../types' +import data from '../data/preguntas.json' +import { sounds } from '../audio/AudioController' + +const { categorias, bancoGenerico } = data as PreguntasData +const MAX_PREGUNTAS = 10 +const MIN_CORRECTAS = 8 +const SECCIONES = categorias.length +const ANGULO_SECCION = 360 / SECCIONES + +export interface EstadoJuego { + isSpinning: boolean + rotacion: number + preguntaActual: Pregunta | null + categoriaActual: Categoria | null + mostrarFeedback: boolean + feedbackCorrecto: boolean + preguntasRespondidas: number + correctas: number + completado: boolean + aprobado: boolean + preguntasUsadas: Record + bancoPreguntasUsadas: number[] +} + +export function useRuleta() { + const [estado, setEstado] = useState({ + isSpinning: false, + rotacion: 0, + preguntaActual: null, + categoriaActual: null, + mostrarFeedback: false, + feedbackCorrecto: false, + preguntasRespondidas: 0, + correctas: 0, + completado: false, + aprobado: false, + preguntasUsadas: {}, + bancoPreguntasUsadas: [], + }) + + const rotacionRef = useRef(0) + + function seleccionarPregunta(categoria: Categoria, preguntasUsadas: Record, bancoPreguntasUsadas: number[]): { pregunta: Pregunta, esGenerica: boolean } { + const usadas = preguntasUsadas[categoria.id] ?? [] + const disponibles = categoria.preguntas.filter((_, i) => !usadas.includes(i)) + + if (disponibles.length > 0) { + const idx = Math.floor(Math.random() * disponibles.length) + return { pregunta: disponibles[idx], esGenerica: false } + } + + // Banco genérico + const disponiblesGenericas = bancoGenerico.filter((_, i) => !bancoPreguntasUsadas.includes(i)) + const pool = disponiblesGenericas.length > 0 ? disponiblesGenericas : bancoGenerico + const idx = Math.floor(Math.random() * pool.length) + return { pregunta: pool[idx], esGenerica: true } + } + + function girar() { + if (estado.isSpinning || estado.completado) return + + sounds.wheelSpin?.play() + + const vueltas = 3 + Math.random() * 2 + const anguloFinal = Math.random() * 360 + const rotacionTotal = rotacionRef.current + vueltas * 360 + anguloFinal + rotacionRef.current = rotacionTotal + + setEstado((prev) => ({ ...prev, isSpinning: true, rotacion: rotacionTotal })) + + setTimeout(() => { + const anguloNormalizado = ((360 - (rotacionRef.current % 360)) + 360) % 360 + const indiceCategoria = Math.floor(anguloNormalizado / ANGULO_SECCION) % SECCIONES + const categoria = categorias[indiceCategoria] + + setEstado((prev) => { + const { pregunta, esGenerica } = seleccionarPregunta(categoria, prev.preguntasUsadas, prev.bancoPreguntasUsadas) + const originalIdx = esGenerica + ? bancoGenerico.indexOf(pregunta) + : categoria.preguntas.indexOf(pregunta) + + return { + ...prev, + preguntaActual: pregunta, + categoriaActual: categoria, + preguntasUsadas: esGenerica ? prev.preguntasUsadas : { + ...prev.preguntasUsadas, + [categoria.id]: [...(prev.preguntasUsadas[categoria.id] ?? []), originalIdx], + }, + bancoPreguntasUsadas: esGenerica + ? [...prev.bancoPreguntasUsadas, originalIdx] + : prev.bancoPreguntasUsadas, + } + }) + }, 4000) + } + + function responder(indiceOpcion: number | null) { + if (!estado.preguntaActual) return + const esCorrecta = indiceOpcion !== null && indiceOpcion === estado.preguntaActual.correcta + + if (esCorrecta) sounds.correct?.play() + else sounds.incorrect?.play() + + const nuevasCorrectas = estado.correctas + (esCorrecta ? 1 : 0) + const nuevasRespondidas = estado.preguntasRespondidas + 1 + const completado = nuevasRespondidas >= MAX_PREGUNTAS + const aprobado = completado && nuevasCorrectas >= MIN_CORRECTAS + + setEstado((prev) => ({ + ...prev, + correctas: nuevasCorrectas, + preguntasRespondidas: nuevasRespondidas, + mostrarFeedback: true, + feedbackCorrecto: esCorrecta, + completado, + aprobado, + })) + } + + function cerrarFeedback() { + setEstado((prev) => ({ + ...prev, + preguntaActual: null, + categoriaActual: null, + mostrarFeedback: false, + isSpinning: false, + })) + } + + return { estado, categorias, girar, responder, cerrarFeedback, MAX_PREGUNTAS, MIN_CORRECTAS } +} diff --git a/src/hooks/useSCORM.ts b/src/hooks/useSCORM.ts new file mode 100644 index 0000000..a33dc42 --- /dev/null +++ b/src/hooks/useSCORM.ts @@ -0,0 +1,46 @@ +import { useEffect } from 'react' +import { + initSCORM, + finishSCORM, + getScore, + setScore, + getLessonStatus, + setLessonStatus, + getLessonLocation, + setLessonLocation, + getSuspendData, + setSuspendData, + getStudentName, + getScormData, + setScormData, + completeLesson, + isConnected, +} from '../scorm/scorm' + +export function useSCORM() { + useEffect(() => { + initSCORM() + window.addEventListener('beforeunload', finishSCORM) + return () => { + window.removeEventListener('beforeunload', finishSCORM) + finishSCORM() + } + }, []) + + return { + isConnected, + getStudentName, + getScore, + setScore, + getLessonStatus, + setLessonStatus, + getLessonLocation, + setLessonLocation, + getSuspendData, + setSuspendData, + getScormData, + setScormData, + completeLesson, + finishSCORM, + } +} diff --git a/src/hooks/useScale.ts b/src/hooks/useScale.ts new file mode 100644 index 0000000..0b46150 --- /dev/null +++ b/src/hooks/useScale.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef } from 'react' + +const BASE_WIDTH = 1920 +const BASE_HEIGHT = 1080 + +export function useScale() { + const ref = useRef(null) + + useEffect(() => { + function scale() { + const el = ref.current + if (!el) return + + const scaleX = window.innerWidth / BASE_WIDTH + const scaleY = window.innerHeight / BASE_HEIGHT + const s = Math.min(scaleX, scaleY) + + el.style.transform = `scale(${s})` + el.style.transformOrigin = 'top left' + el.style.width = `${BASE_WIDTH}px` + el.style.height = `${BASE_HEIGHT}px` + el.style.position = 'absolute' + el.style.left = `${(window.innerWidth - BASE_WIDTH * s) / 2}px` + el.style.top = `${(window.innerHeight - BASE_HEIGHT * s) / 2}px` + el.style.overflow = 'hidden' + } + + scale() + window.addEventListener('resize', scale) + return () => window.removeEventListener('resize', scale) + }, []) + + return ref +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..ebda555 --- /dev/null +++ b/src/layouts/MainLayout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' +import { useScale } from '../hooks/useScale' +import Header from '../components/layout/Header' +import Footer from '../components/layout/Footer' + +interface MainLayoutProps { + children: ReactNode +} + +export default function MainLayout({ children }: MainLayoutProps) { + const scaleRef = useScale() + + return ( +
+
+
+
+ {children} +
+
+
+
+ ) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..572d6ac --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +// Simulador SCORM solo en desarrollo +if (import.meta.env.DEV) { + const { Scorm12API } = await import('scorm-again') + const api = new Scorm12API({}) + window.API = api + api.lmsInitialize() +} + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/scorm/scorm.ts b/src/scorm/scorm.ts new file mode 100644 index 0000000..4cd9fd5 --- /dev/null +++ b/src/scorm/scorm.ts @@ -0,0 +1,145 @@ +interface SCORM12API { + LMSInitialize: (s: string) => string + LMSGetValue: (key: string) => string + LMSSetValue: (key: string, value: string) => string + LMSCommit: (s: string) => string + LMSFinish: (s: string) => string +} + +declare global { + interface Window { + API?: SCORM12API + } +} + +let sessionStartTime: number | null = null +let scormFinished = false + +// ─── API ────────────────────────────────────────────────────────────────────── + +function getAPI(): SCORM12API | null { + if (window.API) return window.API + if (window.parent?.API) return window.parent.API + return null +} + +function isConnected(): boolean { + return getAPI() !== null +} + +// ─── Get / Set genérico con fallback a sessionStorage ──────────────────────── + +export function getScormData(key: string): string { + const api = getAPI() + if (api) return api.LMSGetValue(key) ?? '' + return sessionStorage.getItem(key) ?? '' +} + +export function setScormData(key: string, value: string): boolean { + const api = getAPI() + if (api) { + const ok = api.LMSSetValue(key, value) + api.LMSCommit('') + return ok === 'true' + } + sessionStorage.setItem(key, value) + return true +} + +// ─── Inicialización ─────────────────────────────────────────────────────────── + +export function initSCORM(): void { + const api = getAPI() + if (api) { + api.LMSInitialize('') + const status = api.LMSGetValue('cmi.core.lesson_status') + if (status === 'not attempted') { + api.LMSSetValue('cmi.core.lesson_status', 'incomplete') + api.LMSCommit('') + } + } + sessionStartTime = Date.now() +} + +// ─── Lesson status ──────────────────────────────────────────────────────────── + +export function getLessonStatus(): string { + return getScormData('cmi.core.lesson_status') +} + +export function setLessonStatus(status: 'passed' | 'failed' | 'completed' | 'incomplete'): boolean { + return setScormData('cmi.core.lesson_status', status) +} + +// ─── Lesson location ────────────────────────────────────────────────────────── + +export function getLessonLocation(): string { + return getScormData('cmi.core.lesson_location') +} + +export function setLessonLocation(location: string): boolean { + return setScormData('cmi.core.lesson_location', location) +} + +// ─── Score ──────────────────────────────────────────────────────────────────── + +export function getScore(): number | null { + const val = getScormData('cmi.core.score.raw') + return val !== '' ? Number(val) : null +} + +export function setScore(score: number, maxScore = 100, minScore = 0): void { + setScormData('cmi.core.score.raw', String(score)) + setScormData('cmi.core.score.max', String(maxScore)) + setScormData('cmi.core.score.min', String(minScore)) +} + +// ─── Suspend data ───────────────────────────────────────────────────────────── + +export function getSuspendData(): T | null { + const val = getScormData('cmi.suspend_data') + if (!val) return null + try { + return JSON.parse(val) as T + } catch { + return null + } +} + +export function setSuspendData(data: unknown): boolean { + return setScormData('cmi.suspend_data', JSON.stringify(data)) +} + +// ─── Student name ───────────────────────────────────────────────────────────── + +export function getStudentName(): string { + return getScormData('cmi.core.student_name') +} + +// ─── Finish con session_time ────────────────────────────────────────────────── + +export function finishSCORM(): void { + if (scormFinished) return + const api = getAPI() + if (!api) return + + if (sessionStartTime) { + const elapsed = Math.floor((Date.now() - sessionStartTime) / 1000) + const hh = String(Math.floor(elapsed / 3600)).padStart(2, '0') + const mm = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0') + const ss = String(elapsed % 60).padStart(2, '0') + api.LMSSetValue('cmi.core.session_time', `${hh}:${mm}:${ss}`) + } + + api.LMSCommit('') + api.LMSFinish('') + scormFinished = true +} + +// ─── Helper: completar lección ──────────────────────────────────────────────── + +export function completeLesson(): void { + setLessonStatus('completed') +} + +export { isConnected } diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..b196eb5 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,20 @@ +export interface Pregunta { + id: number + pregunta: string + opciones: string[] + correcta: number + retroalimentacion: string +} + +export interface Categoria { + id: number + nombre: string + color: string + emoji: string + preguntas: Pregunta[] +} + +export interface PreguntasData { + categorias: Categoria[] + bancoGenerico: Pregunta[] +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..a6ca08b --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +declare module '*.png' { + const src: string + export default src +} + +declare module '*.svg' { + const src: string + export default src +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..109f0ac --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..0616e59 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [react(), tailwindcss()], +})