commit 6e85917f730095e62f3c696e81706706e684fcef Author: Gregor Klevze Date: Sat Aug 15 10:59:25 2026 +0200 Initial commit: Zabavna Matematika web and Android app. Kids math practice (SL/EN) in a Vite React app, wrapped with Capacitor for Android (net.bit76.matematika). Includes session refactor, i18n, and text-free difficulty icons with translated overlays. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93c7e67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +web/node_modules/ +web/dist/ +web/android/app/build/ +web/android/app/.cxx/ +web/android/.gradle/ +web/android/local.properties +web/android/captures/ +web/android/.idea/ +web/android/*.iml +*.log +.DS_Store +Thumbs.db diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..09ac762 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#2C2756", + "titleBar.activeBackground": "#3D3778", + "titleBar.activeForeground": "#FBFAFD", + "titleBar.inactiveBackground": "#2C2756", + "titleBar.inactiveForeground": "#FBFAFD", + "statusBar.background": "#2C2756", + "statusBar.foreground": "#FBFAFD", + "statusBar.debuggingBackground": "#2C2756", + "statusBar.debuggingForeground": "#FBFAFD", + "statusBar.noFolderBackground": "#2C2756", + "statusBar.noFolderForeground": "#FBFAFD" + }, + "files.exclude": { + "**/node_modules": true + }, + "search.exclude": { + "**/node_modules": true + }, + "files.watcherExclude": { + "**/node_modules/**": true + } +} \ No newline at end of file diff --git a/LEVELS.md b/LEVELS.md new file mode 100644 index 0000000..b30a5b1 --- /dev/null +++ b/LEVELS.md @@ -0,0 +1,83 @@ +# Math Game – Difficulty Levels + +This document describes **how the game works today**. + +Difficulty changes **how the player answers** and some number caps. +The player still chooses **which operations** to practice in Settings (`+`, `−`, `×`, `÷`). + +--- + +## Overview + +Three difficulties: + +| Key | Label | Input | Timer | +|---|---|---|---| +| `easy` | Lahko | 4 multiple-choice buttons | Stopwatch (on) | +| `medium` | Srednje | 6 multiple-choice buttons | Stopwatch (on) | +| `hard` | Težko | Numeric keypad + OK | Stopwatch (on) | + +Settings also choose: + +- Which operations are active +- How many questions (`stevilo_racunov`) +- Add/subtract range (`max_number`) +- Multiplication factors (`za_mnozenje`) +- Division divisors (`za_deljenje`) + +--- + +## Easy (`easy` / Lahko) + +- Input: **4 choices** +- Timer: stopwatch, enabled +- Negatives: not allowed +- Multiply other-factor cap: **1–10** +- Add/subtract range: from settings (`max_number`) + +--- + +## Medium (`medium` / Srednje) + +- Input: **6 choices** +- Timer: stopwatch, enabled +- Negatives: not allowed +- Multiply other-factor cap: **1–12** +- Add/subtract range: from settings (`max_number`) + +--- + +## Hard (`hard` / Težko) + +- Input: **keypad** (digits, backspace, `OK`; `±` only if `allowNegative` is on) +- No multiple-choice buttons +- Timer: stopwatch, enabled +- Negatives: **off** (`allowNegative: false`) unless flipped later in `levels.js` +- Multiply other-factor cap: **1–20** +- Add/subtract range: from settings (`max_number`) +- Division: exact results only + +--- + +## Code map + +```js +// web/src/levels.js +LEVELS = { + easy: { inputType: 'choices', choicesCount: 4, timerEnabled: true, allowNegative: false, multiplicationMaxA: 10 }, + medium: { inputType: 'choices', choicesCount: 6, timerEnabled: true, allowNegative: false, multiplicationMaxA: 12 }, + hard: { inputType: 'keypad', choicesCount: 0, timerEnabled: true, allowNegative: false, multiplicationMaxA: 20 } +} +``` + +Questions are built by `buildSession(config, level)` in `web/src/questionGenerator.js`. +The Game screen only renders `question.text` and checks `question.result`. + +--- + +## Design principles + +- Difficulty increases interaction complexity, not intimidation +- Same play screen, different answer component +- Hard is framed as a challenge, not a punishment +- Unfinished modes (rounding, dice, calculator, equations, text problems) are **not** in the live path diff --git a/README.md b/README.md new file mode 100644 index 0000000..80bdd1e --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Zabavna Matematika + +Kids math practice game in Slovene. Players pick `+`, `−`, `×`, and/or `÷`, then play a short session. Difficulty changes **how** they answer, not which operations are allowed. + +| Difficulty | Label | How you answer | +|---|---|---| +| Easy | Lahko | 4 large choice buttons | +| Medium | Srednje | 6 choice buttons | +| Hard | Težko | On-screen keypad + OK | + +One game codebase: **web** in the browser, **Android** as a Capacitor shell around the same build. There is no backend. Settings, audio, and language (`sl` / `en`) stay in `localStorage`. The UI follows a saved language, otherwise the device (`sl*` → Slovene, everything else → English). Switch it in Settings (gear). + +## Play (browser) + +From the repo root: + +```bash +cd web +npm install +npm run dev +``` + +Or from the root: `npm run dev` (forwards into `web/`). + +Open the URL Vite prints (usually `http://localhost:5173`). + +1. Wait for **Nadaljuj** after assets load. +2. Tap which operations to practice and a difficulty. +3. **IGRAJ** → confirm question count / ranges → play. + +`npm run build` writes a static site to `web/dist/`. +`npm run preview` serves that build. + +The app uses relative asset paths (`base: './'`), so `dist/` can go in a subfolder on any static host. + +## Android + +Same game, wrapped for phones and tablets. Details: [docs/ANDROID.md](docs/ANDROID.md). + +```bash +cd web +npm install +npm run android +``` + +That builds the web app, syncs it into `web/android/`, and opens Android Studio. You need Android Studio + an SDK / emulator (or a device). + +## Project layout + +``` +ZabavnaMatematika/ + README.md + LEVELS.md + docs/ + package.json root shortcuts (dev, build, test, android) + web/ Vite + React game (the product) + src/ + public/ + capacitor.config.json + android/ Capacitor Android project (after first sync) +``` + +## Docs + +- [LEVELS.md](LEVELS.md) — what each difficulty does +- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) — architecture, session loop, tests +- [docs/ANDROID.md](docs/ANDROID.md) — Play Store / Capacitor workflow +- [web/public/privacy.html](web/public/privacy.html) — privacy policy (Slovene) + +## Tests + +```bash +npm test # from repo root, or: cd web && npm test +npm run smoke # needs Playwright; start preview first +``` + +```bash +npm run preview -- --host 127.0.0.1 --port 4175 +# other terminal +set SMOKE_URL=http://127.0.0.1:4175/ +npm run smoke +``` + +Playwright is not a project dependency. Install it locally if you want the smoke script (`npm install --no-save playwright` inside `web/`). diff --git a/docs/ANDROID.md b/docs/ANDROID.md new file mode 100644 index 0000000..7724766 --- /dev/null +++ b/docs/ANDROID.md @@ -0,0 +1,79 @@ +# Android + +Zabavna Matematika on the phone is the **same web game**, running in a Capacitor WebView. Phones and tablets share one APK. Layout is CSS (already tuned for ~390px and up), not a second codebase. + +## What lives where + +| Path | Role | +|---|---| +| `web/src/` | The game | +| `web/dist/` | Vite production build (synced into Android) | +| `web/capacitor.config.json` | App id `net.bit76.matematika`, `webDir: dist` | +| `web/android/` | Gradle / Android Studio project (generated, then committed) | + +Do not start a Kotlin/Compose game. Change play logic in `web/src/` only, then sync. + +## First-time machine setup + +1. Install [Android Studio](https://developer.android.com/studio). +2. Install an SDK (API 24+ is enough for Capacitor 7) and a virtual device, or plug in a phone with USB debugging. +3. Accept licenses if Studio asks. + +## Commands + +From the repo root or `web/`: + +```bash +cd web +npm install +npm run android +``` + +That runs `vite build`, `cap sync android`, then `cap open android`. In Studio press Run. + +After you change JS/CSS/assets: + +```bash +npm run cap:sync +``` + +Then Run again in Android Studio. Skip a full `cap add android` unless the `android/` folder is missing. + +If `android/` was never generated (clone of a repo that omitted it): + +```bash +cd web +npm run build +npx cap add android +npx cap sync +``` + +## App identity + +Set in `web/capacitor.config.json`: + +- **Application id:** `net.bit76.matematika` (Play Console package name) +- **Display name:** Zabavna Matematika + +Change these before the first Play Store listing. Renaming later is painful. + +## Icons and splash + +Replace the Capacitor placeholders in: + +- `web/android/app/src/main/res/mipmap-*/` +- `web/android/app/src/main/res/drawable*/` + +Keep one icon set. Tablet vs phone is the same WebView. + +## Store notes + +- Privacy policy: `web/public/privacy.html` (also served on the web host). +- Orientation: portrait is already declared in the PWA manifest; you can match that in `AndroidManifest.xml` if you want to lock it. +- Offline: the existing service worker still helps inside the WebView; Capacitor also ships the `dist/` files on device, so the game works without a network after install. + +## What not to do + +- Do not add `apps/android` with a rewritten UI. +- Do not copy `questionGenerator.js` into Java/Kotlin. +- Do not treat tablets as a separate flavor unless you later need a different Play listing. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..e9a6872 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,95 @@ +# Development + +The playable app lives in `web/`. Stack: React 18, Vite 5, Tailwind 3. Android is the same app inside a Capacitor WebView (`web/android/`). No TypeScript, no backend. + +## Screens + +`App.jsx` switches three states: + +1. **Loading** — preload images / audio / the settings video, then **Nadaljuj** +2. **Settings** — name, operations, difficulty, audio, start modal +3. **Game** — one session, then game-over overlay + +Audio prefs persist as `matematika_music`. Game settings persist as `matematika_settings`. Language persists as `matematika_lang` (`sl` | `en`). + +UI copy lives in `web/src/i18n/sl.json` and `en.json`. Screens call `t('key')` from `useI18n()`. Difficulty is stored as `easy` / `medium` / `hard`, never as a translated label. To add a language, add a JSON file with the same keys, register it in `i18n/core.js`, and add a button in Settings. + +## Session loop + +`Game.jsx` only runs a session. It does not invent operands. + +1. `buildSession(config, level)` in `questionGenerator.js` builds the full list. +2. `showQuestion(i)` shows item `i` from that list. +3. `resolveAnswer(given)` scores a tap or keypad OK. +4. After the last item, `GameOver` offers restart (rebuilds the pool) or back to settings. + +Question index is a ref, so restart cannot immediately re-trigger game-over from a stale counter. + +Each question looks like `{ op, a, b, result, text }`. +`op` is one of `add` | `sub` | `mul` | `div` (`src/config.js`). + +## Difficulty vs settings + +- **Settings** choose operations, question count, add/sub range (`max_number`), multiply factors, divide divisors. +- **Difficulty** (`src/levels.js`) chooses input UI and some caps: + +| Key | Input | Choices | Multiply other-factor cap | +|---|---|---|---| +| `easy` | choices | 4 | 1–10 | +| `medium` | choices | 6 | 1–12 | +| `hard` | keypad | — | 1–20 | + +Negatives are off (`allowNegative: false`). The keypad hides `±` unless that flag is flipped. + +Full product wording: [LEVELS.md](../LEVELS.md). + +## Source map + +``` +web/src/ + App.jsx + main.jsx boot + service worker register + config.js defaultConfig, OPS, mergeConfig + levels.js easy / medium / hard + questionGenerator.js + − × ÷ factory + distractors + loadAssets.js + hooks/useAnimatedBackgrounds.js + components/ + Loading.jsx + Settings.jsx + Game.jsx session only + AnswerChoices.jsx + Keypad.jsx + GameQuestionCard.jsx + GameToolbar.jsx + GameOver.jsx + ExitConfirm.jsx + BackgroundMusic.jsx + ToastContainer.jsx + UpdateBanner.jsx + NumberPills.jsx +``` + +`public/sw.js` caches the shell using paths relative to its own scope (safe with Vite `base: './'`). + +## Adding an operation later + +Keep generation out of `Game.jsx`: + +1. Add a named op in `config.js` if needed. +2. Teach `buildSession` how to emit `{ op, a, b, result, text }`. +3. Add a settings toggle only when the UI exists. +4. Cover it in `questionGenerator.test.js`. + +Rounding, dice, calculator, equations, and text problems were removed from the live path on purpose. + +## Local checks + +```bash +npm test +npm run build +``` + +Android store build: [ANDROID.md](ANDROID.md). + +Optional Playwright smoke (`scripts/smoke.mjs`) walks loading → easy → restart → medium (wrong then right) → hard keypad (empty OK disabled, no `±`) → a 390×844 viewport. diff --git a/package.json b/package.json new file mode 100644 index 0000000..1433377 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "zabavna-matematika", + "private": true, + "description": "Zabavna Matematika — web app and Android shell", + "scripts": { + "dev": "npm run dev --prefix web", + "build": "npm run build --prefix web", + "preview": "npm run preview --prefix web", + "test": "npm test --prefix web", + "smoke": "npm run smoke --prefix web", + "cap:sync": "npm run cap:sync --prefix web", + "android": "npm run android --prefix web" + } +} diff --git a/web/android/.gitignore b/web/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/web/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/web/android/app/.gitignore b/web/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/web/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/web/android/app/build.gradle b/web/android/app/build.gradle new file mode 100644 index 0000000..9d95f15 --- /dev/null +++ b/web/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace "net.bit76.matematika" + compileSdk rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "net.bit76.matematika" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/web/android/app/capacitor.build.gradle b/web/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/web/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/web/android/app/proguard-rules.pro b/web/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/web/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/web/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/web/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/web/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/web/android/app/src/main/AndroidManifest.xml b/web/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..be28c69 --- /dev/null +++ b/web/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/android/app/src/main/java/net/bit76/matematika/MainActivity.java b/web/android/app/src/main/java/net/bit76/matematika/MainActivity.java new file mode 100644 index 0000000..1a8b6c3 --- /dev/null +++ b/web/android/app/src/main/java/net/bit76/matematika/MainActivity.java @@ -0,0 +1,5 @@ +package net.bit76.matematika; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity {} diff --git a/web/android/app/src/main/res/drawable-land-hdpi/splash.png b/web/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000..e31573b Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-mdpi/splash.png b/web/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-xhdpi/splash.png b/web/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000..8077255 Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/web/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000..14c6c8f Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/web/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000..244ca25 Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-hdpi/splash.png b/web/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000..74faaa5 Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-mdpi/splash.png b/web/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000..e944f4a Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-xhdpi/splash.png b/web/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000..564a82f Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/web/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000..bfabe68 Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/web/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000..6929071 Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/web/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/web/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/web/android/app/src/main/res/drawable/ic_launcher_background.xml b/web/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/web/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/android/app/src/main/res/drawable/splash.png b/web/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/web/android/app/src/main/res/drawable/splash.png differ diff --git a/web/android/app/src/main/res/layout/activity_main.xml b/web/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/web/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c023e50 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2127973 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b441f37 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..72905b8 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8ed0605 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9502e47 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..4d1e077 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df0f158 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..853db04 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6cdf97c Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2960cbb Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e3093a Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..46de6e2 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2ea9ab Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a40d73e Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/values/ic_launcher_background.xml b/web/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/web/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/web/android/app/src/main/res/values/strings.xml b/web/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..8333be3 --- /dev/null +++ b/web/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Zabavna Matematika + Zabavna Matematika + net.bit76.matematika + net.bit76.matematika + diff --git a/web/android/app/src/main/res/values/styles.xml b/web/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/web/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/xml/file_paths.xml b/web/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/web/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/web/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/web/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/web/android/build.gradle b/web/android/build.gradle new file mode 100644 index 0000000..3d11b7e --- /dev/null +++ b/web/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.2' + classpath 'com.google.gms:google-services:4.4.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/web/android/capacitor.settings.gradle b/web/android/capacitor.settings.gradle new file mode 100644 index 0000000..9a5fa87 --- /dev/null +++ b/web/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') diff --git a/web/android/gradle.properties b/web/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/web/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/web/android/gradle/gradle-daemon-jvm.properties b/web/android/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..baa28d1 --- /dev/null +++ b/web/android/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/web/android/gradle/wrapper/gradle-wrapper.jar b/web/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/web/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/web/android/gradle/wrapper/gradle-wrapper.properties b/web/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ed4c299 --- /dev/null +++ b/web/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/web/android/gradlew b/web/android/gradlew new file mode 100644 index 0000000..f5feea6 --- /dev/null +++ b/web/android/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/web/android/gradlew.bat b/web/android/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/web/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/web/android/settings.gradle b/web/android/settings.gradle new file mode 100644 index 0000000..bbf73dd --- /dev/null +++ b/web/android/settings.gradle @@ -0,0 +1,8 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' +} +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/web/android/variables.gradle b/web/android/variables.gradle new file mode 100644 index 0000000..2c8e408 --- /dev/null +++ b/web/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 23 + compileSdkVersion = 35 + targetSdkVersion = 35 + androidxActivityVersion = '1.9.2' + androidxAppCompatVersion = '1.7.0' + androidxCoordinatorLayoutVersion = '1.2.0' + androidxCoreVersion = '1.15.0' + androidxFragmentVersion = '1.8.4' + coreSplashScreenVersion = '1.0.1' + androidxWebkitVersion = '1.12.1' + junitVersion = '4.13.2' + androidxJunitVersion = '1.2.1' + androidxEspressoCoreVersion = '3.6.1' + cordovaAndroidVersion = '10.1.1' +} \ No newline at end of file diff --git a/web/capacitor.config.json b/web/capacitor.config.json new file mode 100644 index 0000000..f69d2ea --- /dev/null +++ b/web/capacitor.config.json @@ -0,0 +1,8 @@ +{ + "appId": "net.bit76.matematika", + "appName": "Zabavna Matematika", + "webDir": "dist", + "android": { + "allowMixedContent": false + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..119ab64 --- /dev/null +++ b/web/index.html @@ -0,0 +1,27 @@ + + + + + + Matematika — Kids + + + + + + + + + + + + + + + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..35d5ad2 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,3736 @@ +{ + "name": "zabavna-matematika-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zabavna-matematika-web", + "version": "0.1.0", + "dependencies": { + "@capacitor/android": "^7.4.2", + "@capacitor/core": "^7.4.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@capacitor/cli": "^7.4.2", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.23", + "tailwindcss": "^3.4.7", + "vite": "^5.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capacitor/android": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.6.8.tgz", + "integrity": "sha512-N5LXe1ls+TA0mq+RhtyDdP1JISzAAI+OuLGL3G7OWQxfQQOZYkiEjEJMYqMV6JR421IqpRsYj08JOfC0PFC3NA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^7.6.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.8.tgz", + "integrity": "sha512-sJYLoSS1wVHjDPwRc0grtNZcQZnkQc7TJKoVZdHL2HI5TPCq+27WBrMAo0mXvQQSH2axPYCz5cyRrcqqakOvxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@capacitor/cli/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/core": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.6.8.tgz", + "integrity": "sha512-x1xfxvNcTSnj6OoYaAB0n0a0HRvi5f2YjcVnMLxpMhbHDgGDAzzN9z8cnPCYV+SKTvqGF1hezHm82Op32XaAGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.11.tgz", + "integrity": "sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", + "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.277", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", + "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", + "dev": true, + "license": "ISC" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "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/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..3a61c5f --- /dev/null +++ b/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "zabavna-matematika-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "node --test src/questionGenerator.test.js src/i18n/core.test.js", + "smoke": "node scripts/smoke.mjs", + "cap:sync": "npm run build && npx cap sync android", + "android": "npm run cap:sync && npx cap open android" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@capacitor/core": "^7.4.2", + "@capacitor/android": "^7.4.2" + }, + "devDependencies": { + "@capacitor/cli": "^7.4.2", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.23", + "tailwindcss": "^3.4.7", + "vite": "^5.0.0" + } +} diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/web/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/public/favicon/apple-touch-icon.png b/web/public/favicon/apple-touch-icon.png new file mode 100644 index 0000000..aea8c7c Binary files /dev/null and b/web/public/favicon/apple-touch-icon.png differ diff --git a/web/public/favicon/favicon-96x96.png b/web/public/favicon/favicon-96x96.png new file mode 100644 index 0000000..7a265b6 Binary files /dev/null and b/web/public/favicon/favicon-96x96.png differ diff --git a/web/public/favicon/favicon.ico b/web/public/favicon/favicon.ico new file mode 100644 index 0000000..c209a22 Binary files /dev/null and b/web/public/favicon/favicon.ico differ diff --git a/web/public/favicon/favicon.svg b/web/public/favicon/favicon.svg new file mode 100644 index 0000000..ae7aea0 --- /dev/null +++ b/web/public/favicon/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/web/public/favicon/site.webmanifest b/web/public/favicon/site.webmanifest new file mode 100644 index 0000000..cd051a4 --- /dev/null +++ b/web/public/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Zabavna matematika", + "short_name": "Matematika", + "icons": [ + { + "src": "/favicon/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/favicon/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/web/public/favicon/web-app-manifest-192x192.png b/web/public/favicon/web-app-manifest-192x192.png new file mode 100644 index 0000000..6b8e0b1 Binary files /dev/null and b/web/public/favicon/web-app-manifest-192x192.png differ diff --git a/web/public/favicon/web-app-manifest-512x512.png b/web/public/favicon/web-app-manifest-512x512.png new file mode 100644 index 0000000..e41cdc0 Binary files /dev/null and b/web/public/favicon/web-app-manifest-512x512.png differ diff --git a/web/public/gfx/back/background.webp b/web/public/gfx/back/background.webp new file mode 100644 index 0000000..fba594c Binary files /dev/null and b/web/public/gfx/back/background.webp differ diff --git a/web/public/gfx/back/background2.webp b/web/public/gfx/back/background2.webp new file mode 100644 index 0000000..09e89d8 Binary files /dev/null and b/web/public/gfx/back/background2.webp differ diff --git a/web/public/gfx/levels/deljenje.jpg b/web/public/gfx/levels/deljenje.jpg new file mode 100644 index 0000000..d323876 Binary files /dev/null and b/web/public/gfx/levels/deljenje.jpg differ diff --git a/web/public/gfx/levels/level1.jpg b/web/public/gfx/levels/level1.jpg new file mode 100644 index 0000000..155d9f8 Binary files /dev/null and b/web/public/gfx/levels/level1.jpg differ diff --git a/web/public/gfx/levels/level2.jpg b/web/public/gfx/levels/level2.jpg new file mode 100644 index 0000000..d45a68c Binary files /dev/null and b/web/public/gfx/levels/level2.jpg differ diff --git a/web/public/gfx/levels/level3.jpg b/web/public/gfx/levels/level3.jpg new file mode 100644 index 0000000..43c7556 Binary files /dev/null and b/web/public/gfx/levels/level3.jpg differ diff --git a/web/public/gfx/levels/mnozenje.jpg b/web/public/gfx/levels/mnozenje.jpg new file mode 100644 index 0000000..f80dff2 Binary files /dev/null and b/web/public/gfx/levels/mnozenje.jpg differ diff --git a/web/public/gfx/levels/odstevanje.jpg b/web/public/gfx/levels/odstevanje.jpg new file mode 100644 index 0000000..bed3e34 Binary files /dev/null and b/web/public/gfx/levels/odstevanje.jpg differ diff --git a/web/public/gfx/levels/sestevanje.jpg b/web/public/gfx/levels/sestevanje.jpg new file mode 100644 index 0000000..2b76c27 Binary files /dev/null and b/web/public/gfx/levels/sestevanje.jpg differ diff --git a/web/public/gfx/logo.png b/web/public/gfx/logo.png new file mode 100644 index 0000000..732278b Binary files /dev/null and b/web/public/gfx/logo.png differ diff --git a/web/public/gfx/robots/robot1.png b/web/public/gfx/robots/robot1.png new file mode 100644 index 0000000..b09e262 Binary files /dev/null and b/web/public/gfx/robots/robot1.png differ diff --git a/web/public/gfx/robots/robot2.png b/web/public/gfx/robots/robot2.png new file mode 100644 index 0000000..f515005 Binary files /dev/null and b/web/public/gfx/robots/robot2.png differ diff --git a/web/public/gfx/robots/robot3.png b/web/public/gfx/robots/robot3.png new file mode 100644 index 0000000..84842c2 Binary files /dev/null and b/web/public/gfx/robots/robot3.png differ diff --git a/web/public/gfx/robots/robot4.png b/web/public/gfx/robots/robot4.png new file mode 100644 index 0000000..fe31a70 Binary files /dev/null and b/web/public/gfx/robots/robot4.png differ diff --git a/web/public/gfx/robots/robot5.png b/web/public/gfx/robots/robot5.png new file mode 100644 index 0000000..d0d8108 Binary files /dev/null and b/web/public/gfx/robots/robot5.png differ diff --git a/web/public/gfx/robots/robot6.png b/web/public/gfx/robots/robot6.png new file mode 100644 index 0000000..7a1baea Binary files /dev/null and b/web/public/gfx/robots/robot6.png differ diff --git a/web/public/gfx/robots/robot7.png b/web/public/gfx/robots/robot7.png new file mode 100644 index 0000000..e043d57 Binary files /dev/null and b/web/public/gfx/robots/robot7.png differ diff --git a/web/public/gfx/star2.png b/web/public/gfx/star2.png new file mode 100644 index 0000000..0e12e6e Binary files /dev/null and b/web/public/gfx/star2.png differ diff --git a/web/public/manifest.json b/web/public/manifest.json new file mode 100644 index 0000000..4fdc587 --- /dev/null +++ b/web/public/manifest.json @@ -0,0 +1,109 @@ +{ + "name": "Zabavna matematika", + "short_name": "Matematika", + "lang": "hr-HR", + "description": "Interactive math games and exercises for kids.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#ffffff", + "theme_color": "#ffffff", + "icons": [ + { + "src": "./favicon/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "./favicon/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "./favicon/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "any" + } + ], + "screenshots": [ + { + "src": "./screenshot/screenshot1.png", + "sizes": "2048x2732", + "type": "image/png", + "label": "Level selection" + }, + { + "src": "./screenshot/screenshot2.png", + "sizes": "2048x2732", + "type": "image/png", + "label": "Gameplay" + }, + { + "src": "./screenshot/screenshot3.png", + "sizes": "2048x2732", + "type": "image/png", + "label": "Practice mode" + }, + { + "src": "./screenshot/screenshot4.png", + "sizes": "1170x2532", + "type": "image/png", + "label": "Rewards" + }, + { + "src": "./screenshot/screenshot5.png", + "sizes": "1170x2532", + "type": "image/png", + "label": "Settings" + }, + { + "src": "./screenshot/screenshot6.png", + "sizes": "1824x2736", + "type": "image/png", + "label": "Achievements" + }, + { + "src": "./screenshot/screenshot7.png", + "sizes": "1824x2736", + "type": "image/png", + "label": "More levels" + } + ], + "categories": ["education", "games"], + "prefer_related_applications": false, + "related_applications": [], + "shortcuts": [ + { + "name": "Play", + "short_name": "Play", + "description": "Start the main game", + "url": "./?shortcut=play", + "icons": [ + { + "src": "./favicon/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + } + ] + }, + { + "name": "Practice", + "short_name": "Practice", + "description": "Open practice levels", + "url": "./", + "icons": [ + { + "src": "./favicon/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + } + ] + } + ] +} \ No newline at end of file diff --git a/web/public/music/Math-is-Fun-001.mp3 b/web/public/music/Math-is-Fun-001.mp3 new file mode 100644 index 0000000..be7fb2f Binary files /dev/null and b/web/public/music/Math-is-Fun-001.mp3 differ diff --git a/web/public/music/Math-is-Fun-002.mp3 b/web/public/music/Math-is-Fun-002.mp3 new file mode 100644 index 0000000..8fe19c4 Binary files /dev/null and b/web/public/music/Math-is-Fun-002.mp3 differ diff --git a/web/public/music/Math-is-Fun-003.mp3 b/web/public/music/Math-is-Fun-003.mp3 new file mode 100644 index 0000000..2e89ef5 Binary files /dev/null and b/web/public/music/Math-is-Fun-003.mp3 differ diff --git a/web/public/music/Math-is-Fun-004.mp3 b/web/public/music/Math-is-Fun-004.mp3 new file mode 100644 index 0000000..ff35223 Binary files /dev/null and b/web/public/music/Math-is-Fun-004.mp3 differ diff --git a/web/public/music/Math-is-Fun-005.mp3 b/web/public/music/Math-is-Fun-005.mp3 new file mode 100644 index 0000000..e6049a5 Binary files /dev/null and b/web/public/music/Math-is-Fun-005.mp3 differ diff --git a/web/public/music/Math-is-Fun-006.mp3 b/web/public/music/Math-is-Fun-006.mp3 new file mode 100644 index 0000000..8a71fe6 Binary files /dev/null and b/web/public/music/Math-is-Fun-006.mp3 differ diff --git a/web/public/music/Math-is-Fun-007.mp3 b/web/public/music/Math-is-Fun-007.mp3 new file mode 100644 index 0000000..64736c7 Binary files /dev/null and b/web/public/music/Math-is-Fun-007.mp3 differ diff --git a/web/public/music/Math-is-Fun-008.mp3 b/web/public/music/Math-is-Fun-008.mp3 new file mode 100644 index 0000000..c9ec439 Binary files /dev/null and b/web/public/music/Math-is-Fun-008.mp3 differ diff --git a/web/public/music/Math-is-Fun-009.mp3 b/web/public/music/Math-is-Fun-009.mp3 new file mode 100644 index 0000000..8b0f639 Binary files /dev/null and b/web/public/music/Math-is-Fun-009.mp3 differ diff --git a/web/public/music/Math-is-Fun-010.mp3 b/web/public/music/Math-is-Fun-010.mp3 new file mode 100644 index 0000000..2642da9 Binary files /dev/null and b/web/public/music/Math-is-Fun-010.mp3 differ diff --git a/web/public/music/manifest.json b/web/public/music/manifest.json new file mode 100644 index 0000000..48b423c --- /dev/null +++ b/web/public/music/manifest.json @@ -0,0 +1,12 @@ +[ + "Math-is-Fun-001.mp3", + "Math-is-Fun-002.mp3", + "Math-is-Fun-003.mp3", + "Math-is-Fun-004.mp3", + "Math-is-Fun-005.mp3", + "Math-is-Fun-006.mp3", + "Math-is-Fun-007.mp3", + "Math-is-Fun-008.mp3", + "Math-is-Fun-009.mp3", + "Math-is-Fun-010.mp3" +] diff --git a/web/public/privacy.html b/web/public/privacy.html new file mode 100644 index 0000000..1bb99d7 --- /dev/null +++ b/web/public/privacy.html @@ -0,0 +1,61 @@ + + + + + + Politika zasebnosti — Zabavna matematika + + + + + +
+

Politika zasebnosti

+

Zabavna matematika ("aplikacija") spoštuje vašo zasebnost. Ta stran pojasnjuje, katere informacije zbiramo, kako jih uporabljamo in katere so vaše pravice. Dokument je v slovenščini in velja za aplikacijo in njeno spletno mesto.

+ +

Kaj zbiramo

+ + +

Kako uporabljamo podatke

+

Podatke uporabljamo za:

+ + +

Sodelovanje s tretjimi ponudniki

+

Aplikacija lahko uporablja storitve tretjih oseb za analitiko, shranjevanje podatkov ali oglaševanje. Pogosto uporabljeni primeri so Google Play Services, Firebase in Google Analytics. Te storitve imajo svoje politike zasebnosti; prosimo, preverite njihove pogoje, če vas zanima, kako upravljajo podatke.

+ +

Otroci

+

Ta aplikacija je namenjena otrokom. Ne zbiramo namensko osebnih podatkov otrok brez izrecnega soglasja staršev ali zakonitih skrbnikov. Če ste starš in menite, da so bili osebni podatki vašega otroka posredovani brez soglasja, nas kontaktirajte in ukrepali bomo.

+ +

Varnost

+

Ukrepamo za zaščito zbranih informacij, vendar nobena metoda prenosa ali shranjevanja v spletu ni popolnoma varna. Ne moremo zagotoviti absolutne varnosti podatkov.

+ +

Hramba podatkov

+

Podatke hranimo toliko časa, kot je potrebno za namene, opisane v tej politiki, razen če zakon zahteva drugače.

+ +

Spremembe politike

+

To politiko lahko občasno posodobimo. Spremembe bodo objavljene na tej strani z navedbo veljavnega datuma.

+ +

Kontakt

+

Za vprašanja glede zasebnosti nas kontaktirajte na: info@76bit.net.

+ +
+

Uveljavljivost: 9. februar 2026

+

Vrni se na domov.

+
+
+ + diff --git a/web/public/screenshot/screenshot1.png b/web/public/screenshot/screenshot1.png new file mode 100644 index 0000000..0c0abc7 Binary files /dev/null and b/web/public/screenshot/screenshot1.png differ diff --git a/web/public/screenshot/screenshot2.png b/web/public/screenshot/screenshot2.png new file mode 100644 index 0000000..99e3933 Binary files /dev/null and b/web/public/screenshot/screenshot2.png differ diff --git a/web/public/screenshot/screenshot3.png b/web/public/screenshot/screenshot3.png new file mode 100644 index 0000000..fe61607 Binary files /dev/null and b/web/public/screenshot/screenshot3.png differ diff --git a/web/public/screenshot/screenshot4.png b/web/public/screenshot/screenshot4.png new file mode 100644 index 0000000..2ad6eac Binary files /dev/null and b/web/public/screenshot/screenshot4.png differ diff --git a/web/public/screenshot/screenshot5.png b/web/public/screenshot/screenshot5.png new file mode 100644 index 0000000..6299cbc Binary files /dev/null and b/web/public/screenshot/screenshot5.png differ diff --git a/web/public/screenshot/screenshot6.png b/web/public/screenshot/screenshot6.png new file mode 100644 index 0000000..0644b87 Binary files /dev/null and b/web/public/screenshot/screenshot6.png differ diff --git a/web/public/screenshot/screenshot7.png b/web/public/screenshot/screenshot7.png new file mode 100644 index 0000000..d6eee05 Binary files /dev/null and b/web/public/screenshot/screenshot7.png differ diff --git a/web/public/sounds/applause.mp3 b/web/public/sounds/applause.mp3 new file mode 100644 index 0000000..2fd8768 Binary files /dev/null and b/web/public/sounds/applause.mp3 differ diff --git a/web/public/sounds/beep.mp3 b/web/public/sounds/beep.mp3 new file mode 100644 index 0000000..4d6da55 Binary files /dev/null and b/web/public/sounds/beep.mp3 differ diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..3b710ce --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,74 @@ +// Cache the app shell with paths relative to this worker's scope +// so a subfolder deploy (Vite base: './') keeps working. +const CACHE_NAME = 'matematika-shell-v3' + +function scopeUrl(path) { + return new URL(path, self.registration.scope).href +} + +const ASSETS_TO_CACHE = [ + './', + './index.html', + './manifest.json', + './favicon/site.webmanifest', + './favicon/web-app-manifest-192x192.png', + './favicon/web-app-manifest-512x512.png', + './favicon/apple-touch-icon.png', + './favicon/favicon-96x96.png' +].map(scopeUrl) + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME).then(cache => + cache.addAll(ASSETS_TO_CACHE).catch(err => { + console.warn('Precache failed (continuing):', err) + }) + ) + ) + self.skipWaiting() +}) + +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => Promise.all( + keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)) + )) + ) + self.clients.claim() +}) + +self.addEventListener('fetch', event => { + if (event.request.method !== 'GET') return + + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request).then(resp => { + if (resp && resp.status === 200) { + const respClone = resp.clone() + caches.open(CACHE_NAME).then(cache => cache.put(event.request, respClone)) + } + return resp + }).catch(() => caches.match(scopeUrl('./index.html'))) + ) + return + } + + event.respondWith( + caches.match(event.request).then(cached => { + const networkFetch = fetch(event.request).then(resp => { + if (resp && resp.status === 200) { + const respClone = resp.clone() + caches.open(CACHE_NAME).then(cache => cache.put(event.request, respClone)) + } + return resp + }).catch(() => null) + return cached || networkFetch + }) + ) +}) + +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } +}) diff --git a/web/public/video/loop.mp4 b/web/public/video/loop.mp4 new file mode 100644 index 0000000..a8d2510 Binary files /dev/null and b/web/public/video/loop.mp4 differ diff --git a/web/scripts/smoke.mjs b/web/scripts/smoke.mjs new file mode 100644 index 0000000..8eccdcb --- /dev/null +++ b/web/scripts/smoke.mjs @@ -0,0 +1,191 @@ +import { chromium } from 'playwright' + +function evalEq(text) { + const m = String(text || '').match(/(-?\d+)\s*([+\-×÷])\s*(-?\d+)/) + if (!m) return null + const a = Number(m[1]) + const b = Number(m[3]) + const op = m[2] + if (op === '+') return a + b + if (op === '-') return a - b + if (op === '×') return a * b + if (op === '÷') return a / b + return null +} + +const browser = await chromium.launch({ + headless: true, + channel: process.env.PW_CHANNEL || 'msedge' +}) +const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }) + +await page.addInitScript(() => { + localStorage.setItem('matematika_lang', 'sl') + localStorage.setItem('matematika_settings', JSON.stringify({ + name: 'Test', + stevilo_racunov: 2, + max_number: 20, + sestevanje: true, + odstevanje: false, + mnozenje: false, + deljenje: false, + za_mnozenje: [2], + za_deljenje: [2], + difficulty: 'easy' + })) +}) + +const url = process.env.SMOKE_URL || 'http://127.0.0.1:4173/' +await page.goto(url, { waitUntil: 'domcontentloaded' }) + +const continueBtn = page.getByRole('button', { name: 'Nadaljuj' }) +await continueBtn.waitFor({ state: 'visible', timeout: 120000 }) +await page.waitForFunction(() => { + const btn = [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Nadaljuj') + return btn && !btn.disabled +}, null, { timeout: 120000 }) +await continueBtn.click() + +await page.getByRole('button', { name: 'IGRAJ' }).first().click() +await page.getByRole('button', { name: 'IGRAJ' }).last().click() + +await page.locator('.answer-tile').first().waitFor({ timeout: 15000 }) + +async function currentEq() { + return page.locator('.font-extrabold.leading-tight').innerText() +} + +async function clickTile(expected) { + const tiles = page.locator('.answer-tile') + const n = await tiles.count() + for (let i = 0; i < n; i += 1) { + const label = (await tiles.nth(i).innerText()).replace(/[^\d-]/g, '') + if (Number(label) === expected) { + await tiles.nth(i).click() + return + } + } + throw new Error(`No tile for ${expected}`) +} + +async function answerCurrent({ wrongFirst = false } = {}) { + const eq = await currentEq() + const expected = evalEq(eq) + if (expected === null) throw new Error(`Could not parse equation: ${eq}`) + if (wrongFirst) { + const tiles = page.locator('.answer-tile:not([disabled])') + const n = await tiles.count() + for (let i = 0; i < n; i += 1) { + const label = (await tiles.nth(i).innerText()).replace(/[^\d-]/g, '') + if (Number(label) !== expected) { + await tiles.nth(i).click() + await page.getByText(/Narobe/i).first().waitFor({ timeout: 3000 }) + await page.waitForTimeout(400) + break + } + } + } + await clickTile(expected) + return eq +} + +async function exitToSettings() { + await page.getByRole('button', { name: 'Izhod' }).click() + await page.locator('.fixed.inset-0').getByRole('button', { name: 'Izhod' }).click() + await page.getByRole('button', { name: 'IGRAJ' }).first().waitFor({ timeout: 8000 }) +} + +async function startFromSettings() { + await page.getByRole('button', { name: 'IGRAJ' }).first().click() + await page.getByRole('button', { name: 'IGRAJ' }).last().click() +} + +const first = await answerCurrent() +await page.getByText(/Bravo/i).first().waitFor({ timeout: 5000 }) +await page.waitForTimeout(2400) +const second = await answerCurrent() +await page.getByRole('button', { name: 'Začni ponovno' }).waitFor({ timeout: 8000 }) +await page.getByRole('button', { name: 'Začni ponovno' }).click() + +await page.locator('.answer-tile').first().waitFor({ timeout: 8000 }) +const afterRestart = await currentEq() +if (!/[=+]/.test(afterRestart)) { + throw new Error(`Restart did not show a new question: ${afterRestart}`) +} +if (await page.getByRole('button', { name: 'Začni ponovno' }).isVisible().catch(() => false)) { + throw new Error('Restart immediately showed game over') +} + +const easyTiles = await page.locator('.answer-tile').count() +if (easyTiles !== 4) throw new Error(`Easy should have 4 tiles, got ${easyTiles}`) + +await exitToSettings() + +await page.getByRole('button', { name: 'Srednje' }).click() +await startFromSettings() +await page.locator('.answer-tile').first().waitFor({ timeout: 15000 }) +const mediumTiles = await page.locator('.answer-tile').count() +if (mediumTiles !== 6) throw new Error(`Medium should have 6 tiles, got ${mediumTiles}`) +await answerCurrent({ wrongFirst: true }) +await page.getByText(/Bravo/i).first().waitFor({ timeout: 5000 }) +await page.waitForTimeout(2400) +await answerCurrent() +await page.getByRole('button', { name: 'Začni ponovno' }).waitFor({ timeout: 8000 }) +await page.getByRole('button', { name: 'Nastavitve' }).click() +await page.getByRole('button', { name: 'IGRAJ' }).first().waitFor({ timeout: 8000 }) + +await page.getByRole('button', { name: 'Težko' }).click() +await startFromSettings() +await page.getByRole('button', { name: 'OK' }).waitFor({ timeout: 15000 }) +if (await page.locator('.answer-tile').count()) { + throw new Error('Hard should not show choice tiles') +} +if (await page.getByRole('button', { name: '±' }).count()) { + throw new Error('± should be hidden when negatives are off') +} +if (await page.getByRole('button', { name: 'OK' }).isEnabled()) { + throw new Error('OK should stay disabled on empty keypad') +} +const hardEq = await currentEq() +const hardExpected = evalEq(hardEq) +if (hardExpected === null) throw new Error(`Could not parse hard equation: ${hardEq}`) +const digits = String(Math.abs(hardExpected)) +for (const d of digits) { + await page.getByRole('button', { name: d, exact: true }).click() +} +if (!(await page.getByRole('button', { name: 'OK' }).isEnabled())) { + throw new Error('OK should enable after entering digits') +} +await page.getByRole('button', { name: 'OK' }).click() +await page.getByText(/Bravo/i).first().waitFor({ timeout: 5000 }) +await page.waitForTimeout(2400) +const hardEq2 = await currentEq() +const hardExpected2 = evalEq(hardEq2) +if (hardExpected2 === null) throw new Error(`Could not parse hard equation 2: ${hardEq2}`) +for (const d of String(Math.abs(hardExpected2))) { + await page.getByRole('button', { name: d, exact: true }).click() +} +await page.getByRole('button', { name: 'OK' }).click() +await page.getByRole('button', { name: 'Nastavitve' }).waitFor({ timeout: 8000 }) + +await page.setViewportSize({ width: 390, height: 844 }) +await page.getByRole('button', { name: 'Nastavitve' }).click() +await page.getByRole('button', { name: 'Lahko' }).click() +await startFromSettings() +await page.locator('.answer-tile').first().waitFor({ timeout: 15000 }) +const mobileTiles = await page.locator('.answer-tile').count() +if (mobileTiles !== 4) throw new Error(`Mobile easy should have 4 tiles, got ${mobileTiles}`) +const tileBox = await page.locator('.answer-tile').first().boundingBox() +if (!tileBox || tileBox.width < 80 || tileBox.height < 40) { + throw new Error(`Mobile tiles too small: ${JSON.stringify(tileBox)}`) +} +if (tileBox.x + tileBox.width > 390) { + throw new Error('Mobile tile overflows the viewport') +} +const eqBox = await page.locator('.font-extrabold.leading-tight').boundingBox() +if (eqBox && eqBox.x + eqBox.width > 390) { + throw new Error('Equation overflows the mobile viewport') +} + +console.log('SMOKE_OK', { first, second, afterRestart, easyTiles, mediumTiles, hardEq, mobileTiles, tileBox }) +await browser.close() diff --git a/web/src/App.jsx b/web/src/App.jsx new file mode 100644 index 0000000..4a36b74 --- /dev/null +++ b/web/src/App.jsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState, useRef } from 'react' +import Game from './components/Game' +import Settings from './components/Settings' +import BackgroundMusic from './components/BackgroundMusic' +import Loading from './components/Loading' +import UpdateBanner from './components/UpdateBanner' +import ToastContainer from './components/ToastContainer' +import { useI18n } from './i18n' + +export default function App() { + const [showSettings, setShowSettings] = useState(false) + const [showLoading, setShowLoading] = useState(true) + const [config, setConfig] = useState(null) + + const [musicEnabled, setMusicEnabled] = useState(true) + const [musicVolume, setMusicVolume] = useState(0.14) + const [sfxEnabled, setSfxEnabled] = useState(true) + const [sfxVolume, setSfxVolume] = useState(0.85) + const musicPlayRef = useRef(null) + const bgVideoRef = useRef(null) + const [toasts, setToasts] = useState([]) + const { t } = useI18n() + + useEffect(() => { + document.title = t('app.title') + }, [t]) + + useEffect(() => { + const v = bgVideoRef.current + if (!v) return + // Slow down the background video so the motion feels calmer. + // (CSS can't slow video frames; playbackRate does.) + try { + v.playbackRate = 0.6 + v.defaultPlaybackRate = 0.6 + } catch { + // ignore + } + }, []) + + useEffect(() => { + // Listen for service worker update events dispatched from main.jsx + function onSwUpdate(e) { + try { + const id = Date.now() + const toast = { id, type: 'update', registration: e.detail, closing: false, sticky: true } + setToasts(prev => [toast, ...prev]) + } catch { /* ignore */ } + } + window.addEventListener('swUpdate', onSwUpdate) + return () => window.removeEventListener('swUpdate', onSwUpdate) + }, []) + + function removeToastWithAnimation(id) { + setToasts(prev => prev.map(t => t.id === id ? { ...t, closing: true } : t)) + // allow animation to play then remove + setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 220) + } + + function addToast(message, opts = {}) { + const id = Date.now() + const toast = { id, type: 'info', message, closing: false, sticky: !!opts.sticky } + setToasts(prev => [toast, ...prev]) + if (!toast.sticky) setTimeout(() => removeToastWithAnimation(id), opts.duration || 4000) + return id + } + + useEffect(() => { + try { + const raw = localStorage.getItem('matematika_music') + if (!raw) return + const saved = JSON.parse(raw) + if (typeof saved?.enabled === 'boolean') setMusicEnabled(saved.enabled) + if (typeof saved?.volume === 'number') setMusicVolume(saved.volume) + if (typeof saved?.sfxEnabled === 'boolean') setSfxEnabled(saved.sfxEnabled) + if (typeof saved?.sfxVolume === 'number') setSfxVolume(saved.sfxVolume) + } catch { + // ignore + } + }, []) + + useEffect(() => { + try { + localStorage.setItem('matematika_music', JSON.stringify({ enabled: musicEnabled, volume: musicVolume, sfxEnabled, sfxVolume })) + } catch { + // ignore + } + }, [musicEnabled, musicVolume, sfxEnabled, sfxVolume]) + + useEffect(() => { + try { + // When loading, avoid setting the game's background so the loading screen is the only visible element. + if (showLoading) { + document.body.classList.remove('settings-bg') + document.body.classList.remove('playing-bg') + document.body.style.backgroundImage = '' + return () => {} + } + + document.body.classList.toggle('settings-bg', Boolean(showSettings)) + document.body.classList.toggle('playing-bg', !showSettings) + + const base = import.meta.env.BASE_URL || '/' + if (showSettings) { + document.body.style.backgroundImage = '' + } else { + document.body.style.backgroundImage = `url(${base}gfx/back/background.webp)` + } + + return () => { + document.body.classList.remove('settings-bg') + document.body.classList.remove('playing-bg') + document.body.style.backgroundImage = '' + } + } catch { + return undefined + } + }, [showSettings, showLoading]) + + function handleStart(cfg) { + setConfig(cfg) + setShowSettings(false) + } + + function handleShowSettings() { + setShowSettings(true) + } + + function handleBack() { + setShowSettings(true) + } + + return ( +
+ + {showLoading ? ( + { setShowLoading(false); handleShowSettings() }} musicPlayRef={musicPlayRef} /> + ) : showSettings ? ( + <> + + +
+ {t('app.logoAlt')} +
+ +
+
+ + ) : ( + <> + +
+ +
+ + )} +
+ ) +} diff --git a/web/src/components/AnswerChoices.jsx b/web/src/components/AnswerChoices.jsx new file mode 100644 index 0000000..6f599e3 --- /dev/null +++ b/web/src/components/AnswerChoices.jsx @@ -0,0 +1,57 @@ +import React from 'react' +import '../styles/answer-buttons.css' + +export default function AnswerChoices({ + options, + selectedIdx, + triedWrongIdxs, + wiggleIdx, + expected, + zaporedni, + canType, + onSelect +}) { + return ( +
= 6 ? 'grid-cols-3' : 'grid-cols-2'} gap-2 sm:gap-4 md:gap-6 mb-4 sm:mb-6 mt-4 sm:mt-6`}> + {options.map((opt, i) => { + const isSelected = selectedIdx === i + const correct = Number(opt) === Number(expected) + const wasTriedWrong = triedWrongIdxs.includes(i) + const paletteIndex = ((zaporedni || 0) + i) % 4 + const classes = [ + 'answer-tile', + `pastel-${paletteIndex}`, + wasTriedWrong ? 'wrong persist' : '', + isSelected && correct ? 'correct' : '', + isSelected && !correct && !wasTriedWrong ? 'wrong' : '', + wiggleIdx === i ? 'wiggle' : '' + ].filter(Boolean).join(' ') + + return ( + + ) + })} +
+ ) +} diff --git a/web/src/components/BackgroundMusic.jsx b/web/src/components/BackgroundMusic.jsx new file mode 100644 index 0000000..3983ae0 --- /dev/null +++ b/web/src/components/BackgroundMusic.jsx @@ -0,0 +1,155 @@ +import React, { useEffect, useRef, useState } from 'react' + +async function loadManifest() { + try { + const base = import.meta.env.BASE_URL || '/' + const res = await fetch(`${base}music/manifest.json`, { cache: 'no-store' }) + if (!res.ok) return [] + const data = await res.json() + return Array.isArray(data) ? data.filter(Boolean) : [] + } catch { + return [] + } +} + +function shuffleArray(arr, rng = Math.random) { + const a = Array.isArray(arr) ? arr.slice() : [] + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)) + const tmp = a[i] + a[i] = a[j] + a[j] = tmp + } + return a +} + +export default function BackgroundMusic({ enabled, volume, playRef }) { + const audioRef = useRef(null) + const [tracks, setTracks] = useState([]) + const trackIdxRef = useRef(0) + const [, forceRender] = useState(0) + + useEffect(() => { + let mounted = true + loadManifest().then(list => { + if (!mounted) return + const shuffled = shuffleArray(list) + setTracks(shuffled) + trackIdxRef.current = 0 + }) + return () => { + mounted = false + } + }, []) + + // Expose play method via ref + useEffect(() => { + if (playRef) { + playRef.current = { + play: async () => { + if (!audioRef.current) return + try { + await audioRef.current.play() + } catch (e) { + console.warn('Music playback failed:', e) + } + } + } + return () => { + try { + if (playRef) playRef.current = null + } catch {} + } + } + }, [playRef, tracks]) + + function playAtIndex(idx) { + if (!Array.isArray(tracks) || tracks.length === 0) return + const i = Math.max(0, Math.min(tracks.length - 1, idx)) + trackIdxRef.current = i + const base = import.meta.env.BASE_URL || '/' + try { + if (!audioRef.current) audioRef.current = new Audio() + const audio = audioRef.current + const src = `${base}music/${tracks[i]}` + if (audio.src !== src) audio.src = src + audio.loop = false + audio.volume = Math.max(0, Math.min(1, Number(volume) || 0)) + const p = audio.play() + if (p && typeof p.catch === 'function') p.catch(() => {}) + } catch (e) { + // ignore + } + // force UI update for current track label/buttons + forceRender(n => n + 1) + } + + useEffect(() => { + if (!audioRef.current) audioRef.current = new Audio() + const audio = audioRef.current + + audio.volume = Math.max(0, Math.min(1, Number(volume) || 0)) + audio.preload = 'auto' + + const hasTracks = Array.isArray(tracks) && tracks.length > 0 + + const start = async () => { + if (!enabled || !hasTracks) return + const idx = Math.min(tracks.length - 1, Math.max(0, trackIdxRef.current || 0)) + playAtIndex(idx) + } + + const onEnded = () => { + if (!enabled || !hasTracks) return + // advance to next track in shuffled order; reshuffle when exhausted + let next = (trackIdxRef.current || 0) + 1 + if (next >= tracks.length) { + const reshuffled = shuffleArray(tracks) + setTracks(reshuffled) + next = 0 + } + playAtIndex(next) + } + + audio.addEventListener('ended', onEnded) + + if (!enabled) { + try { audio.pause(); audio.currentTime = 0 } catch {} + } else { + start() + } + + return () => { + try { audio.removeEventListener('ended', onEnded) } catch {} + try { audio.pause(); audio.currentTime = 0 } catch {} + // Clear the ref so a new instance won't collide with an orphaned element + try { audioRef.current = null } catch {} + try { if (playRef) playRef.current = null } catch {} + } + }, [enabled, volume, tracks]) + + const current = Array.isArray(tracks) && tracks.length ? tracks[trackIdxRef.current || 0] : null + const currentLabel = current ? String(current).replace(/\.[^.]+$/, '') : '' + + const handlePrev = () => { + if (!tracks || tracks.length === 0) return + let prev = (trackIdxRef.current || 0) - 1 + if (prev < 0) prev = tracks.length - 1 + playAtIndex(prev) + } + + const handleNext = () => { + if (!tracks || tracks.length === 0) return + let next = (trackIdxRef.current || 0) + 1 + if (next >= tracks.length) next = 0 + playAtIndex(next) + } + + return ( +
+ +
{currentLabel || '—'}
+ +
+ ) +} diff --git a/web/src/components/ExitConfirm.jsx b/web/src/components/ExitConfirm.jsx new file mode 100644 index 0000000..d421292 --- /dev/null +++ b/web/src/components/ExitConfirm.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import { useI18n } from '../i18n' + +export default function ExitConfirm({ open, onCancel, onConfirm }) { + const { t } = useI18n() + if (!open) return null + return ( +
+
+
{t('exit.title')}
+
{t('exit.body')}
+
+ + +
+
+
+ ) +} diff --git a/web/src/components/Game.jsx b/web/src/components/Game.jsx new file mode 100644 index 0000000..4be8443 --- /dev/null +++ b/web/src/components/Game.jsx @@ -0,0 +1,478 @@ +import React, { useEffect, useState, useRef } from 'react' +import { getLevelConfig } from '../levels' +import { defaultConfig, mergeConfig } from '../config' +import { buildSession, makeDistractors, shuffleInPlace } from '../questionGenerator' +import { useAnimatedBackgrounds } from '../hooks/useAnimatedBackgrounds' +import AnswerChoices from './AnswerChoices' +import Keypad from './Keypad' +import GameOver from './GameOver' +import ExitConfirm from './ExitConfirm' +import GameQuestionCard from './GameQuestionCard' +import GameToolbar from './GameToolbar' +import { useI18n } from '../i18n' + +function playSound(audio, volume = 1) { + if (!audio) return + if (typeof volume === 'number' && Number.isFinite(volume)) { + try { audio.volume = Math.max(0, Math.min(1, volume)) } catch { /* ignore */ } + } + try { audio.currentTime = 0 } catch { /* ignore */ } + try { + const p = audio.play() + if (p && typeof p.catch === 'function') p.catch(() => {}) + } catch { /* ignore */ } +} + +function getRobotSrc(idx) { + return `./gfx/robots/robot${idx}.png` +} + +export default function Game({ + config: initialConfig, + onBack, + musicEnabled, + setMusicEnabled, + sfxEnabled = true, + sfxVolume = 0.85 +}) { + const [config] = useState(() => mergeConfig(initialConfig || defaultConfig)) + const level = getLevelConfig(config.difficulty) + const maxRacunov = Number(config.stevilo_racunov) || 20 + + const [zaporedni, setZaporedni] = useState(0) + const [trenutnaEnacba, setTrenutnaEnacba] = useState(null) + const [trenutnaResitev, setTrenutnaResitev] = useState(null) + const [vnos, setVnos] = useState('') + const [options, setOptions] = useState([]) + const [selectedIdx, setSelectedIdx] = useState(null) + const [triedWrongIdxs, setTriedWrongIdxs] = useState([]) + const [wiggleIdx, setWiggleIdx] = useState(null) + const [notice, setNotice] = useState(null) + const { t } = useI18n() + const [mainResult, setMainResult] = useState('') + const [showMainBravo, setShowMainBravo] = useState(false) + const [bravoZoom, setBravoZoom] = useState(false) + const [mainZoom, setMainZoom] = useState(false) + const [mainCorrect, setMainCorrect] = useState(false) + const [mainWrong, setMainWrong] = useState(false) + const [exitConfirmOpen, setExitConfirmOpen] = useState(false) + const [pravilno, setPravilno] = useState(0) + const [napacno, setNapacno] = useState(0) + const [attempts, setAttempts] = useState(1) + const [totalStars, setTotalStars] = useState(0) + const [gameOverState, setGameOverState] = useState(false) + const [canType, setCanType] = useState(false) + const [robotIdx, setRobotIdx] = useState(1) + const [timer, setTimer] = useState(0) + const [totalTime, setTotalTime] = useState(0) + + const indexRef = useRef(0) + const sessionRef = useRef([]) + const timeoutsRef = useRef([]) + const pravilnoRef = useRef(0) + const napacnoRef = useRef(0) + const didStartRef = useRef(false) + const timerRef = useRef(null) + const applauseRef = useRef(null) + const beepRef = useRef(null) + const bgCanvasRef = useRef(null) + const blobCanvasRef = useRef(null) + const plasmaFlashRef = useRef({ kind: null, startedAt: 0 }) + const mainResultTokenRef = useRef(0) + const canTypeBeforeExitRef = useRef(false) + const tryAgainTimerRef = useRef(null) + + const canTypeRef = useRef(canType) + const optionsRef = useRef(options) + const selectedIdxRef = useRef(selectedIdx) + const resolveRef = useRef(() => {}) + const selectRef = useRef(() => {}) + canTypeRef.current = canType + optionsRef.current = options + selectedIdxRef.current = selectedIdx + + useAnimatedBackgrounds(bgCanvasRef, blobCanvasRef, plasmaFlashRef) + + function clearTimeouts() { + timeoutsRef.current.forEach(clearTimeout) + timeoutsRef.current = [] + if (tryAgainTimerRef.current) { + clearTimeout(tryAgainTimerRef.current) + tryAgainTimerRef.current = null + } + } + + function later(fn, ms) { + const id = setTimeout(fn, ms) + timeoutsRef.current.push(id) + return id + } + + const triggerPlasmaFlash = (kind) => { + const now = (typeof performance !== 'undefined' && typeof performance.now === 'function') + ? performance.now() + : Date.now() + plasmaFlashRef.current = { kind, startedAt: now } + } + + function showQuestion(i) { + clearTimeouts() + const pool = sessionRef.current + if (!Array.isArray(pool) || pool.length === 0) { + setCanType(false) + setNotice({ kind: 'noOps' }) + return + } + if (i >= pool.length || i >= maxRacunov) { + setCanType(false) + setGameOverState(true) + setNotice({ kind: 'over', vars: { correct: pravilnoRef.current, wrong: napacnoRef.current } }) + return + } + + const q = pool[i] + if (!q || !Number.isFinite(Number(q.result))) { + setCanType(false) + setNotice({ kind: 'badQuestion' }) + return + } + + indexRef.current = i + setZaporedni(i + 1) + setNotice(null) + setVnos('') + setTriedWrongIdxs([]) + setSelectedIdx(null) + setWiggleIdx(null) + setMainResult('') + setShowMainBravo(false) + setMainZoom(false) + setMainCorrect(false) + setMainWrong(false) + setAttempts(1) + setTrenutnaEnacba(q.text) + setTrenutnaResitev(q.result) + setRobotIdx(Math.floor(Math.random() * 7) + 1) + setTimer(0) + setGameOverState(false) + + if (level.inputType === 'choices') { + const desired = level.choicesCount || 4 + const distractors = makeDistractors({ + base: Number(q.result), + count: Math.max(1, desired - 1), + allowNegative: level.allowNegative, + maxResult: config.max_number + }) + const opts = shuffleInPlace([Number(q.result), ...distractors].slice(0, desired)) + setOptions(opts) + } else { + setOptions([]) + } + setCanType(true) + } + + function startSession() { + clearTimeouts() + sessionRef.current = buildSession(config, level) + indexRef.current = 0 + pravilnoRef.current = 0 + napacnoRef.current = 0 + setPravilno(0) + setNapacno(0) + setTotalStars(0) + setTotalTime(0) + setNotice(null) + setGameOverState(false) + showQuestion(0) + } + + useEffect(() => { + applauseRef.current = new Audio('./sounds/applause.mp3') + beepRef.current = new Audio('./sounds/beep.mp3') + }, []) + + useEffect(() => { + if (didStartRef.current) return undefined + didStartRef.current = true + startSession() + return () => clearTimeouts() + // start once per mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (notice?.kind !== 'wrong') return undefined + if (tryAgainTimerRef.current) clearTimeout(tryAgainTimerRef.current) + tryAgainTimerRef.current = setTimeout(() => { + setNotice(null) + tryAgainTimerRef.current = null + }, 1600) + return () => { + if (tryAgainTimerRef.current) { + clearTimeout(tryAgainTimerRef.current) + tryAgainTimerRef.current = null + } + } + }, [notice]) + + useEffect(() => { + timerRef.current = setInterval(() => { + if (gameOverState) return + setTimer((t) => t + 1) + setTotalTime((t) => t + 1) + }, 1000) + return () => clearInterval(timerRef.current) + }, [gameOverState]) + + function resolveAnswer(given) { + if (!canType) return + const expected = Number(trenutnaResitev) + const value = Number(given) + const correct = !Number.isNaN(expected) && expected === value + + if (correct) { + setCanType(false) + if (sfxEnabled) playSound(applauseRef.current, sfxVolume) + triggerPlasmaFlash('right') + mainResultTokenRef.current = Date.now() + setMainWrong(false) + const awarded = Math.max(1, 6 - attempts) + setTotalStars((s) => s + awarded) + pravilnoRef.current += 1 + setPravilno(pravilnoRef.current) + setMainResult(`${trenutnaEnacba}${trenutnaResitev}`) + setShowMainBravo(true) + setMainZoom(true) + setMainCorrect(true) + setNotice(null) + setBravoZoom(true) + later(() => setMainCorrect(false), 900) + later(() => setMainZoom(false), 260) + later(() => setBravoZoom(false), 140) + later(() => { + setMainResult('') + setShowMainBravo(false) + setBravoZoom(false) + showQuestion(indexRef.current + 1) + }, 2200) + return + } + + if (sfxEnabled) playSound(beepRef.current, sfxVolume) + triggerPlasmaFlash('wrong') + napacnoRef.current += 1 + setNapacno(napacnoRef.current) + setNotice({ kind: 'wrong' }) + setAttempts((a) => Math.min(5, a + 1)) + + if (typeof trenutnaEnacba === 'string' && Number.isFinite(value)) { + const wrongEq = trenutnaEnacba.includes('=') + ? `${trenutnaEnacba.replace(/=\s*$/, '≠ ')}${value}` + : `${trenutnaEnacba} ≠ ${value}` + const token = Date.now() + mainResultTokenRef.current = token + setMainResult(wrongEq) + setMainWrong(true) + setMainZoom(true) + later(() => { + if (mainResultTokenRef.current !== token) return + setMainWrong(false) + setMainZoom(false) + setMainResult('') + }, 900) + } + } + + function onSelectOption(idx) { + if (!canType) return + if (triedWrongIdxs.includes(idx)) return + setCanType(false) + setSelectedIdx(idx) + const chosen = Number(options[idx]) + const expected = Number(trenutnaResitev) + resolveAnswer(chosen) + if (Number.isNaN(expected) || chosen !== expected) { + setTriedWrongIdxs((prev) => (prev.includes(idx) ? prev : [...prev, idx])) + setWiggleIdx(idx) + later(() => setWiggleIdx(null), 520) + later(() => setCanType(true), 350) + later(() => setSelectedIdx(null), 700) + } + } + + function checkResult() { + if (!canType) return + const raw = String(vnos || '').trim() + if (raw === '' || raw === '-' || !/^-?\d+$/.test(raw)) { + setNotice({ kind: 'needAnswer' }) + return + } + const given = Number(raw) + if (!Number.isFinite(given)) { + setNotice({ kind: 'needAnswer' }) + return + } + resolveAnswer(given) + if (Number(trenutnaResitev) !== given) setVnos('') + } + + resolveRef.current = checkResult + selectRef.current = onSelectOption + + useEffect(() => { + const onKey = (e) => { + if (!canTypeRef.current) return + + if (level.inputType === 'choices') { + const opts = optionsRef.current + const max = opts.length + if (max > 0 && /^[1-6]$/.test(e.key)) { + const idx = Number(e.key) - 1 + if (idx >= 0 && idx < max) { + e.preventDefault() + return selectRef.current(idx) + } + } + if (e.key === 'Enter') { + e.preventDefault() + if (selectedIdxRef.current !== null) return selectRef.current(selectedIdxRef.current) + } + return + } + + if (e.key === 'Enter') { + e.preventDefault() + return resolveRef.current() + } + if (e.key === 'Backspace') { + e.preventDefault() + return setVnos((s) => s.slice(0, -1)) + } + if (e.key === '-') { + if (!level.allowNegative) return + e.preventDefault() + return setVnos((s) => { + const str = String(s || '') + if (str.startsWith('-')) return str.slice(1) + return str.length === 0 ? '-' : `-${str}` + }) + } + if (/^[0-9]$/.test(e.key)) { + e.preventDefault() + return setVnos((s) => { + const next = `${String(s || '')}${e.key}` + if (String(next).replace(/[^0-9]/g, '').length > 6) return s + return next + }) + } + } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [level.inputType, level.allowNegative]) + + const openExitConfirm = () => { + canTypeBeforeExitRef.current = canType + setCanType(false) + setExitConfirmOpen(true) + } + + const cancelExitConfirm = () => { + setExitConfirmOpen(false) + setCanType(Boolean(canTypeBeforeExitRef.current)) + } + + const confirmExit = () => { + clearTimeouts() + setExitConfirmOpen(false) + if (onBack) onBack() + } + + const noticeText = notice ? t(`game.${notice.kind}`, notice.vars) : '' + const isTryAgainMessage = notice?.kind === 'wrong' + + return ( +
+ {t('app.logoAlt')} + +
+
+ + onBack && onBack()} + /> + + +
+ + + {level.inputType === 'choices' ? ( + + ) : ( + { + setNotice(null) + setVnos(updater) + }} + onSubmit={checkResult} + disabled={!canType} + allowNegative={Boolean(level.allowNegative)} + /> + )} + + {noticeText && !isTryAgainMessage ? ( +
{noticeText}
+ ) : null} + + +
+
+
+
+ ) +} diff --git a/web/src/components/GameOver.jsx b/web/src/components/GameOver.jsx new file mode 100644 index 0000000..55cf8e1 --- /dev/null +++ b/web/src/components/GameOver.jsx @@ -0,0 +1,50 @@ +import React from 'react' +import { useI18n } from '../i18n' + +function formatTimer(sec) { + const m = Math.floor(sec / 60).toString().padStart(2, '0') + const s = (sec % 60).toString().padStart(2, '0') + return `${m}:${s}` +} + +export default function GameOver({ + open, + robotSrc, + zaporedni, + pravilno, + napacno, + totalTime, + onRestart, + onSettings +}) { + const { t } = useI18n() + if (!open) return null + return ( +
+
+
+ +

{t('over.title')}

+
+ + + + + + + + + + + +
{t('over.count')}{zaporedni}
{t('over.correct')}{pravilno}
{t('over.wrong')}{napacno}
{t('over.time')}{formatTimer(totalTime)}
{t('over.avg')}{formatTimer(Math.round(totalTime / Math.max(1, zaporedni)))}
+
+ + +
+
+
+ ) +} + +export { formatTimer } diff --git a/web/src/components/GameQuestionCard.jsx b/web/src/components/GameQuestionCard.jsx new file mode 100644 index 0000000..945fb56 --- /dev/null +++ b/web/src/components/GameQuestionCard.jsx @@ -0,0 +1,84 @@ +import React from 'react' +import { formatTimer } from './GameOver' +import { useI18n } from '../i18n' + +export default function GameQuestionCard({ + bgCanvasRef, + robotSrc, + displayText, + playerName, + mainZoom, + mainWrong, + mainCorrect, + showMainBravo, + bravoZoom, + isTryAgainMessage, + sporocilo, + timerEnabled, + timer, + totalStars, + zaporedni, + maxRacunov +}) { + const { t } = useI18n() + return ( +
+
+ +
+
+ robot +
+
+
+
+ {displayText || ''} +
+
+ {showMainBravo ? ( +
+ {t('game.bravo', { name: playerName || t('player.defaultName') })} +
+ ) : isTryAgainMessage ? ( +
+ {sporocilo} +
+ ) : ( +
BRAVO
+ )} +
+
+
+
+ +
+
+ {timerEnabled ? ( +
+ +
{formatTimer(timer)}
+
+ ) : ( +
+ )} +
+
+
+ star +
{totalStars}
+
+
+
+
+
{zaporedni || 1} / {maxRacunov}
+
+
+
+
+
+ ) +} diff --git a/web/src/components/GameToolbar.jsx b/web/src/components/GameToolbar.jsx new file mode 100644 index 0000000..d71d937 --- /dev/null +++ b/web/src/components/GameToolbar.jsx @@ -0,0 +1,45 @@ +import React from 'react' +import { useI18n } from '../i18n' + +export default function GameToolbar({ + pravilno, + napacno, + musicEnabled, + setMusicEnabled, + onExit +}) { + const { t } = useI18n() + return ( +
+ + +
+
{t('game.correct', { n: pravilno })}
+
{t('game.incorrect', { n: napacno })}
+
+ + {typeof musicEnabled === 'boolean' && typeof setMusicEnabled === 'function' ? ( + + ) : ( +
+ )} +
+ ) +} diff --git a/web/src/components/Keypad.jsx b/web/src/components/Keypad.jsx new file mode 100644 index 0000000..26c2823 --- /dev/null +++ b/web/src/components/Keypad.jsx @@ -0,0 +1,87 @@ +import React from 'react' +import { useI18n } from '../i18n' + +const MAX_DIGITS = 6 + +function digitCount(s) { + return String(s || '').replace(/[^0-9]/g, '').length +} + +export default function Keypad({ + value, + onChange, + onSubmit, + disabled, + allowNegative = false +}) { + const { t } = useI18n() + const raw = String(value || '').trim() + const canSubmit = /^-?\d+$/.test(raw) + + const append = (ch) => { + if (disabled) return + onChange((s) => { + const next = `${String(s || '')}${ch}` + if (digitCount(next) > MAX_DIGITS) return s + return next + }) + } + + const toggleSign = () => { + if (disabled || !allowNegative) return + onChange((s) => { + const str = String(s || '') + if (str.startsWith('-')) return str.slice(1) + return str.length === 0 ? '-' : `-${str}` + }) + } + + const backspace = () => { + if (disabled) return + onChange((s) => String(s || '').slice(0, -1)) + } + + const keyClass = 'min-h-[48px] py-3 md:py-4 lg:py-5 rounded-2xl bg-indigo-50 text-purple-700 text-xl sm:text-3xl md:text-3xl lg:text-4xl font-extrabold shadow-answer transition-all transform hover:-translate-y-1 hover:shadow-xl hover:ring-2 hover:ring-indigo-300/70 disabled:opacity-50' + + return ( +
+
+
{t('keypad.answer')}
+
+ {value || ' '} +
+ +
+ +
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => ( + + ))} + {allowNegative ? ( + + ) : ( + +
+ ) +} diff --git a/web/src/components/Loading.jsx b/web/src/components/Loading.jsx new file mode 100644 index 0000000..779533d --- /dev/null +++ b/web/src/components/Loading.jsx @@ -0,0 +1,172 @@ +import React, { useEffect, useState, useRef } from 'react' +import { preloadAssets } from '../loadAssets' +import { useI18n } from '../i18n' + +export default function Loading({ onContinue, musicPlayRef, basePath = (import.meta.env.BASE_URL || '/') }) { + const { t } = useI18n() + const [percent, setPercent] = useState(0) + const [done, setDone] = useState(false) + const [messageKey, setMessageKey] = useState('loading.preparing') + const [visible, setVisible] = useState(false) + const containerRef = useRef(null) + // no on-screen list of individual asset names — keep loading UI minimal + const abortRef = useRef(null) + + useEffect(() => { + abortRef.current = new AbortController() + const controller = abortRef.current + + // First: preload only the minimal assets required to render the loading screen itself + const minimalImages = ['gfx/logo.png', 'gfx/back/background2.webp'] + const imgs = [ + 'gfx/logo.png', + 'gfx/back/background.webp', + 'gfx/levels/sestevanje.jpg', + 'gfx/levels/odstevanje.jpg', + 'gfx/levels/mnozenje.jpg', + 'gfx/levels/deljenje.jpg', + 'gfx/levels/level1.jpg', + 'gfx/levels/level2.jpg', + 'gfx/levels/level3.jpg' + ] + for (let i = 1; i <= 7; i += 1) imgs.push(`gfx/robots/robot${i}.png`) + + // base audio list (small UI sounds) + const audios = [ 'sounds/beep.mp3' ] + + // orchestrate: (1) ensure minimal assets are ready, (2) reveal app, (3) continue full preload + ;(async () => { + try { + // minimal preload for logo/background2 + try { + await preloadAssets({ images: minimalImages, base: basePath, signal: controller.signal }) + } catch (e) { + // ignore; we'll still reveal the app even if these fail + } + + // reveal loader (fade-in). wait for its opacity transition to finish + setVisible(true) + // wait for the visible transition to complete (fallback timeout 800ms) + const waitForVisible = () => new Promise(resolve => { + const el = containerRef.current + if (!el) return setTimeout(resolve, 360) + let done = false + const onEnd = (ev) => { + if (ev.propertyName === 'opacity') { + if (!done) { + done = true + el.removeEventListener('transitionend', onEnd) + resolve() + } + } + } + el.addEventListener('transitionend', onEnd) + // safety fallback + setTimeout(() => { + if (!done) { + done = true + try { el.removeEventListener('transitionend', onEnd) } catch {} + resolve() + } + }, 800) + }) + await waitForVisible() + + // now fetch music manifest and perform full preload + setMessageKey('loading.resources') + try { + const mres = await fetch(`${basePath}music/manifest.json`, { signal: controller.signal, cache: 'no-store' }) + if (mres && mres.ok) { + const mdata = await mres.json() + if (Array.isArray(mdata) && mdata.length) { + for (const t of mdata) if (typeof t === 'string' && t.trim()) audios.push(`music/${t.trim()}`) + } + } + } catch (e) { + // ignore manifest failure; proceed with available audios + } + + const results = await preloadAssets({ + images: imgs, + audios, + videos: ['video/loop.mp4'], + base: basePath, + onProgress: ({ percent: p }) => setPercent(p), + signal: controller.signal + }) + + setPercent(100) + setMessageKey('loading.ready') + setDone(true) + } catch (err) { + setMessageKey('loading.error') + setDone(true) + setVisible(true) + } + })() + + return () => controller.abort() + }, [basePath]) + + const handleContinue = async () => { + // onContinue will switch the UI; we then call music play via ref to satisfy autoplay gesture + if (onContinue) onContinue() + try { + // BackgroundMusic will start when mounted based on the `enabled` prop. + // Avoid calling `musicPlayRef.current.play()` here to prevent duplicate playback. + } catch (e) { + // ignore play failure + } + } + + return ( +
+
+ {/* subtle glossy overlay + warm tint */} +
+
+
+
+ logo +
{t(messageKey)}
+
+
+
+
{percent}%
+ {/* intentionally not showing individual asset filenames */} + +
+
+ ) +} diff --git a/web/src/components/NumberPills.jsx b/web/src/components/NumberPills.jsx new file mode 100644 index 0000000..d6a246f --- /dev/null +++ b/web/src/components/NumberPills.jsx @@ -0,0 +1,46 @@ +import React from 'react' + +export default function NumberPills({ label, options = [], value, onChange, multiple = false }) { + const selectedSet = React.useMemo(() => { + if (multiple) return new Set(Array.isArray(value) ? value.map(v => Number(v)) : []) + return new Set([Number(value)]) + }, [value, multiple]) + + function handleClick(n) { + const num = Number(n) + if (!multiple) { + onChange && onChange(num) + return + } + const arr = Array.isArray(value) ? [...value.map(v => Number(v))] : [] + const idx = arr.indexOf(num) + if (idx === -1) arr.push(num) + else arr.splice(idx, 1) + onChange && onChange(arr) + } + + return ( +
+ {label &&
{label}
} +
+ {options.map((n) => { + const sel = selectedSet.has(Number(n)) + return ( + + ) + })} +
+
+ ) +} diff --git a/web/src/components/Settings.jsx b/web/src/components/Settings.jsx new file mode 100644 index 0000000..801edca --- /dev/null +++ b/web/src/components/Settings.jsx @@ -0,0 +1,530 @@ +import React, { useState } from 'react' +import { createPortal } from 'react-dom' +import NumberPills from './NumberPills' +import { getLevelConfig, normalizeDifficulty } from '../levels' +import { defaultConfig, mergeConfig } from '../config' +import { useI18n } from '../i18n' + +const basePath = import.meta.env.BASE_URL || '/' + +export default function Settings({ + onStart, + musicEnabled, + setMusicEnabled, + musicVolume, + setMusicVolume, + sfxEnabled, + setSfxEnabled, + sfxVolume, + setSfxVolume, + musicPlayRef +}) { + const { t, lang, setLang, langs } = useI18n() + const [form, setForm] = useState(defaultConfig) + const [error, setError] = useState('') + const [difficulty, setDifficulty] = useState('easy') + const [count, setCount] = useState(form.stevilo_racunov) + const [loaded, setLoaded] = useState(false) + const [settingsOpen, setSettingsOpen] = useState(false) + const [startOpen, setStartOpen] = useState(false) + const [editingOption, setEditingOption] = useState(null) + const sfxTestAudioRef = React.useRef(null) + // use a default player avatar (static) + const defaultAvatar = basePath + 'gfx/robots/robot2.png' + + const testSfx = () => { + if (!(typeof sfxEnabled === 'boolean' ? sfxEnabled : true)) return + try { + if (!sfxTestAudioRef.current) sfxTestAudioRef.current = new Audio(basePath + 'sounds/beep.mp3') + const a = sfxTestAudioRef.current + a.volume = Math.max(0, Math.min(1, Number.isFinite(sfxVolume) ? sfxVolume : 0.85)) + try { + a.currentTime = 0 + } catch { + // ignore + } + const p = a.play() + if (p && typeof p.catch === 'function') p.catch(() => {}) + } catch { + // ignore + } + } + + const Toggle = ({ checked, onChange, label }) => ( +
+ {label} + +
+ ) + + // load saved settings from localStorage + React.useEffect(() => { + try { + const raw = localStorage.getItem('matematika_settings') + if (raw) { + const saved = mergeConfig(JSON.parse(raw)) + setForm(saved) + const restoredCount = Number(saved.stevilo_racunov) + setCount(Number.isFinite(restoredCount) ? restoredCount : 20) + if (saved.difficulty) { + setDifficulty(getLevelConfig(saved.difficulty).key) + } + } else { + setCount(20) + } + // avatar upload removed — we only allow changing player name now + } catch (e) { + // ignore parse errors + setCount(20) + } finally { + setLoaded(true) + } + }, []) + + // Persist settings on change so a refresh keeps selections + React.useEffect(() => { + if (!loaded) return + try { + const cfgToSave = { + ...mergeConfig(form), + stevilo_racunov: Number(form.stevilo_racunov), + max_number: Number(form.max_number), + difficulty: normalizeDifficulty(difficulty) + } + localStorage.setItem('matematika_settings', JSON.stringify(cfgToSave)) + } catch (e) { + // ignore storage errors + } + }, [loaded, form, difficulty]) + + function handleSubmit(e) { + if (e && e.preventDefault) e.preventDefault() + setError('') + // ensure numeric types (this is what we persist) + const userDifficulty = normalizeDifficulty(difficulty) + const cfgToSave = { + ...mergeConfig(form), + stevilo_racunov: Number(form.stevilo_racunov), + max_number: Number(form.max_number), + difficulty: userDifficulty + } + // validate at least one arithmetic mode selected + if (!(cfgToSave.sestevanje || cfgToSave.odstevanje || cfgToSave.mnozenje || cfgToSave.deljenje)) { + setError(t('error.needOp')) + return + } + if (cfgToSave.mnozenje && (!Array.isArray(cfgToSave.za_mnozenje) || cfgToSave.za_mnozenje.length === 0)) { + setError(t('error.needMul')) + return + } + if (cfgToSave.deljenje && (!Array.isArray(cfgToSave.za_deljenje) || cfgToSave.za_deljenje.length === 0)) { + setError(t('error.needDiv')) + return + } + + const cfgToStart = { + ...cfgToSave, + difficulty: userDifficulty, + name: (cfgToSave.name || '').trim() || t('player.defaultName') + } + // BackgroundMusic will start/stop based on `musicEnabled` when mounted. + // Avoid explicit `play()` here to prevent duplicate playback. + onStart && onStart(cfgToStart) + } + + return ( +
+
+ {t('player.avatar')} +
+
{form.name || t('player.defaultName')}
+

