diff --git a/python-backend/tests/test_tippen_curriculum.py b/python-backend/tests/test_tippen_curriculum.py
index 9b8dd57..fd18862 100644
--- a/python-backend/tests/test_tippen_curriculum.py
+++ b/python-backend/tests/test_tippen_curriculum.py
@@ -7,7 +7,17 @@ from pathlib import Path
import pytest
from ruamel.yaml import YAML
-from musicmouse.tippen.curriculum import CurriculumError, load_curriculum
+from musicmouse.tippen.curriculum import (
+ ELIGIBLE_MODES,
+ CurriculumError,
+ first_lesson_id,
+ lesson_by_id,
+ load_curriculum,
+ next_lesson,
+)
+
+#: The real, shipped curriculum - see python-backend/tippen-curriculum.yml.example.
+_EXAMPLE = Path(__file__).parent.parent / "tippen-curriculum.yml.example"
VALID: dict = {
"worlds": [
@@ -150,3 +160,168 @@ def test_unlocks_is_carried_through_unresolved(tmp_path: Path) -> None:
curriculum = load_curriculum(_write(tmp_path, {"worlds": worlds}))
assert curriculum.lessons[0].unlocks == "~/Music/Musik/Album/05Track.mp3"
assert curriculum.lessons[1].unlocks is None
+
+
+# ------------------------------------------------------------- the real curriculum
+#
+# Content-shape invariants the real, shipped curriculum must hold - ported from the
+# frontend's old curriculum.test.ts, which checked these against the same YAML back
+# when it was parsed client-side. The pedagogical content itself did not change in the
+# move to the backend; what moved is the parsing/deriving logic these tests actually
+# exercise (buildLessons's Python port, `_build_lessons`), so the coverage still earns
+# its keep here. A few of the original checks needed real-keyboard finger/hand mapping
+# (`fingers.ts`, never ported to Python, since nothing backend-side needs it) and are
+# not repeated.
+
+
+@pytest.fixture(scope="module")
+def curriculum():
+ return load_curriculum(_EXAMPLE)
+
+
+def test_has_unique_ids_and_consecutive_numbers(curriculum) -> None:
+ ids = [lesson.id for lesson in curriculum.lessons]
+ assert len(set(ids)) == len(ids)
+ for i, lesson in enumerate(curriculum.lessons):
+ assert lesson.number == i + 1
+
+
+def test_starts_on_the_two_keys_with_the_tactile_bumps(curriculum) -> None:
+ assert curriculum.lessons[0].new_keys == ("f", "j")
+ assert first_lesson_id(curriculum) == curriculum.lessons[0].id
+
+
+def test_eases_in_at_most_two_new_keys_per_lesson(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ assert len(lesson.new_keys) <= 2, f"{lesson.number}: {lesson.title}"
+
+
+def test_makes_every_round_a_real_block_of_practice(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ where = f"{lesson.number}: {lesson.title}"
+ if lesson.kind == "sentences":
+ assert lesson.chunks >= 10, where
+ elif lesson.kind == "words":
+ assert lesson.chunks >= 25, where
+ else:
+ assert lesson.chunks * lesson.chunk_size >= 60, where
+
+
+def test_has_more_sentences_to_draw_from_than_a_round_uses(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ if lesson.kind != "sentences":
+ continue
+ assert len(lesson.words) >= lesson.chunks, f"{lesson.number}: {lesson.title}"
+
+
+def test_active_keys_grow_monotonically(curriculum) -> None:
+ previous: set[str] = set()
+ for lesson in curriculum.lessons:
+ active = set(lesson.active_keys)
+ assert previous <= active
+ previous = active
+
+
+def test_every_new_key_enters_its_own_active_keys(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ for key in lesson.new_keys:
+ if key == "⇧": # Shift is not a character the generator can emit.
+ continue
+ assert key in lesson.active_keys, f"{lesson.number}: {lesson.title}"
+
+
+def test_covers_the_whole_alphabet_plus_umlauts_by_the_end(curriculum) -> None:
+ final = set(curriculum.lessons[-1].active_keys)
+ for key in "abcdefghijklmnopqrstuvwxyzäöü":
+ assert key in final
+
+
+def test_plays_a_mode_that_fits_its_kind(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ where = f"{lesson.number}: {lesson.title}"
+ assert lesson.primary_mode in ELIGIBLE_MODES[lesson.kind], where
+ for mode in lesson.bonus_modes:
+ assert mode in ELIGIBLE_MODES[lesson.kind], where
+
+
+def test_never_plays_the_same_arcade_game_twice_in_a_row(curriculum) -> None:
+ arcade = [lesson for lesson in curriculum.lessons if lesson.kind == "letters"]
+ for i in range(1, len(arcade)):
+ where = f"{arcade[i].number}: {arcade[i].title}"
+ assert arcade[i].primary_mode != arcade[i - 1].primary_mode, where
+
+
+def test_offers_every_eligible_mode_not_gated_on_as_a_bonus(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ expected = sorted(
+ mode
+ for mode in ELIGIBLE_MODES[lesson.kind]
+ if mode in ("feed", "race") and mode != lesson.primary_mode
+ )
+ assert sorted(lesson.bonus_modes) == expected
+
+
+def test_mixes_games_instead_of_always_diving(curriculum) -> None:
+ words = [lesson for lesson in curriculum.lessons if lesson.kind == "words"]
+ sentences = [lesson for lesson in curriculum.lessons if lesson.kind == "sentences"]
+ assert any(lesson.primary_mode == "feed" for lesson in words)
+ assert any(lesson.primary_mode == "race" for lesson in words)
+ assert any(lesson.primary_mode == "race" for lesson in sentences)
+
+
+def test_distinguishes_a_mixed_round_from_a_drill(curriculum) -> None:
+ mixed = [lesson for lesson in curriculum.lessons if lesson.emphasis == "mixed"]
+ assert len(mixed) > 0
+ for lesson in mixed:
+ assert lesson.new_keys == ()
+ assert lesson.is_drill is False
+
+
+def test_every_isolated_round_has_something_new_to_drill(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ if lesson.emphasis == "isolated":
+ assert len(lesson.new_keys) > 0, f"{lesson.number}: {lesson.title}"
+
+
+def test_teaches_capitals_only_once_shift_exists(curriculum) -> None:
+ for lesson in curriculum.lessons:
+ has_uppercase = any(word != word.lower() for word in lesson.words)
+ if has_uppercase:
+ assert lesson.world >= 4, f"{lesson.number}: {lesson.title}"
+
+
+def test_follows_every_pair_of_new_keys_with_a_drill(curriculum) -> None:
+ drills = [lesson for lesson in curriculum.lessons if lesson.is_drill]
+ assert len(drills) >= 15
+ for lesson in drills:
+ assert lesson.new_keys == ()
+ assert len(lesson.active_keys) > 0
+
+
+def test_is_a_real_duolingo_length_course(curriculum) -> None:
+ assert len(curriculum.lessons) >= 80
+ assert len(curriculum.worlds) == 5
+
+
+def test_every_world_has_at_least_three_lessons(curriculum) -> None:
+ for world in curriculum.worlds:
+ count = sum(1 for lesson in curriculum.lessons if lesson.world == world.number)
+ assert count >= 3, world.title
+
+
+def test_every_world_has_its_own_creature(curriculum) -> None:
+ rewards = [world.reward for world in curriculum.worlds]
+ assert len(set(rewards)) == len(rewards)
+
+
+def test_navigation_chains_every_lesson_to_the_next(curriculum) -> None:
+ lessons = curriculum.lessons
+ for i in range(len(lessons) - 1):
+ assert next_lesson(curriculum, lessons[i].id).id == lessons[i + 1].id
+ assert next_lesson(curriculum, lessons[-1].id) is None
+ assert next_lesson(curriculum, "gibt-es-nicht") is None
+
+
+def test_navigation_looks_lessons_up_by_id(curriculum) -> None:
+ assert lesson_by_id(curriculum, first_lesson_id(curriculum)).number == 1
+ assert lesson_by_id(curriculum, "gibt-es-nicht") is None
diff --git a/tippen/.gitignore b/tippen/.gitignore
deleted file mode 100644
index b947077..0000000
--- a/tippen/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules/
-dist/
diff --git a/tippen/README.md b/tippen/README.md
deleted file mode 100644
index 7d2427d..0000000
--- a/tippen/README.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# Delfin Tippen
-
-Ein Lernspiel fürs Zehnfingerschreiben auf einer deutschen QWERTZ-Tastatur, gebaut für
-eine Sechsjährige, die Delfine mag. Alles auf Deutsch.
-
-Technisch ein Geschwister des Musik-Players in `../web` — React 19, TypeScript, Vite,
-keine Laufzeit-Abhängigkeiten außer React, dieselbe oklch-Glasoptik und dieselbe
-Nunito — aber **vollständig entkoppelt**: eigenes Vite-Projekt, eigener Dev-Server,
-kein Backend, kein gemeinsamer Build. Was geteilt werden sollte, wurde kopiert.
-
-## Loslegen
-
-```sh
-npm install
-npm run dev # http://localhost:5174
-npm run test # vitest über src/lib
-npx tsc --noEmit # der eigentliche Check
-npm run build # -> dist/
-```
-
-Es braucht eine echte QWERTZ-Tastatur. Der Fortschritt liegt im `localStorage`
-(`delfin-tippen:v1`); zum Zurücksetzen den Key löschen.
-
-## Wie es aufgebaut ist
-
-Die Logik ist DOM-frei und getestet, die Komponenten sind Layout. Das ist dieselbe
-Aufteilung wie in `../web` und der Grund, warum sechs Spielmodi dieselbe Mechanik teilen
-können, ohne sie sechsmal zu implementieren.
-
-| Datei | Wofür |
-|---|---|
-| `lib/fingers.ts` | Die QWERTZ-Belegung: welcher Finger welche Taste, welche Farbe, welche Grundstellungstaste. |
-| `lib/curriculum.ts` | 48 Lektionen in 5 Welten. Höchstens zwei neue Tasten pro Lektion, mit Übungslektionen dazwischen. |
-| `lib/generator.ts` | Was getippt wird — Übungszeilen aus den aktiven Tasten, echte Wörter sobald möglich. |
-| `lib/engine.ts` | Die Tippmechanik: ein Tastendruck rein, ein neuer Zustand und Ereignisse raus. Pur. |
-| `lib/grading.ts` | Zeichen pro Minute, Genauigkeit, Sterne, Tier. |
-| `lib/progress.ts` | Was das Spiel sich merkt, und wann die nächste Lektion aufgeht. |
-| `hooks/useRun.ts` | Der dünne Adapter zwischen echten Tastendrücken und `engine.ts`. |
-
-## Die Entscheidungen, die das Spiel ausmachen
-
-Vier davon sind bewusst anders, als man es zuerst bauen würde. Sie stehen ausführlicher
-in den Dateikommentaren:
-
-**Tempo schaltet nichts frei.** Die nächste Lektion geht bei 93 % Treffern auf — ohne
-jede Geschwindigkeitsbedingung. Wer langsam und sorgfältig tippt, kommt durch den ganzen
-Kurs. Tempo bringt nur das Tier. Zusätzlich geht die nächste Lektion nach fünf Versuchen
-ohnehin auf, damit niemand an einer Taste hängen bleibt.
-
-**`punkte = tempo × genauigkeit³`.** Die Lehrbuchformel `netto = brutto − fehler/minute`
-wird bei einem Anfänger negativ, und ein negatives Ergebnis darf hier nie erscheinen.
-Multiplikativ kann das nicht passieren: 95 % Genauigkeit behalten 86 % des Tempos, 90 %
-behalten 73 %.
-
-**Eine falsche Taste rückt nicht vor und zählt pro Stelle nur einmal.** Es gibt kein
-Backspace und keine zerschossene Zeile zum Anstarren, und zehnmal panisch dieselbe
-falsche Taste ist ein Fehler, nicht zehn.
-
-**Es gibt keinen Verloren-Bildschirm.** Eine schwache Runde zeigt weniger Sterne und ein
-langsameres Tier — nie ein rotes X, nie einen Summer, und die Perlen steigen immer.
-
-## Die sechs Spielmodi
-
-Alle sechs laufen über dieselbe Mechanik (`lib/engine.ts`) und landen in derselben
-Bewertung. Sie unterscheiden sich nur darin, *wie* das Ziel gezeigt wird — deshalb ist
-ein siebter Modus eine Datei und eine Zeile, keine zweite Spiellogik.
-
-| Modus | Was man sieht | Wofür |
-|---|---|---|
-| 🤿 Tauchgang | Eine Zeile mit Cursor | Die Kernübung. Sie zählt fürs Freischalten. |
-| 🫧 Blasenplatzen | Blasen steigen auf, eine pro Buchstabe | Tastenlage, weiche Uhr |
-| 🦑 Quallenalarm | Sechs Quallen, eine leuchtet | Reine Tastenlage — erst suchen, dann tippen |
-| 🐟 Fütterungszeit | Fische mit Wörtern schwimmen zum Delfin | Ganze Wörter statt Buchstaben |
-| 🐬 Delfinrennen | Zwei Bahnen: du gegen deinen Rekord | Tempo, gegen den einzigen fairen Gegner |
-| 🦪 Perlentaucher | Eine Muschel, ein Wort, eine Perlenkette | Genauigkeit — **ohne jede Uhr** |
-
-Beim Delfinrennen ist der Gegner der Geist des eigenen besten Laufs: die Tastenzeiten
-liegen in `progress.lessons[id].ghost` und werden gegen die Uhr abgespielt. Gegen sich
-selbst zu rennen ist das einzige Wettkampfformat, das nicht entmutigen kann — der Gegner
-ist per Definition genau so gut, wie man selbst war. Vor dem ersten Rekord schwimmt eine
-absichtlich schlagbare Krabbe.
-
-## Das Aquarium
-
-Wer eine Welt schafft, bekommt ein Haustier — Clownfisch, Krake, Seepferdchen,
-Schildkröte, Perlmuschel. Es zieht für immer ein und schwimmt ab dann frei hinter jedem
-Bildschirm herum; während einer Runde nur blass, damit es nicht mit der Zeile
-konkurriert. Das Schwimmen ist ein purer, getesteter Schritt in `lib/aquarium.ts`,
-`components/AquariumTiere.tsx` zeichnet nur.
-
-Ein neues Tier: die Zeichnung (auf schwarzem Hintergrund) nach `art/aquarium/` legen,
-
-```sh
-scripts/aquarium-bild.sh art/aquarium/qualle.png public/aquarium/qualle.webp
-```
-
-und in `KREATUREN` eintragen. Das Skript entfernt den Hintergrund als zusammenhängende
-Fläche statt als Farbe — schwarze Augen und Wimpern bleiben stehen — und rechnet die
-Kanten aus dem Schwarz heraus, damit kein dunkler Rand bleibt.
-
-## Stand
-
-Fertig und spielbar: alle 48 Lektionen, alle sechs Modi, Freischalten, Bewertung,
-Tierleiter, Aquarium, Sprachausgabe und die Bildschirmtastatur.
-
-Noch offen: die Geschichten am Ende jeder Welt, der Eltern-Bildschirm (`?eltern=1`) und
-eine Welt 6 mit der Zahlenreihe.
diff --git a/tippen/art/aquarium/clownfisch.png b/tippen/art/aquarium/clownfisch.png
deleted file mode 100644
index fbac340..0000000
Binary files a/tippen/art/aquarium/clownfisch.png and /dev/null differ
diff --git a/tippen/art/aquarium/krake.png b/tippen/art/aquarium/krake.png
deleted file mode 100644
index 136b0ef..0000000
Binary files a/tippen/art/aquarium/krake.png and /dev/null differ
diff --git a/tippen/art/aquarium/perlmuschel.png b/tippen/art/aquarium/perlmuschel.png
deleted file mode 100644
index 6bcb129..0000000
Binary files a/tippen/art/aquarium/perlmuschel.png and /dev/null differ
diff --git a/tippen/art/aquarium/schildkroete.png b/tippen/art/aquarium/schildkroete.png
deleted file mode 100644
index 87b38a6..0000000
Binary files a/tippen/art/aquarium/schildkroete.png and /dev/null differ
diff --git a/tippen/art/aquarium/seepferdchen.png b/tippen/art/aquarium/seepferdchen.png
deleted file mode 100644
index cc043a8..0000000
Binary files a/tippen/art/aquarium/seepferdchen.png and /dev/null differ
diff --git a/tippen/index.html b/tippen/index.html
deleted file mode 100644
index 432eaf0..0000000
--- a/tippen/index.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
- Delfin Tippen
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tippen/package-lock.json b/tippen/package-lock.json
deleted file mode 100644
index 57d4136..0000000
--- a/tippen/package-lock.json
+++ /dev/null
@@ -1,2349 +0,0 @@
-{
- "name": "delfin-tippen",
- "version": "0.1.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "delfin-tippen",
- "version": "0.1.0",
- "dependencies": {
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "yaml": "^2.6.0"
- },
- "devDependencies": {
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "@vitejs/plugin-react": "^5.0.0",
- "typescript": "^5.9.0",
- "vite": "^7.1.0",
- "vitest": "^3.2.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.29.7",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
- "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.8",
- "@babel/types": "^7.29.8",
- "@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.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "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.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
- "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.8"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
- "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
- "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
- "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.8",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.8",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.8",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
- "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@napi-rs/lzma-linux-x64-gnu": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
- "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^22.20 || ^24.12 || >=25"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.3",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
- "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz",
- "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz",
- "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
- "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz",
- "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz",
- "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz",
- "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz",
- "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz",
- "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz",
- "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz",
- "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz",
- "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz",
- "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz",
- "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz",
- "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz",
- "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz",
- "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz",
- "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz",
- "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz",
- "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz",
- "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz",
- "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz",
- "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz",
- "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz",
- "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
- "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
- "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/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "19.3.0",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
- "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.3.0",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
- "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.3.0"
- }
- },
- "node_modules/@vitejs/plugin-react": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
- "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.29.0",
- "@babel/plugin-transform-react-jsx-self": "^7.27.1",
- "@babel/plugin-transform-react-jsx-source": "^7.27.1",
- "@rolldown/pluginutils": "1.0.0-rc.3",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.18.0"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/@vitest/expect": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
- "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.7",
- "@vitest/utils": "3.2.7",
- "chai": "^5.2.0",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
- "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "3.2.7",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
- "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
- "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "3.2.7",
- "pathe": "^2.0.3",
- "strip-literal": "^3.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
- "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.7",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
- "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^4.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
- "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.7",
- "loupe": "^3.1.4",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.11.22",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz",
- "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/browserslist": {
- "version": "4.28.9",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
- "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
- "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": {
- "baseline-browser-mapping": "^2.11.20",
- "caniuse-lite": "^1.0.30001810",
- "electron-to-chromium": "^1.5.420",
- "node-releases": "^2.0.54",
- "update-browserslist-db": "^1.3.2"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001810",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
- "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
- "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/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/check-error": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
- "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/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/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/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.427",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz",
- "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/esbuild": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.2",
- "@esbuild/android-arm": "0.28.2",
- "@esbuild/android-arm64": "0.28.2",
- "@esbuild/android-x64": "0.28.2",
- "@esbuild/darwin-arm64": "0.28.2",
- "@esbuild/darwin-x64": "0.28.2",
- "@esbuild/freebsd-arm64": "0.28.2",
- "@esbuild/freebsd-x64": "0.28.2",
- "@esbuild/linux-arm": "0.28.2",
- "@esbuild/linux-arm64": "0.28.2",
- "@esbuild/linux-ia32": "0.28.2",
- "@esbuild/linux-loong64": "0.28.2",
- "@esbuild/linux-mips64el": "0.28.2",
- "@esbuild/linux-ppc64": "0.28.2",
- "@esbuild/linux-riscv64": "0.28.2",
- "@esbuild/linux-s390x": "0.28.2",
- "@esbuild/linux-x64": "0.28.2",
- "@esbuild/netbsd-arm64": "0.28.2",
- "@esbuild/netbsd-x64": "0.28.2",
- "@esbuild/openbsd-arm64": "0.28.2",
- "@esbuild/openbsd-x64": "0.28.2",
- "@esbuild/openharmony-arm64": "0.28.2",
- "@esbuild/sunos-x64": "0.28.2",
- "@esbuild/win32-arm64": "0.28.2",
- "@esbuild/win32-ia32": "0.28.2",
- "@esbuild/win32-x64": "0.28.2"
- }
- },
- "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/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "node_modules/expect-type": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
- "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
- "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/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
- "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/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.19",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
- "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.55",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz",
- "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
- "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.28",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
- "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.18",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/react": {
- "version": "19.3.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
- "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.3.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
- "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.28.0"
- },
- "peerDependencies": {
- "react": "^19.3.0"
- }
- },
- "node_modules/react-refresh": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
- "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/rollup": {
- "version": "4.63.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
- "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.9"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
- "@rollup/rollup-android-arm-eabi": "4.63.1",
- "@rollup/rollup-android-arm64": "4.63.1",
- "@rollup/rollup-darwin-arm64": "4.63.1",
- "@rollup/rollup-darwin-x64": "4.63.1",
- "@rollup/rollup-freebsd-arm64": "4.63.1",
- "@rollup/rollup-freebsd-x64": "4.63.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.63.1",
- "@rollup/rollup-linux-arm64-gnu": "4.63.1",
- "@rollup/rollup-linux-arm64-musl": "4.63.1",
- "@rollup/rollup-linux-loong64-gnu": "4.63.1",
- "@rollup/rollup-linux-loong64-musl": "4.63.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.63.1",
- "@rollup/rollup-linux-ppc64-musl": "4.63.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.63.1",
- "@rollup/rollup-linux-riscv64-musl": "4.63.1",
- "@rollup/rollup-linux-s390x-gnu": "4.63.1",
- "@rollup/rollup-linux-x64-gnu": "4.63.1",
- "@rollup/rollup-linux-x64-musl": "4.63.1",
- "@rollup/rollup-openbsd-x64": "4.63.1",
- "@rollup/rollup-openharmony-arm64": "4.63.1",
- "@rollup/rollup-win32-arm64-msvc": "4.63.1",
- "@rollup/rollup-win32-ia32-msvc": "4.63.1",
- "@rollup/rollup-win32-x64-gnu": "4.63.1",
- "@rollup/rollup-win32-x64-msvc": "4.63.1",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/scheduler": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
- "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
- "license": "MIT"
- },
- "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/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/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/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/strip-literal": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/strip-literal/node_modules/js-tokens": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinypool": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
- "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
- "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/vite": {
- "version": "7.3.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
- "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.27.0 || ^0.28.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
- "tinyglobby": "^0.2.15"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "lightningcss": "^1.21.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite-node": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.4.1",
- "es-module-lexer": "^1.7.0",
- "pathe": "^2.0.3",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/vitest": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
- "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.7",
- "@vitest/mocker": "3.2.7",
- "@vitest/pretty-format": "^3.2.7",
- "@vitest/runner": "3.2.7",
- "@vitest/snapshot": "3.2.7",
- "@vitest/spy": "3.2.7",
- "@vitest/utils": "3.2.7",
- "chai": "^5.2.0",
- "debug": "^4.4.1",
- "expect-type": "^1.2.1",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.2",
- "std-env": "^3.9.0",
- "tinybench": "^2.9.0",
- "tinyexec": "^0.3.2",
- "tinyglobby": "^0.2.14",
- "tinypool": "^1.1.1",
- "tinyrainbow": "^2.0.0",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
- "vite-node": "3.2.4",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/debug": "^4.1.12",
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.7",
- "@vitest/ui": "3.2.7",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/debug": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/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/yaml": {
- "version": "2.9.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
- "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- }
- }
-}
diff --git a/tippen/package.json b/tippen/package.json
deleted file mode 100644
index a664250..0000000
--- a/tippen/package.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "delfin-tippen",
- "private": true,
- "version": "0.1.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "preview": "vite preview",
- "test": "vitest run"
- },
- "dependencies": {
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "yaml": "^2.6.0"
- },
- "devDependencies": {
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "@vitejs/plugin-react": "^5.0.0",
- "typescript": "^5.9.0",
- "vite": "^7.1.0",
- "vitest": "^3.2.0"
- },
- "allowScripts": {
- "esbuild@0.28.2": true
- }
-}
diff --git a/tippen/public/dolphin-mascot.png b/tippen/public/dolphin-mascot.png
deleted file mode 100644
index bf3fb0c..0000000
Binary files a/tippen/public/dolphin-mascot.png and /dev/null differ
diff --git a/tippen/public/fonts/nunito-latin-ext.woff2 b/tippen/public/fonts/nunito-latin-ext.woff2
deleted file mode 100644
index 584a55b..0000000
Binary files a/tippen/public/fonts/nunito-latin-ext.woff2 and /dev/null differ
diff --git a/tippen/public/fonts/nunito-latin.woff2 b/tippen/public/fonts/nunito-latin.woff2
deleted file mode 100644
index e118818..0000000
Binary files a/tippen/public/fonts/nunito-latin.woff2 and /dev/null differ
diff --git a/tippen/public/fonts/nunito.css b/tippen/public/fonts/nunito.css
deleted file mode 100644
index 5404723..0000000
--- a/tippen/public/fonts/nunito.css
+++ /dev/null
@@ -1,22 +0,0 @@
-/* Nunito, self-hosted: one variable file per subset covers 500-900.
-
- Google Fonts would be a network round trip the mouse may not have, and a
- cached app shell rendered in the fallback face looks broken. latin-ext is
- what carries "Hörbücher" and "Käpt'n"; the Cyrillic and Vietnamese subsets
- Google also offers are dropped, since nothing in this UI can produce them. */
-@font-face {
- font-family: "Nunito";
- font-style: normal;
- font-weight: 500 900;
- font-display: swap;
- src: url("/fonts/nunito-latin-ext.woff2") format("woff2");
- unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-@font-face {
- font-family: "Nunito";
- font-style: normal;
- font-weight: 500 900;
- font-display: swap;
- src: url("/fonts/nunito-latin.woff2") format("woff2");
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
diff --git a/tippen/public/icon-192.png b/tippen/public/icon-192.png
deleted file mode 100644
index a0ef028..0000000
Binary files a/tippen/public/icon-192.png and /dev/null differ
diff --git a/tippen/scripts/aquarium-bild.sh b/tippen/scripts/aquarium-bild.sh
deleted file mode 100755
index 876ada9..0000000
--- a/tippen/scripts/aquarium-bild.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env bash
-# Turns a creature illustration on solid black into a transparent aquarium sprite.
-#
-# scripts/aquarium-bild.sh bild.png public/aquarium/krake.webp
-#
-# The illustrations come out of the image generator as RGB on pure black, with no alpha
-# channel. "Black becomes transparent" is not enough: the eyes and eyelashes are black
-# too, and a creature with see-through eyes looks haunted on a turquoise sea. So the
-# background is found as a *region*, not a colour - every dark area big enough to not be
-# a facial feature - and only that region is removed.
-#
-# The edge is the other half. The art was antialiased onto black, so its outline pixels
-# are the creature's colour already mixed with black. Cutting them hard leaves a dark
-# halo; instead, near the background the opacity follows the brightness and the colour
-# is divided back out of the black (un-premultiplied), which is what the mix actually was.
-set -euo pipefail
-
-if [[ $# -lt 2 ]]; then
- echo "usage: $0 input.png output.webp [size]" >&2
- exit 1
-fi
-src=$1
-out=$2
-size=${3:-512}
-
-tmp=$(mktemp -d)
-trap 'rm -rf "$tmp"' EXIT
-
-# How far each pixel is from black: its brightest channel.
-magick "$src" -alpha off -channel RGB -separate -evaluate-sequence max "$tmp/hell.png"
-
-# Dark pixels, then drop every dark island under 5000 px back into the body. Measured on
-# the first five creatures: the background is one region of ~1M px, the largest facial
-# feature is ~2000 px, and nothing sits in between.
-magick "$tmp/hell.png" -threshold 8% -negate \
- -define connected-components:area-threshold=5000 \
- -define connected-components:mean-color=true \
- -connected-components 8 "$tmp/grund.png"
-
-# Opacity: solid inside the body, zero out in the background, and in a thin band along
-# the outline a ramp on brightness - the band is what catches the antialiased edge. The
-# ramp starts a little above black, or the generator's faint noise in the background
-# would come back as a faint haze around every creature.
-magick "$tmp/grund.png" -morphology Dilate Disk:3 -negate "$tmp/koerper.png"
-magick "$tmp/grund.png" -negate -morphology Dilate Disk:5 "$tmp/umriss.png"
-magick "$tmp/hell.png" -evaluate Subtract 4% -evaluate Multiply 3.8 "$tmp/rampe.png"
-magick "$tmp/rampe.png" "$tmp/koerper.png" -compose Lighten -composite \
- "$tmp/umriss.png" -compose Multiply -composite "$tmp/alpha.png"
-
-# Divide the black back out of the edge colours, attach the opacity, cut to the creature
-# and scale down - the sprites are drawn at a few hundred px at most. `-alpha background`
-# blanks the colour of fully transparent pixels, which is what lets `-trim` see them as
-# border at all.
-magick "$src" -alpha off "$tmp/alpha.png" -compose DivideSrc -composite \
- "$tmp/alpha.png" -alpha off -compose CopyOpacity -composite \
- -background none -alpha background -trim +repage -resize "${size}x${size}>" \
- -quality 90 -define webp:alpha-quality=100 "$out"
diff --git a/tippen/src/data/curriculum.yaml b/tippen/src/data/curriculum.yaml
deleted file mode 100644
index 1ab8275..0000000
--- a/tippen/src/data/curriculum.yaml
+++ /dev/null
@@ -1,422 +0,0 @@
-# The lesson plan, as data rather than code, so wording, word lists and pacing can be
-# tweaked here without touching curriculum.ts.
-#
-# Every lesson: title, subtitle (read aloud). Then either:
-# - keys: the letters-kind key(s) this round drills. The loader tracks which keys were
-# already active: the first time a key appears its lesson is "isolated" (heavy
-# weight, alone); the second (identical) appearance is "mixed" (lighter weight,
-# blended with everything learned so far). Two lessons with the same `keys` back to
-# back is exactly how you write "isolated, then mixed" - no separate flag needed.
-# - drill: true - a pure review round, no new content, whatever is active so far.
-# - kind + words - a fragments/words/sentences consolidation round (dive mode by
-# default; `mode:` overrides it, see curriculum.ts for which modes fit which kind).
-#
-# Letter order follows German letter frequency, adapted to a home-row-first, mirrored
-# pace for a six-year-old - unchanged from the original course.
-
-worlds:
- - number: 1
- title: "Die Grundstellung"
- emoji: "🏝️"
- reward: clownfish
- lessons:
- - title: "F und J"
- subtitle: "Die Zeigefinger - die Tasten mit den Punkten"
- keys: [f, j]
- - title: "F und J üben"
- subtitle: "Die neuen Tasten festigen"
- keys: [f, j]
- - title: "D und K"
- subtitle: "Die Mittelfinger"
- keys: [d, k]
- - title: "D und K üben"
- subtitle: "Die neuen Tasten festigen"
- keys: [d, k]
- - title: "Übung: F J D K"
- subtitle: "Die vier Tasten zusammen"
- drill: true
- - title: "S und L"
- subtitle: "Die Ringfinger"
- keys: [s, l]
- - title: "S und L üben"
- subtitle: "Die neuen Tasten festigen"
- keys: [s, l]
- - title: "Übung: sechs Tasten"
- subtitle: "Alles bisher zusammen"
- drill: true
- - title: "A und Ö"
- subtitle: "Die kleinen Finger"
- keys: [a, ö]
- - title: "A und Ö üben"
- subtitle: "Die neuen Tasten festigen"
- keys: [a, ö]
- - title: "Übung: die Grundstellung"
- subtitle: "Alle acht Finger"
- drill: true
- - title: "Erste kleine Wörter"
- subtitle: "Echte Wörter mit acht Tasten"
- kind: fragments
- words: [da, ja, das, dass, als, all, fall, falls, lass, saal, kalk, salsa, jass]
- - title: "Die Leertaste"
- subtitle: "Der Daumen kommt dazu"
- keys: [" "]
- - title: "Übung: Grundstellung mit Leertaste"
- subtitle: "Jetzt mit dem Daumen"
- drill: true
- - title: "Wörter mit Leertaste"
- subtitle: "Kleine Wörter, kleine Sätze"
- kind: fragments
- words: ["lass das", "das da", "da ja", "fall da", "kalk da", "saal da", "ja lass das", "als da"]
-
- - number: 2
- title: "Nach oben"
- emoji: "🌊"
- reward: octopus
- lessons:
- - title: "Das E"
- subtitle: "Mittelfinger links nach oben"
- keys: [e]
- - title: "E üben"
- subtitle: "Die neue Taste festigen"
- keys: [e]
- - title: "Das I"
- subtitle: "Mittelfinger rechts nach oben"
- keys: [i]
- - title: "I üben"
- subtitle: "Die neue Taste festigen"
- keys: [i]
- - title: "Übung: E und I"
- subtitle: "Beide Mittelfinger nach oben"
- drill: true
- - title: "Das R"
- subtitle: "Zeigefinger links nach oben"
- keys: [r]
- - title: "R üben"
- subtitle: "Die neue Taste festigen"
- keys: [r]
- - title: "Das U"
- subtitle: "Zeigefinger rechts nach oben"
- keys: [u]
- - title: "U üben"
- subtitle: "Die neue Taste festigen"
- keys: [u]
- - title: "Übung: R und U"
- subtitle: "Beide Zeigefinger nach oben"
- drill: true
- - title: "Kleine Wörter: E I R U"
- subtitle: "Erste echte Wörter mit der oberen Reihe"
- kind: fragments
- words: [die, sie, elf, eis, esel, see, keks, fiel, lied, rad, reis, eier, rufe, feuer, sauer, lauf]
- - title: "Das T"
- subtitle: "Zeigefinger links weit nach oben"
- keys: [t]
- - title: "T üben"
- subtitle: "Die neue Taste festigen"
- keys: [t]
- - title: "Das Z"
- subtitle: "Zeigefinger rechts weit nach oben"
- keys: [z]
- - title: "Z üben"
- subtitle: "Die neue Taste festigen"
- keys: [z]
- - title: "Übung: T und Z"
- subtitle: "Weit nach oben greifen"
- drill: true
- - title: "Das O"
- subtitle: "Ringfinger rechts nach oben"
- keys: [o]
- - title: "O üben"
- subtitle: "Die neue Taste festigen"
- keys: [o]
- - title: "Das W"
- subtitle: "Ringfinger links nach oben"
- keys: [w]
- - title: "W üben"
- subtitle: "Die neue Taste festigen"
- keys: [w]
- - title: "Übung: O und W"
- subtitle: "Beide Ringfinger nach oben"
- drill: true
- - title: "Kleine Wörter: T Z O W"
- subtitle: "Noch mehr echte Wörter"
- kind: fragments
- mode: feed
- words: [tier, tafel, kette, leiter, zeit, salz, zelt, katze, rot, tor, los, foto, wo, wald, zwei, wolke]
- - title: "Das P"
- subtitle: "Kleiner Finger rechts nach oben"
- keys: [p]
- - title: "P üben"
- subtitle: "Die neue Taste festigen"
- keys: [p]
- - title: "Das Q"
- subtitle: "Kleiner Finger links nach oben"
- keys: [q]
- - title: "Q üben"
- subtitle: "Die neue Taste festigen"
- keys: [q]
- - title: "Das Ü"
- subtitle: "Kleiner Finger rechts, ganz außen"
- keys: [ü]
- - title: "Ü üben"
- subtitle: "Die neue Taste festigen"
- keys: [ü]
- - title: "Übung: P Q Ü"
- subtitle: "Die kleinen Finger nach oben"
- drill: true
- - title: "Übung: die obere Reihe"
- subtitle: "Die ganze Reihe zusammen"
- kind: words
- mode: feed
- words: [wolke, zeit, pause, prüfe, qualle, torte, reiter, würfel]
- - title: "Übung: Welt 2 komplett"
- subtitle: "Alles aus der oberen Reihe"
- drill: true
-
- - number: 3
- title: "Nach unten"
- emoji: "🪸"
- reward: seahorse
- lessons:
- - title: "Das N"
- subtitle: "Zeigefinger rechts nach unten"
- keys: [n]
- - title: "N üben"
- subtitle: "Die neue Taste festigen"
- keys: [n]
- - title: "Das M"
- subtitle: "Zeigefinger rechts, neben dem N"
- keys: [m]
- - title: "M üben"
- subtitle: "Die neue Taste festigen"
- keys: [m]
- - title: "Übung: N und M"
- subtitle: "Die neuen Tasten festigen"
- drill: true
- - title: "Das G"
- subtitle: "Zeigefinger links, in der Mitte"
- keys: [g]
- - title: "G üben"
- subtitle: "Die neue Taste festigen"
- keys: [g]
- - title: "Das H"
- subtitle: "Zeigefinger rechts, in der Mitte"
- keys: [h]
- - title: "H üben"
- subtitle: "Die neue Taste festigen"
- keys: [h]
- - title: "Übung: G und H"
- subtitle: "Die Mitte der Grundreihe"
- drill: true
- - title: "Kleine Wörter: N M G H"
- subtitle: "Erste echte Wörter nach unten"
- kind: fragments
- words: [nase, nein, kind, wind, sonne, mama, mond, meer, maus, gut, gans, regen, hase, haus, hund, hupe]
- - title: "Das C"
- subtitle: "Mittelfinger links nach unten"
- keys: [c]
- - title: "C üben"
- subtitle: "Die neue Taste festigen"
- keys: [c]
- - title: "Das V"
- subtitle: "Zeigefinger links nach unten"
- keys: [v]
- - title: "V üben"
- subtitle: "Die neue Taste festigen"
- keys: [v]
- - title: "Übung: C und V"
- subtitle: "Nach unten greifen"
- drill: true
- - title: "Das B"
- subtitle: "Zeigefinger links, neben dem V"
- keys: [b]
- - title: "B üben"
- subtitle: "Die neue Taste festigen"
- keys: [b]
- - title: "Das Y"
- subtitle: "Kleiner Finger links nach unten"
- keys: [y]
- - title: "Y üben"
- subtitle: "Die neue Taste festigen"
- keys: [y]
- - title: "Übung: B und Y"
- subtitle: "Ganz unten links"
- drill: true
- - title: "Kleine Wörter: C V B Y"
- subtitle: "Noch mehr echte Wörter"
- kind: fragments
- words: [koch, milch, schule, chaos, vier, vase, voll, vater, baum, boot, bunt, brot, yoga, baby, typ, pony]
- - title: "Das X"
- subtitle: "Ringfinger links nach unten"
- keys: [x]
- - title: "X üben"
- subtitle: "Die neue Taste festigen"
- keys: [x]
- - title: "Das Ä"
- subtitle: "Kleiner Finger rechts, ganz außen"
- keys: [ä]
- - title: "Ä üben"
- subtitle: "Die neue Taste festigen"
- keys: [ä]
- - title: "Übung: X und Ä"
- subtitle: "Die letzten beiden Tasten"
- drill: true
- - title: "Übung: alle Buchstaben"
- subtitle: "Das ganze Alphabet"
- kind: words
- mode: race
- words: [delfin, wasser, xylofon, bäume, vogel, qualle, muschel, tauchen]
- - title: "Übung: Welt 3 komplett"
- subtitle: "Alles aus der unteren Reihe"
- drill: true
-
- - number: 4
- title: "Große Buchstaben"
- emoji: "👑"
- reward: turtle
- lessons:
- - title: "Umschalttaste rechts, Teil 1"
- subtitle: "Große Buchstaben der linken Hand"
- keys: ["⇧"]
- kind: words
- words: [Delfin, Wal, Fisch, Baum]
- - title: "Umschalttaste rechts, Teil 2"
- subtitle: "Noch mehr große Buchstaben"
- kind: words
- words: [Garten, Ente, Vogel, Riff]
- - title: "Umschalttaste links, Teil 1"
- subtitle: "Große Buchstaben der rechten Hand"
- kind: words
- words: [Haus, Kind, Mond, Nase]
- - title: "Umschalttaste links, Teil 2"
- subtitle: "Noch mehr große Buchstaben"
- kind: words
- words: [Lampe, Onkel, Uhr, Puppe]
- - title: "Übung: Namen, Teil 1"
- subtitle: "Namen fangen groß an"
- kind: words
- words: [Anna, Lena, Paul, Mia, Emil, Jonas, Tom, Lisa]
- - title: "Übung: Namen, Teil 2"
- subtitle: "Noch mehr Namen"
- kind: words
- mode: feed
- words: [Ben, Nora, Finn, Ida, Max, Ella, Oskar, Greta]
- - title: "Übung: große und kleine"
- subtitle: "Beides gemischt"
- kind: words
- words: ["Das Meer", "Ein Delfin", "Die Sonne", "Mein Boot", "Der Wal", "Eine Muschel", "Ein Fisch", "Das Riff", "Mein Ball", "Die Welle", "Ein Stern", "Der Hai"]
- - title: "Übung: Welt 4 komplett"
- subtitle: "Groß und klein zusammen"
- kind: words
- drill: true
- words: [Delfin, Haus, Anna, Ben, "Das Meer", "Der Wal", Mond, Riff]
-
- - number: 5
- title: "Ganze Sätze"
- emoji: "📖"
- reward: pearlmussel
- lessons:
- - title: "Der Punkt"
- subtitle: "Ringfinger rechts nach unten"
- keys: ["."]
- - title: "Punkt üben"
- subtitle: "Die neue Taste festigen"
- keys: ["."]
- - title: "Erste Sätze mit Punkt"
- subtitle: "Ein Satz, ein Punkt"
- kind: sentences
- words:
- - "Das Meer ist tief."
- - "Der Hund bellt."
- - "Ich mag Kekse."
- - "Die Sonne scheint."
- - "Der Wal ist riesig."
- - "Wir gehen baden."
- - "Mama liest ein Buch."
- - "Der Fisch schwimmt."
- - "Heute ist es warm."
- - "Ich habe einen Ball."
- - "Die Welle ist hoch."
- - "Papa kocht Suppe."
- - title: "Das Komma"
- subtitle: "Mittelfinger rechts nach unten"
- keys: [","]
- - title: "Komma üben"
- subtitle: "Die neue Taste festigen"
- keys: [","]
- - title: "Sätze mit Komma"
- subtitle: "Zwei Gedanken, ein Satz"
- kind: sentences
- mode: race
- words:
- - "Ich mag Wale, Delfine und Fische."
- - "Erst lesen, dann tippen."
- - "Es ist warm, also baden wir."
- - "Rot, gelb und blau sind Farben."
- - "Wenn es regnet, bleiben wir drinnen."
- - "Der Delfin springt, taucht und spielt."
- - "Morgen, sagt Papa, fahren wir los."
- - "Eins, zwei, drei, vier."
- - "Oma, Opa und ich gehen schwimmen."
- - "Die Sonne scheint, das Meer glitzert."
- - "Muscheln, Steine und Sand liegen am Strand."
- - title: "Der Bindestrich"
- subtitle: "Kleiner Finger rechts, ganz außen"
- keys: ["-"]
- - title: "Bindestrich üben"
- subtitle: "Die neue Taste festigen"
- keys: ["-"]
- - title: "Sätze mit Bindestrich"
- subtitle: "Zwei Wörter, ein Strich"
- kind: sentences
- words:
- - "Wir spielen mit dem Wasser-Ball."
- - "Das ist ein Delfin-Baby."
- - "Meine Ur-Oma kommt heute."
- - "Wir bauen eine Sand-Burg."
- - "Der Fisch-Schwarm ist riesig."
- - "Ich trage mein T-Shirt."
- - "Das Schwimm-Bad ist offen."
- - "Die Bade-Hose ist nass."
- - "Wir essen ein Eis-Hörnchen."
- - "Das Segel-Boot ist blau."
- - "Mein Lieblings-Tier ist der Delfin."
- - title: "Fragezeichen und Ausrufezeichen"
- subtitle: "Mit der Umschalttaste"
- keys: ["ß", "1"]
- - title: "Fragezeichen und Ausrufezeichen üben"
- subtitle: "Die neuen Tasten festigen"
- keys: ["ß", "1"]
- - title: "Fragen und Rufe"
- subtitle: "Wie klingt ein Satz?"
- kind: sentences
- words:
- - "Wo ist der Delfin?"
- - "Das war toll!"
- - "Wie geht es dir?"
- - "Pass auf!"
- - "Kommst du mit?"
- - "Der Wal ist so groß!"
- - "Hast du Hunger?"
- - "Hurra, Ferien!"
- - "Was schwimmt da?"
- - "Schau mal, ein Hai!"
- - "Wie tief ist das Meer?"
- - "Wir haben es geschafft!"
- - title: "Übung: ganze Sätze"
- subtitle: "Alles zusammen"
- kind: sentences
- drill: true
- mode: race
- words:
- - "Der Delfin schwimmt sehr schnell."
- - "Wo ist mein Boot?"
- - "Ich tippe jetzt mit zehn Fingern!"
- - "Das Meer ist blau, tief und kalt."
- - "Kannst du das auch?"
- - "Wir bauen eine Sand-Burg am Strand."
- - "Die Möwe fliegt über das Wasser."
- - "Oma, Opa und ich gehen schwimmen."
- - "Das ist ja super!"
- - "Wie heißt der große Wal?"
- - "Im Riff wohnen bunte Fische."
- - "Der Krake hat acht Arme."
diff --git a/tippen/src/lib/__tests__/curriculum.test.ts b/tippen/src/lib/__tests__/curriculum.test.ts
deleted file mode 100644
index 64a3bf3..0000000
--- a/tippen/src/lib/__tests__/curriculum.test.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { ELIGIBLE_MODES, FIRST_LESSON_ID, LESSONS, WORLDS, lessonById, lessonsOfWorld, nextLesson } from "../curriculum";
-import { fingerOf, keyForChar, needsShift } from "../fingers";
-
-describe("LESSONS", () => {
- it("has unique ids and consecutive numbers", () => {
- const ids = LESSONS.map((lesson) => lesson.id);
- expect(new Set(ids).size).toBe(ids.length);
- LESSONS.forEach((lesson, i) => expect(lesson.number).toBe(i + 1));
- });
-
- it("starts on the two keys with the tactile bumps", () => {
- expect(LESSONS[0]!.newKeys).toEqual(["f", "j"]);
- expect(FIRST_LESSON_ID).toBe(LESSONS[0]!.id);
- });
-
- it("eases in: at most two new keys per lesson, anywhere in the course", () => {
- // The first draft opened with all four left-hand keys at once, which is a steep
- // first five minutes for a six-year-old. Two at a time, always.
- for (const lesson of LESSONS) {
- expect(lesson.newKeys.length, `${lesson.number}: ${lesson.title}`).toBeLessThanOrEqual(2);
- }
- });
-
- it("teaches world 1 as mirrored finger pairs, one finger per hand", () => {
- const pairs = lessonsOfWorld(1).filter((lesson) => lesson.newKeys.length === 2);
- expect(pairs.length).toBe(4);
- for (const lesson of pairs) {
- const [left, right] = lesson.newKeys.map((key) => fingerOf(key)!);
- expect(left!.hand).toBe("left");
- expect(right!.hand).toBe("right");
- // Same finger on each hand - "die Zeigefinger", "die Mittelfinger", …
- expect(left!.id.replace("left-", "")).toBe(right!.id.replace("right-", ""));
- }
- });
-
- it("has the whole home row active by the end of world 1", () => {
- const last = lessonsOfWorld(1).at(-1)!;
- for (const key of "asdfjklö") expect(last.activeKeys).toContain(key);
- expect(last.activeKeys).toContain(" ");
- });
-
- it("makes every round a real block of practice, not a handful of keys", () => {
- // The first version ran 12 characters in world 1 - long enough to finish, too short
- // for a rhythm to form or for the speed score to mean anything.
- for (const lesson of LESSONS) {
- if (lesson.kind === "sentences") {
- expect(lesson.chunks, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(10);
- } else if (lesson.kind === "words") {
- expect(lesson.chunks, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(25);
- } else {
- expect(lesson.chunks * lesson.chunkSize, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(60);
- }
- }
- });
-
- it("still starts gentler than it ends", () => {
- const length = (lesson: (typeof LESSONS)[number]) => lesson.chunks * lesson.chunkSize;
- expect(length(lessonsOfWorld(3)[0]!)).toBeGreaterThan(length(lessonsOfWorld(1)[0]!));
- });
-
- it("has more sentences to draw from than a round uses, so a round never has to repeat", () => {
- for (const lesson of LESSONS.filter((l) => l.kind === "sentences")) {
- expect(lesson.words.length, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(lesson.chunks);
- }
- });
-
- it("grows activeKeys monotonically - no lesson ever loses a key", () => {
- let previous: string[] = [];
- for (const lesson of LESSONS) {
- for (const key of previous) expect(lesson.activeKeys).toContain(key);
- previous = [...lesson.activeKeys];
- }
- });
-
- it("introduces every new key into its own activeKeys", () => {
- for (const lesson of LESSONS) {
- for (const key of lesson.newKeys) {
- if (key === "⇧") continue; // Shift is not a character the generator can emit.
- expect(lesson.activeKeys).toContain(key);
- }
- }
- });
-
- it("covers the whole German alphabet plus the umlauts by the end", () => {
- const final = new Set(LESSONS.at(-1)!.activeKeys);
- for (const key of "abcdefghijklmnopqrstuvwxyzäöü") expect(final.has(key)).toBe(true);
- });
-
- it("assigns every active key to a real finger", () => {
- for (const lesson of LESSONS) {
- for (const key of lesson.activeKeys) expect(fingerOf(key)).not.toBeNull();
- }
- });
-
- it("only lists words its own activeKeys can type", () => {
- // Compared by physical key, not by character: "?" is Shift+ß and "A" is Shift+a,
- // so a lesson can type them as soon as it has the ß or a key.
- for (const lesson of LESSONS) {
- const active = new Set(lesson.activeKeys);
- for (const word of lesson.words) {
- for (const char of word) {
- expect(
- active.has(keyForChar(char)),
- `Lektion ${lesson.number} (${lesson.title}): "${word}" braucht "${char}" (Taste ${keyForChar(char)})`,
- ).toBe(true);
- }
- }
- }
- });
-
- it("only uses shifted characters once shift is taught", () => {
- const shiftFrom = LESSONS.find((lesson) => lesson.newKeys.includes("⇧"))!.number;
- for (const lesson of LESSONS) {
- for (const word of lesson.words) {
- for (const char of word) {
- if (!needsShift(char)) continue;
- expect(
- lesson.number,
- `Lektion ${lesson.number}: "${word}" braucht Umschalt für "${char}"`,
- ).toBeGreaterThanOrEqual(shiftFrom);
- }
- }
- }
- });
-
- it("plays a mode that actually fits its kind", () => {
- for (const lesson of LESSONS) {
- expect(ELIGIBLE_MODES[lesson.kind], `${lesson.number}: ${lesson.title}`).toContain(lesson.primaryMode);
- for (const mode of lesson.bonusModes) {
- expect(ELIGIBLE_MODES[lesson.kind], `${lesson.number}: ${lesson.title}`).toContain(mode);
- }
- }
- });
-
- it("never plays the same arcade game twice in a row", () => {
- const arcade = LESSONS.filter((lesson) => lesson.kind === "letters");
- for (let i = 1; i < arcade.length; i++) {
- expect(arcade[i]!.primaryMode, `${arcade[i]!.number}: ${arcade[i]!.title}`).not.toBe(arcade[i - 1]!.primaryMode);
- }
- });
-
- it("offers every eligible mode this lesson isn't gated on, as a bonus", () => {
- for (const lesson of LESSONS) {
- const expected = ELIGIBLE_MODES[lesson.kind].filter(
- (mode) => (mode === "feed" || mode === "race") && mode !== lesson.primaryMode,
- );
- expect([...lesson.bonusModes].sort()).toEqual([...expected].sort());
- }
- });
-
- it("mixes the games instead of always diving - feed and race take a turn as the required mode too", () => {
- const words = LESSONS.filter((l) => l.kind === "words");
- const sentences = LESSONS.filter((l) => l.kind === "sentences");
- expect(words.some((l) => l.primaryMode === "feed")).toBe(true);
- expect(words.some((l) => l.primaryMode === "race")).toBe(true);
- expect(sentences.some((l) => l.primaryMode === "race")).toBe(true);
- });
-
- it("distinguishes a mixed round from a drill - both have no new keys, only one is a review", () => {
- const mixed = LESSONS.filter((lesson) => lesson.emphasis === "mixed");
- expect(mixed.length).toBeGreaterThan(0);
- for (const lesson of mixed) {
- expect(lesson.newKeys).toEqual([]);
- expect(lesson.isDrill).toBe(false);
- }
- });
-
- it("gives every isolated round something new to drill", () => {
- for (const lesson of LESSONS) {
- if (lesson.emphasis === "isolated") expect(lesson.newKeys.length).toBeGreaterThan(0);
- }
- });
-
- it("teaches capitals only once shift exists", () => {
- for (const lesson of LESSONS) {
- const hasUppercase = lesson.words.some((word) => word !== word.toLowerCase());
- if (hasUppercase) expect(lesson.world).toBeGreaterThanOrEqual(4);
- }
- });
-
- it("gives every new key a lesson of its own after world 1", () => {
- // One key at a time is the pacing decision: world 1 pairs a finger across both
- // hands, everything after introduces exactly one key or none.
- for (const lesson of LESSONS) {
- if (lesson.world === 1) continue;
- expect(lesson.newKeys.length, `${lesson.number}: ${lesson.title}`).toBeLessThanOrEqual(2);
- }
- });
-
- it("follows every pair of new keys with a drill", () => {
- const drills = LESSONS.filter((lesson) => lesson.isDrill);
- expect(drills.length).toBeGreaterThanOrEqual(15);
- // A drill never introduces anything, and always has something to practise.
- for (const lesson of drills) {
- expect(lesson.newKeys).toEqual([]);
- expect(lesson.activeKeys.length).toBeGreaterThan(0);
- }
- });
-
- it("is long enough to be a real, Duolingo-length course", () => {
- expect(LESSONS.length).toBeGreaterThanOrEqual(80);
- expect(WORLDS.length).toBe(5);
- });
-
- it("never leaves a world without a run of at least three lessons", () => {
- for (const world of WORLDS) expect(lessonsOfWorld(world.number).length).toBeGreaterThanOrEqual(3);
- });
-});
-
-describe("Worlds", () => {
- it("has lessons in every world", () => {
- for (const world of WORLDS) expect(lessonsOfWorld(world.number).length).toBeGreaterThan(0);
- });
-
- it("gives every world its own aquarium creature", () => {
- const rewards = WORLDS.map((world) => world.reward);
- expect(new Set(rewards).size).toBe(rewards.length);
- });
-});
-
-describe("navigation", () => {
- it("chains every lesson to the next and stops at the end", () => {
- for (let i = 0; i < LESSONS.length - 1; i++) {
- expect(nextLesson(LESSONS[i]!.id)?.id).toBe(LESSONS[i + 1]!.id);
- }
- expect(nextLesson(LESSONS.at(-1)!.id)).toBeNull();
- expect(nextLesson("gibt-es-nicht")).toBeNull();
- });
-
- it("looks lessons up by id", () => {
- expect(lessonById(FIRST_LESSON_ID)?.number).toBe(1);
- expect(lessonById("gibt-es-nicht")).toBeNull();
- });
-});
diff --git a/tippen/src/lib/__tests__/progress.test.ts b/tippen/src/lib/__tests__/progress.test.ts
deleted file mode 100644
index ff88dd5..0000000
--- a/tippen/src/lib/__tests__/progress.test.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { FIRST_LESSON_ID, LESSONS } from "../curriculum";
-import { press, startRun } from "../engine";
-import { grade } from "../grading";
-import type { RunResult } from "../grading";
-import {
- DILIGENCE_ATTEMPTS,
- focusKeyFor,
- freshProgress,
- mastery,
- migrate,
- recordRun,
- today,
-} from "../progress";
-import type { Progress } from "../progress";
-
-const L1 = LESSONS[0]!.id;
-const L2 = LESSONS[1]!.id;
-
-/** A graded run of `target` with a wrong key at each of `wrongAt`, paced at `tickMs`. */
-function runResult(target: string, wrongAt: number[] = [], tickMs = 1000): RunResult {
- let state = startRun(target);
- let t = 0;
- for (let i = 0; i < target.length; i++) {
- if (wrongAt.includes(i)) {
- [state] = press(state, target[i] === "x" ? "q" : "x", t);
- t += tickMs;
- }
- [state] = press(state, target[i]!, t);
- t += tickMs;
- }
- return grade(state);
-}
-
-const perfect = () => runResult("asdfasdfasdfasdf");
-const bad = () => runResult("asdfasdfasdfasdf", [0, 1, 2, 3, 4, 5]);
-
-describe("freshProgress", () => {
- it("unlocks the first lesson and nothing else", () => {
- const progress = freshProgress();
- expect(progress.lessons[FIRST_LESSON_ID]!.unlocked).toBe(true);
- const unlocked = LESSONS.filter((lesson) => progress.lessons[lesson.id]!.unlocked);
- expect(unlocked).toHaveLength(1);
- });
-
- it("knows every lesson in the curriculum", () => {
- const progress = freshProgress();
- for (const lesson of LESSONS) expect(progress.lessons[lesson.id]).toBeDefined();
- });
-});
-
-describe("recordRun", () => {
- it("unlocks the next lesson on two stars", () => {
- const { progress, unlockedLessonId } = recordRun(freshProgress(), L1, perfect());
- expect(unlockedLessonId).toBe(L2);
- expect(progress.lessons[L2]!.unlocked).toBe(true);
- });
-
- it("does not unlock below two stars", () => {
- const result = bad();
- expect(result.stars).toBeLessThan(2);
- const { progress, unlockedLessonId } = recordRun(freshProgress(), L1, result);
- expect(unlockedLessonId).toBeNull();
- expect(progress.lessons[L2]!.unlocked).toBe(false);
- });
-
- it("unlocks after enough tries however bad the score - the diligence rule", () => {
- let progress = freshProgress();
- for (let i = 0; i < DILIGENCE_ATTEMPTS - 1; i++) {
- progress = recordRun(progress, L1, bad()).progress;
- expect(progress.lessons[L2]!.unlocked).toBe(false);
- }
- const { progress: last, unlockedLessonId } = recordRun(progress, L1, bad());
- expect(unlockedLessonId).toBe(L2);
- expect(last.lessons[L2]!.unlocked).toBe(true);
- });
-
- it("never lets the best animal or star count go down", () => {
- let progress = recordRun(freshProgress(), L1, perfect()).progress;
- const best = progress.lessons[L1]!;
- progress = recordRun(progress, L1, bad()).progress;
- expect(progress.lessons[L1]!.bestStars).toBe(best.bestStars);
- expect(progress.lessons[L1]!.bestPoints).toBe(best.bestPoints);
- expect(progress.lessons[L1]!.bestAnimal).toBe(best.bestAnimal);
- });
-
- it("counts every run, good or bad", () => {
- let progress = freshProgress();
- for (let i = 0; i < 3; i++) progress = recordRun(progress, L1, bad()).progress;
- expect(progress.lessons[L1]!.runs).toBe(3);
- });
-
- it("adds pearls on every run", () => {
- const first = recordRun(freshProgress(), L1, bad());
- expect(first.progress.pearls).toBeGreaterThan(0);
- const second = recordRun(first.progress, L1, bad());
- expect(second.progress.pearls).toBeGreaterThan(first.progress.pearls);
- });
-
- it("releases a creature only when a world is finished, and only once", () => {
- let progress = freshProgress();
- // World 1 is lessons 1-3, so finishing lesson 3 is what crosses into world 2.
- const world1 = LESSONS.filter((lesson) => lesson.world === 1);
- let released: string[] = [];
- for (const lesson of world1) {
- const outcome = recordRun(progress, lesson.id, perfect());
- progress = outcome.progress;
- if (outcome.newCreature) released.push(outcome.newCreature);
- }
- expect(released).toHaveLength(1);
- expect(progress.aquarium).toEqual(released);
-
- // Replaying the same lesson must not hand out a second copy.
- const again = recordRun(progress, world1.at(-1)!.id, perfect());
- expect(again.newCreature).toBeNull();
- expect(again.progress.aquarium).toEqual(released);
- });
-
- it("records a ghost of the best run for the race", () => {
- const { progress } = recordRun(freshProgress(), L1, perfect());
- expect(progress.lessons[L1]!.ghost?.length).toBe(perfect().characters);
- });
-
- it("builds the daily streak and restarts it after a gap", () => {
- let progress = recordRun(freshProgress(), L1, perfect(), "2026-09-11").progress;
- expect(progress.streak).toEqual({ days: 1, lastPlayed: "2026-09-11" });
-
- // A second run the same day does not double-count.
- progress = recordRun(progress, L1, perfect(), "2026-09-11").progress;
- expect(progress.streak.days).toBe(1);
-
- progress = recordRun(progress, L1, perfect(), "2026-09-12").progress;
- expect(progress.streak.days).toBe(2);
-
- // A missed day restarts at 1, never at 0.
- progress = recordRun(progress, L1, perfect(), "2026-09-20").progress;
- expect(progress.streak.days).toBe(1);
- });
-
- it("folds keystrokes into the per-key stats", () => {
- const { progress } = recordRun(freshProgress(), L1, runResult("asdf", [1]));
- expect(progress.keyStats["a"]?.attempts).toBe(1);
- expect(progress.keyStats["s"]?.errors).toBe(1);
- expect(progress.keyStats["s"]?.attempts).toBe(2);
- });
-});
-
-describe("migrate", () => {
- it("returns a fresh profile for anything unusable", () => {
- for (const raw of [null, undefined, 42, "nope", {}, { version: 99 }, []]) {
- const progress = migrate(raw);
- expect(progress.version).toBe(2);
- expect(progress.lessons[FIRST_LESSON_ID]!.unlocked).toBe(true);
- }
- });
-
- it("keeps a valid save", () => {
- const stored = recordRun(freshProgress(), L1, perfect()).progress;
- const restored = migrate(JSON.parse(JSON.stringify(stored)));
- expect(restored.lessons[L2]!.unlocked).toBe(true);
- expect(restored.pearls).toBe(stored.pearls);
- });
-
- it("drops lessons that no longer exist", () => {
- const stored = { ...freshProgress(), lessons: { ...freshProgress().lessons, oldLesson: {} } };
- expect(migrate(JSON.parse(JSON.stringify(stored))).lessons["oldLesson"]).toBeUndefined();
- });
-
- it("moves the emoji pets of old saves into today's aquarium", () => {
- const old = { ...freshProgress(), aquarium: ["🐠", "🐙", "🐠", "🦈", 7] };
- expect(migrate(JSON.parse(JSON.stringify(old))).aquarium).toEqual(["clownfish", "octopus"]);
- });
-
- it("re-unlocks the first lesson even if the save says otherwise", () => {
- const broken = freshProgress();
- broken.lessons[FIRST_LESSON_ID] = { ...broken.lessons[FIRST_LESSON_ID]!, unlocked: false };
- expect(migrate(broken).lessons[FIRST_LESSON_ID]!.unlocked).toBe(true);
- });
-
- it("restarts the lesson path from an old version, but keeps the aquarium, pearls and streak", () => {
- // Version 1's lesson ids don't correspond to today's much finer-grained curriculum,
- // so there is nothing sensible to remap them onto - she starts the path over, but
- // does not lose what she already earned.
- const old = {
- version: 1,
- lessons: { l09: { unlocked: true, runs: 3, bestStars: 3, bestAnimal: "orca", bestPoints: 999, ghost: null } },
- keyStats: { a: { ema: 400, attempts: 10, errors: 1 } },
- pearls: 42,
- aquarium: ["clownfish", "octopus"],
- streak: { days: 5, lastPlayed: "2024-01-01" },
- settings: { sound: false, keyboardHint: "off" },
- };
- const restored = migrate(JSON.parse(JSON.stringify(old)));
- expect(restored.version).toBe(2);
- // "l09" exists in the new curriculum too, just as a different lesson - its old
- // three-star, level-99 progress must not carry over onto whatever l09 means now.
- expect(restored.lessons["l09"]).toEqual({ unlocked: false, runs: 0, bestStars: 0, bestAnimal: null, bestPoints: 0, ghost: null });
- expect(restored.lessons[FIRST_LESSON_ID]!.unlocked).toBe(true);
- expect(Object.values(restored.lessons).filter((l) => l.unlocked)).toHaveLength(1);
- expect(restored.pearls).toBe(42);
- expect(restored.aquarium).toEqual(["clownfish", "octopus"]);
- expect(restored.keyStats["a"]?.attempts).toBe(10);
- expect(restored.streak).toEqual({ days: 5, lastPlayed: "2024-01-01" });
- expect(restored.settings).toEqual({ sound: false, keyboardHint: "off" });
- });
-});
-
-describe("focusKeyFor", () => {
- it("has no focus key on a lesson that has never been played", () => {
- // Otherwise "pick an unpractised key" picks whichever sorts first and drills it half
- // the line, starving the other three fingers on lesson 1.
- expect(focusKeyFor(freshProgress(), ["a", "s", "d", "f"])).toBeNull();
- });
-
- it("picks an unpractised key before a merely slow one", () => {
- const progress: Progress = {
- ...freshProgress(),
- keyStats: { a: { ema: 5000, attempts: 50, errors: 20 } },
- };
- expect(focusKeyFor(progress, ["a", "s"])).toBe("s");
- });
-
- it("picks the slowest and most error-prone once all are practised", () => {
- const progress: Progress = {
- ...freshProgress(),
- keyStats: {
- a: { ema: 300, attempts: 50, errors: 0 },
- s: { ema: 900, attempts: 50, errors: 10 },
- },
- };
- expect(focusKeyFor(progress, ["a", "s"])).toBe("s");
- });
-
- it("ignores the space bar and copes with an empty lesson", () => {
- expect(focusKeyFor(freshProgress(), [" "])).toBeNull();
- expect(focusKeyFor(freshProgress(), [])).toBeNull();
- });
-});
-
-describe("mastery", () => {
- it("is zero until a key has been seen enough times", () => {
- const progress: Progress = { ...freshProgress(), keyStats: { a: { ema: 200, attempts: 2, errors: 0 } } };
- expect(mastery(progress, "a")).toBe(0);
- expect(mastery(progress, "q")).toBe(0);
- });
-
- it("rises with speed and accuracy, and stays within 0..1", () => {
- const stat = (ema: number, errors: number) => ({
- ...freshProgress(),
- keyStats: { a: { ema, attempts: 100, errors } },
- });
- expect(mastery(stat(1500, 0), "a")).toBe(0);
- expect(mastery(stat(300, 0), "a")).toBe(1);
- expect(mastery(stat(900, 0), "a")).toBeCloseTo(0.5, 5);
- expect(mastery(stat(300, 50), "a")).toBeCloseTo(0.5, 5);
- });
-});
-
-describe("today", () => {
- it("formats local time, not UTC", () => {
- expect(today(new Date(2026, 8, 11, 23, 30))).toBe("2026-09-11");
- expect(today(new Date(2026, 0, 1, 0, 5))).toBe("2026-01-01");
- });
-});
diff --git a/tippen/src/lib/curriculum.ts b/tippen/src/lib/curriculum.ts
deleted file mode 100644
index 52c7735..0000000
--- a/tippen/src/lib/curriculum.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-/** The lesson plan: content lives in data/curriculum.yaml, this file only derives and
- * validates it.
- *
- * The shape follows what every serious ten-finger course does - start on the home row
- * `asdf jklö`, add keys ordered by German letter frequency (E, N, I, S, R, A, T, D, H,
- * U, L …), and drill everything learned so far each time. TIPP10's German course is the
- * same idea in 18 lessons.
- *
- * Where this deviates, it deviates for the age and for the Duolingo-style path this app
- * wants: every new key gets an **isolated** round (heavy weight, alone) before a
- * **mixed** round (lighter weight, blended with everything learned so far) - the same
- * new-key / mixed-key / real-content progression typing.com and typingstudy.com use,
- * just written as short rounds a six-year-old can climb one at a time. A drill lesson
- * follows every couple of keys, and periodically a **fragments** round turns the
- * newly-active keys into real short German words - the bridge between drilling letters
- * and typing whole words or sentences.
- *
- * `activeKeys` is cumulative on purpose: lesson 80 still drills `f`, or the first
- * lessons rot while the last ones are learned. */
-
-import { parse } from "yaml";
-
-import curriculumYaml from "../data/curriculum.yaml?raw";
-import type { CreatureId } from "./aquarium";
-import { HOME_ROW, SPACE_KEY } from "./fingers";
-
-/** What a lesson's targets are made of. `fragments` is short real syllables/mini-words -
- * the bridge between drilling isolated letters and typing whole words or sentences. */
-export type LessonKind = "letters" | "fragments" | "words" | "sentences";
-
-/** Which game a lesson can be played as. `pearls` is gone - every other mode measures
- * something, and that one deliberately didn't. */
-export type ModeId = "dive" | "bubbles" | "jellyfish" | "feed" | "race";
-
-export interface Lesson {
- id: string;
- world: number;
- number: number;
- title: string;
- /** What this lesson is about, in words a six-year-old hears read aloud. */
- subtitle: string;
- kind: LessonKind;
- /** The keys that became active *this* lesson - empty for a mixed round or a drill.
- * Used to size how gently a round starts (see World 1's mirrored pairs) and to check
- * the course never introduces more than it should. */
- newKeys: readonly string[];
- /** The keys this round is about, whether or not they are new - what the generator
- * over-represents. Equal to `newKeys` for an isolated round, the same keys again
- * (already active) for the mixed round that follows it, empty for a drill. */
- spotlightKeys: readonly string[];
- /** Isolated: newly-active keys, drilled alone. Mixed: the same keys, blended with
- * everything else. `null` for a drill or for anything that isn't a letters round. */
- emphasis: "isolated" | "mixed" | null;
- /** Everything typable in this lesson, cumulative. */
- activeKeys: readonly string[];
- /** The one mode that gates progress this round - passing a run in this mode is what
- * can unlock the next lesson. Mostly `dive` for non-letters kinds, but `feed`/`race`
- * take a periodic turn so the games stay mixed, not just `dive` end to end. */
- primaryMode: ModeId;
- /** Extra replays, offered on the result sheet once this lesson is passed. Never gate
- * anything - they exist so a favourite game can be played again. */
- bonusModes: readonly ModeId[];
- /** Real German words, fragments or sentences for the non-letters kinds. */
- words: readonly string[];
- /** True for a pure review round - no fresh content, just practice. Independent of
- * `newKeys`: a mixed round also has no new keys but is not a drill. */
- isDrill: boolean;
- /** How long one run is. Grows with the curriculum - see `lengthFor`. */
- chunks: number;
- chunkSize: number;
-}
-
-export interface World {
- number: number;
- title: string;
- emoji: string;
- /** The creature that moves into the aquarium when this world is finished. */
- reward: CreatureId;
-}
-
-/** A world's creature is a pet that stays - a drawing that swims behind every screen
- * from then on. An animal in grading.ts is a speed trophy that changes, and is an
- * emoji. lib/aquarium.ts has why the two are kept in different visual languages. */
-const CREATURE_IDS: readonly CreatureId[] = ["clownfish", "octopus", "seahorse", "turtle", "pearlmussel"];
-
-/** Which modes make sense for a kind. Letters rounds are single keys, so only the
- * arcade modes fit; fragments/words can be fed to a fish; only words and sentences are
- * long enough for a race. */
-export const ELIGIBLE_MODES: Record = {
- letters: ["bubbles", "jellyfish"],
- fragments: ["dive", "feed"],
- words: ["dive", "feed", "race"],
- sentences: ["dive", "race"],
-};
-
-/** Line length by world and kind - a full block of text per round. Letters rounds are
- * never rendered through `dive` any more (they are bubbles/jellyfish), but the length is
- * still computed for type uniformity and so the "still starts gentler than it ends"
- * shape is preserved if a letters round is ever asked for one. */
-function lengthFor(world: number, kind: LessonKind): { chunks: number; chunkSize: number } {
- if (kind === "fragments") return { chunks: 16, chunkSize: 4 }; // 64
- if (kind === "sentences") return { chunks: 10, chunkSize: 4 }; // ten whole sentences
- if (kind === "words") return { chunks: 25, chunkSize: 4 };
- // kind === "letters"
- if (world === 1) return { chunks: 24, chunkSize: 3 }; // 72 characters
- if (world === 2) return { chunks: 25, chunkSize: 4 }; // 100
- return { chunks: 30, chunkSize: 4 }; // 120, world 3 onward
-}
-
-interface YamlLesson {
- title: string;
- subtitle: string;
- kind?: LessonKind;
- keys?: readonly string[];
- drill?: boolean;
- mode?: ModeId;
- words?: readonly string[];
-}
-
-interface YamlWorld {
- number: number;
- title: string;
- emoji: string;
- reward: CreatureId;
- lessons: readonly YamlLesson[];
-}
-
-interface YamlRoot {
- worlds: readonly YamlWorld[];
-}
-
-/** Turns a YAML typo into a clear startup error instead of a silently wrong lesson. */
-function validatePlan(worlds: readonly YamlWorld[]): void {
- if (worlds.length !== CREATURE_IDS.length) {
- throw new Error(`curriculum.yaml: expected ${CREATURE_IDS.length} worlds, found ${worlds.length}`);
- }
- const rewards = new Set();
- for (const world of worlds) {
- if (rewards.has(world.reward)) throw new Error(`curriculum.yaml: world ${world.number} reuses reward "${world.reward}"`);
- if (!CREATURE_IDS.includes(world.reward)) throw new Error(`curriculum.yaml: world ${world.number} has an unknown reward "${world.reward}"`);
- rewards.add(world.reward);
-
- world.lessons.forEach((entry, i) => {
- const where = `world ${world.number}, lesson ${i + 1} (${entry.title})`;
- const words = entry.words ?? [];
- const kind: LessonKind = entry.kind ?? "letters";
- if (words.length > 0 && entry.kind === undefined) {
- throw new Error(`${where}: has words but no explicit kind`);
- }
- if (kind === "letters" && words.length > 0) throw new Error(`${where}: kind "letters" cannot have words`);
- if (kind !== "letters" && words.length === 0) throw new Error(`${where}: kind "${kind}" needs a non-empty words list`);
- if (entry.drill && entry.keys?.length) throw new Error(`${where}: a drill cannot also introduce keys`);
- if ((entry.keys?.length ?? 0) > 2) throw new Error(`${where}: at most two keys per lesson`);
- if (entry.mode && !ELIGIBLE_MODES[kind].includes(entry.mode)) {
- throw new Error(`${where}: mode "${entry.mode}" does not fit kind "${kind}"`);
- }
- });
- }
-}
-
-function loadPlan(): readonly YamlWorld[] {
- const root = parse(curriculumYaml) as YamlRoot;
- validatePlan(root.worlds);
- return root.worlds;
-}
-
-function buildLessons(worlds: readonly YamlWorld[]): Lesson[] {
- const lessons: Lesson[] = [];
- const active = new Set();
- const seenBefore = new Set();
- // Letters rounds alternate bubbles/jellyfish across the *whole* course, not per world
- // or per pair - a fragments/words lesson in between does not reset the count, so the
- // arcade game never repeats twice in a row even across a consolidation gap.
- let lastArcade: "bubbles" | "jellyfish" = "jellyfish"; // so lesson 1 opens on bubbles
-
- for (const world of worlds) {
- for (const entry of world.lessons) {
- const keys = entry.keys ?? [];
- const newKeys = keys.filter((key) => !seenBefore.has(key));
- for (const key of keys) {
- seenBefore.add(key);
- active.add(key);
- }
- // The space-bar lesson is where the whole home row comes together, so it
- // activates every home key - belt-and-braces confirmation that the four
- // finger-pair lessons before it really did cover all eight.
- if (keys.includes(SPACE_KEY)) for (const key of HOME_ROW) active.add(key);
-
- // Shift is not a character the generator can emit - the capitals in the word
- // list are what teaches it - so it never enters activeKeys.
- const activeKeys = [...active].filter((key) => key !== "⇧").sort();
- const words = entry.words ?? [];
- const kind: LessonKind = entry.kind ?? "letters";
- const isDrill = entry.drill === true;
-
- const emphasis: Lesson["emphasis"] =
- kind !== "letters" || isDrill ? null : newKeys.length > 0 ? "isolated" : "mixed";
-
- let primaryMode: ModeId;
- if (kind === "letters") {
- primaryMode = entry.mode ?? (lastArcade === "bubbles" ? "jellyfish" : "bubbles");
- lastArcade = primaryMode as "bubbles" | "jellyfish";
- } else {
- primaryMode = entry.mode ?? "dive";
- }
- // Bonus replays are only ever feed/race - dive is the plain default, not a treat
- // worth offering separately, and bubbles/jellyfish already alternate on their own.
- const bonusModes = ELIGIBLE_MODES[kind].filter(
- (mode) => (mode === "feed" || mode === "race") && mode !== primaryMode,
- );
-
- lessons.push({
- id: `l${String(lessons.length + 1).padStart(2, "0")}`,
- world: world.number,
- number: lessons.length + 1,
- title: entry.title,
- subtitle: entry.subtitle,
- kind,
- newKeys,
- spotlightKeys: kind === "letters" && !isDrill ? keys : [],
- emphasis,
- activeKeys,
- primaryMode,
- bonusModes,
- words,
- isDrill,
- ...lengthFor(world.number, kind),
- });
- }
- }
-
- return lessons;
-}
-
-const PLAN = loadPlan();
-
-export const WORLDS: readonly World[] = PLAN.map((world) => ({
- number: world.number,
- title: world.title,
- emoji: world.emoji,
- reward: world.reward,
-}));
-
-export const LESSONS: readonly Lesson[] = buildLessons(PLAN);
-
-const BY_ID = new Map(LESSONS.map((lesson) => [lesson.id, lesson]));
-
-export function lessonById(id: string): Lesson | null {
- return BY_ID.get(id) ?? null;
-}
-
-export function lessonsOfWorld(world: number): readonly Lesson[] {
- return LESSONS.filter((lesson) => lesson.world === world);
-}
-
-/** The lesson after this one, or null at the end of the curriculum. */
-export function nextLesson(id: string): Lesson | null {
- const index = LESSONS.findIndex((lesson) => lesson.id === id);
- if (index < 0) return null;
- return LESSONS[index + 1] ?? null;
-}
-
-export const FIRST_LESSON_ID = LESSONS[0]!.id;
diff --git a/tippen/src/lib/progress.ts b/tippen/src/lib/progress.ts
deleted file mode 100644
index 2fc127f..0000000
--- a/tippen/src/lib/progress.ts
+++ /dev/null
@@ -1,335 +0,0 @@
-/** Everything the game remembers, in one localStorage blob.
- *
- * The music player deliberately keeps no client state - what is playing lives on the
- * backend. This app has no backend at all, so localStorage is the whole store. One key,
- * one versioned object, and a `migrate` on read: when the shape changes later, old
- * saves upgrade instead of a six-year-old losing her aquarium.
- *
- * Every read and write is wrapped. A private window, a browser with site data blocked,
- * a quota that is full - none of those may stop the game from being playable. They just
- * make it forgetful. */
-
-import { creatureFromRaw } from "./aquarium";
-import type { CreatureId } from "./aquarium";
-import { FIRST_LESSON_ID, LESSONS, WORLDS, nextLesson } from "./curriculum";
-import type { RunResult, AnimalId } from "./grading";
-import { isBetter } from "./grading";
-
-const STORAGE_KEY = "delfin-tippen:v1";
-
-/** How many attempts at one lesson unlock the next regardless of score. The safety
- * valve against getting stuck on a single stubborn key - which at six is the difference
- * between a game she returns to and one she does not. */
-export const DILIGENCE_ATTEMPTS = 5;
-
-export interface LessonProgress {
- unlocked: boolean;
- runs: number;
- bestStars: 0 | 1 | 2 | 3;
- bestAnimal: AnimalId | null;
- bestPoints: number;
- /** Best-run keystrokes, replayed as the opponent in race mode. */
- ghost: { key: string; at: number }[] | null;
-}
-
-export interface KeyStat {
- /** Smoothed reaction time in ms - keybr's exponential moving average. Drives the
- * focus key and how fast the on-screen keyboard hint fades out. */
- ema: number;
- attempts: number;
- errors: number;
-}
-
-export interface Settings {
- sound: boolean;
- keyboardHint: "auto" | "on" | "off";
-}
-
-export interface Progress {
- version: 2;
- lessons: Record;
- keyStats: Record;
- pearls: number;
- /** Pets that have moved into the aquarium, in the order they arrived. */
- aquarium: CreatureId[];
- streak: { days: number; lastPlayed: string | null };
- settings: Settings;
-}
-
-function emptyLesson(unlocked: boolean): LessonProgress {
- return { unlocked, runs: 0, bestStars: 0, bestAnimal: null, bestPoints: 0, ghost: null };
-}
-
-export function freshProgress(): Progress {
- const lessons: Record = {};
- for (const lesson of LESSONS) {
- lessons[lesson.id] = emptyLesson(lesson.id === FIRST_LESSON_ID);
- }
- return {
- version: 2,
- lessons,
- keyStats: {},
- pearls: 0,
- aquarium: [],
- streak: { days: 0, lastPlayed: null },
- settings: { sound: true, keyboardHint: "auto" },
- };
-}
-
-/** Everything that survives a curriculum rebuild - the pets, the currency, the per-key
- * stats and the streak - pulled defensively off whatever shape was stored, old or new. */
-function carryForward(stored: Record, fresh: Progress): Omit