From 0b2e5e326ded06f65c4062c1c25dc12f0db8827d Mon Sep 17 00:00:00 2001 From: snoopy Date: Tue, 8 Sep 2026 22:19:18 +0200 Subject: [PATCH] OpenMind v1 : cartes mentales libres, local-first et dockerisees Premiere version fonctionnelle, demarrable avec `docker compose up -d --build` sur http://localhost:8080. Architecture - Frontend React 18 / TypeScript strict / Vite / Tailwind, canvas @xyflow/react, etat Zustand, persistance IndexedDB via Dexie, auto-layout dagre. - Backend FastAPI / SQLAlchemy 2 / Pydantic v2 / SQLite, qui sert aussi les fichiers statiques du frontend compile. - Image Docker multi-stage, un seul service, bind mount ./data pour SQLite. Local-first Les cartes restent dans le navigateur. Le serveur ne recoit une carte que sur demande explicite de partage. Partage Deux secrets distincts : share_token public en lecture seule dans l'URL, et management_token conserve uniquement cote client, transmis en en-tete pour les modifications et stocke hashe en base. Fonctionnalites Tableau de bord, editeur complet (undo/redo, copier/coller, raccourcis, branches repliables, recherche), 9 themes, 4 dispositions automatiques, panneau de proprietes, import/export .mmap, export PNG/SVG/PDF, impression, page publique en lecture seule, PWA. Qualite 25 tests pytest et 43 tests vitest au vert, ruff et ESLint sans erreur, TypeScript strict sans erreur. Co-Authored-By: Claude Opus 5 --- .dockerignore | 27 + .env.example | 25 + .gitattributes | 9 + .gitignore | 27 + Dockerfile | 63 + LICENSE | 21 + README.md | 530 ++ backend/app/__init__.py | 0 backend/app/api/__init__.py | 11 + backend/app/api/deps.py | 29 + backend/app/api/health.py | 25 + backend/app/api/shares.py | 98 + backend/app/config.py | 52 + backend/app/db/__init__.py | 0 backend/app/db/base.py | 9 + backend/app/db/init_db.py | 26 + backend/app/db/session.py | 62 + backend/app/logging_config.py | 33 + backend/app/main.py | 105 + backend/app/middleware.py | 75 + backend/app/models/__init__.py | 3 + backend/app/models/share.py | 41 + backend/app/schemas/__init__.py | 15 + backend/app/schemas/map.py | 62 + backend/app/schemas/share.py | 51 + backend/app/services/__init__.py | 0 backend/app/services/errors.py | 34 + backend/app/services/shares.py | 131 + backend/app/services/tokens.py | 45 + backend/app/web.py | 76 + backend/pyproject.toml | 22 + backend/requirements-dev.txt | 4 + backend/requirements.txt | 5 + backend/tests/__init__.py | 0 backend/tests/conftest.py | 84 + backend/tests/test_health.py | 28 + backend/tests/test_shares.py | 109 + backend/tests/test_spa.py | 22 + backend/tests/test_validation.py | 78 + docker-compose.yml | 30 + frontend/eslint.config.js | 34 + frontend/index.html | 17 + frontend/package-lock.json | 5041 +++++++++++++++++ frontend/package.json | 45 + frontend/postcss.config.js | 6 + frontend/public/icons/icon-192.png | Bin 0 -> 2038 bytes frontend/public/icons/icon-512.png | Bin 0 -> 5617 bytes frontend/public/icons/icon-maskable-512.png | Bin 0 -> 6044 bytes frontend/public/icons/icon.svg | 12 + frontend/public/manifest.webmanifest | 39 + frontend/public/sw.js | 71 + frontend/src/App.tsx | 44 + frontend/src/components/Button.tsx | 46 + frontend/src/components/ConfirmDialog.tsx | 44 + frontend/src/components/IconButton.tsx | 41 + frontend/src/components/Icons.tsx | 248 + frontend/src/components/Menu.tsx | 133 + frontend/src/components/Modal.tsx | 89 + frontend/src/components/Toasts.tsx | 47 + frontend/src/config/app.ts | 27 + frontend/src/db/database.ts | 37 + frontend/src/features/editor/EditorPage.tsx | 124 + .../src/features/editor/EditorToolbar.tsx | 249 + .../src/features/editor/MindMapCanvas.tsx | 219 + frontend/src/features/editor/MindNodeView.tsx | 200 + .../src/features/editor/PropertiesPanel.tsx | 345 ++ frontend/src/features/editor/SearchPanel.tsx | 99 + frontend/src/features/editor/flowAdapters.ts | 182 + frontend/src/features/editor/layout.test.ts | 49 + frontend/src/features/editor/layout.ts | 121 + frontend/src/features/editor/themes.ts | 235 + frontend/src/features/editor/useAutosave.ts | 66 + .../src/features/editor/useEditorShortcuts.ts | 124 + frontend/src/features/export/ExportMenu.tsx | 149 + .../src/features/export/PdfExportDialog.tsx | 188 + frontend/src/features/export/PrintDialog.tsx | 112 + frontend/src/features/export/exportImage.ts | 218 + frontend/src/features/export/exportPdf.ts | 145 + frontend/src/features/export/printMap.ts | 39 + frontend/src/features/maps/DashboardPage.tsx | 256 + frontend/src/features/maps/MapCard.tsx | 121 + frontend/src/features/sharing/ShareDialog.tsx | 239 + .../src/features/sharing/SharedMapPage.tsx | 138 + frontend/src/hooks/useDebouncedEffect.ts | 16 + frontend/src/hooks/useRoute.ts | 63 + frontend/src/main.tsx | 29 + frontend/src/services/mapFactory.ts | 131 + frontend/src/services/mapRepository.test.ts | 105 + frontend/src/services/mapRepository.ts | 98 + .../src/services/mapSerialization.test.ts | 127 + frontend/src/services/mapSerialization.ts | 293 + frontend/src/services/shareApi.ts | 164 + frontend/src/services/shareRepository.ts | 22 + frontend/src/stores/appConfigStore.ts | 41 + frontend/src/stores/editorStore.test.ts | 185 + frontend/src/stores/editorStore.ts | 652 +++ frontend/src/stores/uiStore.ts | 81 + frontend/src/styles/index.css | 97 + frontend/src/styles/print.css | 46 + frontend/src/test/setup.ts | 2 + frontend/src/types/map.ts | 135 + frontend/src/utils/cn.ts | 4 + frontend/src/utils/date.ts | 41 + frontend/src/utils/download.ts | 34 + frontend/src/utils/id.ts | 12 + frontend/src/utils/nodeSize.ts | 46 + frontend/src/utils/tree.ts | 157 + frontend/src/utils/url.test.ts | 28 + frontend/src/utils/url.ts | 26 + frontend/tailwind.config.js | 35 + frontend/tsconfig.json | 28 + frontend/vite.config.ts | 32 + scripts/generate_icons.py | 140 + 113 files changed, 14576 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/deps.py create mode 100644 backend/app/api/health.py create mode 100644 backend/app/api/shares.py create mode 100644 backend/app/config.py create mode 100644 backend/app/db/__init__.py create mode 100644 backend/app/db/base.py create mode 100644 backend/app/db/init_db.py create mode 100644 backend/app/db/session.py create mode 100644 backend/app/logging_config.py create mode 100644 backend/app/main.py create mode 100644 backend/app/middleware.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/share.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/map.py create mode 100644 backend/app/schemas/share.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/errors.py create mode 100644 backend/app/services/shares.py create mode 100644 backend/app/services/tokens.py create mode 100644 backend/app/web.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements-dev.txt create mode 100644 backend/requirements.txt create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_health.py create mode 100644 backend/tests/test_shares.py create mode 100644 backend/tests/test_spa.py create mode 100644 backend/tests/test_validation.py create mode 100644 docker-compose.yml create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/icons/icon-192.png create mode 100644 frontend/public/icons/icon-512.png create mode 100644 frontend/public/icons/icon-maskable-512.png create mode 100644 frontend/public/icons/icon.svg create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/public/sw.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/Button.tsx create mode 100644 frontend/src/components/ConfirmDialog.tsx create mode 100644 frontend/src/components/IconButton.tsx create mode 100644 frontend/src/components/Icons.tsx create mode 100644 frontend/src/components/Menu.tsx create mode 100644 frontend/src/components/Modal.tsx create mode 100644 frontend/src/components/Toasts.tsx create mode 100644 frontend/src/config/app.ts create mode 100644 frontend/src/db/database.ts create mode 100644 frontend/src/features/editor/EditorPage.tsx create mode 100644 frontend/src/features/editor/EditorToolbar.tsx create mode 100644 frontend/src/features/editor/MindMapCanvas.tsx create mode 100644 frontend/src/features/editor/MindNodeView.tsx create mode 100644 frontend/src/features/editor/PropertiesPanel.tsx create mode 100644 frontend/src/features/editor/SearchPanel.tsx create mode 100644 frontend/src/features/editor/flowAdapters.ts create mode 100644 frontend/src/features/editor/layout.test.ts create mode 100644 frontend/src/features/editor/layout.ts create mode 100644 frontend/src/features/editor/themes.ts create mode 100644 frontend/src/features/editor/useAutosave.ts create mode 100644 frontend/src/features/editor/useEditorShortcuts.ts create mode 100644 frontend/src/features/export/ExportMenu.tsx create mode 100644 frontend/src/features/export/PdfExportDialog.tsx create mode 100644 frontend/src/features/export/PrintDialog.tsx create mode 100644 frontend/src/features/export/exportImage.ts create mode 100644 frontend/src/features/export/exportPdf.ts create mode 100644 frontend/src/features/export/printMap.ts create mode 100644 frontend/src/features/maps/DashboardPage.tsx create mode 100644 frontend/src/features/maps/MapCard.tsx create mode 100644 frontend/src/features/sharing/ShareDialog.tsx create mode 100644 frontend/src/features/sharing/SharedMapPage.tsx create mode 100644 frontend/src/hooks/useDebouncedEffect.ts create mode 100644 frontend/src/hooks/useRoute.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/services/mapFactory.ts create mode 100644 frontend/src/services/mapRepository.test.ts create mode 100644 frontend/src/services/mapRepository.ts create mode 100644 frontend/src/services/mapSerialization.test.ts create mode 100644 frontend/src/services/mapSerialization.ts create mode 100644 frontend/src/services/shareApi.ts create mode 100644 frontend/src/services/shareRepository.ts create mode 100644 frontend/src/stores/appConfigStore.ts create mode 100644 frontend/src/stores/editorStore.test.ts create mode 100644 frontend/src/stores/editorStore.ts create mode 100644 frontend/src/stores/uiStore.ts create mode 100644 frontend/src/styles/index.css create mode 100644 frontend/src/styles/print.css create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/src/types/map.ts create mode 100644 frontend/src/utils/cn.ts create mode 100644 frontend/src/utils/date.ts create mode 100644 frontend/src/utils/download.ts create mode 100644 frontend/src/utils/id.ts create mode 100644 frontend/src/utils/nodeSize.ts create mode 100644 frontend/src/utils/tree.ts create mode 100644 frontend/src/utils/url.test.ts create mode 100644 frontend/src/utils/url.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 scripts/generate_icons.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b08da59 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +# Rien de tout cela ne doit entrer dans le contexte de build. +.git +.gitignore +.github +node_modules +frontend/node_modules +frontend/dist +data +*.db +*.db-wal +*.db-shm +.env +.env.* +!.env.example +__pycache__ +*.py[cod] +.venv +venv +.pytest_cache +.ruff_cache +coverage +.vscode +.idea +.DS_Store +Thumbs.db +README.md +docs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fdba4e7 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# --------------------------------------------------------------------------- +# Configuration OpenMind. Copiez ce fichier en .env puis adaptez si besoin. +# Aucun secret n'est necessaire : l'application ne gere aucun compte. +# --------------------------------------------------------------------------- + +# Nom du produit, affiche dans l'interface et l'onglet du navigateur. +APP_NAME=OpenMind + +# Port publie sur l'hote. L'application ecoute toujours sur 8080 dans le conteneur. +APP_PORT=8080 + +# Base SQLite des cartes PARTAGEES uniquement. +# /app/data est monte depuis ./data sur l'hote (bind mount). +DATABASE_URL=sqlite:////app/data/openmind.db + +# Limites de securite appliquees aux cartes envoyees au serveur. +MAX_MAP_SIZE_MB=10 +MAX_NODES=5000 + +# Origines autorisees en CORS, separees par des virgules. +# Vide = aucune origine externe (l'interface est servie par ce meme serveur). +CORS_ORIGINS= + +# Niveau de logs backend : DEBUG, INFO, WARNING, ERROR. +LOG_LEVEL=INFO diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2259751 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Fins de ligne normalisees en LF dans le depot : le build Docker se fait sous +# Linux, un CRLF casserait les scripts et le service worker. +* text=auto eol=lf + +# Fichiers binaires : aucune conversion. +*.png binary +*.jpg binary +*.ico binary +*.pdf binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e87edb --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependances +node_modules/ +frontend/dist/ +__pycache__/ +*.py[cod] +.venv/ +venv/ + +# Donnees locales (bind mount SQLite) +data/ +*.db +*.db-wal +*.db-shm + +# Environnement +.env +.env.* +!.env.example + +# Outils +.vscode/ +.idea/ +.DS_Store +Thumbs.db +coverage/ +.pytest_cache/ +.ruff_cache/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b51af1e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# Etape 1 : compilation du frontend React / Vite +# --------------------------------------------------------------------------- +FROM node:20-alpine AS frontend-build + +WORKDIR /build + +# Les dependances sont installees avant le code source pour profiter du cache. +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --no-audit --no-fund + +COPY frontend/ ./ +RUN npm run build + +# --------------------------------------------------------------------------- +# Etape 2 : dependances Python +# --------------------------------------------------------------------------- +FROM python:3.12-slim AS backend-deps + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /install +COPY backend/requirements.txt ./ +RUN pip install --prefix=/opt/python -r requirements.txt + +# --------------------------------------------------------------------------- +# Etape 3 : image finale (un seul service : FastAPI sert l'API et le frontend) +# --------------------------------------------------------------------------- +FROM python:3.12-slim AS runtime + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PATH="/opt/python/bin:$PATH" \ + PYTHONPATH="/opt/python/lib/python3.12/site-packages" \ + STATIC_DIR=/app/static \ + DATABASE_URL=sqlite:////app/data/openmind.db + +# curl sert uniquement au healthcheck du conteneur. +RUN apt-get update \ + && apt-get install --no-install-recommends -y curl \ + && rm -rf /var/lib/apt/lists/* + +# L'application ne tourne jamais en root. +RUN useradd --create-home --uid 10001 openmind + +WORKDIR /app + +COPY --from=backend-deps /opt/python /opt/python +COPY backend/app ./app +COPY --from=frontend-build /build/dist ./static + +RUN mkdir -p /app/data && chown -R openmind:openmind /app + +USER openmind +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8080/api/health || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--proxy-headers"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..32da1a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Florian (Harmonia IT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..674fe55 --- /dev/null +++ b/README.md @@ -0,0 +1,530 @@ +# OpenMind + +**Cartes mentales libres, local-first, sans compte et sans abonnement.** + +OpenMind est une alternative gratuite et open source aux services de mind mapping +qui placent l'export, les themes ou le nombre de cartes derriere un abonnement. +Toutes les fonctionnalites sont disponibles, sans exception et sans compte. + +- Aucune limitation artificielle, aucun paiement, aucun "premium". +- Aucun compte obligatoire : il n'y a d'ailleurs aucun systeme de comptes. +- Aucune telemetrie, aucun tracker, aucune publicite. +- Vos cartes vous appartiennent : elles restent dans votre navigateur. +- Export PDF, PNG, SVG et impression : gratuits. +- Un partage par lien, uniquement si vous le demandez explicitement. +- Un seul conteneur Docker a demarrer. + +--- + +## Philosophie local-first + +Une carte que vous creez n'est **jamais** envoyee au serveur. + +Elle est enregistree dans l'**IndexedDB de votre navigateur**, automatiquement, +apres une courte periode d'inactivite. Le backend n'existe que pour une seule +chose : generer un lien de partage lorsque vous cliquez explicitement sur +**Partager**. + +Consequences concretes : + +- Fermer le navigateur, redemarrer le PC ou redemarrer le conteneur ne fait + perdre aucune carte locale. +- Une fois l'application chargee une premiere fois, elle continue de fonctionner + sans reseau (PWA) : creation, edition, export, impression. +- Vider les donnees du site, ou changer de navigateur/machine, fait disparaitre + les cartes locales. **Exportez vos cartes en `.mmap`** pour en conserver une + sauvegarde portable. + +--- + +## Demarrage rapide + +Prerequis : Docker et Docker Compose. + +```bash +git clone +cd Mind-Mapping +cp .env.example .env +docker compose up -d --build +``` + +Puis ouvrez : + +``` +http://localhost:8080 +``` + +Verifier que le service repond : + +```bash +curl http://localhost:8080/api/health +``` + +Reponse attendue : `{"status":"ok"}` + +Etat du conteneur (le healthcheck Docker interroge `/api/health`) : + +```bash +docker compose ps +``` + +Arreter : + +```bash +docker compose down +``` + +--- + +## Architecture + +Un seul service applicatif. FastAPI sert a la fois l'API et les fichiers +statiques du frontend compile. + +``` +Navigateur + | + +-- React / TypeScript (Vite, Tailwind, React Flow, Zustand) + | + +-- IndexedDB (Dexie) + | +-- cartes privees/locales <-- ne quittent jamais le navigateur + | + +-- API, uniquement pour les fonctions serveur (partage) + | + v + FastAPI (+ fichiers statiques du frontend) + | + v + SQLite + | + +-- cartes explicitement partagees +``` + +Choix structurants : + +| Sujet | Choix | Pourquoi | +|---|---|---| +| Hierarchie | `parentId` fait autorite, les liens en sont derives | un seul invariant a maintenir, pas de desynchronisation | +| Modele | types metier propres, adaptes vers React Flow a l'affichage | le format de fichier ne depend pas de la bibliotheque de rendu | +| Historique | instantanes `{nodes, edges}`, groupes par action | un glisser-deposer = une seule annulation | +| Auto-layout | dagre, declenche uniquement a la demande | les deplacements manuels ne sont jamais ecrases | +| Versionnement | champ `version` + point d'extension de migration | le format peut evoluer sans casser les fichiers existants | + +--- + +## Variables d'environnement + +Toutes definies dans `.env` (modele : `.env.example`). Aucun secret n'est requis. + +| Variable | Defaut | Role | +|---|---|---| +| `APP_NAME` | `OpenMind` | Nom du produit, affiche dans l'interface et l'onglet | +| `APP_PORT` | `8080` | Port publie sur l'hote (le conteneur ecoute toujours 8080) | +| `DATABASE_URL` | `sqlite:////app/data/openmind.db` | Base des cartes partagees | +| `MAX_MAP_SIZE_MB` | `10` | Taille maximale d'une carte envoyee au serveur | +| `MAX_NODES` | `5000` | Nombre maximal de noeuds par carte | +| `CORS_ORIGINS` | vide | Origines externes autorisees, separees par des virgules | +| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | + +Le nom du produit est centralise : cote backend via `APP_NAME`, cote frontend +dans `frontend/src/config/app.ts`. Le frontend recupere le nom a l'execution via +`GET /api/config` : changer `APP_NAME` suffit, sans recompiler. + +--- + +## Donnees et sauvegarde + +Deux stockages **totalement distincts** : + +| Donnee | Ou | Sauvegarde | +|---|---|---| +| Cartes privees | IndexedDB du navigateur | Export `.mmap` depuis le tableau de bord ou l'editeur | +| Cartes partagees | SQLite, dans `./data/openmind.db` | Copie du dossier `./data` | + +Le dossier `./data` est monte en **bind mount** (aucun volume Docker nomme) : +les fichiers sont donc directement lisibles depuis l'hote. + +```bash +docker compose stop +tar czf openmind-partages-$(date +%F).tar.gz data/ +docker compose start +``` + +SQLite fonctionne en mode WAL : `openmind.db`, `openmind.db-wal` et +`openmind.db-shm` doivent etre sauvegardes ensemble. + +Redemarrer le serveur n'a **aucun impact** sur les cartes privees locales. + +--- + +## Fonctionnement du partage + +Une carte n'est envoyee au serveur que lorsque vous cliquez sur **Partager**, +apres une explication et une confirmation. + +Deux secrets distincts sont generes cote serveur avec le module Python +`secrets` : + +| Jeton | Entropie | Ou il vit | A quoi il sert | +|---|---|---|---| +| `share_token` | `secrets.token_urlsafe(12)`, environ 96 bits | dans l'URL publique `/s/` | **lecture seule** | +| `management_token` | `secrets.token_urlsafe(32)`, environ 256 bits | **uniquement** dans l'IndexedDB du proprietaire | modification et suppression | + +Regles appliquees : + +- Le `management_token` n'apparait **jamais** dans une URL, jamais dans un export + `.mmap`, jamais dans les logs. +- Il est transmis dans l'en-tete `X-Management-Token` pour `PUT` et `DELETE`. +- Seul son **hash SHA-256** est stocke en base ; la comparaison est faite en + temps constant avec `secrets.compare_digest`. +- Connaitre le `share_token` ne permet donc **ni** de modifier **ni** de + supprimer un partage. +- Le lien est **non repertorie** : il n'apparait dans aucune liste publique, + mais toute personne qui le connait peut consulter la carte. + +Cycle de vie : + +1. **Partager** : un instantane de la carte est copie sur le serveur, un lien est + renvoye, par exemple `http://localhost:8080/s/a8F3xP9k`. +2. **Mettre a jour le partage** : remplace l'instantane par la version actuelle. + Tant que vous ne le faites pas, le lien public montre l'ancienne version. +3. **Supprimer le partage** : efface l'instantane serveur. Le lien affiche alors + une page propre indiquant que le partage n'existe plus. La carte locale n'est + pas affectee. + +Si vous supprimez une carte locale, sa reference de partage locale part avec +elle : supprimez d'abord le partage si vous voulez aussi retirer la copie +serveur. + +### API + +| Methode | Route | Auth | Role | +|---|---|---|---| +| `GET` | `/api/health` | - | Etat du service | +| `GET` | `/api/config` | - | Nom de l'application et limites | +| `POST` | `/api/shares` | - | Creer un partage | +| `GET` | `/api/shares/{share_token}` | - | Lire une carte partagee | +| `PUT` | `/api/shares/{share_token}` | `X-Management-Token` | Mettre a jour | +| `DELETE` | `/api/shares/{share_token}` | `X-Management-Token` | Supprimer | + +Documentation interactive : `http://localhost:8080/api/docs`. + +--- + +## Fonctionnalites + +**Tableau de bord** : creation, import, recherche, date de derniere +modification, renommage, duplication, export, suppression avec confirmation. +Une carte d'exemple est creee au tout premier lancement. + +**Editeur** : canvas infini, zoom, deplacement, zoom-to-fit, minimap, grille +optionnelle, creation/suppression/deplacement de noeuds, selection multiple, +connexion entre noeuds avec detection de boucle, edition du texte au +double-clic, undo/redo, copier/coller, duplication, sauvegarde automatique. + +**Mind mapping** : noeud racine et noeuds standard, branches droites, courbes ou +Bezier, branches repliables avec compteur d'elements masques. + +**Auto-layout**, uniquement sur demande : mind map droite, mind map gauche, +arbre horizontal, arbre vertical. + +**Proprietes du noeud** : texte, couleurs de fond/texte/bordure, forme +(rectangle, arrondi, capsule, cercle), taille du texte, gras, italique, +alignement, icone, URL ouverte dans un nouvel onglet, note. + +**Themes** : Default, Minimal, Dark, Midnight, Ocean, Forest, Pastel, +Monochrome, Blueprint. Plus un mode clair/sombre pour l'interface. Changer de +theme ne supprime jamais une personnalisation explicite d'un noeud. + +**Recherche dans la carte** : resultats listes, navigation entre eux, centrage +et mise en evidence temporaire du noeud, depliage automatique des branches +repliees. + +**Exports** : `.mmap` (format natif), PNG (x1, x2, fond transparent), SVG, PDF +(A4/A3, portrait/paysage, marges, adapter a une page ou taille reelle, fond, +titre, date), impression via `@media print`. + +**PWA** : installable, et utilisable hors ligne pour les cartes locales une fois +l'application chargee. + +--- + +## Raccourcis clavier + +`Cmd` remplace `Ctrl` sur macOS. + +| Raccourci | Action | +|---|---| +| `Tab` | Ajouter un noeud enfant | +| `Entree` | Ajouter un noeud frere | +| `Suppr` / `Retour arriere` | Supprimer la selection, hors saisie de texte | +| `F2` | Editer le texte du noeud selectionne | +| Double-clic | Editer le texte du noeud | +| `Ctrl+Z` | Annuler | +| `Ctrl+Y` ou `Ctrl+Shift+Z` | Retablir | +| `Ctrl+C` | Copier | +| `Ctrl+V` | Coller | +| `Ctrl+D` | Dupliquer | +| `Ctrl+A` | Tout selectionner | +| `Ctrl+S` | Sauvegarde explicite | +| `Ctrl+F` | Rechercher dans la carte | +| `F` | Adapter la carte a l'ecran | +| `Echap` | Quitter l'edition ou deselectionner | + +--- + +## Format de fichier `.mmap` + +Un `.mmap` est un fichier **JSON UTF-8** contenant le document complet de la +carte. Il ne contient jamais de jeton de partage. + +```json +{ + "version": 1, + "id": "uuid", + "name": "Architecture Homelab", + "createdAt": "2026-01-01T10:00:00.000Z", + "updatedAt": "2026-01-01T12:30:00.000Z", + "nodes": [ + { + "id": "uuid", + "type": "root", + "text": "Infrastructure", + "position": { "x": 0, "y": 0 }, + "parentId": null, + "collapsed": false, + "icon": "\ud83d\udda5\ufe0f", + "url": "https://exemple.fr", + "note": "Texte libre", + "style": { "backgroundColor": "#eef2ff", "shape": "rounded" } + } + ], + "edges": [{ "id": "e-a-b", "source": "a", "target": "b" }], + "viewport": { "x": 0, "y": 0, "zoom": 1 }, + "settings": { + "theme": "default", + "layout": "mindmap-right", + "branchStyle": "bezier", + "showGrid": true + } +} +``` + +A l'import, le fichier est **valide et normalise** : structure, version, champs +essentiels, limite de noeuds, assainissement des URL (`javascript:` et `data:` +sont supprimes), rejet des styles invalides. Le contenu n'est jamais execute ni +interprete comme du HTML ; un fichier invalide produit un message clair. + +Le champ `version` permet les migrations futures : le point d'extension se +trouve dans `frontend/src/services/mapSerialization.ts`, fonction `migrate`. + +--- + +## Developpement + +### Backend + +```bash +cd backend +python -m venv .venv +source .venv/bin/activate +pip install -r requirements-dev.txt +DATABASE_URL=sqlite:///./openmind-dev.db STATIC_DIR=./static uvicorn app.main:app --reload --port 8080 +``` + +Sous Windows, activez l'environnement avec `.venv\Scripts\activate`. + +### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Le serveur Vite ecoute sur `http://localhost:5173` et proxyfie `/api` vers +`http://localhost:8080`. + +Autres commandes : + +```bash +npm run build +npm run lint +npm run typecheck +npm run test +``` + +Regenerer les icones de la PWA : + +```bash +python scripts/generate_icons.py +``` + +--- + +## Tests et qualite + +Backend : + +```bash +cd backend +python -m pytest -q +ruff check . +ruff format --check . +``` + +Frontend : + +```bash +cd frontend +npm run test +npm run lint +npm run typecheck +``` + +Sans installation locale de Node ou Python, tout peut tourner via Docker : + +```bash +docker run --rm -v "$PWD/backend:/work" -w /work python:3.12-slim sh -c "pip install -q -r requirements-dev.txt && ruff check . && python -m pytest -q" +``` + +```bash +docker run --rm -v "$PWD/frontend:/app" -w /app node:20-alpine sh -c "npm ci && npm run lint && npm run test && npm run build" +``` + +Les tests backend couvrent : healthcheck, creation, lecture, mise a jour et +suppression d'un partage, refus avec un mauvais jeton de gestion, refus sans +jeton, partage inexistant, payload invalide, limites de taille et de noeuds, +non-divulgation du jeton de gestion, repli SPA. + +Les tests frontend couvrent : stockage IndexedDB, serialisation et validation +d'import, migrations, store Zustand (creation, suppression de branche, +undo/redo, groupement du glisser-deposer, copier/coller, duplication, detection +de boucle, recherche, auto-layout, lecture seule), assainissement des URL. + +--- + +## Structure du projet + +``` +. +├── Dockerfile # build multi-stage : node -> python -> image finale +├── docker-compose.yml # un seul service, bind mount ./data +├── .env.example +├── README.md +├── scripts/ +│ └── generate_icons.py # generation des icones PWA +├── data/ # bind mount : SQLite des partages +├── backend/ +│ ├── requirements.txt +│ ├── pyproject.toml # configuration ruff et pytest +│ ├── app/ +│ │ ├── main.py # assemblage uniquement +│ │ ├── config.py +│ │ ├── logging_config.py +│ │ ├── middleware.py # en-tetes de securite, limite de taille +│ │ ├── web.py # service des fichiers statiques du frontend +│ │ ├── api/ # routes health, config, shares +│ │ ├── db/ # moteur, session, initialisation du schema +│ │ ├── models/ # SharedMap (SQLAlchemy) +│ │ ├── schemas/ # validation Pydantic +│ │ └── services/ # jetons, logique de partage, erreurs metier +│ └── tests/ +└── frontend/ + ├── index.html + ├── public/ # manifest PWA, service worker, icones + └── src/ + ├── config/app.ts # nom du produit et constantes + ├── types/map.ts # modele metier versionne + ├── db/ # schema Dexie + ├── services/ # depots, serialisation, client API + ├── stores/ # Zustand : editeur, interface, configuration + ├── hooks/ # routeur minimal, debounce + ├── components/ # briques d'interface reutilisables + ├── features/ + │ ├── editor/ # canvas, noeud, barre d'outils, panneaux, layout, themes + │ ├── maps/ # tableau de bord + │ ├── export/ # PNG, SVG, PDF, impression + │ └── sharing/ # dialogue de partage, page publique + ├── utils/ + └── styles/ +``` + +--- + +## Securite + +- Le texte d'un noeud est **toujours** traite comme du texte. Aucun + `dangerouslySetInnerHTML`, aucun HTML issu d'une carte n'est interprete. +- Les URL sont assainies : seuls `http`, `https` et `mailto` sont acceptes. Les + liens s'ouvrent avec `rel="noopener noreferrer nofollow"`. +- Validation Pydantic de tout ce qui entre dans l'API, avec limites de taille et + de nombre de noeuds ; un depassement produit un message clair, jamais un + plantage. +- En-tetes de securite : CSP stricte, `X-Content-Type-Options`, + `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, + `Cross-Origin-Opener-Policy`. +- CORS restrictif : aucune origine externe par defaut. +- Acces SQL exclusivement via l'ORM SQLAlchemy. +- Jetons cryptographiquement aleatoires, jeton de gestion stocke hashe et jamais + journalise. +- Le conteneur tourne avec un utilisateur non-root. +- Aucun secret dans le frontend. + +--- + +## Limitations connues + +- **Export SVG** : les noeuds sont encapsules dans un `foreignObject` (HTML + embarque). Le fichier reste vectoriel et se redimensionne sans perte, les + branches sont de vrais chemins SVG, mais certains editeurs vectoriels + n'exploitent pas le contenu HTML des noeuds. Preferez PNG ou PDF pour un usage + bureautique. +- **Exports image et PDF en arriere-plan** : la generation s'appuie sur une + frame d'animation du navigateur, qui n'est pas produite lorsque l'onglet est + masque. Gardez l'onglet au premier plan pendant un export ; sinon l'operation + est interrompue au bout de 45 secondes avec un message explicite. +- **PDF en taille reelle** : si la carte depasse la page choisie, elle est + reduite pour tenir entierement plutot que d'etre coupee ; un message le + signale. +- **Gestion d'un partage** : le jeton de gestion vit dans l'IndexedDB du + navigateur d'origine. Depuis un autre appareil, un partage ne peut etre ni mis + a jour ni supprime. +- **Dispositions reservees** : `mindmap-both`, `organization` et `freeform` + existent dans le modele de donnees mais ne sont pas proposees dans + l'interface. +- **Types de noeuds reserves** : `note`, `image`, `group`, `link` et `task` sont + acceptes par le format, mais la V1 ne produit que `root` et `standard`. +- **Migrations de schema SQLite** : la V1 initialise le schema avec + `create_all`, une seule table. Alembic n'est pas utilise ; il faudra + l'introduire au premier changement de schema. +- **Mobile** : consultation et edition basique fonctionnent, mais l'interface + est concue pour desktop et tablette. +- **Aucun compte utilisateur** : c'est voulu. Pas de connexion, pas de + collaboration temps reel, pas de synchronisation cloud. + +--- + +## Licences des dependances + +Toutes les dependances sont sous licence libre permissive, compatible avec une +application publique et gratuite. Aucune bibliotheque a licence commerciale +n'est utilisee. + +| Dependance | Licence | +|---|---| +| React, React DOM | MIT | +| `@xyflow/react` (React Flow) | MIT | +| Zustand | MIT | +| Dexie | Apache-2.0 | +| dagre | MIT | +| html-to-image | MIT | +| jsPDF | MIT | +| Vite, Tailwind CSS, TypeScript, ESLint, Vitest | MIT | +| FastAPI, Starlette, Pydantic, SQLAlchemy, Uvicorn | MIT / BSD | + +--- + +## Licence + +OpenMind est distribue sous licence **MIT** : voir le fichier [LICENSE](LICENSE). diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..9ed34be --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,11 @@ +"""Agregation des routeurs sous le prefixe /api.""" + +from fastapi import APIRouter + +from app.api import health, shares + +api_router = APIRouter(prefix="/api") +api_router.include_router(health.router) +api_router.include_router(shares.router) + +__all__ = ["api_router"] diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 0000000..3d2f4b2 --- /dev/null +++ b/backend/app/api/deps.py @@ -0,0 +1,29 @@ +"""Dependances communes aux routes.""" + +from __future__ import annotations + +from typing import Annotated + +from fastapi import Depends, Header +from sqlalchemy.orm import Session + +from app.config import Settings, get_settings +from app.db.session import get_db +from app.services.errors import ShareForbiddenError + +MANAGEMENT_TOKEN_HEADER = "X-Management-Token" + +DbSession = Annotated[Session, Depends(get_db)] +AppSettings = Annotated[Settings, Depends(get_settings)] + + +def require_management_token( + x_management_token: Annotated[str | None, Header(alias=MANAGEMENT_TOKEN_HEADER)] = None, +) -> str: + """Recupere le secret de gestion depuis l'en-tete (jamais depuis l'URL).""" + if not x_management_token: + raise ShareForbiddenError("Jeton de gestion manquant.") + return x_management_token + + +ManagementToken = Annotated[str, Depends(require_management_token)] diff --git a/backend/app/api/health.py b/backend/app/api/health.py new file mode 100644 index 0000000..84ecfe7 --- /dev/null +++ b/backend/app/api/health.py @@ -0,0 +1,25 @@ +"""Endpoints de sante et de configuration publique.""" + +from __future__ import annotations + +from fastapi import APIRouter + +from app.api.deps import AppSettings + +router = APIRouter(tags=["system"]) + + +@router.get("/health", summary="Etat du service") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@router.get("/config", summary="Configuration publique exposee au frontend") +def public_config(settings: AppSettings) -> dict[str, object]: + """Aucun secret ici : uniquement ce dont l'interface a besoin.""" + return { + "appName": settings.app_name, + "maxNodes": settings.max_nodes, + "maxMapSizeMb": settings.max_map_size_mb, + "sharingEnabled": True, + } diff --git a/backend/app/api/shares.py b/backend/app/api/shares.py new file mode 100644 index 0000000..5698bba --- /dev/null +++ b/backend/app/api/shares.py @@ -0,0 +1,98 @@ +"""API REST de partage de cartes.""" + +from __future__ import annotations + +from fastapi import APIRouter, Response, status + +from app.api.deps import DbSession, ManagementToken +from app.schemas.share import ( + ShareCreatedOut, + ShareCreateIn, + SharePublicOut, + ShareUpdatedOut, +) +from app.services import shares as share_service + +router = APIRouter(prefix="/shares", tags=["shares"]) + + +def _share_path(share_token: str) -> str: + return f"/s/{share_token}" + + +@router.post( + "", + response_model=ShareCreatedOut, + status_code=status.HTTP_201_CREATED, + summary="Publier un snapshot de carte et obtenir un lien non repertorie", +) +def create_share(payload: ShareCreateIn, session: DbSession) -> ShareCreatedOut: + share, management_token = share_service.create_share( + session, title=payload.title, document=payload.map + ) + return ShareCreatedOut( + share_token=share.share_token, + management_token=management_token, + share_path=_share_path(share.share_token), + title=share.title, + created_at=share.created_at, + updated_at=share.updated_at, + ) + + +@router.get( + "/{share_token}", + response_model=SharePublicOut, + summary="Lire une carte partagee (public, lecture seule)", +) +def read_share(share_token: str, session: DbSession) -> SharePublicOut: + share = share_service.get_share(session, share_token) + return SharePublicOut( + share_token=share.share_token, + title=share.title, + map_version=share.map_version, + map=share_service.load_map_data(share), + created_at=share.created_at, + updated_at=share.updated_at, + ) + + +@router.put( + "/{share_token}", + response_model=ShareUpdatedOut, + summary="Mettre a jour le snapshot partage (necessite le jeton de gestion)", +) +def update_share( + share_token: str, + payload: ShareCreateIn, + session: DbSession, + management_token: ManagementToken, +) -> ShareUpdatedOut: + share = share_service.update_share( + session, + share_token, + management_token=management_token, + title=payload.title, + document=payload.map, + ) + return ShareUpdatedOut( + share_token=share.share_token, + share_path=_share_path(share.share_token), + title=share.title, + updated_at=share.updated_at, + ) + + +@router.delete( + "/{share_token}", + status_code=status.HTTP_204_NO_CONTENT, + response_class=Response, + summary="Supprimer le partage (necessite le jeton de gestion)", +) +def delete_share( + share_token: str, + session: DbSession, + management_token: ManagementToken, +) -> Response: + share_service.delete_share(session, share_token, management_token=management_token) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..85ea49d --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,52 @@ +"""Configuration applicative, entierement pilotee par variables d'environnement.""" + +from __future__ import annotations + +from functools import lru_cache + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Parametres du backend. + + Chaque champ est surchargeable via une variable d'environnement de meme nom + (insensible a la casse) : APP_NAME, DATABASE_URL, MAX_NODES, ... + """ + + model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False) + + app_name: str = "OpenMind" + app_port: int = 8080 + app_env: str = "production" + + database_url: str = "sqlite:////app/data/openmind.db" + static_dir: str = "/app/static" + + max_map_size_mb: float = Field(default=10.0, gt=0) + max_nodes: int = Field(default=5000, gt=0) + max_title_length: int = Field(default=200, gt=0) + + # Liste d'origines autorisees, separees par des virgules. + # Vide = aucune origine externe (l'app est servie par ce meme serveur). + cors_origins: str = "" + + log_level: str = "INFO" + + @property + def max_map_size_bytes(self) -> int: + return int(self.max_map_size_mb * 1024 * 1024) + + @property + def cors_origin_list(self) -> list[str]: + return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()] + + @property + def is_debug(self) -> bool: + return self.app_env.lower() in {"dev", "development", "debug"} + + +@lru_cache +def get_settings() -> Settings: + return Settings() diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/db/base.py b/backend/app/db/base.py new file mode 100644 index 0000000..98fe058 --- /dev/null +++ b/backend/app/db/base.py @@ -0,0 +1,9 @@ +"""Base declarative SQLAlchemy commune a tous les modeles.""" + +from __future__ import annotations + +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + """Classe de base des modeles ORM.""" diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py new file mode 100644 index 0000000..9368722 --- /dev/null +++ b/backend/app/db/init_db.py @@ -0,0 +1,26 @@ +"""Initialisation du schema SQLite. + +La V1 n'utilise pas Alembic : le schema tient dans une seule table et +`create_all` suffit. Toute evolution future du schema devra soit passer par +Alembic, soit par une migration explicite ajoutee ici (voir README). +""" + +from __future__ import annotations + +import logging + +from sqlalchemy import Engine + +from app.db.base import Base +from app.db.session import engine as default_engine +from app.models import share as _share_model # noqa: F401 (enregistre la table) + +logger = logging.getLogger(__name__) + +SCHEMA_VERSION = 1 + + +def init_db(engine: Engine | None = None) -> None: + target = engine or default_engine + Base.metadata.create_all(bind=target) + logger.info("Schema SQLite initialise (version %s)", SCHEMA_VERSION) diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..0323b13 --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,62 @@ +"""Creation du moteur SQLAlchemy et de la session de requete.""" + +from __future__ import annotations + +import logging +from collections.abc import Generator +from pathlib import Path + +from sqlalchemy import Engine, create_engine, event +from sqlalchemy.orm import Session, sessionmaker + +from app.config import get_settings + +logger = logging.getLogger(__name__) + +_SQLITE_FILE_PREFIX = "sqlite:///" + + +def _ensure_sqlite_directory(database_url: str) -> None: + """Cree le dossier parent du fichier SQLite s'il n'existe pas encore.""" + if not database_url.startswith(_SQLITE_FILE_PREFIX): + return + raw_path = database_url[len(_SQLITE_FILE_PREFIX) :] + if not raw_path or raw_path == ":memory:": + return + path = Path(raw_path) + if path.parent and str(path.parent) not in ("", "."): + path.parent.mkdir(parents=True, exist_ok=True) + + +def create_db_engine(database_url: str | None = None) -> Engine: + settings = get_settings() + url = database_url or settings.database_url + _ensure_sqlite_directory(url) + + connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {} + engine = create_engine(url, connect_args=connect_args, future=True, echo=False) + + if url.startswith("sqlite"): + + @event.listens_for(engine, "connect") + def _set_sqlite_pragma(dbapi_connection, _connection_record): # type: ignore[no-untyped-def] + cursor = dbapi_connection.cursor() + # WAL : meilleure resistance aux arrets brutaux du conteneur. + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + return engine + + +engine: Engine = create_db_engine() +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True) + + +def get_db() -> Generator[Session, None, None]: + """Dependance FastAPI : ouvre une session et la referme systematiquement.""" + session = SessionLocal() + try: + yield session + finally: + session.close() diff --git a/backend/app/logging_config.py b/backend/app/logging_config.py new file mode 100644 index 0000000..93a42e9 --- /dev/null +++ b/backend/app/logging_config.py @@ -0,0 +1,33 @@ +"""Configuration des logs backend : lisible, structure, sans donnee sensible.""" + +from __future__ import annotations + +import logging +import sys + +_LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" +_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" + +# Champs qui ne doivent jamais apparaitre dans les logs. +REDACTED_KEYS = frozenset({"management_token", "managementToken", "authorization"}) + + +def configure_logging(level: str = "INFO") -> None: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT)) + + root = logging.getLogger() + root.handlers.clear() + root.addHandler(handler) + root.setLevel(level.upper()) + + # Uvicorn possede ses propres handlers : on les aligne sur les notres. + for name in ("uvicorn", "uvicorn.error", "uvicorn.access"): + logger = logging.getLogger(name) + logger.handlers.clear() + logger.propagate = True + + +def safe_extra(data: dict[str, object]) -> dict[str, object]: + """Retire les valeurs sensibles avant de les envoyer dans un log.""" + return {k: ("***" if k in REDACTED_KEYS else v) for k, v in data.items()} diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..cfb7777 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,105 @@ +"""Point d'entree de l'application FastAPI. + +Ce fichier se limite a l'assemblage : configuration, middlewares, gestion des +erreurs, montage des routeurs et du frontend statique. +""" + +from __future__ import annotations + +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse + +from app.api import api_router +from app.config import get_settings +from app.db.init_db import init_db +from app.logging_config import configure_logging +from app.middleware import MaxBodySizeMiddleware, SecurityHeadersMiddleware +from app.services.errors import ShareError +from app.web import mount_frontend + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): # type: ignore[no-untyped-def] + settings = get_settings() + configure_logging(settings.log_level) + init_db() + logger.info( + "%s demarre (max_nodes=%s, max_map_size_mb=%s)", + settings.app_name, + settings.max_nodes, + settings.max_map_size_mb, + ) + yield + logger.info("Arret de %s", settings.app_name) + + +def create_app() -> FastAPI: + settings = get_settings() + configure_logging(settings.log_level) + + app = FastAPI( + title=f"{settings.app_name} API", + version="1.0.0", + description="API de partage de cartes. Les cartes privees restent dans le navigateur.", + lifespan=lifespan, + docs_url="/api/docs", + openapi_url="/api/openapi.json", + redoc_url=None, + ) + + app.add_middleware(SecurityHeadersMiddleware) + app.add_middleware(MaxBodySizeMiddleware, max_body_size=settings.max_map_size_bytes) + + # CORS restrictif : par defaut aucune origine externe, l'app etant servie + # par ce meme serveur (meme origine). + origins = settings.cors_origin_list + if origins: + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=False, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["Content-Type", "X-Management-Token"], + ) + + register_exception_handlers(app) + app.include_router(api_router) + mount_frontend(app, settings.static_dir) + return app + + +def register_exception_handlers(app: FastAPI) -> None: + """Aucune erreur brute ne doit remonter au client.""" + + @app.exception_handler(ShareError) + async def _share_error(_request: Request, exc: ShareError) -> JSONResponse: + return JSONResponse(status_code=exc.status_code, content={"detail": exc.message}) + + @app.exception_handler(RequestValidationError) + async def _validation_error(_request: Request, exc: RequestValidationError) -> JSONResponse: + # On expose un message lisible, pas la structure interne de validation. + first = exc.errors()[0] if exc.errors() else None + detail = "Les donnees envoyees ne forment pas une carte valide." + if first and first.get("msg"): + reason = str(first["msg"]).replace("Value error, ", "") + detail = f"{detail} ({reason})" + return JSONResponse(status_code=422, content={"detail": detail}) + + @app.exception_handler(Exception) + async def _unhandled(request: Request, exc: Exception) -> JSONResponse: + # La traceback complete reste cote serveur. + logger.exception("Erreur non geree sur %s %s", request.method, request.url.path) + return JSONResponse( + status_code=500, + content={"detail": "Une erreur interne est survenue. Reessayez plus tard."}, + ) + + +app = create_app() diff --git a/backend/app/middleware.py b/backend/app/middleware.py new file mode 100644 index 0000000..f45d82d --- /dev/null +++ b/backend/app/middleware.py @@ -0,0 +1,75 @@ +"""Middlewares HTTP : en-tetes de securite et limite de taille des requetes.""" + +from __future__ import annotations + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +CONTENT_SECURITY_POLICY = "; ".join( + [ + "default-src 'self'", + # Les styles inline sont utilises par React Flow et par l'export image. + "style-src 'self' 'unsafe-inline'", + "script-src 'self'", + "img-src 'self' data: blob:", + "font-src 'self' data:", + "connect-src 'self'", + # Service worker et manifeste de la PWA. + "worker-src 'self'", + "manifest-src 'self'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + ] +) + +SECURITY_HEADERS = { + "Content-Security-Policy": CONTENT_SECURITY_POLICY, + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Referrer-Policy": "no-referrer", + "Permissions-Policy": "geolocation=(), microphone=(), camera=(), interest-cohort=()", + "Cross-Origin-Opener-Policy": "same-origin", +} + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): # type: ignore[no-untyped-def] + response: Response = await call_next(request) + for header, value in SECURITY_HEADERS.items(): + response.headers.setdefault(header, value) + return response + + +class MaxBodySizeMiddleware(BaseHTTPMiddleware): + """Refuse proprement les corps de requete trop volumineux. + + On s'appuie sur Content-Length : le client est rejete avant toute lecture + complete du corps, et recoit un message clair plutot qu'une erreur brute. + """ + + def __init__(self, app, max_body_size: int) -> None: # type: ignore[no-untyped-def] + super().__init__(app) + self.max_body_size = max_body_size + + async def dispatch(self, request: Request, call_next): # type: ignore[no-untyped-def] + content_length = request.headers.get("content-length") + if content_length is not None: + try: + size = int(content_length) + except ValueError: + size = 0 + if size > self.max_body_size: + limit_mb = self.max_body_size / (1024 * 1024) + return JSONResponse( + status_code=413, + content={ + "detail": ( + "La carte est trop volumineuse pour etre partagee " + f"(limite : {limit_mb:.0f} Mo)." + ) + }, + ) + return await call_next(request) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..0fdccec --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,3 @@ +from app.models.share import SharedMap + +__all__ = ["SharedMap"] diff --git a/backend/app/models/share.py b/backend/app/models/share.py new file mode 100644 index 0000000..8b71e41 --- /dev/null +++ b/backend/app/models/share.py @@ -0,0 +1,41 @@ +"""Modele ORM d'une carte explicitement partagee par son proprietaire.""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from sqlalchemy import DateTime, Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +def utcnow() -> datetime: + return datetime.now(UTC) + + +class SharedMap(Base): + """Snapshot serveur d'une carte, cree uniquement sur action de l'utilisateur.""" + + __tablename__ = "shared_maps" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + # Identifiant public present dans l'URL /s/. + share_token: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) + + # Seul le hash du management token est stocke : le secret n'existe que + # dans l'IndexedDB du proprietaire. + management_token_hash: Mapped[str] = mapped_column(String(64), nullable=False) + + title: Mapped[str] = mapped_column(String(255), nullable=False, default="") + map_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + map_data: Mapped[str] = mapped_column(Text, nullable=False) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=utcnow, onupdate=utcnow + ) + + def __repr__(self) -> str: # pragma: no cover - aide au debug uniquement + return f"" diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..9f9f3c5 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1,15 @@ +from app.schemas.map import MapDocumentIn +from app.schemas.share import ( + ShareCreatedOut, + ShareCreateIn, + SharePublicOut, + ShareUpdatedOut, +) + +__all__ = [ + "MapDocumentIn", + "ShareCreateIn", + "ShareCreatedOut", + "SharePublicOut", + "ShareUpdatedOut", +] diff --git a/backend/app/schemas/map.py b/backend/app/schemas/map.py new file mode 100644 index 0000000..8b78309 --- /dev/null +++ b/backend/app/schemas/map.py @@ -0,0 +1,62 @@ +"""Validation Pydantic du document carte envoye au serveur. + +Le backend ne cherche pas a comprendre finement une carte : il valide sa +structure, ses limites de taille, puis la stocke telle quelle. `extra="allow"` +garantit qu'une version future du format frontend reste acceptee. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from app.config import get_settings + +MAX_SUPPORTED_MAP_VERSION = 100 + + +class MapNodeIn(BaseModel): + model_config = ConfigDict(extra="allow") + + id: str = Field(min_length=1, max_length=128) + text: str = Field(default="", max_length=5000) + + +class MapEdgeIn(BaseModel): + model_config = ConfigDict(extra="allow") + + id: str = Field(min_length=1, max_length=256) + source: str = Field(min_length=1, max_length=128) + target: str = Field(min_length=1, max_length=128) + + +class MapDocumentIn(BaseModel): + """Document carte complet, tel que produit par le frontend.""" + + model_config = ConfigDict(extra="allow") + + version: int = Field(ge=1, le=MAX_SUPPORTED_MAP_VERSION) + id: str = Field(min_length=1, max_length=128) + name: str = Field(default="", max_length=255) + nodes: list[MapNodeIn] = Field(default_factory=list) + edges: list[MapEdgeIn] = Field(default_factory=list) + viewport: dict[str, Any] = Field(default_factory=dict) + settings: dict[str, Any] = Field(default_factory=dict) + + @field_validator("nodes") + @classmethod + def _check_node_count(cls, value: list[MapNodeIn]) -> list[MapNodeIn]: + max_nodes = get_settings().max_nodes + if len(value) > max_nodes: + raise ValueError( + f"La carte depasse la limite de {max_nodes} noeuds ({len(value)} noeuds fournis)." + ) + return value + + @model_validator(mode="after") + def _check_edge_count(self) -> MapDocumentIn: + max_edges = get_settings().max_nodes * 4 + if len(self.edges) > max_edges: + raise ValueError(f"La carte depasse la limite de {max_edges} liens.") + return self diff --git a/backend/app/schemas/share.py b/backend/app/schemas/share.py new file mode 100644 index 0000000..1aabdbb --- /dev/null +++ b/backend/app/schemas/share.py @@ -0,0 +1,51 @@ +"""Schemas d'entree/sortie de l'API de partage.""" + +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict, Field + +from app.schemas.map import MapDocumentIn + + +class ShareCreateIn(BaseModel): + """Corps de POST /api/shares et de PUT /api/shares/{token}.""" + + model_config = ConfigDict(extra="forbid") + + title: str = Field(default="", max_length=255) + map: MapDocumentIn + + +class ShareCreatedOut(BaseModel): + """Reponse renvoyee une seule fois, a la creation du partage. + + `management_token` n'est jamais renvoye ailleurs et n'apparait jamais + dans une URL publique : il est conserve par le proprietaire. + """ + + share_token: str + management_token: str + share_path: str + title: str + created_at: datetime + updated_at: datetime + + +class ShareUpdatedOut(BaseModel): + share_token: str + share_path: str + title: str + updated_at: datetime + + +class SharePublicOut(BaseModel): + """Vue publique : aucune information de gestion.""" + + share_token: str + title: str + map_version: int + map: dict + created_at: datetime + updated_at: datetime diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/errors.py b/backend/app/services/errors.py new file mode 100644 index 0000000..8c15338 --- /dev/null +++ b/backend/app/services/errors.py @@ -0,0 +1,34 @@ +"""Erreurs metier du domaine partage. + +Elles sont converties en reponses HTTP propres par les handlers de `app.main` : +aucune trace technique n'est exposee au client. +""" + +from __future__ import annotations + + +class ShareError(Exception): + """Erreur metier generique.""" + + status_code = 400 + message = "Requete invalide." + + def __init__(self, message: str | None = None) -> None: + super().__init__(message or self.message) + if message: + self.message = message + + +class ShareNotFoundError(ShareError): + status_code = 404 + message = "Ce partage n'existe pas ou a ete supprime." + + +class ShareForbiddenError(ShareError): + status_code = 403 + message = "Jeton de gestion invalide." + + +class SharePayloadTooLargeError(ShareError): + status_code = 413 + message = "La carte depasse la taille maximale autorisee." diff --git a/backend/app/services/shares.py b/backend/app/services/shares.py new file mode 100644 index 0000000..7124acd --- /dev/null +++ b/backend/app/services/shares.py @@ -0,0 +1,131 @@ +"""Logique metier du partage de cartes.""" + +from __future__ import annotations + +import json +import logging + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.config import get_settings +from app.models.share import SharedMap, utcnow +from app.schemas.map import MapDocumentIn +from app.services.errors import ( + ShareForbiddenError, + ShareNotFoundError, + SharePayloadTooLargeError, +) +from app.services.tokens import ( + generate_management_token, + generate_share_token, + hash_token, + verify_token, +) + +logger = logging.getLogger(__name__) + + +def serialize_map(document: MapDocumentIn) -> str: + """Serialise le document carte en JSON compact et deterministe.""" + return json.dumps( + document.model_dump(mode="json"), + ensure_ascii=False, + separators=(",", ":"), + ) + + +def _guard_payload_size(payload: str) -> None: + settings = get_settings() + size = len(payload.encode("utf-8")) + if size > settings.max_map_size_bytes: + raise SharePayloadTooLargeError( + f"La carte fait {size / (1024 * 1024):.1f} Mo, " + f"la limite est de {settings.max_map_size_mb} Mo." + ) + + +def _get_by_token(session: Session, share_token: str) -> SharedMap: + stmt = select(SharedMap).where(SharedMap.share_token == share_token) + share = session.execute(stmt).scalar_one_or_none() + if share is None: + raise ShareNotFoundError() + return share + + +def create_share(session: Session, *, title: str, document: MapDocumentIn) -> tuple[SharedMap, str]: + """Cree un partage et retourne (entite, management_token en clair). + + Le management token en clair n'est renvoye qu'ici, une seule fois. + """ + payload = serialize_map(document) + _guard_payload_size(payload) + + management_token = generate_management_token() + share = SharedMap( + share_token=generate_share_token(), + management_token_hash=hash_token(management_token), + title=(title or document.name or "Carte sans titre")[: get_settings().max_title_length], + map_version=document.version, + map_data=payload, + ) + session.add(share) + session.commit() + session.refresh(share) + + logger.info("Partage cree share_token=%s nodes=%s", share.share_token, len(document.nodes)) + return share, management_token + + +def get_share(session: Session, share_token: str) -> SharedMap: + return _get_by_token(session, share_token) + + +def load_map_data(share: SharedMap) -> dict: + """Deserialise le snapshot stocke. Ne fait qu'un json.loads : aucun code execute.""" + try: + data = json.loads(share.map_data) + except json.JSONDecodeError: # pragma: no cover - donnee corrompue + logger.exception("Snapshot illisible pour share_token=%s", share.share_token) + raise ShareNotFoundError("Le contenu de ce partage est illisible.") from None + if not isinstance(data, dict): # pragma: no cover - donnee corrompue + raise ShareNotFoundError("Le contenu de ce partage est illisible.") + return data + + +def update_share( + session: Session, + share_token: str, + *, + management_token: str, + title: str, + document: MapDocumentIn, +) -> SharedMap: + share = _get_by_token(session, share_token) + if not verify_token(management_token, share.management_token_hash): + logger.warning("Tentative de mise a jour refusee share_token=%s", share_token) + raise ShareForbiddenError() + + payload = serialize_map(document) + _guard_payload_size(payload) + + share.title = (title or document.name or share.title)[: get_settings().max_title_length] + share.map_version = document.version + share.map_data = payload + share.updated_at = utcnow() + session.commit() + session.refresh(share) + + logger.info("Partage mis a jour share_token=%s", share_token) + return share + + +def delete_share(session: Session, share_token: str, *, management_token: str) -> None: + share = _get_by_token(session, share_token) + if not verify_token(management_token, share.management_token_hash): + logger.warning("Tentative de suppression refusee share_token=%s", share_token) + raise ShareForbiddenError() + + session.delete(share) + session.commit() + logger.info("Partage supprime share_token=%s", share_token) diff --git a/backend/app/services/tokens.py b/backend/app/services/tokens.py new file mode 100644 index 0000000..83bbb33 --- /dev/null +++ b/backend/app/services/tokens.py @@ -0,0 +1,45 @@ +"""Generation et verification des jetons de partage. + +Deux secrets distincts sont produits a la creation d'un partage : + +* ``share_token`` : identifiant public, present dans /s/, lecture seule. +* ``management_token`` : secret de gestion (PUT/DELETE), jamais dans une URL, + stocke uniquement dans l'IndexedDB du proprietaire. + +Seul le hash SHA-256 du management token est conserve en base. +""" + +from __future__ import annotations + +import hashlib +import secrets + +# 12 octets -> 16 caracteres url-safe, ~96 bits d'entropie : non enumerable. +SHARE_TOKEN_BYTES = 12 +# 32 octets -> ~256 bits d'entropie pour le secret de gestion. +MANAGEMENT_TOKEN_BYTES = 32 + + +def generate_share_token() -> str: + return secrets.token_urlsafe(SHARE_TOKEN_BYTES) + + +def generate_management_token() -> str: + return secrets.token_urlsafe(MANAGEMENT_TOKEN_BYTES) + + +def hash_token(token: str) -> str: + """Hash du secret de gestion. + + Le token est deja un secret aleatoire de haute entropie : un SHA-256 simple + suffit (pas de risque de force brute par dictionnaire, contrairement a un + mot de passe choisi par un humain). + """ + return hashlib.sha256(token.encode("utf-8")).hexdigest() + + +def verify_token(token: str, expected_hash: str) -> bool: + """Comparaison a temps constant.""" + if not token or not expected_hash: + return False + return secrets.compare_digest(hash_token(token), expected_hash) diff --git a/backend/app/web.py b/backend/app/web.py new file mode 100644 index 0000000..9341932 --- /dev/null +++ b/backend/app/web.py @@ -0,0 +1,76 @@ +"""Service des fichiers statiques du frontend (SPA) par FastAPI. + +Le build Vite est copie dans `settings.static_dir`. Toute route qui n'est ni +/api/... ni un fichier existant retourne index.html, afin que le routeur +cote client puisse gerer /map/:id et /s/:token. +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from fastapi import APIRouter, FastAPI, Request +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles + +logger = logging.getLogger(__name__) + +_PLACEHOLDER = """ +Frontend non compile + +