{t('player.ready')}

+
+ +
+ + {settingsOpen && createPortal( +
setSettingsOpen(false)}> +
e.stopPropagation()} + style={{ maxHeight: '100vh' }} + > +
+
{t('settings.title')}
+ +
+ +
+
{t('player.name')}
+
+ setForm({ ...form, name: e.target.value })} + placeholder={t('player.namePlaceholder')} + className="flex-1 rounded-xl bg-white/70 border border-white/50 px-4 py-3 text-lg font-bold text-purple-700 focus:outline-none focus:ring-2 focus:ring-indigo-300" + /> + +
+
+ +
+
{t('settings.language')}
+
+ {langs.map((code) => ( + + ))} +
+ +
{t('settings.sound')}
+
+ { + const next = Boolean(v) + if (setMusicEnabled) setMusicEnabled(next) + // rely on BackgroundMusic `enabled` prop to handle playback + }} + label={t('settings.music')} + /> + setSfxEnabled && setSfxEnabled(Boolean(v))} + label={t('settings.sfx')} + /> + +
+
+
{t('settings.sfxVolume')}
+
+ {Math.round(((typeof sfxVolume === 'number' ? sfxVolume : 0.85) / 1) * 100)}% +
+
+ setSfxVolume && setSfxVolume(Number(e.target.value))} + className="w-full mt-2" + aria-label={t('settings.sfxVolume')} + disabled={!(typeof sfxEnabled === 'boolean' ? sfxEnabled : true)} + /> +
+ +
+
+ +
+
+
{t('settings.musicVolume')}
+
+ {Math.round(((typeof musicVolume === 'number' ? musicVolume : 0.14) / 0.35) * 100)}% +
+
+ setMusicVolume && setMusicVolume(Number(e.target.value))} + className="w-full mt-2" + aria-label={t('settings.musicVolume')} + disabled={!musicEnabled} + /> +
+
+
+
+
, document.body)} + + {startOpen && createPortal( +
{ setStartOpen(false); setEditingOption(null) }}> +
e.stopPropagation()} + > +
+
{t('settings.gameTitle')}
+ +
+ +
+ {t('settings.gameHint')} +
+ + {/* Compact summary view */} + {!editingOption && ( +
+ {(form.sestevanje || form.odstevanje) && ( +
+
+
{t('settings.addSubTo')}
+
{form.max_number}
+
+ +
+ )} + + {form.mnozenje && ( +
+
+
{t('settings.mulFactors')}
+
{Array.isArray(form.za_mnozenje) ? form.za_mnozenje.join(', ') : ''}
+
+ +
+ )} + + {form.deljenje && ( +
+
+
{t('settings.divFactors')}
+
{Array.isArray(form.za_deljenje) ? form.za_deljenje.join(', ') : ''}
+
+ +
+ )} + +
+
+
{t('settings.questionCount')}
+
{count}
+
+ +
+ + {error &&
{error}
} + +
+ +
+
+ )} + + {/* Per-option editor */} + {editingOption && ( +
+ {editingOption === 'max_number' && ( + { setForm(f => ({ ...f, max_number: n })); setEditingOption(null) }} + /> + )} + + {editingOption === 'za_mnozenje' && ( + { setForm(f => ({ ...f, za_mnozenje: Array.isArray(arr) ? arr : [] })); setEditingOption(null) }} + /> + )} + + {editingOption === 'za_deljenje' && ( + { setForm(f => ({ ...f, za_deljenje: Array.isArray(arr) ? arr : [] })); setEditingOption(null) }} + /> + )} + + {editingOption === 'stevilo_racunov' && ( + { setCount(n); setForm(f => ({ ...f, stevilo_racunov: n })); setEditingOption(null) }} + /> + )} + +
+ +
+
+ )} +
+
, document.body)} + +
+
+

