commit 0b2e5e326ded06f65c4062c1c25dc12f0db8827d Author: snoopy Date: Tue Sep 8 22:19:18 2026 +0200 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 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 0000000..ea37e11 Binary files /dev/null and b/frontend/public/icons/icon-192.png differ diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000..46936b8 Binary files /dev/null and b/frontend/public/icons/icon-512.png differ diff --git a/frontend/public/icons/icon-maskable-512.png b/frontend/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..76e18ae Binary files /dev/null and b/frontend/public/icons/icon-maskable-512.png differ 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 ? ( +