Frontend non compile

+

L'API est disponible sur /api/health.

+

En developpement, lancez npm run dev dans frontend/.

+""" + +# Fichiers servis a la racine et qui ne doivent pas etre mis en cache longtemps. +_NO_CACHE_FILES = {"index.html", "sw.js", "manifest.webmanifest"} + + +def _safe_resolve(static_dir: Path, relative_path: str) -> Path | None: + """Empeche toute sortie du dossier statique (path traversal).""" + candidate = (static_dir / relative_path.lstrip("/")).resolve() + try: + candidate.relative_to(static_dir.resolve()) + except ValueError: + return None + return candidate + + +def mount_frontend(app: FastAPI, static_dir_path: str) -> None: + static_dir = Path(static_dir_path) + router = APIRouter(include_in_schema=False) + + if (static_dir / "assets").is_dir(): + # Les assets Vite sont hashes : cache long sans risque. + app.mount( + "/assets", + StaticFiles(directory=static_dir / "assets"), + name="assets", + ) + + @router.api_route("/{full_path:path}", methods=["GET", "HEAD"]) + def serve_spa(full_path: str, request: Request): # type: ignore[no-untyped-def] + if full_path.startswith("api/"): + return JSONResponse(status_code=404, content={"detail": "Ressource introuvable."}) + + if not static_dir.is_dir(): + return HTMLResponse(_PLACEHOLDER, status_code=200) + + if full_path: + candidate = _safe_resolve(static_dir, full_path) + if candidate is not None and candidate.is_file(): + headers = ( + {"Cache-Control": "no-cache"} + if candidate.name in _NO_CACHE_FILES + else {"Cache-Control": "public, max-age=3600"} + ) + return FileResponse(candidate, headers=headers) + + index = static_dir / "index.html" + if index.is_file(): + return FileResponse(index, headers={"Cache-Control": "no-cache"}) + return HTMLResponse(_PLACEHOLDER, status_code=200) + + app.include_router(router) diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..26b2efc --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "openmind-backend" +version = "1.0.0" +description = "Backend FastAPI d'OpenMind : partage de cartes en lecture seule." +requires-python = ">=3.11" + +[tool.ruff] +line-length = 100 +target-version = "py311" +exclude = [".venv", "__pycache__"] + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP", "C4", "SIM", "RUF"] +ignore = ["B008"] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +filterwarnings = ["ignore::DeprecationWarning"] diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..a99f424 --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +pytest>=8.3,<9 +httpx>=0.27,<0.29 +ruff>=0.8,<0.14 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..fc7146a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.115,<0.117 +uvicorn[standard]>=0.32,<0.36 +SQLAlchemy>=2.0.36,<2.1 +pydantic>=2.9,<3 +pydantic-settings>=2.6,<3 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..e495d56 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,84 @@ +"""Fixtures de test : base SQLite temporaire, isolee du volume de production.""" + +from __future__ import annotations + +import os +import tempfile +from collections.abc import Iterator + +import pytest + +# Doit etre defini AVANT le premier import de l'application. +_TMP_DIR = tempfile.mkdtemp(prefix="openmind-tests-") +os.environ["DATABASE_URL"] = f"sqlite:///{_TMP_DIR}/test.db" +os.environ["STATIC_DIR"] = f"{_TMP_DIR}/static" +os.environ["APP_NAME"] = "OpenMind" +os.environ["MAX_NODES"] = "5000" +os.environ["MAX_MAP_SIZE_MB"] = "10" + +from fastapi.testclient import TestClient # noqa: E402 + +from app.config import get_settings # noqa: E402 +from app.db.base import Base # noqa: E402 +from app.db.session import engine # noqa: E402 +from app.main import app # noqa: E402 + + +@pytest.fixture(autouse=True) +def _fresh_database() -> Iterator[None]: + get_settings.cache_clear() + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) + yield + get_settings.cache_clear() + + +@pytest.fixture() +def client() -> Iterator[TestClient]: + with TestClient(app) as test_client: + yield test_client + + +def build_map(node_count: int = 3, name: str = "Carte de test") -> dict: + """Construit un document carte minimal mais valide.""" + nodes = [ + { + "id": "root", + "type": "root", + "text": name, + "position": {"x": 0, "y": 0}, + "parentId": None, + } + ] + edges = [] + for index in range(1, node_count): + node_id = f"n{index}" + nodes.append( + { + "id": node_id, + "type": "standard", + "text": f"Noeud {index}", + "position": {"x": 200, "y": index * 80}, + "parentId": "root", + } + ) + edges.append({"id": f"e-root-{node_id}", "source": "root", "target": node_id}) + + return { + "version": 1, + "id": "11111111-1111-4111-8111-111111111111", + "name": name, + "createdAt": "2026-01-01T00:00:00.000Z", + "updatedAt": "2026-01-01T00:00:00.000Z", + "nodes": nodes, + "edges": edges, + "viewport": {"x": 0, "y": 0, "zoom": 1}, + "settings": {"theme": "default", "layout": "mindmap-right", "branchStyle": "bezier"}, + } + + +@pytest.fixture() +def created_share(client: TestClient) -> dict: + response = client.post("/api/shares", json={"title": "Carte de test", "map": build_map()}) + assert response.status_code == 201, response.text + return response.json() diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 0000000..d008130 --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,28 @@ +from fastapi.testclient import TestClient + + +def test_health_returns_ok(client: TestClient) -> None: + response = client.get("/api/health") + assert response.status_code == 200 + assert response.json()["status"] == "ok" + + +def test_public_config_exposes_no_secret(client: TestClient) -> None: + response = client.get("/api/config") + assert response.status_code == 200 + body = response.json() + assert body["appName"] + assert body["maxNodes"] > 0 + assert "management" not in response.text.lower() + + +def test_security_headers_present(client: TestClient) -> None: + response = client.get("/api/health") + assert response.headers["X-Content-Type-Options"] == "nosniff" + assert "default-src" in response.headers["Content-Security-Policy"] + + +def test_unknown_api_route_returns_clean_404(client: TestClient) -> None: + response = client.get("/api/does-not-exist") + assert response.status_code == 404 + assert response.json()["detail"] diff --git a/backend/tests/test_shares.py b/backend/tests/test_shares.py new file mode 100644 index 0000000..8752015 --- /dev/null +++ b/backend/tests/test_shares.py @@ -0,0 +1,109 @@ +from fastapi.testclient import TestClient + +from tests.conftest import build_map + + +def test_create_share_returns_two_distinct_tokens(client: TestClient) -> None: + response = client.post("/api/shares", json={"title": "Homelab", "map": build_map()}) + assert response.status_code == 201, response.text + body = response.json() + + assert body["share_token"] + assert body["management_token"] + assert body["share_token"] != body["management_token"] + # Entropie suffisante : token_urlsafe(12) et token_urlsafe(32). + assert len(body["share_token"]) >= 16 + assert len(body["management_token"]) >= 40 + assert body["share_path"] == "/s/" + body["share_token"] + + +def test_read_share_returns_map_without_management_token( + client: TestClient, created_share: dict +) -> None: + response = client.get("/api/shares/" + created_share["share_token"]) + assert response.status_code == 200 + + body = response.json() + assert body["title"] == "Carte de test" + assert body["map_version"] == 1 + assert len(body["map"]["nodes"]) == 3 + assert "management_token" not in body + assert created_share["management_token"] not in response.text + + +def test_read_unknown_share_returns_404(client: TestClient) -> None: + response = client.get("/api/shares/inexistant-token") + assert response.status_code == 404 + assert "existe pas" in response.json()["detail"] + + +def test_update_share_with_valid_management_token(client: TestClient, created_share: dict) -> None: + updated_map = build_map(node_count=6, name="Homelab v2") + response = client.put( + "/api/shares/" + created_share["share_token"], + json={"title": "Homelab v2", "map": updated_map}, + headers={"X-Management-Token": created_share["management_token"]}, + ) + assert response.status_code == 200 + assert response.json()["title"] == "Homelab v2" + + public = client.get("/api/shares/" + created_share["share_token"]).json() + assert len(public["map"]["nodes"]) == 6 + assert public["title"] == "Homelab v2" + + +def test_update_share_with_wrong_management_token_is_refused( + client: TestClient, created_share: dict +) -> None: + response = client.put( + "/api/shares/" + created_share["share_token"], + json={"title": "Pirate", "map": build_map()}, + headers={"X-Management-Token": "mauvais-jeton"}, + ) + assert response.status_code == 403 + + public = client.get("/api/shares/" + created_share["share_token"]).json() + assert public["title"] == "Carte de test" + + +def test_update_share_without_management_token_is_refused( + client: TestClient, created_share: dict +) -> None: + response = client.put( + "/api/shares/" + created_share["share_token"], + json={"title": "Pirate", "map": build_map()}, + ) + assert response.status_code == 403 + + +def test_share_token_alone_cannot_delete(client: TestClient, created_share: dict) -> None: + response = client.delete( + "/api/shares/" + created_share["share_token"], + headers={"X-Management-Token": created_share["share_token"]}, + ) + assert response.status_code == 403 + assert client.get("/api/shares/" + created_share["share_token"]).status_code == 200 + + +def test_delete_share_then_read_returns_404(client: TestClient, created_share: dict) -> None: + response = client.delete( + "/api/shares/" + created_share["share_token"], + headers={"X-Management-Token": created_share["management_token"]}, + ) + assert response.status_code == 204 + assert client.get("/api/shares/" + created_share["share_token"]).status_code == 404 + + +def test_delete_unknown_share_returns_404(client: TestClient) -> None: + response = client.delete( + "/api/shares/inexistant", headers={"X-Management-Token": "peu-importe"} + ) + assert response.status_code == 404 + + +def test_share_tokens_are_unique(client: TestClient) -> None: + tokens = set() + for _ in range(15): + body = client.post("/api/shares", json={"title": "x", "map": build_map()}).json() + tokens.add(body["share_token"]) + assert len(tokens) == 15 diff --git a/backend/tests/test_spa.py b/backend/tests/test_spa.py new file mode 100644 index 0000000..1b72c1d --- /dev/null +++ b/backend/tests/test_spa.py @@ -0,0 +1,22 @@ +"""Le routage cote client doit rester fonctionnel derriere FastAPI.""" + +from fastapi.testclient import TestClient + + +def test_share_route_falls_back_to_spa(client: TestClient) -> None: + response = client.get("/s/un-token-quelconque") + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + +def test_root_route_is_served(client: TestClient) -> None: + response = client.get("/") + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + +def test_path_traversal_is_blocked(client: TestClient) -> None: + response = client.get("/../../etc/passwd") + # Soit 404, soit repli SPA : dans tous les cas jamais le contenu du fichier. + assert response.status_code in (200, 404) + assert "root:" not in response.text diff --git a/backend/tests/test_validation.py b/backend/tests/test_validation.py new file mode 100644 index 0000000..d7b0f5a --- /dev/null +++ b/backend/tests/test_validation.py @@ -0,0 +1,78 @@ +"""Tests des garde-fous : payload invalide, limites de taille et de noeuds.""" + +from __future__ import annotations + +import os + +from fastapi.testclient import TestClient + +from app.config import get_settings +from tests.conftest import build_map + + +def test_missing_map_returns_422(client: TestClient) -> None: + response = client.post("/api/shares", json={"title": "Sans carte"}) + assert response.status_code == 422 + assert "carte valide" in response.json()["detail"] + + +def test_map_without_version_returns_422(client: TestClient) -> None: + payload = build_map() + del payload["version"] + response = client.post("/api/shares", json={"title": "x", "map": payload}) + assert response.status_code == 422 + + +def test_node_without_id_returns_422(client: TestClient) -> None: + payload = build_map() + del payload["nodes"][0]["id"] + response = client.post("/api/shares", json={"title": "x", "map": payload}) + assert response.status_code == 422 + + +def test_unknown_top_level_field_is_rejected(client: TestClient) -> None: + response = client.post( + "/api/shares", json={"title": "x", "map": build_map(), "is_premium": True} + ) + assert response.status_code == 422 + + +def test_garbage_body_returns_422(client: TestClient) -> None: + response = client.post("/api/shares", json={"map": "ceci n est pas une carte"}) + assert response.status_code == 422 + + +def test_node_count_limit_is_enforced(client: TestClient) -> None: + os.environ["MAX_NODES"] = "10" + get_settings.cache_clear() + try: + response = client.post("/api/shares", json={"title": "x", "map": build_map(node_count=25)}) + assert response.status_code == 422 + assert "10" in response.json()["detail"] + finally: + os.environ["MAX_NODES"] = "5000" + get_settings.cache_clear() + + +def test_map_size_limit_is_enforced(client: TestClient) -> None: + os.environ["MAX_MAP_SIZE_MB"] = "0.001" + get_settings.cache_clear() + try: + response = client.post("/api/shares", json={"title": "x", "map": build_map(node_count=60)}) + assert response.status_code == 413 + assert "volumineuse" in response.json()["detail"] or "limite" in response.json()["detail"] + finally: + os.environ["MAX_MAP_SIZE_MB"] = "10" + get_settings.cache_clear() + + +def test_node_text_is_stored_as_plain_text(client: TestClient) -> None: + """Le backend stocke le texte tel quel : il ne l interprete jamais comme du HTML.""" + payload = build_map() + payload["nodes"][0]["text"] = "" + created = client.post("/api/shares", json={"title": "x", "map": payload}) + assert created.status_code == 201 + + public = client.get("/api/shares/" + created.json()["share_token"]).json() + assert public["map"]["nodes"][0]["text"] == "" + assert public["map"]["nodes"][0]["type"] == "root" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c5b5364 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +services: + openmind: + build: + context: . + dockerfile: Dockerfile + image: openmind:latest + container_name: openmind + restart: unless-stopped + env_file: + - .env + environment: + # Le conteneur ecoute toujours sur 8080 ; APP_PORT ne change que l'hote. + APP_NAME: ${APP_NAME:-OpenMind} + DATABASE_URL: ${DATABASE_URL:-sqlite:////app/data/openmind.db} + MAX_MAP_SIZE_MB: ${MAX_MAP_SIZE_MB:-10} + MAX_NODES: ${MAX_NODES:-5000} + CORS_ORIGINS: ${CORS_ORIGINS:-} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + STATIC_DIR: /app/static + ports: + - "${APP_PORT:-8080}:8080" + volumes: + # Bind mount explicite : les partages SQLite restent accessibles sur l'hote. + - ./data:/app/data + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/health"] + interval: 30s + timeout: 5s + start_period: 10s + retries: 3 diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..d21d6fd --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,34 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist', 'coverage', 'node_modules', 'public'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2022, + globals: { ...globals.browser, ...globals.node }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-console': ['error', { allow: ['warn', 'error'] }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + }, + }, +); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d5137af --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + OpenMind + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..22029bc --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5041 @@ +{ + "name": "openmind-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openmind-frontend", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@xyflow/react": "^12.3.6", + "dagre": "^0.8.5", + "dexie": "^4.0.10", + "html-to-image": "^1.11.11", + "jspdf": "^2.5.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/dagre": "^0.7.52", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.15.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "fake-indexeddb": "^6.0.0", + "globals": "^15.12.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "~5.6.3", + "typescript-eslint": "^8.15.0", + "vite": "^5.4.11", + "vitest": "^2.1.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.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.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/dagre": { + "version": "0.7.54", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.54.tgz", + "integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==", + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.6", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.6.tgz", + "integrity": "sha512-9XsEJNHjatKYndszKTF/bsU7FOP9dJ6V/EQwzy3oMdtqgBuUq7BjKSwkEo+C7s4qHstHQfwwoHA3E8QfpPxZZQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.82", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.82", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.82.tgz", + "integrity": "sha512-4DKnL3CGtCGLRSmgDqaajRVgeksMXq/Yw4wPfdMfm7JvdIiWHGzVYOfFUztiATwdBXZqUU6HhehwUdbV9G23PQ==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.5.tgz", + "integrity": "sha512-uiRYvQYe/nNSzBJ7OUnd2/TZVsAdob3blml44teEpee9Cc1f4rGZFewO+JT3Wo8mgFOSzNqes4FHZn/Qz8WOuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.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/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.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/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dexie": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.5.tgz", + "integrity": "sha512-wWCHdihT3dmlUSuNhn5mMZDWSpG0suxjAni7YjjiZdzabZcKmy3uNZGZ7AeYYXBoLbpSXX35fikPLkPC44Osiw==", + "license": "Apache-2.0" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.424", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.424.tgz", + "integrity": "sha512-rTOe76LHREmnLOWc0NlakB7AlnWjhTNflk4YE3gnIBPbSXLiY+VDEgwYl4wAYdBPD7eq9NQq6TR8lvd9Fls2CA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.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/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "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/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "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/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "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-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/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/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.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/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": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a0ef9d5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,45 @@ +{ + "name": "openmind-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "license": "MIT", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@xyflow/react": "^12.3.6", + "dagre": "^0.8.5", + "dexie": "^4.0.10", + "html-to-image": "^1.11.11", + "jspdf": "^2.5.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/dagre": "^0.7.52", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.15.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "fake-indexeddb": "^6.0.0", + "globals": "^15.12.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "~5.6.3", + "typescript-eslint": "^8.15.0", + "vite": "^5.4.11", + "vitest": "^2.1.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..ea37e115242e85daabd6e593042c2cffed1d4877 GIT binary patch literal 2038 zcmZuxdpy+X8h?Ldm|u)$)L55}W^yMCg`$g*>(ojtwQVCqiBPB%VdlpOW6EKdQf}qb zx^#r>In27vNSI_bvL(6SrxK=->oo1u{&CJ9=l#6T_xrrx=a1+8e4Y#Y$YD2CWo=~u zfGXA9*;~&1uLdj0(R(l)0|2Ip>g?zf^X5Z=Lx^v%X6k%N-fp(;ZrCD;mBtn2QjH0y zU)?d=SeNlF4kvWF=qBDthbR>+cSn?Yi-GW0UHdeScMY6GNA(D;ZEZsZeX?W)qq=3o znLgeqUwsy-2-K$L_MHlxJB)1p(8Kt&mIZZ6rYuSq@Uurm2DNH(6J&*j- zoF!R{uZtLHs+&j<=@QxwN3G{Ld(RF=t>oqv_mShJqDehv&?z#ES>WfqHw+SzU3ywc zJ4CLx6UN@(A`MeEFZu{7gM~Upk5h9`i=8veopnNXYM&j~@8`CrqVsIxvcETbfh#7a z;goFG&sI<>3M@f~~7VOjz4(N$~1xl_ann~q2kG{J%7_-FrC3o|aw?#zSD zG|#LWfubGxrp-@QXW|qlGnPuS))N_YVN2!V>L(O0_q^-e5c&tYM-Y_z3F7#?*>`Y4 zrE*K)oNRQ-gtCO`YXUnRMlZXj#WEwCEGQC{%DjMa9QTIf6Jn$eYxgXw68A^-l-AzI z;~Le*B`s~B&Zu4RWwahg0g>iF)slNYSq`2 ze0bgOkFA!GUeL5+SPC#7}s zW{-5VUp!m&O_51G>xR9gFn#At)h?Z*`A|A=d!IW+c znlz(;%I0`^jgQUGueakr?@|xHUg6R0hEzJ8KP zKT79OeshCIFAeiLg@kZZ+IoOhJ-DNF06}YAvg&Njy~^F>2LfV4txrIjUx?=jyiZ!& zNvrWCD89QFQjlX|UT^A~3_<2@&!U-`(4{?RSlTc{5+zFaBIFh`Ew zR6}k=9c}0~RuRDxoJ0sq3OR9!KDO;=UzMB{`>%^5 zYMms6vN{|4D;pXKybX}|UR-|Xn5In|t59_TAJ?Um?iqB@`lamdNaD&IOFZ1IkayL* z^Nh9H_%-mVRJKAkE2yR0z1!Xd_P zbLk34Ned$7$tXQWN5vW;OE5&@$mr7^@qbUJ3v)s05)UO$m3yHrowu|_B)Gy59zZ}2 zav}x4JD;%7NJ;@Inx`kWZ~|cZ#-Z|4Dx`8esJYiy$b1Fxd{xsQzfNA5b(p-RB1Vn} z?y+9U<{uL4A*_-ZVYwIX5eCZdnWw}rx5PxuB&zWPSQQ~Y?e7OjcV5AWVCj0jYrumOeJkTyFsi&}g3{ple^&a#k{ilsz{Iim(&3^TN=<xd~K?vPoMW>o1%YNe0MC>uo2tx%_>hCS|v5+CH#+*1AVGwRWI1pGj4QHrQo rv@4x@b&v|*ulAc=i;tEZG8mSs44P$Iqc^#R0{Nl39CofeKu`J$hrpb| literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..46936b85754e93645bd92423000a44450d4ec74d GIT binary patch literal 5617 zcmbVQ2~<;8x864i5GIKOL#vR8wy1#MR8%mzC{(Ogcy$0pB%l^sg~)FOlp$9HMXN*Z*Gsx87Rs{VOZ$oU_l~`|Pv# zKHu5r9`{X^xB?% zXS4Y-+ic}eM7&4t@y?CqhvQ||fiu&s@5Qq}b>8r1+Mda2dlQX+51jcZmbH0O6RY6q z86S^N-DBIV4Nl3fMVz?6S@Lbzz^ZT0FUm+6j}S;E?!KNkl9EWBUr;*`-9K@#_4bnI zzuu)vk8z{lTnh4B=Q3OwA3o`C>V_Ac!}s?Y)%|>b5jXlkgTymMP@wqmruL>DH{!#0 z;&R(0b%{Hl?8r@EZmFnvIxDl*KrgEykvsBcP3uaNeG!i5dT02!ZX7UEKRi`JNwLjS zP1c{f$gn-}?oC*!4p({}=a_71)Xlo>E{GuthOoxqPb&KfN2#3(%2AUwH;Io6DsY#g zEmig9N1_xR3hoq~3eq*=WUaD|nVm6$)ij$XSo+;#yz_2SD>??b`uO-Qz0>hroWo(+ zse#R?DK{Z_Qk9@N=(V1(l_%`n_MYlo@rw%c>8d9>j;yfn9>cD+TVcUilLmGhUYj2> z8aay#2MTnG_sBK~AdGoBGX{EfuigG-dqc;sT;hu=&BDz)r;@OAt~C9I8H^9FjLlU0xNNcN2n13z&e`vyS-~!MCZ`otat?uQ3oNn(Mwt?fOC3vqk5o(ClgF! z0!v5zQD%cm53<3R0;#F+Jr`~}d%N6+7%1$j>bg^417fr~4fTWc_xo<3-%DkGCgjF@%!C>WdM-MVlSWN-%bttY8gU3?6whD~cxgS&%r0$y}8%MMVECAl;^1iafTy~5B4|TB;V8hYz?M6F+15+k3?qVD?3}O9%4)l}^*1nI;U;yCdr+{w+ z9%e8DV-azyG0TsQ4j@4wj4+ucKI)Z|Itp7$?>wr?Zn{T~-cdS*k&WG++eYuqL_-9;xXg;y{;NrUQB*ZeehBqLcvNTB zLoo-4%PO^Uqy8vk>1YZ-kJ8~kqDw6u3rJ%U<&jE#d?it(74lqaA|23j*cf7FViw?% zvdf8_58cY9jwIhg=aN2=Vl~RVaoenh_;Y0ViHC<)(CQf~b>t@Xgd6YGd-;L=`|oX7 zt+6AK+xS$XY4)+*t}{jkER#Ym*kit0s@Of6bb4Y}Dt%x*BPs8m2BpTK)^7hVZ|S^i zo}7?6*|4lHM!WU{K6P__$cZqscUfUNm#anBwN{F9YyI6V5~`fd6bHlftQS}MPrs*c z7Oj=&W;i5`~EF?J&#FwyZ&M_jdJoqvJnGToO*rDU$RzGc8RAh-yO6-tP}~7 zDOHN|+9WT|VOdZj5Nl&j^Tn&HHblB~=G^TWuwl5f3?)k#aFnF!W~5L0d!M1vDZxxv zbY!E&S8L^$mrKUFUrlzjMP9xBRm61(Uy#KAD~|1U-|Ju-P41nl33gCN8XKxYII#P4 zwiuCoNk3n8<|-l$9qJzwvcJ7rD{;5W_489_`Z87`S_U~hzUJ1LA;yLX)yr<1hXs`D zYMtotkHbvqVZo8ZY&fTW*`3*SB_`TWZ4)?J9)28T4bhZJ{W!<4TYX(<1sG8iqS>HQ ztH>?{FplcXd3Moc$X-q1ffA8lvsnTVeN^&U8){_9c%bC-6kS!wWbiqjRcNICM7@ZU zfYjNFm79uv9*VvmklP`5ZVE{ohDj@i>Um&U^?^T+3SAf-f;h!&e|WZ}}Z;<01r zM1y{_5HY8Iqu=<@*>-fN;w)$;GwauBG?MT{`u1gm1#JBOmxXp@KMSRr4*u2G&(FT> z|3x3RPl*HB%Jzt#zPbA1xtKcnIV?rMNr1GzMD_BF3&tB;WQ-xaoQnYq{XRpgc<47r zi=Z-l;;S=T&+>~-&4bFdDe5~%fv|*KAAyt&Um>W)(a(#44i)-K>l>1YVh)={xU`mx z9oe+5yyj)&nGew%rzWbxoLP)Z*GH*qoEW5 zDe(pV=Uvs?6z4_eF`~YNz7IZ2zb21>?6k$k4S#IBwT*Q+tg`%VGXkC>)7<9;#51ff zLFZPjFQrRyz|m+opKwD(BhurHJ=ROzXNAiVyxoH?riU4WhEKE(7q6NreRP)Pj^~Zd zf;Kl|9H#ui>KRA~`r96l^?nFSt?MR$xFm~>9DHW;`Q7@@7jRKVY@DBQ z$es)zX%0!r9GI_zDKW&v@lv)6RxEyX`{ESs_&i}6NhYOdIMNbq?6?{AYP#tkdMM5N z^?L%`>$c>y>8$Nyq7%O#(;1pLtFIjp*1n9Zy}FgD5tiwbXMa+8qn|vVTsZi-w*=JlaWAd(e~U zx}hci%}JuGo4Gx*vq{m_H8Pf6DC+-P==+fyRCK@k$BShn<5O1}(91}}m6(mwm(EG! zS4YKG;xR?bRrfgwc-!a^+LoT4DBR_C^#(F4iHx!Ka?2f@z7hb%UC8JC3o1Ku>)NC6 zi=Jv_FZ-M+PIE0@%H{ z@wHkeuh8g`&XyG&!rHmUS1`(K5C}ft`#t>w~>qrfTh$rl!%=`Xsn&E4bk}D*}5LsA%KD^v{4r7m@`1v4MvyM(kR&1g|WM6OX1rh9IFLjt8RofTNwrP zq;B8#e*zdRuQ$84e>HFAmv-uc33{?6>VojUgBD&}j@z~7CDi6Sb1uy!8ymjP?U@BI z&Bdc*fCGY5<^#(QkhMKaqEJXeIiwnSaD83Jkg9}VurkaeU4 z7xx|k!i9d=7mxhDsy2guSX$*xIH2_(z+fBGliycCGgvK)4zsU0hQYzg{xYO*3zP6> zFmbD=q+B2RQ3AMy!@7A#u_#m?mNpDiV%Rqgy!AQmP+b7L07^asY3ZfznV|Swq$KtZx(p)Vl>;>96q^Ga?QEsW{FhB)gMKpg z9TOGTed1sMk0MQuF@Sy{bsMetmQHz?-He`NK*TjsrxN!tqZ6P`5Sl;GIIpt>y~FGV z`l*T<+gMEK^WwVL7+LjNAY!u!*H|a&6I~Wmh?7|i{KXY<7zVOuWqts^tA<3#fRlVp zlMd4ANue3uTkVv`*uPA*q7e`1qX|+Lml#)rVf^a?tte9TID=P-$XcIz(pk% z8`kJiMrfh(`(>JWWPH>D@RBry`o9nSP6zOl+2U+^Yr{FF0BZ`#Alqb{v2qX|2$etC zDyh+N1;<`0UhrreXQvfFnz?s>&Tw9_Epn2k_&s{zLeGT5roRW$Uwz~Usn%gl3-W2a zWYlRI`XE6TFSh_)o#OVF50N_*4k(6I{wS%?gBm@S#o_31U(P2H+WM&dJ~4ttO{AH4 z5e7Zuw&-*tCLRmbL`rhNM8l;lSx&UyiGu0DZ?E6FFI+xj-PkHd&%`P9khSR2zd*E6N4Ad zwX+#z0|0jO0;P)qP_m1H6`lNOcRn`&@Yc?g`v17|b!VgaT58={M$@xb=Y#*{YCDPg zD|OO-_OI`^PP!|Ru=l>Y7s-rB5(K7h`Kc_xo_)7~vp-qr%A6h0f4rh~g8#(BDf?yU zvQr}}(DJu0X5=igpL=uH?FC!+>>#S%=q;(5;u_Y42~nSSwyiopFjEuzI3!82V3J2^ z_tQt=HE#+E)hFl2Xny->c+tp6u|zyhYbd-;r0p?0Z{1WnlEXRX@IJfT&+&tPb582< zSCcjIk)co6w8IfUOQlc8UpT%NIb0rj+p}V}@9pxE`G3|lw;X7QSxVcR#^rF%YEPJc zje1m!N34BuA++#G@)>DUSmaA z-$%FXXN?m>s~tD+6^aKJnWa~aennIpl*3_`(!%`H1#HelJO169zt`Mxqo}-uLOZ|K zaPg*^hZ>jcnF8VBD^ss)CZ>s^8EY61ttp&iNseaSzGWV1W5XG1!smGjglnG6^-3SX z=Hzr-BHLrRor1K0w26+CMcJbST+YOlF|U-)6PebTw(J?pU4&b!9983DZ!J(tr0Z&) zm9C9lH*0A}5O8{Kv7%{GskZ;_4NMW}^^moYtxzl&pQm4K?8jzHq<{ZtFy`qml7uh9 z$?4ul4qZMhs=umYy{od;IRr{wP3dgXh#^kdJ(dkZn$jGZ_UG=^yANQi{W)GQSPShG z3MW2Od@IDNAUIH(lw!oI_X>qIbd#$o`H;W=v5cCCT>h6~HoJ}K3Pg;jrz<*oDQy&K zS+qFaO&~1K5#c%hoa^3;9yW9_@-{McCw?sJE5SA$2q;@3mU&C0wtD0sHQ=&s{H+@P zLOeL5d_!QcG>h*!S|HqQ#$J)je9o%3V;^hD+=J!YCIv>N@S7a{IZ?%|$Q8ktM`)*Z zFh;PsIjyYpe+kw+;!EaB%1()~Q<%a{4sHmCXM_Tuz{?lP)u+VCCktU-gJysMo&)Re z#xA8k(J(xDf~V`XMr7CpW-wI|FUAUiqZDI8Nm(`@YnFWNUp}K{M9!A{_fyuwO1EN9 zoM1*$+3Xx0#RZkRS|I7aa;_FfIRs$q{{FD!bk;Hx@-fo#=CRx46Wn|wQZPOEtMEUh zj@_qQOtX;%^NW+KZ6=g262n|-!2j15YC?jkf$vDcTW$-Zmvz2pCT_bF?+dRTS-W!@ zHob5dGJ47ccV#ydNzSs$Mjb;;Xm;c;*-tW_|AC+7@7FBi1)LBnLvwLs*w z;LT=A`fx#>1*x@39=COHyTlvdxl7mOoW%pT`ZNq%>|FW2KZZB_CWopw_AXZ}NaQ5R z0yCxQN9>G&fnSH5J{GoA^19x@kp12A`;Mi(Ez?!7^S5lDUKUn_l%18s8@r}EVPa2k zUFq<-N(=>~@g_cuu$rc003EfnAXox!J>LEM?m38r{-i7KOHmICJ3AU3W28VOI?GJK zRzjr(@Epn4Hi!Tj4}T_xdfGMD5ckHC_W2BqP}2gVe3R$PLD1YP`O8GZ0{)u3mlT%V zTM48@^5!9t@o#9|S{6KvM*RK{D^m{H4`ODf&m<7pu}@==3l>Ds?2cWPLtp{3iAlvy z%#3ZTsD9{X$=T8Th=QF^F4=x!GOYP@?=%A%5{tG^xT&i=b-=?8@?u(_HpaS>t2(Pp zSYc$s)H=#c0|Smw?~vo{ej|yR>P^`)Jd6sq9&d|3-By8T?>SkPnTDOMqs}z4po<#W&xbeGG9w>uA)5&b z_AS;ahgy2JnFSLYAdf%=fQg2~Yd@($})S40@T4m{;o93?3h?ZGeP6w$3 zmfr5_Ecxf&*@IxuU$WlD={7%+w_la-+HzOCkV{!KP3QE49yuWP9GlY}ai@bnKAOoz zt4c;Uy-!pXvPO<*xlW7^GF9&S)EN|4I<5LodBVda%bq$WVQIu%JmX$gl?dzm#mQ@X z!V(*40q`0&wGF0?xB~n+k0=SiYRoSwehA>S6yZ^lN&*`n zL_{NzPPIK^!7`276^>A5SQz8anPP-ciIkXGf!n*nMq`Efl#c}Eozl7MoDJ3#d)EoG zh?BFylY#kySuR0fS3774ga+eNDc#M84FrD6NGDh>q|Z5FMj~M|P4ec4rlQps%q7r8 zFbvv8jbF4ad;RcI9)5_fZT@%GtmUP|C$XDYuf3iE7xR3vGo!Q@k(IkUC$mnAONm3R zZ;J=wXVEZk^ywcPK8yzHcRl-xj-F<;szlF;Lx0ebl|bFzJX3AhsW;9|^I$zXA+F2O zsrvezOFd4xQ@0}Q+C^#d^Ut%HO&`(WH&k zDccelXyU4Q(skGI|C^W{?^gGu*%lk*bQjI=7L6rw0XHO=d2-()EVH zgrP9$JjMfB^Vm1+DD zCU@Y|9f{_xwJXsfHKSIU26%|Ugm0ipv`E~DP*deuW^KFptcUzV^Orvf(~|^~JKi&x zkXr2p=bqHp}A4>LMJ3&&Egv)f~_zVTb2@|4`1+Uk7Z|zdVRCnBbt@5qbY{Yg=uTP z-y`2+>AO+%LUrB6rOKKF)3rO|7N*IIn@j6+HEVm+^EnvW6hXr_VLjTcnlPn%zG*lT zIgJJvEPeWvxBVeha)W{hgK2n>fmNc*T94Y&GXPGIBjftam7%7A3&Xl+9zY@0$Snw6w)1mR{vL@p;kC+>DTv< z*P4WqcG^HHnrz)x5;l`=Wkd2WKhlV9d=jSboSv=r;6l@+rYH6uGm?bP`^W=or$@rz zU~rCiY5#@~zet!_XtXuk1tRU!f0rhlXpKF5ab^QS&aT}CIc9P{4wu)ZfIkA)+aUuU zJV&ps9?Xe0WR|!#A*#;*%2GUEH88hCPI~R`_&9=9%|EG>SJst&$<}MWJPO@No8(zWhfGFUu&4@!Cwhx%rH!~S?)KCL1< zj5|C&M1n~6Wnvx2!hiVGi!8F*d}kax$mAIBlL0<^K7N_br@J$z@I8O`zgtk~VY7E% znLOgjp-U@^BaI6ab)Lag+mwR8eWKkAcb@t@G9&oIGID+hz^#--oVvlzcy9HBNI8(( z^ihfJoFe`>Z4Q5%?MSr;+*k&KnTW!-%1m_rbdFA`Te;tO=Z#B)k%rwbH>q!;_zwJ5 z+zi?05XM|i?nd$3IHUf9kLzwm7~gr8wy{Q(WC&;Mr8u2jc?r~17;8K?yVcpQv(2PfM8 zy!9ii7%=x6A;}nrtzz0PSS@OLk-|EyHW?}(@hu(C!sB}$ zU6sek2Oi!eK4<2OclFolzF8fm+k~B`!WB<`cU#KQG`H?po@s~FzuR!cm+JF{ishr| z?P^BZJ>qc?k=t`m6~<-d#5Nh%b-7`ODi2W)&*HO{ee*8pjyiNA>VGW+CwpfE^J=GL z$kl!KR4O-_H#kEo6(*fy={kCzt>O_UH%xXxBT-x+2)-MK2P=uUg`gfwhk^In>yPT< zLh4!3OMYJZK5W1jQGz#$dHs>=%eH2L^lfGsrGciY1I|-pjoxy zht!XgQz`_4+Ni?%s>%B%zQ1DJ(Y0ro<-#g=|Ncme`g#C5Xh^B8X4lZlO`zPOcDem8Ml9UfUL8@@oK@F>Exz zW?~Zs$TImifNXP?9gu0_HUa{3zv^3n@r{-`I$--7h_@t18LhDlB*`H3{qDZ35Gn;1 zs{fSNRll0*y~Q8|na|)gB;;3x2L%o~u+%?s$$}iPgEf{S2Y}-j%-*(Eka&oAV-1k_ z*fN?<#ophSA4JGIMt9n32?WkCg^yr^{V<|%BpV!kw^+#P9zt&sufyh|2>wKzWlhJH z9j~*@u(yIEmRB?&^WYIGD!Fq>?oLO^s}6Yvqd;C)iU5+$AJpX%$lfr}!sk&C($aEq zGy?cD)k=VFZ3GWsvB=y&1MKo(QB2mZL!3av5X)X({%z)A0rPenbq zHKh5Bvcks6*L(x8NAAS}tW%Lf>~@-6MBPCgA5kXR`Pu=21}TfoayE+}N{ zlW18piu`#Lfcg~*p-PAn=OC0;}YSr0D&z)}8>?uSWUULSeZvgIwuy(gdH zhR`XlD|#kpSPkkUb+}rDNu&d-9w3D~oBPM8BB9ULw9NFokI<^=TAD%#c#FsX2Uqx# zsr9x`Qp;&>dTiA6qLbhK@q<$UIqUYx(aSPDzY1{k>1aV?5V;J}tm^`=f)?H=nR%XE zsp3{r5vN{ksci84;IYPsjc2DRnk8^QUU+gC<5z2VwSr93M}|wPXlIz9 zH3n4CpkqK;-~qTkp5unV&lcfw6!J^~x2UjLuiRh_^-ivwb5*4pN zk`*ScqQh>paE&)~Klc!to#S_P(jg_OP$=J<#GSJj8!P5Stz$>t3X&#`^~aOb6dRb3 zXAHP($RnS_W|!vopY7ru%bnpyX3p1|-jpsLr;>g*N5nbyjFqW#4N`q_#n?9SFF5%g;G|77+Dqb!y(R z?VU)a)x+Xy{>>?t@oY{J8cplIxZb$Eu)?b zWs`n3P0$BOxs*_CfK=**1%yeZGd$>3wzc6w>I@I^Y1K>8F%4;0s2Tm(Qkap8I7B$rRGL<9V1DNC-#ayTP*T$Q~LMe_ag+chB#x(lBJr0cdG@b}9+=jL-K zL^We?_(dM)*i*w8nlWbB@;9?WXO(OA9FKXqN-DkVB;J;tdu~>J#NqO|UtTD~o}A!a b7ZZ$Q9m@U(r}<>S0L%+mDE(`;Z14X7a3{sE literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon.svg b/frontend/public/icons/icon.svg new file mode 100644 index 0000000..e460c9b --- /dev/null +++ b/frontend/public/icons/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..b621453 --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,39 @@ +{ + "name": "OpenMind", + "short_name": "OpenMind", + "description": "Cartes mentales libres : local-first, sans compte, sans abonnement.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#f8fafc", + "theme_color": "#4f46e5", + "lang": "fr", + "categories": ["productivity", "utilities"], + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..bfc6863 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,71 @@ +/* + * Service worker d'OpenMind. + * + * Objectif : permettre d'ouvrir et d'editer les cartes locales meme sans + * reseau, une fois l'application chargee au moins une fois. Les appels API + * (partage) ne sont jamais mis en cache : ils echouent proprement hors ligne. + */ +const CACHE_NAME = 'openmind-shell-v1'; +const APP_SHELL = ['/', '/index.html', '/manifest.webmanifest', '/icons/icon.svg']; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(APP_SHELL)) + .catch(() => undefined) + .then(() => self.skipWaiting()), + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), + ) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + // Les routes API restent toujours en reseau direct. + if (url.pathname.startsWith('/api/')) return; + + // Navigation : reseau d'abord, repli sur la coquille applicative en cache. + if (request.mode === 'navigate') { + event.respondWith( + fetch(request) + .then((response) => { + const copy = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put('/index.html', copy)); + return response; + }) + .catch(() => caches.match('/index.html').then((cached) => cached || Response.error())), + ); + return; + } + + // Assets : cache d'abord, rafraichi en arriere-plan. + event.respondWith( + caches.match(request).then((cached) => { + const network = fetch(request) + .then((response) => { + if (response.ok) { + const copy = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, copy)); + } + return response; + }) + .catch(() => cached || Response.error()); + return cached || network; + }), + ); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..676c7a1 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,44 @@ +import { useEffect } from 'react'; +import { Button } from '@/components/Button'; +import { Toasts } from '@/components/Toasts'; +import { DashboardPage } from '@/features/maps/DashboardPage'; +import { EditorPage } from '@/features/editor/EditorPage'; +import { SharedMapPage } from '@/features/sharing/SharedMapPage'; +import { useAppConfigStore } from '@/stores/appConfigStore'; +import { navigate, useRoute } from '@/hooks/useRoute'; + +function NotFoundPage() { + const appName = useAppConfigStore((state) => state.name); + return ( +
+

Page introuvable

+

+ Cette adresse ne correspond a aucune page de {appName}. +

+ +
+ ); +} + +export function App() { + const route = useRoute(); + const loadConfig = useAppConfigStore((state) => state.load); + + useEffect(() => { + void loadConfig(); + }, [loadConfig]); + + return ( + <> +
+ {route.name === 'dashboard' ? : null} + {route.name === 'editor' ? : null} + {route.name === 'shared' ? : null} + {route.name === 'notFound' ? : null} +
+ + + ); +} diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx new file mode 100644 index 0000000..9de0954 --- /dev/null +++ b/frontend/src/components/Button.tsx @@ -0,0 +1,46 @@ +import { forwardRef, type ButtonHTMLAttributes } from 'react'; +import { cn } from '@/utils/cn'; + +type Variant = 'primary' | 'secondary' | 'ghost' | 'danger'; +type Size = 'sm' | 'md'; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: Variant; + size?: Size; +} + +const VARIANTS: Record = { + primary: + 'bg-indigo-600 text-white hover:bg-indigo-500 disabled:hover:bg-indigo-600 shadow-sm border border-transparent', + secondary: + 'bg-white text-zinc-700 border border-zinc-200 hover:bg-zinc-50 dark:bg-zinc-900 dark:text-zinc-200 dark:border-zinc-700 dark:hover:bg-zinc-800', + ghost: + 'bg-transparent text-zinc-600 border border-transparent hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800', + danger: + 'bg-red-600 text-white hover:bg-red-500 border border-transparent disabled:hover:bg-red-600', +}; + +const SIZES: Record = { + sm: 'h-8 px-2.5 text-xs gap-1.5', + md: 'h-9 px-3.5 text-sm gap-2', +}; + +export const Button = forwardRef(function Button( + { variant = 'secondary', size = 'md', className, type = 'button', ...props }, + ref, +) { + return ( + + + + } + > +

{message}

+ + ); +} diff --git a/frontend/src/components/IconButton.tsx b/frontend/src/components/IconButton.tsx new file mode 100644 index 0000000..80255d4 --- /dev/null +++ b/frontend/src/components/IconButton.tsx @@ -0,0 +1,41 @@ +import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'; +import { cn } from '@/utils/cn'; + +export interface IconButtonProps extends ButtonHTMLAttributes { + /** Libelle accessible, egalement utilise comme infobulle. */ + label: string; + icon: ReactNode; + active?: boolean; + tone?: 'default' | 'danger'; +} + +/** + * Bouton represente uniquement par une icone : il porte toujours un aria-label + * et une infobulle native (title). + */ +export const IconButton = forwardRef(function IconButton( + { label, icon, active = false, tone = 'default', className, type = 'button', ...props }, + ref, +) { + return ( + + ); +}); diff --git a/frontend/src/components/Icons.tsx b/frontend/src/components/Icons.tsx new file mode 100644 index 0000000..4527495 --- /dev/null +++ b/frontend/src/components/Icons.tsx @@ -0,0 +1,248 @@ +/** + * Jeu d'icones minimal, dessine a la main (aucune dependance externe). + * Chaque icone est purement decorative : le libelle accessible est porte par + * le bouton qui la contient. + */ +interface IconProps { + className?: string; +} + +const DEFAULT_CLASS = 'h-4 w-4'; + +function Svg({ className, children }: IconProps & { children: React.ReactNode }) { + return ( + + ); +} + +export const PlusIcon = (p: IconProps) => ( + + + +); + +export const ImportIcon = (p: IconProps) => ( + + + +); + +export const DownloadIcon = (p: IconProps) => ( + + + +); + +export const SearchIcon = (p: IconProps) => ( + + + + +); + +export const TrashIcon = (p: IconProps) => ( + + + +); + +export const CopyIcon = (p: IconProps) => ( + + + + +); + +export const PencilIcon = (p: IconProps) => ( + + + +); + +export const UndoIcon = (p: IconProps) => ( + + + + +); + +export const RedoIcon = (p: IconProps) => ( + + + + +); + +export const ZoomInIcon = (p: IconProps) => ( + + + + +); + +export const ZoomOutIcon = (p: IconProps) => ( + + + + +); + +export const FitIcon = (p: IconProps) => ( + + + + +); + +export const LayoutIcon = (p: IconProps) => ( + + + + + + +); + +export const ShareIcon = (p: IconProps) => ( + + + + + + +); + +export const PrintIcon = (p: IconProps) => ( + + + + + +); + +export const SunIcon = (p: IconProps) => ( + + + + +); + +export const MoonIcon = (p: IconProps) => ( + + + +); + +export const CloseIcon = (p: IconProps) => ( + + + +); + +export const ChevronLeftIcon = (p: IconProps) => ( + + + +); + +export const ChevronRightIcon = (p: IconProps) => ( + + + +); + +export const ChevronDownIcon = (p: IconProps) => ( + + + +); + +export const ArrowLeftIcon = (p: IconProps) => ( + + + +); + +export const CheckIcon = (p: IconProps) => ( + + + +); + +export const GridIcon = (p: IconProps) => ( + + + +); + +export const PaletteIcon = (p: IconProps) => ( + + + + + + +); + +export const LinkIcon = (p: IconProps) => ( + + + + +); + +export const NoteIcon = (p: IconProps) => ( + + + + +); + +export const SlidersIcon = (p: IconProps) => ( + + + + + + +); + +export const SaveIcon = (p: IconProps) => ( + + + + +); + +export const MapIcon = (p: IconProps) => ( + + + + + + +); + +export const SpinnerIcon = ({ className }: IconProps) => ( + +); diff --git a/frontend/src/components/Menu.tsx b/frontend/src/components/Menu.tsx new file mode 100644 index 0000000..4719547 --- /dev/null +++ b/frontend/src/components/Menu.tsx @@ -0,0 +1,133 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { cn } from '@/utils/cn'; + +export interface MenuProps { + label: string; + trigger: ReactNode; + children: (close: () => void) => ReactNode; + align?: 'left' | 'right'; + className?: string; + disabled?: boolean; +} + +/** Menu deroulant accessible : Echap ferme, clic exterieur ferme. */ +export function Menu({ + label, + trigger, + children, + align = 'left', + className, + disabled = false, +}: MenuProps) { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (!open) return undefined; + + const onPointerDown = (event: MouseEvent) => { + if (!containerRef.current?.contains(event.target as Node)) setOpen(false); + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setOpen(false); + }; + + document.addEventListener('mousedown', onPointerDown); + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('mousedown', onPointerDown); + document.removeEventListener('keydown', onKeyDown); + }; + }, [open]); + + return ( +
+
!disabled && setOpen((value) => !value)} + onKeyDown={(event) => { + if (disabled) return; + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setOpen((value) => !value); + } + }} + > + {trigger} +
+ + {open ? ( +
+ {children(() => setOpen(false))} +
+ ) : null} +
+ ); +} + +export interface MenuItemProps { + icon?: ReactNode; + label: string; + hint?: string; + onSelect: () => void; + disabled?: boolean; + tone?: 'default' | 'danger'; + active?: boolean; +} + +export function MenuItem({ + icon, + label, + hint, + onSelect, + disabled = false, + tone = 'default', + active = false, +}: MenuItemProps) { + return ( + + ); +} + +export function MenuSeparator() { + return
; +} + +export function MenuLabel({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx new file mode 100644 index 0000000..6009091 --- /dev/null +++ b/frontend/src/components/Modal.tsx @@ -0,0 +1,89 @@ +import { useEffect, useRef, type ReactNode } from 'react'; +import { CloseIcon } from '@/components/Icons'; +import { IconButton } from '@/components/IconButton'; +import { cn } from '@/utils/cn'; + +export interface ModalProps { + open: boolean; + title: string; + description?: string; + onClose: () => void; + children: ReactNode; + footer?: ReactNode; + size?: 'sm' | 'md' | 'lg'; +} + +const SIZES = { + sm: 'max-w-sm', + md: 'max-w-lg', + lg: 'max-w-2xl', +}; + +/** Fenetre modale accessible : fermeture par Echap, focus initial, clic exterieur. */ +export function Modal({ + open, + title, + description, + onClose, + children, + footer, + size = 'md', +}: ModalProps) { + const panelRef = useRef(null); + + useEffect(() => { + if (!open) return undefined; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.stopPropagation(); + onClose(); + } + }; + document.addEventListener('keydown', onKeyDown, true); + panelRef.current?.focus(); + return () => document.removeEventListener('keydown', onKeyDown, true); + }, [open, onClose]); + + if (!open) return null; + + return ( +
{ + if (event.target === event.currentTarget) onClose(); + }} + > +
+
+
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
+ } onClick={onClose} /> +
+ +
{children}
+ + {footer ? ( +
+ {footer} +
+ ) : null} +
+
+ ); +} diff --git a/frontend/src/components/Toasts.tsx b/frontend/src/components/Toasts.tsx new file mode 100644 index 0000000..69c316b --- /dev/null +++ b/frontend/src/components/Toasts.tsx @@ -0,0 +1,47 @@ +import { useUiStore } from '@/stores/uiStore'; +import { CloseIcon } from '@/components/Icons'; +import { cn } from '@/utils/cn'; + +const TONE_CLASSES = { + info: 'border-zinc-200 bg-white text-zinc-800 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100', + success: + 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-100', + error: + 'border-red-200 bg-red-50 text-red-900 dark:border-red-900 dark:bg-red-950 dark:text-red-100', +}; + +/** Zone de notifications : messages courts, jamais d'erreur technique brute. */ +export function Toasts() { + const toasts = useUiStore((state) => state.toasts); + const dismissToast = useUiStore((state) => state.dismissToast); + + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map((toast) => ( +
+ {toast.message} + +
+ ))} +
+ ); +} diff --git a/frontend/src/config/app.ts b/frontend/src/config/app.ts new file mode 100644 index 0000000..97b2742 --- /dev/null +++ b/frontend/src/config/app.ts @@ -0,0 +1,27 @@ +/** + * Configuration centrale de l'application. + * + * Le nom du produit n'est defini QU'ICI (et surchargeable a l'execution via + * APP_NAME cote backend, expose par /api/config). Aucun autre fichier ne doit + * ecrire le nom en dur. + */ +export const APP_CONFIG = { + /** Nom par defaut, utilise tant que /api/config n'a pas repondu. */ + name: 'OpenMind', + tagline: 'Vos cartes mentales, dans votre navigateur. Sans compte, sans limite.', + /** Extension du format natif d'echange. */ + fileExtension: '.mmap', + fileMimeType: 'application/json', + /** Limites par defaut, alignees sur celles du backend. */ + maxNodes: 5000, + maxMapSizeMb: 10, + /** Delai d'inactivite avant sauvegarde automatique dans IndexedDB. */ + autosaveDelayMs: 700, + /** Nombre maximum d'etats conserves pour undo/redo. */ + historyLimit: 100, + /** Prefixe des liens publics de partage. */ + sharePathPrefix: '/s/', + repositoryUrl: 'https://github.com/', +} as const; + +export type AppConfig = typeof APP_CONFIG; diff --git a/frontend/src/db/database.ts b/frontend/src/db/database.ts new file mode 100644 index 0000000..e7f2ce3 --- /dev/null +++ b/frontend/src/db/database.ts @@ -0,0 +1,37 @@ +import Dexie, { type Table } from 'dexie'; +import { type MindMap, type ShareRecord } from '@/types/map'; + +/** Cle/valeur pour les drapeaux locaux (premier lancement, preferences...). */ +export interface MetaEntry { + key: string; + value: unknown; +} + +/** + * Base locale du navigateur. + * + * Trois tables seulement : + * - `maps` : les cartes privees, qui ne quittent jamais le navigateur ; + * - `shares` : les jetons de partage (dont le management token secret) ; + * - `meta` : quelques drapeaux applicatifs. + */ +export class OpenMindDatabase extends Dexie { + maps!: Table; + shares!: Table; + meta!: Table; + + constructor(name = 'openmind') { + super(name); + this.version(1).stores({ + maps: 'id, updatedAt, name', + shares: 'mapId, shareToken', + meta: 'key', + }); + } +} + +export const db = new OpenMindDatabase(); + +export const META_KEYS = { + sampleSeeded: 'sample-map-seeded', +} as const; diff --git a/frontend/src/features/editor/EditorPage.tsx b/frontend/src/features/editor/EditorPage.tsx new file mode 100644 index 0000000..51762f1 --- /dev/null +++ b/frontend/src/features/editor/EditorPage.tsx @@ -0,0 +1,124 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import { Button } from '@/components/Button'; +import { SpinnerIcon } from '@/components/Icons'; +import { EditorToolbar } from '@/features/editor/EditorToolbar'; +import { MindMapCanvas } from '@/features/editor/MindMapCanvas'; +import { PropertiesPanel } from '@/features/editor/PropertiesPanel'; +import { SearchPanel } from '@/features/editor/SearchPanel'; +import { useAutosave } from '@/features/editor/useAutosave'; +import { useEditorShortcuts } from '@/features/editor/useEditorShortcuts'; +import { ShareDialog } from '@/features/sharing/ShareDialog'; +import { getMap } from '@/services/mapRepository'; +import { useEditorStore } from '@/stores/editorStore'; +import { useUiStore } from '@/stores/uiStore'; +import { navigate } from '@/hooks/useRoute'; + +export interface EditorPageProps { + mapId: string; +} + +function EditorPageContent({ mapId }: EditorPageProps) { + const loadMap = useEditorStore((state) => state.loadMap); + const closeMap = useEditorStore((state) => state.closeMap); + const requestFit = useEditorStore((state) => state.requestFit); + const mapName = useEditorStore((state) => state.map?.name); + + const propertiesPanelOpen = useUiStore((state) => state.propertiesPanelOpen); + const searchPanelOpen = useUiStore((state) => state.searchPanelOpen); + const setSearchPanelOpen = useUiStore((state) => state.setSearchPanelOpen); + const pushToast = useUiStore((state) => state.pushToast); + + const [status, setStatus] = useState<'loading' | 'ready' | 'missing'>('loading'); + const [shareOpen, setShareOpen] = useState(false); + const searchInputRef = useRef(null); + + const { saveNow } = useAutosave(status === 'ready'); + + useEffect(() => { + let cancelled = false; + setStatus('loading'); + + void getMap(mapId).then((map) => { + if (cancelled) return; + if (!map) { + setStatus('missing'); + return; + } + loadMap(map); + setStatus('ready'); + }); + + return () => { + cancelled = true; + closeMap(); + }; + }, [mapId, loadMap, closeMap]); + + const openSearch = useCallback(() => { + setSearchPanelOpen(true); + window.setTimeout(() => searchInputRef.current?.focus(), 30); + }, [setSearchPanelOpen]); + + useEditorShortcuts(status === 'ready', { + onSave: () => { + void saveNow().then(() => pushToast('Carte sauvegardee localement.', 'success')); + }, + onFit: requestFit, + onFocusSearch: openSearch, + }); + + if (status === 'loading') { + return ( +
+ + Ouverture de la carte... +
+ ); + } + + if (status === 'missing') { + return ( +
+

+ Impossible de charger cette carte. +

+

+ Elle a peut-etre ete supprimee, ou elle a ete creee dans un autre navigateur : les cartes + restent stockees localement. +

+ +
+ ); + } + + return ( +
+ setShareOpen(true)} + onToggleSearch={() => (searchPanelOpen ? setSearchPanelOpen(false) : openSearch())} + /> + +
+
+

{mapName}

+ + {searchPanelOpen ? : null} +
+ {propertiesPanelOpen ? : null} +
+ + setShareOpen(false)} /> +
+ ); +} + +export function EditorPage(props: EditorPageProps) { + return ( + + + + ); +} diff --git a/frontend/src/features/editor/EditorToolbar.tsx b/frontend/src/features/editor/EditorToolbar.tsx new file mode 100644 index 0000000..a7b8aec --- /dev/null +++ b/frontend/src/features/editor/EditorToolbar.tsx @@ -0,0 +1,249 @@ +import { useEffect, useState } from 'react'; +import { useReactFlow } from '@xyflow/react'; +import { type BranchStyle } from '@/types/map'; +import { Button } from '@/components/Button'; +import { IconButton } from '@/components/IconButton'; +import { Menu, MenuItem, MenuLabel, MenuSeparator } from '@/components/Menu'; +import { + ArrowLeftIcon, + ChevronDownIcon, + FitIcon, + GridIcon, + LayoutIcon, + MoonIcon, + PaletteIcon, + PlusIcon, + RedoIcon, + SearchIcon, + ShareIcon, + SlidersIcon, + SunIcon, + TrashIcon, + UndoIcon, + ZoomInIcon, + ZoomOutIcon, +} from '@/components/Icons'; +import { ExportMenu } from '@/features/export/ExportMenu'; +import { LAYOUT_OPTIONS } from '@/features/editor/layout'; +import { THEMES } from '@/features/editor/themes'; +import { selectCanRedo, selectCanUndo, useEditorStore } from '@/stores/editorStore'; +import { useUiStore } from '@/stores/uiStore'; +import { navigate } from '@/hooks/useRoute'; + +const BRANCH_STYLES: Array<{ id: BranchStyle; label: string }> = [ + { id: 'bezier', label: 'Bezier' }, + { id: 'curved', label: 'Courbe' }, + { id: 'straight', label: 'Droite' }, +]; + +const SAVE_LABELS = { + idle: '', + dirty: 'Modifications non enregistrees', + saving: 'Sauvegarde...', + saved: 'Sauvegarde', + error: 'Sauvegarde impossible', +} as const; + +export interface EditorToolbarProps { + onOpenShare: () => void; + onToggleSearch: () => void; +} + +export function EditorToolbar({ onOpenShare, onToggleSearch }: EditorToolbarProps) { + const map = useEditorStore((state) => state.map); + const saveStatus = useEditorStore((state) => state.saveStatus); + const canUndo = useEditorStore(selectCanUndo); + const canRedo = useEditorStore(selectCanRedo); + const selectedIds = useEditorStore((state) => state.selectedNodeIds); + const undo = useEditorStore((state) => state.undo); + const redo = useEditorStore((state) => state.redo); + const addChild = useEditorStore((state) => state.addChild); + const deleteNodes = useEditorStore((state) => state.deleteNodes); + const applyAutoLayout = useEditorStore((state) => state.applyAutoLayout); + const updateSettings = useEditorStore((state) => state.updateSettings); + const renameMap = useEditorStore((state) => state.renameMap); + const requestFit = useEditorStore((state) => state.requestFit); + + const colorMode = useUiStore((state) => state.colorMode); + const toggleColorMode = useUiStore((state) => state.toggleColorMode); + const propertiesPanelOpen = useUiStore((state) => state.propertiesPanelOpen); + const setPropertiesPanelOpen = useUiStore((state) => state.setPropertiesPanelOpen); + + const { zoomIn, zoomOut } = useReactFlow(); + const [name, setName] = useState(map?.name ?? ''); + + useEffect(() => setName(map?.name ?? ''), [map?.id, map?.name]); + + if (!map) return null; + + const commitName = () => { + const trimmed = name.trim(); + if (!trimmed) { + setName(map.name); + return; + } + if (trimmed !== map.name) renameMap(trimmed); + }; + + return ( +
+ } + onClick={() => navigate('/')} + /> + + setName(event.target.value)} + onBlur={commitName} + onKeyDown={(event) => { + if (event.key === 'Enter') event.currentTarget.blur(); + }} + aria-label="Nom de la carte" + className="min-w-0 max-w-[16rem] flex-1 truncate rounded-md border border-transparent bg-transparent px-2 py-1 text-sm font-semibold text-zinc-900 hover:border-zinc-200 focus:border-indigo-400 dark:text-zinc-50 dark:hover:border-zinc-700" + /> + + + {SAVE_LABELS[saveStatus]} + + +
+ + } disabled={!canUndo} onClick={undo} /> + } disabled={!canRedo} onClick={redo} /> + } + onClick={() => addChild()} + /> + } + tone="danger" + disabled={selectedIds.length === 0} + onClick={() => deleteNodes(selectedIds)} + /> + +
+ + + + Organisation automatique + + + } + > + {(close) => ( + <> + Disposition + {LAYOUT_OPTIONS.map((option) => ( + { + applyAutoLayout(option.id); + close(); + }} + /> + ))} + + { + applyAutoLayout(); + close(); + }} + /> + + )} + + + + + Theme + + + } + > + {() => ( + <> + Theme de la carte +
+ {THEMES.map((theme) => ( + updateSettings({ theme: theme.id })} + /> + ))} +
+ + Style des branches + {BRANCH_STYLES.map((branch) => ( + updateSettings({ branchStyle: branch.id })} + /> + ))} + + } + label={map.settings.showGrid ? 'Masquer la grille' : 'Afficher la grille'} + onSelect={() => updateSettings({ showGrid: !map.settings.showGrid })} + /> + + )} +
+ +
+ } onClick={() => zoomIn()} /> + } onClick={() => zoomOut()} /> + } onClick={requestFit} /> + } + onClick={onToggleSearch} + /> + } + active={propertiesPanelOpen} + onClick={() => setPropertiesPanelOpen(!propertiesPanelOpen)} + /> + : } + onClick={toggleColorMode} + /> + +
+ + + +
+
+ ); +} diff --git a/frontend/src/features/editor/MindMapCanvas.tsx b/frontend/src/features/editor/MindMapCanvas.tsx new file mode 100644 index 0000000..8e309d0 --- /dev/null +++ b/frontend/src/features/editor/MindMapCanvas.tsx @@ -0,0 +1,219 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { + Background, + BackgroundVariant, + Controls, + MiniMap, + ReactFlow, + useReactFlow, + type Connection, + type Edge, + type EdgeChange, + type NodeChange, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { type MindFlowNode, toFlowEdges, toFlowNodes } from '@/features/editor/flowAdapters'; +import { MindNodeView } from '@/features/editor/MindNodeView'; +import { getTheme } from '@/features/editor/themes'; +import { useEditorStore } from '@/stores/editorStore'; +import { useUiStore } from '@/stores/uiStore'; +import { resolveNodeSize } from '@/utils/nodeSize'; + +const NODE_TYPES = { mind: MindNodeView }; + +export interface MindMapCanvasProps { + readOnly?: boolean; + showMiniMap?: boolean; + showControls?: boolean; +} + +export function MindMapCanvas({ + readOnly = false, + showMiniMap = true, + showControls = true, +}: MindMapCanvasProps) { + const map = useEditorStore((state) => state.map); + const selectedNodeIds = useEditorStore((state) => state.selectedNodeIds); + const searchResults = useEditorStore((state) => state.searchResults); + const editingNodeId = useEditorStore((state) => state.editingNodeId); + const focusRequest = useEditorStore((state) => state.focusRequest); + const fitRequest = useEditorStore((state) => state.fitRequest); + const sizeVersion = useEditorStore((state) => state.sizeVersion); + + const pushToast = useUiStore((state) => state.pushToast); + const { fitView, setCenter, screenToFlowPosition } = useReactFlow(); + const initialisedFor = useRef(null); + + const theme = useMemo(() => getTheme(map?.settings.theme), [map?.settings.theme]); + + const adapterOptions = useMemo( + () => ({ + theme, + selectedIds: selectedNodeIds, + searchResults, + editingNodeId, + readOnly, + measuredSizes: useEditorStore.getState().measuredSizes, + }), + // sizeVersion force le recalcul quand une taille de noeud a change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [theme, selectedNodeIds, searchResults, editingNodeId, readOnly, sizeVersion], + ); + + const nodes = useMemo(() => (map ? toFlowNodes(map, adapterOptions) : []), [map, adapterOptions]); + const edges = useMemo(() => (map ? toFlowEdges(map, adapterOptions) : []), [map, adapterOptions]); + + // Cadrage initial a l'ouverture d'une carte. + useEffect(() => { + if (!map || initialisedFor.current === map.id) return undefined; + initialisedFor.current = map.id; + const timer = window.setTimeout(() => fitView({ padding: 0.25, duration: 0 }), 60); + return () => window.clearTimeout(timer); + }, [map, fitView]); + + useEffect(() => { + if (fitRequest === 0) return undefined; + const timer = window.setTimeout(() => fitView({ padding: 0.25, duration: 300 }), 30); + return () => window.clearTimeout(timer); + }, [fitRequest, fitView]); + + // Centrage sur un resultat de recherche. + useEffect(() => { + if (!focusRequest || !map) return; + const target = map.nodes.find((node) => node.id === focusRequest.nodeId); + if (!target) return; + const size = resolveNodeSize(target, useEditorStore.getState().measuredSizes); + setCenter(target.position.x + size.width / 2, target.position.y + size.height / 2, { + zoom: 1.15, + duration: 400, + }); + }, [focusRequest, map, setCenter]); + + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + const store = useEditorStore.getState(); + const positions: Array<{ id: string; position: { x: number; y: number } }> = []; + let selection: Set | null = null; + + for (const change of changes) { + if (change.type === 'position' && change.position) { + positions.push({ id: change.id, position: change.position }); + } else if (change.type === 'select') { + selection ??= new Set(store.selectedNodeIds); + if (change.selected) selection.add(change.id); + else selection.delete(change.id); + } else if (change.type === 'dimensions' && change.dimensions) { + store.setMeasuredSize(change.id, { + width: Math.round(change.dimensions.width), + height: Math.round(change.dimensions.height), + }); + } else if (change.type === 'remove' && !readOnly) { + store.deleteNodes([change.id]); + } + } + + if (positions.length > 0 && !readOnly) store.moveNodes(positions); + if (selection) store.setSelection([...selection]); + }, + [readOnly], + ); + + const onEdgesChange = useCallback( + (changes: EdgeChange[]) => { + if (readOnly) return; + const store = useEditorStore.getState(); + for (const change of changes) { + if (change.type !== 'remove') continue; + const edge = store.map?.edges.find((candidate) => candidate.id === change.id); + if (edge) store.detachNode(edge.target); + } + }, + [readOnly], + ); + + const onConnect = useCallback( + (connection: Connection) => { + if (readOnly || !connection.source || !connection.target) return; + const linked = useEditorStore.getState().reparentNode(connection.target, connection.source); + if (!linked) { + pushToast('Impossible de relier ces noeuds : cela creerait une boucle.', 'error'); + } + }, + [readOnly, pushToast], + ); + + const handlePaneDoubleClick = useCallback( + (event: React.MouseEvent) => { + if (readOnly) return; + const target = event.target as HTMLElement; + if (!target.classList.contains('react-flow__pane')) return; + + const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); + const store = useEditorStore.getState(); + const created = store.addChild(null); + if (created) store.moveNodes([{ id: created, position }]); + }, + [readOnly, screenToFlowPosition], + ); + + if (!map) return null; + + return ( +
+ useEditorStore.getState().pushHistory()} + onMoveEnd={(_event, viewport) => useEditorStore.getState().setViewport(viewport)} + onPaneClick={() => { + const store = useEditorStore.getState(); + store.setSelection([]); + store.setEditingNode(null); + }} + defaultViewport={map.viewport} + minZoom={0.1} + maxZoom={3} + multiSelectionKeyCode={['Shift', 'Meta', 'Control']} + selectionKeyCode="Shift" + deleteKeyCode={null} + nodesDraggable={!readOnly} + nodesConnectable={!readOnly} + elementsSelectable + edgesFocusable={false} + // React Flow donne sinon le focus au noeud selectionne, ce qui coupe + // l'edition en ligne ; Tab est deja utilise comme raccourci. + nodesFocusable={false} + panOnScroll + selectionOnDrag={!readOnly} + zoomOnDoubleClick={false} + aria-label="Canvas de la carte mentale" + > + {map.settings.showGrid ? ( + + ) : null} + {showControls ? ( + + ) : null} + {showMiniMap ? ( + (node.data as MindFlowNode['data']).appearance.background} + maskColor={theme.isDark ? 'rgba(15,23,42,0.6)' : 'rgba(148,163,184,0.25)'} + style={{ background: theme.isDark ? '#0f172a' : '#ffffff' }} + /> + ) : null} + +
+ ); +} diff --git a/frontend/src/features/editor/MindNodeView.tsx b/frontend/src/features/editor/MindNodeView.tsx new file mode 100644 index 0000000..f9b1049 --- /dev/null +++ b/frontend/src/features/editor/MindNodeView.tsx @@ -0,0 +1,200 @@ +import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { Handle, type NodeProps } from '@xyflow/react'; +import { type MindFlowNode } from '@/features/editor/flowAdapters'; +import { useEditorStore } from '@/stores/editorStore'; +import { LinkIcon, NoteIcon } from '@/components/Icons'; +import { cn } from '@/utils/cn'; + +/** + * Rendu d'un noeud. + * + * Le texte est toujours rendu comme du texte : aucun HTML provenant d'une carte + * n'est jamais interprete. + */ +function MindNodeViewComponent({ data, id }: NodeProps) { + const { node, appearance, childCount, hiddenCount, isSearchHit, readOnly, isEditing } = data; + + const updateNodeText = useEditorStore((state) => state.updateNodeText); + const setEditingNode = useEditorStore((state) => state.setEditingNode); + const toggleCollapse = useEditorStore((state) => state.toggleCollapse); + const setMeasuredSize = useEditorStore((state) => state.setMeasuredSize); + + const containerRef = useRef(null); + const textareaRef = useRef(null); + const [draft, setDraft] = useState(node.text); + + useEffect(() => { + if (isEditing) setDraft(node.text); + }, [isEditing, node.text]); + + useEffect(() => { + if (!isEditing) return undefined; + + const focusTextarea = () => { + const textarea = textareaRef.current; + if (!textarea || document.activeElement === textarea) return; + textarea.focus(); + textarea.select(); + }; + + focusTextarea(); + // Seconde tentative apres le rendu du canvas, au cas ou React Flow + // reprendrait le focus sur le noeud selectionne. + const timer = window.setTimeout(focusTextarea, 0); + return () => window.clearTimeout(timer); + }, [isEditing]); + + // Les tailles reelles alimentent l'auto-layout et les exports. + useLayoutEffect(() => { + const element = containerRef.current; + if (!element) return; + setMeasuredSize(id, { + width: Math.round(element.offsetWidth), + height: Math.round(element.offsetHeight), + }); + }, [id, node.text, node.style, node.icon, setMeasuredSize, isEditing]); + + const commitText = () => { + const value = draft.trim(); + if (value !== node.text) updateNodeText(id, value); + setEditingNode(null); + }; + + const cancelEdit = () => { + setDraft(node.text); + setEditingNode(null); + }; + + const isRoot = node.type === 'root'; + const label = node.text || (isRoot ? 'Racine sans titre' : 'Noeud sans titre'); + + return ( +
{ + if (readOnly) return; + event.stopPropagation(); + setEditingNode(id); + }} + > + + +
+ {node.icon ? ( + + ) : null} + + {isEditing ? ( +