{t('settings.practice')}

+
+ {[ + { k: 'sestevanje', labelKey: 'op.add', img: basePath + 'gfx/levels/sestevanje.jpg' }, + { k: 'odstevanje', labelKey: 'op.sub', img: basePath + 'gfx/levels/odstevanje.jpg' }, + { k: 'mnozenje', labelKey: 'op.mul', img: basePath + 'gfx/levels/mnozenje.jpg' }, + { k: 'deljenje', labelKey: 'op.div', img: basePath + 'gfx/levels/deljenje.jpg' } + ].map(item => { + const active = Boolean(form[item.k]) + return ( + + ) + })} +
+
+ +
+

{t('settings.difficulty')}

+
+ {[ + { key: 'easy', img: basePath + 'gfx/levels/level1.jpg' }, + { key: 'medium', img: basePath + 'gfx/levels/level2.jpg' }, + { key: 'hard', img: basePath + 'gfx/levels/level3.jpg' } + ].map(l => { + const sel = difficulty === l.key + return ( + + ) + })} +
+
+ + +
+ +
+
+
+ ) +} diff --git a/web/src/components/ToastContainer.jsx b/web/src/components/ToastContainer.jsx new file mode 100644 index 0000000..74553fb --- /dev/null +++ b/web/src/components/ToastContainer.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import UpdateBanner from './UpdateBanner' + +// Renders a stack of toasts. Each toast object: +// { id, type, registration, message, closing } +export default function ToastContainer({ toasts = [], onRemove = () => {} }) { + return ( +
+
+ {toasts.map(t => ( +
+ {t.type === 'update' ? ( + onRemove(t.id)} /> + ) : ( +
+
{t.message}
+
+ )} +
+ ))} +
+
+ ) +} diff --git a/web/src/components/UpdateBanner.jsx b/web/src/components/UpdateBanner.jsx new file mode 100644 index 0000000..17e8865 --- /dev/null +++ b/web/src/components/UpdateBanner.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import { useI18n } from '../i18n' + +export default function UpdateBanner({ registration, onClose }) { + const { t } = useI18n() + if (!registration) return null + + function applyUpdate() { + if (registration.waiting) { + registration.waiting.postMessage({ type: 'SKIP_WAITING' }) + } + } + + return ( +
+
+ +
{t('update.body')}
+
+
+ + +
+
+ ) +} diff --git a/web/src/config.js b/web/src/config.js new file mode 100644 index 0000000..5e4d867 --- /dev/null +++ b/web/src/config.js @@ -0,0 +1,39 @@ +export const OPS = { + add: 'add', + sub: 'sub', + mul: 'mul', + div: 'div' +} + +export const defaultConfig = { + name: '', + stevilo_racunov: 20, + max_number: 1000, + sestevanje: true, + odstevanje: true, + mnozenje: true, + deljenje: true, + za_mnozenje: [2, 3, 4, 5, 6, 7, 8, 9, 10], + za_deljenje: [2, 3, 4, 5, 6, 7, 8, 9, 10], + difficulty: 'easy' +} + +export const SETTINGS_KEYS = Object.keys(defaultConfig) + +export function enabledOps(cfg) { + const ops = [] + if (cfg?.sestevanje) ops.push(OPS.add) + if (cfg?.odstevanje) ops.push(OPS.sub) + if (cfg?.mnozenje) ops.push(OPS.mul) + if (cfg?.deljenje) ops.push(OPS.div) + return ops +} + +export function mergeConfig(saved) { + const next = { ...defaultConfig } + if (!saved || typeof saved !== 'object') return next + for (const key of SETTINGS_KEYS) { + if (saved[key] !== undefined) next[key] = saved[key] + } + return next +} diff --git a/web/src/hooks/useAnimatedBackgrounds.js b/web/src/hooks/useAnimatedBackgrounds.js new file mode 100644 index 0000000..1be33fa --- /dev/null +++ b/web/src/hooks/useAnimatedBackgrounds.js @@ -0,0 +1,251 @@ +import { useEffect } from 'react' + +function isHidden() { + return typeof document !== 'undefined' && document.hidden +} + +export function useAnimatedBackgrounds(bgCanvasRef, blobCanvasRef, plasmaFlashRef) { + useEffect(() => { + const canvas = bgCanvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + const dpr = Math.max(1, window.devicePixelRatio || 1) + let w = 0 + let h = 0 + let t = 0 + let raf = null + let plasmaBlobs = [] + + function resize() { + const rect = canvas.getBoundingClientRect() + w = Math.floor(rect.width) + h = Math.floor(rect.height) + canvas.width = Math.floor(w * dpr) + canvas.height = Math.floor(h * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + + const base = Math.min(w, h) + const count = Math.max(3, Math.min(8, Math.floor(base / 120))) + plasmaBlobs = [] + for (let i = 0; i < count; i++) { + plasmaBlobs.push({ + x: Math.random() * w, + y: Math.random() * h, + r: base * (0.18 + Math.random() * 0.18), + vx: (Math.random() - 0.5) * 0.08, + vy: (Math.random() - 0.5) * 0.08, + ax: 0, + ay: 0, + drift: 0.6 + Math.random() * 0.8, + phase: Math.random() * Math.PI * 2 + }) + } + } + + function blendRgba(a, b, k) { + const kk = Math.max(0, Math.min(1, k)) + const r = Math.round(a[0] + (b[0] - a[0]) * kk) + const g = Math.round(a[1] + (b[1] - a[1]) * kk) + const bb = Math.round(a[2] + (b[2] - a[2]) * kk) + const aa = a[3] + (b[3] - a[3]) * kk + return `rgba(${r},${g},${bb},${aa})` + } + + function flashIntensity() { + const st = plasmaFlashRef.current?.startedAt || 0 + const kind = plasmaFlashRef.current?.kind + if (!kind || !st) return { kind: null, k: 0 } + const now = (typeof performance !== 'undefined' && typeof performance.now === 'function') + ? performance.now() + : Date.now() + const dur = 520 + const t01 = Math.max(0, Math.min(1, (now - st) / dur)) + const k = Math.sin(Math.PI * t01) + if (t01 >= 1) plasmaFlashRef.current = { kind: null, startedAt: 0 } + return { kind, k } + } + + function drawPlasma() { + if (!ctx) return + if (isHidden()) { + raf = null + return + } + ctx.clearRect(0, 0, w, h) + ctx.fillStyle = 'rgba(255,255,255,0.985)' + ctx.fillRect(0, 0, w, h) + + const cx = w * 0.5 + const cy = h * 0.5 + const r = Math.max(w, h) * 0.6 + + const { kind, k } = flashIntensity() + const baseAccent1 = [99, 102, 241, 0.12] + const baseAccent2 = [56, 189, 248, 0.12] + const baseBlob = [139, 92, 246, 0.10] + const wrongAccent1 = [248, 113, 113, 0.22] + const wrongAccent2 = [239, 68, 68, 0.22] + const wrongBlob = [239, 68, 68, 0.18] + const rightAccent1 = [34, 197, 94, 0.18] + const rightAccent2 = [16, 185, 129, 0.18] + const rightBlob = [34, 197, 94, 0.16] + + const accent1 = kind === 'wrong' + ? blendRgba(baseAccent1, wrongAccent1, k) + : kind === 'right' + ? blendRgba(baseAccent1, rightAccent1, k) + : blendRgba(baseAccent1, baseAccent1, 0) + + const accent2 = kind === 'wrong' + ? blendRgba(baseAccent2, wrongAccent2, k) + : kind === 'right' + ? blendRgba(baseAccent2, rightAccent2, k) + : blendRgba(baseAccent2, baseAccent2, 0) + + const blobAccent = kind === 'wrong' + ? blendRgba(baseBlob, wrongBlob, k) + : kind === 'right' + ? blendRgba(baseBlob, rightBlob, k) + : blendRgba(baseBlob, baseBlob, 0) + + const g1 = ctx.createRadialGradient( + cx + Math.sin(t * 0.8) * (w * 0.12), + cy + Math.cos(t * 0.9) * (h * 0.12), + r * 0.1, + cx, + cy, + r + ) + g1.addColorStop(0, 'rgba(255,255,255,0.32)') + g1.addColorStop(0.45, accent1) + g1.addColorStop(1, 'rgba(99,102,241,0)') + ctx.fillStyle = g1 + ctx.fillRect(0, 0, w, h) + + const g2 = ctx.createRadialGradient( + cx + Math.cos(t * 1.3) * (w * 0.18), + cy + Math.sin(t * 1.1) * (h * 0.16), + r * 0.08, + cx, + cy, + r * 0.9 + ) + g2.addColorStop(0, 'rgba(255,255,255,0.28)') + g2.addColorStop(0.6, accent2) + g2.addColorStop(1, 'rgba(56,189,248,0)') + ctx.fillStyle = g2 + ctx.fillRect(0, 0, w, h) + + for (const b of plasmaBlobs) { + b.ax += (Math.random() - 0.5) * 0.0025 + b.ay += (Math.random() - 0.5) * 0.0025 + b.vx = (b.vx + b.ax + Math.cos(t + b.phase) * 0.012 * b.drift) * 0.995 + b.vy = (b.vy + b.ay + Math.sin(t + b.phase) * 0.012 * b.drift) * 0.995 + b.x += b.vx + b.y += b.vy + b.ax *= 0.5 + b.ay *= 0.5 + if (b.x - b.r < 0 || b.x + b.r > w) b.vx *= -1 + if (b.y - b.r < 0 || b.y + b.r > h) b.vy *= -1 + + const g = ctx.createRadialGradient(b.x, b.y, b.r * 0.15, b.x, b.y, b.r) + g.addColorStop(0, 'rgba(255,255,255,0.38)') + g.addColorStop(0.4, blobAccent) + g.addColorStop(1, 'rgba(139,92,246,0)') + ctx.fillStyle = g + ctx.beginPath() + ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2) + ctx.fill() + } + + t += 0.0016 + raf = requestAnimationFrame(drawPlasma) + } + + function onVis() { + if (!isHidden() && !raf) raf = requestAnimationFrame(drawPlasma) + } + + resize() + raf = requestAnimationFrame(drawPlasma) + window.addEventListener('resize', resize) + document.addEventListener('visibilitychange', onVis) + return () => { + window.removeEventListener('resize', resize) + document.removeEventListener('visibilitychange', onVis) + if (raf) cancelAnimationFrame(raf) + } + }, [bgCanvasRef, plasmaFlashRef]) + + useEffect(() => { + const canvas = blobCanvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + const dpr = Math.max(1, window.devicePixelRatio || 1) + let w = 0 + let h = 0 + let blobs = [] + let raf = null + + function resize() { + const rect = canvas.getBoundingClientRect() + w = Math.floor(rect.width) + h = Math.floor(rect.height) + canvas.width = Math.floor(w * dpr) + canvas.height = Math.floor(h * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + const area = Math.max(1, w * h) + const count = Math.max(10, Math.min(28, Math.floor(area / 35000))) + blobs = [] + for (let i = 0; i < count; i++) { + const r = 24 + Math.random() * 40 + blobs.push({ + x: Math.random() * w, + y: Math.random() * h, + r, + dx: (Math.random() - 0.5) * 0.6, + dy: (Math.random() - 0.5) * 0.6, + a: 0.22 + Math.random() * 0.35 + }) + } + } + + function draw() { + if (!ctx) return + if (isHidden()) { + raf = null + return + } + ctx.clearRect(0, 0, w, h) + for (const b of blobs) { + b.x += b.dx + b.y += b.dy + if (b.x - b.r < 0 || b.x + b.r > w) b.dx *= -1 + if (b.y - b.r < 0 || b.y + b.r > h) b.dy *= -1 + + const gradient = ctx.createRadialGradient(b.x, b.y, b.r * 0.25, b.x, b.y, b.r) + gradient.addColorStop(0, `rgba(99,102,241,${b.a})`) + gradient.addColorStop(1, 'rgba(99,102,241,0)') + ctx.beginPath() + ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2) + ctx.fillStyle = gradient + ctx.fill() + } + raf = requestAnimationFrame(draw) + } + + function onVis() { + if (!isHidden() && !raf) raf = requestAnimationFrame(draw) + } + + resize() + raf = requestAnimationFrame(draw) + window.addEventListener('resize', resize) + document.addEventListener('visibilitychange', onVis) + return () => { + window.removeEventListener('resize', resize) + document.removeEventListener('visibilitychange', onVis) + if (raf) cancelAnimationFrame(raf) + } + }, [blobCanvasRef]) +} diff --git a/web/src/i18n/I18nProvider.jsx b/web/src/i18n/I18nProvider.jsx new file mode 100644 index 0000000..740a14e --- /dev/null +++ b/web/src/i18n/I18nProvider.jsx @@ -0,0 +1,29 @@ +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { LANGS, STORAGE_KEY, detectLang, translate } from './core.js' + +const I18nContext = createContext(null) + +export function I18nProvider({ children }) { + const [lang, setLangState] = useState(detectLang) + + useEffect(() => { + if (typeof document !== 'undefined') document.documentElement.lang = lang + try { localStorage.setItem(STORAGE_KEY, lang) } catch { /* ignore */ } + }, [lang]) + + const t = useCallback((key, vars) => translate(lang, key, vars), [lang]) + + const setLang = useCallback((next) => { + if (LANGS.includes(next)) setLangState(next) + }, []) + + const value = useMemo(() => ({ lang, setLang, t, langs: LANGS }), [lang, setLang, t]) + + return {children} +} + +export function useI18n() { + const ctx = useContext(I18nContext) + if (!ctx) throw new Error('useI18n must be used inside I18nProvider') + return ctx +} diff --git a/web/src/i18n/core.js b/web/src/i18n/core.js new file mode 100644 index 0000000..e8d1d83 --- /dev/null +++ b/web/src/i18n/core.js @@ -0,0 +1,30 @@ +import sl from './sl.json' with { type: 'json' } +import en from './en.json' with { type: 'json' } + +export const LANGS = ['sl', 'en'] +export const DICTS = { sl, en } +export const STORAGE_KEY = 'matematika_lang' + +export function detectLang() { + try { + const saved = localStorage.getItem(STORAGE_KEY) + if (saved && DICTS[saved]) return saved + } catch { + // ignore + } + const nav = (typeof navigator !== 'undefined' && (navigator.language || navigator.userLanguage)) || 'en' + const code = String(nav).toLowerCase() + if (code === 'sl' || code.startsWith('sl-')) return 'sl' + return 'en' +} + +export function interpolate(str, vars) { + if (!vars) return String(str) + return String(str).replace(/\{(\w+)\}/g, (_, k) => (vars[k] == null ? `{${k}}` : String(vars[k]))) +} + +export function translate(lang, key, vars) { + const dict = DICTS[lang] || DICTS.en + const raw = dict[key] ?? DICTS.en[key] ?? key + return interpolate(raw, vars) +} diff --git a/web/src/i18n/core.test.js b/web/src/i18n/core.test.js new file mode 100644 index 0000000..48bc7ce --- /dev/null +++ b/web/src/i18n/core.test.js @@ -0,0 +1,19 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { interpolate, translate } from './core.js' + +test('interpolate replaces named tokens', () => { + assert.equal(interpolate('Bravo {name}', { name: 'Ana' }), 'Bravo Ana') +}) + +test('sl and en have the same keys', async () => { + const sl = (await import('./sl.json', { with: { type: 'json' } })).default + const en = (await import('./en.json', { with: { type: 'json' } })).default + assert.deepEqual(Object.keys(sl).sort(), Object.keys(en).sort()) +}) + +test('translate falls back to English then to the key', () => { + assert.equal(translate('en', 'settings.play'), 'PLAY') + assert.equal(translate('sl', 'settings.play'), 'IGRAJ') + assert.equal(translate('en', 'missing.key'), 'missing.key') +}) diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json new file mode 100644 index 0000000..52d778e --- /dev/null +++ b/web/src/i18n/en.json @@ -0,0 +1,82 @@ +{ + "app.title": "Fun Math", + "app.logoAlt": "Fun Math", + "player.defaultName": "Player", + "player.ready": "Ready for some fun math?", + "player.name": "Player name", + "player.namePlaceholder": "Enter a name", + "player.editName": "Edit name", + "player.avatar": "Player", + "loading.preparing": "Preparing…", + "loading.resources": "Loading…", + "loading.ready": "All set. Tap continue.", + "loading.error": "Could not load some files.", + "loading.continue": "Continue", + "settings.title": "Settings", + "settings.close": "Close", + "settings.sound": "Sound", + "settings.music": "Background music", + "settings.sfx": "Sound effects", + "settings.sfxVolume": "Effects volume", + "settings.musicVolume": "Music volume", + "settings.testSfx": "Test", + "settings.language": "Language", + "settings.gameTitle": "Game settings", + "settings.gameHint": "Review the game setup — tap the gear next to an item to edit it.", + "settings.practice": "What shall we practice?", + "settings.difficulty": "Difficulty", + "settings.play": "PLAY", + "settings.back": "Back", + "settings.addSubTo": "Addition / subtraction up to", + "settings.mulFactors": "Multiplication: factors", + "settings.divFactors": "Division: divisors", + "settings.questionCount": "How many questions?", + "settings.editRange": "Edit range", + "settings.editFactors": "Edit factors", + "settings.editDivisors": "Edit divisors", + "settings.editCount": "Edit question count", + "settings.mulPick": "Multiplication: pick factors", + "settings.divPick": "Division: pick divisors", + "error.needOp": "Pick at least one operation (+, −, × or ÷).", + "error.needMul": "Pick at least one factor for multiplication.", + "error.needDiv": "Pick at least one divisor for division.", + "op.add": "Addition", + "op.sub": "Subtraction", + "op.mul": "Multiplication", + "op.div": "Division", + "difficulty.easy": "Easy", + "difficulty.medium": "Medium", + "difficulty.hard": "Hard", + "game.wrong": "Not quite. Try again!", + "game.needAnswer": "Type an answer.", + "game.noOps": "Pick at least one operation in settings.", + "game.badQuestion": "Could not make that question. Try again.", + "game.over": "Game over — correct: {correct}, wrong: {wrong}", + "game.bravo": "Bravo {name} 🎉✨", + "game.correct": "Correct: {n}", + "game.incorrect": "Wrong: {n}", + "game.exit": "Exit", + "game.musicOff": "Mute music", + "game.musicOn": "Unmute music", + "game.star": "star", + "keypad.answer": "Answer", + "keypad.sign": "Sign", + "keypad.backspace": "Backspace", + "exit.title": "Leave the game?", + "exit.body": "Do you want to leave and go back to settings?", + "exit.stay": "Stay", + "exit.leave": "Leave", + "over.title": "MATH", + "over.count": "Questions:", + "over.correct": "Correct:", + "over.wrong": "Wrong:", + "over.time": "Total time:", + "over.avg": "Average per question:", + "over.restart": "Play again", + "over.settings": "Settings", + "update.body": "A new version is available.", + "update.now": "Update", + "update.later": "Later", + "lang.sl": "Slovenščina", + "lang.en": "English" +} diff --git a/web/src/i18n/index.js b/web/src/i18n/index.js new file mode 100644 index 0000000..0ea0245 --- /dev/null +++ b/web/src/i18n/index.js @@ -0,0 +1,2 @@ +export { I18nProvider, useI18n } from './I18nProvider.jsx' +export { LANGS, detectLang, interpolate, translate } from './core.js' diff --git a/web/src/i18n/sl.json b/web/src/i18n/sl.json new file mode 100644 index 0000000..eefddd3 --- /dev/null +++ b/web/src/i18n/sl.json @@ -0,0 +1,82 @@ +{ + "app.title": "Zabavna matematika", + "app.logoAlt": "Zabavna matematika", + "player.defaultName": "Igralec", + "player.ready": "Pripravljen na zabavno matematiko?", + "player.name": "Ime igralca", + "player.namePlaceholder": "Vpišite ime", + "player.editName": "Uredi ime", + "player.avatar": "Igralec", + "loading.preparing": "Pripravljanje vsebin...", + "loading.resources": "Nalaganje virov...", + "loading.ready": "Vse pripravljeno. Kliknite nadaljuj.", + "loading.error": "Napaka pri nalaganju virov.", + "loading.continue": "Nadaljuj", + "settings.title": "Nastavitve", + "settings.close": "Zapri", + "settings.sound": "Zvok", + "settings.music": "Glasba v ozadju", + "settings.sfx": "Zvočni učinki", + "settings.sfxVolume": "Glasnost učinkov", + "settings.musicVolume": "Glasnost glasbe", + "settings.testSfx": "Preizkusi", + "settings.language": "Jezik", + "settings.gameTitle": "Nastavitve igre", + "settings.gameHint": "Pregled nastavitve igre — klikni zobnik ob posameznem elementu za urejanje.", + "settings.practice": "Kaj bomo vadili?", + "settings.difficulty": "Težavnost", + "settings.play": "IGRAJ", + "settings.back": "Nazaj", + "settings.addSubTo": "Seštevanje / Odštevanje do", + "settings.mulFactors": "Množenje: dejavniki", + "settings.divFactors": "Deljenje: delitelji", + "settings.questionCount": "Koliko vprašanj?", + "settings.editRange": "Uredi obseg", + "settings.editFactors": "Uredi dejavnike", + "settings.editDivisors": "Uredi delitelje", + "settings.editCount": "Uredi število vprašanj", + "settings.mulPick": "Množenje: izberi dejavnike", + "settings.divPick": "Deljenje: izberi delitelje", + "error.needOp": "Izberi vsaj eno vrsto računa (+, -, × ali ÷).", + "error.needMul": "Za množenje izberi vsaj en dejavnik.", + "error.needDiv": "Za deljenje izberi vsaj en delitelj.", + "op.add": "Seštevanje", + "op.sub": "Odštevanje", + "op.mul": "Množenje", + "op.div": "Deljenje", + "difficulty.easy": "Lahko", + "difficulty.medium": "Srednje", + "difficulty.hard": "Težko", + "game.wrong": "Narobe. Poskusi ponovno!", + "game.needAnswer": "Vpiši odgovor.", + "game.noOps": "Izberi vsaj eno vrsto računa v nastavitvah.", + "game.badQuestion": "Napaka pri generiranju naloge. Poskusi ponovno.", + "game.over": "Konec igre — pravilno: {correct}, narobe: {wrong}", + "game.bravo": "Bravo {name} 🎉✨", + "game.correct": "Pravilno: {n}", + "game.incorrect": "Narobe: {n}", + "game.exit": "Izhod", + "game.musicOff": "Izklopi glasbo", + "game.musicOn": "Vklopi glasbo", + "game.star": "zvezda", + "keypad.answer": "Odgovor", + "keypad.sign": "Predznak", + "keypad.backspace": "Izbriši", + "exit.title": "Izhod iz igre?", + "exit.body": "Ali res želiš zapustiti igro in se vrniti v nastavitve?", + "exit.stay": "Ostani", + "exit.leave": "Izhod", + "over.title": "MATEMATIKA", + "over.count": "Število računov:", + "over.correct": "Pravilno izračunani:", + "over.wrong": "Napačno izračunani:", + "over.time": "Skupni čas:", + "over.avg": "Povprečen čas / nalogo:", + "over.restart": "Začni ponovno", + "over.settings": "Nastavitve", + "update.body": "Na voljo je nova različica.", + "update.now": "Posodobi", + "update.later": "Kasneje", + "lang.sl": "Slovenščina", + "lang.en": "English" +} diff --git a/web/src/levels.js b/web/src/levels.js new file mode 100644 index 0000000..430dba5 --- /dev/null +++ b/web/src/levels.js @@ -0,0 +1,43 @@ +export const LEVELS = { + easy: { + key: 'easy', + inputType: 'choices', + choicesCount: 4, + timerEnabled: true, + allowNegative: false, + multiplicationMaxA: 10, + multiplicationMaxB: 10 + }, + medium: { + key: 'medium', + inputType: 'choices', + choicesCount: 6, + timerEnabled: true, + allowNegative: false, + multiplicationMaxA: 12, + multiplicationMaxB: 12 + }, + hard: { + key: 'hard', + inputType: 'keypad', + choicesCount: 0, + timerEnabled: true, + allowNegative: false, + multiplicationMaxA: 20, + multiplicationMaxB: 20 + } +} + +export function normalizeDifficulty(value) { + if (!value) return 'easy' + const v = String(value).trim().toLowerCase() + if (v === 'easy' || v === 'lahko') return 'easy' + if (v === 'medium' || v === 'srednje') return 'medium' + if (v === 'hard' || v === 'težko' || v === 'tezko') return 'hard' + return 'easy' +} + +export function getLevelConfig(difficulty) { + const key = normalizeDifficulty(difficulty) + return LEVELS[key] || LEVELS.easy +} diff --git a/web/src/loadAssets.js b/web/src/loadAssets.js new file mode 100644 index 0000000..02d940d --- /dev/null +++ b/web/src/loadAssets.js @@ -0,0 +1,80 @@ +// Simple asset preloader used by Loading component +// - images: array of URLs +// - audios: array of URLs +// - musicManifest: if true, fetches music/manifest.json and loads tracks listed there + +export async function preloadAssets({ images = [], audios = [], videos = [], base = '/', onProgress = () => {}, signal } = {}) { + const toLoad = [] + const imgList = Array.isArray(images) ? images.map(i => i.startsWith('http') ? i : base + i) : [] + const audioList = Array.isArray(audios) ? audios.map(a => a.startsWith('http') ? a : base + a) : [] + const videoList = Array.isArray(videos) ? videos.map(v => v.startsWith('http') ? v : base + v) : [] + + imgList.forEach(u => toLoad.push({ type: 'image', url: u })) + audioList.forEach(u => toLoad.push({ type: 'audio', url: u })) + videoList.forEach(u => toLoad.push({ type: 'video', url: u })) + + let loaded = 0 + const total = toLoad.length + const results = [] + + function tick(item) { + loaded += 1 + onProgress({ loaded, total, item, percent: total ? Math.round((loaded / total) * 100) : 100 }) + } + + // load images + const imgPromises = imgList.map(url => new Promise(resolve => { + if (signal && signal.aborted) return resolve({ url, ok: false }) + const img = new Image() + img.onload = () => { tick({ type: 'image', url }); resolve({ url, ok: true }) } + img.onerror = () => { tick({ type: 'image', url }); resolve({ url, ok: false }) } + img.src = url + })) + + // load audios (use canplaythrough) + const audioPromises = audioList.map(url => new Promise(resolve => { + if (signal && signal.aborted) return resolve({ url, ok: false }) + try { + const a = new Audio() + let settled = false + const done = (ok) => { if (settled) return; settled = true; tick({ type: 'audio', url }); resolve({ url, ok }) } + a.preload = 'auto' + a.addEventListener('canplaythrough', () => done(true), { once: true }) + a.addEventListener('error', () => done(false), { once: true }) + // timeout fallback in case canplaythrough never fires + setTimeout(() => done(true), 4000) + a.src = url + // start loading + a.load() + } catch (e) { + tick({ type: 'audio', url }) + resolve({ url, ok: false }) + } + })) + + // load videos + const videoPromises = videoList.map(url => new Promise(resolve => { + if (signal && signal.aborted) return resolve({ url, ok: false }) + try { + const v = document.createElement('video') + let settled = false + const done = (ok) => { if (settled) return; settled = true; tick({ type: 'video', url }); resolve({ url, ok }) } + v.preload = 'auto' + v.addEventListener('canplaythrough', () => done(true), { once: true }) + v.addEventListener('loadeddata', () => done(true), { once: true }) + v.addEventListener('error', () => done(false), { once: true }) + // timeout fallback + setTimeout(() => done(true), 5000) + v.src = url + try { v.load() } catch (e) { /* ignore */ } + } catch (e) { + tick({ type: 'video', url }) + resolve({ url, ok: false }) + } + })) + + const all = [...imgPromises, ...audioPromises, ...videoPromises] + const res = await Promise.all(all) + res.forEach(r => results.push(r)) + return results +} diff --git a/web/src/main.jsx b/web/src/main.jsx new file mode 100644 index 0000000..7a4c940 --- /dev/null +++ b/web/src/main.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import { I18nProvider } from './i18n' +import './styles/index.css' + +createRoot(document.getElementById('root')).render( + + + + + +) + +// Service worker registration with friendlier in-app update banner + +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + const swUrl = `${import.meta.env.BASE_URL || './'}sw.js` + navigator.serviceWorker.register(swUrl).then(registration => { + console.log('ServiceWorker registered with scope:', registration.scope) + + // If there's an already-waiting worker, dispatch event to App + if (registration.waiting) { + window.dispatchEvent(new CustomEvent('swUpdate', { detail: registration })) + } + + // Listen for new installing worker + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing + if (!newWorker) return + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // New content is available, dispatch event to App + window.dispatchEvent(new CustomEvent('swUpdate', { detail: registration })) + } else { + console.log('Content cached for offline use.') + } + } + }) + }) + }).catch(err => { + console.warn('ServiceWorker registration failed:', err) + }) + + // Reload the page when the new service worker takes control + navigator.serviceWorker.addEventListener('controllerchange', () => { + window.location.reload() + }) + }) +} diff --git a/web/src/questionGenerator.js b/web/src/questionGenerator.js new file mode 100644 index 0000000..229d53b --- /dev/null +++ b/web/src/questionGenerator.js @@ -0,0 +1,437 @@ +/** + * Controlled Math Question Generator + * + * Goals (per session): + * - Avoid duplicates + * - Avoid commutative equivalents for addition (3+5 and 5+3) + * - Limit questions involving 0 to a small fraction (default ~8%) + * - Keep results within the chosen max value + * - Provide a varied, "fun" distribution of results + * + * No external libraries. + * + * Example usage: + * const rng = createSeededRng(12345) + * const gen = new MathQuestionGenerator({ maxValue: 20, difficulty: 'easy', rng }) + * const questions = gen.generate({ count: 20, operations: ['+', '-'] }) + * // questions[0] => { op: '+', a: 7, b: 6, result: 13, text: '7 + 6 = ' } + * + * Full session (all live ops): + * buildSession(config, level, rng) + */ + +import { OPS, enabledOps } from './config.js' + +export function createSeededRng(seed) { + // Deterministic PRNG (Mulberry32). Good enough for games. + let t = (Number(seed) || 0) >>> 0 + return function rng() { + t += 0x6D2B79F5 + let x = t + x = Math.imul(x ^ (x >>> 15), x | 1) + x ^= x + Math.imul(x ^ (x >>> 7), x | 61) + return ((x ^ (x >>> 14)) >>> 0) / 4294967296 + } +} + +export function shuffleInPlace(arr, rng = Math.random) { + for (let i = arr.length - 1; i > 0; i -= 1) { + const j = Math.floor(rng() * (i + 1)) + ;[arr[i], arr[j]] = [arr[j], arr[i]] + } + return arr +} + +function clampInt(n, min, max) { + const x = Number(n) + if (!Number.isFinite(x)) return min + return Math.min(max, Math.max(min, Math.floor(x))) +} + +function pickFromArray(arr, rng) { + return arr[Math.floor(rng() * arr.length)] +} + +function intBetween(min, max, rng) { + if (max < min) return min + return Math.floor(rng() * (max - min + 1)) + min +} + +function difficultyToOperandCap(maxValue, difficulty) { + const maxV = Math.max(0, Number(maxValue) || 0) + const d = String(difficulty || 'medium').toLowerCase() + if (d === 'easy') return Math.max(6, Math.floor(maxV * 0.6)) + if (d === 'hard') return maxV + return Math.max(8, Math.floor(maxV * 0.85)) +} + +function makeKey(op, a, b) { + if (op === '+') { + const lo = Math.min(a, b) + const hi = Math.max(a, b) + return `+|${lo}|${hi}` + } + return `-|${a}|${b}` +} + +function formatText(op, a, b) { + const sym = op === '+' ? '+' : '-' + return `${a} ${sym} ${b} = ` +} + +export class MathQuestionGenerator { + constructor({ + maxValue, + allowNegative = false, + difficulty = 'medium', + zeroRate = 0.08, + rng = Math.random + } = {}) { + this.maxValue = clampInt(maxValue, 0, 100000) + this.allowNegative = Boolean(allowNegative) + this.difficulty = difficulty + this.zeroRate = Math.max(0, Math.min(0.25, Number(zeroRate) || 0)) + this.rng = typeof rng === 'function' ? rng : Math.random + + this.used = new Set() + this.resultCounts = { + '+': new Map(), + '-': new Map() + } + + this.totalPlanned = 0 + this.zeroQuota = 0 + this.zeroUsed = 0 + } + + resetSession({ count } = {}) { + this.used.clear() + this.resultCounts['+'].clear() + this.resultCounts['-'].clear() + + const c = clampInt(count ?? this.totalPlanned, 0, 100000) + this.totalPlanned = c + this.zeroQuota = Math.round(c * this.zeroRate) + this.zeroUsed = 0 + } + + generate({ count, operations = ['+'] } = {}) { + const c = clampInt(count, 1, 1000) + const ops = (Array.isArray(operations) ? operations : ['+']) + .map(o => (o === '-' ? '-' : '+')) + if (ops.length === 0) ops.push('+') + + this.resetSession({ count: c }) + + const out = [] + for (let i = 0; i < c; i += 1) { + const op = pickFromArray(ops, this.rng) + const q = this.next(op) + out.push(q) + } + + shuffleInPlace(out, this.rng) + return out + } + + next(op) { + const operation = op === '-' ? '-' : '+' + const maxValue = this.maxValue + const operandCap = Math.min(maxValue, difficultyToOperandCap(maxValue, this.difficulty)) + + // We choose among a handful of candidate results and pick the most "underused". + const pickResult = () => { + const minR = this.allowNegative ? -maxValue : 0 + const maxR = maxValue + + // Bias: avoid 0 too often; keep more mid-range results for variety. + // We do this by sampling multiple candidates and selecting the least-used. + const candidates = [] + for (let i = 0; i < 8; i += 1) { + let r + if (this.rng() < 0.72) { + // Triangular-ish around mid + const mid = (minR + maxR) / 2 + const spread = (maxR - minR) / 2 + const u = this.rng() - this.rng() + r = Math.round(mid + u * spread) + } else { + r = intBetween(minR, maxR, this.rng) + } + if (!this.allowNegative && r < 0) r = 0 + if (r === 0 && this.rng() < 0.7) { + // push away from 0 unless we actually want a few zeros + r = intBetween(Math.max(1, minR), maxR, this.rng) + } + candidates.push(clampInt(r, minR, maxR)) + } + + const counts = this.resultCounts[operation] + let best = candidates[0] + let bestCount = counts.get(best) || 0 + for (const r of candidates) { + const c = counts.get(r) || 0 + if (c < bestCount) { + best = r + bestCount = c + } + } + return best + } + + const tryBuild = (targetResult) => { + if (operation === '+') { + // a + b = targetResult + const res = targetResult + + // Prefer smaller operands on easy/medium, allow full range on hard. + const cap = operandCap + if (Math.abs(res) > 2 * cap) return null + + const aMin = Math.max(this.allowNegative ? -cap : 0, res - cap) + const aMax = Math.min(cap, res) + if (aMax < aMin) return null + + const a = intBetween(aMin, aMax, this.rng) + const b = res - a + + if (!this.allowNegative && (a < 0 || b < 0)) return null + if (Math.abs(a) > cap || Math.abs(b) > cap) return null + if (!Number.isFinite(a) || !Number.isFinite(b)) return null + + // Randomize display order sometimes (key still treats commutative as same). + const swap = this.rng() < 0.5 + const aa = swap ? b : a + const bb = swap ? a : b + + return { a: aa, b: bb, result: aa + bb } + } + + // subtraction + // a - b = targetResult + const res = targetResult + + if (!this.allowNegative && res < 0) return null + + if (!this.allowNegative) { + // non-negative results; keep operands within cap and <= maxValue + const cap = operandCap + const maxB = Math.min(cap, cap - res) + if (maxB < 0) return null + const b = intBetween(0, maxB, this.rng) + const a = b + res + if (a < 0 || b < 0) return null + if (a > cap || b > cap) return null + if (a > maxValue || b > maxValue) return null + return { a, b, result: a - b } + } + + // allowNegative: operands in [-cap,cap] + const cap = operandCap + for (let i = 0; i < 25; i += 1) { + const a = intBetween(-cap, cap, this.rng) + const b = a - res + if (b >= -cap && b <= cap) return { a, b, result: a - b } + } + return null + } + + // Attempt to find a non-trivial, unique question. + const maxAttempts = 500 + for (let attempt = 0; attempt < maxAttempts; attempt += 1) { + const targetResult = pickResult() + const pair = tryBuild(targetResult) + if (!pair) continue + + const { a, b, result } = pair + + // Keep results within chosen max value. + if (!this.allowNegative) { + if (result < 0 || result > maxValue) continue + } else { + if (Math.abs(result) > maxValue) continue + } + + // Limit operand 0 frequency across the session. + const hasZeroOperand = (a === 0 || b === 0) + if (hasZeroOperand && this.zeroUsed >= this.zeroQuota) continue + + // Avoid trivial "x + 0" / "x - 0" even within quota unless we get stuck. + if (hasZeroOperand && attempt < Math.floor(maxAttempts * 0.6) && this.rng() < 0.85) continue + + const key = makeKey(operation, a, b) + if (this.used.has(key)) continue + + // Accept. + this.used.add(key) + const counts = this.resultCounts[operation] + counts.set(result, (counts.get(result) || 0) + 1) + if (hasZeroOperand) this.zeroUsed += 1 + + return { + op: operation, + a, + b, + result, + text: formatText(operation, a, b) + } + } + + // Very defensive fallback: produce something valid even if constraints are tight. + // (Still respects maxValue; may exceed zeroQuota if absolutely necessary.) + if (operation === '+') { + const res = intBetween(0, maxValue, this.rng) + const a = intBetween(0, res, this.rng) + const b = res - a + return { op: '+', a, b, result: a + b, text: formatText('+', a, b) } + } + + const a = intBetween(0, maxValue, this.rng) + const b = intBetween(0, a, this.rng) + return { op: '-', a, b, result: a - b, text: formatText('-', a, b) } + } +} + +function pickFrom(arr, rng, fallback) { + if (!Array.isArray(arr) || arr.length === 0) return fallback + return arr[Math.floor(rng() * arr.length)] +} + +function toMulQuestion(a, b) { + return { + op: OPS.mul, + a, + b, + result: a * b, + text: `${a} × ${b} = ` + } +} + +function toDivQuestion(dividend, divisor, quotient) { + return { + op: OPS.div, + a: dividend, + b: divisor, + result: quotient, + text: `${dividend} ÷ ${divisor} = ` + } +} + +function nextMul({ config, level, rng, used }) { + const aMax = Math.max(1, Number(level?.multiplicationMaxA) || 10) + const factors = Array.isArray(config?.za_mnozenje) ? config.za_mnozenje : [2] + for (let i = 0; i < 80; i += 1) { + const a = intBetween(1, aMax, rng) + const b = Number(pickFrom(factors, rng, 2)) || 2 + const key = `×|${Math.min(a, b)}|${Math.max(a, b)}` + if (used.has(key) && i < 60) continue + used.add(key) + return toMulQuestion(a, b) + } + const a = intBetween(1, aMax, rng) + const b = Number(pickFrom(factors, rng, 2)) || 2 + return toMulQuestion(a, b) +} + +function nextDiv({ config, level, rng, used, maxValue }) { + const divisors = Array.isArray(config?.za_deljenje) ? config.za_deljenje : [2] + const allowNegative = Boolean(level?.allowNegative) + for (let i = 0; i < 80; i += 1) { + const b = Number(pickFrom(divisors, rng, 2)) || 2 + const qMax = Math.max(3, Math.floor(maxValue / Math.max(1, Math.abs(b)))) + const q = allowNegative ? intBetween(-qMax, qMax, rng) : intBetween(0, qMax, rng) + if (q === 0 && i < 50) continue + const a = q * b + const key = `÷|${a}|${b}` + if (used.has(key) && i < 60) continue + used.add(key) + return toDivQuestion(a, b, q) + } + const b = Number(pickFrom(divisors, rng, 2)) || 2 + const q = intBetween(1, Math.max(3, Math.floor(maxValue / Math.max(1, Math.abs(b)))), rng) + return toDivQuestion(q * b, b, q) +} + +/** + * Build a full play session from settings + difficulty. + * Returns [{ op, a, b, result, text }, ...] + */ +export function buildSession(config, level, rng = Math.random) { + const roll = typeof rng === 'function' ? rng : Math.random + const count = clampInt(config?.stevilo_racunov, 1, 1000) + const ops = enabledOps(config) + if (ops.length === 0) return [] + + const maxValue = clampInt(config?.max_number, 2, 1000) + const planned = [] + for (let i = 0; i < count; i += 1) { + planned.push(pickFrom(ops, roll, OPS.add)) + } + + const addSubCount = planned.reduce((acc, op) => ( + op === OPS.add || op === OPS.sub ? acc + 1 : acc + ), 0) + + const addSubGen = new MathQuestionGenerator({ + maxValue, + allowNegative: Boolean(level?.allowNegative), + difficulty: level?.key || 'easy', + zeroRate: 0.08, + rng: roll + }) + addSubGen.resetSession({ count: addSubCount }) + + const usedMulDiv = new Set() + const questions = [] + + for (const op of planned) { + if (op === OPS.add) { + const q = addSubGen.next('+') + questions.push({ op: OPS.add, a: q.a, b: q.b, result: q.result, text: q.text }) + } else if (op === OPS.sub) { + const q = addSubGen.next('-') + questions.push({ op: OPS.sub, a: q.a, b: q.b, result: q.result, text: q.text }) + } else if (op === OPS.mul) { + questions.push(nextMul({ config, level, rng: roll, used: usedMulDiv })) + } else if (op === OPS.div) { + questions.push(nextDiv({ config, level, rng: roll, used: usedMulDiv, maxValue })) + } + } + + shuffleInPlace(questions, roll) + return questions +} + +export function makeDistractors({ base, count, allowNegative, maxResult, rng = Math.random }) { + const out = new Set() + let guard = 0 + const roll = typeof rng === 'function' ? rng : Math.random + const baseAbs = Math.abs(Number(base) || 0) + const configuredMax = Number(maxResult) + const maxR = Math.max(Number.isFinite(configuredMax) ? configuredMax : 100, baseAbs + 12) + + const rand = (min, max) => intBetween(min, max, roll) + + while (out.size < count && guard < 300) { + guard += 1 + const mag = Math.max(3, Math.min(80, Math.floor(baseAbs * 0.35) + 10)) + const delta = rand(1, mag) + const sign = roll() > 0.5 ? 1 : -1 + const cand = Number(base) + sign * delta + if (cand === base) continue + if (!allowNegative && cand < 0) continue + if (!allowNegative && cand > maxR) continue + if (allowNegative && Math.abs(cand) > maxR) continue + out.add(cand) + } + + guard = 0 + while (out.size < count && guard < 800) { + guard += 1 + const cand = allowNegative ? rand(-maxR, maxR) : rand(0, maxR) + if (cand === base) continue + out.add(cand) + } + + return Array.from(out) +} diff --git a/web/src/questionGenerator.test.js b/web/src/questionGenerator.test.js new file mode 100644 index 0000000..8272045 --- /dev/null +++ b/web/src/questionGenerator.test.js @@ -0,0 +1,91 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createSeededRng, MathQuestionGenerator, buildSession } from './questionGenerator.js' +import { OPS } from './config.js' +import { getLevelConfig } from './levels.js' + +test('addition treats 3+5 and 5+3 as the same question', () => { + const rng = createSeededRng(1) + const gen = new MathQuestionGenerator({ maxValue: 20, difficulty: 'easy', rng, zeroRate: 0 }) + gen.resetSession({ count: 40 }) + gen.used.add('+|3|5') + const q = gen.next('+') + const isCommutativeDup = (q.a === 3 && q.b === 5) || (q.a === 5 && q.b === 3) + assert.equal(isCommutativeDup, false) +}) + +test('subtraction never goes negative when allowNegative is false', () => { + const rng = createSeededRng(99) + const gen = new MathQuestionGenerator({ + maxValue: 30, + difficulty: 'medium', + allowNegative: false, + rng + }) + const questions = gen.generate({ count: 40, operations: ['+', '-'] }) + for (const q of questions) { + assert.ok(q.result >= 0, `result ${q.result} from ${q.text}`) + assert.ok(q.a >= 0 && q.b >= 0) + } +}) + +test('buildSession length matches stevilo_racunov', () => { + const level = getLevelConfig('easy') + const questions = buildSession({ + stevilo_racunov: 12, + max_number: 20, + sestevanje: true, + odstevanje: true, + mnozenje: false, + deljenje: false + }, level, createSeededRng(7)) + assert.equal(questions.length, 12) +}) + +test('buildSession only includes requested operations', () => { + const level = getLevelConfig('medium') + const questions = buildSession({ + stevilo_racunov: 24, + max_number: 50, + sestevanje: false, + odstevanje: false, + mnozenje: true, + deljenje: true, + za_mnozenje: [2, 5], + za_deljenje: [2, 5] + }, level, createSeededRng(21)) + assert.equal(questions.length, 24) + for (const q of questions) { + assert.ok(q.op === OPS.mul || q.op === OPS.div, q.op) + } +}) + +test('division questions are always exact', () => { + const level = getLevelConfig('hard') + const questions = buildSession({ + stevilo_racunov: 30, + max_number: 80, + sestevanje: false, + odstevanje: false, + mnozenje: false, + deljenje: true, + za_deljenje: [2, 3, 4, 5, 6] + }, level, createSeededRng(44)) + assert.equal(questions.length, 30) + for (const q of questions) { + assert.equal(q.op, OPS.div) + assert.equal(q.a, q.result * q.b) + assert.equal(q.a % q.b, 0) + } +}) + +test('buildSession returns empty when no operations selected', () => { + const questions = buildSession({ + stevilo_racunov: 10, + sestevanje: false, + odstevanje: false, + mnozenje: false, + deljenje: false + }, getLevelConfig('easy')) + assert.deepEqual(questions, []) +}) diff --git a/web/src/styles/answer-buttons.css b/web/src/styles/answer-buttons.css new file mode 100644 index 0000000..50d269e --- /dev/null +++ b/web/src/styles/answer-buttons.css @@ -0,0 +1,138 @@ +/* Answer buttons: pastel, soft 3D, friendly animations */ +:root{ + --pastel-purple:#C9B7FF; + --pastel-purple-dark:#9F7AEA; + --pastel-blue:#BFE8FF; + --pastel-blue-dark:#7BCBFF; + /* replaced mint (green-ish) with warm peach to avoid accidental green hinting */ + --pastel-peach:#FFE5D4; + --pastel-peach-dark:#FFCCAA; + --pastel-yellow:#FFF3C2; + --pastel-yellow-dark:#FFE37A; +} + +.answer-tile { + border: none; + cursor: pointer; + user-select: none; + -webkit-tap-highlight-color: transparent; + display: flex; + align-items: center; + justify-content: center; + gap: .5rem; + padding: 0.7rem 0.65rem; + border-radius: 18px; + min-height: 52px; + font-weight: 800; + font-size: 1.25rem; + line-height: 1; + color: #fff; + text-shadow: 0 1px 0 rgba(255,255,255,0.08); + transition: transform 220ms cubic-bezier(.2,.9,.2,1), box-shadow 220ms ease, filter 220ms ease, opacity 220ms ease; + box-shadow: 0 18px 36px rgba(16,24,40,0.12), inset 0 -8px 14px rgba(255,255,255,0.12), 0 2px 0 rgba(255,255,255,0.02); + position: relative; /* allow icons to be positioned inside */ +} + +.answer-tile:hover, .answer-tile:focus { + transform: scale(1.03) translateY(-3px); + box-shadow: 0 26px 48px rgba(16,24,40,0.14), inset 0 -10px 18px rgba(255,255,255,0.14), 0 2px 0 rgba(255,255,255,0.03); + outline: none; + filter: drop-shadow(0 6px 18px rgba(125, 95, 255, 0.12)); +} + +.answer-tile:active { + transform: scale(0.97) translateY(2px); + box-shadow: 0 8px 18px rgba(16,24,40,0.08), inset 0 -4px 8px rgba(0,0,0,0.12); +} + +@media (min-width: 640px) { + .answer-tile { + padding: 1.05rem 1.25rem; + border-radius: 24px; + min-height: 72px; + font-size: 1.6rem; + } +} + +/* Pastel color variants */ +.pastel-0 { background: linear-gradient(180deg,var(--pastel-purple), var(--pastel-purple-dark)); color: #2b1055; } +.pastel-1 { background: linear-gradient(180deg,var(--pastel-blue), var(--pastel-blue-dark)); color: #07435a; } +.pastel-2 { background: linear-gradient(180deg,var(--pastel-peach), var(--pastel-peach-dark)); color: #5a3a2a; } +.pastel-3 { background: linear-gradient(180deg,var(--pastel-yellow), var(--pastel-yellow-dark)); color: #5a4300; } + +.answer-tile.small { padding: .6rem .9rem; font-size: 1.15rem; min-height: 52px; border-radius: 20px; } + +.answer-tile.disabled, .answer-tile.disabled:hover { cursor: default; transform: none; opacity: 0.72; filter: grayscale(0.02) brightness(0.95); box-shadow: 0 8px 18px rgba(16,24,40,0.06), inset 0 -4px 8px rgba(255,255,255,0.06); } + +/* When a wrong answer should remain visually marked but not interactive */ +.answer-tile.persist, .answer-tile.persist:hover { + cursor: default; + transform: none !important; + box-shadow: 0 8px 18px rgba(16,24,40,0.06), inset 0 -4px 8px rgba(255,255,255,0.06); + opacity: 0.96; +} + +.answer-tile.correct { + background: linear-gradient(180deg,#86efac,#22c55e); + color: #ffffff; + box-shadow: 0 20px 46px rgba(34,197,94,0.18), inset 0 -8px 16px rgba(255,255,255,0.2); +} +.answer-tile.correct .check { + margin-left: .6rem; + width: 28px; height: 28px; border-radius: 999px; display:inline-flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.18); + box-shadow: 0 6px 12px rgba(34,197,94,0.18); +} + +.answer-tile.wrong { + /* Stronger, friendly red feedback for wrong selection */ + background: linear-gradient(180deg,#ffb3b3,#ff6b6b); + color: #3b0000; + box-shadow: 0 20px 46px rgba(255,80,80,0.18), inset 0 -8px 16px rgba(255,255,255,0.06); + transform-origin: center; +} + +/* small fun icon for wrong answers */ +.wrong-icon { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%) scale(0.85); + width: 32px; + height: 32px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 16px; + background: rgba(255,255,255,0.12); + box-shadow: 0 6px 12px rgba(0,0,0,0.12); + transition: transform 320ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; + pointer-events: none; +} +.wrong-icon.pop { + transform: translateY(-50%) scale(1.05); +} + +@keyframes wrongPop { + 0% { transform: translateY(-50%) scale(0.6); opacity: 0 } + 60% { transform: translateY(-50%) scale(1.12); opacity: 1 } + 100% { transform: translateY(-50%) scale(1); opacity: 1 } +} +.wrong-icon.animate { animation: wrongPop 420ms cubic-bezier(.2,.9,.2,1) forwards } + +@keyframes wiggle { 0%{ transform: rotate(-3deg);} 30%{ transform: rotate(3deg);} 60%{ transform: rotate(-2deg);} 100%{ transform: rotate(0deg);} } +.wiggle { animation: wiggle 520ms ease both; transform-origin: center; } + +/* sparkle for correct reveal */ +@keyframes sparkle { 0% { opacity: 0; transform: translateY(6px) scale(0.9);} 40% { opacity: 1; transform: translateY(-2px) scale(1); } 100% { opacity: 0; transform: translateY(-18px) scale(0.85); } } +.sparkle { + position: absolute; pointer-events: none; width: 120px; height: 120px; left: 50%; top: 50%; transform: translate(-50%,-50%); opacity: 0; mix-blend-mode: screen; +} +.sparkle::before, .sparkle::after { content: ''; position: absolute; left: 50%; top: 50%; width: 8px; height: 8px; border-radius: 3px; background: radial-gradient(circle,#fff, rgba(255,255,255,0)); } +.sparkle.show { animation: sparkle 800ms ease forwards; } + +/* small helper for checkmark */ +.checkmark { font-size: 18px; font-weight: 900; color: rgba(255,255,255,0.96); } + +/* Ensure focus rings are gentle and visible */ +.answer-tile:focus-visible { box-shadow: 0 26px 48px rgba(16,24,40,0.16), 0 0 0 6px rgba(99,102,241,0.08); } diff --git a/web/src/styles/index.css b/web/src/styles/index.css new file mode 100644 index 0000000..0b6f02a --- /dev/null +++ b/web/src/styles/index.css @@ -0,0 +1,136 @@ +@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;700&family=Noto+Sans:wght@400;700&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body, #root { + height: 100%; +} + +/* apply Fredoka globally */ +body { + font-family: 'Fredoka', system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + background: #000; + position: relative; + overflow-x: hidden; +} + +/* Settings screen: fixed full-page background image */ +body.settings-bg { + background: #000; +} + +/* Gameplay screen: static background image (no video) */ +body.playing-bg { + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; +} + +/* fixed fullscreen background video (served from /public) */ +.bg-video { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 0; + pointer-events: none; + opacity: 0.9; +} + +@media (prefers-reduced-motion: reduce) { + .bg-video { + opacity: 0.85; + } +} + +/* animated soft blobs background using pseudo-elements */ +body::before, +body::after { + content: none; +} + +/* ensure app cards render above background */ +.app-root, form, .bg-white\/80 { + position: relative; + z-index: 10; +} + +/* kid-friendly adjustments */ +input:disabled { + opacity: 0.9; +} + +/* subtle card and control polish */ +.bg-white\/70 { + backdrop-filter: blur(6px); + /* reduce default padding/min-height so the central white box isn't overly large */ + padding: 1rem; + min-height: auto; +} + +/* tighten inner white panels (used in settings/game) */ +.bg-white.rounded-xl.p-6.shadow-inner { + padding: 0.75rem; /* smaller inner padding */ +} + +/* reduce padding for the pale operator/info strip */ +.bg-indigo-50.rounded-3xl { + padding: 0.75rem; /* reduce space around operator row */ +} + +/* make small table in game over look nicer */ +#gameOver table td { + padding: .35rem .5rem; +} + +/* larger touch targets for keypad */ +.touch-manipulation button { + touch-action: manipulation; +} + +/* big answer button styling */ +.shadow-answer { + box-shadow: 0 18px 30px rgba(99,102,241,0.12), inset 0 -6px 12px rgba(255,255,255,0.12); + transition: transform 160ms ease, box-shadow 160ms ease; +} +.shadow-answer:active { + transform: translateY(3px) scale(0.995); + box-shadow: 0 8px 18px rgba(99,102,241,0.08); +} + +/* Magic start button */ +.magic-btn { + position: relative; + padding: 20px 72px; + font-size: 24px; + font-weight: 800; + letter-spacing: 1.2px; + text-transform: uppercase; + color: #ffffff; + border: none; + border-radius: 999px; + cursor: pointer; + user-select: none; + background: linear-gradient(180deg, #c09bff 0%, #9f7aea 40%, #7c3aed 100%); + box-shadow: 0 14px 30px rgba(124, 58, 237, 0.55), inset 0 3px 5px rgba(255,255,255,0.45), inset 0 -4px 6px rgba(0,0,0,0.3); + transition: transform 0.15s ease, box-shadow 0.15s ease; + overflow: hidden; +} +.magic-btn:hover { transform: scale(1.05); box-shadow: 0 18px 40px rgba(139,92,246,0.75), inset 0 3px 6px rgba(255,255,255,0.55); } +.magic-btn:active { transform: scale(0.97); box-shadow: 0 8px 18px rgba(124,58,237,0.6), inset 0 5px 8px rgba(0,0,0,0.35); } +.magic-btn::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background-image: radial-gradient(3px 3px at 15% 35%, white, transparent), radial-gradient(2px 2px at 30% 70%, white, transparent), radial-gradient(2px 2px at 50% 40%, white, transparent), radial-gradient(3px 3px at 70% 60%, white, transparent), radial-gradient(2px 2px at 85% 35%, white, transparent); + animation: sparkleMove 4s linear infinite; + opacity: 0.8; +} + +@keyframes sparkleMove { 0% { transform: translateX(-10px); opacity: 0.4; } 50% { opacity: 0.9; } 100% { transform: translateX(10px); opacity: 0.4; } } + diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs new file mode 100644 index 0000000..0e4af45 --- /dev/null +++ b/web/tailwind.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + content: [ + './index.html', + './src/**/*.{js,jsx,ts,tsx}' + ], + theme: { + extend: { + fontFamily: { + fredoka: ['Fredoka', 'sans-serif'] + } + }, + }, + plugins: [], +} diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..10b222a --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + root: '.', + // Use relative paths in built assets so the site can be deployed to a subfolder + base: './', + // Build options tuned to reduce memory usage during bundling + build: { + // disable source maps (reduces memory and CPU during build) + sourcemap: false, + // use esbuild for minification (faster and lower memory than terser) + minify: 'esbuild', + // do not compute compressed sizes to avoid extra memory/CPU + reportCompressedSize: false, + // keep CSS code-splitting enabled (default) — avoids large single CSS bundle + cssCodeSplit: true, + // clear output dir before build + emptyOutDir: true, + // raise chunk warning limit to avoid spurious warnings for large bundles + chunkSizeWarningLimit: 2000 + } +})