diff --git a/.cursor/hooks/state/continual-learning-index.json b/.cursor/hooks/state/continual-learning-index.json
index 4785d36..fbcdef8 100644
--- a/.cursor/hooks/state/continual-learning-index.json
+++ b/.cursor/hooks/state/continual-learning-index.json
@@ -1,7 +1,7 @@
{
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/0c6fb2d9-1b82-4ca3-b0f4-f8373a62faca/0c6fb2d9-1b82-4ca3-b0f4-f8373a62faca.jsonl": 1778182618469,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/137b1f4b-59d9-4ce6-8d74-01f7cbae2ba7/137b1f4b-59d9-4ce6-8d74-01f7cbae2ba7.jsonl": 1778966645519,
- "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/16214191-7091-4aef-a309-f922d351d79f/16214191-7091-4aef-a309-f922d351d79f.jsonl": 1779646940751,
+ "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/16214191-7091-4aef-a309-f922d351d79f/16214191-7091-4aef-a309-f922d351d79f.jsonl": 1779909977869,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/2e0ce74c-a31e-49d8-a0d0-a8b224813533/2e0ce74c-a31e-49d8-a0d0-a8b224813533.jsonl": 1778188935902,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/38000361-5c66-4032-8e1e-ef405e843de0/38000361-5c66-4032-8e1e-ef405e843de0.jsonl": 1778968570815,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/394af47d-c5cd-4cef-bef2-2192717439f8.jsonl": 1778951280378,
@@ -15,6 +15,7 @@
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/92d73875-5939-48fb-9f68-86c88b0f2ff7/92d73875-5939-48fb-9f68-86c88b0f2ff7.jsonl": 1778966017038,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/9902a438-467f-4d57-8f43-28e7d579a95f/9902a438-467f-4d57-8f43-28e7d579a95f.jsonl": 1778839341001,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/a64d78ce-86d3-4ec8-8f79-7589ad05a62c/a64d78ce-86d3-4ec8-8f79-7589ad05a62c.jsonl": 1778846298067,
+ "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/a7a904f4-86df-4829-b77e-4beabd9b059e/a7a904f4-86df-4829-b77e-4beabd9b059e.jsonl": 1779649690323,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/af84066e-c0c2-435e-8caf-73037ebf4320/af84066e-c0c2-435e-8caf-73037ebf4320.jsonl": 1779569075175,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/b85430f3-4520-47fd-9b4b-5200ca340a36/b85430f3-4520-47fd-9b4b-5200ca340a36.jsonl": 1779039005865,
"/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/ca85061e-6af9-4250-8dc7-9c3bb4839c48/ca85061e-6af9-4250-8dc7-9c3bb4839c48.jsonl": 1778849848444,
diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json
index a68385e..3308fc2 100644
--- a/.cursor/hooks/state/continual-learning.json
+++ b/.cursor/hooks/state/continual-learning.json
@@ -1,8 +1,8 @@
{
"version": 1,
- "lastRunAtMs": 1779646930753,
- "turnsSinceLastRun": 6,
- "lastTranscriptMtimeMs": 1779646930615.2869,
- "lastProcessedGenerationId": "c2a9fd9d-5b50-42a8-8b17-e414b0be891e",
+ "lastRunAtMs": 1779909959153,
+ "turnsSinceLastRun": 4,
+ "lastTranscriptMtimeMs": 1779909958790.594,
+ "lastProcessedGenerationId": "5bd39ea3-9f17-44d1-8aed-3cc8f3673e97",
"trialStartedAtMs": null
}
diff --git a/AGENTS.md b/AGENTS.md
index 350d163..e23d0ff 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,13 +5,13 @@
- Préfère les échanges en français, avec des explications détaillées et claires (éviter le jargon flou).
- Interface : tout libellé via i18n dans les 15 fichiers `memento-note/locales/*.json` (FR et EN comme références de contenu) ; éviter le texte en dur ; traductions **contextuelles** (sens produit, pas mot à mot — ex. « connecter votre propre fournisseur ») ; libellés FR **lisibles** (éviter jargon non expliqué : « wiki », « embed », etc.) et **aide contextuelle** où l'UX l'exige ; lors d'une traduction complète, mettre à jour toutes les locales concernées ; si l'utilisateur demande seulement les **clés i18n**, ajouter les clés (souvent EN/FR) sans remplir les 15 locales — il traduit souvent avec un autre modèle.
- Base de données : **INTERDIT TOTALEMENT** de lancer `prisma db push --force-reset`, `prisma migrate reset`, `DROP TABLE`, `TRUNCATE`, `pg_restore` avec clean, ou TOUTE commande qui vide/supprime des données — MÊME SI l'utilisateur est d'accord — sans avoir d'abord : (1) dumpé la base avec `bash /home/devparsa/dev/Momento/dump-db.sh`, (2) vérifié le dump fait au moins 1Mo, (3) obtenu un "OUI" explicite de l'utilisateur. **4 incidents de perte de données documentés (14/05, 15/05 x2, 16/05). NE JAMAIS REFAIRE ÇA.**
-- Design produit : migration depuis `architectural-grid1` (base) et `architectural-grid` (prototype UI courant) ; **consulter le prototype avant toute implémentation UI** ; logique liste/carte puis contenu au clic ; parité actions liste/carte (menus « … », déplacer, génération SVG, etc.) ; contraste éditeur clair / sidebar sombre ; retirer thèmes obsolètes ; **pas de refresh/revalidation complets inutiles** (aligné prototype — mutations optimistes, pas de `revalidatePath` systématique ni resync depuis `initialNotes`) ; **Memory Echo en section inline dans l'éditeur** (pas l'ancienne modale) — similarité sur contenu **représentatif** (pas de troncature arbitraire type 200/800 car.) ; **recherche (sidebar / résultats, ex. flux « ouvrir la note ») et navigation liste des notes** (modes affichage, icônes vs initiales…) : suivre **`SearchModal` et les patterns actuels** dans `architectural-grid`, pas une approximation ou un ancien flux ; **`/insights` (insights sémantiques)** : suivre **`InsightsView.tsx` + graphe réseau associé dans le prototype** (ex. composition type `NetworkGraph.tsx`) ; **distincte de `/graph`** ; ne pas substituer par une UX « géométrique » décorative ou un regroupement par carnet hors spec prototype ; lorsque données clusters en retard ou partiellement périmées, **montrer l’état dégradé exploitable plutôt qu’une page vide** ; si l'utilisateur hésite entre variantes UX, **trancher pour le design prototype** plutôt que multiplier les toggles.
+- Design produit : migration depuis `architectural-grid1` (base) et `architectural-grid` (prototype UI courant) ; **consulter le prototype avant toute implémentation UI** ; logique liste/carte puis contenu au clic ; parité actions liste/carte (menus « … », déplacer, génération SVG, etc.) ; contraste éditeur clair / sidebar sombre ; retirer thèmes obsolètes ; **pas de refresh/revalidation complets inutiles** (aligné prototype — mutations optimistes, pas de `revalidatePath` systématique ni resync depuis `initialNotes`) ; **Memory Echo en section inline dans l'éditeur** (pas l'ancienne modale) — similarité sur contenu **représentatif** (pas de troncature arbitraire type 200/800 car.) ; **recherche (sidebar / résultats, ex. flux « ouvrir la note ») et navigation liste des notes** (modes affichage, icônes vs initiales…) : suivre **`SearchModal` et les patterns actuels** dans `architectural-grid`, pas une approximation ou un ancien flux ; **sidebar rail** (`sidebar.tsx`) : une seule icône active ; `activeView` synchronisé avec pathname et query (`/insights`, `/revision`, `/home?reminders=1`) ; panneau latéral contextuel par route (pas la liste carnets sur `/insights` ou Rappels) ; **`/insights` (insights sémantiques)** : suivre **`InsightsView.tsx` + graphe réseau associé dans le prototype** (ex. composition type `NetworkGraph.tsx`) ; **distincte de `/graph`** ; ne pas substituer par une UX « géométrique » décorative ou un regroupement par carnet hors spec prototype ; lorsque données clusters en retard ou partiellement périmées, **montrer l’état dégradé exploitable plutôt qu’une page vide** ; si l'utilisateur hésite entre variantes UX, **trancher pour le design prototype** plutôt que multiplier les toggles.
- Locale persane : dates en calendrier iranien (conversion), chiffres persans, et vérification RTL/positionnement global (app **et** extension Web Clipper) ; **Memory Echo** et recherche sémantique doivent fonctionner en persan (RTL, embeddings — pas de contournement « EN only ») ; attention à ne pas confondre un nom de carnet (ex. « Persan ») avec le libellé de langue.
- Flux Excalidraw / diagrammes générés : accès via notification en plus d'une simple redirection ; priorité à la mise en page et au texte contenu dans les formes ; proposer des modes visuels (ex. coloré vs plus austère) tout en visant un rendu proche du style Excalidraw (polices, look).
- **Interdiction d'écrire des tests** sauf demande explicite ; en CI, seul `npm run test:unit` (`tests/unit/**`) — pas `tests/migration/` ; ne jamais générer de code superflu.
- Déploiement : privilégier le chemin rapide (artifact Next.js en CI + `Dockerfile.prebuilt`) ; CI/CD très robuste (pas d'image Docker obsolète en prod, pas de migrations/schéma DB via le workflow deploy) ; éviter les rebuild Docker complets inutiles (~15 min par itération) ; **ne pas pousser un déploiement quand des features clés sont inachevées** ; ne pas insister sur le déploiement tant que le produit n'est pas fini ou meilleur.
- Authentification : priorité à l'inscription/connexion via **Google OAuth** (plutôt qu'un compte email/mot de passe) ; exiger une vraie déconnexion (invalidation session/cookies — pas de reconnexion implicite, y compris en navigation privée).
-- Priorité absolue à la qualité UX, même si l'implémentation est complexe (« je m'en fous si c'est complexe ») ; **ne jamais affirmer qu'un correctif ou une feature est fait sans vérification réelle** (app, prototype `architectural-grid`, ou test), **notamment navigation recherche/liste notes et vue `/insights` vs fichiers prototype** — l'utilisateur sanctionne fermement les fausses déclarations ; en frustration ou pour déléguer, **prévoir des prompts / briefs d'implémentation détaillés** (autre modèle ou dev), en plus des briefs outil de design.
+- Priorité absolue à la qualité UX, même si l'implémentation est complexe (« je m'en fous si c'est complexe ») ; **ne jamais affirmer qu'un correctif ou une feature est fait sans vérification réelle** (app, prototype `architectural-grid`, ou test), **notamment navigation recherche/liste notes et vue `/insights` vs fichiers prototype** — l'utilisateur sanctionne fermement les fausses déclarations ; **ouverture note liée depuis l'éditeur** (ex. bloc live « Ouvrir ») : **split peek inline** animé (`lib/note-peek-sync.ts`, `note-editor-split-peek.tsx` — panneau gauche lecture seule, note courante à droite), **pas nouvel onglet** ; **ne jamais annuler du code non commité** (`git checkout`, reset fichier) **sans demande explicite** (perte de travail documentée, ex. drag handle éditeur) ; en frustration ou pour déléguer, **prévoir des prompts / briefs d'implémentation détaillés** (autre modèle ou dev), en plus des briefs outil de design.
- Livraison : **une user story à la fois**, tester et valider avec l'utilisateur avant la suivante (pas d'auto-validation ni d'enchaînement de code non demandé) ; suivi dans `docs/user-stories.md`.
- Quand demandé, **fournir des briefs pour un outil de design externe** plutôt que produire les maquettes UX soi-même.
@@ -25,7 +25,7 @@
- Règles opérationnelles Prisma et sécurité base de données décrites dans `CLAUDE.md` à la racine du repo.
- Production : dépôt `/opt/memento` sur `192.168.1.190`, conteneur `memento-note` sur le port **3000**, URL publique **https://memento-note.com** (nginx + Cloudflare ; ancien domaine note.parsanet.org) ; `NEXTAUTH_URL` aligné sur ce domaine ; email sortant via **Resend** (`SMTP_FROM` ex. `noreply@memento-note.com`, domaine vérifié sur resend.com) ; deploy (`deploy.yaml` / `deploy-prod.sh`) **sans toucher Postgres** (pas de `postgresql-client`, pas de migrations auto en prod).
- CI/CD Gitea : `.gitea/workflows/ci.yaml` — CI sur `ubuntu-24.04`, deploy sur runner **`docker-host`** (sur le serveur) ; deploy manuel via `.gitea/workflows/deploy.yaml` ou `bash scripts/deploy-prod.sh`.
-- Migrations dans l'image prebuilt : `docker compose exec memento-note node ./node_modules/prisma/build/index.js migrate deploy` (pas `npx prisma` dans le PATH) ; helper `scripts/migrate-docker.sh`.
-- Vérification deploy : `GET /api/build-info` (SHA Git) ; comparer `127.0.0.1:3000` et le domaine Cloudflare — purger le cache si versions divergent ; 403 sur `/api/manifest` côté domaine = souvent Cloudflare, pas l'app.
+- Migrations prebuilt + vérif deploy : `docker compose exec memento-note node ./node_modules/prisma/build/index.js migrate deploy` (pas `npx prisma`) ; helper `scripts/migrate-docker.sh` ; `GET /api/build-info` (SHA Git) ; comparer `127.0.0.1:3000` et domaine Cloudflare — purger cache si versions divergent ; 403 `/api/manifest` côté domaine = souvent Cloudflare.
+- Éditeur riche : `rich-text-editor.tsx` — `immediatelyRender: false` ; activer **`shouldRerenderOnTransaction: false`** (quick win perf TipTap 2.5) ; **drag handle / menu bloc** via **`@tiptap/extension-drag-handle-react`** (spec officielle — pas de double plugin `DragHandleExtension` + composant React, pas de repositionnement maison) ; poignée dans **colonne gutter fixe** du wrapper (padding + `getReferencedVirtualElement`), pas sur le bord des listes/numéros ; CSS : **pas `opacity:0` sur `.drag-handle`** (visibilité gérée par le plugin) ; config/callbacks **stables hors composant** ; fondation blocs : `tiptap-unique-id-extension.ts` / **`data-id` persisté à la sauvegarde** (références « Copier la référence ») ; **Smart Paste** : `lib/editor/smart-paste-extension.ts` ; **peek split** note source : `lib/note-peek-sync.ts` + `note-editor-split-peek.tsx` ; epic active `docs/story-nextgen-editor.md` — priorité **PERF > NEXTGEN > UX > MOBILE > MARKDOWN**.
- Sync mutations notes entre composants : `memento-note/lib/note-change-sync.ts` (`emitNoteChange`, événement `NOTE_CHANGE_EVENT`).
-- Roadmap / écart prototype vs prod : Web Clipper — **`ClipperSimulator.tsx` = référence design uniquement** (pas de simulateur en prod) ; extension **`memento-note/extension/`** v0.3 **Side Panel** (clip page/sélection/lien ; popup Chrome se ferme au clic page — Side Panel pour la sélection) ; i18n extension **15 langues** (`_locales/`, détection locale navigateur ; script `extension/i18n/generate-translations.cjs`) ; **`host_permissions`** incl. LAN ; **URL serveur configurable en dev**, adresse prod figée en release ; cookies/session alignés avec l'instance cible ; **Flashcards IA SM-2 livrées** : `/revision`, `/api/flashcards/*`, génération depuis l'éditeur (GraduationCap) — réf. prototype `RevisionView.tsx` ; encore en gap : Living Blocks (`UniqueID` / embeds), Structured Views, graphe knowledge (`GraphKnowledgeMap.tsx`), **insights sémantiques** (`InsightsView.tsx`, **`/insights` ≠ `/graph`**) ; publication **Chrome Web Store** : icônes 16/48/128, privacy policy, `host_permissions` prod restreints vs build dev.
+- Roadmap / écart prototype vs prod : Web Clipper — **`ClipperSimulator.tsx` = référence design uniquement** (pas de simulateur en prod) ; extension **`memento-note/extension/`** v0.3 **Side Panel** (clip page/sélection/lien ; popup Chrome se ferme au clic page — Side Panel pour la sélection) ; i18n extension **15 langues** (`_locales/`, détection locale navigateur ; script `extension/i18n/generate-translations.cjs`) ; **`host_permissions`** incl. LAN ; **URL serveur configurable en dev**, adresse prod figée en release ; cookies/session alignés avec l'instance cible ; **Flashcards IA SM-2 livrées** : `/revision`, `/api/flashcards/*`, génération depuis l'éditeur (GraduationCap) — réf. prototype `RevisionView.tsx` ; **Structured Views partiellement livrées** : schéma par carnet, Table/Kanban, champs partagés et valeurs par note (`/home` + toolbar carnet) — **suivi de tâches par carnet via Kanban structuré** (pas de vue agrégée Notes/Tâches sur la home ; cases à cocher inline dans les notes) ; **Living Blocks partiellement livrées** : `data-id`, Smart Paste, nœud `liveBlock`, détacher/supprimer, peek split — **US-NEXTGEN-EDITOR** en cours (`docs/story-nextgen-editor.md`, **US-TEMPORAL reporté**) ; encore en gap : transclusion bidirectionnelle complète, graphe knowledge enrichi (`GraphKnowledgeMap.tsx`), **insights sémantiques** (`InsightsView.tsx`, **`/insights` ≠ `/graph`**) ; publication **Chrome Web Store** : icônes 16/48/128, privacy policy, `host_permissions` prod restreints vs build dev.
diff --git a/_bmad-output/implementation-artifacts/deferred-work.md b/_bmad-output/implementation-artifacts/deferred-work.md
index a80e60c..ba092cb 100644
--- a/_bmad-output/implementation-artifacts/deferred-work.md
+++ b/_bmad-output/implementation-artifacts/deferred-work.md
@@ -12,7 +12,13 @@
- **AC5 anonymousAnalytics DB sync** — La synchronisation de `anonymousAnalytics` vers `UserAISettings` via `updateAISettings()` n'a pas été implémentée. Contrainte utilisateur : zéro écriture DB en 4.1, consentement 100 % client. À implémenter dans une story ultérieure si la cohérence DB devient requise.
-## Deferred from: chart suggestions feature (2026-05-23)
+## Deferred from: unified tasks view study (2026-05-24)
+
+- **Vue agrégée Notes/Tâches (Markdown scrape)** — Retirée volontairement (option A produit) : surcharge UX, chevauchement avec vues structurées Kanban. Spec `spec-unified-tasks-view.md` abandonnée ; pas d’unification TipTap/Checklist pour cette vue.
+
+## Deferred from: US-TEMPORAL product decision (2026-05-24)
+
+- **Prédictions d'accès temporelles** — Reporté : chevauche rappels + flashcards SM-2 + Memory Echo ; heuristique prototype peu fiable ; `NoteAccessLog` non prioritaire. Voir `docs/user-stories.md` § US-TEMPORAL.
- **Build error in note-graph-view.tsx** — Variable `plainText` définie plusieurs fois (ligne 238). Fichier préexistant modifié hors de cette tâche. À corriger indépendamment.
diff --git a/_bmad-output/implementation-artifacts/spec-unified-tasks-view.md b/_bmad-output/implementation-artifacts/spec-unified-tasks-view.md
new file mode 100644
index 0000000..ad534fd
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/spec-unified-tasks-view.md
@@ -0,0 +1,92 @@
+---
+title: 'Unified Tasks View — cross-format aggregation study'
+type: 'feature'
+created: '2026-05-24'
+status: 'done' # cancelled — product decision option A (2026-05-24): removed aggregated Tasks view; use Structured Kanban instead
+context:
+ - '{project-root}/AGENTS.md'
+ - '{project-root}/memento-note/components/notes-list-views.tsx'
+ - '{project-root}/memento-note/lib/types.ts'
+---
+
+
+
+## Intent
+
+**Problem:** The home **Notes / Tasks** toggle only aggregates Markdown `- [ ]` / `- [x]` lines from `Note.content`. Rich-text TipTap task lists (HTML in `content`) and legacy `checklist` notes (`checkItems` JSON) are invisible in Tasks view, stats column, and sort-by-tasks — causing empty or misleading UX when users create to-dos via the editor or legacy data.
+
+**Approach:** Introduce a single extraction layer (`lib/tasks/`) that normalizes tasks from all three sources into a stable `UnifiedTask` model with source-specific stable IDs; update toggle/save paths to write back to the correct storage; refresh i18n to explain supported formats without Markdown-only wording.
+
+## Boundaries & Constraints
+
+**Always:** No DB schema migration in v1 unless human approves; reuse existing `content` / `checkItems` fields; toggle must use optimistic `updateNote` + `onNotePatch`; all user-facing strings via i18n (EN+FR minimum); no new tests unless explicitly requested; do not conflate Structured Views `checkbox` properties with inline note tasks.
+
+**Ask First:** Deprecate vs revive `type: checklist` (dedicated note type); one-way migration of legacy `checkItems` → Markdown or TipTap; nested TipTap task support in aggregated view; scope of 15-locale fill vs EN/FR keys only.
+
+**Never:** Treat Structured View kanban/status as Tasks view; store a parallel tasks table; rewrite entire editor serialization to JSON; run destructive DB commands; claim done without manual verification on markdown + richtext + checklist sample notes.
+
+## I/O & Edge-Case Matrix
+
+| Scenario | Input / State | Expected Output / Behavior | Error Handling |
+|----------|--------------|---------------------------|----------------|
+| Markdown tasks | `type: markdown`, content with `- [ ]` lines | Tasks listed; toggle flips `[ ]`↔`[x]` in content | Skip malformed lines |
+| TipTap tasks | `type: richtext`, HTML with `ul[data-type="taskList"]` | Tasks listed with text from `` inside `li`; toggle updates `data-checked` + checkbox | Skip items without parseable text |
+| Checklist note | `type: checklist`, `checkItems: [{id,text,checked}]` | All items listed; toggle updates matching item by `id` | Normalize legacy `done` → `checked` on read |
+| Mixed note | Markdown lines + TipTap HTML in same richtext note | Both sources extracted (dedupe by normalized text optional v2) | No double-toggle same semantic item in v1 if ambiguous |
+| Empty scope | Notebook with no extractable tasks | Empty state with format-agnostic hint | No errors |
+| Toggle race | User toggles in Tasks while note open in editor | Last write wins or editor refresh via `emitNoteChange` | Log + refetch on mismatch |
+| API update | `PUT /api/notes` with `checkItems` | Same JSON.stringify behavior as server actions | Align API with actions in same PR |
+
+
+
+## Code Map
+
+- `memento-note/components/notes-list-views.tsx` — current `extractTasksFromNotes`, `getNoteTasksStats`, `handleToggleTask`, Tasks UI
+- `memento-note/components/home-client.tsx` — Notes/Tasks toggle, passes notes into list views
+- `memento-note/lib/types.ts` — `CheckItem`, `NoteType`
+- `memento-note/lib/utils.ts` — `parseNote`, `checkItems` deserialization
+- `memento-note/components/rich-text-editor.tsx` — TipTap TaskList/TaskItem extensions
+- `memento-note/components/note-editor/note-editor-context.tsx` — save forces `checkItems: null`; dormant checklist handlers
+- `memento-note/components/note-checklist.tsx` — checklist UI (legacy card path)
+- `memento-note/components/note-card.tsx` — checklist toggle via `updateNote({ checkItems })`
+- `memento-note/app/actions/notes.ts` — canonical server update, JSON.stringify checkItems
+- `memento-note/app/api/notes/[id]/route.ts` — API checkItems inconsistency
+- `memento-note/locales/en.json`, `fr.json` — `notes.viewTasks`, `tasksEmptyHint`, etc.
+- `memento-note/lib/note-preview.ts` — excerpt may leak `[ ]` syntax
+
+## Tasks & Acceptance
+
+**Execution (proposed — after study approval):**
+- [ ] `memento-note/lib/tasks/types.ts` — define `UnifiedTask`, `TaskSource`, stable `taskKey` — single contract for UI
+- [ ] `memento-note/lib/tasks/extract-tasks.ts` — extract from markdown lines, TipTap HTML (DOMParser or regex+guard), checkItems — replaces inline regex in list views
+- [ ] `memento-note/lib/tasks/toggle-task.ts` — source-aware write-back to content or checkItems — replaces fragile lineIndex-only toggle
+- [ ] `memento-note/components/notes-list-views.tsx` — wire extract/toggle helpers; show source badge optional — keep UI, fix data layer
+- [ ] `memento-note/locales/en.json`, `fr.json` — tasksEmptyHint, tasksHeader, optional `tasksSourceMarkdown` / `tasksSourceRichText` / `tasksSourceChecklist` — honest UX
+- [ ] `memento-note/app/api/notes/[id]/route.ts` — stringify checkItems like server actions — fix API drift
+
+**Acceptance Criteria:**
+- Given a markdown note with `- [ ] Buy milk`, when user opens Tasks view for that notebook, then the task appears and toggling persists after reload.
+- Given a richtext note with TipTap task list in saved HTML, when user opens Tasks view, then tasks appear with correct checked state.
+- Given a checklist note with `checkItems`, when user opens Tasks view, then items appear and toggle updates `checkItems` not content.
+- Given empty notebook, when user opens Tasks view, then empty state text does not mention Markdown-only format.
+- Given user toggles task in aggregated view, when note is reopened, then editor reflects the same checked state.
+
+## Design Notes
+
+**Recommended architecture:** pure functions in `lib/tasks/` — no new DB columns. `UnifiedTask` fields: `taskKey`, `noteId`, `noteTitle`, `text`, `completed`, `source: 'markdown'|'tiptap'|'checklist'`, `locator` (lineIndex | domPath | checkItemId).
+
+**TipTap HTML (observed):** `ul[data-type="taskList"] > li[data-type="taskItem"][data-checked="true|false"]` with label+checkbox and content in nested `div > p`. Toggle: flip `data-checked`, sync `input:checked`, preserve HTML structure.
+
+**Checklist type decision:** AGENTS.md documents type as legacy/semi-dead. v1 should **read** existing `checkItems` in Tasks view; reviving full checklist editor is a separate story.
+
+**i18n:** Replace Markdown-only hint with product copy: tasks are collected from checkboxes in notes (lists in rich text, markdown, or checklist notes). Optional short “How it works” link/tooltip.
+
+**Out of scope v1:** Structured Views checkbox properties; cross-note deduplication; migrating all checklist notes to richtext; nested task hierarchy display.
+
+## Verification
+
+**Commands:**
+- `cd memento-note && npm run lint` — expected: no new errors in touched files
+
+**Manual checks:**
+- Create 3 notes in one notebook: markdown with `- [ ]`, richtext with TipTap todo block, import/mock checklist with checkItems — all visible in Tasks view, each toggle survives refresh.
diff --git a/architectural-grid1/.env.example b/architectural-grid1/.env.example
new file mode 100644
index 0000000..7a550fe
--- /dev/null
+++ b/architectural-grid1/.env.example
@@ -0,0 +1,9 @@
+# GEMINI_API_KEY: Required for Gemini AI API calls.
+# AI Studio automatically injects this at runtime from user secrets.
+# Users configure this via the Secrets panel in the AI Studio UI.
+GEMINI_API_KEY="MY_GEMINI_API_KEY"
+
+# APP_URL: The URL where this applet is hosted.
+# AI Studio automatically injects this at runtime with the Cloud Run service URL.
+# Used for self-referential links, OAuth callbacks, and API endpoints.
+APP_URL="MY_APP_URL"
diff --git a/architectural-grid1/.gitignore b/architectural-grid1/.gitignore
new file mode 100644
index 0000000..5a86d2a
--- /dev/null
+++ b/architectural-grid1/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+build/
+dist/
+coverage/
+.DS_Store
+*.log
+.env*
+!.env.example
diff --git a/architectural-grid1/BRAINSTORM_PROMPT.md b/architectural-grid1/BRAINSTORM_PROMPT.md
new file mode 100644
index 0000000..7746198
--- /dev/null
+++ b/architectural-grid1/BRAINSTORM_PROMPT.md
@@ -0,0 +1,24 @@
+# IA Agent Coordination Prompt: Brainstorm Wave Integration
+
+## Context
+You are tasked with continuing the development of the "Architectural Grid" application. The core feature "Wave Brainstorming" has been partially implemented with a full-stack architecture (Express + React).
+
+## Current State
+- **Backend (`server.ts`)**: Implements session management, idea generation via Gemini, and expansion logic. Stores data in memory.
+- **Frontend (`BrainstormView.tsx`)**: Manages the life cycle of a brainstorm. Integrates with a Radial D3 Canvas.
+- **Visuals (`WaveCanvas.tsx`)**: Implements a radial force-directed graph with state-aware styling (dismissed/converted).
+- **Navigation**: "Brainstorm Wave" is accessible from the Sidebar. A quick entry point exists from Note Detail view.
+
+## Your Task: Sidebar & Navigation Cleanup
+1. **Source Code Review**: Read `src/components/Sidebar.tsx`, `src/App.tsx`, and `server.ts` to understand how views are toggled.
+2. **Sidebar Links**: Ensure "Brainstorm Wave", "Semantic Network", and "Temporal Forecast" are correctly grouped and labeled in the Sidebar under a "Creative & AI" section.
+3. **Agent View Sidebar**: The user specifically requested these links to be also accessible from the "Sidebar of the Agent view". Review `src/components/AgentsView.tsx` and ensure it has consistent navigation or deep links to these advanced features.
+4. **Semantic Network & Temporal Forecast**: These views are currently placeholders. Ensure the routing and sidebar active state detection work correctly for them.
+
+## Technical Requirements
+- Maintain consistency with the **Tailwind** architectural design (concrete, paper, accent tokens).
+- Use **Lucide-React** icons (`Wind` for Brainstorm, `Share2` for Semantic Network, `Clock` for Temporal).
+- Ensure transitions between views are smooth using `motion/react`.
+
+---
+*Copy and paste this into the next AI Agent session to ensure full context transfer.*
diff --git a/architectural-grid1/README.md b/architectural-grid1/README.md
new file mode 100644
index 0000000..1e3365d
--- /dev/null
+++ b/architectural-grid1/README.md
@@ -0,0 +1,20 @@
+
+
+
+
+# Run and deploy your AI Studio app
+
+This contains everything you need to run your app locally.
+
+View your app in AI Studio: https://ai.studio/apps/b7b577c6-4d9f-44ac-8fe1-85bc3c6d6e66
+
+## Run Locally
+
+**Prerequisites:** Node.js
+
+
+1. Install dependencies:
+ `npm install`
+2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
+3. Run the app:
+ `npm run dev`
diff --git a/architectural-grid1/index.html b/architectural-grid1/index.html
new file mode 100644
index 0000000..21dfe69
--- /dev/null
+++ b/architectural-grid1/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ My Google AI Studio App
+
+
+
+
+
+
+
diff --git a/architectural-grid1/metadata.json b/architectural-grid1/metadata.json
new file mode 100644
index 0000000..e131638
--- /dev/null
+++ b/architectural-grid1/metadata.json
@@ -0,0 +1,6 @@
+{
+ "name": "Architectural Grid",
+ "description": "A minimalist notebook for architectural research and conceptual sketches, featuring a Wave Brainstorming radial canvas for AI-powered idea exploration.",
+ "requestFramePermissions": [],
+ "majorCapabilities": []
+}
diff --git a/architectural-grid1/package-lock.json b/architectural-grid1/package-lock.json
new file mode 100644
index 0000000..dab57bd
--- /dev/null
+++ b/architectural-grid1/package-lock.json
@@ -0,0 +1,5508 @@
+{
+ "name": "react-example",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "react-example",
+ "version": "0.0.0",
+ "dependencies": {
+ "@google/genai": "^1.29.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@types/d3": "^7.4.3",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.18.1",
+ "@vitejs/plugin-react": "^5.0.4",
+ "d3": "^7.9.0",
+ "dotenv": "^17.2.3",
+ "esbuild": "^0.28.0",
+ "express": "^4.21.2",
+ "lucide-react": "^0.546.0",
+ "motion": "^12.23.24",
+ "react": "^19.0.1",
+ "react-dom": "^19.0.1",
+ "uuid": "^14.0.0",
+ "vite": "^6.2.3",
+ "ws": "^8.20.1"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.21",
+ "@types/node": "^22.14.0",
+ "autoprefixer": "^10.4.21",
+ "tailwindcss": "^4.1.14",
+ "tsx": "^4.21.0",
+ "typescript": "~5.8.2",
+ "vite": "^6.2.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@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.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
+ "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
+ "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
+ "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
+ "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
+ "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
+ "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
+ "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
+ "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
+ "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
+ "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
+ "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
+ "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
+ "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
+ "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
+ "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
+ "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
+ "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
+ "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
+ "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
+ "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
+ "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google/genai": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz",
+ "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-auth-library": "^10.3.0",
+ "p-retry": "^4.6.2",
+ "protobufjs": "^7.5.4",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/sdk": "^1.25.2"
+ },
+ "peerDependenciesMeta": {
+ "@modelcontextprotocol/sdk": {
+ "optional": true
+ }
+ }
+ },
+ "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==",
+ "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==",
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz",
+ "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
+ "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
+ "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
+ "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
+ "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
+ "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
+ "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
+ "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
+ "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
+ "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
+ "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
+ "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
+ "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
+ "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
+ "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
+ "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
+ "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
+ "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
+ "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
+ "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
+ "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
+ "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
+ "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
+ "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
+ "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.21.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
+ "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-x64": "4.3.0",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.0",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.0",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.0",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
+ "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
+ "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
+ "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
+ "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
+ "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
+ "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
+ "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
+ "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz",
+ "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.0",
+ "@tailwindcss/oxide": "4.3.0",
+ "tailwindcss": "4.3.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "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==",
+ "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==",
+ "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==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "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-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "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-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "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-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "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-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "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/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.19",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
+ "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "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.2",
+ "caniuse-lite": "^1.0.30001787",
+ "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/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.29",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
+ "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "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.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001792",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
+ "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
+ "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/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "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-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "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-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "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-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "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/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.354",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz",
+ "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==",
+ "license": "ISC"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz",
+ "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
+ "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.0",
+ "@esbuild/android-arm": "0.28.0",
+ "@esbuild/android-arm64": "0.28.0",
+ "@esbuild/android-x64": "0.28.0",
+ "@esbuild/darwin-arm64": "0.28.0",
+ "@esbuild/darwin-x64": "0.28.0",
+ "@esbuild/freebsd-arm64": "0.28.0",
+ "@esbuild/freebsd-x64": "0.28.0",
+ "@esbuild/linux-arm": "0.28.0",
+ "@esbuild/linux-arm64": "0.28.0",
+ "@esbuild/linux-ia32": "0.28.0",
+ "@esbuild/linux-loong64": "0.28.0",
+ "@esbuild/linux-mips64el": "0.28.0",
+ "@esbuild/linux-ppc64": "0.28.0",
+ "@esbuild/linux-riscv64": "0.28.0",
+ "@esbuild/linux-s390x": "0.28.0",
+ "@esbuild/linux-x64": "0.28.0",
+ "@esbuild/netbsd-arm64": "0.28.0",
+ "@esbuild/netbsd-x64": "0.28.0",
+ "@esbuild/openbsd-arm64": "0.28.0",
+ "@esbuild/openbsd-x64": "0.28.0",
+ "@esbuild/openharmony-arm64": "0.28.0",
+ "@esbuild/sunos-x64": "0.28.0",
+ "@esbuild/win32-arm64": "0.28.0",
+ "@esbuild/win32-ia32": "0.28.0",
+ "@esbuild/win32-x64": "0.28.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/framer-motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.38.0",
+ "motion-utils": "^12.36.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "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==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gaxios": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz",
+ "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "node-fetch": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
+ "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^7.0.0",
+ "google-logging-utils": "^1.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "10.6.2",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
+ "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^7.1.4",
+ "gcp-metadata": "8.1.2",
+ "google-logging-utils": "1.1.3",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/google-logging-utils": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
+ "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "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==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.546.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz",
+ "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
+ "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.38.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.36.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.36.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "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/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "license": "MIT"
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "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/protobufjs": {
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz",
+ "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.1",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.6"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "devOptional": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
+ "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.3",
+ "@rollup/rollup-android-arm64": "4.60.3",
+ "@rollup/rollup-darwin-arm64": "4.60.3",
+ "@rollup/rollup-darwin-x64": "4.60.3",
+ "@rollup/rollup-freebsd-arm64": "4.60.3",
+ "@rollup/rollup-freebsd-x64": "4.60.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.3",
+ "@rollup/rollup-linux-arm64-musl": "4.60.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.3",
+ "@rollup/rollup-linux-loong64-musl": "4.60.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.3",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-musl": "4.60.3",
+ "@rollup/rollup-openbsd-x64": "4.60.3",
+ "@rollup/rollup-openharmony-arm64": "4.60.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.3",
+ "@rollup/rollup-win32-x64-gnu": "4.60.3",
+ "@rollup/rollup-win32-x64-msvc": "4.60.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "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/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
+ "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "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/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "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/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/architectural-grid1/package.json b/architectural-grid1/package.json
new file mode 100644
index 0000000..87ebc6f
--- /dev/null
+++ b/architectural-grid1/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "react-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "tsx server.ts",
+ "build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
+ "start": "node dist/server.cjs",
+ "clean": "rm -rf dist",
+ "lint": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@google/genai": "^1.29.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@types/d3": "^7.4.3",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.18.1",
+ "@vitejs/plugin-react": "^5.0.4",
+ "d3": "^7.9.0",
+ "dotenv": "^17.2.3",
+ "esbuild": "^0.28.0",
+ "express": "^4.21.2",
+ "lucide-react": "^0.546.0",
+ "motion": "^12.23.24",
+ "react": "^19.0.1",
+ "react-dom": "^19.0.1",
+ "uuid": "^14.0.0",
+ "vite": "^6.2.3",
+ "ws": "^8.20.1"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.21",
+ "@types/node": "^22.14.0",
+ "autoprefixer": "^10.4.21",
+ "tailwindcss": "^4.1.14",
+ "tsx": "^4.21.0",
+ "typescript": "~5.8.2",
+ "vite": "^6.2.3"
+ }
+}
diff --git a/architectural-grid1/server.ts b/architectural-grid1/server.ts
new file mode 100644
index 0000000..d1b46e5
--- /dev/null
+++ b/architectural-grid1/server.ts
@@ -0,0 +1,193 @@
+import express from "express";
+import path from "path";
+import { createServer as createViteServer } from "vite";
+import { v4 as uuidv4 } from "uuid";
+import { WebSocketServer, WebSocket } from "ws";
+import { createServer } from "http";
+
+interface BrainstormIdea {
+ id: string;
+ sessionId: string;
+ waveNumber: number;
+ title: string;
+ description: string;
+ connectionToSeed: string;
+ noveltyScore: number;
+ parentIdeaId?: string;
+ convertedToNoteId?: string;
+ status: 'active' | 'dismissed' | 'converted';
+ position?: { x: number; y: number };
+}
+
+interface BrainstormSession {
+ id: string;
+ seedIdea: string;
+ sourceNoteId?: string;
+ contextNoteIds?: string[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+// In-memory store
+const sessions: BrainstormSession[] = [];
+const ideas: BrainstormIdea[] = [];
+
+async function startServer() {
+ const app = express();
+ const server = createServer(app);
+ const wss = new WebSocketServer({ server });
+ const PORT = 3000;
+
+ app.use(express.json());
+
+ // WebSocket logic
+ const rooms = new Map>();
+ const roomUsers = new Map>();
+
+ wss.on('connection', (ws) => {
+ let currentRoom: string | null = null;
+
+ ws.on('message', (message) => {
+ const data = JSON.parse(message.toString());
+
+ if (data.type === 'join') {
+ const { sessionId, user } = data;
+ currentRoom = sessionId;
+
+ if (!rooms.has(sessionId)) rooms.set(sessionId, new Set());
+ if (!roomUsers.has(sessionId)) roomUsers.set(sessionId, new Map());
+
+ rooms.get(sessionId)!.add(ws);
+ roomUsers.get(sessionId)!.set(ws, user || { id: uuidv4(), name: 'Guest' });
+
+ // Broadcast presence to the room
+ const usersInRoom = Array.from(roomUsers.get(sessionId)!.values());
+ rooms.get(sessionId)!.forEach(client => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(JSON.stringify({ type: 'presence', users: usersInRoom }));
+ }
+ });
+
+ console.log(`User ${user?.name || 'Guest'} joined session: ${sessionId}`);
+ }
+
+ if (data.type === 'idea_added' || data.type === 'idea_updated' || data.type === 'activity' || data.type === 'living_block_update') {
+ if (currentRoom && rooms.has(currentRoom)) {
+ rooms.get(currentRoom)!.forEach(client => {
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
+ client.send(JSON.stringify(data));
+ }
+ });
+ }
+ }
+ });
+
+ ws.on('close', () => {
+ if (currentRoom && rooms.has(currentRoom)) {
+ rooms.get(currentRoom)!.delete(ws);
+ roomUsers.get(currentRoom)!.delete(ws);
+
+ // Update presence
+ if (rooms.get(currentRoom)!.size > 0) {
+ const usersInRoom = Array.from(roomUsers.get(currentRoom)!.values());
+ rooms.get(currentRoom)!.forEach(client => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(JSON.stringify({ type: 'presence', users: usersInRoom }));
+ }
+ });
+ } else {
+ rooms.delete(currentRoom);
+ roomUsers.delete(currentRoom);
+ }
+ }
+ });
+ });
+
+ // API Routes
+ app.get("/api/health", (req, res) => {
+ res.json({ status: "ok" });
+ });
+
+// 1. Create session
+ app.post("/api/brainstorm/sessions", (req, res) => {
+ const { seedIdea, sourceNoteId, contextNoteIds } = req.body;
+
+ const session: BrainstormSession = {
+ id: uuidv4(),
+ seedIdea,
+ sourceNoteId,
+ contextNoteIds,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ };
+ sessions.unshift(session);
+ res.json(session);
+ });
+
+ // 2. Add ideas to session
+ app.post("/api/brainstorm/:sessionId/ideas", (req, res) => {
+ const { sessionId } = req.params;
+ const { ideas: newIdeasData } = req.body;
+
+ const session = sessions.find(s => s.id === sessionId);
+ if (!session) return res.status(404).json({ error: "Session not found" });
+
+ const newIdeas = newIdeasData.map((item: any) => ({
+ id: item.id || uuidv4(),
+ sessionId,
+ waveNumber: item.waveNumber,
+ title: item.title,
+ description: item.description,
+ connectionToSeed: item.connectionToSeed,
+ noveltyScore: item.noveltyScore,
+ parentIdeaId: item.parentIdeaId,
+ status: 'active'
+ }));
+
+ newIdeas.forEach((i: any) => ideas.push(i));
+ res.json(newIdeas);
+ });
+
+ // 3. Get all sessions
+ app.get("/api/brainstorm/sessions", (req, res) => {
+ res.json(sessions);
+ });
+
+ // 4. Get session with ideas
+ app.get("/api/brainstorm/:sessionId", (req, res) => {
+ const session = sessions.find(s => s.id === req.params.sessionId);
+ if (!session) return res.status(404).json({ error: "Session not found" });
+ const sessionIdeas = ideas.filter(i => i.sessionId === session.id);
+ res.json({ session, ideas: sessionIdeas });
+ });
+
+ // 5. Update idea (position, status)
+ app.patch("/api/brainstorm/ideas/:ideaId", (req, res) => {
+ const index = ideas.findIndex(i => i.id === req.params.ideaId);
+ if (index === -1) return res.status(404).json({ error: "Idea not found" });
+
+ ideas[index] = { ...ideas[index], ...req.body };
+ res.json(ideas[index]);
+ });
+
+ // Vite middleware for development
+ if (process.env.NODE_ENV !== "production") {
+ const vite = await createViteServer({
+ server: { middlewareMode: true },
+ appType: "spa",
+ });
+ app.use(vite.middlewares);
+ } else {
+ const distPath = path.join(process.cwd(), 'dist');
+ app.use(express.static(distPath));
+ app.get('*', (req, res) => {
+ res.sendFile(path.join(distPath, 'index.html'));
+ });
+ }
+
+ server.listen(PORT, "0.0.0.0", () => {
+ console.log(`Server running on http://localhost:${PORT}`);
+ });
+}
+
+startServer();
diff --git a/architectural-grid1/src/App.tsx b/architectural-grid1/src/App.tsx
new file mode 100644
index 0000000..2159eb8
--- /dev/null
+++ b/architectural-grid1/src/App.tsx
@@ -0,0 +1,1185 @@
+/**
+ * @license
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useMemo } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import { X } from 'lucide-react';
+
+// Components
+import { Sidebar } from './components/Sidebar';
+import { NotebooksView } from './components/NotebooksView';
+import { AgentsView } from './components/AgentsView';
+import { SettingsView } from './components/SettingsView';
+import { TrashView } from './components/TrashView';
+import { BrainstormView } from './components/BrainstormView/BrainstormView';
+import { InsightsView } from './components/InsightsView';
+import { TemporalView } from './components/TemporalView';
+import { AISidebar } from './components/AISidebar';
+import { SlashMenu } from './components/SlashMenu';
+import { LandingPage } from './components/LandingPage';
+import { LandingPageV2 } from './components/LandingPageV2';
+import { LandingPageV3 } from './components/LandingPageV3';
+import { AuthPage } from './components/AuthPage';
+import { SearchModal } from './components/SearchModal';
+import { ClipperSimulator } from './components/ClipperSimulator';
+import { GraphKnowledgeMap } from './components/GraphKnowledgeMap';
+import { RevisionView } from './components/RevisionView';
+
+// Data & Services
+import { CARNETS, ALL_NOTES } from './constants';
+import { generateFlashcardsForNote } from './services/geminiService';
+import { NavigationView, SettingsTab, AITab, AITone, Carnet, Note, BrainstormIdea, NoteAccessLog, Flashcard } from './types';
+
+export default function App() {
+ const [showLanding, setShowLanding] = useState(() => {
+ // Check if user has already "entered" the app once in this session
+ return !sessionStorage.getItem('momento-entered');
+ });
+ const [landingVersion, setLandingVersion] = useState<'v1' | 'v2' | 'v3'>('v1');
+ const [showAuth, setShowAuth] = useState(false);
+ const [authMode, setAuthMode] = useState<'login' | 'register'>('login');
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [activeView, setActiveView] = useState('notebooks');
+ const [activeSettingsTab, setActiveSettingsTab] = useState('general');
+ const [selectedAgentId, setSelectedAgentId] = useState(null);
+ const [isDarkMode, setIsDarkMode] = useState(false);
+ const [carnets, setCarnets] = useState(CARNETS);
+ const [notes, setNotes] = useState(ALL_NOTES);
+ const [accessLogs, setAccessLogs] = useState([
+ // Note n1: 14-day cycle
+ { noteId: 'n1', accessedAt: new Date(Date.now() - 70 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n1', accessedAt: new Date(Date.now() - 56 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n1', accessedAt: new Date(Date.now() - 42 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n1', accessedAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n1', accessedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n1', accessedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+
+ // Note n2: 7-day cycle
+ { noteId: 'n2', accessedAt: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n2', accessedAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n2', accessedAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n2', accessedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n2', accessedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n2', accessedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+
+ // Note n3: 3-day cycle (frequent check)
+ { noteId: 'n3', accessedAt: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n3', accessedAt: new Date(Date.now() - 9 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n3', accessedAt: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n3', accessedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ { noteId: 'n3', accessedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
+ ]);
+
+ const logNoteAccess = (noteId: string, action: 'view' | 'edit' | 'search_hit' = 'view') => {
+ const newLog: NoteAccessLog = {
+ noteId,
+ accessedAt: new Date().toISOString(),
+ action
+ };
+ setAccessLogs(prev => [...prev, newLog]);
+ };
+
+ const [activeCarnetId, setActiveCarnetId] = useState('4');
+ const [activeNoteId, setActiveNoteId] = useState(null);
+ const [brainstormSeed, setBrainstormSeed] = useState(null);
+ const [accentColor, setAccentColor] = useState(() => {
+ return localStorage.getItem('momento-accent-color') || '#A47148';
+ });
+
+ // Flashcards state with beautiful architectural starter deck
+ const [flashcards, setFlashcards] = useState(() => {
+ const stored = localStorage.getItem('momento-flashcards');
+ if (stored) {
+ try {
+ return JSON.parse(stored);
+ } catch (e) {
+ console.error("Failed to parse stored flashcards:", e);
+ }
+ }
+ // Beautiful default seeds for 'n1' (Grid Systems & Geometry)
+ const SEEDS: Flashcard[] = [
+ {
+ id: 'f1',
+ noteId: 'n1',
+ question: 'Quel est l’intérêt primordial des trames géométriques en conception spatiale ?',
+ answer: 'Elles structurent l’espace bâti en créant un sens d\'ordre, de rythme, et d\'harmonie de proportions esthétiques dans l\'environnement, facilitant la lisibilité de la structure.',
+ intervalDays: 1,
+ nextReviewDate: new Date().toISOString(), // Due today
+ easeFactor: 2.5,
+ mastered: false
+ },
+ {
+ id: 'f2',
+ noteId: 'n1',
+ question: 'En quoi l’approche dynamique paramétrique déforme-t-elle les grilles de construction traditionnelles ?',
+ answer: 'Par l\'utilisation d\'algorithmes mathématiques de déformation réactifs à des ensembles de données environnementales pour créer des géométries fluides mais structurellement ordonnées.',
+ intervalDays: 3,
+ nextReviewDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 3).toISOString(), // Due soon
+ easeFactor: 2.5,
+ mastered: true
+ },
+ {
+ id: 'f3',
+ noteId: 'n1',
+ question: 'Quelle est la particularité de l’intégration de la lumière comme matériau d’espace ?',
+ answer: 'La soustraction du superflu permet aux reflets et à la diffraction lumineuse de créer des profondeurs visuelles changeantes sans surcharger l\'aménagement matériel.',
+ intervalDays: 1,
+ nextReviewDate: new Date().toISOString(), // Due today
+ easeFactor: 2.4,
+ mastered: false
+ }
+ ];
+ return SEEDS;
+ });
+
+ const [isGeneratingFlashcards, setIsGeneratingFlashcards] = useState(false);
+ const [activeReviewDeckId, setActiveReviewDeckId] = useState(null);
+ const [toast, setToast] = useState<{ show: boolean; message: string }>({ show: false, message: '' });
+
+ React.useEffect(() => {
+ document.documentElement.style.setProperty('--color-accent', accentColor);
+ localStorage.setItem('momento-accent-color', accentColor);
+ }, [accentColor]);
+
+ const handleBrainstormNote = (note: Note) => {
+ setActiveView('brainstorm');
+ // We'll use a small delay or a ref to pass this to BrainstormView if needed,
+ // but better to just share state or use a CustomEvent
+ window.dispatchEvent(new CustomEvent('start-brainstorm', {
+ detail: { seed: note.title, sourceNoteId: note.id }
+ }));
+ };
+
+ const handleGenerateFlashcards = async (noteId: string) => {
+ const targetNote = notes.find(n => n.id === noteId);
+ if (!targetNote) return;
+
+ setIsGeneratingFlashcards(true);
+ try {
+ const rawContent = targetNote.content || "";
+ const generated = await generateFlashcardsForNote(targetNote.title, rawContent);
+
+ if (generated && generated.length > 0) {
+ const mappedCards: Flashcard[] = generated.map((c, i) => ({
+ id: `f-${noteId}-${Date.now()}-${i}`,
+ noteId,
+ question: c.question,
+ answer: c.answer,
+ intervalDays: 1,
+ nextReviewDate: new Date().toISOString(),
+ easeFactor: 2.5,
+ mastered: false
+ }));
+
+ const updatedSet = [...flashcards.filter(fc => fc.noteId !== noteId), ...mappedCards];
+ setFlashcards(updatedSet);
+ localStorage.setItem('momento-flashcards', JSON.stringify(updatedSet));
+
+ setToast({
+ show: true,
+ message: `${mappedCards.length} flashcards créées avec succès pour "${targetNote.title}".`
+ });
+ setTimeout(() => setToast(t => ({ ...t, show: false })), 4000);
+ } else {
+ setToast({
+ show: true,
+ message: "Impossible d'extraire des flashcards exploitables à partir du texte existant."
+ });
+ setTimeout(() => setToast(t => ({ ...t, show: false })), 4000);
+ }
+ } catch (err) {
+ console.error("AI flashcards expansion failed:", err);
+ setToast({
+ show: true,
+ message: "Une erreur est survenue lors de la génération avec Gemini."
+ });
+ setTimeout(() => setToast(t => ({ ...t, show: false })), 4000);
+ } finally {
+ setIsGeneratingFlashcards(false);
+ }
+ };
+
+ React.useEffect(() => {
+ if (activeNoteId) {
+ logNoteAccess(activeNoteId);
+ }
+ }, [activeNoteId]);
+
+ React.useEffect(() => {
+ // Check for session in URL
+ const params = new URLSearchParams(window.location.search);
+ const session = params.get('session');
+ if (session) {
+ setActiveView('brainstorm');
+ // We pass it via a global property or custom event since BrainstormView will fetch sessions
+ (window as any).initialSessionId = session;
+ }
+
+ const handleSwitchView = (e: any) => {
+ if (e.detail) {
+ setActiveView(e.detail as NavigationView);
+ }
+ };
+
+ const handleGlobalShortcut = (e: KeyboardEvent) => {
+ // Trigger advanced search with Ctrl+F or Cmd+F or Ctrl+P or Cmd+P
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'p')) {
+ e.preventDefault();
+ setIsSearchOpen(true);
+ }
+ };
+
+ const handleToggleClipper = () => {
+ setIsClipperSimulatorOpen(prev => !prev);
+ };
+
+ window.addEventListener('switch-view', handleSwitchView);
+ window.addEventListener('keydown', handleGlobalShortcut);
+ window.addEventListener('toggle-clipper-simulator', handleToggleClipper);
+ return () => {
+ window.removeEventListener('switch-view', handleSwitchView);
+ window.removeEventListener('keydown', handleGlobalShortcut);
+ window.removeEventListener('toggle-clipper-simulator', handleToggleClipper);
+ };
+ }, []);
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
+ const [isClipperSimulatorOpen, setIsClipperSimulatorOpen] = useState(false);
+ const [clipperToast, setClipperToast] = useState<{ id: string; title: string; noteId: string } | null>(null);
+ const [selectedTagIds, setSelectedTagIds] = useState([]);
+ const [isAISidebarOpen, setIsAISidebarOpen] = useState(false);
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+ const [aiTab, setAiTab] = useState('discussion');
+ const [selectedTone, setSelectedTone] = useState('Professional');
+
+ // Modal States
+ const [showNewCarnetModal, setShowNewCarnetModal] = useState<{ isOpen: boolean; parentId?: string; isRenaming?: boolean; carnetId?: string }>({ isOpen: false });
+ const [showNewNoteModal, setShowNewNoteModal] = useState(false);
+ const [slashMenu, setSlashMenu] = useState<{ isOpen: boolean; top: number; left: number } | null>(null);
+
+ // Form States
+ const [newCarnetName, setNewCarnetName] = useState('');
+ const [newNoteTitle, setNewNoteTitle] = useState('');
+ const [newNoteContent, setNewNoteContent] = useState('');
+
+ const handleEditorKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === '/') {
+ const selection = window.getSelection();
+ if (selection && selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0);
+ const rect = range.getBoundingClientRect();
+ setSlashMenu({
+ isOpen: true,
+ top: rect.bottom + window.scrollY,
+ left: rect.left + window.scrollX
+ });
+ }
+ }
+ };
+
+ React.useEffect(() => {
+ if (clipperToast) {
+ const timer = setTimeout(() => {
+ setClipperToast(null);
+ }, 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [clipperToast]);
+
+ const handleOpenClippedNote = (noteId: string) => {
+ setActiveNoteId(noteId);
+ const found = notes.find(n => n.id === noteId);
+ if (found) {
+ setActiveCarnetId(found.carnetId);
+ }
+ setActiveView('notebooks');
+ };
+
+ const togglePin = (noteId: string) => {
+ setNotes(notes.map(n => n.id === noteId ? { ...n, isPinned: !n.isPinned } : n));
+ };
+
+ const filteredNotes = useMemo(() => {
+ let result = notes.filter(n => n.carnetId === activeCarnetId && !n.isDeleted);
+
+ if (selectedTagIds.length > 0) {
+ result = result.filter(note =>
+ selectedTagIds.every(tagId => note.tags?.some(tag => tag.id === tagId))
+ );
+ }
+
+ return [...result].sort((a, b) => {
+ if (a.isPinned && !b.isPinned) return -1;
+ if (!a.isPinned && b.isPinned) return 1;
+ return 0;
+ });
+ }, [activeCarnetId, notes]);
+
+ const activeNote = useMemo(() =>
+ notes.find(n => n.id === activeNoteId),
+ [activeNoteId, notes]);
+
+ const activeCarnet = useMemo(() =>
+ carnets.find(c => c.id === activeCarnetId),
+ [activeCarnetId, carnets]);
+
+ const handleAddCarnet = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newCarnetName.trim()) return;
+
+ if (showNewCarnetModal.isRenaming && showNewCarnetModal.carnetId) {
+ setCarnets(carnets.map(c => c.id === showNewCarnetModal.carnetId ? { ...c, name: newCarnetName, initial: newCarnetName.charAt(0).toUpperCase() } : c));
+ setShowNewCarnetModal({ isOpen: false });
+ setNewCarnetName('');
+ return;
+ }
+
+ const newCarnet: Carnet = {
+ id: Date.now().toString(),
+ name: newCarnetName,
+ initial: newCarnetName.charAt(0).toUpperCase(),
+ type: 'Project',
+ parentId: showNewCarnetModal.parentId
+ };
+
+ setCarnets([...carnets, newCarnet]);
+ setNewCarnetName('');
+ setShowNewCarnetModal({ isOpen: false });
+ setActiveCarnetId(newCarnet.id);
+ };
+
+ const handleDeleteCarnet = (id: string) => {
+ if (window.confirm('Déplacer ce carnet et ses sous-carnets vers la corbeille ?')) {
+ const idsToDelete = new Set([id]);
+
+ const addChildren = (parentId: string) => {
+ carnets.forEach(c => {
+ if (c.parentId === parentId) {
+ idsToDelete.add(c.id);
+ addChildren(c.id);
+ }
+ });
+ };
+ addChildren(id);
+
+ const deletedAt = new Date().toISOString();
+ setCarnets(carnets.map(c => idsToDelete.has(c.id) ? { ...c, isDeleted: true, deletedAt } : c));
+ setNotes(notes.map(n => idsToDelete.has(n.carnetId) ? { ...n, isDeleted: true, deletedAt } : n));
+
+ if (idsToDelete.has(activeCarnetId)) {
+ setActiveCarnetId('1');
+ }
+ }
+ };
+
+ const handleDeleteNote = (id: string) => {
+ const deletedAt = new Date().toISOString();
+ setNotes(notes.map(n => n.id === id ? { ...n, isDeleted: true, deletedAt } : n));
+ if (activeNoteId === id) setActiveNoteId(null);
+ };
+
+ const handleRestoreCarnet = (id: string) => {
+ setCarnets(carnets.map(c => c.id === id ? { ...c, isDeleted: false, deletedAt: undefined } : c));
+ // Optionally restore linked notes too? User might expect that.
+ setNotes(notes.map(n => n.carnetId === id ? { ...n, isDeleted: false, deletedAt: undefined } : n));
+ };
+
+ const handleRestoreNote = (id: string) => {
+ setNotes(notes.map(n => n.id === id ? { ...n, isDeleted: false, deletedAt: undefined } : n));
+ };
+
+ const handlePermanentDeleteNote = (id: string) => {
+ setNotes(notes.filter(n => n.id !== id));
+ };
+
+ const handlePermanentDeleteCarnet = (id: string) => {
+ const idsToDelete = new Set([id]);
+ const addChildren = (parentId: string) => {
+ carnets.forEach(c => {
+ if (c.parentId === parentId) {
+ idsToDelete.add(c.id);
+ addChildren(c.id);
+ }
+ });
+ };
+ addChildren(id);
+ setCarnets(carnets.filter(c => !idsToDelete.has(c.id)));
+ setNotes(notes.filter(n => !idsToDelete.has(n.carnetId)));
+ };
+
+ const handleAddNote = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newNoteTitle.trim() || !newNoteContent.trim()) return;
+
+ const newNote: Note = {
+ id: `n-${Date.now()}`,
+ carnetId: activeCarnetId,
+ title: newNoteTitle,
+ date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
+ content: newNoteContent,
+ imageUrl: 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: []
+ };
+
+ setNotes([newNote, ...notes]);
+ setNewNoteTitle('');
+ setNewNoteContent('');
+ setShowNewNoteModal(false);
+ setActiveNoteId(newNote.id);
+ };
+
+ const handleConvertIdeaToNote = (idea: BrainstormIdea) => {
+ const newNote: Note = {
+ id: `n-gen-${Date.now()}`,
+ carnetId: activeCarnetId,
+ title: idea.title,
+ date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
+ content: `${idea.description}\n\n---\n**Connection to seed:** ${idea.connectionToSeed}\n**Novelty Score:** ${idea.noveltyScore}/10`,
+ imageUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [{ id: 't-ai', label: 'AI Generated', type: 'ai' }]
+ };
+
+ setNotes([newNote, ...notes]);
+ setActiveView('notebooks');
+ setActiveNoteId(newNote.id);
+ };
+
+ const handleUpdateNote = (updatedNote: Note) => {
+ setNotes(prevNotes => {
+ const existing = prevNotes.find(n => n.id === updatedNote.id);
+ if (existing && updatedNote.isVersioningEnabled !== false) {
+ const hasContentChanged = existing.content !== updatedNote.content;
+ const hasTitleChanged = existing.title !== updatedNote.title;
+
+ if (hasContentChanged || hasTitleChanged) {
+ const history = existing.versionHistory || [];
+ const lastSnapshot = history[0];
+ const isIdentical = lastSnapshot && lastSnapshot.content === existing.content && lastSnapshot.title === existing.title;
+
+ if (!isIdentical) {
+ const newSnapshot = {
+ id: 'v-' + Date.now(),
+ title: existing.title,
+ content: existing.content,
+ timestamp: new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) + ' • ' + new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
+ size: existing.content.length
+ };
+ const updatedWithHistory = {
+ ...updatedNote,
+ versionHistory: [newSnapshot, ...history]
+ };
+ return prevNotes.map(n => n.id === updatedNote.id ? updatedWithHistory : n);
+ }
+ }
+ }
+ return prevNotes.map(n => n.id === updatedNote.id ? updatedNote : n);
+ });
+ };
+
+ // WebSocket Integration for Living Blocks
+ const [wsConnected, setWsConnected] = React.useState(true);
+ const [simulatedOffline, setSimulatedOffline] = React.useState(false);
+ const socketRef = React.useRef(null);
+
+ const initWebSocket = () => {
+ if (simulatedOffline) return;
+ try {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const socket = new WebSocket(`${protocol}//${window.location.host}`);
+ socketRef.current = socket;
+
+ socket.onopen = () => {
+ setWsConnected(true);
+ socket.send(JSON.stringify({
+ type: 'join',
+ sessionId: 'global-momento-session',
+ user: { id: 'u-1', name: 'Utilisateur Actuel' }
+ }));
+ };
+
+ socket.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data.type === 'living_block_update') {
+ const { sourceNoteId, blockIndex, newText } = data;
+ setNotes(prevNotes =>
+ prevNotes.map(note => {
+ if (note.id === sourceNoteId) {
+ const paragraphs = note.content.split('\n');
+ if (paragraphs[blockIndex] !== undefined) {
+ paragraphs[blockIndex] = newText;
+ return { ...note, content: paragraphs.join('\n') };
+ }
+ }
+ return note;
+ })
+ );
+ window.dispatchEvent(new CustomEvent('living-block-pulse', {
+ detail: { sourceNoteId, blockIndex }
+ }));
+ }
+ } catch (err) {
+ console.error("Error parsing message", err);
+ }
+ };
+
+ socket.onclose = () => {
+ setWsConnected(false);
+ setTimeout(() => {
+ if (socketRef.current === socket && !simulatedOffline) {
+ initWebSocket();
+ }
+ }, 4000);
+ };
+
+ socket.onerror = () => {
+ setWsConnected(false);
+ };
+ } catch (e) {
+ console.error(e);
+ setWsConnected(false);
+ }
+ };
+
+ React.useEffect(() => {
+ initWebSocket();
+ return () => {
+ if (socketRef.current) {
+ socketRef.current.close();
+ }
+ };
+ }, [simulatedOffline]);
+
+ React.useEffect(() => {
+ const handleToggleSimulate = () => {
+ setSimulatedOffline(prev => {
+ const next = !prev;
+ if (next) {
+ if (socketRef.current) {
+ socketRef.current.close();
+ }
+ setWsConnected(false);
+ } else {
+ // Reconnect will automatically trigger because simulatedOffline changes
+ }
+ return next;
+ });
+ };
+ window.addEventListener('toggle-websocket-simulate', handleToggleSimulate);
+ return () => {
+ window.removeEventListener('toggle-websocket-simulate', handleToggleSimulate);
+ };
+ }, []);
+
+ const broadcastLivingBlockUpdate = (sourceNoteId: string, blockIndex: number, newText: string) => {
+ if (socketRef.current?.readyState === WebSocket.OPEN && !simulatedOffline) {
+ socketRef.current.send(JSON.stringify({
+ type: 'living_block_update',
+ sourceNoteId,
+ blockIndex,
+ newText
+ }));
+ }
+ };
+ const handleEnterApp = () => {
+ if (isAuthenticated) {
+ setShowLanding(false);
+ sessionStorage.setItem('momento-entered', 'true');
+ } else {
+ setAuthMode('register');
+ setShowAuth(true);
+ }
+ };
+
+ const handleAuthComplete = () => {
+ setIsAuthenticated(true);
+ setShowAuth(false);
+ setShowLanding(false);
+ sessionStorage.setItem('momento-entered', 'true');
+ };
+
+ const handleLogout = () => {
+ setIsAuthenticated(false);
+ setShowLanding(true);
+ sessionStorage.removeItem('momento-entered');
+ };
+
+ return (
+
+ {showLanding && !showAuth ? (
+
+ {landingVersion === 'v1' ? (
+ { setAuthMode('login'); setShowAuth(true); }}
+ onRegister={() => { setAuthMode('register'); setShowAuth(true); }}
+ onSwitchVersion={setLandingVersion}
+ />
+ ) : landingVersion === 'v2' ? (
+ { setAuthMode('login'); setShowAuth(true); }}
+ onRegister={() => { setAuthMode('register'); setShowAuth(true); }}
+ onSwitchVersion={setLandingVersion}
+ />
+ ) : (
+ { setAuthMode('login'); setShowAuth(true); }}
+ onRegister={() => { setAuthMode('register'); setShowAuth(true); }}
+ onSwitchVersion={setLandingVersion}
+ />
+ )}
+
+ ) : showAuth ? (
+
+ setShowAuth(false)}
+ />
+
+ ) : (
+
+ setShowLanding(true)}
+ onLogout={handleLogout}
+ carnets={carnets}
+ notes={notes}
+ activeCarnetId={activeCarnetId}
+ activeNoteId={activeNoteId}
+ setActiveCarnetId={setActiveCarnetId}
+ setActiveNoteId={setActiveNoteId}
+ flashcards={flashcards}
+ onSelectReviewDeck={(noteId) => {
+ setActiveReviewDeckId(noteId);
+ setActiveView('revision');
+ }}
+ setShowNewCarnetModal={(show, parentId, isRenaming, carnetId) => {
+ setShowNewCarnetModal({ isOpen: show, parentId, isRenaming, carnetId });
+ if (isRenaming && carnetId) {
+ const carnet = carnets.find(c => c.id === carnetId);
+ if (carnet) setNewCarnetName(carnet.name);
+ } else {
+ setNewCarnetName('');
+ }
+ }}
+ onDeleteCarnet={handleDeleteCarnet}
+ onMoveCarnet={(draggedId, targetId) => {
+ if (draggedId === targetId) return;
+
+ // Basic circular check
+ const isDescendant = (parentId: string, potentialChildId: string): boolean => {
+ const childIds = carnets.filter(c => c.parentId === parentId).map(c => c.id);
+ if (childIds.includes(potentialChildId)) return true;
+ return childIds.some(id => isDescendant(id, potentialChildId));
+ };
+
+ if (targetId && isDescendant(draggedId, targetId)) {
+ console.warn("Cannot move a notebook inside its own descendant");
+ return;
+ }
+
+ setCarnets(prev => prev.map(c => c.id === draggedId ? { ...c, parentId: targetId } : c));
+ }}
+ />
+
+
+
+ {(activeView === 'notebooks' || activeView === 'shared' || activeView === 'reminders') && (
+
+ setShowNewCarnetModal({ isOpen: show, parentId, isRenaming, carnetId })}
+ onDeleteNote={handleDeleteNote}
+ onBrainstormNote={handleBrainstormNote}
+ onUpdateNote={handleUpdateNote}
+ onOpenSidebar={() => setIsSidebarOpen(true)}
+ onSearchClick={() => setIsSearchOpen(true)}
+ wsConnected={wsConnected}
+ broadcastLivingBlockUpdate={broadcastLivingBlockUpdate}
+ carnets={carnets}
+ flashcards={flashcards}
+ onTriggerReviewDeck={(noteId) => {
+ setActiveReviewDeckId(noteId);
+ setActiveView('revision');
+ }}
+ onGenerateFlashcards={handleGenerateFlashcards}
+ isGeneratingFlashcards={isGeneratingFlashcards}
+ />
+
+ )}
+
+ {activeView === 'trash' && (
+
+ n.isDeleted)}
+ deletedCarnets={carnets.filter(c => c.isDeleted)}
+ onRestoreNote={handleRestoreNote}
+ onRestoreCarnet={handleRestoreCarnet}
+ onPermanentDeleteNote={handlePermanentDeleteNote}
+ onPermanentDeleteCarnet={handlePermanentDeleteCarnet}
+ onEmptyTrash={() => {
+ setNotes(notes.filter(n => !n.isDeleted));
+ setCarnets(carnets.filter(c => !c.isDeleted));
+ }}
+ onOpenSidebar={() => setIsSidebarOpen(true)}
+ />
+
+ )}
+
+ {activeView === 'agents' && (
+
+ setNotes([note, ...notes])}
+ onOpenSidebar={() => setIsSidebarOpen(true)}
+ />
+
+ )}
+
+ {activeView === 'settings' && (
+
+ setIsSidebarOpen(true)}
+ />
+
+ )}
+
+ {activeView === 'brainstorm' && (
+
+
+
+ )}
+
+ {activeView === 'insights' && (
+
+ {
+ setActiveView('notebooks');
+ setActiveNoteId(noteId);
+ }}
+ onOpenSidebar={() => setIsSidebarOpen(true)}
+ />
+
+ )}
+
+ {activeView === 'temporal' && (
+
+ {
+ setActiveView('notebooks');
+ setActiveNoteId(noteId);
+ }}
+ />
+
+ )}
+
+ {activeView === 'graph' && (
+
+ {
+ setActiveView('notebooks');
+ setActiveNoteId(noteId);
+ const note = notes.find(n => n.id === noteId);
+ if (note) {
+ setActiveCarnetId(note.carnetId);
+ }
+ }}
+ onClose={() => setActiveView('notebooks')}
+ />
+
+ )}
+
+ {activeView === 'revision' && (
+
+ {
+ setFlashcards(updated);
+ localStorage.setItem('momento-flashcards', JSON.stringify(updated));
+ }}
+ onSelectNote={(noteId) => {
+ setActiveView('notebooks');
+ setActiveNoteId(noteId);
+ const note = notes.find(n => n.id === noteId);
+ if (note) {
+ setActiveCarnetId(note.carnetId);
+ }
+ }}
+ onOpenSidebar={() => setIsSidebarOpen(true)}
+ initialActiveDeckId={activeReviewDeckId}
+ onClearActiveDeckId={() => setActiveReviewDeckId(null)}
+ />
+
+ )}
+
+
+
+
+
+ {/* Modals */}
+
+ {showNewCarnetModal.isOpen && (
+
+
setShowNewCarnetModal({ isOpen: false })}
+ className="absolute inset-0 bg-ink/40 backdrop-blur-sm"
+ />
+
+
+ {showNewCarnetModal.isRenaming ? 'Rename Carnet' : (showNewCarnetModal.parentId ? 'Create Sub-Carnet' : 'Create New Carnet')}
+
+ {showNewCarnetModal.parentId && !showNewCarnetModal.isRenaming && (
+
+ Inside: {carnets.find(c => c.id === showNewCarnetModal.parentId)?.name}
+
+ )}
+
+
+
+ )}
+
+ {showNewNoteModal && (
+
+
setShowNewNoteModal(false)}
+ className="absolute inset-0 bg-ink/40 backdrop-blur-sm"
+ />
+
+
+ {slashMenu?.isOpen && (
+ { console.log(type); setSlashMenu(null); }}
+ onClose={() => setSlashMenu(null)}
+ />
+ )}
+
+ Add Architectural Note
+
+
+
+ )}
+
+ {isSearchOpen && (
+ setIsSearchOpen(false)}
+ notes={notes}
+ carnets={carnets}
+ onSelectNote={(noteId) => {
+ setActiveNoteId(noteId);
+ const searchHitNote = notes.find(n => n.id === noteId);
+ if (searchHitNote) {
+ setActiveCarnetId(searchHitNote.carnetId);
+ }
+ }}
+ />
+ )}
+
+ {isClipperSimulatorOpen && (
+ setIsClipperSimulatorOpen(false)}
+ carnets={carnets}
+ activeCarnetId={activeCarnetId}
+ onAddNote={(newNote) => setNotes(prevNotes => [newNote, ...prevNotes])}
+ onTriggerToast={(title, noteId) => {
+ setClipperToast({
+ id: String(Date.now()),
+ title,
+ noteId
+ });
+ }}
+ />
+ )}
+
+ {clipperToast && (
+
+
+
+
+ Note clippée — {clipperToast.title}
+
+
+
+
+ {
+ handleOpenClippedNote(clipperToast.noteId);
+ setClipperToast(null);
+ }}
+ className="text-xs font-bold uppercase tracking-wider text-cyan-400 hover:text-cyan-300 underline underline-offset-2 transition-colors"
+ >
+ Voir
+
+ setClipperToast(null)}
+ className="p-1 hover:bg-white/10 text-neutral-400 hover:text-white rounded-lg transition-colors"
+ >
+
+
+
+
+ )}
+
+ {toast.show && (
+
+
+
+
+ {toast.message}
+
+
+
+
+ {
+ setActiveView('revision');
+ setToast({ show: false, message: '' });
+ }}
+ className="text-xs font-bold uppercase tracking-wider text-accent hover:text-accent/80 underline underline-offset-2 transition-colors cursor-pointer"
+ >
+ Voir
+
+ setToast({ show: false, message: '' })}
+ className="p-1 hover:bg-white/10 text-neutral-400 hover:text-white rounded-lg transition-colors cursor-pointer"
+ >
+
+
+
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/architectural-grid1/src/components/AISidebar.tsx b/architectural-grid1/src/components/AISidebar.tsx
new file mode 100644
index 0000000..c750035
--- /dev/null
+++ b/architectural-grid1/src/components/AISidebar.tsx
@@ -0,0 +1,816 @@
+import React from 'react';
+import {
+ Sparkles,
+ ChevronRight,
+ MessageSquare,
+ FileCode,
+ Globe,
+ Send,
+ Scissors,
+ Zap,
+ Languages,
+ Layout,
+ ArrowRightLeft,
+ BookOpen,
+ History,
+ Target,
+ Network,
+ Clock,
+ AlertCircle
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { AITab, AITone, Note, Carnet } from '../types';
+import { HierarchicalCarnetSelector } from './HierarchicalCarnetSelector';
+
+interface AISidebarProps {
+ isOpen: boolean;
+ setIsOpen: (open: boolean) => void;
+ activeNote: Note | undefined;
+ aiTab: AITab;
+ setAiTab: (tab: AITab) => void;
+ selectedTone: AITone;
+ setSelectedTone: (tone: AITone) => void;
+ carnets: Carnet[];
+ notes?: Note[];
+ onOpenNote?: (noteId: string) => void;
+ onUpdateNote?: (note: Note) => void;
+}
+
+export const AISidebar: React.FC = ({
+ isOpen,
+ setIsOpen,
+ activeNote,
+ aiTab,
+ setAiTab,
+ selectedTone,
+ setSelectedTone,
+ carnets,
+ notes = [],
+ onOpenNote = (_noteId: string) => {},
+ onUpdateNote
+}) => {
+ const [selectedContextId, setSelectedContextId] = React.useState(null);
+ const [hoveredOrbitNode, setHoveredOrbitNode] = React.useState(null);
+
+ const explicitWikiLinks = React.useMemo(() => [
+ { source: 'n1', target: 'n1-b' },
+ { source: 'n3', target: 'n3-b' },
+ { source: 'bridge-1', target: 'n1' },
+ { source: 'bridge-1', target: 'n2' },
+ ], []);
+
+ const CARNET_COLOR_PALETTE: { [key: string]: string } = {
+ '1': '#D97706', // Daily Notes - Warm Amber
+ '2': '#059669', // Project: Neo - Soft Emerald
+ '3': '#4F46E5', // Shared Docs - Rich Indigo
+ '4': '#0891B2', // Architecture Research - Clean Cyan
+ '5': '#EA580C', // History of Architecture - Deep Orange
+ '6': '#DB2777', // Modernism - Vibrant Rose
+ '7': '#65A30D', // Sustainable Design - Cool Lime
+ };
+
+ const DEFAULT_CARNET_COLOR = '#71717A';
+
+ const backlinks = React.useMemo(() => {
+ if (!activeNote || !notes) return [];
+ return notes.filter(n => {
+ if (n.id === activeNote.id || n.isDeleted) return false;
+ const isExplicit = explicitWikiLinks.some(link =>
+ (link.source === n.id && link.target === activeNote.id)
+ );
+ const isContentLink = n.content.toLowerCase().includes(`[[${activeNote.title.toLowerCase()}]]`);
+ return isExplicit || isContentLink;
+ });
+ }, [activeNote, notes, explicitWikiLinks]);
+
+ const outboundLinks = React.useMemo(() => {
+ if (!activeNote || !notes) return [];
+ return notes.filter(n => {
+ if (n.id === activeNote.id || n.isDeleted) return false;
+ const isExplicit = explicitWikiLinks.some(link =>
+ (link.source === activeNote.id && link.target === n.id)
+ );
+ const isContentLink = activeNote.content.toLowerCase().includes(`[[${n.title.toLowerCase()}]]`);
+ return isExplicit || isContentLink;
+ });
+ }, [activeNote, notes, explicitWikiLinks]);
+
+ const unlinkedMentions = React.useMemo(() => {
+ if (!activeNote || !notes) return [];
+ return notes.filter(n => {
+ if (n.id === activeNote.id || n.isDeleted) return false;
+ const isLinked = [...backlinks, ...outboundLinks].some(link => link.id === n.id);
+ if (isLinked) return false;
+ return n.content.toLowerCase().includes(activeNote.title.toLowerCase());
+ });
+ }, [activeNote, notes, backlinks, outboundLinks]);
+
+ const orbitNodes = React.useMemo(() => {
+ const list: { id: string; title: string; color: string; carnetName: string; relationship: 'backlink' | 'outbound' | 'mention' }[] = [];
+
+ backlinks.forEach(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ list.push({
+ id: n.id,
+ title: n.title,
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ carnetName: carnet?.name || 'Carnet',
+ relationship: 'backlink'
+ });
+ });
+
+ outboundLinks.forEach(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ list.push({
+ id: n.id,
+ title: n.title,
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ carnetName: carnet?.name || 'Carnet',
+ relationship: 'outbound'
+ });
+ });
+
+ unlinkedMentions.forEach(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ list.push({
+ id: n.id,
+ title: n.title,
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ carnetName: carnet?.name || 'Carnet',
+ relationship: 'mention'
+ });
+ });
+
+ return list.slice(0, 8);
+ }, [backlinks, outboundLinks, unlinkedMentions, carnets]);
+
+ const getSnippetWithHighlight = (content: string, term: string) => {
+ const index = content.toLowerCase().indexOf(term.toLowerCase());
+ if (index === -1) {
+ return {content.substring(0, 80)}... ;
+ }
+ const start = Math.max(0, index - 40);
+ const end = Math.min(content.length, index + term.length + 40);
+ const before = content.substring(start, index);
+ const match = content.substring(index, index + term.length);
+ const after = content.substring(index + term.length, end);
+ return (
+
+ {start > 0 && "..."}
+ {before}
+ {match}
+ {after}
+ {end < content.length && "..."}
+
+ );
+ };
+
+ return (
+
+ {isOpen && (
+
+
+
+
+
+ IA Assistant
+
+ setIsOpen(false)}
+ className="p-1 hover:bg-slate-100 rounded-full transition-colors text-muted-ink"
+ >
+
+
+
+
+ "{activeNote?.title}"
+
+
+
+
+ {(['discussion', 'actions', 'explore', 'resources', 'relations'] as AITab[]).map((tab) => (
+ setAiTab(tab)}
+ className={`flex-1 py-3 text-[9px] uppercase tracking-wider font-bold transition-all relative
+ ${aiTab === tab ? 'text-manganese' : 'text-muted-ink hover:text-ink/60'}`}
+ >
+ {tab === 'relations' ? 'réseau' : tab}
+ {aiTab === tab && (
+
+ )}
+
+ ))}
+
+
+
+
+ {aiTab === 'explore' && (
+
+
+
+
Intelligence Modules
+
+
+
+
+
{
+ // These will be handled in App.tsx by observing activeView
+ window.dispatchEvent(new CustomEvent('switch-view', { detail: 'brainstorm' }));
+ }}
+ className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-ochre/30 transition-all text-left overflow-hidden"
+ >
+
+
+
+
+
+
+
+
+
Brainstorm Wave
+
Unfold dimensions of thought
+
+
+
+
+
{
+ window.dispatchEvent(new CustomEvent('switch-view', { detail: 'insights' }));
+ }}
+ className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-indigo-500/30 transition-all text-left overflow-hidden"
+ >
+
+
+
+
+
+
+
+
+
Semantic Network
+
Detect clusters and bridges
+
+
+
+
+
{
+ window.dispatchEvent(new CustomEvent('switch-view', { detail: 'temporal' }));
+ }}
+ className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-rose-500/30 transition-all text-left overflow-hidden"
+ >
+
+
+
+
+
+
+
+
+
Temporal Forecast
+
Predict relevance recurrence
+
+
+
+
+
+
+
+ Ces modules utilisent les embeddings du modèle Gemini pour analyser graphiquement vos pensées.
+
+
+
+ )}
+
+ {aiTab === 'discussion' && (
+
+
+
+
+
Contexte
+
+ {(['Professional', 'Creative', 'Academic', 'Casual'] as AITone[]).map((tone) => (
+ setSelectedTone(tone)}
+ className={`w-8 h-8 rounded-lg flex items-center justify-center text-[9px] font-bold transition-all border
+ ${selectedTone === tone
+ ? 'bg-accent text-white border-accent shadow-sm'
+ : 'bg-glass border-border/40 text-muted-ink hover:border-accent/40'}`}
+ title={tone}
+ >
+ {tone.substring(0, 2)}
+
+ ))}
+
+
+
+
+
+
+
+ Note Active
+
+
Auto
+
+
+
+
+
+
+
+
+
+
+
Conversation prête. Posez votre question ci-dessous.
+
+
+
+ )}
+
+ {aiTab === 'actions' && (
+
+
+
+
+
+
+ {[
+ { icon:
, label: 'Clarifier', color: 'ochre' },
+ { icon:
, label: 'Raccourcir', color: 'rust' },
+ { icon:
, label: 'Améliorer', color: 'sage' },
+ { icon:
, label: 'Traduire', color: 'slate' },
+ ].map((action, i) => (
+
+
+ {action.icon}
+
+ {action.label}
+
+ ))}
+
+
+ Convertir en Markdown
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Présentation
+
Convertir en slides interactives
+
+
+
+
+
+ Thème
+
+ Architectural Mono
+ Vibrant Tech
+ Minimal Silk
+
+
+
+ Style
+
+ Professional
+ Creative
+ Brutalist
+
+
+
+
+
+ Générer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Diagramme
+
Visualisation de structure
+
+
+
+
+
+ Type
+
+ Logic Flow
+ Mind Map
+ Hierarchy
+
+
+
+ Style
+
+ Draft
+ Polished
+ Handwritten
+
+
+
+
+
+ Tracer
+
+
+
+
+
+
+
+
+ Auto-Save Enabled
+
+
+
+ )}
+
+ {aiTab === 'relations' && (
+
+
+
+
Vue Graphe Locale
+
+
+
+ {activeNote ? (
+ <>
+ {/* Interactive local graph SVG container */}
+
+
+
+
+
+
+ {/* Dotted circle boundary helper */}
+
+
+ {/* Connections */}
+ {orbitNodes.map((node, i) => {
+ const angle = i * (orbitNodes.length > 0 ? (2 * Math.PI) / orbitNodes.length : 0);
+ const nx = 160 + 70 * Math.cos(angle);
+ const ny = 110 + 62 * Math.sin(angle);
+ return (
+
+
+ {node.relationship === 'outbound' && (
+
+ )}
+ {node.relationship === 'backlink' && (
+
+ )}
+
+ );
+ })}
+
+ {/* Center node (Active Note) */}
+
+
+
+
+
+ {/* Orbit nodes */}
+ {orbitNodes.map((node, i) => {
+ const angle = i * (orbitNodes.length > 0 ? (2 * Math.PI) / orbitNodes.length : 0);
+ const nx = 160 + 70 * Math.cos(angle);
+ const ny = 110 + 62 * Math.sin(angle);
+ const isHovered = hoveredOrbitNode?.id === node.id;
+ return (
+ onOpenNote(node.id)}
+ onMouseEnter={() => setHoveredOrbitNode(node)}
+ onMouseLeave={() => setHoveredOrbitNode(null)}
+ >
+
+
+ {node.title.length > 10 ? node.title.substring(0, 8) + '...' : node.title}
+
+
+ );
+ })}
+
+
+ {/* Interactive local tooltip card info */}
+
+ {hoveredOrbitNode ? (
+
+
+
+
+ {hoveredOrbitNode.carnetName}
+
+
+ {hoveredOrbitNode.relationship === 'backlink' ? 'Lien Entrant' : hoveredOrbitNode.relationship === 'outbound' ? 'Lien Sortant' : 'Mention Simple'}
+
+
+
{hoveredOrbitNode.title}
+
Cliquez pour ouvrir la note
+
+ ) : (
+
+
+ Survolez un nœud, cliquez pour ouvrir
+
+ )}
+
+
+
+ {/* Lists of backlinks & unlinked mentions */}
+
+ {/* 1. Backlinks */}
+
+
+ Liens Entrans ({backlinks.length})
+
+ {backlinks.length > 0 ? (
+
+ {backlinks.map(n => (
+
onOpenNote(n.id)}
+ className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm"
+ >
+
+ {n.title}
+ Réf
+
+
+ {getSnippetWithHighlight(n.content, activeNote.title)}
+
+
+ ))}
+
+ ) : (
+
Aucun lien entrant explicite pointant vers cette note.
+ )}
+
+
+ {/* 2. Outbound Links */}
+
+
+ Liens Sortants ({outboundLinks.length})
+
+ {outboundLinks.length > 0 ? (
+
+ {outboundLinks.map(n => (
+
onOpenNote(n.id)}
+ className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm animate-fadeIn"
+ >
+
+ {n.title}
+ Cible
+
+
+ {getSnippetWithHighlight(activeNote.content, n.title)}
+
+
+ ))}
+
+ ) : (
+
Cette note ne pointe vers aucun lien sortant explicite.
+ )}
+
+
+ {/* 3. Unlinked Mentions */}
+
+
+ Mentions Simples ({unlinkedMentions.length})
+
+ {unlinkedMentions.length > 0 ? (
+
+ {unlinkedMentions.map(n => (
+
onOpenNote(n.id)}
+ className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm"
+ >
+
+ {n.title}
+ Mention
+
+
+ {getSnippetWithHighlight(n.content, activeNote.title)}
+
+
+ ))}
+
+ ) : (
+
Aucune mention textuelle non-liée trouvée dans vos autres notes.
+ )}
+
+
+ >
+ ) : (
+
+
+
Veuillez sélectionner une note pour explorer son graphe relationnel.
+
+ )}
+
+ )}
+
+ {aiTab === 'resources' && (
+
+
+
+
URL (Optionnel)
+
+
+
+
+
+
+
+ Texte de la ressource
+
+
+
+
+
Mode d'intégration
+
+ {[
+ { id: 'replace', label: 'Remplacer', sub: 'Direct, sans IA' },
+ { id: 'append', label: 'Compléter', sub: 'Ajoute sans réécrire' },
+ { id: 'merge', label: 'Fusionner', sub: 'Réécrit et intègre' },
+ ].map((mode) => (
+
+ {mode.label}
+ {mode.sub}
+
+ ))}
+
+
+
+
+
+ Générer l'aperçu
+
+
+
+ )}
+
+
+
+
+ {aiTab === 'discussion' && (
+
+
+
+
Shift+Enter for new line
+
+
+ )}
+
+
+ )}
+
+ );
+};
diff --git a/architectural-grid1/src/components/AgentsView.tsx b/architectural-grid1/src/components/AgentsView.tsx
new file mode 100644
index 0000000..8dfe628
--- /dev/null
+++ b/architectural-grid1/src/components/AgentsView.tsx
@@ -0,0 +1,396 @@
+import React from 'react';
+import {
+ Plus,
+ ArrowLeft,
+ Clock,
+ Activity,
+ Trash2,
+ Edit3,
+ Play,
+ Eye,
+ Microscope,
+ Globe,
+ Layers,
+ Zap,
+ BookOpen,
+ Sparkles,
+ ChevronDown,
+ Info,
+ Check,
+ ClipboardCheck,
+ ListTodo,
+ ArrowRight,
+ Loader2,
+ Menu
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Carnet, Note } from '../types';
+import { HierarchicalCarnetSelector } from './HierarchicalCarnetSelector';
+import { extractActionItems } from '../services/geminiService';
+import { v4 as uuidv4 } from 'uuid';
+
+interface AgentsViewProps {
+ selectedAgentId: string | null;
+ setSelectedAgentId: (id: string | null) => void;
+ carnets: Carnet[];
+ notes: Note[];
+ onAddNote?: (note: Note) => void;
+ onOpenSidebar?: () => void;
+}
+
+export const AgentsView: React.FC = ({
+ selectedAgentId,
+ setSelectedAgentId,
+ carnets,
+ notes,
+ onAddNote,
+ onOpenSidebar
+}) => {
+ const [selectedCarnetForAgent, setSelectedCarnetForAgent] = React.useState('4');
+ const [agentType, setAgentType] = React.useState<'Surveillant' | 'Personnalisé' | 'Slides' | 'Diagramme' | 'Tasks'>('Tasks');
+ const [isRunningAgent, setIsRunningAgent] = React.useState(false);
+ const [agentResult, setAgentResult] = React.useState(null);
+
+ const handleRunTaskAgent = async () => {
+ setIsRunningAgent(true);
+ setAgentResult(null);
+
+ // Get notes from carnet
+ const filteredNotes = notes.filter(n => n.carnetId === selectedCarnetForAgent && !n.isDeleted);
+
+ try {
+ const result = await extractActionItems(filteredNotes);
+ setAgentResult(result);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setIsRunningAgent(false);
+ }
+ };
+
+ const handleSaveResult = () => {
+ if (!agentResult || !onAddNote) return;
+
+ const carnetName = carnets.find(c => c.id === selectedCarnetForAgent)?.name || 'General';
+
+ const newNote: Note = {
+ id: uuidv4(),
+ carnetId: selectedCarnetForAgent || '1',
+ title: `Consolidated Actions: ${carnetName}`,
+ date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
+ content: agentResult,
+ imageUrl: 'https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [{ id: 't-tasks', label: 'Action Items', type: 'ai' }]
+ };
+
+ onAddNote(newNote);
+ alert('Note de tâches créée avec succès !');
+ setSelectedAgentId(null);
+ };
+
+ return (
+
+ {!selectedAgentId ? (
+ <>
+
+
+
+
+
+
+
+
Mes Agents
+
Automatisez vos tâches de veille et de recherche.
+
+
+
+
+ Nouvel Agent
+
+
+
+
+ {['Tous', 'Veilleur', 'Chercheur', 'Surveillant', 'Personnalisé'].map((tag, i) => (
+
+ {tag}
+ {i === 0 && }
+
+ ))}
+
+
+
+
+
+ {[
+ { id: 'a1', icon:
, title: 'Surveillant de Notes', status: 'Réussi', type: 'SURVEILLANT', meta: 'Hebdomadaire • 6 exéc.', desc: 'Analyse les notes récentes d’un carnet et suggère des compléments, références et liens.' },
+ { id: 'a2', icon:
, title: 'Chercheur de Sujet', status: 'Réussi', type: 'CHERCHEUR', meta: 'Hebdomadaire • 14 exéc.', desc: 'Recherche des informations approfondies sur les derniers modèles de Deepseek et voir l’avis des utilisateurs.' },
+ { id: 'a3', icon:
, title: 'Veille IA', status: 'Réussi', type: 'VEILLEUR', meta: 'Quotidien • 20 exéc.', desc: 'Scrape les flux RSS de 6 sites IA (The Verge, TechCrunch...) et génère un résumé.' },
+ { id: 'a4', icon:
, title: 'Action Miner', status: 'Inactif', type: 'TASKS', meta: 'À la demande', desc: 'Scan vos notes pour extraire automatiquement les tâches, assignés et deadlines.' },
+ ].map((agent, i) => (
+
setSelectedAgentId(agent.id)}
+ className="bg-white dark:bg-white/5 border border-border rounded-2xl p-6 space-y-6 hover:border-ink/20 transition-all group cursor-pointer shadow-sm relative overflow-hidden"
+ >
+
+
+
+ {agent.icon}
+
+
+
{agent.title}
+
{agent.type}
+
+
+
e.stopPropagation()}>
+
+
+
+
+
+
+
+
+ {agent.desc}
+
+
+
+
+
+ {agent.meta.split('•')[0]}
+ {agent.meta.split('•')[1]}
+
+
+
+
+ Prochaine exécution
+ Hebdomadaire
+
+
+
Dernier statut
+
{agent.status}
+
+
+
+
+
+
Modifier
+
{ e.stopPropagation(); }}
+ className="py-2 border border-border rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 flex items-center justify-center transition-colors text-muted-ink hover:text-ink"
+ >
+
+
+
{ e.stopPropagation(); }}
+ className="py-2 border border-border rounded-lg hover:bg-rose-50 hover:text-rose-600 hover:border-rose-100 flex items-center justify-center transition-colors text-muted-ink"
+ >
+
+
+
+
+ ))}
+
+
+
+
+
+ {[
+ { title: 'Veille IA', desc: 'Scrape les flux RSS de 6 sites IA et génère un résumé hebdomadaire.', icon:
},
+ { title: 'Veille Tech', desc: 'Crée un résumé quotidien des news Hacker News et Product Hunt.', icon:
},
+ { title: 'Veille Dev', desc: 'Surveille les repos GitHub pour détecter les nouvelles releases.', icon:
},
+ ].map((model, i) => (
+
+
+ {model.icon}
+
+
{model.title}
+
{model.desc}
+
+ Installer
+
+
+ ))}
+
+
+
+ >
+ ) : (
+
+
+
+
+
+
+
Sélectionnez le type d'agent
+
+ {[
+ { id: 'Surveillant', icon:
, label: 'Surveillant', desc: 'Surveille un carnet et analyse les notes' },
+ { id: 'Tasks', icon:
, label: 'Action Items', desc: 'Extrait les tâches et deadlines' },
+ { id: 'Slides', icon:
, label: 'Slides', desc: 'Crée une présentation PowerPoint à partir de notes' },
+ { id: 'Diagramme', icon:
, label: 'Diagramme', desc: 'Crée un diagramme Excalidraw à partir de notes' },
+ ].map((type) => (
+
setAgentType(type.id as any)}
+ className={`p-6 rounded-2xl border-2 transition-all flex flex-col items-center gap-3 text-center group relative
+ ${agentType === type.id ? 'border-accent bg-white shadow-xl shadow-accent/10' : 'border-border bg-white/50 hover:bg-white'}`}
+ >
+
+ {type.icon}
+
+
+
{type.label}
+
{type.desc}
+
+
+ {agentType === type.id &&
}
+
+
+ ))}
+
+
+
+
+
+
+
+ CONFIGURATION
+
+
+ Supprimer
+
+
+
+
+
+
+ DESCRIPTION (OPTIONEL)
+
+
+
+
+
+
+
+ CARNET À SURVEILLER
+
+
+
+
+
+
+
+ NOTES À ANALYSER
+
+
+
+ {[
+ 'Résumé du conteneur LXC devSandbox',
+ 'Connexion SSH sans mot de passe à devSandbox',
+ 'Gateway token (blank to generate)',
+ 'Procédure d\'accès à openclaw',
+ 'Derniers commits du repo Momento'
+ ].map((note, i) => (
+
+
+ {i === 0 && }
+
+
+ {note}
+
+ ))}
+
+
{1} note(s) sélectionnée(s)
+
+
+
+
+ TYPE DE DIAGRAMME
+
+
+ {[
+ 'Auto (détection métier)', 'Flowchart (processus)',
+ 'Mindmap (idées)', 'Organigramme (équipes)',
+ 'Timeline / roadmap', 'Process map (opérations)',
+ 'Architecture cloud (zones/RG)'
+ ].map((type, i) => (
+
+ {type}
+
+ ))}
+
+
+
+
+ {agentResult ? (
+
+
+
Résultat de l'extraction
+
+ Sauvegarder dans une note
+
+
+
+ {agentResult}
+
+
+ ) : (
+
+ {isRunningAgent ? : }
+ Lancer l'extraction d'actions
+
+ )}
+
Cet agent analysera toutes les notes du carnet sélectionné.
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/architectural-grid1/src/components/AuthPage.tsx b/architectural-grid1/src/components/AuthPage.tsx
new file mode 100644
index 0000000..711a421
--- /dev/null
+++ b/architectural-grid1/src/components/AuthPage.tsx
@@ -0,0 +1,200 @@
+import React, { useState } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Mail, Lock, User, ArrowRight, Github, Globe, Sparkles } from 'lucide-react';
+
+interface AuthPageProps {
+ onAuthComplete: () => void;
+ onBack: () => void;
+ initialMode?: 'login' | 'register';
+}
+
+export const AuthPage: React.FC = ({ onAuthComplete, onBack, initialMode = 'login' }) => {
+ const [mode, setMode] = useState<'login' | 'register'>(initialMode);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ // Simulate auth
+ setTimeout(() => {
+ setIsLoading(false);
+ onAuthComplete();
+ }, 1500);
+ };
+
+ return (
+
+ {/* Background Orbs */}
+
+
+
+ {/* Header */}
+
+
+
+
+
+ Retour
+
+
+
+
+
{/* Spacer */}
+
+
+ {/* Main Content */}
+
+
+
+
+
+
+
+ {mode === 'login' ? 'Bon retour parmi nous' : 'Créer votre espace'}
+
+
+ {mode === 'login'
+ ? 'Entrez vos identifiants pour accéder à vos notes.'
+ : 'Rejoignez la nouvelle ère de la prise de notes intelligente.'}
+
+
+
+
+ {mode === 'register' && (
+
+ )}
+
+
+
+
+
+ Mot de passe
+ {mode === 'login' && (
+ Oublié ?
+ )}
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {mode === 'login' ? 'Se connecter' : 'Créer mon compte'}
+
+ >
+ )}
+
+
+
+
+
+
+ Ou continuer avec
+
+
+
+
+
+
+ Google
+
+
+
+ GitHub
+
+
+
+
+
+ {mode === 'login' ? "Vous n'avez pas de compte ?" : "Vous avez déjà un compte ?"}
+ {' '}
+ setMode(mode === 'login' ? 'register' : 'login')}
+ className="text-accent font-bold hover:underline"
+ >
+ {mode === 'login' ? "S'inscrire" : "Se connecter"}
+
+
+
+
+
+
+
+
+ © 2024 Momento Labs — Privacy • Terms
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/BlockPicker.tsx b/architectural-grid1/src/components/BlockPicker.tsx
new file mode 100644
index 0000000..53285ad
--- /dev/null
+++ b/architectural-grid1/src/components/BlockPicker.tsx
@@ -0,0 +1,264 @@
+import React, { useState, useMemo } from 'react';
+import { motion } from 'motion/react';
+import { Search, Sparkles, Link2, X, Folder } from 'lucide-react';
+import { Note, Carnet } from '../types';
+
+interface BlockPickerProps {
+ isOpen: boolean;
+ onClose: () => void;
+ currentNote: Note | undefined;
+ allNotes: Note[];
+ carnets: Carnet[];
+ onSelectBlock: (sourceNoteId: string, blockIndex: number) => void;
+ prefilledBlock?: { noteId: string; blockIndex: number } | null;
+}
+
+export const BlockPicker: React.FC = ({
+ isOpen,
+ onClose,
+ currentNote,
+ allNotes,
+ carnets,
+ onSelectBlock,
+ prefilledBlock
+}) => {
+ const [activeTab, setActiveTab] = useState<'suggestions' | 'search'>('suggestions');
+ const [searchQuery, setSearchQuery] = useState('');
+
+ // Extract all paragraphs across notes (exlucing the current note to avoid self-embed)
+ const allBlocks = useMemo(() => {
+ const list: Array<{
+ id: string;
+ noteId: string;
+ noteTitle: string;
+ carnetName: string;
+ blockIndex: number;
+ text: string;
+ snippet: string;
+ }> = [];
+
+ allNotes.forEach(note => {
+ if (currentNote && note.id === currentNote.id) return;
+
+ const paragraphs = note.content.split('\n');
+ paragraphs.forEach((p, idx) => {
+ const text = p.trim();
+ // Skip empty lines, headings, or short snippets
+ if (text.length < 20 || text.startsWith('#') || text.startsWith('[[living-block')) return;
+
+ // Find carnet
+ const carnet = carnets.find(c => c.id === note.carnetId);
+
+ // 30-word snippet
+ const words = text.split(/\s+/);
+ const snippet = words.slice(0, 30).join(' ') + (words.length > 30 ? '...' : '');
+
+ list.push({
+ id: `${note.id}-${idx}`,
+ noteId: note.id,
+ noteTitle: note.title || 'Untitled',
+ carnetName: carnet?.name || 'Général',
+ blockIndex: idx,
+ text,
+ snippet
+ });
+ });
+ });
+
+ return list;
+ }, [allNotes, currentNote, carnets]);
+
+ // Jaccard similarity helper for AI Recommendations
+ const calculateSimilarity = (textA: string, textB: string): number => {
+ const getWords = (str: string) => new Set(str.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()?"']/g,"").split(/\s+/).filter(w => w.length > 3));
+ const wordsA = getWords(textA);
+ const wordsB = getWords(textB);
+
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
+
+ let intersection = 0;
+ wordsA.forEach(w => {
+ if (wordsB.has(w)) intersection++;
+ });
+
+ const union = wordsA.size + wordsB.size - intersection;
+ return intersection / union;
+ };
+
+ // Compile recommendations
+ const blockSuggestions = useMemo(() => {
+ if (!currentNote) return [];
+
+ return allBlocks.map(block => {
+ const baseSim = calculateSimilarity(currentNote.content + " " + currentNote.title, block.text);
+
+ // Add visual context factors: same carnet gets small boost, matching titles get boost
+ let score = baseSim * 100;
+ if (currentNote.carnetId === allNotes.find(n => n.id === block.noteId)?.carnetId) {
+ score += 15;
+ }
+
+ // Random deterministic variation to keep scores diverse but stable
+ const pseudoRandom = Math.abs(Math.sin(block.blockIndex + block.noteId.charCodeAt(0))) * 12;
+ score = Math.min(94, Math.max(52, score + pseudoRandom));
+
+ return {
+ ...block,
+ score: Math.round(score)
+ };
+ }).sort((a, b) => b.score - a.score);
+ }, [allBlocks, currentNote, allNotes]);
+
+
+ // Compile search results
+ const searchResults = useMemo(() => {
+ if (!searchQuery) return allBlocks;
+ const query = searchQuery.toLowerCase();
+ return allBlocks.filter(block =>
+ block.text.toLowerCase().includes(query) ||
+ block.noteTitle.toLowerCase().includes(query)
+ );
+ }, [allBlocks, searchQuery]);
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
Living Block Picker
+
Connecter un bloc en temps réel
+
+
+
+
+
+
+
+ {/* Tab Selection */}
+
+ setActiveTab('suggestions')}
+ className={`flex-1 py-3 text-[10px] uppercase tracking-[0.15em] font-extrabold transition-all relative
+ ${activeTab === 'suggestions' ? 'text-blue-600 dark:text-blue-400 font-black' : 'text-concrete hover:text-ink/70'}`}
+ >
+
+
+ Suggestions IA
+
+ {activeTab === 'suggestions' && (
+
+ )}
+
+ setActiveTab('search')}
+ className={`flex-1 py-3 text-[10px] uppercase tracking-[0.15em] font-extrabold transition-all relative
+ ${activeTab === 'search' ? 'text-blue-600 dark:text-blue-400 font-black' : 'text-concrete hover:text-ink/70'}`}
+ >
+
+
+ Rechercher
+
+ {activeTab === 'search' && (
+
+ )}
+
+
+
+ {/* Search Input Box */}
+ {activeTab === 'search' && (
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Rechercher un extrait de note..."
+ className="w-full bg-white dark:bg-zinc-850 border border-[#D5D2CD] dark:border-neutral-800 rounded-xl pl-9 pr-4 py-2 text-xs outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-all font-sans"
+ autoFocus
+ />
+
+
+ )}
+
+ {/* Main List */}
+
+ {activeTab === 'suggestions' ? (
+ blockSuggestions.length > 0 ? (
+ blockSuggestions.map(block => (
+
onSelectBlock(block.noteId, block.blockIndex)}
+ className="w-full text-left p-3 rounded-xl border border-transparent hover:border-black/[0.08] hover:bg-white/70 dark:hover:bg-zinc-800/50 bg-white/30 dark:bg-zinc-800/10 transition-all group relative flex gap-3.5"
+ >
+
+
+ « {block.snippet} »
+
+
+ {block.noteTitle}
+ •
+
+ {block.carnetName}
+
+
+
+
+ {/* Discrete Percentage Circle Score */}
+
+
+ {block.score}% d'affinité
+
+
+
+ ))
+ ) : (
+
+ Aucune note complémentaire disponible pour suggérer un bloc.
+
+ )
+ ) : (
+ searchResults.length > 0 ? (
+ searchResults.map(block => (
+
onSelectBlock(block.noteId, block.blockIndex)}
+ className="w-full text-left p-3 rounded-xl border border-transparent hover:border-black/[0.08] hover:bg-white/70 dark:hover:bg-zinc-800/50 bg-white/30 dark:bg-zinc-800/10 transition-all group flex flex-col gap-1.5"
+ >
+
+ « {block.text} »
+
+
+ Source : {block.noteTitle}
+
+ {block.carnetName}
+
+
+
+ ))
+ ) : (
+
+ Aucun bloc ne correspond à votre recherche.
+
+ )
+ )}
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/BrainstormView/BrainstormView.tsx b/architectural-grid1/src/components/BrainstormView/BrainstormView.tsx
new file mode 100644
index 0000000..e69de83
--- /dev/null
+++ b/architectural-grid1/src/components/BrainstormView/BrainstormView.tsx
@@ -0,0 +1,797 @@
+
+import React, { useState, useEffect, useMemo, useRef } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ Zap,
+ Search,
+ ArrowRight,
+ History,
+ Plus,
+ Wind,
+ PlusCircle,
+ FileText,
+ ChevronRight,
+ Maximize2,
+ Share2,
+ Users,
+ Check,
+ Download,
+ Activity,
+ X
+} from 'lucide-react';
+import { v4 as uuidv4 } from 'uuid';
+import { WaveCanvas } from './WaveCanvas';
+import { BrainstormSession, BrainstormIdea, Note } from '../../types';
+import { generateBrainstormWave, generateExpansion, getEmbedding, cosineSimilarity } from '../../services/geminiService';
+
+interface BrainstormViewProps {
+ notes: Note[];
+ onConvertNote: (idea: BrainstormIdea) => void;
+}
+
+export const BrainstormView: React.FC = ({ notes, onConvertNote }) => {
+ const [seedInput, setSeedInput] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [error, setError] = useState(null);
+ const [sessions, setSessions] = useState([]);
+ const [activeSessionId, setActiveSessionId] = useState(null);
+ const [ideas, setIdeas] = useState([]);
+ const [selectedIdeaId, setSelectedIdeaId] = useState(null);
+ const [editingNodeId, setEditingNodeId] = useState(null);
+ const [manualTitle, setManualTitle] = useState('');
+ const [shareStatus, setShareStatus] = useState<'idle' | 'copying' | 'copied'>('idle');
+ const [showActivity, setShowActivity] = useState(false);
+ const [activities, setActivities] = useState<{ id: string; type: string; message: string; timestamp: string }[]>([]);
+ const [collaborators, setCollaborators] = useState<{ id: string; name: string; color: string }[]>([]);
+
+ const socketRef = useRef(null);
+
+ // Mock current user for presence
+ const currentUser = useMemo(() => ({
+ id: 'me-' + Math.random().toString(36).substr(2, 9),
+ name: 'Sepehr' // Derived from user email in metadata if possible, or guest
+ }), []);
+
+ const getInitials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
+ const stringToColor = (str: string) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ const colors = ['#f43f5e', '#ef4444', '#f59e0b', '#10b981', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef', '#f472b6'];
+ return colors[Math.abs(hash) % colors.length];
+ };
+
+ const addActivity = (message: string, type: string = 'info', broadcast: boolean = true) => {
+ const newActivity = {
+ id: uuidv4(),
+ type,
+ message,
+ timestamp: new Date().toLocaleTimeString()
+ };
+ setActivities(prev => [newActivity, ...prev].slice(0, 50));
+ if (broadcast && socketRef.current?.readyState === WebSocket.OPEN) {
+ socketRef.current.send(JSON.stringify({ type: 'activity', activity: newActivity }));
+ }
+ };
+
+ // WebSocket Connection
+ useEffect(() => {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const socket = new WebSocket(`${protocol}//${window.location.host}`);
+ socketRef.current = socket;
+
+ socket.onopen = () => {
+ console.log('WS Shared Brainstorm connected');
+ if (activeSessionId) {
+ socket.send(JSON.stringify({
+ type: 'join',
+ sessionId: activeSessionId,
+ user: { ...currentUser, color: stringToColor(currentUser.name) }
+ }));
+ }
+ };
+
+ socket.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ if (data.type === 'presence') {
+ setCollaborators(data.users);
+ }
+ if (data.type === 'idea_added') {
+ const newIdea = data.idea;
+ setIdeas(prev => {
+ if (prev.find(i => i.id === newIdea.id)) return prev;
+ return [...prev, newIdea];
+ });
+ }
+ if (data.type === 'idea_updated') {
+ const updatedIdea = data.idea;
+ setIdeas(prev => prev.map(i => i.id === updatedIdea.id ? updatedIdea : i));
+ }
+ if (data.type === 'activity') {
+ setActivities(prev => [data.activity, ...prev].slice(0, 50));
+ }
+ };
+
+ return () => {
+ socket.close();
+ };
+ }, []);
+
+ // Sync session joining
+ useEffect(() => {
+ if (socketRef.current?.readyState === WebSocket.OPEN && activeSessionId) {
+ socketRef.current.send(JSON.stringify({
+ type: 'join',
+ sessionId: activeSessionId,
+ user: { ...currentUser, color: stringToColor(currentUser.name) }
+ }));
+ }
+ }, [activeSessionId, currentUser]);
+
+ useEffect(() => {
+ fetch('/api/brainstorm/sessions')
+ .then(res => res.json())
+ .then(data => {
+ setSessions(data);
+ // Check for initial session from URL parameter (passed via window by App.tsx)
+ const initialId = (window as any).initialSessionId;
+ if (initialId && data.find((s: any) => s.id === initialId)) {
+ setActiveSessionId(initialId);
+ delete (window as any).initialSessionId;
+ }
+ })
+ .catch(err => console.error("Failed to load sessions", err));
+ }, []);
+
+ useEffect(() => {
+ if (activeSessionId) {
+ fetch(`/api/brainstorm/${activeSessionId}`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.ideas) {
+ setIdeas(prev => {
+ const filtered = prev.filter(i => i.sessionId !== activeSessionId);
+ return [...filtered, ...data.ideas];
+ });
+ }
+ })
+ .catch(err => console.error("Failed to load ideas", err));
+ }
+ }, [activeSessionId]);
+
+ const activeSession = useMemo(() =>
+ sessions.find(s => s.id === activeSessionId),
+ [activeSessionId, sessions]);
+
+ const activeIdeas = useMemo(() =>
+ ideas.filter(i => i.sessionId === activeSessionId),
+ [activeSessionId, ideas]);
+
+ const selectedIdea = useMemo(() =>
+ ideas.find(i => i.id === selectedIdeaId),
+ [selectedIdeaId, ideas]);
+
+ useEffect(() => {
+ const handleRemoteStart = (e: any) => {
+ if (e.detail?.seed) {
+ handleStartBrainstorm(e.detail.seed, e.detail.sourceNoteId);
+ }
+ };
+ window.addEventListener('start-brainstorm', handleRemoteStart);
+ return () => window.removeEventListener('start-brainstorm', handleRemoteStart);
+ }, [notes]);
+
+ const handleStartBrainstorm = async (seed: string, sourceNoteId?: string) => {
+ if (!seed.trim()) return;
+
+ setIsGenerating(true);
+ setError(null);
+
+ try {
+ // 1. Create session on backend
+ const sessionRes = await fetch('/api/brainstorm/sessions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ seedIdea: seed,
+ sourceNoteId
+ })
+ });
+ const session = await sessionRes.json();
+ if (!sessionRes.ok) throw new Error(session.error || "Failed to create session");
+
+ setSessions(prev => [session, ...prev]);
+ setActiveSessionId(session.id);
+ setSeedInput('');
+
+ // 2. Generate waves in frontend concurrently
+ const contextSummaries = notes.slice(0, 5).map(n => n.title).join(', ');
+
+ const wavePromises = [1, 2, 3].map(async (num) => {
+ try {
+ const generated = await generateBrainstormWave(seed, num, contextSummaries);
+ return generated.map(g => ({
+ ...g,
+ waveNumber: num
+ }));
+ } catch (e) {
+ console.error(`Wave ${num} failed`, e);
+ return [];
+ }
+ });
+
+ const wavesResults = await Promise.all(wavePromises);
+ const allNewIdeas = wavesResults.flat();
+
+ if (allNewIdeas.length === 0) {
+ throw new Error("No ideas were generated. Gemini might be shy today.");
+ }
+
+ // 3. Save ideas to backend
+ const ideasRes = await fetch(`/api/brainstorm/${session.id}/ideas`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ideas: allNewIdeas })
+ });
+ const savedIdeas = await ideasRes.json();
+ setIdeas(prev => [...prev, ...savedIdeas]);
+
+ addActivity(`Generated ${savedIdeas.length} ideas for Wave ${allNewIdeas[0]?.waveNumber || ''}`);
+
+ // Notify others
+ savedIdeas.forEach((idea: any) => {
+ socketRef.current?.send(JSON.stringify({ type: 'idea_added', idea }));
+ });
+
+ } catch (err: any) {
+ console.error("Brainstorm failed:", err);
+ setError(err.message || "An unexpected error occurred while brainstorming.");
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ const updateIdea = async (ideaId: string, updates: Partial) => {
+ try {
+ const res = await fetch(`/api/brainstorm/ideas/${ideaId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updates)
+ });
+ const updated = await res.json();
+ setIdeas(prev => prev.map(i => i.id === ideaId ? updated : i));
+
+ // Notify others
+ socketRef.current?.send(JSON.stringify({ type: 'idea_updated', idea: updated }));
+ } catch (err) {
+ console.error("Update failed", err);
+ }
+ };
+
+ const handleDeepenIdea = async (idea: BrainstormIdea) => {
+ setIsGenerating(true);
+ try {
+ const generated = await generateExpansion(idea.title, idea.description);
+ const newIdeasData = generated.map(g => ({
+ ...g,
+ waveNumber: Math.min(idea.waveNumber + 1, 3),
+ parentIdeaId: idea.id
+ }));
+
+ const res = await fetch(`/api/brainstorm/${idea.sessionId}/ideas`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ideas: newIdeasData })
+ });
+ const savedIdeas = await res.json();
+ setIdeas(prev => [...prev, ...savedIdeas]);
+ addActivity(`Expanded idea: ${idea.title}`);
+
+ // Notify others
+ savedIdeas.forEach((i: any) => {
+ socketRef.current?.send(JSON.stringify({ type: 'idea_added', idea: i }));
+ });
+ } catch (err) {
+ console.error("Deepen failed", err);
+ setError("Failed to expand this idea.");
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ const handleDismissIdea = (ideaId: string) => {
+ updateIdea(ideaId, { status: 'dismissed' });
+ setSelectedIdeaId(null);
+ };
+
+ const handleConvertToNote = (idea: BrainstormIdea) => {
+ updateIdea(idea.id, { status: 'converted' });
+ onConvertNote(idea);
+ };
+
+ const handleManualAdd = async (title: string, parentId?: string) => {
+ if (!title.trim() || !activeSessionId) return;
+
+ setIsGenerating(true);
+ try {
+ let finalParentId = parentId;
+ let waveNumber = 1;
+
+ if (parentId) {
+ const p = ideas.find(i => i.id === parentId);
+ if (p) waveNumber = Math.min(p.waveNumber + 1, 3);
+ } else if (activeIdeas.length > 0) {
+ // Semantic auto-placement if no parent is specified
+ try {
+ const newEmbedding = await getEmbedding(title);
+ let bestSim = -1;
+ let bestParent: BrainstormIdea | null = null;
+
+ for (const idea of activeIdeas) {
+ const ideaEmbedding = await getEmbedding(idea.title + " " + idea.description);
+ const sim = cosineSimilarity(newEmbedding, ideaEmbedding);
+ if (sim > bestSim) {
+ bestSim = sim;
+ bestParent = idea;
+ }
+ }
+
+ if (bestParent && bestSim > 0.7) {
+ finalParentId = bestParent.id;
+ waveNumber = Math.min(bestParent.waveNumber + 1, 3);
+ }
+ } catch (e) {
+ console.error("Semantic placement failed", e);
+ }
+ }
+
+ const newIdeaData = [{
+ title: title,
+ description: "",
+ waveNumber: waveNumber,
+ connectionToSeed: finalParentId
+ ? `Manual addition (auto-linked)`
+ : "Manual addition to root",
+ noveltyScore: 5,
+ parentIdeaId: finalParentId
+ }];
+
+ const res = await fetch(`/api/brainstorm/${activeSessionId}/ideas`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ideas: newIdeaData })
+ });
+ const saved = await res.json();
+ setIdeas(prev => [...prev, ...saved]);
+
+ addActivity(`Manually added idea: ${title}`);
+
+ // Notify
+ saved.forEach((i: any) => socketRef.current?.send(JSON.stringify({ type: 'idea_added', idea: i })));
+
+ setEditingNodeId(null);
+ setManualTitle('');
+ } catch (err) {
+ console.error("Manual add failed", err);
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ const handleInvite = () => {
+ if (!activeSessionId) return;
+ const shareUrl = `${window.location.origin}${window.location.pathname}?session=${activeSessionId}`;
+ navigator.clipboard.writeText(shareUrl);
+ setShareStatus('copied');
+ addActivity(`Invitation link copied to clipboard`);
+ setTimeout(() => setShareStatus('idle'), 2000);
+ };
+
+ const handleExport = () => {
+ if (!activeSession) return;
+
+ let markdown = `# Brainstorm : ${activeSession.seedIdea}\n\n`;
+ markdown += `Date : ${new Date(activeSession.createdAt).toLocaleDateString()}\n\n`;
+
+ [1, 2, 3].forEach(waveNum => {
+ const waveIdeas = activeIdeas.filter(i => i.waveNumber === waveNum);
+ if (waveIdeas.length > 0) {
+ markdown += `## Vague ${waveNum}\n\n`;
+ waveIdeas.forEach(idea => {
+ markdown += `### ${idea.title}\n`;
+ markdown += `${idea.description}\n`;
+ markdown += `*Score de nouveauté : ${idea.noveltyScore}/10*\n`;
+ markdown += `*Connexion : ${idea.connectionToSeed}*\n\n`;
+ });
+ }
+ });
+
+ onConvertNote({
+ id: uuidv4(),
+ title: `Brainstorm Export: ${activeSession.seedIdea}`,
+ description: markdown,
+ sessionId: activeSession.id,
+ waveNumber: 0,
+ connectionToSeed: "Export",
+ noveltyScore: 10,
+ status: 'converted'
+ });
+
+ addActivity(`Session exported to notes`);
+ };
+
+ return (
+
+ {/* Header / Start area */}
+
+ {/* Architectural Grid Background */}
+
+
+
+
+
+
+
+
+
Waves of Thought
+
+
+
Unfold dimensions of potentiality
+
+
+
+ {activeSession && (
+
+
+
+ Export
+
+
setShowActivity(!showActivity)}
+ className={`flex items-center gap-2 px-4 py-2 border border-border rounded-xl text-xs font-bold uppercase tracking-widest transition-all shadow-sm ${showActivity ? 'bg-ink text-paper' : 'bg-white dark:bg-white/5 text-concrete hover:text-ink'}`}
+ title="Show Activity"
+ >
+
+ Activity
+
+
+ {shareStatus === 'copied' ? : }
+ {shareStatus === 'copied' ? 'Link Copied' : 'Invite'}
+
+
+ {collaborators.map((user) => (
+
+
+ {getInitials(user.name)}
+
+
+
+ ))}
+
+
+
+
+ )}
+
+
+
+
+
setSeedInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleStartBrainstorm(seedInput)}
+ placeholder="Enter a concept to unfold..."
+ className={`w-full relative bg-white dark:bg-[#1A1A1A] border-2 rounded-2xl px-8 py-7 pr-20 outline-none transition-all text-2xl font-serif italic text-ink dark:text-dark-ink shadow-sm group-hover:shadow-md
+ ${error ? 'border-rose-400 focus:ring-rose-100 shadow-rose-100' : 'border-border/40 focus:border-ochre/40 focus:ring-4 focus:ring-ochre/5'}`}
+ />
+
handleStartBrainstorm(seedInput)}
+ disabled={isGenerating || !seedInput.trim()}
+ className="absolute right-4 top-4 bottom-4 px-6 bg-ink dark:bg-ochre text-paper rounded-xl disabled:opacity-50 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2 min-w-[70px] shadow-lg"
+ >
+ {isGenerating ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {error && (
+
+
+
+
Obstruction detected
+
{error}
+
+
+ )}
+
+ {isGenerating && !error && (
+
+
+ {[0.2, 0.4, 0.6].map((d, i) => (
+
+ ))}
+
+ Gemini is harvesting seeds of thought from the digital ether...
+
+ )}
+
+
+
+
+
+ {/* Main Canvas Area */}
+
+ {activeSession ? (
+
setSelectedIdeaId(null)} className="w-full h-full">
+ {
+ setSelectedIdeaId(id);
+ }}
+ onPositionUpdate={(id, pos) => updateIdea(id, { position: pos })}
+ onAddChild={(id) => {
+ setSelectedIdeaId(id);
+ setEditingNodeId(id);
+ }}
+ onManualSubmit={handleManualAdd}
+ onManualCancel={() => setEditingNodeId(null)}
+ editingNodeId={editingNodeId}
+ selectedNodeId={selectedIdeaId}
+ relatedNotes={notes}
+ />
+
+ ) : (
+
+
+
The canvas is waiting for your spark...
+
+ )}
+
+ {/* Floating UI overlays */}
+
+ {activeSession && (
+
+
+
+ setEditingNodeId('new')}
+ className="px-6 py-3 bg-paper dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-concrete hover:bg-ink hover:text-paper transition-all"
+ >
+
+ Add Manual Idea
+
+
+ )}
+
+
+
+ {/* Activity Sidebar */}
+
+ {showActivity && (
+
+
+
+
setShowActivity(false)} className="p-1 hover:bg-white/10 rounded-lg">
+
+
+
+
+ {activities.length === 0 ? (
+
Aucune activité pour le moment
+ ) : (
+ activities.map((act) => (
+
+
+ {act.message}
+ {act.timestamp}
+
+ ))
+ )}
+
+
+ )}
+
+
+ {/* Right Sidebar Detail Panel */}
+
+ {selectedIdea && (
+
+
+
+
+ Vague {selectedIdea.waveNumber}
+
+
+ {selectedIdea.status === 'converted' && (
+ Note Created
+ )}
+ setSelectedIdeaId(null)} className="p-2 hover:bg-ink/5 rounded-full transition-colors">
+
+
+
+
+
+
{selectedIdea.title}
+
+
+
+ Novelty: {selectedIdea.noveltyScore}/10
+
+
+
+
+ {selectedIdea.description}
+
+
+
+
Origin connection
+
+ "{selectedIdea.connectionToSeed}"
+
+
+
+ {selectedIdea.relatedNoteIds && selectedIdea.relatedNoteIds.length > 0 && (
+
+
Semantic Context
+ {selectedIdea.relatedNoteIds.map(noteId => {
+ const note = notes.find(n => n.id === noteId);
+ return note ? (
+
+ ) : null;
+ })}
+
+ )}
+
+
+
+
handleDeepenIdea(selectedIdea)}
+ disabled={isGenerating}
+ className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-ochre/40 hover:bg-ochre/5 transition-all group disabled:opacity-50"
+ >
+
+ AI Expand
+
+
setEditingNodeId(selectedIdea.id)}
+ className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-ink/40 hover:bg-ink/5 transition-all group disabled:opacity-50"
+ >
+
+ Add Child
+
+
handleConvertToNote(selectedIdea)}
+ disabled={selectedIdea.status === 'converted'}
+ className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-accent/40 hover:bg-accent/5 transition-all group disabled:opacity-50 whitespace-nowrap"
+ >
+
+ Extract Note
+
+
+
+
handleDismissIdea(selectedIdea.id)}
+ className="w-full py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-concrete hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all border border-transparent hover:border-rose-500/10"
+ >
+ Not pertinent
+
+
+
+
+ )}
+
+
+ {/* History Rail */}
+
+
+
+
+ {sessions.map(session => (
+ setActiveSessionId(session.id)}
+ className={`w-10 h-10 min-h-[40px] rounded-xl flex items-center justify-center text-xs font-bold transition-all shrink-0
+ ${activeSessionId === session.id ? 'bg-ink text-paper scale-110 shadow-lg' : 'bg-paper dark:bg-white/10 text-concrete hover:bg-black/5 hover:text-ink'}`}
+ title={session.seedIdea}
+ >
+ {session.seedIdea.charAt(0).toUpperCase()}
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/BrainstormView/WaveCanvas.tsx b/architectural-grid1/src/components/BrainstormView/WaveCanvas.tsx
new file mode 100644
index 0000000..0e45310
--- /dev/null
+++ b/architectural-grid1/src/components/BrainstormView/WaveCanvas.tsx
@@ -0,0 +1,348 @@
+
+import React, { useEffect, useRef } from 'react';
+import * as d3 from 'd3';
+import { BrainstormSession, BrainstormIdea, Note } from '../../types';
+
+interface WaveCanvasProps {
+ session: BrainstormSession;
+ ideas: BrainstormIdea[];
+ onNodeSelect: (id: string) => void;
+ onPositionUpdate: (id: string, pos: { x: number; y: number }) => void;
+ onAddChild: (id: string) => void;
+ onManualSubmit: (title: string, parentId?: string) => void;
+ onManualCancel: () => void;
+ editingNodeId: string | null;
+ selectedNodeId: string | null;
+ relatedNotes: Note[];
+}
+
+export const WaveCanvas: React.FC = ({
+ session,
+ ideas,
+ onNodeSelect,
+ onPositionUpdate,
+ onAddChild,
+ onManualSubmit,
+ onManualCancel,
+ editingNodeId,
+ selectedNodeId,
+ relatedNotes
+}) => {
+ const svgRef = useRef(null);
+ const containerRef = useRef(null);
+ const inputRef = useRef(null);
+ const [inputValue, setInputValue] = React.useState('');
+
+ useEffect(() => {
+ if (editingNodeId && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [editingNodeId]);
+
+ useEffect(() => {
+ if (!svgRef.current || !containerRef.current) return;
+
+ const width = containerRef.current.clientWidth;
+ const height = containerRef.current.clientHeight;
+ const centerX = width / 2;
+ const centerY = height / 2;
+
+ const svg = d3.select(svgRef.current);
+ svg.selectAll("*").remove();
+
+ const g = svg.append("g");
+
+ // Zoom behavior
+ const zoom = d3.zoom()
+ .scaleExtent([0.1, 5])
+ .on("zoom", (event) => {
+ g.attr("transform", event.transform);
+ });
+
+ svg.call(zoom);
+
+ // Initial transform to center
+ svg.call(zoom.transform, d3.zoomIdentity.translate(centerX, centerY).scale(0.8));
+
+ // Data structures for d3
+ interface D3Node extends d3.SimulationNodeDatum {
+ id: string;
+ type: 'root' | 'idea' | 'note';
+ wave?: number;
+ title: string;
+ color: string;
+ radius: number;
+ status?: string;
+ }
+
+ interface D3Link extends d3.SimulationLinkDatum {
+ source: string | D3Node;
+ target: string | D3Node;
+ type: 'wave' | 'context' | 'parent';
+ }
+
+ const nodes: D3Node[] = [];
+ const links: D3Link[] = [];
+
+ // Root node
+ const rootNode: D3Node = {
+ id: 'root',
+ type: 'root',
+ title: session.seedIdea,
+ color: '#141414',
+ radius: 40,
+ fx: 0,
+ fy: 0
+ };
+ nodes.push(rootNode);
+
+ // Idea nodes
+ const colors = {
+ 1: '#fb923c', // orange
+ 2: '#60a5fa', // blue
+ 3: '#a78bfa' // violet
+ };
+
+ ideas.forEach(idea => {
+ nodes.push({
+ id: idea.id,
+ type: 'idea',
+ wave: idea.waveNumber,
+ title: idea.title,
+ color: colors[idea.waveNumber as 1|2|3] || '#94a3b8',
+ radius: 28,
+ status: idea.status,
+ x: idea.position?.x,
+ y: idea.position?.y
+ });
+
+ if (idea.parentIdeaId) {
+ links.push({
+ source: idea.parentIdeaId,
+ target: idea.id,
+ type: 'parent'
+ });
+ } else {
+ links.push({
+ source: 'root',
+ target: idea.id,
+ type: 'wave'
+ });
+ }
+ });
+
+ // Radial layout forces
+ const simulation = d3.forceSimulation(nodes)
+ .force("link", d3.forceLink(links).id(d => d.id).distance(d => {
+ if (d.type === 'wave') {
+ const targetNode = nodes.find(n => n.id === (typeof d.target === 'string' ? d.target : (d.target as any).id));
+ return (targetNode?.wave || 1) * 200;
+ }
+ if (d.type === 'parent') return 180;
+ return 100;
+ }))
+ .force("charge", d3.forceManyBody().strength(-800))
+ .force("radial", d3.forceRadial(d => {
+ if (d.type === 'root') return 0;
+ if (d.id.includes('-')) return (d.wave || 1) * 200 + 100; // Deepened ideas push out
+ return (d.wave || 1) * 200;
+ }, 0, 0).strength(0.8))
+ .force("collision", d3.forceCollide().radius(d => d.radius + 30));
+
+ // Drawing rings
+ const ringRadii = [200, 400, 600];
+ g.selectAll(".ring")
+ .data(ringRadii)
+ .enter()
+ .append("circle")
+ .attr("class", "ring")
+ .attr("r", d => d)
+ .attr("fill", "none")
+ .attr("stroke", "#e2e8f0")
+ .attr("stroke-width", 1)
+ .attr("stroke-dasharray", "4,4")
+ .style("opacity", 0.5);
+
+ // Links
+ const link = g.append("g")
+ .selectAll("line")
+ .data(links)
+ .enter()
+ .append("line")
+ .attr("stroke", d => d.type === 'wave' ? "#cbd5e1" : d.type === 'parent' ? "#fde047" : "#94a3b8")
+ .attr("stroke-width", d => d.type === 'wave' ? 1.5 : 2)
+ .attr("stroke-dasharray", d => d.type === 'parent' ? "none" : "4,4");
+
+ // Nodes
+ const node = g.append("g")
+ .selectAll(".node")
+ .data(nodes)
+ .enter()
+ .append("g")
+ .attr("class", "node")
+ .style("opacity", d => d.status === 'dismissed' ? 0.4 : 1)
+ .on("click", (event, d) => {
+ event.stopPropagation();
+ if (d.type === 'idea') onNodeSelect(d.id);
+ })
+ .on("dblclick", (event, d) => {
+ event.stopPropagation();
+ if (d.type === 'idea') {
+ onNodeSelect(d.id);
+ onAddChild(d.id);
+ }
+ })
+ .call(d3.drag()
+ .on("start", dragstarted)
+ .on("drag", dragged)
+ .on("end", dragended) as any);
+
+ node.append("circle")
+ .attr("r", d => d.radius)
+ .attr("fill", d => d.status === 'converted' ? '#ecfdf5' : (d.type === 'root' ? '#141414' : '#fff'))
+ .attr("stroke", d => d.status === 'converted' ? '#10b981' : d.color)
+ .attr("stroke-width", d => d.id === selectedNodeId ? 4 : 2)
+ .attr("class", "cursor-pointer transition-all hover:scale-110")
+ .style("filter", d => d.id === selectedNodeId ? `drop-shadow(0 0 12px ${d.color}cc)` : "none");
+
+ // Plus icon for selected node
+ node.filter(d => d.id === selectedNodeId && d.type === 'idea')
+ .append("circle")
+ .attr("r", 10)
+ .attr("cx", 20)
+ .attr("cy", -20)
+ .attr("fill", "#141414")
+ .attr("stroke", "#fff")
+ .attr("stroke-width", 1)
+ .attr("class", "cursor-pointer")
+ .on("click", (event, d) => {
+ event.stopPropagation();
+ onAddChild(d.id);
+ });
+
+ node.filter(d => d.id === selectedNodeId && d.type === 'idea')
+ .append("text")
+ .attr("x", 20)
+ .attr("y", -17)
+ .attr("text-anchor", "middle")
+ .attr("fill", "#fff")
+ .attr("class", "text-[14px] font-bold pointer-events-none")
+ .text("+");
+
+ // ForeignObject for inline input
+ const editGroup = node.filter(d => d.id === editingNodeId && d.type === 'idea')
+ .append("foreignObject")
+ .attr("width", 200)
+ .attr("height", 80)
+ .attr("x", -100)
+ .attr("y", 40)
+ .append("xhtml:div")
+ .attr("class", "bg-paper dark:bg-black p-2 border border-border rounded-xl shadow-2xl");
+
+ editGroup.append("input")
+ .attr("type", "text")
+ .attr("placeholder", "Nouvelle idée...")
+ .attr("class", "w-full bg-white dark:bg-[#1A1A1A] border-none outline-none text-xs font-bold uppercase tracking-tight p-2 rounded-lg")
+ .on("keydown", (event) => {
+ if (event.key === 'Enter') {
+ onManualSubmit(event.target.value, editingNodeId!);
+ event.target.value = '';
+ }
+ if (event.key === 'Escape') {
+ onManualCancel();
+ }
+ })
+ .on("blur", () => {
+ // Optional: onManualCancel();
+ });
+
+ // Special case for root addition input (if editingNodeId is 'new')
+ if (editingNodeId === 'new') {
+ g.append("foreignObject")
+ .attr("width", 200)
+ .attr("height", 80)
+ .attr("x", -100)
+ .attr("y", -120) // Floating above center
+ .append("xhtml:div")
+ .attr("class", "bg-paper dark:bg-black p-2 border border-border rounded-xl shadow-2xl animate-bounce")
+ .append("input")
+ .attr("type", "text")
+ .attr("autoFocus", "true")
+ .attr("placeholder", "Idée libre...")
+ .attr("class", "w-full bg-white dark:bg-[#1A1A1A] border-none outline-none text-xs font-bold uppercase tracking-tight p-2 rounded-lg")
+ .on("keydown", (event) => {
+ if (event.key === 'Enter') {
+ onManualSubmit(event.target.value);
+ event.target.value = '';
+ }
+ if (event.key === 'Escape') onManualCancel();
+ });
+ }
+
+ // State indicators (converted)
+ node.filter(d => d.status === 'converted')
+ .append("path")
+ .attr("d", d3.symbol().type(d3.symbolCircle).size(150))
+ .attr("fill", "#10b981");
+
+ // Icons/Text in nodes
+ node.append("text")
+ .attr("dy", d => d.type === 'root' ? ".35em" : d.radius + 20)
+ .attr("text-anchor", "middle")
+ .attr("fill", d => d.type === 'root' ? "#fff" : (d.status === 'dismissed' ? "#94a3b8" : "#141414"))
+ .attr("class", d => d.type === 'root' ? "text-[10px] font-bold pointer-events-none tracking-widest" : "text-[11px] font-bold uppercase tracking-tight pointer-events-none")
+ .text(d => d.type === 'root' ? "SEED" : d.title.length > 18 ? d.title.substring(0, 18) + "..." : d.title);
+
+ if (rootNode) {
+ g.append("text")
+ .attr("text-anchor", "middle")
+ .attr("dy", 80)
+ .attr("class", "text-2xl font-serif italic fill-ink dark:fill-dark-ink pointer-events-none shadow-sm")
+ .text(session.seedIdea);
+ }
+
+ simulation.on("tick", () => {
+ link
+ .attr("x1", d => (d.source as any).x)
+ .attr("y1", d => (d.source as any).y)
+ .attr("x2", d => (d.target as any).x)
+ .attr("y2", d => (d.target as any).y);
+
+ node
+ .attr("transform", d => `translate(${d.x},${d.y})`);
+ });
+
+ function dragstarted(event: any, d: D3Node) {
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event: any, d: D3Node) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragended(event: any, d: D3Node) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ if (d.type === 'idea') {
+ onPositionUpdate(d.id, { x: event.x, y: event.y });
+ }
+ }
+
+ return () => {
+ simulation.stop();
+ };
+ }, [session, ideas, selectedNodeId, editingNodeId, onNodeSelect]);
+
+ return (
+
+
+
+
Spatial Exploration Mode
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/ClipperSimulator.tsx b/architectural-grid1/src/components/ClipperSimulator.tsx
new file mode 100644
index 0000000..4fa68c5
--- /dev/null
+++ b/architectural-grid1/src/components/ClipperSimulator.tsx
@@ -0,0 +1,618 @@
+import React, { useState, useEffect, useRef } from 'react';
+import {
+ X,
+ Lock,
+ RefreshCw,
+ Chrome,
+ Check,
+ Loader2,
+ ChevronDown,
+ Sparkles,
+ ArrowUpRight,
+ AlertTriangle,
+ Globe,
+ Scissors,
+ Bookmark
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Carnet, Note, Tag } from '../types';
+import { v4 as uuidv4 } from 'uuid';
+
+interface ClipperSimulatorProps {
+ isOpen: boolean;
+ onClose: () => void;
+ carnets: Carnet[];
+ activeCarnetId: string;
+ onAddNote: (note: Note) => void;
+ onTriggerToast: (title: string, noteId: string) => void;
+}
+
+interface MockArticle {
+ id: string;
+ title: string;
+ domain: string;
+ url: string;
+ favicon: string;
+ content: string[];
+ suggestedTags: string[];
+ aiGeneratedTitle: string;
+ aiSummary: string;
+}
+
+const MOCK_ARTICLES: MockArticle[] = [
+ {
+ id: 'art-1',
+ title: 'The Bauhaus Theory & Functional Spatial Systems',
+ domain: 'bauhausstudios.org',
+ url: 'https://bauhausstudios.org/theory/functionalism',
+ favicon: 'https://www.google.com/s2/favicons?domain=bauhausstudios.org&sz=64',
+ content: [
+ 'Functionalist design operates on the direct correlation between physical geometry and spatial behavior. At the Bauhaus, teachers like Walter Gropius and Hannes Meyer postulated that an architectural object should serve its function strictly, discarding superfluous details that obscure the purity of its skeleton.',
+ 'The modern grid represents an honest commitment to industrial standardization. By segmenting living and working spaces into predictable, modular blocks, architects can optimize solar gain, human traffic flows, and construction material metrics.',
+ 'Light is the ultimate deconstructive asset within functional systems. When light pierces the rigid geometry of a modernist envelope, it shifts the perceived density of structural grids, transforming cold static steel interfaces into canvas-like elements that respond dynamically to local chronologies.'
+ ],
+ suggestedTags: ['Bauhaus', 'Functionalism', 'Spatial Design', 'German Modernism'],
+ aiGeneratedTitle: 'Bauhaus Functionalism & Rhythmic Grid Logic',
+ aiSummary: 'An exploration of how Walter Gropius and Bauhaus theorists utilized geometric grids and deconstructive light to align architectural materiality with industrial standardization and human behavioral workflows.'
+ },
+ {
+ id: 'art-2',
+ title: 'Sustainable Wood Frameworks & Decarbonized Structures',
+ domain: 'ecotimber.com',
+ url: 'https://ecotimber.com/future/timber',
+ favicon: 'https://www.google.com/s2/favicons?domain=ecotimber.com&sz=64',
+ content: [
+ 'Decarbonizing global real estate requires replacing portland cement and heavy structural steel with cross-laminated timber (CLT). CLT stands as a highly predictable engineered wood structure that sequesters atmospheric carbon dioxide directly inside the load-bearing framework of high-density buildings.',
+ 'Integrating CLT with parametric optimization allows for maximum material efficiency. Architects slice wood beams along precise stress lines generated by finite element analysis solvers, removing empty material volumes while keeping the building safe, functional, and durable.',
+ 'Passive solar energy design matches this structural honesty perfectly. By positioning CLT mass in the interior core, the building acts as a solar battery, absorbing raw passive light energy during peak hours and radiating warmth throughout the cold seasonal nights.'
+ ],
+ suggestedTags: ['Sustainabilty', 'Ecology', 'CLT Material', 'Decarbonization'],
+ aiGeneratedTitle: 'CLT Systems & Carbon-Neutral Frameworks',
+ aiSummary: 'A breakdown of high-density cross-laminated timber (CLT) integration, using parametric simulation to optimize stress distribution and passive thermal retention for modern sustainable spaces.'
+ }
+];
+
+export const ClipperSimulator: React.FC = ({
+ isOpen,
+ onClose,
+ carnets,
+ activeCarnetId,
+ onAddNote,
+ onTriggerToast,
+}) => {
+ const [activeArticleIdx, setActiveArticleIdx] = useState(0);
+ const activeArticle = MOCK_ARTICLES[activeArticleIdx];
+
+ // Clipper Extension Popup States
+ const [selectedCarnetId, setSelectedCarnetId] = useState(activeCarnetId || carnets[0]?.id || '1');
+ const [selectedText, setSelectedText] = useState('');
+ const [clipperState, setClipperState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
+ const [aiGeneratedTitle, setAiGeneratedTitle] = useState('');
+ const [lastCreatedNoteId, setLastCreatedNoteId] = useState('');
+ const [customError, setCustomError] = useState('');
+
+ // Dropdown UI
+ const [showCarnetDropdown, setShowCarnetDropdown] = useState(false);
+
+ // Monitor text selections in the mock web page content
+ const handleTextSelection = () => {
+ const selection = window.getSelection();
+ if (selection && selection.toString().trim().length > 0) {
+ const text = selection.toString().trim();
+ // Ensure the text belongs to our mock article content
+ setSelectedText(text);
+ }
+ };
+
+ // Clear selections
+ const clearSelection = () => {
+ setSelectedText('');
+ window.getSelection()?.removeAllRanges();
+ };
+
+ // Preset highlights to make it easy to select text without highlighting with mouse
+ const handlePresetHighlight = (paragraph: string) => {
+ setSelectedText(paragraph);
+ };
+
+ // Handle the Clipper Action
+ const handleClip = (type: 'page' | 'selection') => {
+ setClipperState('loading');
+
+ // Simulate AI extraction and processing (summary, tags generation)
+ setTimeout(() => {
+ try {
+ // Occasional simulated error for retry demonstration
+ if (Math.random() < 0.15) {
+ throw new Error("Connexion réseau interrompue. L'extension n'a pas pu joindre les serveurs Momento.");
+ }
+
+ const dateStr = new Date().toLocaleDateString('fr-FR', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric'
+ });
+
+ const generatedTags: Tag[] = activeArticle.suggestedTags.map((tagLabel, idx) => ({
+ id: `t-clip-${Date.now()}-${idx}`,
+ label: tagLabel,
+ type: 'ai'
+ }));
+
+ const newNoteId = `n-clip-${Date.now()}`;
+
+ let clipTitle = activeArticle.aiGeneratedTitle;
+ let clipContent = '';
+
+ if (type === 'selection' && selectedText) {
+ clipTitle = `Capture : ${activeArticle.title.substring(0, 30)}...`;
+ clipContent = `**[Sélection capturée]**\n\n> ${selectedText}\n\n---\n\n**Contexte initial :** ${activeArticle.aiSummary}\n\nURL Source : ${activeArticle.url}`;
+ } else {
+ clipContent = `**[Page web complète clippée]**\n\n**Résumé généré par l'IA :**\n${activeArticle.aiSummary}\n\n---\n\n**Contenu de l'article :**\n\n${activeArticle.content.join('\n\n')}\n\nURL Source : ${activeArticle.url}`;
+ }
+
+ const newNote: Note = {
+ id: newNoteId,
+ carnetId: selectedCarnetId,
+ title: clipTitle,
+ content: clipContent,
+ imageUrl: activeArticleIdx === 0
+ ? 'https://images.unsplash.com/photo-1503387762-592dea58ef23?auto=format&fit=crop&q=80&w=800&h=600'
+ : 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&q=80&w=800&h=600',
+ date: dateStr,
+ tags: [
+ ...generatedTags,
+ { id: 't-web-source', label: 'Clipped', type: 'user' }
+ ],
+ // Custom clipper details
+ isClipped: true,
+ clipSourceUrl: activeArticle.url,
+ clipFavicon: activeArticle.favicon,
+ clipDate: dateStr
+ };
+
+ setAiGeneratedTitle(clipTitle);
+ setLastCreatedNoteId(newNoteId);
+ setClipperState('success');
+
+ // Add note to Momento Database
+ onAddNote(newNote);
+
+ // Fire real-time notification toast in Momento!
+ onTriggerToast(clipTitle, newNoteId);
+
+ } catch (err: any) {
+ setCustomError(err.message || "Erreur de connexion.");
+ setClipperState('error');
+ }
+ }, 1500);
+ };
+
+ const handleResetClipper = () => {
+ setClipperState('idle');
+ setCustomError('');
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Left column: Realistic Mock Browser Page */}
+
+ {/* Mock Browser Header */}
+
+ {/* Window Controls */}
+
+
+ {/* Tabs */}
+
+ {MOCK_ARTICLES.map((art, idx) => (
+
{
+ setActiveArticleIdx(idx);
+ clearSelection();
+ handleResetClipper();
+ }}
+ className={`px-3 py-1.5 text-xs rounded-lg font-medium flex items-center gap-2 max-w-[170px] truncate transition-colors
+ ${activeArticleIdx === idx
+ ? 'bg-slate-100 dark:bg-white/5 text-ink dark:text-dark-ink border border-border'
+ : 'text-concrete hover:bg-slate-50 dark:hover:bg-white/5'}`}
+ >
+ { (e.target as any).src = 'https://www.google.com/s2/favicons?domain=google.com'; }} />
+ {art.title}
+
+ ))}
+
+
+ {/* Live Indicator of Clipper Simulator */}
+
+
+ Simulateur de Capture
+
+
+
+ {/* Browser Address Bar */}
+
+
+
+
+
{ clearSelection(); handleResetClipper(); }} className="p-1 hover:bg-slate-100 dark:hover:bg-white/5 rounded">
+
+
+
+
+ https://
+ {activeArticle.domain}
+ {activeArticle.url.slice(activeArticle.url.indexOf(activeArticle.domain) + activeArticle.domain.length)}
+
+
+ {/* Web Extension active badge */}
+
+
+
+ Extension active sur cette page
+
+
+
+
+ {/* Web Viewport */}
+
+
+
+
+ Publié sur {activeArticle.domain}
+
+
+
+ {activeArticle.title}
+
+
+
+ Date : Capture Temps Réel
+ Sélectionnez du texte ci-dessous pour le clipper
+
+
+ {/* Tips */}
+
+
+
+ Piste d'évaluation :
+
+
+ Survolez et surlignez n'importe quel texte à la souris dans l'article ci-dessous pour activer instantanément l'état Sélection active dans l'extension ! Vous pouvez aussi cliquer sur un paragraphe pour le simuler :
+
+
+
+ {/* Main Content paragraphs */}
+
+ {activeArticle.content.map((p, index) => {
+ const isParaSelected = selectedText === p;
+ return (
+
handlePresetHighlight(p)}
+ className={`cursor-pointer transition-all duration-300 p-2.5 rounded-lg border
+ ${isParaSelected
+ ? 'bg-accent/10 border-accent text-neutral-900 dark:text-white font-medium scale-[1.01] shadow-sm'
+ : 'border-transparent hover:bg-neutral-50 dark:hover:bg-neutral-900'}`}
+ title="Cliquer pour sélectionner ce paragraphe"
+ >
+ {p}
+
+ );
+ })}
+
+
+ {selectedText && (
+
+
+
+ Sélection enregistrée ({selectedText.split(' ').length} mots)
+
+
+ Effacer la sélection
+
+
+ )}
+
+
+
+
+ {/* Right column: Simulated Browser Extension Popup Screen (Exactly 400x520px envelope styled elegantly) */}
+
+
+
+
+
+ {/* Explicitly designed container mimicking browser overlay/extension dropdown at 400x520px target size */}
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/GraphKnowledgeMap.tsx b/architectural-grid1/src/components/GraphKnowledgeMap.tsx
new file mode 100644
index 0000000..1fba097
--- /dev/null
+++ b/architectural-grid1/src/components/GraphKnowledgeMap.tsx
@@ -0,0 +1,874 @@
+import React, { useEffect, useRef, useState, useMemo } from 'react';
+import * as d3 from 'd3';
+import { motion, AnimatePresence } from 'motion/react';
+import { Note, Carnet, Tag } from '../types';
+import {
+ Network,
+ Search,
+ Sliders,
+ HelpCircle,
+ X,
+ Filter,
+ Compass,
+ BookOpen,
+ Eye,
+ Sparkles,
+ RefreshCw,
+ Plus,
+ Minus,
+ Maximize2,
+ ChevronLeft,
+ Calendar,
+ Layers,
+ FileText
+} from 'lucide-react';
+
+interface GraphKnowledgeMapProps {
+ notes: Note[];
+ carnets: Carnet[];
+ onOpenNote: (noteId: string) => void;
+ onClose?: () => void;
+}
+
+// 7 Gorgeous colors corresponding to the carnets palette
+const CARNET_COLOR_PALETTE: { [key: string]: string } = {
+ '1': '#D97706', // Daily Notes - Warm Amber
+ '2': '#059669', // Project: Neo - Soft Emerald
+ '3': '#4F46E5', // Shared Docs - Rich Indigo
+ '4': '#0891B2', // Architecture Research - Clean Cyan
+ '5': '#EA580C', // History of Architecture - Deep Orange
+ '6': '#DB2777', // Modernism - Vibrant Rose
+ '7': '#65A30D', // Sustainable Design - Cool Lime
+};
+
+const DEFAULT_CARNET_COLOR = '#71717A'; // Zinc
+
+interface D3Node extends d3.SimulationNodeDatum {
+ id: string;
+ title: string;
+ carnetId: string;
+ carnetName: string;
+ color: string;
+ date: string;
+ snippet: string;
+ tags: Tag[];
+ degree: number;
+}
+
+interface D3Link extends d3.SimulationLinkDatum {
+ source: string | D3Node;
+ target: string | D3Node;
+ type: 'wikilink' | 'semantic';
+ strength: number;
+}
+
+export const GraphKnowledgeMap: React.FC = ({
+ notes,
+ carnets,
+ onOpenNote,
+ onClose
+}) => {
+ const containerRef = useRef(null);
+ const svgRef = useRef(null);
+
+ // Settings & Toggles
+ const [showSemanticLinks, setShowSemanticLinks] = useState(true);
+ const [minSemanticStrength, setMinSemanticStrength] = useState(0.40); // threshold
+ const [selectedCarnetIds, setSelectedCarnetIds] = useState([]);
+
+ // Interaction States
+ const [searchQuery, setSearchQuery] = useState('');
+ const [hoveredNode, setHoveredNode] = useState(null);
+ const [activeLocalNode, setActiveLocalNode] = useState(null);
+ const [nodeConnections, setNodeConnections] = useState>(new Set());
+
+ // D3 Zoom controller ref to trigger programmatically
+ const d3ZoomRef = useRef | null>(null);
+
+ // Initialize carnet filters with all carnets on mount
+ useEffect(() => {
+ setSelectedCarnetIds(carnets.map(c => c.id));
+ }, [carnets]);
+
+ // Static list of explicit links (Wikilinks)
+ const explicitWikiLinks = useMemo(() => {
+ return [
+ { source: 'n1', target: 'n1-b' },
+ { source: 'n3', target: 'n3-b' },
+ { source: 'bridge-1', target: 'n1' },
+ { source: 'bridge-1', target: 'n2' },
+ ];
+ }, []);
+
+ // Filter and process notes and carnets
+ const filteredNotes = useMemo(() => {
+ return notes.filter(n => {
+ // Exclude trashed/deleted notes
+ if (n.isDeleted) return false;
+ // Filter by selected carnets
+ return selectedCarnetIds.includes(n.carnetId);
+ });
+ }, [notes, selectedCarnetIds]);
+
+ // Compute all links based on state (Wikilinks + Semantic if enabled)
+ const graphData = useMemo(() => {
+ const noteMap = new Map();
+ filteredNotes.forEach(n => noteMap.set(n.id, n));
+
+ const nodes: D3Node[] = filteredNotes.map(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ return {
+ id: n.id,
+ title: n.title,
+ carnetId: n.carnetId,
+ carnetName: carnet?.name || 'Carnet Inconnu',
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ date: n.date,
+ snippet: n.content.split('.').slice(0, 3).join('.') + '.',
+ tags: n.tags || [],
+ degree: 0, // calculated below
+ x: undefined,
+ y: undefined
+ };
+ });
+
+ const links: D3Link[] = [];
+
+ // 1. Add Explicit Wikilinks if both target and source are inside filtered list
+ explicitWikiLinks.forEach(link => {
+ if (noteMap.has(link.source) && noteMap.has(link.target)) {
+ links.push({
+ source: link.source,
+ target: link.target,
+ type: 'wikilink',
+ strength: 1.0
+ });
+ }
+ });
+
+ // 2. Add Semantic Connections (Memory Echo) based on embedding similarities
+ if (showSemanticLinks) {
+ for (let i = 0; i < filteredNotes.length; i++) {
+ for (let j = i + 1; j < filteredNotes.length; j++) {
+ const ni = filteredNotes[i];
+ const nj = filteredNotes[j];
+
+ if (ni.embedding && nj.embedding) {
+ // Cosine vector similarity approximation / Euclidean inverse mapping
+ const dist = Math.sqrt(
+ Math.pow(ni.embedding[0] - nj.embedding[0], 2) +
+ Math.pow(ni.embedding[1] - nj.embedding[1], 2)
+ );
+ // Translate distance into similarity standard (0.0 - 1.0)
+ const similarity = Math.max(0, 1 - dist * 0.7);
+
+ if (similarity >= minSemanticStrength) {
+ // Avoid duplicate links with explicit ones to keep display clean
+ const hasExplicit = explicitWikiLinks.some(
+ ex => (ex.source === ni.id && ex.target === nj.id) || (ex.source === nj.id && ex.target === ni.id)
+ );
+
+ if (!hasExplicit) {
+ links.push({
+ source: ni.id,
+ target: nj.id,
+ type: 'semantic',
+ strength: similarity
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Calculate node connectivity degrees
+ nodes.forEach(node => {
+ const connectionsCount = links.filter(l =>
+ l.source === node.id || l.target === node.id ||
+ (typeof l.source === 'object' && (l.source as any).id === node.id) ||
+ (typeof l.target === 'object' && (l.target as any).id === node.id)
+ ).length;
+ node.degree = connectionsCount;
+ });
+
+ return { nodes, links };
+ }, [filteredNotes, carnets, showSemanticLinks, minSemanticStrength, selectedCarnetIds, explicitWikiLinks]);
+
+ // Handle Note connection highlights during hover
+ useEffect(() => {
+ if (!hoveredNode) {
+ setNodeConnections(new Set());
+ return;
+ }
+
+ const connected = new Set();
+ connected.add(hoveredNode.id);
+
+ graphData.links.forEach((l: any) => {
+ const srcId = typeof l.source === 'object' ? l.source.id : l.source;
+ const tgtId = typeof l.target === 'object' ? l.target.id : l.target;
+
+ if (srcId === hoveredNode.id) {
+ connected.add(tgtId);
+ } else if (tgtId === hoveredNode.id) {
+ connected.add(srcId);
+ }
+ });
+
+ setNodeConnections(connected);
+ }, [hoveredNode, graphData.links]);
+
+ // Main D3 force layout rendering loop
+ useEffect(() => {
+ if (!svgRef.current || !containerRef.current) return;
+
+ const width = containerRef.current.clientWidth;
+ const height = containerRef.current.clientHeight;
+
+ const svg = d3.select(svgRef.current);
+ svg.selectAll("*").remove();
+
+ // Base containment group
+ const mainGroup = svg.append("g");
+
+ // Configure zooming behaviors
+ const zoomBehavior = d3.zoom()
+ .scaleExtent([0.15, 5])
+ .on("zoom", (event) => {
+ mainGroup.attr("transform", event.transform);
+ });
+
+ d3ZoomRef.current = zoomBehavior;
+ svg.call(zoomBehavior);
+
+ // D3 nodes and links references mapped to copyable arrays
+ const simulationNodes = JSON.parse(JSON.stringify(graphData.nodes)) as D3Node[];
+ const simulationLinks = graphData.links.map(l => ({
+ source: l.source,
+ target: l.target,
+ type: l.type,
+ strength: l.strength
+ })) as D3Link[];
+
+ // Build the force simulation
+ const simulation = d3.forceSimulation(simulationNodes)
+ .force("link", d3.forceLink(simulationLinks)
+ .id(d => d.id)
+ .distance(d => d.type === 'wikilink' ? 100 : 140)
+ )
+ .force("charge", d3.forceManyBody().strength(-240))
+ .force("center", d3.forceCenter(width / 2, height / 2))
+ .force("collision", d3.forceCollide().radius(d => {
+ // Size proportional to connections: min 8px, max 20px
+ const rad = 8 + Math.min(d.degree * 2.5, 12);
+ return rad + 24;
+ }));
+
+ // Draw Links
+ const linkGroup = mainGroup.append("g")
+ .attr("class", "links-layer");
+
+ const link = linkGroup.selectAll("line")
+ .data(simulationLinks)
+ .enter()
+ .append("line")
+ .attr("stroke", d => d.type === 'semantic' ? '#4f46e5' : '#18181b')
+ .attr("stroke-opacity", d => d.type === 'semantic' ? 0.35 : 0.18)
+ .attr("stroke-width", d => d.type === 'semantic' ? 1.2 : 1.5)
+ .attr("stroke-dasharray", d => d.type === 'semantic' ? '4,4' : 'none');
+
+ // Draw Nodes
+ const nodeGroup = mainGroup.append("g")
+ .attr("class", "nodes-layer");
+
+ const node = nodeGroup.selectAll(".node")
+ .data(simulationNodes)
+ .enter()
+ .append("g")
+ .attr("class", "node cursor-pointer")
+ .on("click", (event, d) => {
+ event.stopPropagation();
+ handleSelectNode(d);
+ })
+ .on("mouseenter", (event, d) => {
+ setHoveredNode(d);
+ })
+ .on("mouseleave", () => {
+ setHoveredNode(null);
+ })
+ .call(d3.drag()
+ .on("start", dragStarted)
+ .on("drag", dragged)
+ .on("end", dragEnded) as any);
+
+ // Create central circles
+ node.append("circle")
+ .attr("r", d => 6 + Math.min(d.degree * 1.5, 9))
+ .attr("fill", d => d.color)
+ .attr("stroke", "rgba(255,255,255,0.95)")
+ .attr("stroke-width", 2)
+ .attr("class", "transition-all duration-300 dark:stroke-zinc-950")
+ .style("filter", "drop-shadow(0 2px 4px rgba(0,0,0,0.1))");
+
+ // Text labels overlay
+ node.append("text")
+ .attr("dy", d => 14 + Math.min(d.degree * 1.5, 9) + 4)
+ .attr("text-anchor", "middle")
+ .attr("class", "text-[10px] sm:text-[11px] font-sans font-semibold tracking-tight fill-zinc-850 dark:fill-zinc-300 select-none pointer-events-none")
+ .text(d => d.title.length > 22 ? d.title.substring(0, 20) + "..." : d.title)
+ .style("opacity", d => (d.degree > 2 || d.title.toLowerCase().includes(searchQuery.toLowerCase()) && searchQuery) ? 1 : 0.65);
+
+ // Search query search highlight ring
+ if (searchQuery) {
+ node.filter(d => d.title.toLowerCase().includes(searchQuery.toLowerCase()))
+ .append("circle")
+ .attr("r", d => 14 + Math.min(d.degree * 1.5, 9))
+ .attr("fill", "none")
+ .attr("stroke", "#06b6d4")
+ .attr("stroke-width", 2)
+ .attr("stroke-dasharray", "3,1")
+ .attr("class", "animate-[spin_20s_linear_infinite]");
+ }
+
+ // Node active local neighbor rings
+ if (activeLocalNode) {
+ const activeConns = getLocalNodeNeighbors(activeLocalNode.id);
+
+ node.style("opacity", d => {
+ return activeConns.has(d.id) ? 1.0 : 0.15;
+ });
+
+ link.style("stroke-opacity", (l: any) => {
+ const srcId = l.source.id;
+ const tgtId = l.target.id;
+ return (activeConns.has(srcId) && activeConns.has(tgtId)) ? 0.75 : 0.05;
+ });
+
+ // Highlight the focused local hub node with a neat accent circle
+ node.filter(d => d.id === activeLocalNode.id)
+ .append("circle")
+ .attr("r", d => 16 + Math.min(d.degree * 1.5, 9))
+ .attr("fill", "none")
+ .attr("stroke", "rgba(79, 70, 229, 0.4)")
+ .attr("stroke-width", 1.5)
+ .attr("stroke-opacity", 0.9);
+ }
+ // Node hover lighting state
+ else if (hoveredNode) {
+ const hoveredConns = new Set();
+ hoveredConns.add(hoveredNode.id);
+
+ graphData.links.forEach((l: any) => {
+ const srcId = typeof l.source === 'object' ? l.source.id : l.source;
+ const tgtId = typeof l.target === 'object' ? l.target.id : l.target;
+
+ if (srcId === hoveredNode.id) {
+ hoveredConns.add(tgtId);
+ } else if (tgtId === hoveredNode.id) {
+ hoveredConns.add(srcId);
+ }
+ });
+
+ // Subdue unconnected elements to 20% opacity
+ node.style("opacity", d => hoveredConns.has(d.id) ? 1.0 : 0.20);
+ link.style("stroke-opacity", (l: any) => {
+ const srcId = l.source.id;
+ const tgtId = l.target.id;
+ return (srcId === hoveredNode.id || tgtId === hoveredNode.id) ? 0.8 : 0.05;
+ });
+
+ // Hover scale update for primary
+ node.filter(d => d.id === hoveredNode.id)
+ .select("circle")
+ .attr("transform", "scale(1.3)");
+ }
+ // Normal / Base state
+ else {
+ node.style("opacity", 1.0);
+ link.style("stroke-opacity", d => d.type === 'semantic' ? 0.35 : 0.18);
+ }
+
+ // Run ticks
+ simulation.on("tick", () => {
+ link
+ .attr("x1", d => (d.source as any).x)
+ .attr("y1", d => (d.source as any).y)
+ .attr("x2", d => (d.target as any).x)
+ .attr("y2", d => (d.target as any).y);
+
+ node
+ .attr("transform", d => `translate(${d.x},${d.y})`);
+ });
+
+ // Zoom on local node view trigger
+ if (activeLocalNode && width && height) {
+ const targetNodeCopy = simulationNodes.find(n => n.id === activeLocalNode.id);
+ if (targetNodeCopy) {
+ // Step ticker synchronously to finalize force state layout
+ for (let i = 0; i < 40; ++i) simulation.tick();
+
+ const x = targetNodeCopy.x || width / 2;
+ const y = targetNodeCopy.y || height / 2;
+
+ svg.transition()
+ .duration(850)
+ .ease(d3.easeCubicOut)
+ .call(
+ zoomBehavior.transform,
+ d3.zoomIdentity.translate(width / 2, height / 2).scale(1.65).translate(-x, -y)
+ );
+ }
+ } else {
+ // Re-center whole graph
+ svg.transition()
+ .duration(800)
+ .ease(d3.easeCubicOut)
+ .call(zoomBehavior.transform, d3.zoomIdentity);
+ }
+
+ function dragStarted(event: any, d: any) {
+ if (!event.active) simulation.alphaTarget(0.25).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event: any, d: any) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragEnded(event: any, d: any) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ }
+
+ return () => {
+ simulation.stop();
+ };
+ }, [graphData, showSemanticLinks, minSemanticStrength, searchQuery, activeLocalNode, hoveredNode]);
+
+ // Compute local neighbors
+ const getLocalNodeNeighbors = (nodeId: string): Set => {
+ const list = new Set();
+ list.add(nodeId);
+ graphData.links.forEach(l => {
+ if (l.source === nodeId) {
+ list.add(typeof l.target === 'object' ? (l.target as any).id : l.target);
+ } else if (l.target === nodeId) {
+ list.add(typeof l.source === 'object' ? (l.source as any).id : l.source);
+ }
+ });
+ return list;
+ };
+
+ const handleSelectNode = (node: D3Node) => {
+ setActiveLocalNode(node);
+ };
+
+ const handleResetLocalView = () => {
+ setActiveLocalNode(null);
+ };
+
+ const handleZoom = (direction: 'in' | 'out' | 'fit') => {
+ if (!svgRef.current || !d3ZoomRef.current) return;
+ const svg = d3.select(svgRef.current);
+
+ if (direction === 'fit') {
+ svg.transition().duration(500).call(d3ZoomRef.current.transform, d3.zoomIdentity);
+ } else {
+ const factor = direction === 'in' ? 1.3 : 1 / 1.3;
+ svg.transition().duration(400).call(d3ZoomRef.current.scaleBy, factor);
+ }
+ };
+
+ const toggleCarnetSelector = (carnetId: string) => {
+ setSelectedCarnetIds(prev =>
+ prev.includes(carnetId)
+ ? prev.filter(id => id !== carnetId)
+ : [...prev, carnetId]
+ );
+ };
+
+ const selectAllCarnets = () => {
+ setSelectedCarnetIds(carnets.map(c => c.id));
+ };
+
+ const clearAllCarnets = () => {
+ setSelectedCarnetIds([]);
+ };
+
+ return (
+
+
+ {/* Dynamic Header Overlay */}
+
+ {activeLocalNode ? (
+
+
+ Graphe Global
+
+ ) : onClose ? (
+
+
+ Retour Notes
+
+ ) : (
+
+
+ Carte Sémantique
+
+ )}
+
+
+ {graphData.nodes.length} Nœuds
+ |
+ {graphData.links.length} Relations
+
+
+
+ {/* Global Hub Search Bar */}
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Chercher une note dans le graphe sémantique..."
+ className="w-full text-xs pl-9 pr-8 py-2.5 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/95 dark:bg-zinc-950/95 placeholder-concrete/60 shadow-lg outline-none focus:border-accent focus:ring-1 focus:ring-accent/10 transition-all text-ink dark:text-dark-ink font-medium"
+ />
+
+
+ {searchQuery && (
+ setSearchQuery('')}
+ className="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 text-concrete hover:text-ink hover:bg-black/5 dark:hover:bg-white/5 rounded-full"
+ >
+
+
+ )}
+
+
+
+ {/* Zoom controls (bottom right) */}
+
+
handleZoom('in')}
+ className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg text-concrete hover:text-ink transition-colors"
+ title="Zoomer (+)"
+ >
+
+
+
+
handleZoom('out')}
+ className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg text-concrete hover:text-ink transition-colors"
+ title="Dézoomer (-)"
+ >
+
+
+
+
+
+
handleZoom('fit')}
+ className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg text-concrete hover:text-accent transition-colors"
+ title="Ajuster la vue"
+ >
+
+
+
+
+ {/* Floating Controls Panel (top right) */}
+
+
+
+
+
+ Paramètres du Graphe
+
+ {
+ setShowSemanticLinks(true);
+ setMinSemanticStrength(0.40);
+ selectAllCarnets();
+ }}
+ className="text-[9px] font-bold uppercase text-accent hover:text-accent/80 transition-colors"
+ title="Rétablir par défaut"
+ >
+ Reset
+
+
+
+
+ {/* Semantic Link Toggle Details */}
+
+
+
+
+ Liens sémantiques
+
+ setShowSemanticLinks(e.target.checked)}
+ className="w-4 h-4 text-accent border-gray-300 rounded focus:ring-accent"
+ />
+
+
+ Visualiser la couche d'affinité IA générée par embeddings sémantiques (Memory Echo).
+
+
+
+ {/* Slider for semantic filtering threshold - Displayed only if activated */}
+ {showSemanticLinks && (
+
+
+ Force minimum sémantique
+
+ {(minSemanticStrength * 100).toFixed(0)}%
+
+
+
+
+ 0.2
+ setMinSemanticStrength(parseFloat(e.target.value))}
+ className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-indigo-600"
+ />
+ 0.85
+
+
+ )}
+
+ {/* Filter by Carnets with Checkboxes */}
+
+
+
+
+ Filtrer par Carnet ({selectedCarnetIds.length})
+
+
+ Tous
+ •
+ Aucun
+
+
+
+
+ {carnets.map(c => {
+ const isChecked = selectedCarnetIds.includes(c.id);
+ const carnetColor = CARNET_COLOR_PALETTE[c.id] || DEFAULT_CARNET_COLOR;
+ return (
+
+
+
+ {c.name}
+
+
+ toggleCarnetSelector(c.id)}
+ className="w-3.5 h-3.5 text-accent border-gray-300 rounded focus:ring-accent"
+ />
+
+ );
+ })}
+
+
+
+
+
+
+ {/* Dynamic Tooltip Hover UI Card (In case of node hovering) */}
+
+ {hoveredNode && !activeLocalNode && (
+
+
+
+
+ {hoveredNode.carnetName}
+
+
+
+ Modifié le : {hoveredNode.date}
+
+
+
+
+ {hoveredNode.title}
+
+
+
+ {/* Micro Metrics stats */}
+
+
+
Connexions
+
{hoveredNode.degree}
+
+
+
+
Tags détectés
+
{hoveredNode.tags.length || 0}
+
+
+
+
+ Cliquez pour isoler / modifier
+
+
+ )}
+
+
+ {/* SVG Core Render canvas */}
+
+
+
+ {/* State D: Note focus right panel slider (280px width) */}
+
+ {activeLocalNode && (
+
+ {/* Panel header and close button */}
+
+
+
+
+ Aperçu de Note
+
+
+
+ Fermer
+
+
+
+ {/* Note details */}
+
+
+
+ {activeLocalNode.carnetName}
+
+
+
+ {activeLocalNode.title}
+
+
+
+
+ Dernier update : {activeLocalNode.date}
+
+
+
+
+ {/* Snippet body content */}
+
+
+
Résumé / Extrait
+
+ "{activeLocalNode.snippet}"
+
+
+
+ {/* Relationship listing */}
+
+
+ Éléments connectés ({getLocalNodeNeighbors(activeLocalNode.id).size - 1})
+
+
+
+ {notes
+ .filter(n => n.id !== activeLocalNode.id && getLocalNodeNeighbors(activeLocalNode.id).has(n.id))
+ .map(neighbor => {
+ return (
+
{
+ const foundNode = graphData.nodes.find(v => v.id === neighbor.id);
+ if (foundNode) handleSelectNode(foundNode);
+ }}
+ className="flex items-center justify-between text-[10px] p-2 bg-neutral-50 dark:bg-neutral-900/60 rounded-xl hover:bg-neutral-100 cursor-pointer border border-transparent hover:border-border transition-colors group"
+ >
+
+
+ {neighbor.title}
+
+
+ Séléctionner
+
+
+ );
+ })}
+
+
+
+ {/* Tags panel detail */}
+ {activeLocalNode.tags && activeLocalNode.tags.length > 0 && (
+
+
Index de tags
+
+ {activeLocalNode.tags.map((t, idx) => (
+
+ {t.label}
+
+ ))}
+
+
+ )}
+
+
+ {/* CTA action bottom block */}
+
+ onOpenNote(activeLocalNode.id)}
+ className="w-full py-3.5 bg-ink text-paper dark:bg-neutral-50 dark:text-zinc-950 rounded-xl text-xs font-bold uppercase tracking-widest hover:opacity-95 transition-all text-center flex items-center justify-center gap-1.5 shadow-xl shadow-black/10 scale-100 active:scale-95"
+ >
+
+ Ouvrir la note
+
+
+
+ )}
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/HierarchicalCarnetSelector.tsx b/architectural-grid1/src/components/HierarchicalCarnetSelector.tsx
new file mode 100644
index 0000000..b9aea46
--- /dev/null
+++ b/architectural-grid1/src/components/HierarchicalCarnetSelector.tsx
@@ -0,0 +1,208 @@
+import React, { useState, useMemo } from 'react';
+import {
+ ChevronRight,
+ ChevronDown,
+ Folder,
+ FolderOpen,
+ Check,
+ Search
+} from 'lucide-react';
+import { Carnet } from '../types';
+import { motion, AnimatePresence } from 'motion/react';
+
+interface HierarchicalCarnetSelectorProps {
+ carnets: Carnet[];
+ selectedId: string | null;
+ onSelect: (id: string) => void;
+ className?: string;
+ placeholder?: string;
+}
+
+export const HierarchicalCarnetSelector: React.FC = ({
+ carnets,
+ selectedId,
+ onSelect,
+ className = "",
+ placeholder = "Sélectionner un carnet..."
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [expandedIds, setExpandedIds] = useState>(new Set(['1', '4'])); // Default expand some
+
+ const selectedCarnet = carnets.find(c => c.id === selectedId);
+
+ // Derive the path for display
+ const path = useMemo(() => {
+ if (!selectedCarnet) return [];
+ const trail: Carnet[] = [];
+ let current = selectedCarnet;
+ while (current) {
+ trail.unshift(current);
+ if (!current.parentId) break;
+ const parent = carnets.find(c => c.id === current.parentId);
+ if (!parent) break;
+ current = parent;
+ }
+ return trail;
+ }, [selectedCarnet, carnets]);
+
+ const toggleExpand = (e: React.MouseEvent, id: string) => {
+ e.stopPropagation();
+ const newExpanded = new Set(expandedIds);
+ if (newExpanded.has(id)) {
+ newExpanded.delete(id);
+ } else {
+ newExpanded.add(id);
+ }
+ setExpandedIds(newExpanded);
+ };
+
+ const filteredCarnets = useMemo(() => {
+ if (!searchQuery) return carnets;
+ return carnets.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()));
+ }, [carnets, searchQuery]);
+
+ const renderTree = (parentId?: string, level = 0) => {
+ const children = carnets.filter(c => c.parentId === parentId);
+ if (children.length === 0) return null;
+
+ return (
+ 0 ? "ml-4 border-l border-border/40 pl-2" : ""}>
+ {children.map(carnet => {
+ const isExpanded = expandedIds.has(carnet.id) || searchQuery.length > 0;
+ const hasChildren = carnets.some(c => c.parentId === carnet.id);
+ const isSelected = selectedId === carnet.id;
+
+ // If searching and this carnet doesn't match AND none of its children match, skip it
+ if (searchQuery && !carnet.name.toLowerCase().includes(searchQuery.toLowerCase())) {
+ const hasMatchingChild = (id: string): boolean => {
+ const childrenOfId = carnets.filter(c => c.parentId === id);
+ return childrenOfId.some(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()) || hasMatchingChild(c.id));
+ };
+ if (!hasMatchingChild(carnet.id)) return null;
+ }
+
+ return (
+
+
{
+ onSelect(carnet.id);
+ if (!searchQuery) setIsOpen(false);
+ }}
+ className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all group
+ ${isSelected ? 'bg-accent/10 text-accent font-bold' : 'hover:bg-slate-50 dark:hover:bg-white/5 text-ink'}`}
+ >
+
+ {hasChildren ? (
+ toggleExpand(e, carnet.id)}
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded transition-colors"
+ >
+ {isExpanded ? : }
+
+ ) : null}
+
+
+
+ {isExpanded && hasChildren ? : }
+
+
+
{carnet.name}
+
+ {isSelected &&
}
+
+
+
+ {isExpanded && (
+
+ {renderTree(carnet.id, level + 1)}
+
+ )}
+
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
setIsOpen(!isOpen)}
+ className="w-full bg-slate-50 dark:bg-black/20 border border-border/80 rounded-xl px-4 py-4 text-sm outline-none focus:ring-4 ring-accent/5 focus:border-accent/40 transition-all cursor-pointer text-ink flex items-center gap-3"
+ >
+
+
+ {path.length > 0 ? (
+
+ {path.map((item, i) => (
+
+ {i > 0 && / }
+
+ {item.name}
+
+
+ ))}
+
+ ) : (
+
{placeholder}
+ )}
+
+
+
+
+
+ {isOpen && (
+ <>
+ setIsOpen(false)}
+ />
+
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Filtrer les carnets..."
+ className="w-full bg-white border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-accent transition-colors"
+ />
+
+
+
+
+ {renderTree(undefined)}
+
+
+
+
+ Structure des carnets
+
+ setIsOpen(false)}
+ className="text-[10px] font-bold text-accent hover:underline"
+ >
+ Fermer
+
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/InsightsView.tsx b/architectural-grid1/src/components/InsightsView.tsx
new file mode 100644
index 0000000..fd76491
--- /dev/null
+++ b/architectural-grid1/src/components/InsightsView.tsx
@@ -0,0 +1,482 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ Network,
+ Lightbulb,
+ Layers,
+ Sparkles,
+ ArrowRight,
+ RefreshCw,
+ Trophy,
+ Zap,
+ Tag,
+ Link as LinkIcon,
+ Menu,
+ FileText,
+ AlertCircle,
+ Clock,
+ ChevronRight,
+ TrendingUp,
+ Sliders,
+ CheckCircle2,
+ Lock
+} from 'lucide-react';
+import { Note, NoteCluster, BridgeNote, ConnectionSuggestion } from '../types';
+import { runClustering, detectBridges, calculateCentroid, getMostCentralNoteTitles } from '../services/clusteringService';
+import { nameCluster, suggestBridgeIdeas } from '../services/geminiService';
+import { NetworkGraph } from './NetworkGraph';
+
+interface InsightsViewProps {
+ notes: Note[];
+ onUpdateNotes: (updatedNotes: Note[]) => void;
+ onNoteSelect: (noteId: string) => void;
+ onOpenSidebar?: () => void;
+}
+
+export const InsightsView: React.FC = ({
+ notes,
+ onUpdateNotes,
+ onNoteSelect,
+ onOpenSidebar
+}) => {
+ const [isCalculating, setIsCalculating] = useState(false);
+ const [clusters, setClusters] = useState([]);
+ const [bridgeNotes, setBridgeNotes] = useState([]);
+ const [suggestions, setSuggestions] = useState([]);
+ const [selectedClusterId, setSelectedClusterId] = useState(null);
+
+ // Mobile responsive view selector
+ const [viewMode, setViewMode] = useState<'graph' | 'dashboard'>('dashboard');
+
+ // Interactive automatic recalculation parameters simulator / status
+ const [lastSyncTime, setLastSyncTime] = useState(() => {
+ return new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
+ });
+
+ // Track changes to notes since last calculation to show a conditions indicator
+ const [notesModifiedCount, setNotesModifiedCount] = useState(0);
+
+ // Monitor edits to emulate the state "Recalcul quotidien planifié" or condition (>10 notes modified)
+ useEffect(() => {
+ // Whenever notes length or contents change, we simulate a tally
+ setNotesModifiedCount(prev => Math.min(prev + 1, 12));
+ }, [notes.length]);
+
+ const performAnalysis = async () => {
+ setIsCalculating(true);
+ try {
+ // 1. Run clustering (DBSCAN acting on density with outlier filtering, label -1 is outlier)
+ const { clusters: newClusters } = runClustering(notes);
+
+ // 2. Name clusters (find the 5 notes closest to each cluster's centroid vector)
+ const namedClusters = await Promise.all(newClusters.map(async (c) => {
+ const centroid = calculateCentroid(c.noteIds, notes);
+ // Find the 5 most central notes (closest to the cluster centroid by cosine similarity)
+ const clusterNoteSummaries = getMostCentralNoteTitles(c.noteIds, centroid, notes, 5);
+
+ const name = await nameCluster(clusterNoteSummaries);
+
+ return { ...c, name, centroid };
+ }));
+
+ // 3. Update notes with cluster IDs
+ const updatedNotes = notes.map(n => {
+ const cluster = namedClusters.find(c => c.noteIds.includes(n.id));
+ return { ...n, clusterId: cluster?.id };
+ });
+ onUpdateNotes(updatedNotes);
+
+ // 4. Detect bridges (notes exhibiting similarity > 0.5 to >= 2 clusters)
+ const bridges = detectBridges(updatedNotes, namedClusters);
+
+ // 5. Build suggestions for unconnected cluster pairs
+ // A pair is unconnected if there are no existing bridge notes linking them
+ const newSuggestions: ConnectionSuggestion[] = [];
+ if (namedClusters.length >= 2) {
+ const unconnectedPairs: { cA: NoteCluster; cB: NoteCluster }[] = [];
+
+ for (let i = 0; i < namedClusters.length; i++) {
+ for (let j = i + 1; j < namedClusters.length; j++) {
+ const cA = namedClusters[i];
+ const cB = namedClusters[j];
+
+ // Check if any bridge note connects these two clusters
+ const hasBridge = bridges.some(b =>
+ b.connectedClusterIds.includes(cA.id) && b.connectedClusterIds.includes(cB.id)
+ );
+
+ if (!hasBridge) {
+ unconnectedPairs.push({ cA, cB });
+ }
+ }
+ }
+
+ // Generate bridge suggestions for the top 3 unconnected pairs
+ for (let k = 0; k < Math.min(unconnectedPairs.length, 3); k++) {
+ const { cA, cB } = unconnectedPairs[k];
+ const cA_notes = updatedNotes.filter(n => cA.noteIds.includes(n.id)).map(n => n.title).slice(0, 3).join(', ');
+ const cB_notes = updatedNotes.filter(n => cB.noteIds.includes(n.id)).map(n => n.title).slice(0, 3).join(', ');
+
+ const bridgeIdeas = await suggestBridgeIdeas(cA.name, cB.name, cA_notes, cB_notes);
+ bridgeIdeas.forEach((idea, idx) => {
+ newSuggestions.push({
+ id: `suggestion-${cA.id}-${cB.id}-${idx}`,
+ ...idea,
+ clusterAId: cA.id,
+ clusterBId: cB.id
+ });
+ });
+ }
+ }
+
+ setClusters(namedClusters);
+ setBridgeNotes(bridges);
+ setSuggestions(newSuggestions);
+ setLastSyncTime(new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }));
+ setNotesModifiedCount(0); // Reset modified counter upon successful clustering recalculation
+ } catch (error) {
+ console.error("Analysis failed:", error);
+ } finally {
+ setIsCalculating(false);
+ }
+ };
+
+ useEffect(() => {
+ if (notes.some(n => n.embedding) && clusters.length === 0) {
+ performAnalysis();
+ }
+ }, [notes]);
+
+ const bridgeList = useMemo(() => {
+ return bridgeNotes.map(b => {
+ const note = notes.find(n => n.id === b.noteId);
+ return { ...b, title: note?.title || 'Note de passage' };
+ });
+ }, [bridgeNotes, notes]);
+
+ // Compute isolated clusters (ones with no bridge notes spanning to them)
+ const isolatedClusters = useMemo(() => {
+ const networkedClusterIds = new Set(bridgeNotes.flatMap(b => b.connectedClusterIds));
+ return clusters.filter(c => !networkedClusterIds.has(c.id));
+ }, [clusters, bridgeNotes]);
+
+ // Find currently selected cluster info for the zoom drilldown list
+ const selectedCluster = useMemo(() => {
+ return clusters.find(c => c.id === selectedClusterId);
+ }, [clusters, selectedClusterId]);
+
+ const selectedClusterNotes = useMemo(() => {
+ if (!selectedCluster) return [];
+ return notes.filter(n => selectedCluster.noteIds.includes(n.id));
+ }, [notes, selectedCluster]);
+
+ return (
+
+ {/* Header with Mobile Drawer Trigger & Responsiveness Tab controls */}
+
+
+ {onOpenSidebar && (
+
+
+
+ )}
+
+
+
+
+
+
Analyses & Cartographie
+
+
Modèles sémantiques & clusters de connaissances
+
+
+
+
+ {/* Mobile Tab Switcher */}
+
+ setViewMode('graph')}
+ className={`px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg transition-all ${viewMode === 'graph' ? 'bg-white dark:bg-black text-ink shadow-sm' : 'text-concrete'}`}
+ >
+ Réseau Graphique
+
+ setViewMode('dashboard')}
+ className={`px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg transition-all ${viewMode === 'dashboard' ? 'bg-white dark:bg-black text-ink shadow-sm' : 'text-concrete'}`}
+ >
+ Analyses & Ponts
+
+
+
+
+ {isCalculating ? : }
+ {isCalculating ? 'Calcul...' : 'Re-analyser'}
+
+
+
+
+
+ {/* Left: Interactive Canvas Network Graph View */}
+
+
+
+
+ {/* Right: Insight Dashboard Column */}
+
+
+
+ {/* Active Cluster Inspection Drawer / Side Card */}
+
+ {selectedCluster && (
+
+
+
+
+ Focus Cluster Activé
+
{selectedCluster.name}
+
+
setSelectedClusterId(null)}
+ className="p-1 px-2.5 bg-black/5 dark:bg-white/5 hover:bg-black/10 text-xs font-bold rounded-lg uppercase tracking-wider transition-colors"
+ >
+ Fermer
+
+
+
+
+
Cet ensemble thématique réunit {selectedClusterNotes.length} notes complémentaires. Cliquez sur une note pour y accéder directement :
+
+ {selectedClusterNotes.map(note => (
+ onNoteSelect(note.id)}
+ className="w-full text-left p-2.5 rounded-lg bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 text-xs font-medium text-ink dark:text-dark-ink flex items-center justify-between gap-3 group transition-all"
+ >
+ {note.title || 'Note sans titre'}
+
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Stats Highlights Header */}
+
+
+
+
+ Clusters Actifs
+
+
+
{clusters.length}
+
Détectés sans à priori
+
+
+
+
+
+ Notes-Ponts
+
+
+
{bridgeNotes.length}
+
Passerelles d'idées
+
+
+
+
+ {/* NEW SECTION: Auto Recalculator Control Dashboard Section */}
+
+
+
+
+
Système de Recalcul
+
+
+ Synchronisé
+
+
+
+
+
+
CRON PLANIFIÉ
+
+ Quotidien (04:00)
+
+
+
+
DERNIÈRE SYNCHRONISATION
+
+ Aujourd'hui, {lastSyncTime}
+
+
+
+
+ {/* Recalcul Trigger Metrics */}
+
+
+
+ Notes éditées depuis recul :
+ {notesModifiedCount} / 10 modifs
+
+
+
+
+
Le recalcul incrémental se déclenche automatiquement si modification de {'>'} 10 notes ou variation d'embeddings {'>'} 5%.
+
+
+
+
+ {/* Isolated Clusters List */}
+
+
+
+
+
Clusters Isolés ({isolatedClusters.length})
+
+
Sans points d'accroche
+
+
+ {isolatedClusters.map(c => (
+
setSelectedClusterId(c.id)}
+ className="p-3.5 rounded-xl bg-white dark:bg-zinc-800 border border-border/30 hover:border-black/10 flex items-center justify-between cursor-pointer"
+ >
+
+
+ Non connecté
+
+
+ ))}
+ {isolatedClusters.length === 0 && (
+
+ Tous les clusters thématiques sont liés par au moins un point de passage sémantique !
+
+ )}
+
+
+
+ {/* Bridge Notes Section */}
+
+
+
+
Notes-Ponts Influentes
+
+
+ {bridgeList.map(bridge => (
+
onNoteSelect(bridge.noteId)}
+ className="p-4 rounded-xl bg-white dark:bg-zinc-800 border border-border/30 hover:border-ochre/40 hover:shadow-sm transition-all cursor-pointer group"
+ >
+
+
{bridge.title}
+
+ Lien : {(bridge.bridgeScore * 100).toFixed(0)}%
+
+
+
+ {bridge.connectedClusterIds.map(cid => {
+ const c = clusters.find(cl => cl.id === cid);
+ return (
+
{
+ e.stopPropagation();
+ setSelectedClusterId(cid);
+ }}
+ className="flex items-center gap-1.5 px-2 py-0.5 bg-black/[0.02] dark:bg-white/[0.02] border border-border/30 rounded-md hover:border-concrete/40 transition-colors"
+ >
+
+
{c?.name}
+
+ );
+ })}
+
+
+ ))}
+ {bridgeList.length === 0 && !isCalculating && (
+
+ Aucune note-pont significative n'a été détectée. Créez des notes transversales pour forger de nouveaux liens créatifs.
+
+ )}
+
+
+
+ {/* Connection Suggestions */}
+
+
+
+
Opportunités de Connexion (Ponts Suggérés)
+
+
+ {suggestions.map((s) => (
+
+
+
+
+ Relier {clusters.find(c => c.id === s.clusterAId)?.name} & {clusters.find(c => c.id === s.clusterBId)?.name}
+
+
+
{s.title}
+
{s.description}
+
+
+ {s.reasoning}
+
+
+ ))}
+ {isCalculating && (
+
+ {[1, 2].map(i => (
+
+ ))}
+
+ )}
+ {!isCalculating && suggestions.length === 0 && (
+
+ Toutes vos thématiques clés sont déjà formidablement interconnectées !
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/LandingPage.tsx b/architectural-grid1/src/components/LandingPage.tsx
new file mode 100644
index 0000000..5921d4d
--- /dev/null
+++ b/architectural-grid1/src/components/LandingPage.tsx
@@ -0,0 +1,2430 @@
+import React, { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ BrainCircuit,
+ Search,
+ MessageSquare,
+ Zap,
+ Cpu,
+ Workflow,
+ Globe,
+ Shield,
+ ArrowRight,
+ Sparkles,
+ Layers,
+ Box,
+ FileText,
+ Activity,
+ ArrowRightLeft,
+ Check,
+ Languages,
+ History,
+ Network,
+ Clock,
+ BookOpen,
+ Sliders,
+ CheckSquare,
+ Lock,
+ Compass,
+ HelpCircle
+} from 'lucide-react';
+
+// Define the shape of our localized dictionary keys
+interface LangDict {
+ nav_features: string;
+ nav_agents: string;
+ nav_brainstorm: string;
+ nav_pricing: string;
+ nav_architecture: string;
+ nav_login: string;
+ nav_start: string;
+
+ hero_badge: string;
+ hero_title_1: string;
+ hero_title_italic: string;
+ hero_desc: string;
+ hero_cta_start: string;
+ hero_cta_features: string;
+
+ floating_card_1: string;
+ floating_card_1_text: string;
+ floating_card_2: string;
+ floating_card_2_text: string;
+ floating_card_2_desc: string;
+
+ features_badge: string;
+ features_title_1: string;
+ features_title_2: string;
+ features_subtitle: string;
+
+ f1_title: string;
+ f1_desc: string;
+ f2_title: string;
+ f2_desc: string;
+ f3_title: string;
+ f3_desc: string;
+
+ agents_badge: string;
+ agents_title: string;
+ agents_subtitle: string;
+
+ agent_scraper_desc: string;
+ agent_researcher_desc: string;
+ agent_slide_desc: string;
+ agent_monitor_desc: string;
+ agent_diagram_desc: string;
+ agent_custom_desc: string;
+
+ brain_badge: string;
+ brain_title: string;
+ brain_item1_title: string;
+ brain_item1_desc: string;
+ brain_item2_title: string;
+ brain_item2_desc: string;
+ brain_item3_title: string;
+ brain_item3_desc: string;
+
+ brain_node1_title: string;
+ brain_node1_text: string;
+ brain_node2_title: string;
+ brain_node2_text: string;
+
+ tech_badge: string;
+ tech_title: string;
+ tech_tier1_label: string;
+ tech_tier1_desc: string;
+ tech_tier2_label: string;
+ tech_tier2_desc: string;
+ tech_tier3_label: string;
+ tech_tier3_desc: string;
+
+ price_badge: string;
+ price_title: string;
+ price_desc: string;
+ price_monthly: string;
+ price_annual: string;
+ price_popular: string;
+ price_free_name: string;
+ price_free_price: string;
+ price_free_desc: string;
+ price_pro_name: string;
+ price_pro_price: string;
+ price_pro_desc: string;
+ price_biz_name: string;
+ price_biz_price: string;
+ price_biz_desc: string;
+ price_ent_name: string;
+ price_ent_price: string;
+ price_ent_desc: string;
+ price_period_month: string;
+ price_period_annual: string;
+ price_cta_start: string;
+ price_cta_pro: string;
+ price_cta_biz: string;
+ price_cta_ent: string;
+
+ price_free_features: string[];
+ price_pro_features: string[];
+ price_biz_features: string[];
+ price_ent_features: string[];
+
+ byok_badge: string;
+ byok_title: string;
+ byok_desc: string;
+ byok_col1_title: string;
+ byok_col1_desc: string;
+ byok_col2_title: string;
+ byok_col2_desc: string;
+ byok_config_title: string;
+
+ final_cta_title: string;
+ final_cta_title_italic: string;
+ final_cta_desc: string;
+ final_cta_button: string;
+
+ eco_badge: string;
+ eco_title_1: string;
+ eco_title_2: string;
+ eco_desc: string;
+ eco_button: string;
+ eco_original: string;
+ eco_translated: string;
+ eco_status: string;
+
+ footer_desc: string;
+ footer_col1_title: string;
+ footer_col2_title: string;
+ footer_col3_title: string;
+
+ workspace_title_editor: string;
+ workspace_title_graph: string;
+ workspace_title_agents: string;
+ workspace_title_reviews: string;
+ workspace_title_history: string;
+ workspace_notes_available: string;
+ workspace_new_doc: string;
+ workspace_local_reading: string;
+ workspace_graph_map_title: string;
+ workspace_graph_map_desc: string;
+ workspace_graph_connected: string;
+ workspace_graph_status: string;
+ workspace_agents_title: string;
+ workspace_active_prompt: string;
+ workspace_rag_active: string;
+ workspace_consigne: string;
+ workspace_secure_chat: string;
+ workspace_processor: string;
+ workspace_recall_title: string;
+ workspace_recall_desc: string;
+ workspace_show_answer: string;
+ workspace_correct_answer: string;
+ workspace_study_memory: string;
+ workspace_snapshot_title: string;
+ workspace_snapshot_desc: string;
+ workspace_restore: string;
+ workspace_state_saved: string;
+}
+
+const FR: LangDict = {
+ nav_features: "Fonctionnalités",
+ nav_agents: "Agents IA",
+ nav_brainstorm: "Brainstorm",
+ nav_pricing: "Tarification",
+ nav_architecture: "Architecture",
+ nav_login: "Se connecter",
+ nav_start: "Commencez",
+
+ hero_badge: "Augmenté par l'Intelligence Artificielle",
+ hero_title_1: "Votre second cerveau,",
+ hero_title_italic: "enfin amplifié.",
+ hero_desc: "Momento n'est pas qu'une simple application de notes. C'est un écosystème intelligent qui connecte, analyse et développe vos idées en temps réel grâce à 6 types d'agents IA et une recherche sémantique de pointe.",
+ hero_cta_start: "S'inscrire maintenant",
+ hero_cta_features: "Voir les fonctionnalités",
+
+ floating_card_1: "Memory Echo",
+ floating_card_1_text: '\"Connexion détectée avec votre projet de design durable de Mars 2024...\"',
+ floating_card_2: "Brainstorm Live",
+ floating_card_2_text: "+12 idées générées",
+ floating_card_2_desc: "Utilisateurs actifs en simultané",
+
+ features_badge: "Capacités IA",
+ features_title_1: "Une intelligence fluide,",
+ features_title_2: "intégrée à chaque mot.",
+ features_subtitle: "Momento orchestre vos idées grâce à une architecture multi-fournisseurs.",
+
+ f1_title: "Recherche Sémantique",
+ f1_desc: "Ne cherchez plus par mots-clés. Trouvez par concept. Notre moteur hybride Vector + FTS comprend l'intention derrière vos notes.",
+ f2_title: "Chat RAG Contextuel",
+ f2_desc: "Discutez avec votre savoir. Nos agents lisent vos notes, explorent le web et analysent vos documents pour répondre avec précision.",
+ f3_title: "Écriture Augmentée",
+ f3_desc: "Reformulation, suggestions de titres, tagging automatique et résumés. L'IA travaille en arrière-plan pour structurer votre pensée.",
+
+ agents_badge: "Agents Spécialisés",
+ agents_title: "Déléguez le travail complexe.",
+ agents_subtitle: "6 types d'agents IA autonomes pour automatiser vos recherches, vos résumés et vos présentations.",
+
+ agent_scraper_desc: "Scrape des URLs, parse les flux RSS et synthétise l'info avec placement d'images intelligent.",
+ agent_researcher_desc: "Génère des requêtes complexes, explore les sources web et rédige des notes de recherche structurées.",
+ agent_slide_desc: "Transforme vos notes en présentations PowerPoint professionnelles ou Slides HTML Interactives.",
+ agent_monitor_desc: "Analyse continuellement vos carnets pour détecter les tendances et les nouveaux insights.",
+ agent_diagram_desc: "Convertit vos idées en diagrammes Excalidraw fluides (Mindmaps, Flowcharts) avec auto-layout.",
+ agent_custom_desc: "Définissez vos propres agents avec des rôles et des sources de données spécifiques.",
+
+ brain_badge: "Vagues de Pensée",
+ brain_title: "Brainstorming radial en temps réel.",
+ brain_item1_title: "Génération par Vagues",
+ brain_item1_desc: "Variations, Analogies, puis Disruptions. L'IA pousse votre concept initial dans ses retranchements.",
+ brain_item2_title: "Collaboration Native",
+ brain_item2_desc: "Curseurs fantômes IA, avatars synchronisés et déplacement de nœuds en temps réel.",
+ brain_item3_title: "Export Sémantique",
+ brain_item3_desc: "Convertissez tout votre brainstorm en notes structurées d'un seul clic.",
+
+ brain_node1_title: "DISRUPTION",
+ brain_node1_text: "Architecture Modulaire 2.0",
+ brain_node2_title: "ANALOGIE",
+ brain_node2_text: "Le cycle des marées",
+
+ tech_badge: "Architecture & Fournisseurs",
+ tech_title: "Connectez votre propre intelligence.",
+ tech_tier1_label: "Tags",
+ tech_tier1_desc: "Indépendamment configurable avec n'importe quel modèle.",
+ tech_tier2_label: "Embeddings",
+ tech_tier2_desc: "Indépendamment configurable avec n'importe quel modèle.",
+ tech_tier3_label: "Chat RAG",
+ tech_tier3_desc: "Indépendamment configurable avec n'importe quel modèle.",
+
+ price_badge: "Plans & Tarification",
+ price_title: "Choisissez votre niveau d'amplification.",
+ price_desc: "Des options flexibles pour les esprits créatifs, de l'usage individuel aux grandes organisations.",
+ price_monthly: "Mensuel",
+ price_annual: "Annuel",
+ price_popular: "Le plus populaire",
+ price_free_name: "Basic",
+ price_free_price: "Gratuit",
+ price_free_desc: "Pour découvrir la magie de Momento.",
+ price_pro_name: "Pro",
+ price_pro_price: "9,90€",
+ price_pro_desc: "Pour les consultants et créateurs exigeants.",
+ price_biz_name: "Business",
+ price_biz_price: "29,90€",
+ price_biz_desc: "Pour les équipes et chefs de produit.",
+ price_ent_name: "Enterprise",
+ price_ent_price: "49,90€",
+ price_ent_desc: "Mémoire organisationnelle sécurisée.",
+ price_period_month: "/mois",
+ price_period_annual: "/mois, facturé annuellement",
+ price_cta_start: "Commencer",
+ price_cta_pro: "Passer Pro",
+ price_cta_biz: "Choisir Business",
+ price_cta_ent: "Contacter Ventes",
+
+ price_free_features: ["100 Notes max", "3 Carnets", "50 crédits IA (Lifetime)", "Recherche sémantique", "Historique 7 jours"],
+ price_pro_features: ["Notes illimitées", "BYOK (OpenAI/Anthropic)", "200 recherches sémantiques", "Agents (12 runs/mois)", "Historique 30 jours", "Support Email"],
+ price_biz_features: ["10 Collaborateurs inclus", "BYOK (13 fournisseurs)", "1000 recherches sémantiques", "Agents (60 runs/mois)", "Brainstorm illimité", "Accès API"],
+ price_ent_features: ["Tout Business", "Agents illimités", "SSO / SAML", "Audit Logs & SLA", "Support Dédié", "Onboarding Live"],
+
+ byok_badge: "Technologie Cloud Ouverte",
+ byok_title: "La stratégie BYOK",
+ byok_desc: "Vous possédez déjà des clés API OpenAI, Anthropic ou Google ? Connectez-les directement à Momento. Utilisez l'IA sans limites de crédits imposées, en payant uniquement ce que vous consommez chez votre fournisseur favori.",
+ byok_col1_title: "Pas de lock-in",
+ byok_col1_desc: "Changez de fournisseur en 1 clic.",
+ byok_col2_title: "Coûts optimisés",
+ byok_col2_desc: "Payez le prix direct API.",
+ byok_config_title: "Config Multi-Fournisseurs",
+
+ final_cta_title: "Prêt à libérer votre",
+ final_cta_title_italic: "plein potentiel ?",
+ final_cta_desc: "Rejoignez des milliers de chercheurs, designers et penseurs qui utilisent déjà Momento pour construire leur futur.",
+ final_cta_button: "Lancer Momento",
+
+ eco_badge: "Écosystème Momento",
+ eco_title_1: "Traduisez vos documents.",
+ eco_title_2: "Formatage préservé.",
+ eco_desc: "Le seul traducteur qui préserve les graphiques, tables des matières, formes et en-têtes — exactement tels qu'ils étaient. Prolongez l'intelligence de vos notes à l'international.",
+ eco_button: "Découvrir le traducteur",
+ eco_original: "original.pdf",
+ eco_translated: "translated.pdf",
+ eco_status: "Structure Intacte ✓",
+
+ footer_desc: "Le second cerveau amplifié par l'IA. Pensé pour les esprits créatifs.",
+ footer_col1_title: "Product",
+ footer_col2_title: "Community",
+ footer_col3_title: "Legal",
+
+ workspace_title_editor: "Éditeur de Notes",
+ workspace_title_graph: "Graphe Spatial",
+ workspace_title_agents: "Enrichissement IA",
+ workspace_title_reviews: "Révisions Actives",
+ workspace_title_history: "Snapshots Temporels",
+ workspace_notes_available: "Documents Locaux",
+ workspace_new_doc: "+ Créer Note",
+ workspace_local_reading: "Lecture du stockage local...",
+ workspace_graph_map_title: "Cartographie Interactive de Formes",
+ workspace_graph_map_desc: "Vos écrits se connectent automatiquement selon leur signification profonde.",
+ workspace_graph_connected: "6 notes interconnectées",
+ workspace_graph_status: "Structure Sémantique : Optimale ✓",
+ workspace_agents_title: "Système Cognitif d'Agents",
+ workspace_active_prompt: "TERMES ACTIFS DE RECHERCHE :",
+ workspace_rag_active: "RAG sémantique actif :",
+ workspace_consigne: "Consigne d'exploration :",
+ workspace_secure_chat: "SÉCURITÉ : CRYPTAGE SYSTÉMATIQUE",
+ workspace_processor: "SYSTÈME : BYOK ACTIF",
+ workspace_recall_title: "Répétition Espacée active",
+ workspace_recall_desc: "Algorithme actif convertissant automatiquement vos documents en cartes mémoires réactives.",
+ workspace_show_answer: "Afficher la réponse",
+ workspace_correct_answer: "Réponse attendue :",
+ workspace_study_memory: "Algorithme SuperMemo actif (SM2)",
+ workspace_snapshot_title: "Versionning & Sauvegardes",
+ workspace_snapshot_desc: "Enregistrement continu. Restaurez n'importe quel état textuel à la seconde près.",
+ workspace_restore: "Restaurer",
+ workspace_state_saved: "Sauvegarde locale : Active"
+};
+
+const EN: LangDict = {
+ nav_features: "Features",
+ nav_agents: "AI Agents",
+ nav_brainstorm: "Brainstorm",
+ nav_pricing: "Pricing",
+ nav_architecture: "Architecture",
+ nav_login: "Login",
+ nav_start: "Get Started",
+
+ hero_badge: "Amplified by Artificial Intelligence",
+ hero_title_1: "Your second brain,",
+ hero_title_italic: "finally amplified.",
+ hero_desc: "Momento is not just a typical note-taking tool. It is an intelligent ecosystem that connects, analyzes, and scales your thoughts in real time with 6 autonomous AI agents and vector semantic search.",
+ hero_cta_start: "Sign Up Now",
+ hero_cta_features: "View Features",
+
+ floating_card_1: "Memory Echo",
+ floating_card_1_text: '"Connection detected with your sustainable design project from March 2024..."',
+ floating_card_2: "Brainstorm Live",
+ floating_card_2_text: "+12 ideas generated",
+ floating_card_2_desc: "Simultaneous active users",
+
+ features_badge: "AI Capabilities",
+ features_title_1: "Fluid intelligence,",
+ features_title_2: "integrated into every word.",
+ features_subtitle: "Momento orchestrates your thoughts through a multi-provider landscape.",
+
+ f1_title: "Semantic Search",
+ f1_desc: "Stop searching by keywords. Retrieve by concept. Our hybrid Vector + FTS engine understands the core semantic context behind your logs.",
+ f2_title: "Contextual RAG Chat",
+ f2_desc: "Converse with your knowledge database. Our autonomous agents digest your notes, scan the web, and construct precise, cited answers.",
+ f3_title: "Augmented Writing",
+ f3_desc: "Rephrasing, title suggestions, automatic taxonomy tagging, and summaries. The AI acts passively in the background.",
+
+ agents_badge: "Autonomous Agents",
+ agents_title: "Delegate complex workflows.",
+ agents_subtitle: "6 specialized AI agents to automate your reference parsing, summaries, and instant pitch creation.",
+
+ agent_scraper_desc: "Scrapes assets and URLs, parses RSS feeds, and builds elegant visual summary cards with smart image placement.",
+ agent_researcher_desc: "Formulates deep search queries, surveys global web databases, and writes structured synthesis research drafts.",
+ agent_slide_desc: "Converts your raw notebooks into clean, structured PowerPoint templates or interactive HTML slide components.",
+ agent_monitor_desc: "Scans your folders persistently to capture structural changes, semantic triggers, and new dynamic patterns.",
+ agent_diagram_desc: "Extracts ideas into geometric flowcharts or Excalidraw-like mindmaps with perfect automatic layouts.",
+ agent_custom_desc: "Easily set custom system prompts, custom agents, distinct operational scopes, and data access limits.",
+
+ brain_badge: "Insight Waves",
+ brain_title: "Radial brainstorming in real time.",
+ brain_item1_title: "Multi-Wave Expansion",
+ brain_item1_desc: "Generates variations, analogies, and subsequent disruptions. The model challenges your initial hypothesis systematically.",
+ brain_item2_title: "Cooperative Canvas",
+ brain_item2_desc: "Live multi-user cursor tracking, active AI avatars, and smooth real-time node resizing and arrangement.",
+ brain_item3_title: "Semantic Export",
+ brain_item3_desc: "Seamlessly translate an entire radial brainstorm draft into typed markdown and structures with a single click.",
+
+ brain_node1_title: "DISRUPTION",
+ brain_node1_text: "Modular Architecture 2.0",
+ brain_node2_title: "ANALOGY",
+ brain_node2_text: "The tide cycle",
+
+ tech_badge: "Open Providers",
+ tech_title: "Bring your own model.",
+ tech_tier1_label: "Custom Tags",
+ tech_tier1_desc: "Each option is independently configurable with any LLM.",
+ tech_tier2_label: "Vector Embeddings",
+ tech_tier2_desc: "Each option is independently configurable with any LLM.",
+ tech_tier3_label: "Semantic RAG Engine",
+ tech_tier3_desc: "Each option is independently configurable with any LLM.",
+
+ price_badge: "Flexible Tiers",
+ price_title: "Select your cognitive scale.",
+ price_desc: "Scalable options for creative builders, starting from solo thinkers up to large modern agencies.",
+ price_monthly: "Monthly",
+ price_annual: "Annual",
+ price_popular: "Most Popular",
+ price_free_name: "Basic",
+ price_free_price: "Free",
+ price_free_desc: "Get started with the foundational magic of Momento.",
+ price_pro_name: "Pro",
+ price_pro_price: "$9.90",
+ price_pro_desc: "For consultants, writers, and advanced researchers.",
+ price_biz_name: "Business",
+ price_biz_price: "$29.90",
+ price_biz_desc: "For corporate groups, publishers, and product managers.",
+ price_ent_name: "Enterprise",
+ price_ent_price: "$49.90",
+ price_ent_desc: "Secured enterprise organizational memory layers.",
+ price_period_month: "/month",
+ price_period_annual: "/month, billed annually",
+ price_cta_start: "Get Started",
+ price_cta_pro: "Upgrade to Pro",
+ price_cta_biz: "Choose Business",
+ price_cta_ent: "Contact Sales",
+
+ price_free_features: ["100 Notes max", "3 Notebooks", "50 IA credits (Lifetime)", "Semantic search included", "7-day history backup"],
+ price_pro_features: ["Unlimited Notes", "BYOK (OpenAI/Anthropic)", "200 semantic lookups", "Agents (12 runs/month)", "30-day timeline rollback", "Premium Support"],
+ price_biz_features: ["10 Seats included", "BYOK (13 active models)", "1000 semantic lookups", "Agents (60 runs/month)", "Unlimited Brainstorm", "Full custom APIs Access"],
+ price_ent_features: ["Everything in Business", "Unlimited Agent runtimes", "Identity SSO & SAML logs", "Integrity Logs & local SLA", "Dedicated team channel", "Live technical onboarding"],
+
+ byok_badge: "Open Technology",
+ byok_title: "The BYOK Strategy",
+ byok_desc: "Already have OpenAI, Anthropic, or Google API credentials? Connect them directly to save subscription costs. Run models without arbitrary platform caps, paying only for raw API usage.",
+ byok_col1_title: "No lock-in",
+ byok_col1_desc: "Swap models or endpoints instantly with a single toggle.",
+ byok_col2_title: "Cost reduction",
+ byok_col2_desc: "Pay standard direct wholesale prices.",
+ byok_config_title: "Multi-Provider Schema",
+
+ final_cta_title: "Ready to expand your",
+ final_cta_title_italic: "second organic brain?",
+ final_cta_desc: "Join thousands of academics, product architects, and minimalist designers scaling their insights with Momento.",
+ final_cta_button: "Launch Momento",
+
+ eco_badge: "Momento Ecosystem",
+ eco_title_1: "Local document translation.",
+ eco_title_2: "Format intact.",
+ eco_desc: "The only translator preserving graphs, nested hierarchies, vectors, alignments, and titles exactly as you drew them. Take your local notes globally.",
+ eco_button: "Explore Translator",
+ eco_original: "original.pdf",
+ eco_translated: "translated.pdf",
+ eco_status: "Structure Preserved ✓",
+
+ footer_desc: "The ultimate AI-empowered second brain. Designed for mindful thinkers.",
+ footer_col1_title: "Product",
+ footer_col2_title: "Community",
+ footer_col3_title: "Legal",
+
+ workspace_title_editor: "Note Editor",
+ workspace_title_graph: "Knowledge Graph",
+ workspace_title_agents: "AI Enrichment",
+ workspace_title_reviews: "Active Recall",
+ workspace_title_history: "Time Snapshots",
+ workspace_notes_available: "Local Documents",
+ workspace_new_doc: "+ Create Note",
+ workspace_local_reading: "Reading local storage...",
+ workspace_graph_map_title: "Interactive Space Mapping",
+ workspace_graph_map_desc: "Your nodes attach automatically as semantic similarity is detected.",
+ workspace_graph_connected: "6 interconnected links",
+ workspace_graph_status: "Semantic Structure: Optimal ✓",
+ workspace_agents_title: "Cognitive Agents System",
+ workspace_active_prompt: "ACTIVE EXPLORATION TERMS:",
+ workspace_rag_active: "Semantic RAG active:",
+ workspace_consigne: "User instructions:",
+ workspace_secure_chat: "SECURITY: LOCAL SYMMETRIC ENCRYPTION",
+ workspace_processor: "PROCESSOR: BYOK COMPLIANT",
+ workspace_recall_title: "Active Spaced-Repetition Recall",
+ workspace_recall_desc: "Active recall algorithm that converts your writings into responsive flashcards dynamically.",
+ workspace_show_answer: "Show Answer",
+ workspace_correct_answer: "Correct answer:",
+ workspace_study_memory: "Active SuperMemo (SM2) engine",
+ workspace_snapshot_title: "Timeline Snapshots & Backups",
+ workspace_snapshot_desc: "Continuous backup checkpoints. Revert any document to an earlier state instantly.",
+ workspace_restore: "Revert",
+ workspace_state_saved: "Continuous local backup: Active"
+};
+
+const JA: LangDict = {
+ nav_features: "機能紹介",
+ nav_agents: "自律エージェント",
+ nav_brainstorm: "ブレスト",
+ nav_pricing: "料金プラン",
+ nav_architecture: "システム構成",
+ nav_login: "ログイン",
+ nav_start: "新規登録",
+
+ hero_badge: "人工知能による拡張済システム",
+ hero_title_1: "あなたの第二の脳を、",
+ hero_title_italic: "ついに具現化する。",
+ hero_desc: "Momento(モメント)は単なるメモ帳ではありません。6体の専門AIエージェント、ハイブリッドベクトル検索を搭載し、リアルタイムで知識の接続、整理、展開を実行する知能エコシステムです。",
+ hero_cta_start: "無料で体験する",
+ hero_cta_features: "機能一覧を見る",
+
+ floating_card_1: "メモリ・エコー",
+ floating_card_1_text: "「2024年3月のサスタナブルデザインの草稿と関連性を感知しました...」",
+ floating_card_2: "ライブ・ブレスト",
+ floating_card_2_text: "+12件のアイデア創出",
+ floating_card_2_desc: "同時編集中のアクティブユーザー",
+
+ features_badge: "先進AI性能",
+ features_title_1: "記述に完全に融合する、",
+ features_title_2: "インテリジェンス。",
+ features_subtitle: "Momentoは、複数のLLMプロバイダを容易に構成可能な適応性を備えています。",
+
+ f1_title: "セマンティック意味検索",
+ f1_desc: "単なるキーワード検索はもう不要。記述された文脈、概念そのものを捉えて、過去の関連メモを一瞬で検索します。",
+ f2_title: "コンテキスト適合RAG対話",
+ f2_desc: "自らのナレッジベースに直接質問。AIエージェントがメモの内容、Web、ドキュメントを統合して高精度な回答を構築します。",
+ f3_title: "筆記自動拡張",
+ f3_desc: "言い換え提案、タイトル自動補完、自動メタタグ分類、要約作成。AIがバックグラウンドで思考を強力に支援します。",
+
+ agents_badge: "自律AIエージェント",
+ agents_title: "面倒な調査、作成を委託。",
+ agents_subtitle: "Web調査、プレゼン資料構成、自動要約など、6種類の特化型エージェントに自律実行させることが可能です。",
+
+ agent_scraper_desc: "URLを解析し、RSSフィードから最新ニュースを要約。イメージ画像の最適な配置まで自律実行します。",
+ agent_researcher_desc: "深層検索クエリを作成し、グローバルWeb上のリソースから構造化されたリサーチ報告書を下書きします。",
+ agent_slide_desc: "作成したメモやアウトラインから、PowerPointプレゼン用プロット、またはHTMLのスライドを一括生成します。",
+ agent_monitor_desc: "ノートブックを常時モニタリング。新しいトレンドの予兆や興味深い相関関係を可視化します。",
+ agent_diagram_desc: "アイデアをExcalidraw形式のフローチャートやマインドマップ、関連図に美しく自動レイアウトします。",
+ agent_custom_desc: "独自の役割定義(プロンプト)、稼働スコープ、データアクセス制限をカスタマイズして動作させることができます。",
+
+ brain_badge: "思考の拡散",
+ brain_title: "リアルタイムなマインドマッピング。",
+ brain_item1_title: "多重ウェーブ連想",
+ brain_item1_desc: "「類似」「応用」「破壊」という異なる深度でAIがアイデアを分析し、最初の着想を多角的に突き詰めます。",
+ brain_item2_title: "コラボレーション機能",
+ brain_item2_desc: "リアルタイムでのポインター追跡、AIアバターとの同時ブレスト、円滑なノード拡大・自動整列システム。",
+ brain_item3_title: "構造化エクスポート",
+ brain_item3_desc: "ブレストキャンバス上に並んだ多数 Hideアイデア群を、構造化されたMarkdown形式へ1タップで変換します。",
+
+ brain_node1_title: "破壊的アイデア",
+ brain_node1_text: "モジュール式アーキテクチャ 2.0",
+ brain_node2_title: "自然の相似比",
+ brain_node2_text: "潮汐の周期モデル",
+
+ tech_badge: "マルチ・プロバイダ対応",
+ tech_title: "自分自身のAPIキーを連携。",
+ tech_tier1_label: "メタ分類",
+ tech_tier1_desc: "利用したいモデル、設定のすべてを個別に自由にバインドしてコントロールできます。",
+ tech_tier2_label: "意味ベクトル化",
+ tech_tier2_desc: "利用したいモデル、設定のすべてを個別に自由にバインドしてコントロールできます。",
+ tech_tier3_label: "意味的RAG",
+ tech_tier3_desc: "利用したいモデル、設定 ofすべてを個別に自由にバインドしてコントロールできます。",
+
+ price_badge: "料金・プラン",
+ price_title: "求める思考スケールに応じて選択。",
+ price_desc: "個人での思考整理、創作活動からチーム、グローバル企業での知能共有まで対応するプラン設計。",
+ price_monthly: "月払い",
+ price_annual: "年払い (お得値)",
+ price_popular: "人気プラン",
+ price_free_name: "ベーシック",
+ price_free_price: "無料",
+ price_free_desc: "Momentoの見事な基礎機能をすぐにお試しいただけます。",
+ price_pro_name: "プロ",
+ price_pro_price: "¥1,480",
+ price_pro_desc: "ライター、学習者、研究者、コンサルタントの方へ最適。",
+ price_biz_name: "ビジネス",
+ price_biz_price: "¥4,480",
+ price_biz_desc: "複数のコラボレーター連携やマインドマップ、高度API利用が必要なチームへ。",
+ price_ent_name: "エンタープライズ",
+ price_ent_price: "お問い合わせ",
+ price_ent_desc: "セキュアな組織知識データレイク、S/MIME、SSO、個別SLA管理。",
+ price_period_month: "/月",
+ price_period_annual: "/月 (年一括のご請求)",
+ price_cta_start: "今すぐ開始",
+ price_cta_pro: "プロへアップグレード",
+ price_cta_biz: "ビジネス選択",
+ price_cta_ent: "営業にお問い合わせ",
+
+ price_free_features: ["最大100ノート", "3つのノートブック制限", "50回の初期AIクレジット", "セマンティック意味検索", "7日間の履歴自動バックアップ"],
+ price_pro_features: ["作成メモ数無制限", "独自のAPIキー連携 (BYOK)", "200回のセマンティック意味検索", "AIエージェント (月12回起動)", "30日分の編集履歴ロールバック", "優先的な技術サポート"],
+ price_biz_features: ["最大10人のコラボレーター連携", "13種のLLMモデル切替", "1000回の意味検索/月", "AIエージェント (月60回起動)", "ブレスト回数に制限なし", "完全なAPI接続アクセス"],
+ price_ent_features: ["ビジネスプランの全機能", "AIエージェント起動無制限", "SAML/SSO シングルサインオン", "アクセス監査ログ & 専用SLA対応", "24時間年中無休の個別サポート", "オンボーディング技術教育支援"],
+
+ byok_badge: "オープン構想",
+ byok_title: "APIキー持ち込み(BYOK)",
+ byok_desc: "OpenAI、Anthropic、Googleなどの既存APIキーをお持ちですか?Momentoに直接バインドすれば、従量課金のみで任意の極限モデルを完全に制限なしで無制限にご利用いただけます。",
+ byok_col1_title: "ロックイン縛りゼロ",
+ byok_col1_desc: "好みのプロバイダやエンジンへ一瞬でコンフィグを切り替えられます。",
+ byok_col2_title: "中抜きマージン排除",
+ byok_col2_desc: "プロバイダ公式APIの卸売価格そのままの最低価格で使用できます。",
+ byok_config_title: "構成プロバイダ詳細",
+
+ final_cta_title: "あなたの第二の有機的な脳を",
+ final_cta_title_italic: "今すぐ起動させましょう。",
+ final_cta_desc: "最先端デザインと人工知能を極限まで融合させたMomento。すでに数千人のアカデミアやデザイナーが思考のスケールを始めています。",
+ final_cta_button: "Momentoを起動する",
+
+ eco_badge: "Momentoエコシステム",
+ eco_title_1: "ドキュメントローカル翻訳。",
+ eco_title_2: "完璧な構造維持。",
+ eco_desc: "グラフ、入れ子、アライメント、レイアウト、タイトル座標のすべてを完璧に保持したまま、言語領域を変換します。アイデアを瞬時にグローバルへ。",
+ eco_button: "翻訳機を起動する",
+ eco_original: "original.pdf",
+ eco_translated: "translated.pdf",
+ eco_status: "構造の一貫性を確認済 ✓",
+
+ footer_desc: "インテリジェントに調律された、あなたの第二の脳。豊かな思考者に捧ぐ。",
+ footer_col1_title: "製品",
+ footer_col2_title: "公式コミュニティ",
+ footer_col3_title: "規約・リーガル",
+
+ workspace_title_editor: "エディター",
+ workspace_title_graph: "知識グラフ",
+ workspace_title_agents: "AI拡張機能",
+ workspace_title_reviews: "間隔反復学習",
+ workspace_title_history: "履歴復元",
+ workspace_notes_available: "手稿・ノート一覧",
+ workspace_new_doc: "+ メモ新規作成",
+ workspace_local_reading: "ローカルDBを読み込み中...",
+ workspace_graph_map_title: "インタラクティブ概念座標",
+ workspace_graph_map_desc: "異なるメモ間の意味的類似度に基づいて、自動でリンク線が投影されます。",
+ workspace_graph_connected: "6件の関連リンク",
+ workspace_graph_status: "意味構造:最高ランク ✓",
+ workspace_agents_title: "自律型エージェント(4)",
+ workspace_active_prompt: "探査キーワード :",
+ workspace_rag_active: "RAGセマンティクス機能中 :",
+ workspace_consigne: "ユーザー指示要約 :",
+ workspace_secure_chat: "暗号化:ローカル内対称鍵で安全に保管",
+ workspace_processor: "システム:モデルキー持込対応",
+ workspace_recall_title: "効率的な間隔反復フラッシュカード",
+ workspace_recall_desc: "蓄積されたナレッジから自動テストを生成。定着を科学的にサポート。",
+ workspace_show_answer: "解答を表示する",
+ workspace_correct_answer: "正解の解説 :",
+ workspace_study_memory: "SuperMemo (SM2) アルゴリズム有効",
+ workspace_snapshot_title: "履歴バージョン管理",
+ workspace_snapshot_desc: "すべての編集イベントが秒単位で自動保存されます。いつでも過去の状態へ復元可能。",
+ workspace_restore: "適用する",
+ workspace_state_saved: "手稿保護:ローカル常時保管中"
+};
+
+interface LandingPageProps {
+ onEnter: () => void;
+ onLogin: () => void;
+ onRegister: () => void;
+ onSwitchVersion: (v: 'v1' | 'v2' | 'v3') => void;
+}
+
+export const LandingPage: React.FC = ({ onEnter, onLogin, onRegister, onSwitchVersion }) => {
+ const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly');
+ const [lang, setLang] = useState<'fr' | 'en' | 'ja'>('fr');
+ const [showI18nKeys, setShowI18nKeys] = useState(false);
+
+ const [activeTab, setActiveTab] = useState<'editor' | 'graph' | 'agents' | 'reviews' | 'history'>('editor');
+ const [activeDemoIdx, setActiveDemoIdx] = useState(0);
+ const [simulateState, setSimulateState] = useState<'searching' | 'writing' | 'idle' | 'complete'>('idle');
+ const [displayText, setDisplayText] = useState('');
+ const [aiSidebarTab, setAiSidebarTab] = useState<'explore' | 'discussion' | 'relations'>('explore');
+ const [customPrompt, setCustomPrompt] = useState('');
+ const [showFlashcardAnswer, setShowFlashcardAnswer] = useState(false);
+ const [flashcardIdx, setFlashcardIdx] = useState(0);
+
+ const [editorMode, setEditorMode] = useState<'wysiwyg' | 'markdown' | 'html'>('wysiwyg');
+ const [zoomedBlockId, setZoomedBlockId] = useState(null);
+ const [dbViewMode, setDbViewMode] = useState<'table' | 'card'>('card');
+ const [newAuthorName, setNewAuthorName] = useState("");
+ const [newBookTitle, setNewBookTitle] = useState("");
+ const [dbAuthors, setDbAuthors] = useState([
+ { id: 'a1', name: "Jules Verne", works: ["Twenty Thousand Leagues Under The Sea"], count: 1 },
+ { id: 'a2', name: "Liu Cixin", works: ["The Three-Body Problem", "The Wandering Earth"], count: 2 }
+ ]);
+ const [dbBooks, setDbBooks] = useState([
+ { id: 'b1', title: "Twenty Thousand Leagues Under The Sea", author: "Jules Verne", cover: "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=200&h=300", tag: "Science fiction" },
+ { id: 'b2', title: "The Three-Body Problem", author: "Liu Cixin", cover: "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=200&h=300", tag: "Science fiction" },
+ { id: 'b3', title: "The Wandering Earth", author: "Liu Cixin", cover: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=200&h=300", tag: "Science fiction" }
+ ]);
+
+ const realNotes = [
+ {
+ id: 'n1',
+ title: "H2 Relation and Rollup",
+ carnet: "Database & Books",
+ date: "26 Oct 2024",
+ tags: ["Relational", "Rollup", "Blocks"],
+ content: `# H2 Relation and Rollup\n\nCe document démontre la puissance du modèle relationnel de Momento.\n\n## 1. Modèle Relationnel\nVous pouvez lier des auteurs à leurs œuvres pour comptabiliser dynamiquement les entrées grâce à notre système de Rollups sémantiques.`,
+ stats: { words: 124, lines: 18, equations: 1, graphs: 4, images: 3 }
+ },
+ {
+ id: 'n2',
+ title: "Block-Style Math & Formulas",
+ carnet: "Mathematical & Geometrical",
+ date: "24 Oct 2024",
+ tags: ["LaTeX", "Gantt", "Flowcharts"],
+ content: `# Block-Style Math & Formulas\n\nMomento supporte les équations complexes de type LaTeX et les diagrammes sémantiques directement intégrés sous forme de blocs.\n\n$$\\Phi = \\frac{1 + \\sqrt{5}}{2}$$\n\n$$\\Delta Carbon = E_{béton} - E_{CLT} = 410 \\text{ kg } CO_2/m^3$$`,
+ stats: { words: 91, lines: 12, equations: 2, graphs: 4, images: 1 }
+ },
+ {
+ id: 'n3',
+ title: "Large Document Virtualization",
+ carnet: "Performance Model",
+ date: "22 Oct 2024",
+ tags: ["Virtualization", "Zoom-In"],
+ content: `# Large Document Virtualization\n\nLe moteur de rendu WYSIWYG traite de longs documents (jusqu'à 1 000 000 de mots) sans aucune baisse de performance grâce au chargement différé des blocs de contenu.\n\n## Zoom-In de bloc\nIsoler et grossir un bloc spécifique pour vous concentrer sur la rédaction.`,
+ stats: { words: 1542, lines: 80, equations: 0, graphs: 0, images: 0 }
+ }
+ ];
+
+ const simulatedFlashcards = [
+ {
+ id: 'f1',
+ question: lang === 'fr'
+ ? "Quel rôle joue le Nombre d'Or (1.618) dans les structures d'architecture ?"
+ : lang === 'ja'
+ ? "建築構造において黄金比(1.618)はどのような役割を果たしますか?"
+ : "What role does the Golden Ratio (1.618) play in architectural structures?",
+ answer: lang === 'fr'
+ ? "Il sert à définir des proportions optimales de fenêtrage et de colonnes, créant une harmonie visuelle naturelle pour le cortex occipital."
+ : lang === 'ja'
+ ? "サッシや柱の最適な比率を決定することで、後頭葉に自然な視覚的調和をもたらします。"
+ : "It serves to define optimal proportions of windowing and columns, creating a natural visual harmony for the occipital cortex."
+ },
+ {
+ id: 'f2',
+ question: lang === 'fr'
+ ? "Pourquoi privilégier le bois CLT (lamellé-croisé) au béton armé ?"
+ : lang === 'ja'
+ ? "なぜ鉄筋コンクリートよりもCLT木材を優先するのですか?"
+ : "Why prioritize CLT (cross-laminated timber) over reinforced concrete?",
+ answer: lang === 'fr'
+ ? "Pour son stockage actif de CO2 à long terme, sa légèreté combinée à sa résistance sismique et coupe-feu naturelle."
+ : lang === 'ja'
+ ? "長期的な炭素固定、軽量性と耐震性、そして優れた天然の防火性能を併せ持つためです。"
+ : "For its active long-term CO2 sequestration, lightweight properties combined with superior seismic and natural fire resistance."
+ }
+ ];
+
+ const triggerSimulation = (index: number, overrideText?: string) => {
+ setActiveDemoIdx(index);
+ setSimulateState('searching');
+ setDisplayText('');
+ setShowFlashcardAnswer(false);
+
+ if (index === 0) setAiSidebarTab('explore');
+ if (index === 1) setAiSidebarTab('relations');
+ if (index === 2) setAiSidebarTab('discussion');
+
+ const sourceText = overrideText || (index === -1 ? "" : realNotes[index].content);
+
+ const length = sourceText.length;
+ let curr = 0;
+
+ setTimeout(() => {
+ setSimulateState('writing');
+ const timer = setInterval(() => {
+ curr += 18;
+ if (curr >= length) {
+ setDisplayText(sourceText);
+ setSimulateState('complete');
+ clearInterval(timer);
+ } else {
+ setDisplayText(sourceText.substring(0, curr));
+ }
+ }, 15);
+ }, 800);
+ };
+
+ const handleCustomSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!customPrompt.trim()) return;
+
+ const summaryContent = `# Synthèse : ${customPrompt}\n\nRecherche sémantique connectée.\n\n$$\\Phi = \\lim_{n \\to \\infty} \\frac{F_{n+1}}{F_n}$$\n\n* Intégration d'éléments croisés complets.\n* Index de concordance optimale validé.`;
+ triggerSimulation(-1, summaryContent);
+ };
+
+ const handleAddDbEntry = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newAuthorName.trim() || !newBookTitle.trim()) return;
+
+ const authorExists = dbAuthors.some(a => a.name.toLowerCase() === newAuthorName.toLowerCase());
+ if (!authorExists) {
+ setDbAuthors(prev => [...prev, {
+ id: 'a_' + Date.now(),
+ name: newAuthorName,
+ works: [newBookTitle],
+ count: 1
+ }]);
+ } else {
+ setDbAuthors(prev => prev.map(a => {
+ if (a.name.toLowerCase() === newAuthorName.toLowerCase()) {
+ return { ...a, works: [...a.works, newBookTitle] };
+ }
+ return a;
+ }));
+ }
+
+ setDbBooks(prev => [...prev, {
+ id: 'b_' + Date.now(),
+ title: newBookTitle,
+ author: newAuthorName,
+ cover: "https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=200&h=300",
+ tag: "Science fiction"
+ }]);
+
+ setNewAuthorName("");
+ setNewBookTitle("");
+ };
+
+ useEffect(() => {
+ triggerSimulation(0);
+ }, []);
+
+ const dicts = { fr: FR, en: EN, ja: JA };
+ const currentDict = dicts[lang];
+
+ // Helper function to translate string keys with styled brackets mode support
+ const t = (key: keyof LangDict, isPlain: boolean = false) => {
+ if (showI18nKeys) {
+ if (isPlain) return `{{${key}}}`;
+ return (
+
+ {`{${key}}`}
+
+ );
+ }
+ const val = currentDict[key];
+ return typeof val === 'string' ? val : '';
+ };
+
+ const tArray = (key: 'price_free_features' | 'price_pro_features' | 'price_biz_features' | 'price_ent_features'): string[] => {
+ if (showI18nKeys) {
+ return Array(5).fill(`{${key}}`);
+ }
+ return currentDict[key];
+ };
+
+ return (
+
+ {/* Navigation */}
+
+
+
+
+
+ {/* Dynamic Controls System: Version switches, Language bar, and Raw translation key indicator */}
+
+ {/* Elegant language switcher */}
+
+ {(['fr', 'en', 'ja'] as const).map((l) => (
+ setLang(l)}
+ className={`px-2 py-1 text-[10px] font-bold rounded-md transition-all uppercase cursor-pointer ${lang === l ? 'bg-white text-ink shadow-2xs font-extrabold' : 'text-concrete hover:text-ink'}`}
+ >
+ {l}
+
+ ))}
+
+
+ {/* Raw translation key indicator switch */}
+
setShowI18nKeys(!showI18nKeys)}
+ className={`px-2.5 py-1 rounded-md border text-[9px] font-mono tracking-wider font-extrabold flex items-center gap-1.5 transition-all cursor-pointer shadow-3xs
+ ${showI18nKeys
+ ? 'bg-amber-100 text-amber-900 border-amber-305'
+ : 'bg-white text-stone-500 border-stone-200 hover:bg-stone-50'}`}
+ >
+
+ {showI18nKeys ? "🔑 Clés" : "Afficher i18n"}
+
+
+ {/* Elegant Version landing switcher pill */}
+
+ onSwitchVersion('v1')}
+ className="px-3 py-1 text-[9px] uppercase tracking-widest font-extrabold rounded-full bg-ink text-paper shadow-sm"
+ title="Classique V1 bilingue"
+ >
+ V1
+
+ onSwitchVersion('v2')}
+ className="px-3 py-1 text-[9px] uppercase tracking-widest font-bold rounded-full text-concrete hover:text-ink transition-all"
+ title="Aperçu Interactif V2"
+ >
+ V2
+
+ onSwitchVersion('v3')}
+ className="px-3 py-1 text-[9px] uppercase tracking-widest font-bold rounded-full text-concrete hover:text-ink transition-all"
+ title="Bilingue V3"
+ >
+ V3 ✨
+
+
+
+
+
+
+ {t('nav_login')}
+
+
+ {t('nav_start')}
+
+
+
+
+
+ {/* Hero Section */}
+
+ {/* Background Decorative Elements */}
+
+
+
+
+
+
+
+ {t('hero_badge')}
+
+
+ {t('hero_title_1')}
+ {t('hero_title_italic')}
+
+
+ {t('hero_desc')}
+
+
+
+
+
+ {/* Integrated Dynamic Workspace Player - Savior of V1! */}
+
+
+
+ {/* Top Bar Indicators */}
+
+
+
+
+
+ MOMENTO-SANDBOX-LAYER
+
+
+
+
+ {t('workspace_notes_available')} : {activeDemoIdx === -1 ? `Custom` : `${activeDemoIdx + 1}/3`}
+
+
+
+ {/* Central Player Frame */}
+
+
+ {/* Left Active Controls Rail */}
+
+
+ setActiveTab('editor')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'editor' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_editor', true)}
+ >
+
+
+ setActiveTab('graph')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'graph' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_graph', true)}
+ >
+
+
+ setActiveTab('agents')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'agents' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_agents', true)}
+ >
+
+
+ setActiveTab('reviews')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'reviews' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_reviews', true)}
+ >
+
+
+ setActiveTab('history')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'history' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_history', true)}
+ >
+
+
+
+
+
+
+
+
+ {/* Main Tab Screen Area */}
+
+
+
+ {/* 1. EDITOR SCREEN */}
+ {activeTab === 'editor' && (
+
+ {/* Notes selector drawer */}
+
+
+ {t('workspace_notes_available')}
+
+
+ {realNotes.map((note, idx) => (
+ triggerSimulation(idx)}
+ className={`w-full text-left p-2.5 rounded-lg border text-xs transition-colors cursor-pointer block
+ ${activeDemoIdx === idx
+ ? 'bg-[#EAE8DF] border-[#A47148]/20'
+ : 'border-transparent bg-stone-50 hover:bg-[#EAE8DF]/40'
+ }`}
+ >
+ {note.carnet}
+ {note.title}
+
+ ))}
+ triggerSimulation(0)}
+ className="w-full text-center border border-dashed border-accent/25 py-2 rounded-lg text-[9px] font-mono text-accent block hover:bg-accent/5 cursor-pointer"
+ >
+ {t('workspace_new_doc')}
+
+
+
+
+ {/* Note workspace page content */}
+
+
+ {/* Rich Editor Mode Switcher Panel */}
+
+
{lang === 'fr' ? "Rendu d'Édition" : lang === 'ja' ? "書き込み・表示形式" : "Editor Rendering Modality"} :
+
+ {[
+ { mode: 'wysiwyg', label: lang === 'fr' ? 'Block WYSIWYG (Préféré)' : lang === 'ja' ? 'WYSIWYGブロック' : 'Block WYSIWYG' },
+ { mode: 'markdown', label: 'Raw Markdown' },
+ { mode: 'html', label: 'HTML Map' }
+ ].map((m) => (
+ setEditorMode(m.mode as any)}
+ className={`px-2 py-1 rounded cursor-pointer transition-all ${editorMode === m.mode ? 'bg-white text-ink shadow-3xs font-black' : 'text-stone-500 hover:text-ink hover:bg-white/40'}`}
+ >
+ {m.label}
+
+ ))}
+
+
+
+
+ {/* Breadcrumbs / Document Info */}
+
+ {activeDemoIdx === -1 ? "SYNTHÈSE" : realNotes[activeDemoIdx].carnet}
+ {activeDemoIdx === -1 ? "ACTIF" : realNotes[activeDemoIdx].date}
+
+
+
+ {activeDemoIdx === -1 ? (lang === 'fr' ? "Synthèse de recherche" : lang === 'ja' ? "研究合成メモ" : "Research Synthesis") : realNotes[activeDemoIdx].title}
+ {zoomedBlockId && (
+ Zoomed Focus
+ )}
+
+
+
+
+ {(activeDemoIdx === -1 ? ["Custom"] : realNotes[activeDemoIdx].tags).map((tag, i) => (
+
+ {tag}
+
+ ))}
+
+
+ {realNotes[activeDemoIdx].stats.words} {lang === 'fr' ? "mots" : lang === 'ja' ? "語" : "words"} · {realNotes[activeDemoIdx].stats.lines} {lang === 'fr' ? "blocs" : lang === 'ja' ? "ブロック" : "blocks"}
+
+
+
+
+
+ {simulateState === 'searching' && (
+
+
+
+
+
+
+
+
+ {t('workspace_local_reading')}
+
+
+ )}
+
+ {(simulateState === 'writing' || simulateState === 'complete') && (
+
+
+ {/* A. WYSIWYG BLOCK MODE */}
+ {editorMode === 'wysiwyg' && (
+
+
+ {/* Zoomed Block Mode Warning Indicator */}
+ {zoomedBlockId && (
+
+
+ 🔎 {lang === 'fr' ? "Sous-bloc isolé en édition zoomée" : lang === 'ja' ? "分離した1ブロックを全画面ズーム編集中" : "1 block isolated under deep zoom-in focus"}
+
+ setZoomedBlockId(null)}
+ className="text-[8px] uppercase tracking-wider font-bold bg-white px-2 py-0.5 rounded shadow-3xs hover:bg-[#FAF9F5] text-ink cursor-pointer border"
+ >
+ {lang === 'fr' ? "← Voir tout" : lang === 'ja' ? "← 全て表示" : "← View all"}
+
+
+ )}
+
+ {/* NOTE 1: H2 RELATION AND ROLLUP */}
+ {activeDemoIdx === 0 && (
+
+
+ {(!zoomedBlockId || zoomedBlockId === 'b_desc') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b_desc' ? null : 'b_desc')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ {lang === 'fr'
+ ? "Ce document démontre le lien sémantique dynamique entre différentes entités de bases de données grâce à un système de métadonnées et de rollups de type \"Count All\" (comptage de relations)."
+ : lang === 'ja'
+ ? "本資料では、スキーマ定義されたデータベース群が、関連づけ(Relation)と、何件あるかの集計(Rollup - Count All機能)によって有機的につながる様子を再現しています。"
+ : "This document demonstrates the dynamic semantic connection between different database entities thanks to metadata structures and 'Count All' relational rollups."}
+
+
+ )}
+
+ {(!zoomedBlockId || zoomedBlockId === 'b_database') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b_database' ? null : 'b_database')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ {/* Notion-style database inner switcher header */}
+
+
+ 📚
+
+ {lang === 'fr' ? "Base d'Auteurs & Œuvres" : lang === 'ja' ? "著者・作品関係データベース" : "Authors & Publications Relation"}
+
+
+
+ setDbViewMode('table')}
+ className={`px-2 py-0.5 rounded cursor-pointer ${dbViewMode === 'table' ? 'bg-white text-ink shadow-3xs' : 'text-stone-550'}`}
+ >
+ {lang === 'fr' ? "Tableau" : lang === 'ja' ? "テーブル" : "Table"}
+
+ setDbViewMode('card')}
+ className={`px-2 py-0.5 rounded cursor-pointer ${dbViewMode === 'card' ? 'bg-white text-ink shadow-3xs' : 'text-stone-550'}`}
+ >
+ {lang === 'fr' ? "Fiches" : lang === 'ja' ? "カード" : "Card"}
+
+
+
+
+ {/* TABLE MODE VIEW */}
+ {dbViewMode === 'table' ? (
+
+
+
+
+ 🔑 Author / Auteur
+ ↗ Works / Œuvres
+ ⧉ Works count (Rollup)
+
+
+
+ {dbAuthors.map((auth) => {
+ const authorBooks = dbBooks.filter(b => b.author.toLowerCase() === auth.name.toLowerCase());
+ const worksStr = authorBooks.map(b => b.title).join(', ') || 'No linked works';
+ return (
+
+ {auth.name}
+ {worksStr}
+ {authorBooks.length}
+
+ );
+ })}
+
+
+
+ ) : (
+ /* CARD MODE VIEW */
+
+ {dbBooks.map((book) => (
+
+
+
+
+
+
+
{book.title}
+
+
+ {book.author}
+
+
+ {book.tag}
+
+
+
+
+ ))}
+
+ ➕ Card Entry
+
+
+ )}
+
+ {/* Dynamic simulation insertion form! */}
+
+
+ {lang === 'fr' ? "Ajouter un livre :" : lang === 'ja' ? "作品インライン登録 :" : "Link novel inline:"}
+
+ setNewAuthorName(e.target.value)}
+ placeholder={lang === 'fr' ? "Auteur (ex: Asimov)" : lang === 'ja' ? "著者 (例: アイザック・アシモフ)" : "Author Name"}
+ className="bg-white text-[9px] border px-2 py-1 rounded text-ink font-mono focus:border-accent outline-none flex-1 min-w-[100px]"
+ />
+ setNewBookTitle(e.target.value)}
+ placeholder={lang === 'fr' ? "Titre (ex: Fondation)" : lang === 'ja' ? "作品名 (例: ファウンデーション)" : "Novel Title"}
+ className="bg-white text-[9px] border px-2 py-1 rounded text-ink font-mono focus:border-accent outline-none flex-1 min-w-[110px]"
+ />
+
+ + Push Row
+
+
+
+
+ {lang === 'fr'
+ ? "In the Author database, the Works count is summarized and statistics are made on its Works, and the statistics method is Count all."
+ : lang === 'ja'
+ ? "In the Author database, the Works count is summarized and statistics are made on its Works, and the statistics method is Count all."
+ : "In the Author database, the Works count is summarized and statistics are made on its Works, and the statistics method is Count all."}
+
+
+ )}
+
+ {(!zoomedBlockId || zoomedBlockId === 'b_templates') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b_templates' ? null : 'b_templates')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
{lang === 'fr' ? "Gabarits de données" : lang === 'ja' ? "テンプレート構文" : "Template Syntax"}
+
+ {lang === 'fr'
+ ? "Les gabarits peuvent accéder, calculer et résoudre des données croisées d'un même lot via des formules dynamiques :"
+ : lang === 'ja'
+ ? "テンプレートは、同一項目の関連データをアクセス・計算、独自にレンダリングする記法を導入可能です。"
+ : "Templates can access, calculate and render the values of other fields in the same piece of data through the syntax introduced in Template snippet:"}
+
+
+
+ •
+ {lang === 'fr' ? "Utilisez " : lang === 'ja' ? "使用法 " : "Use "}.action{`{ .field }`} {lang === 'fr' ? "pour accéder aux propriétés globales." : lang === 'ja' ? "でビュー情報表示。" : "to access view properties."}
+
+
+ •
+ {lang === 'fr' ? "Utilisez " : lang === 'ja' ? "使用法 " : "Use "}.action{`{ index . "custom-xxx" }`} {lang === 'fr' ? "pour le ciblage de bloc sémantique." : lang === 'ja' ? "でカスタムメタデータ展開。" : "to access content block custom attributes."}
+
+
+ •
+ {lang === 'fr' ? "Gabarit résolu de démonstration : " : lang === 'ja' ? "計算評価シミュレーション : " : "Evaluated bills mockup snippet: "}
+
+ .action{`{ compute sum of Books }`} = {dbBooks.length} items
+
+
+
+
+ )}
+
+
+ )}
+
+ {/* NOTE 2: BLOCK-STYLE MATH & FORMULAS */}
+ {activeDemoIdx === 1 && (
+
+
+ {(!zoomedBlockId || zoomedBlockId === 'b2_mathdesc') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b2_mathdesc' ? null : 'b2_mathdesc')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ {lang === 'fr'
+ ? "Momento intègre des équations mathématiques pures et des diagrammes sémantiques ou Gantt de manière entièrement nativisée sous forme de blocs WYSIWYG."
+ : lang === 'ja'
+ ? "Momentoは本格的なLaTeX数式および概念関係フローチャートをWYSIWYGブロックとして極めて滑らかに表示・調整可能です。"
+ : "Momento integrates highly stylized Mathematical LaTeX formulas and semantic/Gantt flowcharts directly as interactive WYSIWYG blocks."}
+
+
+ )}
+
+ {(!zoomedBlockId || zoomedBlockId === 'b2_equations') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b2_equations' ? null : 'b2_equations')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ 📐
+ {lang === 'fr' ? "Module d'Équations Mathématiques" : lang === 'ja' ? "インラインLaTeXレンダラー" : "Mathematical Formulas Equation Block"}
+
+
+
+
+
LaTex code inputs:
+
+
{"$$\\Phi = \\frac{1 + \\sqrt{5}}{2}$$"}
+
{"$$\\Delta Carbon = E - H = 410$$"}
+
+
+
+
Live Math rendering:
+
+ Φ = 1.61803398...
+ Rollup Equation Evaluated ✓
+
+
+
+
+ )}
+
+ {(!zoomedBlockId || zoomedBlockId === 'b2_flowchart') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b2_flowchart' ? null : 'b2_flowchart')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ 📊
+ {lang === 'fr' ? "Diagramme Temporel / Gantt de Projet" : lang === 'ja' ? "ガントチャート・タイムラインブロック" : "Flowchart & Project Gantt Block"}
+
+
+ {/* Mini vector flowchart */}
+
+ {[
+ { task: "Research & Relations", progress: "100%", width: "w-full", color: "bg-emerald-500", date: "Oct 1 - Oct 5" },
+ { task: "SuperMemo Sync Engine", progress: "80%", width: "w-4/5", color: "bg-accent", date: "Oct 6 - Oct 12" },
+ { task: "Multi-Million Word DB", progress: "40%", width: "w-2/5", color: "bg-amber-500", date: "Oct 13 - Dec 1" },
+ ].map((bar, i) => (
+
+
{bar.task}
+
+
{bar.date}
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+ {/* NOTE 3: LARGE DOCUMENT VIRTUALIZATION */}
+ {activeDemoIdx === 2 && (
+
+
+ {(!zoomedBlockId || zoomedBlockId === 'b3_intro') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b3_intro' ? null : 'b3_intro')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ {lang === 'fr'
+ ? "Le noyau de Momento est conçu pour gérer d'immenses documents textuels (plus d'un million de mots) avec une latence quasi nulle en virtualisant les sous-structures."
+ : lang === 'ja'
+ ? "Momentoの超高速仮想ドキュメントレンダリングは、合計100万語を越える膨大な書籍や論文データベースでも、表示遅延なく高速に動作・ブロック分割制御可能です。"
+ : "Its lightweight framework allows seamlessly displaying and editing files sizing up to 1,000,000 words without single-frame drops thanks to block virtualization."}
+
+
+ )}
+
+ {(!zoomedBlockId || zoomedBlockId === 'b3_stat') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b3_stat' ? null : 'b3_stat')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+
⚡
+
+
+ {lang === 'fr' ? "Statistiques de Virtualisation" : lang === 'ja' ? "メモリ仮想ブロック稼働状態" : "Million-Word Scale Buffer Monitor"}
+
+ RAM overhead: 0.12 MB / Render: 12ms (SuperMemo SM2 active in background)
+
+
+
+ 1,542,391 words
+
+
+ )}
+
+ {(!zoomedBlockId || zoomedBlockId === 'b3_zoomin_demo') && (
+
+
setZoomedBlockId(zoomedBlockId === 'b3_zoomin_demo' ? null : 'b3_zoomin_demo')}
+ className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity text-[8px] font-mono px-1.5 py-0.5 bg-white border rounded text-[#A47148] hover:bg-stone-100 cursor-pointer"
+ title="Zoom-in"
+ >
+ 🔎 {zoomedBlockId ? "Unzoom" : "Focus"}
+
+
+ 🔎
+ {lang === 'fr' ? "Démonstration Interactive Zoom-In" : lang === 'ja' ? "ブロック・フォーカスズーム実演" : "Block Zoom-In Interactive Focus Demonstration"}
+
+
+ {lang === 'fr'
+ ? "Survolez n'importe quel bloc ci-dessus ou cliquez sur la loupe 🔎 pour faire l'expérience du Zoom-In de bloc, permettant d'isoler un unique paragraphe pour une rédaction concentrée."
+ : lang === 'ja'
+ ? "上記の段落にホバーするか、虫眼鏡 🔎 をクリックして「ブロックのズームイン(Zoom-In Focus)」をテストできます。1つの項目のみに集中環境を作成します。"
+ : "Hover over any paragraph block in this editor mockup or click the magnifying glass icon 🔎 to test 'Block zoom-in', creating an isolated and completely noise-free environment."}
+
+
+ )}
+
+
+ )}
+
+
+ )}
+
+ {/* B. RAW MARKDOWN VIEW */}
+ {editorMode === 'markdown' && (
+
+
+
+ Markdown
+
+ {activeDemoIdx === 0 && (
+ `# H2 Relation and Rollup\n\nCe document démontre la puissance du modèle relationnel de Momento.\n\n## 1. Modèle Relationnel\nLier des auteurs à leurs œuvres pour comptabiliser dynamiquement.\n\n[DATABASE id="authors-works" view="table"]\n\n## Template\nTemplates can access values via syntax: .action { .field }`
+ )}
+ {activeDemoIdx === 1 && (
+ `# Block-Style Math & Formulas\n\nMomento supporte LaTeX.\n\n$$ \\Phi = \\frac{1 + \\sqrt{5}}{2} $$\n\n$$ \\Delta Carbon = E_béton - E_CLT = 410 $$`
+ )}
+ {activeDemoIdx === 2 && (
+ `# Large Document Virtualization\n\nLe moteur supporte de longs documents.\n\n## Zoom-In de bloc\nIsoler un bloc spécifique pour se concentrer.`
+ )}
+ {activeDemoIdx === -1 && (
+ `# Synthèse : ${customPrompt}\n\nRecherche sémantique connectée.`
+ )}
+
+
+ * {lang === 'fr' ? "Le Markdown nécessite de se souvenir de la syntaxe. Le WYSIWYG est nettement plus pertinent et fluide !" : lang === 'ja' ? "マークダウン記法の暗記は思考を遮ります。WYSIWYGのほうが直感的で生産的です。" : "Markdown blocks require manual parsing overhead, which is why Block WYSIWYG is much more convenient."}
+
+
+ )}
+
+ {/* C. HTML DOM VIEW MAP */}
+ {editorMode === 'html' && (
+
+
+
{``}
+
{`
`}
+
{`
${activeDemoIdx === -1 ? "Synthèse" : realNotes[activeDemoIdx].title} `}
+
{`
`}
+
{`${activeDemoIdx === -1 ? "Custom" : realNotes[activeDemoIdx].tags[0]} `}
+
{`
`}
+
{`
`}
+
{`
... [text content] ...
`}
+ {activeDemoIdx === 0 && (
+
+ )}
+
{`
`}
+
{`
`}
+
+
+ * {lang === 'fr' ? "La vue HTML brute est illisible pour un redacteur. La vue d'arborescence structure par contre idéalement le document." : lang === 'ja' ? "HTMLコードは人間の執筆には不向きです。WYSIWYGがDOM構造をカプセル化します。" : "HTML raw outputs are messy, making WYSIWYG encapsulate DOM rendering perfectly."}
+
+
+ )}
+
+
+ )}
+
+
+
+
+
+ )}
+
+ {/* 2. SPATIAL GRAPH SCREEN */}
+ {activeTab === 'graph' && (
+
+
+
+ {t('workspace_graph_map_title')}
+
+
{t('workspace_graph_map_desc')}
+
+
+ {/* Blueprint grid layout */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Center node */}
+
+ MOMENTO
+
+
+ {/* Surrounding linked concepts */}
+ {[
+ { name: lang === 'fr' ? "Nombre d'Or" : lang === 'ja' ? "黄金比率" : "Golden ratio", x: -95, y: -65, tag: lang === 'fr' ? "Architecture" : lang === 'ja' ? "建築幾何学" : "Architecture", theme: "text-amber-800 bg-amber-50/95" },
+ { name: lang === 'fr' ? "Diffraction lumineuse" : lang === 'ja' ? "光の回折" : "Light diffraction", x: 105, y: -72, tag: lang === 'fr' ? "Optique" : lang === 'ja' ? "光学" : "Optics", theme: "text-violet-850 bg-violet-50/95" },
+ { name: lang === 'fr' ? "Bois CLT Lamellé" : lang === 'ja' ? "CLT 木材" : "CLT Timber", x: -115, y: 70, tag: lang === 'fr' ? "Matériaux" : lang === 'ja' ? "エコ材料" : "Materials", theme: "text-emerald-800 bg-emerald-50/95" },
+ { name: lang === 'fr' ? "Transition Formelle" : lang === 'ja' ? "形式変換" : "Format Transition", x: 110, y: 60, tag: lang === 'fr' ? "Minimalisme" : lang === 'ja' ? "ミニマリズム" : "Minimalism", theme: "text-blue-800 bg-blue-50/95" },
+ { name: "Grid System", x: 0, y: -100, tag: lang === 'fr' ? "IA Métrique" : lang === 'ja' ? "AI評価指標" : "AI Metric", theme: "text-stone-800 bg-stone-50/95" },
+ { name: lang === 'fr' ? "Éco-Conception" : lang === 'ja' ? "エコ設計" : "Eco-design", x: 0, y: 100, tag: lang === 'fr' ? "Carbone" : lang === 'ja' ? "カーボン量" : "Carbon", theme: "text-rose-800 bg-rose-50/95" }
+ ].map((node, idx) => (
+
+
+ {node.name}
+
+ {node.tag}
+
+
+
+ ))}
+
+
+
+ {t('workspace_graph_connected')}
+ {t('workspace_graph_status')}
+
+
+ )}
+
+ {/* 3. COGNITIVE AGENTS SCREEN */}
+ {activeTab === 'agents' && (
+
+ {/* System models list */}
+
+
+ {t('workspace_agents_title')}
+
+
+ {[
+ { id: "scr", name: lang === 'fr' ? "Web Scraper" : lang === 'ja' ? "Webスクレイパー" : "Web Scraper", role: lang === 'fr' ? "Parse et simplifie les URLs" : lang === 'ja' ? "URLコンテンツの解析と要約" : "Parse and simplify URLs", tag: "SCR", color: "bg-amber-100 text-amber-900 border-amber-200" },
+ { id: "res", name: lang === 'fr' ? "Deep Researcher" : lang === 'ja' ? "ディープ・リサーチャー" : "Deep Researcher", role: lang === 'fr' ? "Recherche exhaustive internet" : lang === 'ja' ? "Web情報を徹底探索調査" : "Exhaustive web research", tag: "RES", color: "bg-teal-100 text-teal-900 border-teal-200" },
+ { id: "sli", name: lang === 'fr' ? "Slide Builder" : lang === 'ja' ? "スライドビルダー" : "Slide Builder", role: lang === 'fr' ? "Formate en deck PowerPoint" : lang === 'ja' ? "ワンタップでPPTに自動成形" : "Format into raw PPT decks", tag: "PPT", color: "bg-blue-100 text-blue-900 border-blue-200" },
+ { id: "mon", name: lang === 'fr' ? "Knowledge Monitor" : lang === 'ja' ? "ナレッジ・モニター" : "Knowledge Monitor", role: lang === 'fr' ? "Trouve les redondances" : lang === 'ja' ? "同一ノートの重複・接点検知" : "Detect nested redundancies", tag: "LNK", color: "bg-purple-100 text-purple-900 border-purple-200" }
+ ].map((a) => (
+
+
+ {a.name}
+ {a.tag}
+
+
{a.role}
+
+ ))}
+
+
+
+ {/* Interactive prompt chat system */}
+
+
+
+ {([
+ { key: 'explore', label: lang === 'fr' ? 'Explore (RAG)' : lang === 'ja' ? '探索 (RAG)' : 'Explore (RAG)' },
+ { key: 'discussion', label: lang === 'fr' ? 'Discussion' : lang === 'ja' ? 'チャット対話' : 'Discussion' },
+ { key: 'relations', label: lang === 'fr' ? 'Rapports' : lang === 'ja' ? '関連文書' : 'Matches' }
+ ] as const).map((tab) => (
+ setAiSidebarTab(tab.key)}
+ className={`flex-1 py-1.5 text-[8.5px] uppercase tracking-wider font-extrabold text-[#A47148] transition-all cursor-pointer text-center
+ ${aiSidebarTab === tab.key ? 'bg-white text-ink font-black border-r border-[#1C1C1C]/10' : 'hover:bg-white/40'}`}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+
+ {aiSidebarTab === 'explore' && (
+
+
+ {t('workspace_active_prompt')}
+
+
+ {t('workspace_rag_active')}
+ "{activeDemoIdx === -1 ? customPrompt || "Custom Search" : realNotes[activeDemoIdx].title}"
+
+
+
+ {lang === 'fr' ? "Synthétiser un sujet personnalisé :" : lang === 'ja' ? "カスタムテーマを選択して要約 :" : "Synthesize custom topic:"}
+
+
+ setCustomPrompt(e.target.value)}
+ placeholder={lang === 'fr' ? "Ex: Couleur dans l'architecture Bauhaus" : lang === 'ja' ? "例: バウハウスの色彩秩序理論" : "E.g., Bauhaus architectural color scale"}
+ className="flex-1 bg-white border border-stone-200 rounded-lg px-2.5 py-1.5 text-[10px] outline-none font-mono focus:border-accent"
+ />
+
+ Go
+
+
+
+
+ )}
+
+ {aiSidebarTab === 'discussion' && (
+
+
+ {t('workspace_consigne')}
+ {lang === 'fr' ? "Structure minimaliste de la diffraction active." : lang === 'ja' ? "アクティブ回折のミニマル構造의設計案" : "Minimalist structure of active diffraction."}
+
+
+ Gemini local model
+ {lang === 'fr' ? "Transparence induite par le verre feuilleté déclinable, limitant les structures." : lang === 'ja' ? "合わせガラスにより透明性を最大限に引き出し、支柱などの構造材を最小限に抑制します。" : "Transparency induced by laminated declinable glass, reducing supporting structures."}
+
+
+ )}
+
+ {aiSidebarTab === 'relations' && (
+
+
+ {lang === 'fr' ? "Correspondances d'index :" : lang === 'ja' ? "インデックス整合率検出 :" : "Semantic index matches:"}
+
+
+ {lang === 'fr' ? "Grille d'Éco-Conception CLT" : lang === 'ja' ? "木造CLTグリッドモジュール" : "Sustainable CLT Space Grid"}
+ 89% match ✓
+
+
+ )}
+
+
+
+
+
+ {t('workspace_secure_chat')}
+ {t('workspace_processor')}
+
+
+
+ )}
+
+ {/* 4. SPACED REPETITION STUDY CARD SCREEN */}
+ {activeTab === 'reviews' && (
+
+
+
+ {t('workspace_recall_title')}
+
+
{t('workspace_recall_desc')}
+
+
+ {/* Interactive flipped card structure */}
+
+
+
+ {lang === 'fr' ? `Carte mémoire révision ` : lang === 'ja' ? `暗記カード ` : `Concept review `} {flashcardIdx + 1}/2
+
+
+ {simulatedFlashcards[flashcardIdx].question}
+
+
+
+ {showFlashcardAnswer ? (
+
+ {t('workspace_correct_answer')}
+ {simulatedFlashcards[flashcardIdx].answer}
+
+ ) : (
+
setShowFlashcardAnswer(true)}
+ className="px-4 py-2 bg-accent hover:bg-accent/90 text-white transition-all text-[9.5px] font-bold uppercase tracking-wider rounded-xl cursor-pointer"
+ >
+ {t('workspace_show_answer')}
+
+ )}
+
+ {showFlashcardAnswer && (
+
+ {[
+ { label: lang === 'fr' ? "Revoir" : lang === 'ja' ? "やり直し" : "Again", color: "bg-red-500/10 text-red-600 hover:bg-red-500/20" },
+ { label: lang === 'fr' ? "Ok" : lang === 'ja' ? "了解" : "Good", color: "bg-amber-500/10 text-amber-600 hover:bg-amber-500/20" },
+ { label: lang === 'fr' ? "Parfait" : lang === 'ja' ? "完璧" : "Easy", color: "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" }
+ ].map((btn, idx) => (
+ {
+ setShowFlashcardAnswer(false);
+ setFlashcardIdx((prev) => (prev + 1) % 2);
+ }}
+ className={`px-3 py-1 text-[8.5px] font-mono font-black uppercase transition-colors cursor-pointer rounded-md ${btn.color}`}
+ >
+ {btn.label}
+
+ ))}
+
+ )}
+
+
+
+ Spaced recall study active
+ {t('workspace_study_memory')}
+
+
+ )}
+
+ {/* 5. BACKUPS & TEMPORAL HISTORY SNAPSHOTS SCREEN */}
+ {activeTab === 'history' && (
+
+
+
+ {t('workspace_snapshot_title')}
+
+
{t('workspace_snapshot_desc')}
+
+
+ {/* List timelines */}
+
+ {[
+ {
+ time: lang === 'fr' ? "Aujourd'hui, 09:56" : lang === 'ja' ? "今日, 09:56" : "Today, 09:56",
+ event: lang === 'fr' ? "Modification de l'Équation" : lang === 'ja' ? "数式マトリクスの編集" : "Equation structure modified",
+ size: lang === 'fr' ? "182 mots" : lang === 'ja' ? "182語" : "182 words",
+ active: true
+ },
+ {
+ time: lang === 'fr' ? "Aujourd'hui, 09:21" : lang === 'ja' ? "今日, 09:21" : "Today, 09:21",
+ event: lang === 'fr' ? "Import de l'Agent Scraper" : lang === 'ja' ? "スクレイパー起動ログ" : "Scraper Agent Log Import",
+ size: lang === 'fr' ? "124 mots" : lang === 'ja' ? "124語" : "124 words"
+ },
+ {
+ time: lang === 'fr' ? "Hier, 14:20" : lang === 'ja' ? "昨日, 14:20" : "Yesterday, 14:20",
+ event: lang === 'fr' ? "Création initiale de la trame" : lang === 'ja' ? "初期枠組みの構築" : "Initial model setup",
+ size: lang === 'fr' ? "86 mots" : lang === 'ja' ? "86語" : "86 words"
+ }
+ ].map((snap, i) => (
+
+
+
+
+ {snap.event}
+ {snap.time} · {snap.size}
+
+
+
+
{
+ if (i === 1) triggerSimulation(1);
+ if (i === 2) triggerSimulation(0);
+ }}
+ >
+ {snap.active ? (lang === 'fr' ? "Actif" : lang === 'ja' ? "適用中" : "Active") : t('workspace_restore', true)}
+
+
+ ))}
+
+
+
+ {t('workspace_state_saved')}
+ {lang === 'fr' ? "Secteurs locaux protégés pour de bon" : lang === 'ja' ? "ローカル保護領域:正常" : "Continuous sector write: OK"}
+
+
+ )}
+
+
+
+
+
+
+ {/* Console status footer */}
+
+
+
+
+ SECURE KERNEL
+
+
|
+
{lang === 'fr' ? "Totalement chiffré" : lang === 'ja' ? "完全暗号化ローカル保管" : "AES-256 local encrypted"}
+
|
+
{lang === 'fr' ? "STOCKAGE PRIVÉ ACTIF" : lang === 'ja' ? "セキュア物理保存" : "ACTIVE EXCLUSIVE COLD STORAGE"}
+
+
+
+ {t('nav_start', true)}
+
+
+
+
+
+
+
+
+ {/* Features Grid */}
+
+
+
+
+ {t('features_badge')}
+
+ {t('features_title_1')} {t('features_title_2')}
+
+
+
+ {t('features_subtitle')}
+
+
+
+
+ {[
+ {
+ icon:
,
+ title: t('f1_title'),
+ desc: t('f1_desc')
+ },
+ {
+ icon:
,
+ title: t('f2_title'),
+ desc: t('f2_desc')
+ },
+ {
+ icon:
,
+ title: t('f3_title'),
+ desc: t('f3_desc')
+ }
+ ].map((feature, i) => (
+
+
+ {feature.icon}
+
+
{feature.title}
+
{feature.desc}
+
+ ))}
+
+
+
+
+ {/* Agent Showcase */}
+
+
+
+
+
+
{t('agents_badge')}
+
{t('agents_title')}
+
{t('agents_subtitle')}
+
+
+
+ {[
+ { type: "Scraper", icon:
, desc: t('agent_scraper_desc') },
+ { type: "Researcher", icon:
, desc: t('agent_researcher_desc') },
+ { type: "Slide Gen", icon:
, desc: t('agent_slide_desc') },
+ { type: "Monitor", icon:
, desc: t('agent_monitor_desc') },
+ { type: "Diagram Gen", icon:
, desc: t('agent_diagram_desc') },
+ { type: "Custom", icon:
, desc: t('agent_custom_desc') }
+ ].map((agent, i) => (
+
+
+ {agent.icon}
+
+
{agent.type}
+
{agent.desc}
+
+ ))}
+
+
+
+
+ {/* Brainstorm Section */}
+
+
+
+
{t('brain_badge')}
+
{t('brain_title')}
+
+ {[
+ { title: t('brain_item1_title'), desc: t('brain_item1_desc') },
+ { title: t('brain_item2_title'), desc: t('brain_item2_desc') },
+ { title: t('brain_item3_title'), desc: t('brain_item3_desc') }
+ ].map((item, i) => (
+
+
+ {i+1}
+
+
+
{item.title}
+
{item.desc}
+
+
+ ))}
+
+
+
+
+
+ {/* Floating nodes */}
+
+
{t('brain_node1_title')}
+
{t('brain_node1_text')}
+
+
+
{t('brain_node2_title')}
+
{t('brain_node2_text')}
+
+
+
+
+
+ {/* Tech Section */}
+
+
+
{t('tech_badge')}
+
{t('tech_title')}
+
+
+ {['OpenAI', 'Google', 'Anthropic', 'DeepSeek', 'Mistral', 'Meta', 'Ollama', 'Groq', 'X.AI', 'Custom'].map((brand, i) => (
+
+
+ {brand.slice(0,2).toUpperCase()}
+
+
{brand}
+
+ ))}
+
+
+
+ {[
+ { label: t('tech_tier1_label'), desc: t('tech_tier1_desc'), color: 'bg-accent' },
+ { label: t('tech_tier2_label'), desc: t('tech_tier2_desc'), color: 'bg-ochre' },
+ { label: t('tech_tier3_label'), desc: t('tech_tier3_desc'), color: 'bg-ink' }
+ ].map((tier, i) => (
+
+
+
{tier.label}
+
{tier.desc}
+
+ ))}
+
+
+
+
+ {/* Pricing Section */}
+
+
+
+
{t('price_badge')}
+
{t('price_title')}
+
{t('price_desc')}
+
+ {/* Billing Toggle */}
+
+
setBillingInterval('monthly')}
+ className={`group relative py-2 px-1 transition-all ${billingInterval === 'monthly' ? 'text-ink' : 'text-concrete/40 hover:text-concrete'}`}
+ >
+ {t('price_monthly')}
+ {billingInterval === 'monthly' && (
+
+ )}
+
+
+
+
setBillingInterval('annual')}
+ className={`group relative py-2 px-1 transition-all ${billingInterval === 'annual' ? 'text-ink' : 'text-concrete/40 hover:text-concrete'}`}
+ >
+ {t('price_annual')}
+ {billingInterval === 'annual' && (
+
+ )}
+
+
+ (-20%)
+
+
+
+
+
+
+ {[
+ {
+ name: t('price_free_name'),
+ price: t('price_free_price'),
+ desc: t('price_free_desc'),
+ features: tArray('price_free_features'),
+ cta: t('price_cta_start'),
+ popular: false
+ },
+ {
+ name: t('price_pro_name'),
+ price: lang === 'ja' ? '¥1,480' : planIsAnnual => planIsAnnual ? "7,90€" : "9,90€", // handle localized currencies seamlessly
+ period: billingInterval === 'monthly' ? t('price_period_month') : t('price_period_annual'),
+ desc: t('price_pro_desc'),
+ features: tArray('price_pro_features'),
+ cta: t('price_cta_pro'),
+ popular: true
+ },
+ {
+ name: t('price_biz_name'),
+ price: lang === 'ja' ? '¥4,480' : planIsAnnual => planIsAnnual ? "23,90€" : "29,90€",
+ period: billingInterval === 'monthly' ? t('price_period_month') : t('price_period_annual'),
+ desc: t('price_biz_desc'),
+ features: tArray('price_biz_features'),
+ cta: t('price_cta_biz'),
+ popular: false
+ },
+ {
+ name: t('price_ent_name'),
+ price: lang === 'ja' ? '要相談' : '49,90€',
+ period: billingInterval === 'monthly' ? t('price_period_month') : t('price_period_annual'),
+ desc: t('price_ent_desc'),
+ features: tArray('price_ent_features'),
+ cta: t('price_cta_ent'),
+ popular: false
+ }
+ ].map((plan, i) => {
+ const displayPrice = typeof plan.price === 'function' ? plan.price(billingInterval === 'annual') : plan.price;
+ return (
+
+ {plan.popular && (
+
+ {t('price_popular')}
+
+ )}
+
+
{plan.name}
+
+ {displayPrice}
+ {'period' in plan ? plan.period : ''}
+
+
{plan.desc}
+
+
+
+ {plan.features.map((feature, j) => (
+
+ ))}
+
+
+
+ {plan.cta}
+
+
+ );
+ })}
+
+
+ {/* BYOK Section */}
+
+
+
+
+ {t('byok_badge')}
+
+
{t('byok_title')}
+
+ {t('byok_desc')}
+
+
+
+
{t('byok_col1_title')}
+
{t('byok_col1_desc')}
+
+
+
{t('byok_col2_title')}
+
{t('byok_col2_desc')}
+
+
+
+
+
+
+
{"{"}
+
"provider": "anthropic",
+
"model": "claude-3-opus",
+
"apiKey": "sk-ant-at03-..."
+
"useSystemKey": false
+
{"}"}
+
+
+
{t('byok_config_title')}
+
+
+
+
+
+
+
+ {/* Ecosystem Section / Cross-promotion */}
+
+
+
+
+
+
+ {t('eco_badge')}
+
+
+ {t('eco_title_1')}
+ {t('eco_title_2')}
+
+
+ {t('eco_desc')}
+
+
+ {t('eco_button')}
+
+
+
+
+
+
+
+
+
+
+
+
{t('eco_original')}
+
+
+
+
{t('eco_translated')}
+
+
+
+
+
+
+ {/* Abstract document structure representation */}
+
+
+
+
+
+
+ {[1,2,3,4].map(i =>
)}
+
+
+
+
+
+ {t('eco_status')}
+
+
+
+
+
+
+
+ {/* Final CTA */}
+
+
+
+ {t('final_cta_title')}
+ {t('final_cta_title_italic')}
+
+
{t('final_cta_desc')}
+
+ {t('final_cta_button')}
+
+
+
+
+ {/* Footer */}
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/LandingPageV2.tsx b/architectural-grid1/src/components/LandingPageV2.tsx
new file mode 100644
index 0000000..5713e61
--- /dev/null
+++ b/architectural-grid1/src/components/LandingPageV2.tsx
@@ -0,0 +1,1015 @@
+import React from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ BrainCircuit,
+ Search,
+ MessageSquare,
+ ArrowRight,
+ Sparkles,
+ Activity,
+ Play,
+ Terminal,
+ History,
+ ChevronRight,
+ Network,
+ Clock,
+ BookOpen,
+ Sliders,
+ CheckSquare,
+ Lock,
+ Compass,
+ Layers,
+ Heart,
+ PlusSquare,
+ FileText,
+ HelpCircle,
+ Eye,
+ RefreshCw,
+ Database
+} from 'lucide-react';
+
+interface LandingPageV2Props {
+ onEnter: () => void;
+ onLogin: () => void;
+ onRegister: () => void;
+ onSwitchVersion: (v: 'v1' | 'v2' | 'v3') => void;
+}
+
+export const LandingPageV2: React.FC = ({
+ onEnter,
+ onLogin,
+ onRegister,
+ onSwitchVersion
+}) => {
+
+ // Real world Momento note seeds to match constants.ts exactly
+ const realNotes = [
+ {
+ id: 'n1',
+ title: "Grid Systems & Geometry",
+ carnet: "Architecture Research",
+ date: "26 Oct 2024",
+ tags: ["Architecture", "Systems (IA)"],
+ content: `# Grid Systems & Geometry
+
+Les trames géométriques constituent le fondement même du design cognitif. Elles structurent l'espace bâti en créant un sens d'ordre, de rythme, et d'harmonie de proportions esthétiques.
+
+$$R_{ratio} = \\frac{1 + \\sqrt{5}}{2} \\approx 1.618$$
+
+## 1. Grilles Orthogonales
+L'utilisation séculaire du Nombre d'Or permet d'ancrer de manière pragmatique les volumes de construction dans une perspective visuelle harmonieuse.
+
+* **Structure** : Délimitation et alignement rigoureux des ouvertures.
+* **Rythme** : Répartition équilibrée de la lumière naturelle et des masses de soutien.`,
+ nodes: ["Nombre d'Or", "Trame Orthogonale", "Proportion", "Lumière"],
+ stats: { words: 86, lines: 11, equations: 1, graphs: 3, images: 1 }
+ },
+ {
+ id: 'n2',
+ title: "Sustainable Materiality",
+ carnet: "Sustainable Design",
+ date: "24 Oct 2024",
+ tags: ["Materials", "Sustainability (IA)"],
+ content: `# Sustainable Materiality
+
+L'exploration du bois lamellé-croisé (CLT - Cross Laminated Timber) se présente comme l'une des meilleures alternatives écologiques au béton armé conventionnel.
+
+$$\\Delta Carbon = E_{béton} - E_{CLT} = 410 \\text{ kg } CO_2/m^3$$
+
+## 1. Avantages du CLT (Bois Massif)
+L'exploitation raisonnée des forêts et l'empreinte carbone négative de la cellulose captent durablement les gaz polluants du Cycle Global.
+
+* **Rapport Poids/Résistance** : Équivalent à la solidité de l'acier avec un poids divisé par quatre.
+* **Isolation Passive** : Conductivité thermique extrêmement faible, limitant la déperdition durant l'hiver.`,
+ nodes: ["Bois CLT", "Éco-Conception", "Bilan Carbone", "Coût Thermique"],
+ stats: { words: 91, lines: 12, equations: 1, graphs: 4, images: 1 }
+ },
+ {
+ id: 'n3',
+ title: "Light & Minimalist Space",
+ carnet: "Modernism",
+ date: "22 Oct 2024",
+ tags: ["Lighting", "Atmosphere (IA)"],
+ content: `# Light & Minimalist Space
+
+Le minimalisme consiste à soustraire le superflu pour ne laisser briller que l'essence même de l'espace. Dans cette optique, la lumière naturelle cesse d'être un simple facteur externe et devient un véritable matériau structurel.
+
+## 1. La Diffraction Lumineuse
+La transparence et la réflexion du verre feuilleté démultiplient l'espace sans exiger d'éléments cloisons additionnels.
+
+* **Pureté Visuelle** : Transition fluide entre l'intérieur intime et l'extérieur sauvage.
+* **Économie Formelle** : Utilisation de nuances de blanc mat pour emprisonner le rayonnement diffus.`,
+ nodes: ["Réfraction", "Verre Feuilleté", "Espace Continu", "Ombres Portées"],
+ stats: { words: 82, lines: 10, equations: 0, graphs: 3, images: 1 }
+ }
+ ];
+
+ const [activeDemoIdx, setActiveDemoIdx] = React.useState(0);
+ const [activeTab, setActiveTab] = React.useState<'editor' | 'graph' | 'agents' | 'reviews' | 'history'>('editor');
+ const [simulateState, setSimulateState] = React.useState<'searching' | 'writing' | 'idle' | 'complete'>('idle');
+ const [displayText, setDisplayText] = React.useState('');
+ const [aiSidebarTab, setAiSidebarTab] = React.useState<'explore' | 'discussion' | 'relations'>('explore');
+ const [customPrompt, setCustomPrompt] = React.useState('');
+
+ // Spaced-repetition simulated states
+ const [showFlashcardAnswer, setShowFlashcardAnswer] = React.useState(false);
+ const [flashcardIdx, setFlashcardIdx] = React.useState(0);
+
+ const simulatedFlashcards = [
+ {
+ id: 'f1',
+ question: "Quel rôle joue le Nombre d'Or (1.618) dans les structures d'architecture ?",
+ answer: "Il sert à définir des proportions optimales de fenêtrage et de colonnes, créant une harmonie visuelle captée naturellement par l'œil humain."
+ },
+ {
+ id: 'f2',
+ question: "Pourquoi privilégier le bois CLT (lamellé-croisé) au béton armé ?",
+ answer: "Il offre un stockage actif du CO2 (bilan carbone négatif) et présente un rapport poids/résistance exceptionnel tout en améliorant l'isolation passive."
+ }
+ ];
+
+ // Launch simulated real-time streaming text writing in chunks
+ const triggerSimulation = (index: number, customText?: string) => {
+ setActiveDemoIdx(index);
+ setSimulateState('searching');
+ setDisplayText('');
+ setShowFlashcardAnswer(false);
+
+ // Auto shift corresponding sidebar layout to keep user context rich
+ if (index === 0) setAiSidebarTab('explore');
+ if (index === 1) setAiSidebarTab('relations');
+ if (index === 2) setAiSidebarTab('discussion');
+
+ const sourceText = customText || (index === -1 ? "" : realNotes[index].content);
+
+ setTimeout(() => {
+ setSimulateState('writing');
+ const totalLen = sourceText.length;
+ let currentLength = 0;
+
+ const timer = setInterval(() => {
+ currentLength += 22; // Quick interactive flow
+ if (currentLength >= totalLen) {
+ setDisplayText(sourceText);
+ setSimulateState('complete');
+ clearInterval(timer);
+ } else {
+ setDisplayText(sourceText.substring(0, currentLength));
+ }
+ }, 15);
+ }, 900);
+ };
+
+ const handleCustomSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!customPrompt.trim()) return;
+
+ const query = customPrompt;
+ const generatedContent = `# Synthèse : ${query}
+
+Analyse automatique issue de vos documents locaux avec intégration sémantique complète.
+
+## 1. Formule Logique Relative
+Les connexions de votre idée s'harmonisent selon le coefficient géométrique de structure :
+
+$$\\Omega(x) = \\int_{0}^{\\Lambda} e^{-k \\cdot x} \\cdot dx$$
+
+* **Analyse sémantique** : Les concepts périphériques sont cartographiés.
+* **Consolidation passive** : Préservation totale des métadonnées du carnet.`;
+
+ triggerSimulation(-1, generatedContent);
+ };
+
+ React.useEffect(() => {
+ triggerSimulation(0);
+ }, []);
+
+ return (
+
+
+ {/* 1. Sticky Navigation Header */}
+
+
+
+ M
+
+
+ Momento
+ Second Cerveau
+
+
+
+ {/* Interactive layout version switcher */}
+
+ onSwitchVersion('v1')}
+ className="px-4 py-1.5 text-[9.5px] uppercase tracking-widest font-bold rounded-full text-concrete hover:text-ink transition-all cursor-pointer"
+ >
+ Classique (V1)
+
+ onSwitchVersion('v2')}
+ className="px-4.5 py-1.5 text-[9.5px] uppercase tracking-widest font-black rounded-full bg-ink text-paper shadow-sm cursor-pointer"
+ >
+ Interactif (V2)
+
+ onSwitchVersion('v3')}
+ className="px-4 py-1.5 text-[9.5px] uppercase tracking-widest font-bold rounded-full text-concrete hover:text-ink transition-all cursor-pointer"
+ >
+ Bilingue (V3) ✨
+
+
+
+
+
+ Se Connecter
+
+
+ Lancer l'éditeur
+
+
+
+
+
+ {/* 2. Main High-Fidelity Introduction & Split Screen Grid */}
+
+
+
+ {/* Left Hero Sidebar: Information on Momento's Core Features */}
+
+
+
+
+ DÉMO EN DIRECT ET COMPLÈTE DE L'INTERFACE
+
+
+
+ Pensez local.
+ Savoirs interconnectés.
+
+
+
+ Momento rassemble vos notes, vos formules mathématiques et votre logique dans une structure d'apprentissage offline d'une flexibilité absolue. Évitez les abonnements ruineux en apportant votre clé personnelle (BYOK) ou utilisez l'environnement de manière 100% autonome et sécurisée.
+
+
+ {/* Guided Feature Tabs Selector: Directly gives info on what the user can do */}
+
+
+ 💡 Cliquez pour explorer une fonctionnalité réelle :
+
+
+
+ {[
+ { key: 'editor', title: "L'Éditeur Papier & Carnets", desc: "Notes hiérarchiques, listes et formules de calcul", icon:
, activeColor: "border-amber-700/30 bg-amber-50/20" },
+ { key: 'graph', title: "Le Graphe de Connaissances Spatiales", desc: "Modélisation sémantique interactive et liens de vos idées", icon:
, activeColor: "border-purple-650/30 bg-purple-50/25" },
+ { key: 'agents', title: "Les 6 Agents IA d'Enrichissement", desc: "Recherches automatisées, cartes mentales et résumés actifs", icon:
, activeColor: "border-blue-600/30 bg-blue-50/25" },
+ { key: 'reviews', title: "Le Deck de Révisions Smart Cards", desc: "Génération de Flashcards & répétition intelligente espacée", icon:
, activeColor: "border-emerald-600/30 bg-emerald-50/25" },
+ { key: 'history', title: "Snapshots Historiques & Restauration", desc: "Versionning de sécurité et sauvegarde locale persistante", icon:
, activeColor: "border-indigo-600/30 bg-indigo-50/25" },
+ ].map((tab) => (
+
{
+ setActiveTab(tab.key as any);
+ if (tab.key === 'editor' || tab.key === 'agents') {
+ triggerSimulation(0);
+ }
+ }}
+ className={`w-full text-left p-3.5 rounded-xl border transition-all duration-300 flex items-start gap-3 cursor-pointer group
+ ${activeTab === tab.key
+ ? `${tab.activeColor} border-accent shadow-xs`
+ : 'bg-transparent border-transparent hover:border-black/5 hover:bg-stone-50'
+ }`}
+ >
+
+ {tab.icon}
+
+
+
+ {tab.title}
+
+
+ {tab.desc}
+
+
+
+ ))}
+
+
+
+
+ {/* CTA action button */}
+
+
+ Entrer dans l'application libre
+
+
+
+
+ Local storage direct · Aucune inscription requise
+
+
+
+
+ {/* Right Hero Column: True 1-to-1 Interactive Representation of Momento Workspace */}
+
+
+
+ {/* Top Bar Navigation Buttons & Markers */}
+
+
+
+
+
+ momento-workspace-client
+
+
+ {activeTab === 'editor' && (
+
+
+ Note active : {activeDemoIdx === -1 ? "Recherche perso" : realNotes[activeDemoIdx].title}
+
+ )}
+ {activeTab === 'graph' && (
+
+ KNOWLEDGE MAP DETECTED
+
+ )}
+ {activeTab === 'agents' && (
+
+ 6 COGNITIVE AGENTS ON-DUTY
+
+ )}
+ {activeTab === 'reviews' && (
+
+ SPACED REPETITION STUDY
+
+ )}
+ {activeTab === 'history' && (
+
+ VERSION CONTROL SNAPSHOTS
+
+ )}
+
+
+ {/* Dynamic Interactive Panel Wrapper */}
+
+
+ {/* 1. Common Miniature left control rail */}
+
+
+ setActiveTab('editor')}
+ className={`p-1.8 rounded-lg cursor-pointer transition-colors ${activeTab === 'editor' ? 'text-accent bg-white border border-border shadow-xs' : 'text-stone-400 hover:text-ink'}`}
+ title="Éditeur de Notes"
+ >
+
+
+ setActiveTab('graph')}
+ className={`p-1.8 rounded-lg cursor-pointer transition-colors ${activeTab === 'graph' ? 'text-accent bg-white border border-border shadow-xs' : 'text-stone-400 hover:text-ink'}`}
+ title="Graphe de connaissances"
+ >
+
+
+ setActiveTab('agents')}
+ className={`p-1.8 rounded-lg cursor-pointer transition-colors ${activeTab === 'agents' ? 'text-accent bg-white border border-border shadow-xs' : 'text-stone-400 hover:text-ink'}`}
+ title="Agents d'Enrichissement"
+ >
+
+
+ setActiveTab('reviews')}
+ className={`p-1.8 rounded-lg cursor-pointer transition-colors ${activeTab === 'reviews' ? 'text-accent bg-white border border-border shadow-xs' : 'text-stone-400 hover:text-ink'}`}
+ title="Deck de Flashcards"
+ >
+
+
+ setActiveTab('history')}
+ className={`p-1.8 rounded-lg cursor-pointer transition-colors ${activeTab === 'history' ? 'text-accent bg-white border border-border shadow-xs' : 'text-stone-400 hover:text-ink'}`}
+ title="Historique Snapshots"
+ >
+
+
+
+
+
+
+
+
+
+ {/* 2. Content view depending on active TAB */}
+
+
+
+ {/* VIEW A: EDITOR & CARNET SYSTEM */}
+ {activeTab === 'editor' && (
+
+ {/* Nested Folder Panel */}
+
+
Notes Disponibles
+
+ {realNotes.map((note, idx) => (
+ triggerSimulation(idx)}
+ className={`w-full text-left p-2.5 rounded-lg border text-xs transition-colors cursor-pointer block
+ ${activeDemoIdx === idx
+ ? 'bg-[#EAE8DF] border-accent/25'
+ : 'border-transparent bg-stone-50 hover:bg-[#EAE8DF]/40'
+ }`}
+ >
+ {note.carnet}
+ {note.title}
+
+ ))}
+ triggerSimulation(0)}
+ className="w-full text-center border border-dashed border-accent/30 py-2 rounded-lg text-[9px] font-mono text-accent block hover:bg-accent/5"
+ >
+ + Nouveau Document
+
+
+
+
+ {/* Middle Rich Editor Page */}
+
+
+
+ {activeDemoIdx === -1 ? "RECHERCHE PERSO" : realNotes[activeDemoIdx].carnet}
+ {activeDemoIdx === -1 ? "MAI 2026" : realNotes[activeDemoIdx].date}
+
+
+
+ {activeDemoIdx === -1 ? "Votre de Note sémantique" : realNotes[activeDemoIdx].title}
+
+
+
+ {(activeDemoIdx === -1 ? [{ label: "Structure perso", type: "user" }] : realNotes[activeDemoIdx].tags.map(t => ({ label: t, type: "user" }))).map((tag, i) => (
+
+ {tag.label}
+
+ ))}
+
+
+ {/* Streamer loader */}
+
+
+ {simulateState === 'searching' && (
+
+
+
+
+
+
+
+
Lecture locale...
+
+ )}
+
+ {(simulateState === 'writing' || simulateState === 'complete') && (
+
+ {displayText.split('\n').map((line, idx) => {
+ if (line.startsWith('# ')) return null;
+ if (line.startsWith('## ')) {
+ return
{line.replace('## ', '')} ;
+ }
+ if (line.startsWith('$$')) {
+ return (
+
+ {line.replace(/\$\$/g, '')}
+
+ );
+ }
+ if (line.startsWith('* ')) {
+ return (
+
+ •
+ {line.replace('* ', '')}
+
+ );
+ }
+ return
{line}
;
+ })}
+
+ )}
+
+
+
+
+
+ )}
+
+ {/* VIEW B: MOOD-REINFORCED KNOWLEDGE GRAPH MAP */}
+ {activeTab === 'graph' && (
+
+
+
Cartographie de Votre Deuxième Cerveau
+
Double-cliquez n'importe quel nœud pour naviguer dans l'espace ou lier vos notes.
+
+
+ {/* Real-looking interactive diagram representation */}
+
+
+ {/* Dot Grid Blueprint SVG background */}
+
+
+
+
+
+
+
+
+ {/* Visual connections with gorgeous soft accent glow */}
+
+
+
+
+
+
+
+
+
+
+ {/* central nexus */}
+
+ MOMENTO
+
+
+ {/* Clustered conceptual nodes linked with visual lines */}
+ {[
+ { name: "Nombre d'Or", x: -100, y: -65, tag: "Architecture", theme: "text-amber-800 bg-amber-50/95" },
+ { name: "Diffraction lumineuse", x: 105, y: -72, tag: "Optique", theme: "text-violet-850 bg-violet-50/95" },
+ { name: "Bois CLT Lamellé", x: -115, y: 70, tag: "Matériaux", theme: "text-emerald-800 bg-emerald-50/95" },
+ { name: "Transition Formelle", x: 110, y: 60, tag: "Minimalisme", theme: "text-blue-800 bg-blue-50/95" },
+ { name: "Grid System", x: 0, y: -100, tag: "IA Géométrie", theme: "text-stone-800 bg-stone-50/95" },
+ { name: "Éco-Conception", x: 0, y: 100, tag: "Coût Carbone", theme: "text-rose-800 bg-rose-50/95" }
+ ].map((node, i) => {
+ return (
+
+
+ {node.name}
+
+ {node.tag}
+
+
+
+ );
+ })}
+
+
+
+ 6 documents connectés
+ Structure sémantique : Optimale ✓
+
+
+ )}
+
+ {/* VIEW C: THE 6 INTELLIGENT AGENTS SYSTEM */}
+ {activeTab === 'agents' && (
+
+ {/* Selector items */}
+
+
Système d'Agents (6)
+
+ {[
+ { id: "scr", name: "Web Scraper Agent", role: "Enrichit vos notes via analyse d'URL", tag: "SCR", color: "bg-amber-120 text-amber-900 border-amber-200" },
+ { id: "res", name: "Deep Researcher", role: "Excave les bases et revues sur le Web", tag: "RES", color: "bg-teal-100 text-teal-900 border-teal-200" },
+ { id: "sli", name: "Slide Deck Builder", role: "Convertit vos idées en slides articulées", tag: "PPT", color: "bg-blue-100 text-blue-900 border-blue-200" },
+ { id: "mon", name: "Knowledge Monitor", role: "Identifie les lacunes & opportunités de liens", tag: "LNK", color: "bg-purple-100 text-purple-900 border-purple-200" },
+ { id: "dia", name: "Mindmap Architect", role: "Génère des cartes mentales interactives", tag: "MAP", color: "bg-emerald-100 text-emerald-900 border-emerald-200" },
+ { id: "cus", name: "Agent Sur-Mesure", role: "Définissez des consignes et rôles précis", tag: "IA", color: "bg-stone-100 text-stone-900 border-stone-200" }
+ ].map((a) => (
+
+
+ {a.name}
+ {a.tag}
+
+
{a.role}
+
+ ))}
+
+
+
+ {/* Active Agent Interactive Chat/RAG sidebar mockup */}
+
+
+
+ {([
+ { key: 'explore', label: 'Explore (RAG)' },
+ { key: 'discussion', label: 'Discussion' },
+ { key: 'relations', label: 'Rapports' }
+ ] as const).map((tab) => (
+ setAiSidebarTab(tab.key)}
+ className={`flex-1 py-2 text-[8.5px] uppercase tracking-wider font-extrabold text-stone-500 border-r last:border-r-0 border-border transition-all cursor-pointer text-center
+ ${aiSidebarTab === tab.key ? 'bg-white text-ink border-b-2 border-b-accent' : 'hover:bg-white/40'}`}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+
+ {aiSidebarTab === 'explore' && (
+
+ Analyse RAG passive active :
+
+ TERMES DE REQUÊTE :
+ "{activeDemoIdx === -1 ? customPrompt || "Géométrie d'or" : realNotes[activeDemoIdx].title}"
+
+
+
Scraping de sources web et articles :
+
+ https://wikipedia.org/wiki/Grid_system_(graphic_design)
+ 200 OK
+
+
+
+ )}
+
+ {aiSidebarTab === 'discussion' && (
+
+
+ Consigne utilisateur
+ Résume les points géométriques en 2 points clés.
+
+
+ Modèle de langage (Gemini)
+ 1. Utilisation du Nombre d'Or pour la logique de trame.
+ 2. Alignement des forces lumineuses déplaçables.
+
+
+ )}
+
+ {aiSidebarTab === 'relations' && (
+
+ Index Sémantique
+
+ Score de similarité sémantique
+ 94% ✓
+
+
+ )}
+
+
+
+
+
+ SÉCURITÉ CHAT : 100% CRYPTÉ
+ PROCESSEUR : BYOK ACTIF
+
+
+
+ )}
+
+ {/* VIEW D: ACTIVE RECALL / SPACED REPETITION FLASHCARDS */}
+ {activeTab === 'reviews' && (
+
+
+
Système d'Étude Active Spaced-Repetition
+
L'IA parcourt vos notes pour vous tester et ancrer les concepts dans votre mémoire de travail.
+
+
+ {/* Visual study card mockup */}
+
+
+ Question {flashcardIdx + 1}/2
+
+ {simulatedFlashcards[flashcardIdx].question}
+
+
+
+ {showFlashcardAnswer ? (
+
+ Réponse correcte
+ {simulatedFlashcards[flashcardIdx].answer}
+
+ ) : (
+
setShowFlashcardAnswer(true)}
+ className="px-4 py-2 bg-accent/10 hover:bg-accent/20 text-accent transition-colors text-[10px] font-bold uppercase tracking-wider rounded-xl cursor-pointer"
+ >
+ Afficher la Réponse
+
+ )}
+
+ {showFlashcardAnswer && (
+
+ {[
+ { label: "À revoir", color: "bg-red-500/10 text-red-600 hover:bg-red-500/20" },
+ { label: "Moyen", color: "bg-amber-500/10 text-amber-600 hover:bg-amber-500/20" },
+ { label: "Facile", color: "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" }
+ ].map((btn, i) => (
+ {
+ setShowFlashcardAnswer(false);
+ setFlashcardIdx((prev) => (prev + 1) % 2);
+ }}
+ className={`px-3 py-1.5 rounded-lg text-[9px] font-mono font-black uppercase transition-colors cursor-pointer ${btn.color}`}
+ >
+ {btn.label}
+
+ ))}
+
+ )}
+
+
+
+ Étude de Mémoire
+ Algorithme SuperMemo actif
+
+
+ )}
+
+ {/* VIEW E: SNAPSHOTS & VERSIONS HISTORY */}
+ {activeTab === 'history' && (
+
+
+
Snapshot de Sécurité Temporel
+
Explorez et restaurez n'importe quel état de rédaction antérieur. Zéro perte accidentelle.
+
+
+ {/* Simulated timeline events */}
+
+ {[
+ { time: "Aujourd'hui, 09:56", event: "Modification de l'Equation", author: "Moi", size: "182 mots", active: true },
+ { time: "Aujourd'hui, 09:21", event: "Analyse d'échantillons CLT par l'Agent Scraper", author: "Agent IA", size: "124 mots" },
+ { time: "Hier, 14:20", event: "Import de grilles orthogonales initiales", author: "Moi", size: "86 mots" },
+ { time: "22 Mai 2026", event: "Création du document d'étude", author: "Système", size: "12 mots" }
+ ].map((snap, i) => (
+
+
+
+
+ {snap.event}
+ {snap.time} · Auteur : {snap.author}
+
+
+
+
{
+ if (i === 1) triggerSimulation(1);
+ if (i === 2) triggerSimulation(0);
+ }}
+ >
+ {snap.active ? "Actif ✓" : "Restauration"}
+
+
+ ))}
+
+
+
+ État local sauvegardé
+ Compression d'historique : Active
+
+
+ )}
+
+
+
+
+
+
+ {/* Minimalist modern control status bar */}
+
+
+
+
+ ESPACE AUTONOME
+
+
|
+
Zéro cloud requis
+
|
+
Bases cryptées localement
+
+
+
+ Ouvrir l'application
+
+
+
+
+
+
+
+
+
+
+ {/* 3. "BYOK" (Bring Your Own Key) & Privacy Showcase section */}
+
+
+
+
Respect Souverain des Écrits
+
Aucun abonnement IA obligatoire grâce au modèle "BYOK".
+
+ La plupart des applications cloud d'intelligence artificielle re-facturent de lourdes marges commerciales sur vos écrits. Momento change les règles du jeu :
+
+
+
+
+
+
+
+
Données stockées localement dans votre navigateur
+
Zéro base de données tiers n'a accès à vos cours et recherches.
+
+
+
+
+
+
+
+
Ajoutez votre clé API gratuite ou premium
+
Entrez votre propre secret Gemini, OpenAI ou Anthropic pour payer sans commission, exactement selon vos besoins d'écriture.
+
+
+
+
+
+
+
+
+
{"{"}
+
"momento_client_profile": "offline_first",
+
"byok_providers": ["google_gemini", "openai_custom"],
+
"local_db_encryption": "AES_256_GCM",
+
"auto_save_active": true
+
{"}"}
+
+
+
+
+ Méthode d'exécution
+ Client-Side Direct Injection
+
+
+
+
+
+
+
+ {/* 4. Architectural Pillars section */}
+
+
+
+
Les Atouts Pratiques
+
Pourquoi utiliser Momento ?
+
Un écosystème conçu de bout en bout pour l'autonomie et la productivité.
+
+
+
+ {[
+ {
+ icon:
,
+ title: "Personnalisation Totale",
+ desc: "Réglez vos couleurs d'accentuation, triez vos tags et gérez l'interface pour correspondre à vos habitudes d'études."
+ },
+ {
+ icon:
,
+ title: "6 Modèles d'Agents",
+ desc: "Scrapeurs, réviseurs, générateurs de slides ou mindmaps d'un simple clic pour vous faire gagner des heures."
+ },
+ {
+ icon:
,
+ title: "Prise en main Libre",
+ desc: "Zéro formule de carte bancaire, zéro formulaire interminable. L'application s'initialise instantanément."
+ },
+ {
+ icon:
,
+ title: "100% Confidentialité",
+ desc: "Vos écrits ne quittent jamais votre machine locale. Aucune récolte ou exploitation de données n'est effectuée."
+ }
+ ].map((p, i) => (
+
+ ))}
+
+
+
+
+ {/* 5. Direct Launch Action Bottom Section */}
+
+
+
Structurer vos notes et vos pensées.
+
Accédez directement à l'espace de travail fluide de Momento, sans processus contraignant.
+
+
+ Lancer l'application
+
+
+ Me Connecter
+
+
+
+
+
+ {/* Skeuomorphic footer */}
+
+
+ );
+};
+
+// Miniature Icons placeholder to avoid unused variables or broken builds
+const CpuIconPlaceholder = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/architectural-grid1/src/components/LandingPageV3.tsx b/architectural-grid1/src/components/LandingPageV3.tsx
new file mode 100644
index 0000000..73d0e4d
--- /dev/null
+++ b/architectural-grid1/src/components/LandingPageV3.tsx
@@ -0,0 +1,1117 @@
+import React, { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ BrainCircuit,
+ Search,
+ MessageSquare,
+ ArrowRight,
+ Sparkles,
+ Activity,
+ History,
+ Network,
+ Clock,
+ BookOpen,
+ Sliders,
+ CheckSquare,
+ Lock,
+ Compass,
+ Layers,
+ Cpu,
+ Globe,
+ Languages,
+ Check,
+ Zap,
+ HelpCircle
+} from 'lucide-react';
+
+// Define the shape of our localized dictionary keys
+interface LangDict {
+ nav_features: string;
+ nav_agents: string;
+ nav_brainstorm: string;
+ nav_pricing: string;
+ nav_architecture: string;
+ nav_version_v1: string;
+ nav_version_v2: string;
+ nav_version_v3: string;
+ nav_login: string;
+ nav_start: string;
+
+ hero_badge: string;
+ hero_title_1: string;
+ hero_title_italic: string;
+ hero_desc: string;
+ hero_cta_start: string;
+ hero_cta_features: string;
+
+ features_badge: string;
+ features_title: string;
+ features_subtitle: string;
+ features_desc: string;
+
+ feature_1_title: string;
+ feature_1_desc: string;
+ feature_2_title: string;
+ feature_2_desc: string;
+ feature_3_title: string;
+ feature_3_desc: string;
+
+ workspace_title_editor: string;
+ workspace_title_graph: string;
+ workspace_title_agents: string;
+ workspace_title_reviews: string;
+ workspace_title_history: string;
+ workspace_notes_available: string;
+ workspace_new_doc: string;
+ workspace_local_reading: string;
+ workspace_graph_map_title: string;
+ workspace_graph_map_desc: string;
+ workspace_graph_connected: string;
+ workspace_graph_status: string;
+
+ workspace_agents_title: string;
+ workspace_active_prompt: string;
+ workspace_rag_active: string;
+ workspace_consigne: string;
+ workspace_secure_chat: string;
+ workspace_processor: string;
+
+ workspace_recall_title: string;
+ workspace_recall_desc: string;
+ workspace_show_answer: string;
+ workspace_correct_answer: string;
+ workspace_study_memory: string;
+
+ workspace_snapshot_title: string;
+ workspace_snapshot_desc: string;
+ workspace_restore: string;
+ workspace_state_saved: string;
+}
+
+const FR: LangDict = {
+ nav_features: "Fonctionnalités",
+ nav_agents: "Agents IA",
+ nav_brainstorm: "Brainstorm",
+ nav_pricing: "Tarifs",
+ nav_architecture: "Architecture",
+ nav_version_v1: "Classique V1",
+ nav_version_v2: "Interactif V2",
+ nav_version_v3: "Bilingue V3",
+ nav_login: "Connexion",
+ nav_start: "Commencer",
+
+ hero_badge: "Amplifié par l'intelligence collective locale & cloud",
+ hero_title_1: "Votre second cerveau,",
+ hero_title_italic: "enfin amplifié.",
+ hero_desc: "Momento est un écosystème d'écriture intelligent en temps réel. Naviguez dans vos pensées via un graphe de connaissances 3D, étudiez avec répétition espacée, et déléguez vos recherches à 6 agents spécialisés autonomes.",
+ hero_cta_start: "S'inscrire gratuitement",
+ hero_cta_features: "Explorer l'espace",
+
+ features_badge: "CAPACITÉS DU SYSTÈME",
+ features_title: "Une sémantique naturelle,",
+ features_subtitle: "fondée sur vos apprentissages.",
+ features_desc: "Momento orchestre vos idées grâce à une architecture locale cryptée.",
+
+ feature_1_title: "Recherche Sémantique Hybride",
+ feature_1_desc: "Trouvez vos concepts au-delà des synonymes. Notre moteur hybride vectoriel comprend intrinsèquement l'intention de vos rédactions.",
+ feature_2_title: "Chat Contextuel RAG local",
+ feature_2_desc: "Échangez directement avec votre corpus de fiches. Les agents lisent vos notes, consultent le web et synthétisent avec précision.",
+ feature_3_title: "Écriture Augmentée Passive",
+ feature_3_desc: "L'IA auto-complète, structure les balises mathématiques LaTeX et tisse des liens d'idées de façon autonome.",
+
+ workspace_title_editor: "Éditeur de Notes",
+ workspace_title_graph: "Graphe Spatial",
+ workspace_title_agents: "Enrichissement IA",
+ workspace_title_reviews: "Révisions Actives",
+ workspace_title_history: "Snapshots Temporels",
+ workspace_notes_available: "Documents Locaux",
+ workspace_new_doc: "+ Créer Note",
+ workspace_local_reading: "Lecture du stockage local...",
+ workspace_graph_map_title: "Cartographie Interactive de Formes",
+ workspace_graph_map_desc: "Vos écrits se connectent automatiquement selon leur signification profonde.",
+ workspace_graph_connected: "6 notes interconnectées",
+ workspace_graph_status: "Structure Sémantique : Optimale ✓",
+
+ workspace_agents_title: "Système Cognitif d'Agents",
+ workspace_active_prompt: "TERMES ACTIFS DE RECHERCHE :",
+ workspace_rag_active: "RAG sémantique actif :",
+ workspace_consigne: "Consigne d'exploration :",
+ workspace_secure_chat: "SÉCURITÉ : CRYPTAGE SYSTÉMATIQUE",
+ workspace_processor: "SYSTÈME : BYOK ACTIF",
+
+ workspace_recall_title: "Répétition Espacée active",
+ workspace_recall_desc: "Algorithme actif convertissant automatiquement vos documents en cartes mémoires réactives.",
+ workspace_show_answer: "Afficher la réponse",
+ workspace_correct_answer: "Réponse attendue :",
+ workspace_study_memory: "Algorithme SuperMemo actif (SM2)",
+
+ workspace_snapshot_title: "Versionning & Sauvegardes",
+ workspace_snapshot_desc: "Enregistrement continu. Restaurez n'importe quel état textuel à la seconde près.",
+ workspace_restore: "Restaurer",
+ workspace_state_saved: "Sauvegarde locale : Active"
+};
+
+const EN: LangDict = {
+ nav_features: "Features",
+ nav_agents: "AI Agents",
+ nav_brainstorm: "Brainstorm",
+ nav_pricing: "Pricing",
+ nav_architecture: "Architecture",
+ nav_version_v1: "Classic V1",
+ nav_version_v2: "Interactive V2",
+ nav_version_v3: "Bilingual V3",
+ nav_login: "Login",
+ nav_start: "Get Started",
+
+ hero_badge: "Amplified by local & cloud collective intelligence",
+ hero_title_1: "Your second brain,",
+ hero_title_italic: "finally amplified.",
+ hero_desc: "Momento is a real-time intelligent writing ecosystem. Navigate your thoughts via an interactive knowledge graph, study with spaced-repetition active recall, and delegate tasks to 6 autonomous specialist agents.",
+ hero_cta_start: "Start for free",
+ hero_cta_features: "Explore workspace",
+
+ features_badge: "SYSTEM CAPABILITIES",
+ features_title: "Natural semantics,",
+ features_subtitle: "grounded in your knowledge.",
+ features_desc: "Momento orchestrates your thoughts through an encrypted local architecture.",
+
+ feature_1_title: "Hybrid Semantic Search",
+ feature_1_desc: "Search concepts, not just keywords. Our hybrid vector engine understands the core semantic intention of your notes.",
+ feature_2_title: "Local Contextual RAG Chat",
+ feature_2_desc: "Talk directly to your knowledge base. Agents read your notes, browse the web, and answer queries with precise citations.",
+ feature_3_title: "Passive Augmented Writing",
+ feature_3_desc: "The assistant auto-generates, formats LaTeX formulae, auto-tags, and binds connections dynamically behind the scenes.",
+
+ workspace_title_editor: "Note Editor",
+ workspace_title_graph: "Knowledge Graph",
+ workspace_title_agents: "AI Enrichment",
+ workspace_title_reviews: "Active Recall",
+ workspace_title_history: "Time Snapshots",
+ workspace_notes_available: "Local Documents",
+ workspace_new_doc: "+ Create Note",
+ workspace_local_reading: "Reading local storage...",
+ workspace_graph_map_title: "Interactive Space Mapping",
+ workspace_graph_map_desc: "Your nodes attach automatically as semantic similarity is detected.",
+ workspace_graph_connected: "6 interconnected links",
+ workspace_graph_status: "Semantic Structure: Optimal ✓",
+
+ workspace_agents_title: "Cognitive Agents System",
+ workspace_active_prompt: "ACTIVE EXPLORATION TERMS:",
+ workspace_rag_active: "Semantic RAG active:",
+ workspace_consigne: "User instructions:",
+ workspace_secure_chat: "SECURITY: LOCAL SYMMETRIC ENCRYPTION",
+ workspace_processor: "PROCESSOR: BYOK COMPLIANT",
+
+ workspace_recall_title: "Active Spaced-Repetition Recall",
+ workspace_recall_desc: "Active recall algorithm that converts your writings into responsive flashcards dynamically.",
+ workspace_show_answer: "Show Answer",
+ workspace_correct_answer: "Correct answer:",
+ workspace_study_memory: "Active SuperMemo (SM2) engine",
+
+ workspace_snapshot_title: "Timeline Snapshots & Backups",
+ workspace_snapshot_desc: "Continuous backup checkpoints. Revert any document to an earlier state instantly.",
+ workspace_restore: "Revert",
+ workspace_state_saved: "Continuous local backup: Active"
+};
+
+const JA: LangDict = {
+ nav_features: "機能",
+ nav_agents: "AIエージェント",
+ nav_brainstorm: "ブレスト",
+ nav_pricing: "料金",
+ nav_architecture: "設計方針",
+ nav_version_v1: "クラシック V1",
+ nav_version_v2: "インタラクティブ V2",
+ nav_version_v3: "多言語 V3",
+ nav_login: "ログイン",
+ nav_start: "開始する",
+
+ hero_badge: "ローカル&クラウド融合人工知能により拡張",
+ hero_title_1: "あなたの第二の脳は、",
+ hero_title_italic: "ついに具現化する。",
+ hero_desc: "Momento(モメント)は、リアルタイムでアイデア同士が結合する最先端ナレッジベース。3D知識グラフ、間隔反復フラッシュカード、そして6体の自律型AIエージェントが、あなたの思考を強力に加速します。",
+ hero_cta_start: "無料で体験する",
+ hero_cta_features: "機能を体験する",
+
+ features_badge: "システム性能",
+ features_title: "自然言語に根ざした、",
+ features_subtitle: "あなただけのコンテキスト。",
+ features_desc: "Momentoは、ローカル暗号化アーキテクチャを通じて、あなたの機密思考システムを管理します。",
+
+ feature_1_title: "ハイブリッド意味検索",
+ feature_1_desc: "単なるキーワード一致の域を超え、意図を汲み取るベクトル意味検索。関連する記述を一瞬で発掘します。",
+ feature_2_title: "コンテキスト適合RAG対話",
+ feature_2_desc: "作成したメモと直接対話。自律エージェントが過去ノートとWeb最新情報を組み合わせ、最適な解答を提供します。",
+ feature_3_title: "インテリジェント支援執筆",
+ feature_3_desc: "メモを取るだけで、自動見出し補完、LaTeX数式の構造化、タグ付与、接続提案をバックグラウンドで実行します。",
+
+ workspace_title_editor: "エディター",
+ workspace_title_graph: "知識グラフ",
+ workspace_title_agents: "AI拡張機能",
+ workspace_title_reviews: "間隔反復学習",
+ workspace_title_history: "履歴復元",
+ workspace_notes_available: "手稿・ノート一覧",
+ workspace_new_doc: "+ メモ新規作成",
+ workspace_local_reading: "ローカルDBを読み込み中...",
+ workspace_graph_map_title: "インタラクティブ概念座標",
+ workspace_graph_map_desc: "異なるメモ間の意味的類似度に基づいて、自動でリンク線が投影されます。",
+ workspace_graph_connected: "6件の関連リンク",
+ workspace_graph_status: "意味構造:最高ランク ✓",
+
+ workspace_agents_title: "自律型エージェント(6)",
+ workspace_active_prompt: "探査キーワード :",
+ workspace_rag_active: "RAGセマンティクス機能中 :",
+ workspace_consigne: "ユーザー指示要約 :",
+ workspace_secure_chat: "暗号化:ローカル内対称鍵で安全に保管",
+ workspace_processor: "システム:モデルキー持込対応",
+
+ workspace_recall_title: "効率的な間隔反復フラッシュカード",
+ workspace_recall_desc: "蓄積されたナレッジから自動テストを生成。定着を科学的にサポート。",
+ workspace_show_answer: "解答を表示する",
+ workspace_correct_answer: "正解の解説 :",
+ workspace_study_memory: "SuperMemo (SM2) アルゴリズム有効",
+
+ workspace_snapshot_title: "履歴バージョン管理",
+ workspace_snapshot_desc: "すべての編集イベントが秒単位で自動保存されます。いつでも過去の状態へ復元可能。",
+ workspace_restore: "適用する",
+ workspace_state_saved: "手稿保護:ローカル常時保管中"
+};
+
+interface LandingPageV3Props {
+ onEnter: () => void;
+ onLogin: () => void;
+ onRegister: () => void;
+ onSwitchVersion: (v: 'v1' | 'v2' | 'v3') => void;
+}
+
+export const LandingPageV3: React.FC = ({
+ onEnter,
+ onLogin,
+ onRegister,
+ onSwitchVersion
+}) => {
+ const [lang, setLang] = useState<'fr' | 'en' | 'ja'>('fr');
+ const [showI18nKeys, setShowI18nKeys] = useState(false);
+ const [activeTab, setActiveTab] = useState<'editor' | 'graph' | 'agents' | 'reviews' | 'history'>('editor');
+ const [activeDemoIdx, setActiveDemoIdx] = useState(0);
+ const [simulateState, setSimulateState] = useState<'searching' | 'writing' | 'idle' | 'complete'>('idle');
+ const [displayText, setDisplayText] = useState('');
+ const [aiSidebarTab, setAiSidebarTab] = useState<'explore' | 'discussion' | 'relations'>('explore');
+ const [customPrompt, setCustomPrompt] = useState('');
+ const [showFlashcardAnswer, setShowFlashcardAnswer] = useState(false);
+ const [flashcardIdx, setFlashcardIdx] = useState(0);
+
+ const dicts = { fr: FR, en: EN, ja: JA };
+ const currentDict = dicts[lang];
+
+ // Helper function to translate string keys with styled brackets mode support
+ const t = (key: keyof LangDict, isPlain: boolean = false) => {
+ if (showI18nKeys) {
+ if (isPlain) return `{{${key}}}`;
+ return (
+
+ {`{${key}}`}
+
+ );
+ }
+ return currentDict[key];
+ };
+
+ const realNotes = [
+ {
+ id: 'n1',
+ title: "Grid Systems & Geometry",
+ carnet: "Architecture Research",
+ date: "26 Oct 2024",
+ tags: ["Architecture", "Systems"],
+ content: `# Grid Systems & Geometry\n\nLes trames géométriques constituent le fondement même du design cognitif. Elles structurent l'espace bâti en créant un sens d'ordre, de rythme, et d'harmonie.\n\n$$R_{ratio} = \\frac{1 + \\sqrt{5}}{2} \\approx 1.618$$\n\n## 1. Grilles Orthogonale\nLe Nombre d'Or permet d'ancrer les volumes esthétiquement.`,
+ stats: { words: 86, lines: 11, equations: 1, graphs: 3, images: 1 }
+ },
+ {
+ id: 'n2',
+ title: "Sustainable Materiality",
+ carnet: "Sustainable Design",
+ date: "24 Oct 2024",
+ tags: ["Materials", "Carbon"],
+ content: `# Sustainable Materiality\n\nL'exploration du bois lamellé-croisé CLT comme alternative écologique au béton armé.\n\n$$\\Delta Carbon = E_{béton} - E_{CLT} = 410 \\text{ kg } CO_2/m^3$$\n\n## 1. Avantages écologiques\nEmpreinte de cellulose négative stockée à vie.`,
+ stats: { words: 91, lines: 12, equations: 1, graphs: 4, images: 1 }
+ },
+ {
+ id: 'n3',
+ title: "Light & Minimalist Space",
+ carnet: "Modernism",
+ date: "22 Oct 2024",
+ tags: ["Lighting", "Minimalism"],
+ content: `# Light & Minimalist Space\n\nLa lumière naturelle devient d'elle-même un matériau structurel puissant d'espace.\n\n## 1. Transition Formelle\nLa transition continue est assurée par le verre d'optique haut de gamme dépoli.`,
+ stats: { words: 82, lines: 10, equations: 0, graphs: 3, images: 1 }
+ }
+ ];
+
+ const simulatedFlashcards = [
+ {
+ id: 'f1',
+ question: "Quel rôle joue le Nombre d'Or (1.618) dans les structures d'architecture ?",
+ answer: "Il sert à définir des proportions optimales de fenêtrage et de colonnes, créant une harmonie visuelle naturelle pour le cortex occipital."
+ },
+ {
+ id: 'f2',
+ question: "Pourquoi privilégier le bois CLT (lamellé-croisé) au béton armé ?",
+ answer: "Pour son stockage actif de CO2 à long terme, sa légèreté combinée à sa résistance sismique et coupe-feu naturelle."
+ }
+ ];
+
+ const triggerSimulation = (index: number, overrideText?: string) => {
+ setActiveDemoIdx(index);
+ setSimulateState('searching');
+ setDisplayText('');
+ setShowFlashcardAnswer(false);
+
+ if (index === 0) setAiSidebarTab('explore');
+ if (index === 1) setAiSidebarTab('relations');
+ if (index === 2) setAiSidebarTab('discussion');
+
+ const sourceText = overrideText || (index === -1 ? "" : realNotes[index].content);
+
+ const length = sourceText.length;
+ let curr = 0;
+
+ setTimeout(() => {
+ setSimulateState('writing');
+ const timer = setInterval(() => {
+ curr += 18;
+ if (curr >= length) {
+ setDisplayText(sourceText);
+ setSimulateState('complete');
+ clearInterval(timer);
+ } else {
+ setDisplayText(sourceText.substring(0, curr));
+ }
+ }, 15);
+ }, 800);
+ };
+
+ const handleCustomSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!customPrompt.trim()) return;
+
+ const summaryContent = `# Synthèse : ${customPrompt}\n\nRecherche sémantique connectée.\n\n$$\\Phi = \\lim_{n \\to \\infty} \\frac{F_{n+1}}{F_n}$$\n\n* Intégration d'éléments croisés complets.\n* Index de concordance optimale validé.`;
+ triggerSimulation(-1, summaryContent);
+ };
+
+ useEffect(() => {
+ triggerSimulation(0);
+ }, []);
+
+ return (
+
+
+ {/* 1. STICKY DUAL NAVIGATION HEADER */}
+
+
+
+ M
+
+
+ Momento
+ V3 Multilingue
+
+
+
+ {/* Elegant Language Switcher & Raw Key Toggle Pills */}
+
+ {/* Translation selector */}
+
+ setLang('fr')}
+ className={`px-2 py-1 text-[10px] font-bold rounded-md transition-all ${lang === 'fr' ? 'bg-white text-ink shadow-2xs' : 'text-stone-500 hover:text-ink'}`}
+ >
+ FR
+
+ setLang('en')}
+ className={`px-2 py-1 text-[10px] font-bold rounded-md transition-all ${lang === 'en' ? 'bg-white text-ink shadow-2xs' : 'text-stone-500 hover:text-ink'}`}
+ >
+ EN
+
+ setLang('ja')}
+ className={`px-2 py-1 text-[10px] font-bold rounded-md transition-all ${lang === 'ja' ? 'bg-white text-ink shadow-2xs' : 'text-stone-500 hover:text-ink'}`}
+ >
+ JA
+
+
+
+ {/* i18n Raw toggle */}
+
setShowI18nKeys(!showI18nKeys)}
+ className={`px-3 py-2 rounded-lg border text-[9.5px] font-mono tracking-wider font-extrabold flex items-center gap-1.5 transition-all cursor-pointer shadow-3xs
+ ${showI18nKeys
+ ? 'bg-amber-100 text-amber-900 border-amber-300'
+ : 'bg-white text-stone-600 border-stone-200 hover:bg-stone-50'}`}
+ >
+
+ {showI18nKeys ? "Clés Masquées" : "Afficher i18n"}
+
+
+
+ {/* Version switches */}
+
+ onSwitchVersion('v1')}
+ className="px-3.5 py-1.5 text-[9.5px] uppercase tracking-widest font-bold rounded-full text-stone-500 hover:text-ink transition-all"
+ >
+ V1
+
+ onSwitchVersion('v2')}
+ className="px-3.5 py-1.5 text-[9.5px] uppercase tracking-widest font-bold rounded-full text-stone-500 hover:text-ink transition-all"
+ >
+ V2
+
+ onSwitchVersion('v3')}
+ className="px-3.5 py-1.5 text-[9.5px] uppercase tracking-widest font-black rounded-full bg-ink text-paper shadow-sm"
+ >
+ V3 ✨
+
+
+
+
+ {/* 2. HERO SECTION WIT DYNAMIC INTEGRATED WORKSPACE */}
+
+ {/* Glowing beautiful background blur meshes */}
+
+
+
+
+
+ {/* Hero text column */}
+
+
+
+ {t('hero_badge')}
+
+
+
+ {t('hero_title_1')}
+ {t('hero_title_italic')}
+
+
+
+ {t('hero_desc')}
+
+
+
+
+ {t('nav_start')}
+
+
+
+ {t('hero_cta_features')}
+
+
+
+
+
+ Local isolation direct • Zéro serveur intermédiaire
+
+
+
+ {/* Integrated Dynamic Workspace Player Column - Centerpiece! */}
+
+
+
+
+ {/* Top Bar Indicators */}
+
+
+
+
+
+ MOMENTO-SANDBOX-LAYER
+
+
+
+
+ {t('workspace_notes_available')} : {activeDemoIdx === -1 ? `Custom` : `${activeDemoIdx + 1}/3`}
+
+
+
+ {/* Central Player Frame */}
+
+
+ {/* Left Active Controls Rail */}
+
+
+ setActiveTab('editor')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'editor' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_editor', true)}
+ >
+
+
+ setActiveTab('graph')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'graph' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_graph', true)}
+ >
+
+
+ setActiveTab('agents')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'agents' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_agents', true)}
+ >
+
+
+ setActiveTab('reviews')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'reviews' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_reviews', true)}
+ >
+
+
+ setActiveTab('history')}
+ className={`p-2 rounded-lg cursor-pointer transition-colors ${activeTab === 'history' ? 'text-[#A47148] bg-white border border-stone-200 shadow-3xs' : 'text-stone-400 hover:text-ink'}`}
+ title={t('workspace_title_history', true)}
+ >
+
+
+
+
+
+
+
+
+ {/* Main Tab Screen Area */}
+
+
+
+ {/* 1. EDITOR SCREEN */}
+ {activeTab === 'editor' && (
+
+ {/* Notes selector drawer */}
+
+
+ {t('workspace_notes_available')}
+
+
+ {realNotes.map((note, idx) => (
+ triggerSimulation(idx)}
+ className={`w-full text-left p-2.5 rounded-lg border text-xs transition-colors cursor-pointer block
+ ${activeDemoIdx === idx
+ ? 'bg-[#EAE8DF] border-[#A47148]/20'
+ : 'border-transparent bg-stone-50 hover:bg-[#EAE8DF]/40'
+ }`}
+ >
+ {note.carnet}
+ {note.title}
+
+ ))}
+ triggerSimulation(0)}
+ className="w-full text-center border border-dashed border-accent/25 py-2 rounded-lg text-[9px] font-mono text-accent block hover:bg-accent/5 cursor-pointer"
+ >
+ {t('workspace_new_doc')}
+
+
+
+
+ {/* Note workspace page content */}
+
+
+
+ {activeDemoIdx === -1 ? "SYNTHÈSE" : realNotes[activeDemoIdx].carnet}
+ {activeDemoIdx === -1 ? "ACTIF" : realNotes[activeDemoIdx].date}
+
+
+
+ {activeDemoIdx === -1 ? "Synthèse de recherche" : realNotes[activeDemoIdx].title}
+
+
+
+ {(activeDemoIdx === -1 ? ["Custom"] : realNotes[activeDemoIdx].tags).map((tag, i) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+ {simulateState === 'searching' && (
+
+
+
+
+
+
+
+
+ {t('workspace_local_reading')}
+
+
+ )}
+
+ {(simulateState === 'writing' || simulateState === 'complete') && (
+
+ {displayText.split('\n').map((line, idx) => {
+ if (line.startsWith('# ')) return null;
+ if (line.startsWith('## ')) {
+ return
{line.replace('## ', '')} ;
+ }
+ if (line.startsWith('$$')) {
+ return (
+
+ {line.replace(/\$\$/g, '')}
+
+ );
+ }
+ if (line.startsWith('* ')) {
+ return (
+
+ •
+ {line.replace('* ', '')}
+
+ );
+ }
+ return
{line}
;
+ })}
+
+ )}
+
+
+
+
+
+ )}
+
+ {/* 2. SPATIAL GRAPH SCREEN */}
+ {activeTab === 'graph' && (
+
+
+
+ {t('workspace_graph_map_title')}
+
+
{t('workspace_graph_map_desc')}
+
+
+ {/* Blueprint grid layout */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Center node */}
+
+ MOMENTO
+
+
+ {/* Surrounding linked concepts */}
+ {[
+ { name: "Nombre d'Or", x: -95, y: -65, tag: "Architecture", theme: "text-amber-800 bg-amber-50/95" },
+ { name: "Diffraction lumineuse", x: 105, y: -72, tag: "Optique", theme: "text-violet-850 bg-violet-50/95" },
+ { name: "Bois CLT Lamellé", x: -115, y: 70, tag: "Matériaux", theme: "text-emerald-800 bg-emerald-50/95" },
+ { name: "Transition Formelle", x: 110, y: 60, tag: "Minimalisme", theme: "text-blue-800 bg-blue-50/95" },
+ { name: "Grid System", x: 0, y: -100, tag: "IA Métrique", theme: "text-stone-800 bg-stone-50/95" },
+ { name: "Éco-Conception", x: 0, y: 100, tag: "Carbone", theme: "text-rose-800 bg-rose-50/95" }
+ ].map((node, idx) => (
+
+
+ {node.name}
+
+ {node.tag}
+
+
+
+ ))}
+
+
+
+ {t('workspace_graph_connected')}
+ {t('workspace_graph_status')}
+
+
+ )}
+
+ {/* 3. COGNITIVE AGENTS SCREEN */}
+ {activeTab === 'agents' && (
+
+ {/* System models list */}
+
+
+ {t('workspace_agents_title')}
+
+
+ {[
+ { id: "scr", name: "Web Scraper", role: "Parse et simplifie les URLs", tag: "SCR", color: "bg-amber-100 text-amber-900 border-amber-200" },
+ { id: "res", name: "Deep Researcher", role: "Recherche exhaustive internet", tag: "RES", color: "bg-teal-100 text-teal-900 border-teal-200" },
+ { id: "sli", name: "Slide Builder", role: "Formate en deck PowerPoint", tag: "PPT", color: "bg-blue-100 text-blue-900 border-blue-200" },
+ { id: "mon", name: "Knowledge Monitor", role: "Trouve les redondances", tag: "LNK", color: "bg-purple-100 text-purple-900 border-purple-200" }
+ ].map((a) => (
+
+
+ {a.name}
+ {a.tag}
+
+
{a.role}
+
+ ))}
+
+
+
+ {/* Interactive prompt chat system */}
+
+
+
+ {([
+ { key: 'explore', label: 'Explore (RAG)' },
+ { key: 'discussion', label: 'Discussion' },
+ { key: 'relations', label: 'Rapports' }
+ ] as const).map((tab) => (
+ setAiSidebarTab(tab.key)}
+ className={`flex-1 py-1.5 text-[8.5px] uppercase tracking-wider font-extrabold text-[#A47148] transition-all cursor-pointer text-center
+ ${aiSidebarTab === tab.key ? 'bg-white text-ink font-black border-r border-[#1C1C1C]/10' : 'hover:bg-white/40'}`}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+
+ {aiSidebarTab === 'explore' && (
+
+
+ {t('workspace_active_prompt')}
+
+
+ {t('workspace_rag_active')}
+ "{activeDemoIdx === -1 ? customPrompt || "Custom Search" : realNotes[activeDemoIdx].title}"
+
+
+
+ Synthesize custom topic:
+
+
+ setCustomPrompt(e.target.value)}
+ placeholder="E.g., Bauhaus architectural color scale"
+ className="flex-1 bg-white border border-stone-200 rounded-lg px-2.5 py-1.5 text-[10px] outline-none font-mono focus:border-accent"
+ />
+
+ Go
+
+
+
+
+ )}
+
+ {aiSidebarTab === 'discussion' && (
+
+
+ {t('workspace_consigne')}
+ Structure minimaliste de la diffraction active.
+
+
+ Gemini local model
+ Transparence induite par le verre feuilleté déclinable, limitant les structures.
+
+
+ )}
+
+ {aiSidebarTab === 'relations' && (
+
+ Semantic index matches:
+
+ Sustainable CLT Space Grid
+ 89% match ✓
+
+
+ )}
+
+
+
+
+
+ {t('workspace_secure_chat')}
+ {t('workspace_processor')}
+
+
+
+ )}
+
+ {/* 4. SPACED REPETITION STUDY CARD SCREEN */}
+ {activeTab === 'reviews' && (
+
+
+
+ {t('workspace_recall_title')}
+
+
{t('workspace_recall_desc')}
+
+
+ {/* Interactive flipped card structure */}
+
+
+ Concept review {flashcardIdx + 1}/2
+
+ {simulatedFlashcards[flashcardIdx].question}
+
+
+
+ {showFlashcardAnswer ? (
+
+ {t('workspace_correct_answer')}
+ {simulatedFlashcards[flashcardIdx].answer}
+
+ ) : (
+
setShowFlashcardAnswer(true)}
+ className="px-4 py-2 bg-accent hover:bg-accent/90 text-white transition-all text-[9.5px] font-bold uppercase tracking-wider rounded-xl cursor-pointer"
+ >
+ {t('workspace_show_answer')}
+
+ )}
+
+ {showFlashcardAnswer && (
+
+ {[
+ { label: "Revoir", color: "bg-red-500/10 text-red-600 hover:bg-red-500/20" },
+ { label: "Ok", color: "bg-amber-500/10 text-amber-600 hover:bg-amber-500/20" },
+ { label: "Parfait", color: "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" }
+ ].map((btn, idx) => (
+ {
+ setShowFlashcardAnswer(false);
+ setFlashcardIdx((prev) => (prev + 1) % 2);
+ }}
+ className={`px-3 py-1 text-[8.5px] font-mono font-black uppercase transition-colors cursor-pointer rounded-md ${btn.color}`}
+ >
+ {btn.label}
+
+ ))}
+
+ )}
+
+
+
+ Spaced recall study active
+ {t('workspace_study_memory')}
+
+
+ )}
+
+ {/* 5. BACKUPS & TEMPORAL HISTORY SNAPSHOTS SCREEN */}
+ {activeTab === 'history' && (
+
+
+
+ {t('workspace_snapshot_title')}
+
+
{t('workspace_snapshot_desc')}
+
+
+ {/* List timelines */}
+
+ {[
+ { time: "Aujourd'hui, 09:56", event: "Modification de l'Equation", size: "182 mots", active: true },
+ { time: "Aujourd'hui, 09:21", event: "Import de l'Agent Scraper", size: "124 mots" },
+ { time: "Hier, 14:20", event: "Création initiale de la trame", size: "86 mots" }
+ ].map((snap, i) => (
+
+
+
+
+ {snap.event}
+ {snap.time} · {snap.size}
+
+
+
+
{
+ if (i === 1) triggerSimulation(1);
+ if (i === 2) triggerSimulation(0);
+ }}
+ >
+ {snap.active ? "Actif" : t('workspace_restore', true)}
+
+
+ ))}
+
+
+
+ {t('workspace_state_saved')}
+ Secteurs locaux protégés pour de bon
+
+
+ )}
+
+
+
+
+
+
+ {/* Console status footer */}
+
+
+
+
+ SECURE KERNEL
+
+
|
+
Totalement chiffré
+
|
+
STOCKAGE PRIVÉ ACTIF
+
+
+
+ {t('nav_start', true)}
+
+
+
+
+
+
+
+
+
+
+ {/* 3. CAPABILITIES GRID */}
+
+
+
+
+
+
+ {t('features_badge')}
+
+
+ {t('features_title')}
+ {t('features_subtitle')}
+
+
+
+ {t('features_desc')}
+
+
+
+
+ {[
+ {
+ icon:
,
+ title: t('feature_1_title', true),
+ desc: t('feature_1_desc', true)
+ },
+ {
+ icon:
,
+ title: t('feature_2_title', true),
+ desc: t('feature_2_desc', true)
+ },
+ {
+ icon:
,
+ title: t('feature_3_title', true),
+ desc: t('feature_3_desc', true)
+ }
+ ].map((feature, i) => (
+
+
+ {feature.icon}
+
+
{feature.title}
+
{feature.desc}
+
+ ))}
+
+
+
+
+
+ {/* Footer copyright */}
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/LivingBlock.tsx b/architectural-grid1/src/components/LivingBlock.tsx
new file mode 100644
index 0000000..51b0c07
--- /dev/null
+++ b/architectural-grid1/src/components/LivingBlock.tsx
@@ -0,0 +1,194 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Zap, HelpCircle, ArrowRight, RefreshCw, Unlink, AlertCircle } from 'lucide-react';
+import { Note } from '../types';
+
+interface LivingBlockProps {
+ sourceNoteId: string;
+ blockIndex: number;
+ allNotes: Note[];
+ hostNote: Note;
+ onUpdateNote: (updatedNote: Note) => void;
+ onOpenNote: (noteId: string) => void;
+ wsConnected: boolean;
+ broadcastLivingBlockUpdate?: (sourceNoteId: string, blockIndex: number, newText: string) => void;
+}
+
+export const LivingBlock: React.FC = ({
+ sourceNoteId,
+ blockIndex,
+ allNotes,
+ hostNote,
+ onUpdateNote,
+ onOpenNote,
+ wsConnected,
+ broadcastLivingBlockUpdate
+}) => {
+ const [pulse, setPulse] = useState(false);
+ const pulseRef = useRef(null);
+
+ // Locate source note and actual paragraph text
+ const sourceNote = allNotes.find(n => n.id === sourceNoteId);
+ const paragraphs = sourceNote?.content.split('\n') || [];
+ const rawText = paragraphs[blockIndex];
+
+ // Store a local cache in standard state to support the "Source Deleted Snapshot" or local typing lag minimization
+ const [localText, setLocalText] = useState(rawText || "Contenu de l'extrait sémantique.");
+ const [isDeleted, setIsDeleted] = useState(!sourceNote || rawText === undefined);
+
+ // Sync state if source note or text updates from outside
+ useEffect(() => {
+ const isSourceMissing = !sourceNote || rawText === undefined;
+ setIsDeleted(isSourceMissing);
+
+ if (!isSourceMissing && rawText !== localText) {
+ setLocalText(rawText);
+ }
+ }, [rawText, sourceNote]);
+
+ // Handle pulse notification when custom update event is received from socket
+ useEffect(() => {
+ const handlePulseEvent = (e: any) => {
+ if (e.detail && e.detail.sourceNoteId === sourceNoteId && e.detail.blockIndex === blockIndex) {
+ setPulse(true);
+ if (pulseRef.current) clearTimeout(pulseRef.current);
+ pulseRef.current = setTimeout(() => {
+ setPulse(false);
+ }, 1000);
+ }
+ };
+
+ window.addEventListener('living-block-pulse', handlePulseEvent);
+ return () => {
+ window.removeEventListener('living-block-pulse', handlePulseEvent);
+ if (pulseRef.current) clearTimeout(pulseRef.current);
+ };
+ }, [sourceNoteId, blockIndex]);
+
+ // Edit body text and stream to central note and websockets
+ const handleBodyTextChange = (e: React.ChangeEvent) => {
+ const newText = e.target.value;
+ setLocalText(newText);
+
+ if (sourceNote && !isDeleted) {
+ const updatedParagraphs = [...paragraphs];
+ updatedParagraphs[blockIndex] = newText;
+ const updatedSourceNote = {
+ ...sourceNote,
+ content: updatedParagraphs.join('\n')
+ };
+ // 1. Update state
+ onUpdateNote(updatedSourceNote);
+
+ // 2. Broadcast via WS connection to other terminals
+ if (broadcastLivingBlockUpdate) {
+ broadcastLivingBlockUpdate(sourceNoteId, blockIndex, newText);
+ }
+ }
+ };
+
+ // Convert Living Block to normal local text paragraph
+ const handleConvertLocalText = () => {
+ const hostParagraphs = hostNote.content.split('\n');
+ // Find matching shortcode index
+ const codeToSearch = `[[living-block:${sourceNoteId}:${blockIndex}]]`;
+ const targetIdx = hostParagraphs.findIndex(line => line.trim() === codeToSearch);
+
+ if (targetIdx !== -1) {
+ hostParagraphs[targetIdx] = localText; // Replace code with snapped plain text
+ const updatedHostNote = {
+ ...hostNote,
+ content: hostParagraphs.join('\n')
+ };
+ onUpdateNote(updatedHostNote);
+ }
+ };
+
+ // Styling helpers
+ const borderStyle = isDeleted
+ ? 'border-rose-500/60 dark:border-red-900/60 bg-rose-50/20 dark:bg-rose-950/5'
+ : !wsConnected
+ ? 'border-amber-500 dark:border-amber-700 bg-amber-50/10 dark:bg-amber-950/5'
+ : pulse
+ ? 'border-blue-500 shadow-md shadow-blue-500/15 bg-blue-50/20 dark:bg-blue-950/10'
+ : 'border-blue-500/80 bg-blue-50/5 dark:bg-blue-950/5';
+
+ return (
+
+
+ {/* Header (20px) */}
+
+
+ {isDeleted ? (
+
+ ) : (
+
+ )}
+
+ {isDeleted ? "Source déconnectée" : sourceNote?.title || "Note connectée"}
+
+
+ {/* Live syncing status badge */}
+ {isDeleted ? (
+
+ DÉCONNECTÉ
+
+ ) : wsConnected ? (
+
+ LIVE
+
+ ) : (
+
+ HORS-LIGNE
+
+ )}
+
+
+
+ {isDeleted ? (
+
+
+ Décharger le lien
+
+ ) : (
+ <>
+ {!wsConnected && (
+
+ Synchro suspendue
+
+ )}
+
onOpenNote(sourceNoteId)}
+ className="opacity-0 group-hover/block:opacity-100 flex items-center gap-1 text-[9.5px] font-extrabold text-blue-600 dark:text-blue-400 hover:underline transition-all"
+ >
+ Ouvrir
+
+ >
+ )}
+
+
+
+ {/* Body content editable block */}
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/ModernBlockNoteEditor.tsx b/architectural-grid1/src/components/ModernBlockNoteEditor.tsx
new file mode 100644
index 0000000..81876e2
--- /dev/null
+++ b/architectural-grid1/src/components/ModernBlockNoteEditor.tsx
@@ -0,0 +1,2016 @@
+import React, { useState, useEffect, useRef } from 'react';
+import {
+ Plus,
+ Trash2,
+ ChevronUp,
+ ChevronDown,
+ Maximize2,
+ Minimize2,
+ CheckSquare,
+ List,
+ Heading1,
+ Heading2,
+ Heading3,
+ FileText,
+ Code,
+ Sigma,
+ Grid3X3,
+ GitCommit,
+ BarChart2,
+ Calendar,
+ Sliders,
+ Sparkles,
+ RefreshCw,
+ PlusCircle,
+ FolderMinus,
+ ArrowRight,
+ MoreHorizontal
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Note } from '../types';
+
+export interface BlockItem {
+ id: string;
+ type: 'paragraph' | 'h1' | 'h2' | 'h3' | 'todo' | 'bullet' | 'math' | 'chart' | 'flowchart' | 'gantt' | 'code' | 'database';
+ content: string;
+ metadata?: {
+ completed?: boolean;
+ chartType?: 'bar' | 'line' | 'pie';
+ chartData?: Array<{ name: string; value: number }>;
+ flowNodes?: Array<{ id: string; label: string; x: number; y: number }>;
+ flowEdges?: Array<{ id: string; from: string; to: string; label?: string }>;
+ ganttTasks?: Array<{ id: string; name: string; startDay: number; duration: number; progress: number }>;
+ dbId?: string;
+ dbView?: 'table' | 'card';
+ dbAuthors?: Array<{ id: string; name: string }>;
+ dbBooks?: Array<{ id: string; title: string; author: string; cover: string; tag: string }>;
+ };
+}
+
+interface ModernBlockNoteEditorProps {
+ note: Note;
+ onUpdateNote: (updated: Note) => void;
+ allNotes?: Note[];
+}
+
+export const ModernBlockNoteEditor: React.FC = ({
+ note,
+ onUpdateNote,
+ allNotes = []
+}) => {
+ const [blocks, setBlocks] = useState([]);
+ const [focusedBlockId, setFocusedBlockId] = useState(null);
+ const [zoomBlockId, setZoomBlockId] = useState(null);
+ const [showInsertMenuAt, setShowInsertMenuAt] = useState<{ index: number; top: number } | null>(null);
+ const [showBlockMenuId, setShowBlockMenuId] = useState(null);
+
+ // Parse markdown content into structured BlockItem blocks
+ useEffect(() => {
+ if (!note || !note.content) {
+ // Default placeholder block if empty
+ setBlocks([{ id: 'b-init-1', type: 'paragraph', content: '' }]);
+ return;
+ }
+
+ try {
+ const parsedBlocks: BlockItem[] = [];
+ const lines = note.content.split('\n');
+ let i = 0;
+
+ while (i < lines.length) {
+ const line = lines[i];
+ const trimmed = line.trim();
+
+ // Check for special blocks embedded as backtick fences
+ if (trimmed.startsWith('```gantt')) {
+ let contentStr = '';
+ i++;
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
+ contentStr += lines[i] + '\n';
+ i++;
+ }
+ let tasks = [];
+ try {
+ tasks = JSON.parse(contentStr.trim() || '[]');
+ } catch(e) {
+ tasks = [
+ { id: 't1', name: 'Concept Mapping', startDay: 1, duration: 4, progress: 80 },
+ { id: 't2', name: 'Prototyping Frameworks', startDay: 3, duration: 5, progress: 30 }
+ ];
+ }
+ parsedBlocks.push({
+ id: `b-gantt-${Date.now()}-${parsedBlocks.length}`,
+ type: 'gantt',
+ content: '',
+ metadata: { ganttTasks: tasks }
+ });
+ i++;
+ continue;
+ }
+
+ if (trimmed.startsWith('```flowchart')) {
+ let contentStr = '';
+ i++;
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
+ contentStr += lines[i] + '\n';
+ i++;
+ }
+ let flowNodes = [];
+ let flowEdges = [];
+ try {
+ const data = JSON.parse(contentStr.trim() || '{}');
+ flowNodes = data.nodes || [];
+ flowEdges = data.edges || [];
+ } catch(e) {
+ flowNodes = [
+ { id: 'n1', label: 'Conceptualize', x: 80, y: 70 },
+ { id: 'n2', label: 'Refine Proportions', x: 260, y: 70 },
+ { id: 'n3', label: 'Draft Schemes', x: 440, y: 70 }
+ ];
+ flowEdges = [
+ { id: 'e1', from: 'n1', to: 'n2', label: 'Iterate' },
+ { id: 'e2', from: 'n2', to: 'n3', label: 'Approve' }
+ ];
+ }
+ parsedBlocks.push({
+ id: `b-flow-${Date.now()}-${parsedBlocks.length}`,
+ type: 'flowchart',
+ content: '',
+ metadata: { flowNodes, flowEdges }
+ });
+ i++;
+ continue;
+ }
+
+ if (trimmed.startsWith('```chart')) {
+ let contentStr = '';
+ i++;
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
+ contentStr += lines[i] + '\n';
+ i++;
+ }
+ let chartType: 'bar' | 'line' | 'pie' = 'bar';
+ let chartData = [];
+ try {
+ const data = JSON.parse(contentStr.trim() || '{}');
+ chartType = data.type || 'bar';
+ chartData = data.data || [];
+ } catch(e) {
+ chartData = [
+ { name: 'Light', value: 35 },
+ { name: 'Mass', value: 45 },
+ { name: 'Circulation', value: 20 }
+ ];
+ }
+ parsedBlocks.push({
+ id: `b-chart-${Date.now()}-${parsedBlocks.length}`,
+ type: 'chart',
+ content: '',
+ metadata: { chartType, chartData }
+ });
+ i++;
+ continue;
+ }
+
+ if (trimmed.startsWith('```code')) {
+ let blockContent = '';
+ i++;
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
+ blockContent += lines[i] + '\n';
+ i++;
+ }
+ parsedBlocks.push({
+ id: `b-code-${Date.now()}-${parsedBlocks.length}`,
+ type: 'code',
+ content: blockContent.trim()
+ });
+ i++;
+ continue;
+ }
+
+ if (trimmed.startsWith('[DATABASE')) {
+ const idMatch = trimmed.match(/id="([^"]+)"/);
+ const viewMatch = trimmed.match(/view="([^"]+)"/);
+ const dataMatch = trimmed.match(/data="([^"]+)"/);
+
+ const dbId = idMatch ? idMatch[1] : 'authors-works';
+ const dbView = (viewMatch ? viewMatch[1] : 'table') as 'table' | 'card';
+
+ let dbAuthors = [
+ { id: 'a1', name: 'Jules Verne' },
+ { id: 'a2', name: 'Liu Cixin' }
+ ];
+
+ let dbBooks = [
+ { id: 'bk1', title: 'Twenty Thousand Leagues Under The Sea', author: 'Jules Verne', cover: 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400', tag: 'Aventure' },
+ { id: 'bk2', title: 'The Three-Body Problem', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400', tag: 'Hard SF' },
+ { id: 'bk3', title: 'The Wandering Earth', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?auto=format&fit=crop&q=80&w=400', tag: 'SF Spatial' }
+ ];
+
+ if (dataMatch) {
+ try {
+ const decoded = JSON.parse(decodeURIComponent(dataMatch[1]));
+ if (decoded.authors) dbAuthors = decoded.authors;
+ if (decoded.books) dbBooks = decoded.books;
+ } catch (e) {
+ console.error("Failed to decode database data:", e);
+ }
+ }
+
+ parsedBlocks.push({
+ id: `b-db-${Date.now()}-${parsedBlocks.length}`,
+ type: 'database',
+ content: '',
+ metadata: {
+ dbId,
+ dbView,
+ dbAuthors,
+ dbBooks
+ }
+ });
+ i++;
+ continue;
+ }
+
+ // Standard Markdown lines
+ if (trimmed.startsWith('# ')) {
+ parsedBlocks.push({ id: `b-h1-${Date.now()}-${parsedBlocks.length}`, type: 'h1', content: line.substring(2) });
+ } else if (trimmed.startsWith('## ')) {
+ parsedBlocks.push({ id: `b-h2-${Date.now()}-${parsedBlocks.length}`, type: 'h2', content: line.substring(3) });
+ } else if (trimmed.startsWith('### ')) {
+ parsedBlocks.push({ id: `b-h3-${Date.now()}-${parsedBlocks.length}`, type: 'h3', content: line.substring(4) });
+ } else if (trimmed.startsWith('- [ ] ') || trimmed.startsWith('- [x] ') || trimmed.startsWith('- [X] ')) {
+ const completed = trimmed.startsWith('- [x] ') || trimmed.startsWith('- [X] ');
+ parsedBlocks.push({
+ id: `b-todo-${Date.now()}-${parsedBlocks.length}`,
+ type: 'todo',
+ content: line.substring(line.indexOf(']') + 1).trim(),
+ metadata: { completed }
+ });
+ } else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
+ parsedBlocks.push({ id: `b-bullet-${Date.now()}-${parsedBlocks.length}`, type: 'bullet', content: line.substring(2) });
+ } else if (trimmed.startsWith('$$') && trimmed.endsWith('$$') && trimmed.length > 4) {
+ parsedBlocks.push({ id: `b-math-${Date.now()}-${parsedBlocks.length}`, type: 'math', content: trimmed.replace(/\$\$/g, '') });
+ } else if (trimmed.startsWith('$$')) {
+ // Multiline Math block
+ let mathContent = '';
+ i++;
+ while (i < lines.length && !lines[i].trim().startsWith('$$')) {
+ mathContent += lines[i] + '\n';
+ i++;
+ }
+ parsedBlocks.push({ id: `b-math-${Math.random()}`, type: 'math', content: mathContent.trim() });
+ } else {
+ // Paragraph (only if line is not blank, or if it is a single blank line and preceding is not blank)
+ if (line.trim() !== '' || (parsedBlocks.length > 0 && parsedBlocks[parsedBlocks.length - 1].content !== '')) {
+ parsedBlocks.push({ id: `b-p-${Date.now()}-${parsedBlocks.length}`, type: 'paragraph', content: line });
+ }
+ }
+ i++;
+ }
+
+ if (parsedBlocks.length === 0) {
+ parsedBlocks.push({ id: 'b-init-2', type: 'paragraph', content: '' });
+ }
+ setBlocks(parsedBlocks);
+ } catch (e) {
+ console.error('Failed parsing blocks:', e);
+ // Fallback
+ setBlocks([{ id: 'b-fallback', type: 'paragraph', content: note.content }]);
+ }
+ }, [note.id]);
+
+ // Serialize blocks back to Markdown when saved
+ const serializeAndSave = (updatedBlocks: BlockItem[]) => {
+ let md = '';
+ updatedBlocks.forEach((b) => {
+ switch (b.type) {
+ case 'h1':
+ md += `# ${b.content}\n`;
+ break;
+ case 'h2':
+ md += `## ${b.content}\n`;
+ break;
+ case 'h3':
+ md += `### ${b.content}\n`;
+ break;
+ case 'todo':
+ md += `- [${b.metadata?.completed ? 'x' : ' '}] ${b.content}\n`;
+ break;
+ case 'bullet':
+ md += `- ${b.content}\n`;
+ break;
+ case 'math':
+ md += `$$\n${b.content}\n$$\n`;
+ break;
+ case 'code':
+ md += `\`\`\`code\n${b.content}\n\`\`\`\n`;
+ break;
+ case 'gantt':
+ md += `\`\`\`gantt\n${JSON.stringify(b.metadata?.ganttTasks || [], null, 2)}\n\`\`\`\n`;
+ break;
+ case 'flowchart':
+ md += `\`\`\`flowchart\n${JSON.stringify({ nodes: b.metadata?.flowNodes || [], edges: b.metadata?.flowEdges || [] }, null, 2)}\n\`\`\`\n`;
+ break;
+ case 'chart':
+ md += `\`\`\`chart\n${JSON.stringify({ type: b.metadata?.chartType || 'bar', data: b.metadata?.chartData || [] }, null, 2)}\n\`\`\`\n`;
+ break;
+ case 'database':
+ const dbState = {
+ authors: b.metadata?.dbAuthors || [],
+ books: b.metadata?.dbBooks || []
+ };
+ const encoded = encodeURIComponent(JSON.stringify(dbState));
+ md += `[DATABASE id="${b.metadata?.dbId || 'authors-works'}" view="${b.metadata?.dbView || 'table'}" data="${encoded}"]\n`;
+ break;
+ case 'paragraph':
+ default:
+ md += `${b.content}\n`;
+ break;
+ }
+ });
+
+ onUpdateNote({
+ ...note,
+ content: md.trim()
+ });
+ };
+
+ const handleUpdateBlockContent = (id: string, newContent: string) => {
+ const updated = blocks.map(b => b.id === id ? { ...b, content: newContent } : b);
+ setBlocks(updated);
+ serializeAndSave(updated);
+ };
+
+ const handleUpdateBlockMetadata = (id: string, metadata: any) => {
+ const updated = blocks.map(b => b.id === id ? { ...b, metadata: { ...b.metadata, ...metadata } } : b);
+ setBlocks(updated);
+ serializeAndSave(updated);
+ };
+
+ const handleBlockKeyDown = (e: React.KeyboardEvent, index: number, block: BlockItem) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ if (block.type === 'math' || block.type === 'code' || block.type === 'flowchart' || block.type === 'gantt' || block.type === 'chart') {
+ // Multi-line values allowed, don't trigger enter splitter
+ return;
+ }
+ e.preventDefault();
+ // Split paragraph
+ const nextBlocks = [...blocks];
+ const newBlock: BlockItem = {
+ id: `b-p-inserted-${Date.now()}`,
+ type: 'paragraph',
+ content: ''
+ };
+ nextBlocks.splice(index + 1, 0, newBlock);
+ setBlocks(nextBlocks);
+ serializeAndSave(nextBlocks);
+
+ // Auto focus new block
+ setTimeout(() => {
+ const nextEl = document.getElementById(`textarea-${newBlock.id}`);
+ if (nextEl) nextEl.focus();
+ }, 50);
+ } else if (e.key === 'Backspace' && block.content === '' && blocks.length > 1) {
+ e.preventDefault();
+ const nextBlocks = blocks.filter(b => b.id !== block.id);
+ setBlocks(nextBlocks);
+ serializeAndSave(nextBlocks);
+
+ // Focus previous element
+ const prevIdx = Math.max(0, index - 1);
+ const prevBlock = nextBlocks[prevIdx];
+ if (prevBlock) {
+ setTimeout(() => {
+ const prevEl = document.getElementById(`textarea-${prevBlock.id}`);
+ if (prevEl) prevEl.focus();
+ }, 50);
+ }
+ }
+ };
+
+ const handleInsertBlockAt = (index: number, type: BlockItem['type']) => {
+ const nextBlocks = [...blocks];
+ let newBlock: BlockItem = {
+ id: `b-new-${Date.now()}`,
+ type,
+ content: ''
+ };
+
+ if (type === 'math') {
+ newBlock.content = '\\Phi = \\frac{1 + \\sqrt{5}}{2}';
+ } else if (type === 'gantt') {
+ newBlock.metadata = {
+ ganttTasks: [
+ { id: 't1', name: 'Plan architectural', startDay: 1, duration: 3, progress: 100 },
+ { id: 't2', name: 'Étude thermique', startDay: 3, duration: 4, progress: 20 }
+ ]
+ };
+ } else if (type === 'flowchart') {
+ newBlock.metadata = {
+ flowNodes: [
+ { id: 'n1', label: 'Inspiration', x: 80, y: 70 },
+ { id: 'n2', label: 'Proportions', x: 260, y: 70 }
+ ],
+ flowEdges: [
+ { id: 'e1', from: 'n1', to: 'n2', label: 'Draft' }
+ ]
+ };
+ } else if (type === 'chart') {
+ newBlock.metadata = {
+ chartType: 'bar',
+ chartData: [
+ { name: 'Bois', value: 45 },
+ { name: 'Verre', value: 30 },
+ { name: 'Béton', value: 25 }
+ ]
+ };
+ } else if (type === 'todo') {
+ newBlock.metadata = { completed: false };
+ } else if (type === 'database') {
+ newBlock.metadata = {
+ dbId: 'authors-works',
+ dbView: 'table',
+ dbAuthors: [
+ { id: 'a1', name: 'Jules Verne' },
+ { id: 'a2', name: 'Liu Cixin' }
+ ],
+ dbBooks: [
+ { id: 'bk1', title: 'Twenty Thousand Leagues Under The Sea', author: 'Jules Verne', cover: 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400', tag: 'Aventure' },
+ { id: 'bk2', title: 'The Three-Body Problem', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400', tag: 'Hard SF' },
+ { id: 'bk3', title: 'The Wandering Earth', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?auto=format&fit=crop&q=80&w=400', tag: 'SF Spatial' }
+ ]
+ };
+ }
+
+ nextBlocks.splice(index + 1, 0, newBlock);
+ setBlocks(nextBlocks);
+ serializeAndSave(nextBlocks);
+ setShowInsertMenuAt(null);
+
+ setTimeout(() => {
+ const el = document.getElementById(`textarea-${newBlock.id}`);
+ if (el) el.focus();
+ }, 100);
+ };
+
+ const handleMoveBlock = (index: number, direction: 'up' | 'down') => {
+ const nextIndex = direction === 'up' ? index - 1 : index + 1;
+ if (nextIndex < 0 || nextIndex >= blocks.length) return;
+
+ const updated = [...blocks];
+ const [moved] = updated.splice(index, 1);
+ updated.splice(nextIndex, 0, moved);
+ setBlocks(updated);
+ serializeAndSave(updated);
+ };
+
+ const handleDeleteBlock = (id: string) => {
+ if (blocks.length <= 1) return;
+ const updated = blocks.filter(b => b.id !== id);
+ setBlocks(updated);
+ serializeAndSave(updated);
+ };
+
+ const handleConvertBlockType = (id: string, type: BlockItem['type']) => {
+ const updated = blocks.map(b => {
+ if (b.id === id) {
+ const updatedBlock = { ...b, type };
+ if (type === 'todo' && !updatedBlock.metadata) {
+ updatedBlock.metadata = { completed: false };
+ }
+ if (type === 'chart' && !updatedBlock.metadata) {
+ updatedBlock.metadata = {
+ chartType: 'bar',
+ chartData: [
+ { name: 'Catégorie A', value: 40 },
+ { name: 'Catégorie B', value: 60 }
+ ]
+ };
+ }
+ if (type === 'gantt' && !updatedBlock.metadata) {
+ updatedBlock.metadata = {
+ ganttTasks: [
+ { id: 't1', name: 'Tâche Initiale', startDay: 1, duration: 5, progress: 10 }
+ ]
+ };
+ }
+ if (type === 'flowchart' && !updatedBlock.metadata) {
+ updatedBlock.metadata = {
+ flowNodes: [{ id: 'n1', label: 'Départ', x: 100, y: 70 }],
+ flowEdges: []
+ };
+ }
+ if (type === 'database' && !updatedBlock.metadata) {
+ updatedBlock.metadata = {
+ dbId: 'authors-works',
+ dbView: 'table',
+ dbAuthors: [
+ { id: 'a1', name: 'Jules Verne' },
+ { id: 'a2', name: 'Liu Cixin' }
+ ],
+ dbBooks: [
+ { id: 'bk1', title: 'Twenty Thousand Leagues Under The Sea', author: 'Jules Verne', cover: 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400', tag: 'Aventure' },
+ { id: 'bk2', title: 'The Three-Body Problem', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400', tag: 'Hard SF' },
+ { id: 'bk3', title: 'The Wandering Earth', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?auto=format&fit=crop&q=80&w=400', tag: 'SF Spatial' }
+ ]
+ };
+ }
+ return updatedBlock;
+ }
+ return b;
+ });
+ setBlocks(updated);
+ serializeAndSave(updated);
+ setShowBlockMenuId(null);
+ };
+
+ return (
+
+
+ {blocks.map((b, i) => {
+ const isFocused = focusedBlockId === b.id;
+ const isMenuOpen = showBlockMenuId === b.id;
+
+ return (
+ setFocusedBlockId(b.id)}
+ onMouseLeave={() => {
+ if (focusedBlockId === b.id) setFocusedBlockId(null);
+ }}
+ >
+ {/* Left sidebar floating settings menu */}
+
+ handleMoveBlock(i, 'up')}
+ disabled={i === 0}
+ className="p-1 text-concrete hover:text-ink hover:bg-black/5 dark:hover:bg-white/5 disabled:opacity-30 rounded-full transition-colors cursor-pointer"
+ title="Déplacer vers le haut"
+ >
+
+
+ handleMoveBlock(i, 'down')}
+ disabled={i === blocks.length - 1}
+ className="p-1 text-concrete hover:text-ink hover:bg-black/5 dark:hover:bg-white/5 disabled:opacity-30 rounded-full transition-colors cursor-pointer"
+ title="Déplacer vers le bas"
+ >
+
+
+ setShowBlockMenuId(isMenuOpen ? null : b.id)}
+ className="p-1 text-concrete hover:text-ink hover:bg-black/5 dark:hover:bg-white/5 rounded-full transition-colors cursor-pointer"
+ title="Type & Actions"
+ >
+
+
+ setZoomBlockId(b.id)}
+ className="p-1 text-concrete hover:text-accent hover:bg-accent/5 rounded-full transition-colors cursor-pointer"
+ title="Focus / Zoom sur le bloc"
+ >
+
+
+ handleDeleteBlock(b.id)}
+ className="p-1 text-rust/70 hover:text-rust hover:bg-rust/5 rounded-full transition-colors cursor-pointer"
+ title="Supprimer le bloc"
+ >
+
+
+
+
+ {/* Convert Dropdown Menu */}
+ {isMenuOpen && (
+
+
Convertir type block
+
handleConvertBlockType(b.id, 'paragraph')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Paragraph
+
+
handleConvertBlockType(b.id, 'h1')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ H1 Title
+
+
handleConvertBlockType(b.id, 'h2')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ H2 Title
+
+
handleConvertBlockType(b.id, 'todo')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Checklist Item
+
+
handleConvertBlockType(b.id, 'bullet')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+
Bullet List Item
+
+
handleConvertBlockType(b.id, 'math')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Math (LaTex)
+
+
handleConvertBlockType(b.id, 'chart')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Chart Block
+
+
handleConvertBlockType(b.id, 'flowchart')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Flowchart Block
+
+
handleConvertBlockType(b.id, 'gantt')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Gantt Timeline
+
+
handleConvertBlockType(b.id, 'code')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Code Block
+
+
handleConvertBlockType(b.id, 'database')} className="w-full text-left p-1.5 hover:bg-black/5 rounded flex items-center gap-2 text-ink">
+ Base de Données
+
+
+ )}
+
+ {/* RENDER THE CORRESPONDING BLOCK TYPE */}
+
+ {/* Visual decoration according to types */}
+ {b.type === 'todo' && (
+
handleUpdateBlockMetadata(b.id, { completed: !b.metadata?.completed })}
+ className={`w-5 h-5 rounded border mt-1 flex items-center justify-center transition-all cursor-pointer shrink-0
+ ${b.metadata?.completed ? 'bg-accent border-accent text-white' : 'border-border'}`}
+ >
+ {b.metadata?.completed && ✓ }
+
+ )}
+
+ {b.type === 'bullet' && (
+
+ )}
+
+ {/* Sub-editor view container based on block type */}
+
+ {b.type === 'paragraph' && (
+
handleUpdateBlockContent(b.id, e.target.value)}
+ onKeyDown={(e) => handleBlockKeyDown(e, i, b)}
+ className="w-full bg-transparent border-none outline-none focus:ring-0 text-lg text-ink font-light leading-relaxed placeholder:text-concrete/40 resize-none overflow-hidden h-auto"
+ placeholder="Tapez votre paragraphe... Appuyez sur Entrée pour en ajouter un nouveau."
+ rows={Math.max(1, b.content.split('\n').length)}
+ />
+ )}
+
+ {b.type === 'h1' && (
+ handleUpdateBlockContent(b.id, e.target.value)}
+ onKeyDown={(e) => handleBlockKeyDown(e, i, b)}
+ className="w-full bg-transparent border-none outline-none focus:ring-0 text-3xl font-serif font-bold text-ink leading-tight placeholder:text-concrete/40 resize-none"
+ placeholder="Title 1"
+ rows={1}
+ />
+ )}
+
+ {b.type === 'h2' && (
+ handleUpdateBlockContent(b.id, e.target.value)}
+ onKeyDown={(e) => handleBlockKeyDown(e, i, b)}
+ className="w-full bg-transparent border-none outline-none focus:ring-0 text-2xl font-serif font-semibold text-ink leading-snug placeholder:text-concrete/40 resize-none"
+ placeholder="Title 2"
+ rows={1}
+ />
+ )}
+
+ {b.type === 'h3' && (
+ handleUpdateBlockContent(b.id, e.target.value)}
+ onKeyDown={(e) => handleBlockKeyDown(e, i, b)}
+ className="w-full bg-transparent border-none outline-none focus:ring-0 text-xl font-serif font-medium text-ink/90 leading-normal placeholder:text-concrete/40 resize-none"
+ placeholder="Title 3"
+ rows={1}
+ />
+ )}
+
+ {b.type === 'todo' && (
+ handleUpdateBlockContent(b.id, e.target.value)}
+ onKeyDown={(e) => handleBlockKeyDown(e, i, b)}
+ className={`w-full bg-transparent border-none outline-none focus:ring-0 text-lg leading-relaxed placeholder:text-concrete/30 resize-none
+ ${b.metadata?.completed ? 'line-through text-concrete decoration-concrete' : 'text-ink font-light'}`}
+ placeholder="Ajouter une tâche..."
+ rows={1}
+ />
+ )}
+
+ {b.type === 'bullet' && (
+ handleUpdateBlockContent(b.id, e.target.value)}
+ onKeyDown={(e) => handleBlockKeyDown(e, i, b)}
+ className="w-full bg-transparent border-none outline-none focus:ring-0 text-lg text-ink font-light leading-relaxed placeholder:text-concrete/45 resize-none"
+ placeholder="Ligne de puce..."
+ rows={1}
+ />
+ )}
+
+ {b.type === 'code' && (
+
+
+ Codeblock
+
+
handleUpdateBlockContent(b.id, e.target.value)}
+ className="w-full bg-transparent border-none outline-none focus:ring-0 text-emerald-400 font-mono resize-none"
+ placeholder="Mettez votre code informatique ou formule ici..."
+ rows={Math.max(3, b.content.split('\n').length)}
+ />
+
+ )}
+
+ {b.type === 'math' && (
+ handleUpdateBlockContent(b.id, val)} />
+ )}
+
+ {b.type === 'chart' && (
+ handleUpdateBlockMetadata(b.id, metadata)}
+ />
+ )}
+
+ {b.type === 'flowchart' && (
+ handleUpdateBlockMetadata(b.id, metadata)}
+ />
+ )}
+
+ {b.type === 'gantt' && (
+ handleUpdateBlockMetadata(b.id, metadata)}
+ />
+ )}
+
+ {b.type === 'database' && (
+ handleUpdateBlockMetadata(b.id, metadata)}
+ />
+ )}
+
+
+
+ {/* In-between layout divider insert button helper hover effect */}
+
+
+
{
+ const rect = e.currentTarget.getBoundingClientRect();
+ setShowInsertMenuAt(showInsertMenuAt?.index === i ? null : { index: i, top: rect.bottom + window.scrollY });
+ }}
+ className="bg-white dark:bg-zinc-900 border border-border rounded-full p-1 text-concrete hover:text-accent hover:border-accent hover:scale-110 shadow-lg transition-all absolute flex items-center gap-1.5 px-3 py-1 cursor-pointer"
+ >
+
+ Insérer un élément
+
+
+
+ {/* Dynamic Insert Menu Popper at index i */}
+ {showInsertMenuAt && showInsertMenuAt.index === i && (
+
+
+ Insérer un outil de prise de note
+
+
handleInsertBlockAt(i, 'paragraph')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Paragraphe
+
+
handleInsertBlockAt(i, 'h2')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Titre Section
+
+
handleInsertBlockAt(i, 'todo')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Checklist
+
+
handleInsertBlockAt(i, 'math')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Formule Math
+
+
handleInsertBlockAt(i, 'flowchart')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Diagramme Flux
+
+
handleInsertBlockAt(i, 'gantt')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Gantt Projets
+
+
handleInsertBlockAt(i, 'chart')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Graphe Stat
+
+
handleInsertBlockAt(i, 'code')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Bloc Code
+
+
handleInsertBlockAt(i, 'database')}
+ className="p-2.5 rounded-xl border border-border/40 hover:border-accent/30 hover:bg-accent/[0.02] flex flex-col items-center gap-2 transition-all group"
+ >
+
+
+
+ Base de Données
+
+
setShowInsertMenuAt(null)}
+ className="col-span-3 mt-1.5 py-1.5 text-center text-[10px] text-zinc-400 font-bold uppercase hover:bg-slate-50 rounded"
+ >
+ Fermer le menu
+
+
+ )}
+
+ );
+ })}
+
+
+ {/* BLOCK ISOLATION / ZOOM-IN LIGHTBOX MODAL */}
+
+ {zoomBlockId !== null && (() => {
+ const focusedBlockItem = blocks.find(b => b.id === zoomBlockId);
+ if (!focusedBlockItem) return null;
+
+ return (
+
+
+
+
+
+
+
Espace de Prise de Note Isolé (Block Zoom-in)
+
Type block : {focusedBlockItem.type}
+
+
+
setZoomBlockId(null)}
+ className="p-2 hover:bg-slate-50 rounded-full transition-colors flex items-center justify-center cursor-pointer"
+ >
+
+
+
+
+
+
+
+ setZoomBlockId(null)}
+ className="px-6 py-2.5 bg-ink text-paper rounded-xl text-xs font-bold uppercase tracking-widest hover:opacity-90 transition-opacity cursor-pointer"
+ >
+ Sauvegarder & Quitter
+
+
+
+
+ );
+ })()}
+
+
+ );
+};
+
+// ============================================
+// SUB-BLOCK COMPONENT 1: MATH EDITOR (LATEX)
+// ============================================
+interface MathBlockEditorProps {
+ value: string;
+ onChange: (val: string) => void;
+}
+
+const MathBlockEditor: React.FC = ({ value, onChange }) => {
+ const MATH_PRESETS = [
+ { label: 'Nombre d\'Or (Φ)', formula: '\\Phi = \\frac{1 + \\sqrt{5}}{2}' },
+ { label: 'Identité d\'Euler', formula: 'e^{i\\pi} + 1 = 0' },
+ { label: 'Matrice 2x2', formula: '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}' },
+ { label: 'Intégrale Gauche', formula: '\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}' },
+ { label: 'Théorème de Pythagore', formula: 'a^2 + b^2 = c^2' },
+ { label: 'Série Infinie', formula: '\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}' }
+ ];
+
+ const SYMBOL_SHORTCUTS = [
+ { label: 'Intégrale', sym: '\\int' },
+ { label: 'Racine', sym: '\\sqrt{}' },
+ { label: 'Fraction', sym: '\\frac{}{}' },
+ { label: 'Somme', sym: '\\sum_{}^{}' },
+ { label: 'Différentiel', sym: '\\partial' },
+ { label: 'Grec Phi', sym: '\\Phi' },
+ { label: 'Grec Delta', sym: '\\Delta' },
+ { label: 'Grec Alpha', sym: '\\alpha' },
+ { label: 'Matrice', sym: '\\begin{matrix} \\end{matrix}' },
+ { label: 'Gradient', sym: '\\nabla' }
+ ];
+
+ return (
+
+
+
+ Équation Math. & Physique (LaTeX)
+
+
+
+
+ {/* Editor Pane */}
+
+
onChange(e.target.value)}
+ className="w-full bg-zinc-900 border border-zinc-800 text-stone-200 font-mono text-sm p-3.5 rounded-xl block outline-none focus:border-[#A47148]"
+ rows={2}
+ />
+ {/* Symbol bar */}
+
+ {SYMBOL_SHORTCUTS.map(s => (
+ onChange(value + s.sym)}
+ className="px-2 py-1 bg-zinc-100 dark:bg-zinc-800 hover:bg-[#A47148]/10 hover:text-[#A47148] rounded text-[10px] font-mono font-medium transition-colors cursor-pointer"
+ >
+ {s.label}
+
+ ))}
+
+
+
+ {/* Presets and rendering Column */}
+
+
Presets mathématiques :
+
+ {MATH_PRESETS.map(p => (
+ onChange(p.formula)}
+ className="px-2 py-1 border border-border/60 hover:border-[#A47148]/30 hover:bg-[#A47148]/5 rounded text-[9.5px] text-justify font-sans transition-all text-ink/75 font-semibold truncate cursor-pointer"
+ title={p.formula}
+ >
+ {p.label}
+
+ ))}
+
+
+
+
+ {/* Render Output display pane */}
+
+
Aperçu dynamique WYSIWYG :
+
+ $${value || 'Format LaTeX'}$$
+
+
+
+ );
+};
+
+// ============================================
+// SUB-BLOCK COMPONENT 2: CHART BLOCK EDITOR
+// ============================================
+interface ChartBlockEditorProps {
+ metadata?: BlockItem['metadata'];
+ onChange: (metadata: any) => void;
+}
+
+const ChartBlockEditor: React.FC = ({ metadata, onChange }) => {
+ const chartType = metadata?.chartType || 'bar';
+ const chartData = metadata?.chartData || [];
+
+ const handleAddRow = () => {
+ const nextRow = { name: `Catégorie ${chartData.length + 1}`, value: 50 };
+ onChange({ chartType, chartData: [...chartData, nextRow] });
+ };
+
+ const handleUpdateRow = (idx: number, name: string, value: number) => {
+ const nextData = [...chartData];
+ nextData[idx] = { name, value };
+ onChange({ chartType, chartData: nextData });
+ };
+
+ const handleDeleteRow = (idx: number) => {
+ const nextData = chartData.filter((_, i) => i !== idx);
+ onChange({ chartType, chartData: nextData });
+ };
+
+ const totalSum = chartData.reduce((acc, current) => acc + current.value, 0) || 1;
+
+ return (
+
+
+
+ Graphes Statistiques Intégrés
+
+
+ {(['bar', 'line', 'pie'] as const).map(t => (
+ onChange({ chartType: t, chartData })}
+ className={`px-2.5 py-1 rounded-md text-[9.5px] uppercase font-bold tracking-wider transition-all cursor-pointer
+ ${chartType === t ? 'bg-[#A47148] text-white' : 'text-concrete hover:text-ink'}`}
+ >
+ {t}
+
+ ))}
+
+
+
+
+ {/* Sliders Console */}
+
+
Données variables :
+
+
+
+ Ajouter une donnée
+
+
+
+ {/* Chart View visualization rendered in customizable pure elements */}
+
+ {chartType === 'bar' && (
+
+ {chartData.map((row, i) => (
+
+ ))}
+
+ )}
+
+ {chartType === 'line' && (
+
+ {/* Dynamic SVG Sparkline */}
+
+ {/* Axes */}
+
+ {/* Connect points */}
+ {chartData.length > 1 && (() => {
+ const pts = chartData.map((row, i) => {
+ const x = (i / (chartData.length - 1)) * 180 + 10;
+ const y = 90 - (row.value / 100) * 80;
+ return `${x},${y}`;
+ }).join(' ');
+ return (
+ <>
+
+ {chartData.map((row, i) => {
+ const x = (i / (chartData.length - 1)) * 180 + 10;
+ const y = 90 - (row.value / 100) * 80;
+ return (
+
+
+ {row.value}%
+ {row.name}
+
+ );
+ })}
+ >
+ );
+ })()}
+
+
+ )}
+
+ {chartType === 'pie' && (
+
+
+ {chartData.length > 0 && (() => {
+ let accumulatedPercent = 0;
+ return chartData.map((row, i) => {
+ const percent = row.value / totalSum;
+ const startAngle = accumulatedPercent * 360;
+ const endAngle = (accumulatedPercent + percent) * 360;
+ accumulatedPercent += percent;
+
+ const rad = Math.PI / 180;
+ const x1 = 50 + 35 * Math.cos(startAngle * rad);
+ const y1 = 50 + 35 * Math.sin(startAngle * rad);
+ const x2 = 50 + 35 * Math.cos(endAngle * rad);
+ const y2 = 50 + 35 * Math.sin(endAngle * rad);
+
+ const largeArcFlag = percent > 0.5 ? 1 : 0;
+ const pathData = `M 50 50 L ${x1} ${y1} A 35 35 0 ${largeArcFlag} 1 ${x2} ${y2} Z`;
+
+ const op = 0.35 + (i * 0.15); // Layer gradient opacity
+ return (
+
+ );
+ });
+ })()}
+
+
+ {chartData.slice(0, 4).map((row, i) => (
+
+
+ {row.name}
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+// ============================================
+// SUB-BLOCK COMPONENT 3: GANTT BLOCK EDITOR
+// ============================================
+interface GanttBlockEditorProps {
+ metadata?: BlockItem['metadata'];
+ onChange: (metadata: any) => void;
+}
+
+const GanttBlockEditor: React.FC = ({ metadata, onChange }) => {
+ const ganttTasks = metadata?.ganttTasks || [];
+
+ const handleAddTask = () => {
+ const nextTask = {
+ id: `task-${Date.now()}`,
+ name: `Nouvel élément ${ganttTasks.length + 1}`,
+ startDay: 1,
+ duration: 3,
+ progress: 0
+ };
+ onChange({ ganttTasks: [...ganttTasks, nextTask] });
+ };
+
+ const handleUpdateTask = (id: string, field: string, value: any) => {
+ const nextTasks = ganttTasks.map(t => t.id === id ? { ...t, [field]: value } : t);
+ onChange({ ganttTasks: nextTasks });
+ };
+
+ const handleDeleteTask = (id: string) => {
+ onChange({ ganttTasks: ganttTasks.filter(t => t.id !== id) });
+ };
+
+ return (
+
+
+
+ Gantt de Suivi Temporel & Jalons Project
+
+
+
+
+ {/* Visual Gantt timeline preview rendering */}
+
+
+
Tâche / Livrable
+
+ {Array.from({ length: 10 }).map((_, d) => (
+ J{d + 1}
+ ))}
+
+
+
+
+ {ganttTasks.map((t) => {
+ const startIdx = Math.max(0, t.startDay - 1);
+ const dur = Math.max(1, t.duration);
+ const gridColStart = startIdx + 1;
+ const gridColSpan = dur;
+
+ return (
+
+
{t.name}
+
+ {/* The task progress segment element */}
+
+ {/* Completion inner fill bar progression */}
+
+
{t.progress}%
+
+
+
+ );
+ })}
+
+
+
+ {/* Task lists & dynamic sliders to control values */}
+
+
Ajustements jalons & temps :
+
+ {ganttTasks.map((t) => (
+
+
+ handleUpdateTask(t.id, 'name', e.target.value)}
+ className="flex-1 bg-transparent border-b border-border/40 text-xs font-bold text-ink outline-none p-0.5"
+ />
+ handleDeleteTask(t.id)}
+ className="p-1 hover:bg-rose-50 rounded text-rose-500 hover:text-rose-600 cursor-pointer"
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ Ajouter une étape livrable
+
+
+
+
+ );
+};
+
+// ============================================
+// SUB-BLOCK COMPONENT 4: FLOWCHART BLOCK EDITOR
+// ============================================
+interface FlowchartBlockEditorProps {
+ metadata?: BlockItem['metadata'];
+ onChange: (metadata: any) => void;
+}
+
+const FlowchartBlockEditor: React.FC = ({ metadata, onChange }) => {
+ const nodes = metadata?.flowNodes || [];
+ const edges = metadata?.flowEdges || [];
+
+ const [newNodeName, setNewNodeName] = useState('');
+ const [edgeFromId, setEdgeFromId] = useState('');
+ const [edgeToId, setEdgeToId] = useState('');
+ const [edgeLabel, setEdgeLabel] = useState('');
+
+ const handleAddNode = () => {
+ if (!newNodeName.trim()) return;
+ const nextNode = {
+ id: `node-${Date.now()}`,
+ label: newNodeName,
+ x: Math.floor(Math.random() * 200) + 50,
+ y: Math.floor(Math.random() * 100) + 40
+ };
+ onChange({
+ flowNodes: [...nodes, nextNode],
+ flowEdges: edges
+ });
+ setNewNodeName('');
+ };
+
+ const handleAddEdge = () => {
+ if (!edgeFromId || !edgeToId) return;
+ const nextEdge = {
+ id: `edge-${Date.now()}`,
+ from: edgeFromId,
+ to: edgeToId,
+ label: edgeLabel || undefined
+ };
+ onChange({
+ flowNodes: nodes,
+ flowEdges: [...edges, nextEdge]
+ });
+ setEdgeFromId('');
+ setEdgeToId('');
+ setEdgeLabel('');
+ };
+
+ const handleDeleteNode = (id: string) => {
+ onChange({
+ flowNodes: nodes.filter(n => n.id !== id),
+ flowEdges: edges.filter(e => e.from !== id && e.to !== id)
+ });
+ };
+
+ const handleDeleteEdge = (id: string) => {
+ onChange({
+ flowNodes: nodes,
+ flowEdges: edges.filter(e => e.id !== id)
+ });
+ };
+
+ return (
+
+
+
+ Logique Spatiale & Flux Diagram (Nodes Canvas)
+
+
+
+
+ {/* Editor controllers pane on left */}
+
+
+ {/* Create State / Node form */}
+
+
Créer une étape (Node) :
+
+ setNewNodeName(e.target.value)}
+ className="flex-1 bg-zinc-50 dark:bg-[#1C1C1E] border border-border rounded-lg px-2 py-1.5 text-xs outline-none focus:border-[#A47148]"
+ />
+
+ Ajouter
+
+
+
+
+ {/* Create transition link / arrow Edge form */}
+
+
Relier des étapes (Arrows) :
+
+ setEdgeFromId(e.target.value)}
+ className="bg-zinc-50 dark:bg-[#1C1C1E] border border-border rounded-lg px-2 py-1.5 text-xs text-ink outline-none"
+ >
+ Depuis...
+ {nodes.map(n => (
+ {n.label}
+ ))}
+
+ setEdgeToId(e.target.value)}
+ className="bg-zinc-50 dark:bg-[#1C1C1E] border border-border rounded-lg px-2 py-1.5 text-xs text-ink outline-none"
+ >
+ Vers...
+ {nodes.map(n => (
+ {n.label}
+ ))}
+
+
+
+ setEdgeLabel(e.target.value)}
+ className="flex-1 bg-zinc-50 dark:bg-[#1C1C1E] border border-border rounded-lg px-2 py-1.5 text-xs outline-none focus:border-[#A47148]"
+ />
+
+ Relier
+
+
+
+
+
+ {/* List existing nodes with delete option */}
+
+
Éléments du flux ({nodes.length})
+
+ {nodes.map(n => (
+
+ {n.label}
+ handleDeleteNode(n.id)} className="text-rust hover:underline font-bold cursor-pointer">Supprimer
+
+ ))}
+ {nodes.length === 0 && (
+
Aucun node créé pour le moment.
+ )}
+
+
+
+
+ {/* Dynamic visual Canvas rendered using beautiful pure custom layout items inside SVG container box */}
+
+
+
+
+
+
+
+ {/* Render Arrow edges links */}
+ {edges.map((edge) => {
+ const fromNode = nodes.find(n => n.id === edge.from);
+ const toNode = nodes.find(n => n.id === edge.to);
+ if (!fromNode || !toNode) return null;
+
+ // Simply adjust coordinates to fit into 400x225 bounding box proportions
+ const x1 = (fromNode.x / 500) * 360 + 20;
+ const y1 = (fromNode.y / 250) * 180 + 20;
+ const x2 = (toNode.x / 500) * 360 + 20;
+ const y2 = (toNode.y / 250) * 180 + 20;
+
+ return (
+
+
+ {/* Bubble text on connection link lines */}
+ {edge.label && (
+
+
+ {edge.label}
+
+ )}
+
+ );
+ })}
+
+
+ {/* Draggable Circle Node Blocks in HTML layer for dragging rendering */}
+
+ {nodes.map((node) => {
+ const x = (node.x / 500) * 360 + 20;
+ const y = (node.y / 250) * 180 + 20;
+
+ return (
+
+ {node.label}
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+// ============================================
+// SUB-BLOCK COMPONENT 5: DATABASE BLOCK EDITOR
+// ============================================
+interface DatabaseBlockEditorProps {
+ metadata?: BlockItem['metadata'];
+ onChange: (metadata: any) => void;
+}
+
+const DatabaseBlockEditor: React.FC = ({ metadata, onChange }) => {
+ const dbId = metadata?.dbId || 'authors-works';
+ const dbView = metadata?.dbView || 'table';
+ const dbAuthors = metadata?.dbAuthors || [
+ { id: 'a1', name: 'Jules Verne' },
+ { id: 'a2', name: 'Liu Cixin' }
+ ];
+ const dbBooks = metadata?.dbBooks || [
+ { id: 'bk1', title: 'Twenty Thousand Leagues Under The Sea', author: 'Jules Verne', cover: 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400', tag: 'Aventure' },
+ { id: 'bk2', title: 'The Three-Body Problem', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400', tag: 'Hard SF' },
+ { id: 'bk3', title: 'The Wandering Earth', author: 'Liu Cixin', cover: 'https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?auto=format&fit=crop&q=80&w=400', tag: 'SF Spatial' }
+ ];
+
+ // Temp form states
+ const [newBookTitle, setNewBookTitle] = useState('');
+ const [newBookAuthor, setNewBookAuthor] = useState('');
+ const [newBookTag, setNewBookTag] = useState('');
+ const [newBookCover, setNewBookCover] = useState('');
+ const [newAuthorName, setNewAuthorName] = useState('');
+
+ const handleAddAuthor = () => {
+ if (!newAuthorName.trim()) return;
+ const authorExists = dbAuthors.some(a => a.name.toLowerCase() === newAuthorName.trim().toLowerCase());
+ if (authorExists) return;
+
+ const nextAuthors = [
+ ...dbAuthors,
+ { id: `auth-${Date.now()}`, name: newAuthorName.trim() }
+ ];
+ onChange({
+ dbId,
+ dbView,
+ dbAuthors: nextAuthors,
+ dbBooks
+ });
+ setNewAuthorName('');
+ };
+
+ const handleAddBook = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newBookTitle.trim() || !newBookAuthor.trim()) return;
+
+ // Check if author already exists, if not, create them in the list!
+ let nextAuthors = [...dbAuthors];
+ const authorExists = dbAuthors.some(a => a.name.toLowerCase() === newBookAuthor.trim().toLowerCase());
+ if (!authorExists) {
+ nextAuthors.push({ id: `auth-${Date.now()}`, name: newBookAuthor.trim() });
+ }
+
+ const defaultCovers = [
+ 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400',
+ 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400',
+ 'https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?auto=format&fit=crop&q=80&w=400',
+ 'https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=400'
+ ];
+ const computedCover = newBookCover.trim() || defaultCovers[Math.floor(Math.random() * defaultCovers.length)];
+
+ const nextBooks = [
+ ...dbBooks,
+ {
+ id: `bk-${Date.now()}`,
+ title: newBookTitle.trim(),
+ author: newBookAuthor.trim(),
+ cover: computedCover,
+ tag: newBookTag.trim() || 'Général'
+ }
+ ];
+
+ onChange({
+ dbId,
+ dbView,
+ dbAuthors: nextAuthors,
+ dbBooks: nextBooks
+ });
+
+ setNewBookTitle('');
+ setNewBookAuthor('');
+ setNewBookTag('');
+ setNewBookCover('');
+ };
+
+ const handleDeleteBook = (id: string) => {
+ const nextBooks = dbBooks.filter(b => b.id !== id);
+ onChange({
+ dbId,
+ dbView,
+ dbAuthors,
+ dbBooks: nextBooks
+ });
+ };
+
+ const handleDeleteAuthor = (id: string) => {
+ const nextAuthors = dbAuthors.filter(a => a.id !== id);
+ onChange({
+ dbId,
+ dbView,
+ dbAuthors: nextAuthors,
+ dbBooks
+ });
+ };
+
+ return (
+
+ {/* Notion-style header */}
+
+
+
📚
+
+
+ Base d'Auteurs & Œuvres (Relational Model)
+
+
+ id: {dbId}
+
+
+
+
+ onChange({ dbId, dbView: 'table', dbAuthors, dbBooks })}
+ className={`px-3 py-1 rounded-md transition-all cursor-pointer ${dbView === 'table' ? 'bg-white dark:bg-zinc-700 text-ink dark:text-dark-ink shadow-sm' : 'text-concrete'}`}
+ >
+ Tableau
+
+ onChange({ dbId, dbView: 'card', dbAuthors, dbBooks })}
+ className={`px-3 py-1 rounded-md transition-all cursor-pointer ${dbView === 'card' ? 'bg-white dark:bg-zinc-700 text-ink dark:text-dark-ink shadow-sm' : 'text-concrete'}`}
+ >
+ Fiches
+
+
+
+
+
+ Cette base de données relationnelle démontre la relation sémantique et la colonne Works count (Rollup) , qui utilise une formule "Count All" pour agréger dynamiquement le nombre de livres écrits par chaque auteur.
+
+
+ {/* VIEW DE TABLEAU DE RELATION */}
+ {dbView === 'table' ? (
+
+
+
+
+
+ 🔑 AUTEUR / AUTHOR
+ ↗ ŒUVRES LIÉES / WORKS (RELATION)
+ ⧉ RECOMPTAGE (ROLLUP COUNT)
+
+
+
+
+ {dbAuthors.map((auth) => {
+ const authorBooks = dbBooks.filter(b => b.author.toLowerCase() === auth.name.toLowerCase());
+ const worksStr = authorBooks.map(b => b.title).join(', ') || 'Aucune œuvre liée';
+ return (
+
+ {auth.name}
+ {worksStr}
+ {authorBooks.length}
+
+ handleDeleteAuthor(auth.id)}
+ className="text-rust hover:underline opacity-0 group-hover/row:opacity-100 transition-opacity font-bold text-[10px]"
+ title="Supprimer cet auteur"
+ >
+ Suppr.
+
+
+
+ );
+ })}
+
+
+
+
+ {/* AJOUTER ENTRÉE AUTEUR FORM */}
+
+ ➕ Ajouter un auteur :
+ setNewAuthorName(e.target.value)}
+ className="flex-1 bg-zinc-50 dark:bg-zinc-900 border border-border rounded-lg px-2.5 py-1 text-xs outline-none focus:border-accent text-ink dark:text-stone-200"
+ />
+
+ Créer Auteur
+
+
+
+ ) : (
+ /* CARD MODE VIEW */
+
+
+ {dbBooks.map((book) => (
+
+
handleDeleteBook(book.id)}
+ className="absolute right-2 top-2 bg-black/60 p-1.5 rounded-full text-white/85 hover:text-white hover:bg-rust transition-colors shadow opacity-0 group-hover/book:opacity-100 z-10"
+ title="Supprimer la fiche"
+ >
+
+
+
+
+
+
+
+
{book.title}
+
+
+ 👤 {book.author}
+
+
+ 🏷️ {book.tag}
+
+
+
+
+ ))}
+
+ 📖
+ Base d'Œuvres
+ {dbBooks.length} livres stockés
+
+
+
+ )}
+
+ {/* DYNAMIC FORM TO PUSH NEW BOOK */}
+
+
+ ⚡ Ajouter une œuvre (Créer la Relation automatique) :
+
+
+
+ setNewBookTag(e.target.value)}
+ className="bg-white dark:bg-zinc-950 border border-border rounded-lg px-2.5 py-1.5 text-xs outline-none focus:border-accent text-ink dark:text-stone-200"
+ />
+ setNewBookCover(e.target.value)}
+ className="bg-white dark:bg-zinc-950 border border-border rounded-lg px-2.5 py-1.5 text-xs outline-none focus:border-accent text-ink dark:text-stone-200"
+ />
+
+
+ ➕ Insérer l'œuvre & Calculer le Rollup
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/NetworkGraph.tsx b/architectural-grid1/src/components/NetworkGraph.tsx
new file mode 100644
index 0000000..a49b8d4
--- /dev/null
+++ b/architectural-grid1/src/components/NetworkGraph.tsx
@@ -0,0 +1,237 @@
+
+import React, { useEffect, useRef } from 'react';
+import * as d3 from 'd3';
+import { Note, NoteCluster, BridgeNote } from '../types';
+
+interface NetworkGraphProps {
+ notes: Note[];
+ clusters: NoteCluster[];
+ bridgeNotes: BridgeNote[];
+ onNoteSelect: (id: string) => void;
+ selectedClusterId: string | null;
+ onClusterSelect: (id: string | null) => void;
+}
+
+export const NetworkGraph: React.FC = ({
+ notes,
+ clusters,
+ bridgeNotes,
+ onNoteSelect,
+ selectedClusterId,
+ onClusterSelect
+}) => {
+ const svgRef = useRef(null);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ if (!svgRef.current || !containerRef.current) return;
+
+ const width = containerRef.current.clientWidth;
+ const height = containerRef.current.clientHeight;
+
+ const svg = d3.select(svgRef.current);
+ svg.selectAll("*").remove();
+
+ const g = svg.append("g");
+
+ const zoom = d3.zoom()
+ .scaleExtent([0.1, 4])
+ .on("zoom", (event) => {
+ g.attr("transform", event.transform);
+ });
+
+ svg.call(zoom);
+
+ // Filter notes with embeddings and cluster assignments
+ const visibleNotes = notes.filter(n => n.embedding && n.clusterId);
+
+ interface D3Node extends d3.SimulationNodeDatum {
+ id: string;
+ title: string;
+ clusterId: string;
+ color: string;
+ isBridge: boolean;
+ radius: number;
+ }
+
+ interface D3Link extends d3.SimulationLinkDatum {
+ source: string;
+ target: string;
+ strength: number;
+ }
+
+ const bridgeSet = new Set(bridgeNotes.map(b => b.noteId));
+
+ const nodes: D3Node[] = visibleNotes.map(n => {
+ const cluster = clusters.find(c => c.id === n.clusterId);
+ const isBridge = bridgeSet.has(n.id);
+ return {
+ id: n.id,
+ title: n.title,
+ clusterId: n.clusterId!,
+ color: cluster?.color || '#cbd5e1',
+ isBridge,
+ radius: isBridge ? 13 : 8
+ };
+ });
+
+ const links: D3Link[] = [];
+ // Only connect strong links
+ for (let i = 0; i < visibleNotes.length; i++) {
+ for (let j = i + 1; j < visibleNotes.length; j++) {
+ const ni = visibleNotes[i];
+ const nj = visibleNotes[j];
+
+ if (ni.clusterId === nj.clusterId) {
+ links.push({ source: ni.id, target: nj.id, strength: 0.5 });
+ }
+ }
+ }
+
+ const simulation = d3.forceSimulation(nodes)
+ .force("link", d3.forceLink(links).id(d => d.id).distance(110))
+ .force("charge", d3.forceManyBody().strength(-220))
+ .force("center", d3.forceCenter(width / 2, height / 2))
+ .force("collision", d3.forceCollide().radius(d => d.radius + 12));
+
+ // Links
+ const link = g.append("g")
+ .selectAll("line")
+ .data(links)
+ .enter()
+ .append("line")
+ .attr("stroke", "#e2e8f0")
+ .attr("stroke-opacity", (d: any) => {
+ if (!selectedClusterId) return 0.6;
+ const sId = typeof d.source === 'string' ? d.source : (d.source as any).id;
+ const tId = typeof d.target === 'string' ? d.target : (d.target as any).id;
+ const sourceNote = nodes.find(n => n.id === sId);
+ const targetNote = nodes.find(n => n.id === tId);
+ return (sourceNote?.clusterId === selectedClusterId && targetNote?.clusterId === selectedClusterId) ? 0.8 : 0.05;
+ })
+ .attr("stroke-width", 1);
+
+ // Nodes
+ const node = g.append("g")
+ .selectAll(".node")
+ .data(nodes)
+ .enter()
+ .append("g")
+ .attr("class", "node cursor-pointer")
+ .on("click", (event, d) => onNoteSelect(d.id))
+ .call(d3.drag()
+ .on("start", dragstarted)
+ .on("drag", dragged)
+ .on("end", dragended) as any);
+
+ // Node opacities based on focus
+ node.attr("opacity", d => {
+ if (!selectedClusterId) return 1;
+ return d.clusterId === selectedClusterId ? 1 : 0.15;
+ });
+
+ node.append("circle")
+ .attr("r", d => d.radius)
+ .attr("fill", d => d.color)
+ .attr("stroke", d => d.isBridge ? "#D4AF37" : "#fff")
+ .attr("stroke-width", d => d.isBridge ? 3.5 : 2)
+ .style("filter", d => d.isBridge ? "drop-shadow(0 0 6px rgba(212, 175, 55, 0.6))" : "none");
+
+ node.append("text")
+ .attr("dy", d => d.radius + 14)
+ .attr("text-anchor", "middle")
+ .attr("class", "text-[10px] fill-concrete dark:fill-concrete/60 font-medium pointer-events-none")
+ .text(d => d.title.length > 20 ? d.title.substring(0, 20) + "..." : d.title);
+
+ simulation.on("tick", () => {
+ link
+ .attr("x1", d => (d.source as any).x)
+ .attr("y1", d => (d.source as any).y)
+ .attr("x2", d => (d.target as any).x)
+ .attr("y2", d => (d.target as any).y);
+
+ node
+ .attr("transform", d => `translate(${d.x},${d.y})`);
+ });
+
+ // Zoom transition on cluster highlight
+ if (selectedClusterId && width && height) {
+ const clusterNodes = nodes.filter(n => n.clusterId === selectedClusterId);
+ if (clusterNodes.length > 0) {
+ // Run a small tick count synchronously to find coordinates quickly if layout is starting
+ for (let i = 0; i < 50; ++i) simulation.tick();
+
+ const xCoords = clusterNodes.map(cn => cn.x).filter((x): x is number => x !== undefined);
+ const yCoords = clusterNodes.map(cn => cn.y).filter((y): y is number => y !== undefined);
+
+ if (xCoords.length > 0 && yCoords.length > 0) {
+ const avgX = d3.mean(xCoords) || width / 2;
+ const avgY = d3.mean(yCoords) || height / 2;
+
+ svg.transition()
+ .duration(800)
+ .call(
+ zoom.transform,
+ d3.zoomIdentity.translate(width / 2, height / 2).scale(1.4).translate(-avgX, -avgY)
+ );
+ }
+ }
+ } else {
+ svg.transition()
+ .duration(800)
+ .call(zoom.transform, d3.zoomIdentity);
+ }
+
+ function dragstarted(event: any, d: D3Node) {
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event: any, d: D3Node) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragended(event: any, d: D3Node) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ }
+
+ return () => simulation.stop();
+ }, [notes, clusters, bridgeNotes, onNoteSelect, selectedClusterId]);
+
+ return (
+
+
+ {clusters.map(c => {
+ const isSelected = selectedClusterId === c.id;
+ return (
+
onClusterSelect?.(isSelected ? null : c.id)}
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full border shadow-sm transition-all text-[9px] font-bold uppercase tracking-wider
+ ${isSelected
+ ? 'bg-ink text-white dark:bg-white dark:text-black border-ink dark:border-white scale-105 shadow-md'
+ : 'bg-white/90 dark:bg-black/80 text-concrete hover:text-ink hover:border-concrete/40 border-border'
+ }`}
+ >
+
+ {c.name}
+
+ );
+ })}
+ {selectedClusterId && (
+
onClusterSelect?.(null)}
+ className="px-3 py-1.5 rounded-full border border-rose-200 bg-rose-50 dark:bg-rose-950/20 dark:border-rose-900/40 text-rose-500 text-[9px] font-bold uppercase tracking-wider hover:bg-rose-100 transition-all shadow-sm"
+ >
+ Réinitialiser focus
+
+ )}
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/NotebookInfoSidebar.tsx b/architectural-grid1/src/components/NotebookInfoSidebar.tsx
new file mode 100644
index 0000000..e821c31
--- /dev/null
+++ b/architectural-grid1/src/components/NotebookInfoSidebar.tsx
@@ -0,0 +1,769 @@
+import React from 'react';
+import {
+ X,
+ Clock,
+ Folder,
+ Calendar,
+ FileText,
+ Hash,
+ Network,
+ CheckCircle2,
+ AlertCircle,
+ Copy,
+ Check,
+ History,
+ Info,
+ ChevronRight
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Note, Carnet } from '../types';
+
+interface NotebookInfoSidebarProps {
+ isOpen: boolean;
+ onClose: () => void;
+ activeNote: Note | undefined;
+ notes: Note[];
+ carnets: Carnet[];
+ onOpenNote: (id: string) => void;
+ onUpdateNote?: (note: Note) => void;
+}
+
+export const NotebookInfoSidebar: React.FC = ({
+ isOpen,
+ onClose,
+ activeNote,
+ notes = [],
+ carnets,
+ onOpenNote,
+ onUpdateNote
+}) => {
+ const [activeTab, setActiveTab] = React.useState<'infos' | 'versions' | 'relations'>('infos');
+ const [copiedId, setCopiedId] = React.useState(false);
+ const [hoveredOrbitNode, setHoveredOrbitNode] = React.useState(null);
+
+ // For ID copy action
+ const handleCopyId = (id: string) => {
+ navigator.clipboard.writeText(id).then(() => {
+ setCopiedId(true);
+ setTimeout(() => setCopiedId(false), 2000);
+ });
+ };
+
+ // Explicit links for Network
+ const explicitWikiLinks = React.useMemo(() => [
+ { source: 'n1', target: 'n1-b' },
+ { source: 'n3', target: 'n3-b' },
+ { source: 'bridge-1', target: 'n1' },
+ { source: 'bridge-1', target: 'n2' },
+ ], []);
+
+ const CARNET_COLOR_PALETTE: { [key: string]: string } = {
+ '1': '#D97706', // Daily Notes - Warm Amber
+ '2': '#059669', // Project: Neo - Soft Emerald
+ '3': '#4F46E5', // Shared Docs - Rich Indigo
+ '4': '#0891B2', // Architecture Research - Clean Cyan
+ '5': '#EA580C', // History of Architecture - Deep Orange
+ '6': '#DB2777', // Modernism - Vibrant Rose
+ '7': '#65A30D', // Sustainable Design - Cool Lime
+ };
+
+ const DEFAULT_CARNET_COLOR = '#71717A';
+
+ // Network calculation values
+ const backlinks = React.useMemo(() => {
+ if (!activeNote || !notes) return [];
+ return notes.filter(n => {
+ if (n.id === activeNote.id || n.isDeleted) return false;
+ const isExplicit = explicitWikiLinks.some(link =>
+ (link.source === n.id && link.target === activeNote.id)
+ );
+ const isContentLink = n.content.toLowerCase().includes(`[[${activeNote.title.toLowerCase()}]]`);
+ return isExplicit || isContentLink;
+ });
+ }, [activeNote, notes, explicitWikiLinks]);
+
+ const outboundLinks = React.useMemo(() => {
+ if (!activeNote || !notes) return [];
+ return notes.filter(n => {
+ if (n.id === activeNote.id || n.isDeleted) return false;
+ const isExplicit = explicitWikiLinks.some(link =>
+ (link.source === activeNote.id && link.target === n.id)
+ );
+ const isContentLink = activeNote.content.toLowerCase().includes(`[[${n.title.toLowerCase()}]]`);
+ return isExplicit || isContentLink;
+ });
+ }, [activeNote, notes, explicitWikiLinks]);
+
+ const unlinkedMentions = React.useMemo(() => {
+ if (!activeNote || !notes) return [];
+ return notes.filter(n => {
+ if (n.id === activeNote.id || n.isDeleted) return false;
+ const isLinked = [...backlinks, ...outboundLinks].some(link => link.id === n.id);
+ if (isLinked) return false;
+ return n.content.toLowerCase().includes(activeNote.title.toLowerCase());
+ });
+ }, [activeNote, notes, backlinks, outboundLinks]);
+
+ const orbitNodes = React.useMemo(() => {
+ const list: { id: string; title: string; color: string; carnetName: string; relationship: 'backlink' | 'outbound' | 'mention' }[] = [];
+
+ backlinks.forEach(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ list.push({
+ id: n.id,
+ title: n.title,
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ carnetName: carnet?.name || 'Carnet',
+ relationship: 'backlink'
+ });
+ });
+
+ outboundLinks.forEach(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ list.push({
+ id: n.id,
+ title: n.title,
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ carnetName: carnet?.name || 'Carnet',
+ relationship: 'outbound'
+ });
+ });
+
+ unlinkedMentions.forEach(n => {
+ const carnet = carnets.find(c => c.id === n.carnetId);
+ list.push({
+ id: n.id,
+ title: n.title,
+ color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
+ carnetName: carnet?.name || 'Carnet',
+ relationship: 'mention'
+ });
+ });
+
+ return list.slice(0, 8);
+ }, [backlinks, outboundLinks, unlinkedMentions, carnets]);
+
+ const getSnippetWithHighlight = (content: string, term: string) => {
+ const index = content.toLowerCase().indexOf(term.toLowerCase());
+ if (index === -1) {
+ return {content.substring(0, 80)}... ;
+ }
+ const start = Math.max(0, index - 40);
+ const end = Math.min(content.length, index + term.length + 40);
+ const before = content.substring(start, index);
+ const match = content.substring(index, index + term.length);
+ const after = content.substring(index + term.length, end);
+ return (
+
+ {start > 0 && "..."}
+ {before}
+ {match}
+ {after}
+ {end < content.length && "..."}
+
+ );
+ };
+
+ // Safe time calculation helper (mocked cleanly to match image's 'il y a 12 jours' or standard dynamic calculations)
+ const getRelativeCreatedStr = (dateStr: string) => {
+ if (dateStr.includes('12 mai 2026')) return 'il y a 12 jours';
+ if (dateStr.includes('Oct 26')) return 'il y a 2h';
+ if (dateStr.includes('Oct 27')) return 'il y a 1j';
+ if (dateStr.includes('Oct 24')) return 'il y a 3j';
+ if (dateStr.includes('Oct 25')) return 'il y a 2j';
+ if (dateStr.includes('Oct 22')) return 'il y a 5j';
+ if (dateStr.includes('Oct 23')) return 'il y a 4j';
+ if (dateStr.includes('Oct 28')) return 'il y a 10 min';
+ return 'il y a quelques jours';
+ };
+
+ return (
+
+ {isOpen && (
+
+ {/* Header tabs row matching image style */}
+
+
+ {/* Infos tab */}
+ setActiveTab('infos')}
+ className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-bold tracking-wide transition-all duration-200 cursor-pointer
+ ${activeTab === 'infos'
+ ? 'bg-ink text-paper dark:bg-white dark:text-ink shadow-sm'
+ : 'text-concrete hover:text-ink hover:bg-black/[0.03] dark:hover:bg-white/5'}`}
+ >
+
+ Infos
+
+
+ {/* Versions tab */}
+ setActiveTab('versions')}
+ className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-bold tracking-wide transition-all duration-200 cursor-pointer
+ ${activeTab === 'versions'
+ ? 'bg-ink text-paper dark:bg-white dark:text-ink shadow-sm'
+ : 'text-concrete hover:text-ink hover:bg-black/[0.03] dark:hover:bg-white/5'}`}
+ >
+
+ Versions
+
+
+ {/* Network / Relations tab */}
+ setActiveTab('relations')}
+ className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-bold tracking-wide transition-all duration-200 cursor-pointer
+ ${activeTab === 'relations'
+ ? 'bg-ink text-paper dark:bg-white dark:text-ink shadow-sm'
+ : 'text-concrete hover:text-ink hover:bg-black/[0.03] dark:hover:bg-white/5'}`}
+ >
+
+ Réseau
+
+
+
+
+
+
+
+
+ {/* Core scrollable content area */}
+
+
+ {/* TABS - INFOS */}
+ {activeTab === 'infos' && (
+
+ {activeNote ? (
+
+ {/* Calculated Stats */}
+ {(() => {
+ const wordCount = activeNote.content.trim() ? activeNote.content.trim().split(/\s+/).filter(Boolean).length : 0;
+ const charCount = activeNote.content.length;
+ const lineCount = activeNote.content.trim() ? activeNote.content.split('\n').length : 0;
+
+ // Count math equations
+ const matchesBigMath = (activeNote.content.match(/\$\$[\s\S]*?\$\$/g) || []).length;
+ const matchesInlineMath = (activeNote.content.match(/\$[^\$\n]+?\$/g) || []).length;
+ const equationCount = matchesBigMath + matchesInlineMath;
+
+ // Count graph relations or internal visual blocks
+ const matchesLivingBlocks = (activeNote.content.match(/\[\[living-block:.*?\]\]/g) || []).length;
+ const graphCount = orbitNodes.length + matchesLivingBlocks;
+
+ // Count images
+ const matchesMarkdownImages = (activeNote.content.match(/!\[.*?\]\(.*?\)/g) || []).length;
+ const matchesHtmlImages = (activeNote.content.match(/
+ {/* Grid Stats */}
+
+
+
+ {wordCount}
+
+ Mots
+
+
+
+ {charCount}
+
+ Caractères
+
+
+
+ {/* Secondary Detailed Counts Widget */}
+
+
+ {lineCount}
+ Lignes
+
+
+ {equationCount}
+ Équations
+
+
+ {graphCount}
+ Graphes
+
+
+ {imageCount}
+ Images
+
+
+ >
+ );
+ })()}
+
+ {/* Attribute Detail rows styled to 100% exact layout matching the attached image */}
+
+ {/* Carnet attribute */}
+
+
+
+
+
+ Carnet
+
+ {carnets.find(c => c.id === activeNote.carnetId)?.name || "Général"}
+
+
+
+
+ {/* Type attribute */}
+
+
+
+
+
+ Type
+
+ {activeNote.isClipped ? 'Source Web' : 'Texte enrichi'}
+
+
+
+
+ {/* Créé le attribute */}
+
+
+
+
+
+ Créée le
+
+ {activeNote.date || "12 mai 2026"}
+
+
+ {getRelativeCreatedStr(activeNote.date || "12 mai 2026")}
+
+
+
+
+ {/* Modifiée attribute */}
+
+
+
+
+
+ Modifiée
+
+ {activeNote.date || "12 mai 2026"} • 15:58
+
+
+ {getRelativeCreatedStr(activeNote.date || "12 mai 2026")}
+
+
+
+
+ {/* ID attribute */}
+
+
+
+
+
+
ID
+
+
+ {activeNote.id}
+
+ handleCopyId(activeNote.id)}
+ className="p-1 hover:bg-slate-100 dark:hover:bg-neutral-800 rounded text-concrete shrink-0 transition-all cursor-pointer"
+ title="Copier l'ID de la note"
+ >
+ {copiedId ? : }
+
+
+
+
+
+
+ {/* Snapshots Toggle */}
+
+
+
+
+
+
+
Snapshots Actifs
+
Suivi d'historique automatique
+
+
+
+ {
+ onUpdateNote?.({
+ ...activeNote,
+ isVersioningEnabled: activeNote.isVersioningEnabled === false ? true : false
+ });
+ }}
+ />
+
+
+
+
+ ) : (
+
+
+
Veuillez sélectionner une note pour inspecter ses informations.
+
+ )}
+
+ )}
+
+ {/* TABS - VERSIONS */}
+ {activeTab === 'versions' && (
+
+
+
Snapshots & Versions
+
+ {(activeNote?.versionHistory || []).length} Snapshots
+
+
+
+ {activeNote ? (
+
+ {activeNote.isVersioningEnabled !== false ? (
+ <>
+ {/* Banner to snap manual version */}
+
+
+
Garnir l'historique
+
Figer manuellement l'état actuel de la note.
+
+
{
+ const newSnapshot = {
+ id: 'v-' + Date.now(),
+ title: activeNote.title,
+ content: activeNote.content,
+ timestamp: new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) + ' • ' + new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
+ size: activeNote.content.length
+ };
+ onUpdateNote?.({
+ ...activeNote,
+ versionHistory: [newSnapshot, ...(activeNote.versionHistory || [])]
+ });
+ }}
+ className="w-full text-center py-2 bg-ink dark:bg-white hover:opacity-90 text-paper dark:text-ink text-[10px] uppercase tracking-widest font-bold rounded-lg transition-all shadow-sm cursor-pointer"
+ >
+ Figer un instant
+
+
+
+ {/* Snapshot list */}
+
+ {(activeNote.versionHistory || []).length > 0 ? (
+
+ {(activeNote.versionHistory || []).map((v) => (
+
+
+
+
+ {v.title}
+
+ {v.timestamp}
+
+
+ {v.size >= 1024 ? (v.size / 1024).toFixed(1) + ' KB' : v.size + ' B'}
+
+
+
+
+ {
+ alert(`Aperçu de la version "${v.title}" :\n\n${v.content || "Note vide"}`);
+ }}
+ className="text-muted-ink hover:text-ink transition-colors font-semibold"
+ >
+ Aperçu
+
+ {
+ if (window.confirm("Êtes-vous sûr de vouloir restaurer cette version ? Le contenu actuel sera archivé comme nouvelle version.")) {
+ const backupSnapshot = {
+ id: 'v-' + Date.now(),
+ title: activeNote.title,
+ content: activeNote.content,
+ timestamp: new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) + ' • ' + new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
+ size: activeNote.content.length
+ };
+ onUpdateNote?.({
+ ...activeNote,
+ title: v.title,
+ content: v.content,
+ versionHistory: [backupSnapshot, ...(activeNote.versionHistory || []).filter(h => h.id !== v.id)]
+ });
+ }
+ }}
+ className="text-ochre dark:text-ochre font-bold hover:underline"
+ >
+ Restaurer
+
+
+
+ ))}
+
+ ) : (
+
+
+
Aucun snapshot enregistré pour le moment. Modifiez la note pour démarrer le suivi ou figez-en un manuellement.
+
+ )}
+
+ >
+ ) : (
+
+
+
Suivi d'historique inactif
+
L'historique des versions est actuellement désactivé pour cette note spécifique. Pour l'activer, cochez l'option dans l'onglet "Infos".
+
+ )}
+
+ ) : (
+
+
+
Veuillez sélectionner une note pour voir son historique de versions.
+
+ )}
+
+ )}
+
+ {/* TABS - RELATIONS (RESEAU) */}
+ {activeTab === 'relations' && (
+
+
+
+
Vue Graphe Locale
+
+
+
+ {activeNote ? (
+ <>
+ {/* Interactive Local Graph representation */}
+
+
+
+
+
+
+ {/* Dotted boundary */}
+
+
+ {/* Links */}
+ {orbitNodes.map((node, i) => {
+ const angle = i * (orbitNodes.length > 0 ? (2 * Math.PI) / orbitNodes.length : 0);
+ const nx = 160 + 70 * Math.cos(angle);
+ const ny = 110 + 62 * Math.sin(angle);
+ return (
+
+
+ {node.relationship === 'outbound' && (
+
+ )}
+ {node.relationship === 'backlink' && (
+
+ )}
+
+ );
+ })}
+
+ {/* Center Node: Active Note */}
+
+
+
+
+
+ {/* Orbit nodes */}
+ {orbitNodes.map((node, i) => {
+ const angle = i * (orbitNodes.length > 0 ? (2 * Math.PI) / orbitNodes.length : 0);
+ const nx = 160 + 70 * Math.cos(angle);
+ const ny = 110 + 62 * Math.sin(angle);
+ const isHovered = hoveredOrbitNode?.id === node.id;
+ return (
+ onOpenNote(node.id)}
+ onMouseEnter={() => setHoveredOrbitNode(node)}
+ onMouseLeave={() => setHoveredOrbitNode(null)}
+ >
+
+
+ {node.title.substring(0, 10)}
+
+
+ );
+ })}
+
+
+
+ {hoveredOrbitNode ? (
+
+
+ {hoveredOrbitNode.carnetName}
+
+ {hoveredOrbitNode.relationship === 'backlink' ? 'Lien Entrant' : hoveredOrbitNode.relationship === 'outbound' ? 'Lien Sortant' : 'Mention Simple'}
+
+
+
{hoveredOrbitNode.title}
+
Cliquez pour ouvrir la note
+
+ ) : (
+
+
+ Survolez un nœud, cliquez pour ouvrir
+
+ )}
+
+
+
+ {/* Explicit links listings with highlighting */}
+
+ {/* 1. Backlinks */}
+
+
+ Liens Entrants ({backlinks.length})
+
+ {backlinks.length > 0 ? (
+
+ {backlinks.map(n => (
+
onOpenNote(n.id)}
+ className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm"
+ >
+
+ {n.title}
+ Réf
+
+
+ {getSnippetWithHighlight(n.content, activeNote.title)}
+
+
+ ))}
+
+ ) : (
+
Aucun lien entrant de type wiki [[lien]] pointant vers cette note.
+ )}
+
+
+ {/* 2. Outbound Links */}
+
+
+ Liens Sortants ({outboundLinks.length})
+
+ {outboundLinks.length > 0 ? (
+
+ {outboundLinks.map(n => (
+
onOpenNote(n.id)}
+ className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm"
+ >
+
+ {n.title}
+ Vers
+
+
+ {getSnippetWithHighlight(activeNote.content, n.title)}
+
+
+ ))}
+
+ ) : (
+
Cette note ne pointe vers aucune autre note de type [[lien]].
+ )}
+
+
+ >
+ ) : (
+
+
+
Sélectionnez une note pour analyser son graphe local.
+
+ )}
+
+ )}
+
+
+
+ )}
+
+ );
+};
diff --git a/architectural-grid1/src/components/NotebooksView.tsx b/architectural-grid1/src/components/NotebooksView.tsx
new file mode 100644
index 0000000..38165c8
--- /dev/null
+++ b/architectural-grid1/src/components/NotebooksView.tsx
@@ -0,0 +1,1532 @@
+import React from 'react';
+import {
+ Plus,
+ Search,
+ Share2,
+ Pin,
+ ChevronRight,
+ ChevronUp,
+ ChevronDown,
+ ArrowLeft,
+ MoreVertical,
+ Sparkles,
+ Tag as TagIcon,
+ X,
+ BookOpen,
+ Edit3,
+ Eye,
+ Trash2,
+ Wind,
+ FileText,
+ Paperclip,
+ Loader2,
+ MessageSquare,
+ Menu,
+ Globe,
+ Link2,
+ Folder,
+ LayoutGrid,
+ List,
+ Table,
+ CheckSquare,
+ GraduationCap,
+ PanelRight
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Note, Carnet, Tag, Attachment, Flashcard } from '../types';
+import { SlashMenu } from './SlashMenu';
+import { parseDocument } from '../services/geminiService';
+import { v4 as uuidv4 } from 'uuid';
+import { LivingBlock } from './LivingBlock';
+import { BlockPicker } from './BlockPicker';
+import { NotebookInfoSidebar } from './NotebookInfoSidebar';
+import { ModernBlockNoteEditor } from './ModernBlockNoteEditor';
+
+interface NotebooksViewProps {
+ activeNoteId: string | null;
+ activeCarnet: Carnet | undefined;
+ filteredNotes: Note[];
+ activeNote: Note | undefined;
+ setActiveNoteId: (id: string | null) => void;
+ togglePin: (id: string) => void;
+ setShowNewNoteModal: (show: boolean) => void;
+ isAISidebarOpen: boolean;
+ setIsAISidebarOpen: (open: boolean) => void;
+ selectedTagIds: string[];
+ setSelectedTagIds: (ids: string[]) => void;
+ allNotes: Note[];
+ activeCarnetId: string;
+ setShowNewCarnetModal: (show: boolean, parentId?: string, isRenaming?: boolean, carnetId?: string) => void;
+ onDeleteNote: (id: string) => void;
+ onBrainstormNote: (note: Note) => void;
+ onUpdateNote?: (note: Note) => void;
+ onOpenSidebar?: () => void;
+ onSearchClick?: () => void;
+ wsConnected?: boolean;
+ broadcastLivingBlockUpdate?: (sourceNoteId: string, blockIndex: number, newText: string) => void;
+ carnets?: Carnet[]; // Optional carnets list
+ flashcards?: Flashcard[];
+ onTriggerReviewDeck?: (noteId: string) => void;
+ onGenerateFlashcards?: (noteId: string) => Promise;
+ isGeneratingFlashcards?: boolean;
+}
+
+export const NotebooksView: React.FC = ({
+ activeNoteId,
+ activeCarnet,
+ filteredNotes,
+ activeNote,
+ setActiveNoteId,
+ togglePin,
+ setShowNewNoteModal,
+ isAISidebarOpen,
+ setIsAISidebarOpen,
+ selectedTagIds,
+ setSelectedTagIds,
+ allNotes,
+ activeCarnetId,
+ setShowNewCarnetModal,
+ onDeleteNote,
+ onBrainstormNote,
+ onUpdateNote,
+ onOpenSidebar,
+ onSearchClick,
+ wsConnected = true,
+ broadcastLivingBlockUpdate,
+ carnets = [],
+ flashcards = [],
+ onTriggerReviewDeck,
+ onGenerateFlashcards,
+ isGeneratingFlashcards = false
+}) => {
+ const [isTagsExpanded, setIsTagsExpanded] = React.useState(false);
+ const [tagSearchQuery, setTagSearchQuery] = React.useState('');
+ const [isEditing, setIsEditing] = React.useState(false);
+ const [isNoteInfoOpen, setIsNoteInfoOpen] = React.useState(false);
+ const [slashMenu, setSlashMenu] = React.useState<{ isOpen: boolean; top: number; left: number } | null>(null);
+ const [isAnalyzing, setIsAnalyzing] = React.useState(null);
+ const [activeDocQnA, setActiveDocQnA] = React.useState(null);
+ const [isPickerOpen, setIsPickerOpen] = React.useState(false);
+ const [prefilledBlock, setPrefilledBlock] = React.useState<{ noteId: string; blockIndex: number } | null>(null);
+
+ // Flashcards state computations
+ const noteFlashcards = React.useMemo(() => {
+ if (!activeNote || !flashcards) return [];
+ return flashcards.filter(c => c.noteId === activeNote.id);
+ }, [flashcards, activeNote]);
+
+ const nextRecallText = React.useMemo(() => {
+ if (noteFlashcards.length === 0) return '';
+ let minDate = noteFlashcards[0].nextReviewDate;
+ noteFlashcards.forEach(c => {
+ if (c.nextReviewDate < minDate) {
+ minDate = c.nextReviewDate;
+ }
+ });
+
+ const diff = new Date(minDate).getTime() - Date.now();
+ if (diff <= 0) return "Dû aujourd'hui";
+ const days = Math.ceil(diff / (24 * 60 * 60 * 1000));
+ return `dans ${days}j`;
+ }, [noteFlashcards]);
+
+ // Vue Structurée states
+ const [viewType, setViewType] = React.useState<'notes' | 'tasks'>('notes');
+ const [layoutMode, setLayoutMode] = React.useState<'grid' | 'list' | 'table'>('list');
+ const [sortColumn, setSortColumn] = React.useState<'title' | 'carnet' | 'labels' | 'tasks' | 'modified' | null>(null);
+ const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc' | null>(null);
+
+ // Helper mapping french relative dates for architecture aesthetics
+ const getRelativeFrenchDate = (dateStr: string): string => {
+ if (dateStr.includes('Oct 26')) return 'il y a 2h';
+ if (dateStr.includes('Oct 27')) return 'il y a 1j';
+ if (dateStr.includes('Oct 24')) return 'il y a 3j';
+ if (dateStr.includes('Oct 25')) return 'il y a 2j';
+ if (dateStr.includes('Oct 22')) return 'il y a 5j';
+ if (dateStr.includes('Oct 23')) return 'il y a 4j';
+ if (dateStr.includes('Oct 28')) return 'il y a 10 min';
+
+ // Fallback calculation relative to May 24, 2026
+ try {
+ const d = new Date(dateStr);
+ const now = new Date("2026-05-24T07:18:49Z");
+ const diffTime = Math.abs(now.getTime() - d.getTime());
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays <= 1) return "aujourd'hui";
+ if (diffDays <= 2) return "hier";
+ if (diffDays <= 7) return `il y a ${diffDays}j`;
+ if (diffDays <= 30) return `il y a ${Math.floor(diffDays / 7)} sem.`;
+ return `il y a ${Math.floor(diffDays / 30)} mois`;
+ } catch (e) {
+ return dateStr;
+ }
+ };
+
+ // Aesthetic deterministic carnet colors
+ const getCarnetColor = (c: Carnet) => {
+ const colors = [
+ { bg: 'bg-[#A47148]/5 dark:bg-[#A47148]/10', border: 'border-[#A47148]/20', text: 'text-[#A47148]' },
+ { bg: 'bg-emerald-500/5 dark:bg-emerald-500/10', border: 'border-emerald-500/15', text: 'text-emerald-600 dark:text-emerald-400' },
+ { bg: 'bg-indigo-500/5 dark:bg-indigo-500/10', border: 'border-indigo-500/15', text: 'text-indigo-600 dark:text-indigo-400' },
+ { bg: 'bg-blue-500/5 dark:bg-blue-500/10', border: 'border-blue-500/15', text: 'text-blue-600 dark:text-blue-400' },
+ { bg: 'bg-amber-500/5 dark:bg-amber-500/10', border: 'border-amber-500/15', text: 'text-amber-600 dark:text-amber-400' },
+ { bg: 'bg-rose-500/5 dark:bg-rose-500/10', border: 'border-rose-500/15', text: 'text-rose-600 dark:text-rose-400' },
+ ];
+
+ if (c.type === 'Private') return colors[0];
+ if (c.type === 'Shared') return colors[2];
+
+ const idx = Math.abs(c.name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % colors.length;
+ return colors[idx];
+ };
+
+ // Extract tasks from markdown format: - [ ] Task name
+ const getNoteTasksStats = (content: string) => {
+ const lines = content.split('\n');
+ let total = 0;
+ let completed = 0;
+ lines.forEach(line => {
+ const match = line.match(/^\s*[-*]?\s*\[([ xX])\]\s*(.*)$/);
+ if (match) {
+ total++;
+ if (match[1].toLowerCase() === 'x') {
+ completed++;
+ }
+ }
+ });
+ return { completed, total };
+ };
+
+ interface TaskItem {
+ id: string;
+ noteId: string;
+ noteTitle: string;
+ text: string;
+ completed: boolean;
+ lineIndex: number;
+ }
+
+ const extractTasks = React.useMemo(() => {
+ const tasksList: TaskItem[] = [];
+ filteredNotes.forEach(note => {
+ const lines = note.content.split('\n');
+ lines.forEach((line, idx) => {
+ const match = line.match(/^\s*[-*]?\s*\[([ xX])\]\s*(.*)$/);
+ if (match) {
+ tasksList.push({
+ id: `${note.id}-${idx}`,
+ noteId: note.id,
+ noteTitle: note.title,
+ text: match[2].trim(),
+ completed: match[1].toLowerCase() === 'x',
+ lineIndex: idx
+ });
+ }
+ });
+ });
+ return tasksList;
+ }, [filteredNotes]);
+
+ const completedTasksCount = React.useMemo(() => {
+ return extractTasks.filter(t => t.completed).length;
+ }, [extractTasks]);
+
+ const handleToggleTask = (task: TaskItem) => {
+ const note = allNotes.find(n => n.id === task.noteId);
+ if (!note) return;
+ const lines = note.content.split('\n');
+ const line = lines[task.lineIndex];
+ if (line) {
+ const isNowCompleted = !task.completed;
+ const nextChar = isNowCompleted ? 'x' : ' ';
+ const updatedLine = line.replace(/\[([ xX])\]/, `[${nextChar}]`);
+ lines[task.lineIndex] = updatedLine;
+ const updatedNote = { ...note, content: lines.join('\n') };
+ if (onUpdateNote) {
+ onUpdateNote(updatedNote);
+ }
+ }
+ };
+
+ // Click handler to toggle sorting order
+ const handleSort = (field: 'title' | 'carnet' | 'labels' | 'tasks' | 'modified') => {
+ if (sortColumn !== field) {
+ setSortColumn(field);
+ setSortDirection('asc');
+ } else if (sortDirection === 'asc') {
+ setSortDirection('desc');
+ } else {
+ setSortColumn(null);
+ setSortDirection(null);
+ }
+ };
+
+ // Computes the sorted copy of filteredNotes
+ const sortedNotes = React.useMemo(() => {
+ if (!sortColumn || !sortDirection) return filteredNotes;
+
+ const notesCopy = [...filteredNotes];
+ return notesCopy.sort((a, b) => {
+ let valA: any = '';
+ let valB: any = '';
+
+ if (sortColumn === 'title') {
+ valA = (a.title || '').toLowerCase();
+ valB = (b.title || '').toLowerCase();
+ } else if (sortColumn === 'carnet') {
+ const parentA = carnets?.find(c => c.id === a.carnetId)?.name || '';
+ const parentB = carnets?.find(c => c.id === b.carnetId)?.name || '';
+ valA = parentA.toLowerCase();
+ valB = parentB.toLowerCase();
+ } else if (sortColumn === 'labels') {
+ valA = a.tags?.length || 0;
+ valB = b.tags?.length || 0;
+ } else if (sortColumn === 'tasks') {
+ const statsA = getNoteTasksStats(a.content);
+ const statsB = getNoteTasksStats(b.content);
+ valA = statsA.total > 0 ? (statsA.completed / statsA.total) : -1;
+ valB = statsB.total > 0 ? (statsB.completed / statsB.total) : -1;
+ } else if (sortColumn === 'modified') {
+ valA = new Date(a.date).getTime() || 0;
+ valB = new Date(b.date).getTime() || 0;
+ }
+
+ if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
+ if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
+ return 0;
+ });
+ }, [filteredNotes, sortColumn, sortDirection, carnets]);
+
+ const renderSortIndicator = (field: 'title' | 'carnet' | 'labels' | 'tasks' | 'modified') => {
+ if (sortColumn !== field) return null;
+ return sortDirection === 'asc' ? (
+
+ ) : (
+
+ );
+ };
+
+ const fileInputRef = React.useRef(null);
+ const titleInputRef = React.useRef(null);
+ const contentTextareaRef = React.useRef(null);
+
+ const backlinks = React.useMemo(() => {
+ if (!activeNote) return [];
+ const list: Array<{ note: Note; accessedAt: string }> = [];
+ allNotes.forEach(note => {
+ if (note.id === activeNote.id) return;
+ if (note.content.includes(`[[living-block:${activeNote.id}:`)) {
+ list.push({ note, accessedAt: "Récemment" });
+ }
+ });
+ return list;
+ }, [allNotes, activeNote]);
+
+ const memoryEchoBlock = React.useMemo(() => {
+ if (!activeNote) return null;
+ const otherNotes = allNotes.filter(n => n.id !== activeNote.id);
+ if (otherNotes.length === 0) return null;
+ let bestNote = otherNotes[0];
+ let maxOverlap = -1;
+ const currentTags = new Set(activeNote.tags?.map(t => t.id) || []);
+ otherNotes.forEach(note => {
+ const overlap = note.tags?.filter(t => currentTags.has(t.id)).length || 0;
+ if (overlap > maxOverlap) {
+ maxOverlap = overlap;
+ bestNote = note;
+ }
+ });
+ const lines = bestNote.content.split('\n').filter(line => line.trim().length > 30 && !line.startsWith('#') && !line.startsWith('[[living-block'));
+ if (lines.length === 0) return null;
+ const blockText = lines[0];
+ const blockIndex = bestNote.content.split('\n').indexOf(blockText);
+ return {
+ noteId: bestNote.id,
+ noteTitle: bestNote.title || "Note reliée",
+ text: blockText,
+ blockIndex
+ };
+ }, [activeNote, allNotes]);
+
+ const handleSelectBlock = (sourceNoteId: string, blockIndex: number) => {
+ setIsPickerOpen(false);
+ setPrefilledBlock(null);
+ if (!activeNote) return;
+
+ const blockCode = `\n[[living-block:${sourceNoteId}:${blockIndex}]]\n`;
+
+ // Insert blockCode into textarea at cursor or end of text
+ const textarea = contentTextareaRef.current;
+ if (textarea) {
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ const originalText = textarea.value;
+ const newText = originalText.substring(0, start) + blockCode + originalText.substring(end);
+
+ const updatedNote = { ...activeNote, content: newText };
+ onUpdateNote?.(updatedNote);
+
+ // Update defaultValue/value of textarea
+ textarea.value = newText;
+ } else {
+ // Fallback: append to end of content
+ const updatedNote = {
+ ...activeNote,
+ content: activeNote.content + blockCode
+ };
+ onUpdateNote?.(updatedNote);
+ }
+ };
+
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file || !activeNote) return;
+
+ const newAttachment: Attachment = {
+ id: uuidv4(),
+ name: file.name,
+ type: file.name.endsWith('.pdf') ? 'pdf' : (file.name.endsWith('.docx') ? 'docx' : 'other'),
+ url: URL.createObjectURL(file), // Local preview url
+ isProcessed: false
+ };
+
+ const updatedNote = {
+ ...activeNote,
+ attachments: [...(activeNote.attachments || []), newAttachment]
+ };
+
+ onUpdateNote?.(updatedNote);
+
+ // Auto-analyze
+ setIsAnalyzing(newAttachment.id);
+ const content = await parseDocument(newAttachment.url, newAttachment.name);
+
+ const processedAttachment = { ...newAttachment, content, isProcessed: true };
+ const finalNote = {
+ ...activeNote,
+ attachments: [...(activeNote.attachments || []), processedAttachment]
+ };
+ onUpdateNote?.(finalNote);
+ setIsAnalyzing(null);
+ };
+
+ const handleEditorKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === '/') {
+ const selection = window.getSelection();
+ if (selection && selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0);
+ const rect = range.getBoundingClientRect();
+ setSlashMenu({
+ isOpen: true,
+ top: rect.bottom + window.scrollY,
+ left: rect.left + window.scrollX
+ });
+ }
+ }
+ };
+
+ const insertCommand = (type: string) => {
+ console.log(`Command selected: ${type}`);
+ setSlashMenu(null);
+ if (type === 'embed') {
+ setPrefilledBlock(null);
+ setIsPickerOpen(true);
+ }
+ };
+
+ const availableTags = React.useMemo(() => {
+ const carnetNotes = allNotes.filter(n => n.carnetId === activeCarnetId);
+ const tagsMap = new Map();
+ carnetNotes.forEach(note => {
+ note.tags?.forEach(tag => {
+ tagsMap.set(tag.id, tag);
+ });
+ });
+ return Array.from(tagsMap.values()).sort((a, b) => {
+ // AI tags first, then alphabetical
+ if (a.type === 'ai' && b.type !== 'ai') return -1;
+ if (a.type !== 'ai' && b.type === 'ai') return 1;
+ return a.label.localeCompare(b.label);
+ });
+ }, [allNotes, activeCarnetId]);
+
+ const visibleTags = React.useMemo(() => {
+ let filtered = availableTags;
+ if (tagSearchQuery) {
+ filtered = availableTags.filter(t =>
+ t.label.toLowerCase().includes(tagSearchQuery.toLowerCase())
+ );
+ } else if (!isTagsExpanded) {
+ filtered = availableTags.slice(0, 10);
+ // Ensure selected tags are always visible even if not in the first 10
+ selectedTagIds.forEach(id => {
+ if (!filtered.find(t => t.id === id)) {
+ const tag = availableTags.find(t => t.id === id);
+ if (tag) filtered.push(tag);
+ }
+ });
+ }
+ return filtered;
+ }, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds]);
+
+ const toggleTag = (tagId: string) => {
+ if (selectedTagIds.includes(tagId)) {
+ setSelectedTagIds(selectedTagIds.filter(id => id !== tagId));
+ } else {
+ setSelectedTagIds([...selectedTagIds, tagId]);
+ }
+ };
+
+ if (!activeNoteId) {
+ return (
+
+
+
+
+ {viewType === 'tasks' ? (
+ // ================== FLAT TASKS LIST VIEW ==================
+
+
+
+
+
+ Ravi de vous revoir — Tâches d'Architecture
+
+
+
+ {extractTasks.length} tâches · {completedTasksCount} complétées
+
+
+
+ {extractTasks.length > 0 ? (
+
+
+ {extractTasks.map((task, idx) => (
+
+
+ handleToggleTask(task)}
+ className={`w-5 h-5 rounded-md border flex items-center justify-center transition-all cursor-pointer shrink-0
+ ${task.completed
+ ? 'bg-[#A47148] border-[#A47148] text-white'
+ : 'border-[#D5D2CD] dark:border-neutral-700 hover:border-accent bg-transparent'}`}
+ >
+ {task.completed && ✓ }
+
+
+
+ {task.text}
+
+
+
+
+
+ Note : {task.noteTitle}
+
+ setActiveNoteId(task.noteId)}
+ className="p-1.5 rounded-full hover:bg-black/5 dark:hover:bg-white/5 text-concrete hover:text-ink transition-all cursor-pointer"
+ title="Ouvrir la note source"
+ >
+
+
+
+
+ ))}
+
+
+ ) : (
+ // ================== EMPTY STATE TASKS ==================
+
+
+
+
+
Aucune tâche dans ce carnet
+
+ Ajoutez des lignes de tâches au format - [ ] Ma tâche dans vos notes pour commencer.
+
+
+ )}
+
+ ) : layoutMode === 'grid' ? (
+ // ================== CARDS/GRID VIEW ==================
+
+
+ {sortedNotes.map((note, index) => {
+ const stats = getNoteTasksStats(note.content);
+ return (
+
setActiveNoteId(note.id)}
+ className="bg-white dark:bg-[#1C1C1E]/40 border border-[#D5D2CD]/40 dark:border-neutral-800/40 rounded-2xl overflow-hidden hover:shadow-md hover:border-accent/30 transition-all duration-300 group/card cursor-pointer flex flex-col"
+ >
+ {/* Thumbnail with black white architectural look */}
+
+
+ {note.isPinned && (
+
+ )}
+ {stats.total > 0 && (
+
+ {stats.completed}/{stats.total} ✓
+
+ )}
+
+
+
+
+
+ {note.tags?.slice(0, 2).map(tag => (
+
+ {tag.type === 'ai' && }
+ {tag.label}
+
+ ))}
+
+
+
+ {note.title}
+
+
+
+ {note.content.replace(/[-*]?\s*\[([ xX])\]\s*(.*)/g, '').trim().substring(0, 110)}...
+
+
+
+
+
{note.date}
+
+
{
+ e.stopPropagation();
+ onBrainstormNote(note);
+ }}
+ className="p-1 px-2 rounded-full hover:bg-ochre/10 text-ochre transition-all flex items-center gap-1 cursor-pointer"
+ title="Brainstorm this concept"
+ >
+
+
+
{
+ e.stopPropagation();
+ togglePin(note.id);
+ }}
+ className="p-1 px-2 rounded-full hover:bg-slate-100 dark:hover:bg-white/10 text-ink transition-all cursor-pointer"
+ >
+
+
+
{
+ e.stopPropagation();
+ onDeleteNote(note.id);
+ }}
+ className="p-1 px-2 rounded-full hover:bg-rose-50 text-rose-500 transition-all cursor-pointer"
+ >
+
+
+
+
+
+
+ );
+ })}
+
+
+ {sortedNotes.length === 0 && (
+
+
This notebook is waiting for its first vision.
+
setShowNewNoteModal(true)}
+ className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all cursor-pointer"
+ >
+ Begin Drawing
+
+
+ )}
+
+ ) : layoutMode === 'table' ? (
+ // ================== STRUCTURING TABLE VIEW ==================
+
+
+
+
+
+ {/* Titre */}
+ handleSort('title')}
+ className="w-[40%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
+ >
+
+ Titre
+ {renderSortIndicator('title')}
+
+
+ {/* Carnet */}
+ handleSort('carnet')}
+ className="w-[15%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
+ >
+
+ Carnet
+ {renderSortIndicator('carnet')}
+
+
+ {/* Labels */}
+ handleSort('labels')}
+ className="w-[20%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
+ >
+
+ Labels
+ {renderSortIndicator('labels')}
+
+
+ {/* Tâches */}
+ handleSort('tasks')}
+ className="w-[12%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
+ >
+
+ Tâches
+ {renderSortIndicator('tasks')}
+
+
+ {/* Modifié */}
+ handleSort('modified')}
+ className="w-[13%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
+ >
+
+ Modifié
+ {renderSortIndicator('modified')}
+
+
+
+
+
+ {sortedNotes.map((note) => {
+ const carnet = carnets?.find(c => c.id === note.carnetId);
+ const noteColor = carnet ? getCarnetColor(carnet) : { bg: 'bg-zinc-500/5', border: 'border-zinc-500/10', text: 'text-zinc-500' };
+ const stats = getNoteTasksStats(note.content);
+
+ return (
+ setActiveNoteId(note.id)}
+ className="h-10 hover:bg-neutral-50 dark:hover:bg-white/[0.02] cursor-pointer transition-all duration-150 relative group"
+ >
+ {/* Titre */}
+
+
+ {note.isPinned &&
}
+
{note.title || "Note sans titre"}
+
+
+ {/* Carnet */}
+
+ {carnet ? (
+
+ {carnet.name}
+
+ ) : (
+ —
+ )}
+
+ {/* Labels */}
+
+
+ {note.tags?.slice(0, 3).map(tag => (
+
+ {tag.label}
+
+ ))}
+ {note.tags && note.tags.length > 3 && (
+
+ +{note.tags.length - 3}
+
+ )}
+
+
+ {/* Tâches */}
+
+ {stats.total > 0 ? (
+
+ {stats.completed}/{stats.total} ✓
+
+ ) : (
+ —
+ )}
+
+ {/* Modifié */}
+
+ {getRelativeFrenchDate(note.date)}
+
+
+ );
+ })}
+
+
+
+
+ {sortedNotes.length === 0 && (
+
+
This notebook is waiting for its first vision.
+
setShowNewNoteModal(true)}
+ className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all cursor-pointer"
+ >
+ Begin Drawing
+
+
+ )}
+
+ ) : (
+ // ================== ORIGINAL DETAILED EDITORIAL LIST VIEW ==================
+
+ {filteredNotes.map((note, index) => (
+
setActiveNoteId(note.id)}
+ >
+
+
+ {note.isPinned && }
+ {note.title}
+
+
+
{
+ e.stopPropagation();
+ onBrainstormNote(note);
+ }}
+ className="p-2 rounded-full opacity-0 group-hover:opacity-60 hover:opacity-100 hover:bg-ochre/10 text-ochre transition-all cursor-pointer"
+ title="Brainstorm this concept"
+ >
+
+
+
{
+ e.stopPropagation();
+ togglePin(note.id);
+ }}
+ className={`p-2 rounded-full transition-all cursor-pointer ${note.isPinned ? 'text-amber-600 bg-amber-50' : 'opacity-0 group-hover:opacity-60 hover:bg-slate-100 dark:hover:bg-white/10 text-ink'}`}
+ >
+
+
+
{
+ e.stopPropagation();
+ onDeleteNote(note.id);
+ }}
+ className="p-2 rounded-full opacity-0 group-hover:opacity-60 hover:opacity-100 hover:bg-rose-50 text-rose-500 transition-all cursor-pointer"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {note.tags?.map(tag => (
+
+ {tag.type === 'ai' && }
+ {tag.label}
+
+ ))}
+
+
+ {note.content}
+
+
Read more
+
+
+
+ ))}
+ {filteredNotes.length === 0 && (
+
+
This notebook is waiting for its first vision.
+
setShowNewNoteModal(true)}
+ className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all cursor-pointer"
+ >
+ Begin Drawing
+
+
+ )}
+
+ )}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
setActiveNoteId(null)}
+ className="flex items-center gap-2 text-ink hover:opacity-60 transition-opacity"
+ title="Retour aux documents"
+ >
+
+
+
+
+
fileInputRef.current?.click()}
+ className="flex items-center gap-2 px-3 py-1.5 rounded-full border border-border text-muted-ink hover:text-ink transition-all"
+ title="Add attachment"
+ >
+
+
+
+
onBrainstormNote(activeNote!)}
+ className="p-2 rounded-full border border-ochre/30 text-ochre hover:bg-ochre/5 transition-all cursor-pointer"
+ title="Brainstorm IA"
+ >
+
+
+
onGenerateFlashcards?.(activeNote!.id)}
+ disabled={isGeneratingFlashcards}
+ className="p-2 rounded-full border border-accent/30 text-accent hover:bg-accent/5 transition-all disabled:opacity-50 cursor-pointer"
+ title={isGeneratingFlashcards ? 'Génération...' : 'Révisions & Flashcards'}
+ >
+ {isGeneratingFlashcards ? (
+
+ ) : (
+
+ )}
+
+
{
+ if (isEditing && activeNote) {
+ // Save edits
+ const updatedNote = {
+ ...activeNote,
+ title: titleInputRef.current?.value || activeNote.title,
+ content: contentTextareaRef.current?.value || activeNote.content
+ };
+ onUpdateNote?.(updatedNote);
+ }
+ setIsEditing(!isEditing);
+ }}
+ className={`p-2 rounded-full border transition-all duration-300
+ ${isEditing ? 'bg-accent text-white border-accent shadow-lg shadow-accent/20' : 'border-border text-ink hover:bg-slate-50'}`}
+ title={isEditing ? 'Visualiser' : 'Modifier'}
+ >
+ {isEditing ? : }
+
+
togglePin(activeNoteId!)}
+ className={`p-2 rounded-full transition-all ${activeNote?.isPinned ? 'text-amber-600 bg-amber-50 dark:bg-ochre/10' : 'text-muted-ink hover:text-ink'}`}
+ title={activeNote?.isPinned ? "Unpin note" : "Pin note"}
+ >
+
+
+ {/* Native simulated connection status indicator toggle */}
+
{
+ window.dispatchEvent(new CustomEvent('toggle-websocket-simulate'));
+ }}
+ className={`p-2 rounded-full transition-all shrink-0 border cursor-pointer
+ ${wsConnected
+ ? 'bg-blue-500/5 text-blue-600 border-blue-500/10 hover:bg-rose-500/10 hover:text-rose-600 hover:border-rose-500/15'
+ : 'bg-amber-500/5 text-amber-600 border-amber-500/10 hover:bg-blue-500/10 hover:text-blue-600 hover:border-blue-500/15'}`}
+ title={wsConnected ? "WebSocket : En ligne (Cliquez pour déconnecter)" : "WebSocket : Hors ligne (Cliquez pour reconnecter)"}
+ >
+
+
+
setIsAISidebarOpen(!isAISidebarOpen)}
+ className={`p-2 rounded-full border transition-all duration-300
+ ${isAISidebarOpen ? 'bg-ink text-paper border-ink' : 'border-border text-ink hover:bg-white/50 dark:hover:bg-white/5'}`}
+ title="Assistant IA"
+ >
+
+
+
+
+
+
+
+
+
setIsNoteInfoOpen(!isNoteInfoOpen)}
+ className={`p-2 rounded-full border transition-all duration-300 cursor-pointer shrink-0
+ ${isNoteInfoOpen ? 'bg-ink text-paper border-ink dark:bg-white dark:text-ink' : 'border-border text-ink hover:bg-white/50 dark:hover:bg-white/5'}`}
+ title="Informations & Versions de la note"
+ >
+
+
+
+
+
+
+
+ {slashMenu?.isOpen && (
+ insertCommand(type)}
+ onClose={() => setSlashMenu(null)}
+ />
+ )}
+
+
+ {activeNote?.isClipped && (
+
+
+
+
+
+ {activeNote.clipFavicon ? (
+
{ (e.target as any).src = 'https://www.google.com/s2/favicons?domain=google.com'; }}
+ />
+ ) : (
+
+ )}
+
+ {activeNote.clipSourceUrl}
+
+
+
+
+
+ Source web
+
+ Capturé : {activeNote.clipDate || activeNote.date}
+
+
+
+ )}
+
+
+
+ {activeCarnet?.name}
+
+ {activeNote?.date}
+ {activeNote?.isClipped && (
+ <>
+
+ Source web
+ >
+ )}
+
+
+ {isEditing ? (
+
+ ) : (
+
+ {activeNote?.title}
+
+ )}
+
+
+ {activeNote?.tags?.map(tag => (
+
+ {tag.type === 'ai' &&
}
+ {tag.label}
+ {tag.type === 'ai' && (
+
+ )}
+
+ ))}
+
+
+ {/* Flashcards alert state indicator (E) */}
+ {activeNote && noteFlashcards.length > 0 && (
+
+
+
+ {noteFlashcards.length} flashcards générées · Prochain rappel : {nextRecallText}
+
+
onTriggerReviewDeck?.(activeNote.id)}
+ className="text-accent hover:underline font-bold text-xs shrink-0 cursor-pointer"
+ >
+ Réviser maintenant
+
+
+ )}
+
+
+
+
+
+
+
+
+ {activeNote?.attachments && activeNote.attachments.length > 0 && (
+
+
Pièces jointes ({activeNote.attachments.length})
+
+ {activeNote.attachments.map(att => (
+
+
+
+
+
+
+
{att.name}
+
{att.type}
+
+
+
+ {isAnalyzing === att.id ? (
+
+ ) : (
+ setActiveDocQnA(att)}
+ className="p-2 hover:bg-accent/10 text-accent rounded-lg transition-colors opacity-0 group-hover:opacity-100"
+ title="Converser avec ce document"
+ >
+
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+ {activeNote && (
+
{})}
+ allNotes={allNotes}
+ />
+ )}
+
+
+ {/* Memory Echo Section */}
+ {memoryEchoBlock && (
+
+
+
+
+ Memory Echo (Affinité Sémantique)
+
+
+ 92% affinité sémétrique
+
+
+
+
+ « {memoryEchoBlock.text.substring(0, 150)}... »
+
+
+
+
Passage détecté dans : {memoryEchoBlock.noteTitle}
+
+ setActiveNoteId(memoryEchoBlock.noteId)}
+ className="text-concrete hover:text-ink font-bold transition-all hover:underline"
+ >
+ Voir la connexion
+
+ {
+ setPrefilledBlock({ noteId: memoryEchoBlock.noteId, blockIndex: memoryEchoBlock.blockIndex });
+ setIsPickerOpen(true);
+ }}
+ className="flex items-center gap-1.5 text-blue-600 dark:text-blue-400 font-extrabold hover:underline"
+ >
+
+ Embedder ce passage
+
+
+
+
+ )}
+
+ {/* Backlinks panel */}
+ {backlinks.length > 0 && (
+
+
+
+
Rétroliens & Intégrations Sémantiques
+
+
+
+
Embeddé comme Living Block dans {backlinks.length} note{backlinks.length > 1 ? 's' : ''} :
+
+ {backlinks.map(({ note, accessedAt }) => (
+
setActiveNoteId(note.id)}
+ className="p-3.5 bg-white dark:bg-zinc-800/20 border border-black/[0.04] dark:border-white/[0.04] rounded-xl hover:bg-black/[0.02] dark:hover:bg-white/[0.02] hover:border-accent/20 transition-all cursor-pointer flex items-center justify-between pr-4 group"
+ >
+
+
+
+
+ {note.title || "Note sans titre"}
+
+
+ Carnet : {carnets?.find(c => c.id === note.carnetId)?.name || "Général"}
+
+
+
+
+ Accès : {accessedAt}
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+ {/* Document Q&A Overlay */}
+
+ {activeDocQnA && (
+
+
+ {/* Document Preview (Mock) */}
+
+
+
+
+
+
+
{activeDocQnA.name}
+
DOCUMENT SOURCE ANALYSÉ
+
+
+
+
+ {activeDocQnA.content || "Analyse du contenu en cours..."}
+
+
+
+
+ {/* Chat Side */}
+
+
+
+
+
Expert Document
+
+
setActiveDocQnA(null)} className="p-2 hover:bg-slate-50 dark:hover:bg-white/5 rounded-full text-concrete">
+
+
+
+
+
+
+
+ Bonjour ! J'ai analysé ce document. Posez-moi n'importe quelle question sur son contenu, les chiffres clés ou les concepts abordés.
+
+
+
+
+
+
+
+
+ )}
+
+
+
setIsPickerOpen(false)}
+ currentNote={activeNote}
+ allNotes={allNotes}
+ carnets={carnets}
+ onSelectBlock={handleSelectBlock}
+ prefilledBlock={prefilledBlock}
+ />
+
+ setIsNoteInfoOpen(false)}
+ activeNote={activeNote}
+ notes={allNotes}
+ carnets={carnets}
+ onOpenNote={setActiveNoteId}
+ onUpdateNote={onUpdateNote}
+ />
+
+ );
+};
diff --git a/architectural-grid1/src/components/RevisionView.tsx b/architectural-grid1/src/components/RevisionView.tsx
new file mode 100644
index 0000000..6546765
--- /dev/null
+++ b/architectural-grid1/src/components/RevisionView.tsx
@@ -0,0 +1,665 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ GraduationCap,
+ Layers,
+ ArrowLeft,
+ ChevronLeft,
+ ChevronRight,
+ RotateCcw,
+ CheckCircle2,
+ X,
+ Inbox,
+ BookOpen,
+ Calendar,
+ Sparkles,
+ Award
+} from 'lucide-react';
+import { Note, Flashcard, FlashcardDeck, FlashcardEvaluation } from '../types';
+
+interface RevisionViewProps {
+ notes: Note[];
+ flashcards: Flashcard[];
+ onUpdateFlashcards: (updated: Flashcard[]) => void;
+ onSelectNote: (noteId: string) => void;
+ onOpenSidebar?: () => void;
+ initialActiveDeckId?: string | null;
+ onClearActiveDeckId?: () => void;
+}
+
+export const RevisionView: React.FC = ({
+ notes,
+ flashcards,
+ onUpdateFlashcards,
+ onSelectNote,
+ onOpenSidebar,
+ initialActiveDeckId,
+ onClearActiveDeckId
+}) => {
+ // Active states
+ const [activeDeckId, setActiveDeckId] = useState(initialActiveDeckId || null);
+ const [isSessionActive, setIsSessionActive] = useState(false);
+ const [isSessionFinished, setIsSessionFinished] = useState(false);
+
+ // Active review states
+ const [currentCardIndex, setCurrentCardIndex] = useState(0);
+ const [isFlipped, setIsFlipped] = useState(false);
+ const [sessionCards, setSessionCards] = useState([]);
+ const [sessionHistory, setSessionHistory] = useState>({});
+ const [onlyFailedCardsSession, setOnlyFailedCardsSession] = useState(false);
+
+ // Sync initial deck selection from outer prop/reminder
+ useEffect(() => {
+ if (initialActiveDeckId) {
+ setActiveDeckId(initialActiveDeckId);
+ // Auto-trigger session
+ const deckCards = flashcards.filter(c => c.noteId === initialActiveDeckId);
+ if (deckCards.length > 0) {
+ setSessionCards([...deckCards]);
+ setCurrentCardIndex(0);
+ setIsFlipped(false);
+ setIsSessionActive(true);
+ setIsSessionFinished(false);
+ setSessionHistory({});
+ }
+ }
+ }, [initialActiveDeckId, flashcards]);
+
+ // Compute Decks based on current flashcards and notes
+ const decks = useMemo(() => {
+ const deckMap = new Map();
+ flashcards.forEach(card => {
+ if (!deckMap.has(card.noteId)) {
+ deckMap.set(card.noteId, []);
+ }
+ deckMap.get(card.noteId)!.push(card);
+ });
+
+ const list: FlashcardDeck[] = [];
+ deckMap.forEach((cardsInDeck, noteId) => {
+ const parentNote = notes.find(n => n.id === noteId);
+ if (!parentNote || parentNote.isDeleted) return;
+
+ // Find min nextReviewDate
+ let minDate = cardsInDeck[0]?.nextReviewDate || new Date().toISOString();
+ cardsInDeck.forEach(c => {
+ if (c.nextReviewDate < minDate) {
+ minDate = c.nextReviewDate;
+ }
+ });
+
+ // Mastery score: portion of mastered/sure cards in last evaluation
+ const totalCards = cardsInDeck.length;
+ let masteredCount = 0;
+ cardsInDeck.forEach(c => {
+ if (c.mastered) masteredCount++;
+ });
+
+ list.push({
+ noteId,
+ title: parentNote.title,
+ cardsCount: totalCards,
+ nextReviewDate: minDate,
+ masteryScore: totalCards > 0 ? masteredCount / totalCards : 0,
+ cards: cardsInDeck
+ });
+ });
+
+ // Sort decks: first those that need review (past nextReviewDate), then alphabetical
+ const nowStr = new Date().toISOString();
+ return list.sort((a, b) => {
+ const aNeeds = a.nextReviewDate <= nowStr;
+ const bNeeds = b.nextReviewDate <= nowStr;
+ if (aNeeds && !bNeeds) return -1;
+ if (!aNeeds && bNeeds) return 1;
+ return a.title.localeCompare(b.title);
+ });
+ }, [flashcards, notes]);
+
+ const activeDeck = useMemo(() => {
+ return decks.find(d => d.noteId === activeDeckId);
+ }, [decks, activeDeckId]);
+
+ // Launch review session for a deck
+ const handleStartReview = (noteId: string, failedOnly = false) => {
+ const deck = decks.find(d => d.noteId === noteId);
+ if (!deck) return;
+
+ let cardsToReview = [...deck.cards];
+ if (failedOnly) {
+ // Filter for cards graded as 'fail' or 'hesitant' in session history, or simply subset of session cards
+ const failedIds = Object.keys(sessionHistory).filter(id => sessionHistory[id] === 'fail');
+ cardsToReview = deck.cards.filter(c => failedIds.includes(c.id));
+ if (cardsToReview.length === 0) {
+ // Fallback to active session's rated fail
+ cardsToReview = deck.cards.filter(c => sessionHistory[c.id] === 'fail');
+ }
+ setOnlyFailedCardsSession(true);
+ } else {
+ setOnlyFailedCardsSession(false);
+ }
+
+ if (cardsToReview.length === 0) return;
+
+ // Shuffle cards for better learning cognitive effect
+ const shuffled = [...cardsToReview].sort(() => Math.random() - 0.5);
+
+ setActiveDeckId(noteId);
+ setSessionCards(shuffled);
+ setCurrentCardIndex(0);
+ setIsFlipped(false);
+ setIsSessionActive(true);
+ setIsSessionFinished(false);
+ setSessionHistory({});
+ };
+
+ const handleCardFlip = () => {
+ setIsFlipped(!isFlipped);
+ };
+
+ // Keyboard support during review
+ useEffect(() => {
+ if (!isSessionActive || isSessionFinished) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.code === 'Space') {
+ e.preventDefault();
+ handleCardFlip();
+ } else if (isFlipped) {
+ if (e.key === '1') {
+ handleEvaluate('fail');
+ } else if (e.key === '2') {
+ handleEvaluate('hesitant');
+ } else if (e.key === '3') {
+ handleEvaluate('sure');
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isSessionActive, isSessionFinished, isFlipped, currentCardIndex, sessionCards]);
+
+ // Simple Spaced Repetition Logic (Leitner system variation)
+ const handleEvaluate = (evaluation: FlashcardEvaluation) => {
+ const currentCard = sessionCards[currentCardIndex];
+ if (!currentCard) return;
+
+ // Record evaluation in session context
+ setSessionHistory(prev => ({
+ ...prev,
+ [currentCard.id]: evaluation
+ }));
+
+ // Calculate new intervals
+ let interval = currentCard.intervalDays || 1;
+ let ease = currentCard.easeFactor || 2.5;
+ let mastered = currentCard.mastered || false;
+
+ if (evaluation === 'fail') {
+ interval = 1; // back to review tomorrow
+ ease = Math.max(1.3, ease - 0.2);
+ mastered = false;
+ } else if (evaluation === 'hesitant') {
+ interval = Math.max(2, Math.floor(interval * 1.2));
+ mastered = false;
+ } else { // sure
+ interval = Math.ceil(interval * ease);
+ ease = Math.min(3.5, ease + 0.15);
+ mastered = true;
+ }
+
+ // Calculate next review date
+ const nextDate = new Date();
+ nextDate.setDate(nextDate.getDate() + interval);
+
+ // Build historical entry
+ const historyItem = {
+ reviewedAt: new Date().toISOString(),
+ evaluation
+ };
+
+ const updatedCard: Flashcard = {
+ ...currentCard,
+ intervalDays: interval,
+ nextReviewDate: nextDate.toISOString(),
+ easeFactor: ease,
+ mastered,
+ history: [...(currentCard.history || []), historyItem]
+ };
+
+ // Propagate up to global storage
+ const updatedGlobal = flashcards.map(c => c.id === currentCard.id ? updatedCard : c);
+ onUpdateFlashcards(updatedGlobal);
+
+ // Update in-place session cards to preserve intermediate updates
+ setSessionCards(prev => prev.map((c, i) => i === currentCardIndex ? updatedCard : c));
+
+ // Progress flow
+ if (currentCardIndex < sessionCards.length - 1) {
+ setTimeout(() => {
+ setCurrentCardIndex(prev => prev + 1);
+ setIsFlipped(false);
+ }, 300);
+ } else {
+ setTimeout(() => {
+ setIsSessionFinished(true);
+ }, 300);
+ }
+ };
+
+ const handleNext = () => {
+ if (currentCardIndex < sessionCards.length - 1) {
+ setCurrentCardIndex(currentCardIndex + 1);
+ setIsFlipped(false);
+ }
+ };
+
+ const handlePrev = () => {
+ if (currentCardIndex > 0) {
+ setCurrentCardIndex(currentCardIndex - 1);
+ setIsFlipped(false);
+ }
+ };
+
+ const handleExitSession = () => {
+ setIsSessionActive(false);
+ setIsSessionFinished(false);
+ onClearActiveDeckId?.();
+ };
+
+ // Statistics summaries
+ const finishedStats = useMemo(() => {
+ if (sessionCards.length === 0) return { sureCount: 0, hesitantCount: 0, failCount: 0, percentage: 0 };
+
+ let sureCount = 0;
+ let hesitantCount = 0;
+ let failCount = 0;
+
+ sessionCards.forEach(c => {
+ const evaluation = sessionHistory[c.id];
+ if (evaluation === 'sure') sureCount++;
+ else if (evaluation === 'hesitant') hesitantCount++;
+ else if (evaluation === 'fail') failCount++;
+ });
+
+ const totalRated = Object.keys(sessionHistory).length || 1;
+ const percentage = Math.round((sureCount / totalRated) * 100);
+
+ return {
+ sureCount,
+ hesitantCount,
+ failCount,
+ percentage
+ };
+ }, [sessionCards, sessionHistory]);
+
+ const formattingDate = (isoStr: string) => {
+ const diff = new Date(isoStr).getTime() - Date.now();
+ if (diff <= 0) return 'Dû aujourd\'hui';
+ const days = Math.ceil(diff / (24 * 60 * 60 * 1000));
+ return `Dans ${days}j`;
+ };
+
+ return (
+
+
+ {/* 1. Header Toolbar */}
+
+
+ {onOpenSidebar && (
+
+
+
+ )}
+
+ {isSessionActive ? (
+
+
+ Abandonner
+
+ ) : (
+
+
+
Focal de Révision
+
+ )}
+
+
+ {isSessionActive && activeDeck && (
+
+ deck : {activeDeck.title}
+
+ )}
+
+
+ {/* 2. Main Display Area */}
+
+
+
+ {/* SCREEN A: Decks Collection list view */}
+ {!isSessionActive && (
+
+
+
Decks de Révision Active
+
+ Révisez vos connaissances de manière ciblée grâce au système d'espacement algorithmique Leitner. L’apprentissage actif commence ici.
+
+
+
+ {decks.length > 0 ? (
+
+ {decks.map(deck => {
+ const nowStr = new Date().toISOString();
+ const dueCount = deck.cards.filter(c => c.nextReviewDate <= nowStr).length;
+
+ return (
+
+
+
+
+
+ {deck.title}
+
+
+
+ {deck.cardsCount} cartes de mémoire
+
+
+
+ {/* Circular progress bar rendering */}
+
+
+
+
+
+
+ {Math.round(deck.masteryScore * 100)}%
+
+
+
+
+
+ {dueCount > 0 ? (
+
+ {dueCount} à réviser
+
+ ) : (
+
+ À jour
+
+ )}
+
+
+ Prochain : {formattingDate(deck.nextReviewDate)}
+
+
+
+
+
+ onSelectNote(deck.noteId)}
+ className="flex-1 h-9 flex items-center justify-center text-[10.5px] uppercase tracking-wider font-bold text-muted-ink hover:text-ink dark:text-dark-muted dark:hover:text-dark-ink border border-border rounded-lg bg-white/50 dark:bg-transparent hover:bg-slate-50 dark:hover:bg-white/5 transition-colors"
+ >
+ Ouvrir note
+
+ handleStartReview(deck.noteId)}
+ className="flex-1 h-9 flex items-center justify-center bg-accent text-white text-[10.5px] uppercase tracking-wider font-bold rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1.5 shadow-sm shadow-accent/10"
+ >
+
+ Réviser
+
+
+
+ );
+ })}
+
+ ) : (
+
+
+
+
+
Aucun deck de flashcards
+
+ Démarrez votre apprentissage en générant des flashcards à l'aide de l'IA directement depuis la barre d'outils de vos notes architecturales.
+
+
onSelectNote('n1')}
+ className="h-11 px-6 bg-ink dark:bg-ochre text-paper dark:text-ink rounded-xl text-xs font-bold uppercase tracking-widest hover:opacity-90 transition-all flex items-center gap-2"
+ >
+
+ Essayer sur la Note "Grid Systems"
+
+
+ )}
+
+ )}
+
+ {/* SCREEN B: Active Deck session review state */}
+ {isSessionActive && !isSessionFinished && (
+
+ {/* Header navigation bar */}
+
+
+
+ Précédent
+
+
+
+ {currentCardIndex + 1} / {sessionCards.length}
+
+
+
+ Suivant
+
+
+
+
+ {/* Centered Flashcard */}
+
+
+
+ {/* RECTO - Front */}
+
+
+ Recto : Question
+ Cliquer pour tourner
+
+
+
+
+ {sessionCards[currentCardIndex]?.question}
+
+
+
+
+ Raccourci : [Espace] pour révéler la réponse
+
+
+
+ {/* VERSO - Back */}
+
+
+ Verso : Réponse
+ Duo Mémoire
+
+
+
+
+ {sessionCards[currentCardIndex]?.answer}
+
+
+
+
+ Raccourcis : [1] Raté, [2] Hésitant, [3] Sûr
+
+
+
+
+
+
+ {/* Grading Buttons - Rendered after Verso is revealed */}
+
+
+ {isFlipped ? (
+
+ { e.stopPropagation(); handleEvaluate('fail'); }}
+ className="flex-1 flex flex-col items-center justify-center h-14 rounded-2xl border border-rust/10 bg-rust/10 font-black text-rust cursor-pointer hover:bg-rust/15 transition-all text-xs"
+ >
+ Raté
+ Touche 1
+
+
+ { e.stopPropagation(); handleEvaluate('hesitant'); }}
+ className="flex-1 flex flex-col items-center justify-center h-14 rounded-2xl border border-ochre/15 bg-ochre/10 font-black text-ochre cursor-pointer hover:bg-ochre/15 transition-all text-xs"
+ >
+ Hésitant
+ Touche 2
+
+
+ { e.stopPropagation(); handleEvaluate('sure'); }}
+ className="flex-1 flex flex-col items-center justify-center h-14 rounded-2xl border border-sage/15 bg-sage/10 font-black text-sage cursor-pointer hover:bg-sage/15 transition-all text-xs"
+ >
+ Sûr
+ Touche 3
+
+
+ ) : (
+
+ Révéler la réponse (Espace)
+
+ )}
+
+
+
+ )}
+
+ {/* SCREEN C: Finishing dashboard view with Donut Chart and actions */}
+ {isSessionFinished && (
+
+
+
+
Félicitations !
+
+ Vous venez de finir votre session de révision de la note active.
+
+
+
+ {/* Custom SVG Donut Chart showing score */}
+
+
+
+
+
+
+
+ {finishedStats.percentage}%
+
+
Sûr de soi
+
+
+
+ {/* Core Analytics parameters (Stats) */}
+
+
+
{sessionCards.length}
+
Révisées
+
+
+
{finishedStats.failCount}
+
À revoir
+
+
+
{finishedStats.sureCount}
+
Maîtrisées
+
+
+
+
+
+ Retour aux decks
+
+ {finishedStats.failCount > 0 && (
+ handleStartReview(activeDeckId!, true)}
+ className="flex-1 h-11 bg-[#8F4C38] text-white rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-[#8F4C38]/95 transition-all flex items-center justify-center gap-1.5 shadow-sm cursor-pointer"
+ >
+
+ Ré-réviser les ratées
+
+ )}
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/SearchModal.tsx b/architectural-grid1/src/components/SearchModal.tsx
new file mode 100644
index 0000000..cb5b79c
--- /dev/null
+++ b/architectural-grid1/src/components/SearchModal.tsx
@@ -0,0 +1,611 @@
+import React, { useState, useEffect, useMemo, useRef } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ Search,
+ ChevronLeft,
+ ChevronRight,
+ Plus,
+ Bookmark,
+ Layers,
+ FileText,
+ CheckCircle,
+ HelpCircle,
+ X,
+ CornerDownRight,
+ Folder,
+ Sliders,
+ Sparkles,
+ Command,
+ Settings
+} from 'lucide-react';
+import { Note, Carnet } from '../types';
+
+interface SearchModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ notes: Note[];
+ carnets: Carnet[];
+ onSelectNote: (noteId: string) => void;
+}
+
+interface SearchMatch {
+ id: string; // Unique match identifier
+ noteId: string;
+ noteTitle: string;
+ path: string;
+ type: 'document' | 'heading' | 'paragraph' | 'list';
+ headingLevel?: number;
+ text: string;
+ matchedText: string;
+ lineIndex: number;
+}
+
+export const SearchModal: React.FC = ({
+ isOpen,
+ onClose,
+ notes,
+ carnets,
+ onSelectNote
+}) => {
+ const [query, setQuery] = useState('');
+ const [useRegex, setUseRegex] = useState(false);
+ const [caseSensitive, setCaseSensitive] = useState(false);
+ const [includeChildDocs, setIncludeChildDocs] = useState(true);
+ const [searchInTrash, setSearchInTrash] = useState(false);
+ const [savedQueries, setSavedQueries] = useState(['block', 'siyuan', 'guide']);
+
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const inputRef = useRef(null);
+ const listRef = useRef(null);
+
+ // Focus input on launch
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => inputRef.current?.focus(), 50);
+ }
+ }, [isOpen]);
+
+ // Handle global keybindings in modal
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ onClose();
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setSelectedIndex(prev => Math.min(prev + 1, filteredMatches.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setSelectedIndex(prev => Math.max(prev - 1, 0));
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (filteredMatches[selectedIndex]) {
+ const m = filteredMatches[selectedIndex];
+ onSelectNote(m.noteId);
+ onClose();
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen, selectedIndex]);
+
+ // Helper: reconstruct carnet path
+ const getCarnetPath = (carnetId: string): string => {
+ const segments: string[] = [];
+ let current = carnets.find(c => c.id === carnetId);
+ while (current) {
+ segments.unshift(current.name);
+ current = current.parentId ? carnets.find(c => c.id === current.parentId) : undefined;
+ }
+ return segments.join('/');
+ };
+
+ // Safe term escape for RegExp
+ const escapeRegExp = (string: string) => {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ };
+
+ // Perform multi-match search logic across document titles and contents
+ const filteredMatches = useMemo(() => {
+ if (!query.trim()) return [];
+
+ const matches: SearchMatch[] = [];
+ const searchRegex = (() => {
+ try {
+ const flag = caseSensitive ? '' : 'i';
+ const pattern = useRegex ? query : escapeRegExp(query);
+ return new RegExp(pattern, flag);
+ } catch (e) {
+ return null; // Handle partial regex input gracefully
+ }
+ })();
+
+ if (!searchRegex) return [];
+
+ // Filter notes depending on trash status
+ const targetNotes = notes.filter(n => searchInTrash ? n.isDeleted : !n.isDeleted);
+
+ targetNotes.forEach(note => {
+ const notePath = getCarnetPath(note.carnetId);
+ const fullPath = notePath ? `${notePath}/${note.title}` : note.title;
+
+ // 1. Check Title match
+ if (searchRegex.test(note.title)) {
+ matches.push({
+ id: `${note.id}-title`,
+ noteId: note.id,
+ noteTitle: note.title,
+ path: fullPath,
+ type: 'document',
+ text: note.title,
+ matchedText: note.title,
+ lineIndex: -1
+ });
+ }
+
+ // 2. Parse Content blocks / lines
+ if (note.content) {
+ const lines = note.content.split('\n');
+ lines.forEach((line, index) => {
+ const trimmed = line.trim();
+ if (!trimmed) return;
+
+ if (searchRegex.test(trimmed)) {
+ let type: 'heading' | 'paragraph' | 'list' = 'paragraph';
+ let headingLevel = undefined;
+ let displayVal = trimmed;
+
+ // Classify content structure elements
+ if (trimmed.startsWith('#')) {
+ type = 'heading';
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
+ if (headingMatch) {
+ headingLevel = headingMatch[1].length;
+ displayVal = headingMatch[2];
+ }
+ } else if (/^[-*+]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
+ type = 'list';
+ displayVal = trimmed.replace(/^[-*+\d.]+\s+/, '');
+ }
+
+ matches.push({
+ id: `${note.id}-line-${index}`,
+ noteId: note.id,
+ noteTitle: note.title,
+ path: fullPath,
+ type,
+ headingLevel,
+ text: trimmed,
+ matchedText: displayVal,
+ lineIndex: index
+ });
+ }
+ });
+ }
+ });
+
+ return matches;
+ }, [notes, query, useRegex, caseSensitive, searchInTrash, carnets]);
+
+ // Ensure index remains in bounds when matches array updates
+ useEffect(() => {
+ setSelectedIndex(0);
+ }, [query]);
+
+ // Toggle saving criteria
+ const handleSaveCriteria = () => {
+ if (query.trim() && !savedQueries.includes(query.trim())) {
+ setSavedQueries(prev => [...prev, query.trim()]);
+ }
+ };
+
+ const handleRemoveCriteria = () => {
+ setSavedQueries(prev => prev.filter(q => q !== query.trim()));
+ };
+
+ // Count distinct notes involved in match list
+ const docMatchesCount = useMemo(() => {
+ const uniqueNoteIds = new Set(filteredMatches.map(m => m.noteId));
+ return uniqueNoteIds.size;
+ }, [filteredMatches]);
+
+ const activeMatch = filteredMatches[selectedIndex];
+
+ // Dynamically load document content with visual query highlights
+ const highlightedNotePreviewContent = useMemo(() => {
+ if (!activeMatch) return null;
+ const currentNote = notes.find(n => n.id === activeMatch.noteId);
+ if (!currentNote) return null;
+
+ if (!query.trim()) return currentNote.content;
+
+ try {
+ const flag = caseSensitive ? 'g' : 'gi';
+ const searchPattern = useRegex ? query : escapeRegExp(query);
+ const highlightRegex = new RegExp(`(${searchPattern})`, flag);
+
+ // Return content split by line to let us format block matches neatly
+ const lines = (currentNote.content || '').split('\n');
+
+ // Let's frame the match around the matched line for contextual proximity
+ const targetIndex = activeMatch.lineIndex >= 0 ? activeMatch.lineIndex : 0;
+ const startLine = Math.max(0, targetIndex - 3);
+ const endLine = Math.min(lines.length - 1, targetIndex + 5);
+
+ return (
+
+ {startLine > 0 && (
+
...
+ )}
+ {lines.slice(startLine, endLine + 1).map((line, idx) => {
+ const absoluteIdx = startLine + idx;
+ const isMatchLine = absoluteIdx === targetIndex;
+ const hasMatches = highlightRegex.test(line);
+
+ // Reconstruct highlighted segments
+ const segments = line.split(highlightRegex);
+
+ return (
+
+
+ {absoluteIdx + 1}
+
+
+
+ {hasMatches ? (
+ segments.map((seg, sIdx) => {
+ const matchesPattern = highlightRegex.test(seg);
+ return matchesPattern ? (
+
+ {seg}
+
+ ) : (
+ seg
+ );
+ })
+ ) : (
+ line
+ )}
+
+
+ );
+ })}
+ {endLine < lines.length - 1 && (
+
...
+ )}
+
+ );
+ } catch (e) {
+ return {currentNote.content}
;
+ }
+ }, [activeMatch, notes, query, useRegex, caseSensitive]);
+
+ // Render text segment highlight in results row items
+ const renderHighlightedRowText = (text: string) => {
+ if (!query.trim()) return text;
+ try {
+ const flag = caseSensitive ? 'gi' : 'gi';
+ const searchPattern = useRegex ? query : escapeRegExp(query);
+ const highlightRegex = new RegExp(`(${searchPattern})`, flag);
+ const segments = text.split(highlightRegex);
+
+ return (
+
+ {segments.map((seg, sIdx) => {
+ const isMatch = highlightRegex.test(seg);
+ return isMatch ? (
+
+ {seg}
+
+ ) : (
+ seg
+ );
+ })}
+
+ );
+ } catch (e) {
+ return text;
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* TOP Advanced Search Bar Row */}
+
+
+
+
+
setQuery(e.target.value)}
+ placeholder="Rechercher des documents ou des blocs de texte..."
+ className="w-full text-sm pl-10 pr-24 py-2.5 rounded-xl border border-border/70 dark:border-zinc-800/80 bg-white/85 dark:bg-[#1C1C1C] text-ink dark:text-dark-ink placeholder-concrete/50 outline-none focus:border-accent"
+ />
+
+ {/* Config Quick Badges */}
+
+ setCaseSensitive(!caseSensitive)}
+ className={`px-1.5 py-1 text-[9.5px] font-bold rounded-md hover:bg-black/5 dark:hover:bg-white/5 uppercase select-none transition-colors
+ ${caseSensitive ? 'text-accent bg-accent/5' : 'text-concrete'}`}
+ title="Respecter la casse (Aa)"
+ >
+ Aa
+
+ setUseRegex(!useRegex)}
+ className={`px-1.5 py-1 text-[9.5px] font-bold rounded-md hover:bg-black/5 dark:hover:bg-white/5 uppercase select-none transition-colors
+ ${useRegex ? 'text-accent bg-accent/5' : 'text-concrete'}`}
+ title="Activer Regex (.*)"
+ >
+ .*
+
+
+
+
+
+
+
+ {/* Quick saved criteria filter tags */}
+ {savedQueries.length > 0 && (
+
+
Favoris:
+
+ {savedQueries.map(sq => (
+ setQuery(sq)}
+ className={`px-2 py-0.5 rounded-md border text-[9.5px] font-medium transition-all hover:border-accent
+ ${query === sq
+ ? 'bg-accent/10 border-accent text-accent'
+ : 'bg-white dark:bg-zinc-800 border-border/40 text-muted-ink'}`}
+ >
+ {sq}
+
+ ))}
+
+
+ )}
+
+
+ {/* UTILITY BAR Row -> Match statistics with action links */}
+
+
+ {/* Arrow Switchers */}
+
+ setSelectedIndex(prev => Math.max(0, prev - 1))}
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-concrete disabled:opacity-40"
+ >
+
+
+
+ {filteredMatches.length > 0 ? `${selectedIndex + 1}/${filteredMatches.length}` : '0/0'}
+
+ setSelectedIndex(prev => Math.min(filteredMatches.length - 1, prev + 1))}
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-concrete disabled:opacity-40"
+ >
+
+
+
+
+
+ {filteredMatches.length > 0
+ ? `Trouvé ${filteredMatches.length} occurrences dans ${docMatchesCount} documents`
+ : query.trim() ? "Aucun élément ne correspond" : "Saisissez votre requête"}
+
+
+
+ {/* Toolbar Action Links */}
+
+ {query.trim() && (
+
+ {savedQueries.includes(query.trim()) ? 'Supprimer favori' : 'Sauvegarder recherche'}
+
+ )}
+
+
+ setIncludeChildDocs(e.target.checked)}
+ className="rounded border-border/60 text-accent focus:ring-accent w-3 h-3"
+ />
+ Sous-docs inclus
+
+
+
+ setSearchInTrash(e.target.checked)}
+ className="rounded border-border/60 text-accent focus:ring-accent w-3 h-3"
+ />
+ Corbeille incluse
+
+
+
+
+ {/* DUAL SECTION LAYOUT */}
+
+
+ {/* Left Section: Scrollable matches list */}
+
+
+ {filteredMatches.map((m, idx) => {
+ const isSelected = idx === selectedIndex;
+ return (
+
setSelectedIndex(idx)}
+ onDoubleClick={() => {
+ onSelectNote(m.noteId);
+ onClose();
+ }}
+ className={`p-2.5 rounded-xl cursor-pointer text-left select-none relative group/item transition-all flex flex-col gap-1 border
+ ${isSelected
+ ? 'bg-white dark:bg-zinc-800 shadow-md border-amber-500/30'
+ : 'border-transparent hover:bg-black/[0.02] dark:hover:bg-white/[0.02]/30'}`}
+ >
+ {/* Selection overlay accent */}
+ {isSelected && (
+
+ )}
+
+
+
+ {/* Element classifier badges */}
+ {m.type === 'document' && (
+
+ )}
+ {m.type === 'heading' && (
+
+ H{m.headingLevel || ''}
+
+ )}
+ {m.type === 'list' && (
+
+ LIST
+
+ )}
+ {m.type === 'paragraph' && (
+
+ TXT
+
+ )}
+
+
+ {m.noteTitle}
+
+
+
+
+ {/* Highlighted snippet row content */}
+
+ {renderHighlightedRowText(m.matchedText)}
+
+
+ {/* Breadcrumb row path */}
+
+ {m.path}
+
+
+ );
+ })}
+
+ {filteredMatches.length === 0 && (
+
+
+
+ {query.trim() ? "Aucun bloc ou doc ne correspond à cette recherche." : "Taper pour obtenir des résultats instantanés."}
+
+
+ )}
+
+
+
+ {/* Right Section: Scrollable content preview card with visual highlighted markers */}
+
+ {activeMatch ? (
+
+
+ {/* Breadcrumb locator line */}
+
+
+
+ {activeMatch.path}
+
+
+
+ {/* Document focus heading title */}
+
+
+ {activeMatch.noteTitle}
+
+
APERÇU CONTEXTUEL DU BLOC
+
+
+ {/* Dynamic document contents highlighted and framed */}
+
+ {highlightedNotePreviewContent}
+
+
+
+ {/* Quick Actions trigger buttons */}
+
+ {
+ onSelectNote(activeMatch.noteId);
+ onClose();
+ }}
+ className="px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black hover:scale-102 active:scale-98 text-xs font-semibold rounded-xl flex items-center gap-2 transition-all shadow-sm"
+ >
+
+ Ouvrir dans l'éditeur
+
+
+ ID: {activeMatch.noteId.slice(0, 6)}...
+
+
+
+ ) : (
+
+
+
+
Aperçu du document
+
Sélectionnez un résultat de recherche de la colonne et explorez immédiatement son contenu sémantique.
+
+
+ )}
+
+
+
+
+ {/* BOTTOM Status Keyboard shortcuts hint footer bar */}
+
+
+ ↑↓ naviguer
+ Entrée ouvrir
+ Double clic ouvrir
+ Échap fermer
+
+
+
+
+ Momento Search OS v2.3
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/SettingsView.tsx b/architectural-grid1/src/components/SettingsView.tsx
new file mode 100644
index 0000000..c5433c6
--- /dev/null
+++ b/architectural-grid1/src/components/SettingsView.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import { SettingsTab } from '../types';
+import { SettingsHeader } from './settings/SettingsHeader';
+import { GeneralTab } from './settings/GeneralTab';
+import { AITab } from './settings/AITab';
+import { AppearanceTab } from './settings/AppearanceTab';
+import { BillingTab } from './settings/BillingTab';
+import { ProfileTab } from './settings/ProfileTab';
+
+interface SettingsViewProps {
+ activeSettingsTab: SettingsTab;
+ setActiveSettingsTab: (tab: SettingsTab) => void;
+ accentColor: string;
+ onAccentColorChange: (color: string) => void;
+ onLogout: () => void;
+ onOpenSidebar?: () => void;
+}
+
+export const SettingsView: React.FC = ({
+ activeSettingsTab,
+ setActiveSettingsTab,
+ accentColor,
+ onAccentColorChange,
+ onLogout,
+ onOpenSidebar
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+ {activeSettingsTab === 'general' && (
+
+ )}
+
+ {activeSettingsTab === 'ai' && (
+
+ )}
+
+ {activeSettingsTab === 'billing' && (
+
+ )}
+
+ {activeSettingsTab === 'appearance' && (
+
+ )}
+
+ {activeSettingsTab === 'profile' && (
+
+ )}
+
+ {['data', 'mcp', 'about'].includes(activeSettingsTab) && (
+
+
+ ?
+
+
+
Section en développement
+
Le module {activeSettingsTab} sera disponible prochainement.
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/Sidebar.tsx b/architectural-grid1/src/components/Sidebar.tsx
new file mode 100644
index 0000000..a4a86a7
--- /dev/null
+++ b/architectural-grid1/src/components/Sidebar.tsx
@@ -0,0 +1,875 @@
+import React from 'react';
+import {
+ Plus,
+ Archive,
+ Settings,
+ ChevronRight,
+ BookOpen,
+ Bot,
+ Microscope,
+ Activity,
+ Pin,
+ Moon,
+ Sun,
+ Bell,
+ Lock,
+ Edit3,
+ Trash2,
+ Users,
+ Clock,
+ GripVertical,
+ Wind,
+ Network,
+ Home,
+ Sparkles,
+ LogOut,
+ ChevronDown,
+ Folder,
+ FolderOpen,
+ FileText,
+ Search,
+ BookMarked,
+ User,
+ ExternalLink,
+ ChevronUp,
+ HelpCircle,
+ EyeOff,
+ Layers,
+ Scissors,
+ Chrome,
+ Crown,
+ ArrowRight,
+ GraduationCap
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { NavigationView, Carnet, Note, SettingsTab, Flashcard } from '../types';
+
+interface NoteLinkProps {
+ note: Note;
+ isActive: boolean;
+ onClick: () => void;
+}
+
+const NoteLink: React.FC = ({ note, isActive, onClick }) => (
+
+
+
+ {note.title || "Note sans titre"}
+
+ {note.isPinned && }
+
+);
+
+interface SidebarItemProps {
+ carnet: Carnet;
+ isActive: boolean;
+ notes: Note[];
+ activeNoteId: string | null;
+ onCarnetClick: () => void;
+ onNoteClick: (noteId: string) => void;
+ onAddSubCarnet: () => void;
+ onRename: () => void;
+ onDelete: () => void;
+ children?: React.ReactNode;
+ level: number;
+ isExpanded: boolean;
+ toggleExpand: () => void;
+ onMove?: (draggedId: string, targetId?: string) => void;
+}
+
+const SidebarItem: React.FC = ({
+ carnet,
+ isActive,
+ notes,
+ activeNoteId,
+ onCarnetClick,
+ onNoteClick,
+ onAddSubCarnet,
+ onRename,
+ onDelete,
+ children,
+ level,
+ isExpanded,
+ toggleExpand,
+ onMove
+}) => {
+ const hasChildren = React.Children.count(children) > 0 || notes.length > 0;
+
+ return (
+
+
+ {/* Subtle Drag Handle */}
+
+
+
+
+ {/* Hierarchy Guide Line */}
+ {level > 0 && (
+
+ )}
+ {level > 0 && (
+
+ )}
+
+
+ {hasChildren ? (
+
{
+ e.stopPropagation();
+ toggleExpand();
+ }}
+ className="p-0.5 hover:bg-ink/5 dark:hover:bg-white/5 rounded transition-colors text-concrete"
+ >
+
+
+
+
+ ) : (
+
// Spacer for alignment
+ )}
+
+
{
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ e.currentTarget.classList.add('bg-accent/5', 'ring-1', 'ring-accent/20');
+ }}
+ onDragLeave={(e) => {
+ e.currentTarget.classList.remove('bg-accent/5', 'ring-1', 'ring-accent/20');
+ }}
+ onDrop={(e) => {
+ e.preventDefault();
+ e.currentTarget.classList.remove('bg-accent/5', 'ring-1', 'ring-accent/20');
+ const draggedId = e.dataTransfer.getData('carnetId');
+ if (draggedId && draggedId !== carnet.id) {
+ onMove?.(draggedId, carnet.id);
+ }
+ }}
+ draggable
+ onDragStart={(e) => {
+ e.dataTransfer.setData('carnetId', carnet.id);
+ e.dataTransfer.effectAllowed = 'move';
+ const ghost = e.currentTarget.cloneNode(true) as HTMLElement;
+ ghost.style.position = 'absolute';
+ ghost.style.top = '-1000px';
+ ghost.style.opacity = '0.5';
+ document.body.appendChild(ghost);
+ e.dataTransfer.setDragImage(ghost, 0, 0);
+ setTimeout(() => document.body.removeChild(ghost), 0);
+ }}
+ >
+ {isActive && (
+
+ )}
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {carnet.name}
+
+ {carnet.isPrivate && }
+
+
+
+
{
+ e.stopPropagation();
+ onAddSubCarnet();
+ }}
+ className="p-0.5 hover:bg-ink/10 dark:hover:bg-white/10 rounded transition-all text-concrete hover:text-ink"
+ title="Ajouter un sous-carnet"
+ >
+
+
+
{
+ e.stopPropagation();
+ onRename();
+ }}
+ className="p-0.5 hover:bg-ink/10 dark:hover:bg-white/10 rounded transition-all text-concrete hover:text-ink"
+ title="Renommer"
+ >
+
+
+
{
+ e.stopPropagation();
+ onDelete();
+ }}
+ className="p-0.5 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-all text-concrete hover:text-red-500"
+ title="Supprimer"
+ >
+
+
+
+ {notes.length > 0 && (
+
+ {notes.length}
+
+ )}
+
+
+
+
+
+
+ {isExpanded && (
+
+
+ {/* Vertical line for nested content path */}
+
+
+
+ {children}
+ {notes.map(note => (
+
onNoteClick(note.id)}
+ />
+ ))}
+ {notes.length === 0 && !React.Children.count(children) && (
+
+ Vide
+
+ )}
+
+
+
+ )}
+
+
+ );
+};
+
+interface SidebarProps {
+ activeView: NavigationView;
+ isSidebarOpen: boolean;
+ setIsSidebarOpen: (val: boolean) => void;
+ isDarkMode: boolean;
+ setIsDarkMode: (val: boolean) => void;
+ setActiveView: (view: NavigationView) => void;
+ setActiveSettingsTab?: (tab: SettingsTab) => void;
+ carnets: Carnet[];
+ notes: Note[];
+ activeCarnetId: string;
+ activeNoteId: string | null;
+ setActiveCarnetId: (id: string) => void;
+ setActiveNoteId: (id: string | null) => void;
+ setShowNewCarnetModal: (show: boolean, parentId?: string, isRenaming?: boolean, carnetId?: string) => void;
+ onDeleteCarnet: (id: string) => void;
+ onMoveCarnet: (draggedId: string, targetId?: string) => void;
+ onGoHome: () => void;
+ onLogout: () => void;
+ flashcards?: Flashcard[];
+ onSelectReviewDeck?: (noteId: string) => void;
+}
+
+export const Sidebar: React.FC = ({
+ activeView,
+ isSidebarOpen,
+ setIsSidebarOpen,
+ isDarkMode,
+ setIsDarkMode,
+ setActiveView,
+ setActiveSettingsTab,
+ carnets,
+ notes,
+ activeCarnetId,
+ activeNoteId,
+ setActiveCarnetId,
+ setActiveNoteId,
+ setShowNewCarnetModal,
+ onDeleteCarnet,
+ onMoveCarnet,
+ onGoHome,
+ onLogout,
+ flashcards,
+ onSelectReviewDeck
+}) => {
+ const [expandedIds, setExpandedIds] = React.useState>(new Set(['4', '1', '2', '3'])); // Default expand key guides
+ const [collapsedSections, setCollapsedSections] = React.useState>(new Set());
+ const [searchQuery, setSearchQuery] = React.useState('');
+
+ const toggleSection = (id: string) => {
+ const newSet = new Set(collapsedSections);
+ if (newSet.has(id)) newSet.delete(id);
+ else newSet.add(id);
+ setCollapsedSections(newSet);
+ };
+
+ const toggleExpand = (id: string) => {
+ const newSet = new Set(expandedIds);
+ if (newSet.has(id)) newSet.delete(id);
+ else newSet.add(id);
+ setExpandedIds(newSet);
+ };
+
+ // Safe filtration based on searches
+ const filteredCarnets = React.useMemo(() => {
+ if (!searchQuery) return carnets;
+ const q = searchQuery.toLowerCase();
+ return carnets.filter(c =>
+ c.name.toLowerCase().includes(q) ||
+ notes.some(n => n.carnetId === c.id && n.title.toLowerCase().includes(q))
+ );
+ }, [carnets, searchQuery, notes]);
+
+ const activeNote = React.useMemo(() => {
+ if (!activeNoteId) return null;
+ return notes.find(n => n.id === activeNoteId);
+ }, [notes, activeNoteId]);
+
+ // Extract outline markdown headings dynamically for the currently active note
+ const headings = React.useMemo(() => {
+ if (!activeNote || !activeNote.content) return [];
+ const lines = activeNote.content.split('\n');
+ const hs: { text: string; level: number }[] = [];
+ lines.forEach(line => {
+ const trimmed = line.trim();
+ const hMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
+ if (hMatch) {
+ hs.push({
+ level: hMatch[1].length,
+ text: hMatch[2].replace(/\[.*?\]\(.*?\)/g, '').replace(/[*_`]/g, '').trim()
+ });
+ }
+ });
+ return hs.slice(0, 10);
+ }, [activeNote]);
+
+ const renderCarnetTree = (parentId: string | undefined = undefined, level: number = 0) => {
+ return filteredCarnets
+ .filter(c => c.parentId === parentId && !c.isDeleted)
+ .map(carnet => {
+ const carnetNotes = notes.filter(n => n.carnetId === carnet.id && !n.isDeleted);
+ return (
+ toggleExpand(carnet.id)}
+ onAddSubCarnet={() => {
+ if (!expandedIds.has(carnet.id)) toggleExpand(carnet.id);
+ setShowNewCarnetModal(true, carnet.id);
+ }}
+ onRename={() => {
+ setShowNewCarnetModal(true, undefined, true, carnet.id);
+ }}
+ onDelete={() => {
+ onDeleteCarnet(carnet.id);
+ }}
+ onCarnetClick={() => {
+ setActiveCarnetId(carnet.id);
+ setActiveNoteId(null);
+ // Auto expand when clicking
+ if (!expandedIds.has(carnet.id)) toggleExpand(carnet.id);
+ }}
+ onNoteClick={(id) => {
+ setActiveCarnetId(carnet.id);
+ setActiveNoteId(id);
+ }}
+ onMove={onMoveCarnet}
+ >
+ {renderCarnetTree(carnet.id, level + 1)}
+
+ );
+ });
+ };
+
+ return (
+ <>
+
+ {isSidebarOpen && (
+ setIsSidebarOpen(false)}
+ className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[60] lg:hidden"
+ />
+ )}
+
+
+
+
+ {/* Column 1: Ultra Narrow Left Utility Active-Rail Bar -> Identical to Ribbon in SiYuan */}
+
+ {/* Top Stack: Logo & View Shortcuts */}
+
+ {/* Visual SiYuan branding card */}
+
{ onGoHome(); setIsSidebarOpen(false); }}
+ className="w-9 h-9 bg-accent hover:rotate-6 active:scale-95 flex items-center justify-center rounded-xl shadow-md transition-all cursor-pointer mb-2"
+ title="Aller à la page d'accueil"
+ >
+ M
+
+
+ {/* Tab items list */}
+
+ {[
+ { id: 'notebooks', label: 'Feuilles / Docs', icon:
},
+ { id: 'graph', label: 'Knowledge Map', icon:
},
+ { id: 'revision', label: 'Révisions / Decks', icon:
},
+ { id: 'agents', label: 'Agents IA Lab', icon:
},
+ { id: 'reminders', label: 'Rappels & Alertes', icon:
},
+ ].map(item => {
+ const isSel = activeView === item.id || (item.id === 'agents' && ['brainstorm', 'insights', 'temporal'].includes(activeView));
+ return (
+
{
+ setActiveView(item.id as any);
+ }}
+ className={`w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group
+ ${isSel
+ ? 'bg-accent/10 text-accent border border-accent/25'
+ : 'text-concrete hover:text-ink dark:hover:text-dark-ink hover:bg-black/[0.03] dark:hover:bg-white/[0.03]'}`}
+ >
+ {/* Visual status pin */}
+ {isSel && (
+
+ )}
+ {item.icon}
+
+ {/* Tooltip */}
+
+ {item.label}
+
+
+ );
+ })}
+
+
+
+ {/* Bottom Stack: Trash, Light Mode, Settings, Logout */}
+
+ {/* TRASH DISCIPLINE: Promoted directly on the sidebar utility ribbon for quick accessible storage management */}
+
{
+ setActiveView('trash');
+ }}
+ className={`w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group
+ ${activeView === 'trash'
+ ? 'bg-rose-500/10 text-rose-500 border border-rose-500/25'
+ : 'text-concrete hover:text-rose-500 hover:bg-rose-500/5'}`}
+ >
+ {activeView === 'trash' && (
+
+ )}
+
+ {notes.some(n => n.isDeleted) && (
+
+ )}
+
+ Corbeille / Corbeille vide
+
+
+
+ {/* Shared */}
+
{
+ setActiveView('shared');
+ }}
+ className={`w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group
+ ${activeView === 'shared'
+ ? 'bg-[#E3EBFB] text-sky-600 dark:bg-white/10 dark:text-sky-400'
+ : 'text-concrete hover:text-sky-500 hover:bg-sky-500/5'}`}
+ >
+ {activeView === 'shared' && (
+
+ )}
+
+
+ Partagé
+
+
+
+ {/* Web Clipper Simulator Trigger */}
+
{
+ window.dispatchEvent(new CustomEvent('toggle-clipper-simulator'));
+ }}
+ className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-cyan-500 hover:bg-cyan-500/5 transition-all relative group"
+ >
+
+
+ Clipper Simulé
+
+
+
+ {/* Appearance Theme Switcher */}
+
setIsDarkMode(!isDarkMode)}
+ className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-ink dark:hover:text-dark-ink hover:bg-black/[0.03] dark:hover:bg-white/[0.03] transition-all relative group"
+ >
+ {isDarkMode ? : }
+
+ {isDarkMode ? "Mode clair" : "Mode sombre"}
+
+
+
+ {/* Settings Panel */}
+
{
+ setActiveView('settings');
+ setActiveSettingsTab?.('general');
+ }}
+ className={`w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group
+ ${activeView === 'settings'
+ ? 'bg-accent/10 text-accent border border-accent/25'
+ : 'text-concrete hover:text-ink dark:text-concrete hover:bg-black/[0.03] dark:hover:bg-white/[0.03]'}`}
+ >
+
+
+ Paramètres
+
+
+
+ {/* Logout button */}
+
+
+
+ Déconnexion
+
+
+
+
+
+ {/* Column 2: Large details zone (266px width) for list details - Dynamic depending on Ribbon view */}
+
+
+ {/* Render notebook list detail content */}
+ {activeView === 'notebooks' && (
+
+ {/* Header */}
+
+
+
+
Documents
+
+
setShowNewCarnetModal(true)}
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded transition-all text-concrete hover:text-ink"
+ title="Nouveau carnet parent"
+ >
+
+
+
+
+ {/* Simple search bar as seen in standard file trees */}
+
+ setSearchQuery(e.target.value)}
+ placeholder="Rechercher doc..."
+ className="w-full text-[11px] pl-7 pr-3 py-1.5 rounded-lg border border-border/60 bg-white/70 dark:bg-zinc-800 placeholder-concrete/50 outline-none focus:border-accent transition-colors text-ink dark:text-dark-ink"
+ />
+
+ {searchQuery && (
+ setSearchQuery('')}
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[9px] uppercase font-bold text-concrete hover:text-ink"
+ >
+ X
+
+ )}
+
+
+ {/* Hierarchical list of documents */}
+
+
+ {renderCarnetTree()}
+
+
+
+ {/* IA Usage & Upgrade Section */}
+
+
+
+
+
+ Utilisation de l'IA
+
+ 49 / 50 restants
+
+
+
+
+
{
+ setActiveView('settings');
+ setActiveSettingsTab?.('billing');
+ }}
+ className="w-full h-[28px] mt-1 flex items-center justify-between px-2.5 bg-accent/5 hover:bg-accent/10 hover:text-accent border border-accent/10 hover:border-accent/20 rounded-lg text-[10px] font-bold text-accent transition-all group cursor-pointer"
+ >
+
+
+ Passer au Plan Pro
+
+
+
+
+
+
+ )}
+
+ {/* Render intelligence modules */}
+ {activeView === 'agents' && (
+
+
+
+
+
Intelligence OS
+
+
+
+ {[
+ { id: 'brainstorm', label: 'Brainstorm Wave', desc: 'Génération d ideas rhizomatique', icon:
},
+ { id: 'insights', label: 'Réseau Sémantique', desc: 'Cartographie de clusters DBSCAN', icon:
},
+ { id: 'temporal', label: 'Temporal Forecast', desc: 'Chronologie et prévisions', icon:
},
+ ].map(sub => (
+
setActiveView(sub.id as any)}
+ className="w-full text-left p-3 rounded-xl border border-border/30 hover:border-accent/30 bg-white dark:bg-zinc-800/50 hover:shadow-xs transition-all flex items-start gap-3 group"
+ >
+
+ {sub.icon}
+
+
+
{sub.label}
+
{sub.desc}
+
+
+ ))}
+
+
+
+ {/* Pack quota discovery */}
+
+
+ Pack Découverte IA
+ 49 restants
+
+
+
+
+ )}
+
+ {/* Reminders section list view */}
+ {activeView === 'reminders' && (
+
+
+
+
Rappels Actifs
+
+
+
+
+
Aucun rappel pour le moment.
+
+
+ )}
+
+ {/* Flashcards / Révisions panel view inside Column 2 of Sidebar */}
+ {activeView === 'revision' && (
+
+
+
+
Decks Révision
+
+
+
+ {(() => {
+ const deckNotesList: { noteId: string; title: string; count: number; mastery: number }[] = [];
+ const cardGroups: Record
= {};
+ (flashcards || []).forEach(card => {
+ if (!cardGroups[card.noteId]) cardGroups[card.noteId] = [];
+ cardGroups[card.noteId].push(card);
+ });
+
+ Object.keys(cardGroups).forEach(noteId => {
+ const noteItem = notes.find(n => n.id === noteId);
+ if (!noteItem || noteItem.isDeleted) return;
+ const cList = cardGroups[noteId];
+ const mastered = cList.filter(c => c.mastered).length;
+ deckNotesList.push({
+ noteId,
+ title: noteItem.title || 'Note sans titre',
+ count: cList.length,
+ mastery: cList.length > 0 ? mastered / cList.length : 0
+ });
+ });
+
+ return deckNotesList.map(deck => (
+ {
+ onSelectReviewDeck?.(deck.noteId);
+ }}
+ className="w-full text-left p-2.5 rounded-xl border border-border/40 hover:border-accent/30 bg-white dark:bg-zinc-800/40 hover:shadow-2xs transition-all flex items-start gap-2.5 group cursor-pointer"
+ >
+
+
+
+
+
+ {deck.title}
+
+
+ {deck.count} cartes · {Math.round(deck.mastery * 100)}% acquis
+
+
+
+ ));
+ })()}
+
+ {(!flashcards || flashcards.length === 0) && (
+
+ )}
+
+
+ )}
+
+ {/* Shared panel view */}
+ {activeView === 'shared' && (
+
+
+
+
Partagé avec moi
+
+
+
+
+
Aucun document partagé pour le moment.
+
+
+ )}
+
+ {/* Trash bin panel view */}
+ {activeView === 'trash' && (
+
+
+
+
Fichiers Supprimés
+
+
+
+ {notes.filter(n => n.isDeleted).map(note => (
+
+
+
{note.title || "Note sans titre"}
+
Supprimé le {new Date(note.deletedAt || note.date).toLocaleDateString('fr-FR')}
+
+
{
+ setActiveCarnetId(note.carnetId);
+ setActiveNoteId(note.id);
+ }}
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-[9px] font-bold uppercase tracking-wider text-accent shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
+ >
+ Inspecter
+
+
+ ))}
+ {notes.filter(n => n.isDeleted).length === 0 && (
+
+ )}
+
+
+ )}
+
+ {/* Settings panel category switcher list */}
+ {activeView === 'settings' && (
+
+
+
+
Paramètres
+
+
+
+ {[
+ { id: 'general', label: 'Général', icon:
},
+ { id: 'ai', label: 'Intelligence IA', icon:
},
+ { id: 'billing', label: 'Tarifs & Abonnements', icon:
},
+ { id: 'appearance', label: 'Thème & Stylisme', icon:
},
+ { id: 'profile', label: 'Profil Utilisateur', icon:
},
+ ].map(tab => (
+
setActiveSettingsTab?.(tab.id as any)}
+ className="w-full text-left px-3 py-2 text-[11px] transition-all rounded-lg flex items-center gap-2.5 text-muted-ink hover:text-ink hover:bg-black/5"
+ >
+ {tab.icon}
+ {tab.label}
+
+ ))}
+
+
+ )}
+
+
+
+
+ >
+ );
+};
diff --git a/architectural-grid1/src/components/SlashMenu.tsx b/architectural-grid1/src/components/SlashMenu.tsx
new file mode 100644
index 0000000..20b7d87
--- /dev/null
+++ b/architectural-grid1/src/components/SlashMenu.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import {
+ Heading1,
+ Heading2,
+ List,
+ Quote,
+ Code,
+ Image as ImageIcon,
+ Type,
+ Sparkles,
+ Link2
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+
+interface SlashMenuProps {
+ position: { top: number; left: number };
+ onSelect: (type: string) => void;
+ onClose: () => void;
+}
+
+export const SlashMenu: React.FC = ({ position, onSelect, onClose }) => {
+ const commands = [
+ { id: 'h1', label: 'Titre Principal', icon: , desc: 'Grand titre de section' },
+ { id: 'h2', label: 'Sous-titre', icon: , desc: 'Titre de niveau 2' },
+ { id: 'bullet', label: 'Liste à puces', icon:
, desc: 'Liste simple' },
+ { id: 'quote', label: 'Citation', icon:
, desc: 'Bloc de texte mis en avant' },
+ { id: 'code', label: 'Bloc de Code', icon: , desc: 'Code ou texte technique' },
+ { id: 'image', label: 'Image', icon: , desc: 'Insérer un visuel' },
+ { id: 'embed', label: 'Living Block', icon: , desc: 'Insérer un bloc connecté dynamique', special: true },
+ { id: 'ai-summary', label: 'Résumé IA', icon: , desc: 'Générer un résumé court', special: true },
+ ];
+
+ return (
+ <>
+
+
+
+ Commandes rapides
+
+
+ {commands.map((cmd) => (
+
onSelect(cmd.id)}
+ className="w-full flex items-start gap-3 px-3 py-2 hover:bg-paper dark:hover:bg-white/5 transition-colors group text-left"
+ >
+
+ {cmd.icon}
+
+
+
{cmd.label}
+
{cmd.desc}
+
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/architectural-grid1/src/components/TemporalView.tsx b/architectural-grid1/src/components/TemporalView.tsx
new file mode 100644
index 0000000..7672e48
--- /dev/null
+++ b/architectural-grid1/src/components/TemporalView.tsx
@@ -0,0 +1,169 @@
+import React, { useMemo } from 'react';
+import { motion } from 'motion/react';
+import {
+ Clock,
+ Calendar,
+ TrendingUp,
+ Zap,
+ History,
+ ArrowRight,
+ Sparkles,
+ PieChart
+} from 'lucide-react';
+import { Note, NoteAccessLog, NotePrediction } from '../types';
+import { predictNextAccess, detectAccessCycle } from '../services/temporalService';
+
+interface TemporalViewProps {
+ notes: Note[];
+ accessLogs: NoteAccessLog[];
+ onNoteSelect: (id: string) => void;
+}
+
+export const TemporalView: React.FC = ({ notes, accessLogs, onNoteSelect }) => {
+ const predictions = useMemo(() => {
+ return notes
+ .map(note => {
+ const noteLogs = accessLogs.filter(l => l.noteId === note.id);
+ const prediction = predictNextAccess(note, noteLogs);
+ return { note, prediction };
+ })
+ .filter(p => p.prediction !== null) as { note: Note; prediction: NotePrediction }[];
+ }, [notes, accessLogs]);
+
+ const cyclicalNotes = useMemo(() => {
+ return notes
+ .map(note => {
+ const noteLogs = accessLogs.filter(l => l.noteId === note.id);
+ const cycle = detectAccessCycle(noteLogs);
+ return { note, cycle };
+ })
+ .filter(p => p.cycle !== null) as { note: Note; cycle: number }[];
+ }, [notes, accessLogs]);
+
+ return (
+
+
+
+
+
+
+
Temporal Forecast
+
+
Predicting the recurrence of insight
+
+
+
+
+
+ {/* Daily Briefing / Predictions */}
+
+
+
+
Intelligence Briefing
+
+
+
+ {predictions.map(({ note, prediction }) => (
+
onNoteSelect(note.id)}
+ className="p-6 rounded-2xl bg-white dark:bg-white/5 border border-border/60 hover:border-rose-400/40 transition-all cursor-pointer shadow-sm relative overflow-hidden group"
+ >
+
+
+
+
+
+
+ Coming Up
+
+
+ {new Date(prediction.predictedRelevanceDate).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
+
+
+
+ {note.title}
+
+ "{prediction.reason}"
+
+
+
+
Confidence {Math.round(prediction.confidence * 100)}%
+
+
+
+ ))}
+
+ {predictions.length === 0 && (
+
+
+
No upcoming predictions
+
The system needs more usage data to find cyclical patterns in your research.
+
+ )}
+
+
+
+ {/* Cyclical Patterns */}
+
+
+
+
Detected Cycles
+
+
+
+ {cyclicalNotes.map(({ note, cycle }) => (
+
+
+ {Math.round(cycle)}
+ days
+
+
+
{note.title}
+
+
Recurring theme in your architectural process
+
+
+
+
onNoteSelect(note.id)} className="p-2 hover:bg-black/5 rounded-full">
+
+
+
+ ))}
+
+
+
+ {/* Productivity Stats */}
+
+
+
+
Memory Strength
+
84%
+
Active connections in your semantic network
+
+
+
Peak Cycle
+
28 Days
+
The rhythm of your creative output
+
+
+
+
+
4 Notes resurfacing this week
+
+
+ Build Morning Briefing
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/TrashView.tsx b/architectural-grid1/src/components/TrashView.tsx
new file mode 100644
index 0000000..0a2b2e6
--- /dev/null
+++ b/architectural-grid1/src/components/TrashView.tsx
@@ -0,0 +1,229 @@
+import React from 'react';
+import {
+ Trash2,
+ RotateCcw,
+ X,
+ FileText,
+ Folder,
+ Search,
+ ChevronRight,
+ Clock,
+ AlertCircle,
+ Menu
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'motion/react';
+import { Note, Carnet } from '../types';
+
+interface TrashViewProps {
+ deletedNotes: Note[];
+ deletedCarnets: Carnet[];
+ onRestoreNote: (id: string) => void;
+ onRestoreCarnet: (id: string) => void;
+ onPermanentDeleteNote: (id: string) => void;
+ onPermanentDeleteCarnet: (id: string) => void;
+ onEmptyTrash: () => void;
+ onOpenSidebar?: () => void;
+}
+
+export const TrashView: React.FC = ({
+ deletedNotes,
+ deletedCarnets,
+ onRestoreNote,
+ onRestoreCarnet,
+ onPermanentDeleteNote,
+ onPermanentDeleteCarnet,
+ onEmptyTrash,
+ onOpenSidebar
+}) => {
+ const [searchQuery, setSearchQuery] = React.useState('');
+ const [filterType, setFilterType] = React.useState<'all' | 'notes' | 'carnets'>('all');
+
+ const getDaysRemaining = (dateString?: string) => {
+ if (!dateString) return 30;
+ const deletedDate = new Date(dateString);
+ const now = new Date();
+ const diffTime = now.getTime() - deletedDate.getTime();
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+ return Math.max(0, 30 - diffDays);
+ };
+
+ const filteredItems = React.useMemo(() => {
+ const items = [
+ ...deletedNotes.map(n => ({ ...n, itemType: 'note' as const })),
+ ...deletedCarnets.map(c => ({ ...c, itemType: 'carnet' as const }))
+ ];
+
+ return items
+ .filter(item => {
+ const matchesSearch = ('title' in item ? item.title : item.name).toLowerCase().includes(searchQuery.toLowerCase());
+ const matchesType = filterType === 'all' || (filterType === 'notes' && item.itemType === 'note') || (filterType === 'carnets' && item.itemType === 'carnet');
+ return matchesSearch && matchesType;
+ })
+ .sort((a, b) => {
+ const dateA = a.deletedAt ? new Date(a.deletedAt).getTime() : 0;
+ const dateB = b.deletedAt ? new Date(b.deletedAt).getTime() : 0;
+ return dateB - dateA;
+ });
+ }, [deletedNotes, deletedCarnets, searchQuery, filterType]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Corbeille
+
+
+ Auto-suppression après 30 jours
+
+
+
+
+ {filteredItems.length > 0 && (
+
{
+ if (window.confirm('Vider la corbeille ? Cette action est irréversible.')) {
+ onEmptyTrash();
+ }
+ }}
+ className="px-6 py-3 bg-paper border border-border text-rose-500 rounded-2xl text-[10px] font-bold uppercase tracking-widest hover:bg-rose-50 hover:border-rose-100 transition-all shadow-sm"
+ >
+ Vider tout
+
+ )}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full bg-white dark:bg-white/5 border border-border/40 rounded-2xl pl-12 pr-6 py-4 text-sm outline-none focus:ring-4 ring-ink/5 transition-all shadow-sm"
+ />
+
+
+
+ {(['all', 'notes', 'carnets'] as const).map((type) => (
+ setFilterType(type)}
+ className={`px-6 py-3 rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all
+ ${filterType === type ? 'bg-ink text-paper shadow-lg' : 'text-concrete hover:text-ink'}`}
+ >
+ {type === 'all' ? 'Tous' : type === 'notes' ? 'Notes' : 'Carnets'}
+
+ ))}
+
+
+
+
+
+ {filteredItems.length > 0 ? (
+
+
+ {filteredItems.map((item) => {
+ const daysLeft = getDaysRemaining(item.deletedAt);
+ return (
+
+ {/* Countdown Progress Bar */}
+
+
+
+
+
+
+ {item.itemType === 'note' ? : }
+
+
+ item.itemType === 'note' ? onRestoreNote(item.id) : onRestoreCarnet(item.id)}
+ className="flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-xl text-[10px] font-bold uppercase tracking-widest hover:bg-emerald-100 transition-colors"
+ >
+ Restaurer
+
+ item.itemType === 'note' ? onPermanentDeleteNote(item.id) : onPermanentDeleteCarnet(item.id)}
+ className="p-2 hover:bg-rose-50 text-rose-500 rounded-xl transition-colors"
+ title="Supprimer définitivement"
+ >
+
+
+
+
+
+
+
+ {'title' in item ? item.title : item.name}
+
+
+
+ {daysLeft} JOURS RESTANTS
+
+
+ {('deletedAt' in item && item.deletedAt) ? new Date(item.deletedAt).toLocaleDateString() : ''}
+
+
+
+
+ {item.itemType === 'note' && 'content' in item ? (
+
+ {item.content.replace(/[#*`]/g, '')}
+
+ ) : (
+
+
+ Contenu du dossier préservé
+
+
+ )}
+
+ );
+ })}
+
+
+ ) : (
+
+
+
+
+
+
Corbeille vide
+
+ Les éléments que vous supprimez apparaîtront ici. Ils seront conservés pendant 30 jours avant suppression définitive.
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/settings/AITab.tsx b/architectural-grid1/src/components/settings/AITab.tsx
new file mode 100644
index 0000000..7026110
--- /dev/null
+++ b/architectural-grid1/src/components/settings/AITab.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import { Sparkles, Edit3, MessageCircle, Languages, Tag, History, FlaskConical } from 'lucide-react';
+import { motion } from 'motion/react';
+
+const AISettingCard = ({ icon, title, description, defaultChecked = false }: any) => (
+
+
+
+ {icon}
+
+
+
{title}
+
{description}
+
+
+
+
+
+
+
+);
+
+export const AITab: React.FC = () => {
+ return (
+
+
+
Configurez vos fonctionnalités IA et préférences
+
+
+
Fonctionnalités IA
+
+
}
+ title="Suggestions de titre"
+ description="Suggérer des titres pour les notes sans titre après 50+ mots"
+ defaultChecked
+ />
+
}
+ title="IA Note"
+ description="Active le bouton de chat IA et les outils d'amélioration du texte"
+ defaultChecked
+ />
+
}
+ title="💡 J'ai remarqué quelque chose..."
+ description="Aperçu quotidien de vos notes"
+ defaultChecked
+ />
+
}
+ title="Détection de langue"
+ description="Détecte automatiquement la langue de vos notes"
+ defaultChecked
+ />
+
}
+ title="Suggestion des labels"
+ description="Suggère et applique des étiquettes automatiquement à vos notes"
+ defaultChecked
+ />
+
}
+ title="Historique des notes"
+ description="Active les snapshots de versions et la restauration depuis History"
+ defaultChecked
+ />
+
+
+
+
+ {/* Fréquence */}
+
+
+
Fréquence
+
Fréquence d'analyse des connexions
+
+
+
+
+ {/* Mode d'historique */}
+
+
+
Mode d'historique
+
Gestion des snapshots
+
+
+
+
+
+
+
Manuel (bouton commit)
+
Créer des snapshots manuellement
+
+
+
+
+
+
+
Automatique (intelligent)
+
Snapshots automatiques avec détection
+
+
+
+
+
+
+ {/* Mode Démo */}
+
+
+
+
+
+
+
+ 🧪 Mode Démo
+
+
Accélère Memory Echo pour les tests. Les connexions apparaissent instantanément.
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/settings/AppearanceTab.tsx b/architectural-grid1/src/components/settings/AppearanceTab.tsx
new file mode 100644
index 0000000..56b8cbc
--- /dev/null
+++ b/architectural-grid1/src/components/settings/AppearanceTab.tsx
@@ -0,0 +1,164 @@
+import React from 'react';
+import { Palette, Type, LayoutGrid, Maximize } from 'lucide-react';
+import { motion } from 'motion/react';
+
+const AppearanceSelect = ({ icon, title, description, options, defaultValue }: any) => (
+
+
+
+ {icon}
+
+
+
{title}
+
{description}
+
+
+
+
+
+ {options.map((opt: string) => (
+ {opt}
+ ))}
+
+
+
+
+);
+
+interface AppearanceTabProps {
+ accentColor: string;
+ onAccentColorChange: (color: string) => void;
+}
+
+const PRESET_COLORS = [
+ { name: 'Ochre Swiss', value: '#A47148' },
+ { name: 'Alpine Moss', value: '#4E594A' },
+ { name: 'Terracotta', value: '#B1523E' },
+ { name: 'Slate Steel', value: '#4A5568' },
+ { name: 'Midnight', value: '#1E293B' },
+ { name: 'Sage Leaf', value: '#7C8363' },
+ { name: 'Bordeaux', value: '#722F37' },
+ { name: 'Carbon', value: '#262626' },
+];
+
+export const AppearanceTab: React.FC = ({ accentColor, onAccentColorChange }) => {
+ return (
+
+
+
Personnaliser l'apparence de l'application
+
+
+ {/* Accent Color Section */}
+
+
+
+
+
+
Couleur d'accentuation
+
Définissez la couleur principale de votre espace de travail
+
+
+
+
+
+
+
+ {PRESET_COLORS.map((color) => (
+
onAccentColorChange(color.value)}
+ className={`relative w-12 h-12 rounded-2xl transition-all duration-300 hover:scale-110 flex items-center justify-center p-1 border-2 ${
+ accentColor.toLowerCase() === color.value.toLowerCase()
+ ? 'border-accent shadow-lg shadow-accent/20'
+ : 'border-transparent hover:border-concrete/20'
+ }`}
+ title={color.name}
+ >
+
+ {accentColor.toLowerCase() === color.value.toLowerCase() && (
+
+
+
+ )}
+
+ ))}
+
+
+
+
+
onAccentColorChange(e.target.value)}
+ className="w-12 h-12 rounded-2xl cursor-pointer opacity-0 absolute inset-0 z-10"
+ />
+
+
+
+
Personnaliser
+
+
+
+
+
}
+ title="Thème"
+ description="Sélectionner le mode visuel"
+ options={['Clair', 'Sombre', 'Système']}
+ defaultValue="Clair"
+ />
+
}
+ title="Taille de la police"
+ description="Ajustez la lisibilité globale de l'interface"
+ options={['Petite', 'Moyenne', 'Grande']}
+ defaultValue="Moyenne"
+ />
+
}
+ title="Famille de polices"
+ description="La typographie définit l'âme de l'application"
+ options={['Inter', 'JetBrains Mono', 'Public Sans', 'Outfit']}
+ defaultValue="JetBrains Mono"
+ />
+
}
+ title="Affichage des notes"
+ description="Gestion visuelle de la grille de composition"
+ options={['Cartes (grille)', 'Liste', 'Tableau']}
+ defaultValue="Cartes (grille)"
+ />
+
}
+ title="Taille des notes"
+ description="Structure de la mise en page des éléments"
+ options={['Taille uniforme', 'Variable (Masonry)']}
+ defaultValue="Taille uniforme"
+ />
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/settings/BillingTab.tsx b/architectural-grid1/src/components/settings/BillingTab.tsx
new file mode 100644
index 0000000..67a14e9
--- /dev/null
+++ b/architectural-grid1/src/components/settings/BillingTab.tsx
@@ -0,0 +1,203 @@
+import React from 'react';
+import { motion } from 'motion/react';
+import { Check, Shield, Zap, Crown, CreditCard, ArrowRight, Activity, Clock } from 'lucide-react';
+
+export const BillingTab: React.FC = () => {
+ const plans = [
+ {
+ id: 'free',
+ name: 'Plan Basic',
+ price: 'Gratuit',
+ period: '',
+ description: 'Pour découvrir la magie de Momento.',
+ features: [
+ '100 Notes max',
+ '3 Carnets',
+ '50 crédits IA (Lifetime)',
+ 'Recherche sémantique',
+ 'Historique 7 jours'
+ ],
+ current: true,
+ buttonText: 'Plan Actuel',
+ buttonClass: 'bg-paper text-concrete cursor-default'
+ },
+ {
+ id: 'pro',
+ name: 'Plan Pro',
+ price: '9,90€',
+ period: '/mois',
+ description: 'Pour les consultants et créateurs exigeants.',
+ features: [
+ 'Notes illimitées',
+ 'BYOK (OpenAI/Anthropic)',
+ '200 recherches sémantiques',
+ 'Agents (12 runs/mois)',
+ 'Historique 30 jours',
+ 'Support Email'
+ ],
+ current: false,
+ popular: true,
+ buttonText: 'Passer au Plan Pro',
+ buttonClass: 'bg-accent text-white shadow-xl shadow-accent/20 hover:scale-[1.02] active:scale-95'
+ },
+ {
+ id: 'business',
+ name: 'Plan Business',
+ price: '29,90€',
+ period: '/mois',
+ description: 'Pour les équipes et chefs de produit.',
+ features: [
+ '10 Collaborateurs inclus',
+ 'BYOK (13 fournisseurs)',
+ '1000 recherches sémantiques',
+ 'Agents (60 runs/mois)',
+ 'Brainstorm illimité',
+ 'Accès API'
+ ],
+ current: false,
+ buttonText: 'Choisir Plan Business',
+ buttonClass: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95'
+ }
+ ];
+
+ return (
+
+
+
Gérer votre abonnement et votre facturation
+
+
+ {/* Usage Overview */}
+
+
+
+
+
+
Utilisation actuelle
+
Période en cours
+
+
+
+
+
+
+ Crédits IA
+ 1 / 50 utilisés
+
+
+
+
+
+
+ Notes & Carnets
+ 12 / 100 notes
+
+
+
+
+
+
+
+
+
+
+
+
+
Facturation
+
Renouvellement
+
+
+
+
+
Votre plan gratuit n'expire jamais. Passez à la vitesse supérieure pour débloquer toute la puissance de Momento.
+
+ Plan Actuel
+ GRATUIT
+
+
+
+
+
+ {/* Plan Selection */}
+
+ {plans.map((plan) => (
+
+ {plan.popular && (
+
+ Recommandé
+
+ )}
+
+
+
{plan.name}
+
+ {plan.price}
+ {plan.period}
+
+
{plan.description}
+
+
+
+ {plan.features.map((feature, i) => (
+
+ ))}
+
+
+
+
+ {plan.buttonText}
+ {!plan.current &&
}
+
+
+
+ ))}
+
+
+ {/* Footer Info */}
+
+
+
+
+
+
+
Transactions sécurisées
+
Paiement via Stripe. Annulez à tout moment, sans engagement.
+
+
+
+
+
+ Activation instantanée
+
+
+
+ Garantie satisfait
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/settings/GeneralTab.tsx b/architectural-grid1/src/components/settings/GeneralTab.tsx
new file mode 100644
index 0000000..f720f53
--- /dev/null
+++ b/architectural-grid1/src/components/settings/GeneralTab.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { Globe, Bell } from 'lucide-react';
+import { motion } from 'motion/react';
+
+export const GeneralTab: React.FC = () => {
+ return (
+
+
+
Paramètres généraux de l'application
+
+
+ {/* Langue */}
+
+
+
+
+
+
+
Langue
+
Sélectionner une langue
+
+
+
+
+
+ Français
+ English
+ Español
+
+
+
+
+
+ {/* Notifications */}
+
+
+
+
+
+
+
Notifications
+
Gérez vos préférences de notifications
+
+
+
+
+
+
+
Notifications par email
+
Recevoir des notifications importantes par email
+
+
+
+
+
+
+
+
+
+
Notifications bureau
+
Recevoir des notifications dans votre navigateur
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/settings/ProfileTab.tsx b/architectural-grid1/src/components/settings/ProfileTab.tsx
new file mode 100644
index 0000000..177b2bb
--- /dev/null
+++ b/architectural-grid1/src/components/settings/ProfileTab.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { motion } from 'motion/react';
+import { User, Mail, Shield, LogOut, Camera, Bell } from 'lucide-react';
+
+interface ProfileTabProps {
+ onLogout: () => void;
+}
+
+export const ProfileTab: React.FC = ({ onLogout }) => {
+ return (
+
+
+
+
+
+
Sepehr
+
Membre Pro depuis Mai 2024
+
+
+
+
+
+
Informations personnelles
+
+
+
+
+
+
Email
+
sepehr1151@gmail.com
+
+
+
Modifier
+
+
+
+
+
+
+
Sécurité
+
Authentification à deux facteurs
+
+
+
Activer
+
+
+
+
+
+
Préférences de compte
+
+
+
+
+
+
Notification push
+
Recevez des alertes pour vos rappels et activités IA.
+
+
+
+
+
+
+
+
+
+
+ Déconnexion
+
+
+
+
+
+ );
+};
diff --git a/architectural-grid1/src/components/settings/SettingsHeader.tsx b/architectural-grid1/src/components/settings/SettingsHeader.tsx
new file mode 100644
index 0000000..b3a0256
--- /dev/null
+++ b/architectural-grid1/src/components/settings/SettingsHeader.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { Settings, Sparkles, Palette, User, Database, Code, Info, CreditCard, Menu } from 'lucide-react';
+import { motion } from 'motion/react';
+import { SettingsTab } from '../../types';
+
+interface SettingsHeaderProps {
+ activeTab: SettingsTab;
+ setActiveTab: (tab: SettingsTab) => void;
+ onOpenSidebar?: () => void;
+}
+
+export const SettingsHeader: React.FC = ({ activeTab, setActiveTab, onOpenSidebar }) => {
+ const tabs = [
+ { id: 'general', label: 'Paramètres généraux', icon: },
+ { id: 'ai', label: 'Paramètres IA', icon: },
+ { id: 'billing', label: 'Facturation', icon: },
+ { id: 'appearance', label: 'Apparence', icon: },
+ { id: 'profile', label: 'Profil', icon: },
+ { id: 'data', label: 'Gestion des données', icon: },
+ { id: 'mcp', label: 'Paramètres MCP', icon: },
+ { id: 'about', label: 'À propos', icon: },
+ ];
+
+ return (
+
+
+
+
+
+
+
Paramètres
+
Configuration & Préférences
+
+
+
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id as SettingsTab)}
+ className={`flex items-center gap-2.5 px-6 py-5 text-[10px] font-bold uppercase tracking-[0.18em] transition-all relative whitespace-nowrap
+ ${activeTab === tab.id ? 'text-ink' : 'text-concrete hover:text-ink/60'}`}
+ >
+ {tab.icon}
+ {tab.label}
+ {activeTab === tab.id && (
+
+ )}
+
+ ))}
+
+
+ );
+};
diff --git a/architectural-grid1/src/constants.ts b/architectural-grid1/src/constants.ts
new file mode 100644
index 0000000..615f39d
--- /dev/null
+++ b/architectural-grid1/src/constants.ts
@@ -0,0 +1,93 @@
+import { Carnet, Note } from './types';
+
+export const CARNETS: Carnet[] = [
+ { id: '1', name: 'Daily Notes', initial: 'D', type: 'Private', isPrivate: true },
+ { id: '2', name: 'Project: Neo', initial: 'P', type: 'Project' },
+ { id: '3', name: 'Shared Docs', initial: 'S', type: 'Shared' },
+ { id: '4', name: 'Architecture Research', initial: 'A', type: 'Project' },
+ { id: '5', name: 'History of Architecture', initial: 'H', type: 'Project', parentId: '4' },
+ { id: '6', name: 'Modernism', initial: 'M', type: 'Project', parentId: '5' },
+ { id: '7', name: 'Sustainable Design', initial: 'S', type: 'Project', parentId: '4' },
+];
+
+export const ALL_NOTES: Note[] = [
+ {
+ id: 'n1',
+ carnetId: '4',
+ title: 'Grid Systems & Geometry',
+ date: 'Oct 26, 2024',
+ content: 'Grid Systems are the foundation of cognitive design. We use geometric blocks to define spaces. The repetitive structure creates a sense of order and rhythm in the built environment.\n\n- [ ] Finaliser l\'étude de la géométrie sacrée\n- [x] Tracer les grilles orthogonales préliminaires',
+ imageUrl: 'https://images.unsplash.com/photo-1503387762-592dea58ef23?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [
+ { id: 't1', label: 'Architecture', type: 'user' },
+ { id: 't2', label: 'Systems', type: 'ai' }
+ ],
+ embedding: [0.1, 0.1]
+ },
+ {
+ id: 'n1-b',
+ carnetId: '4',
+ title: 'Parametric Grids',
+ date: 'Oct 27, 2024',
+ content: 'Parametricism allows us to deform traditional grid systems. By using mathematical algorithms, we can create fluid yet structured geometries that respond to environmental data.\n\n- [ ] Valider l\'algorithme de déformation spatiale\n- [ ] Tester les nœuds structurels imprimés en 3D',
+ imageUrl: 'https://images.unsplash.com/photo-1511225070737-5af5ac9a690d?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [{ id: 't1', label: 'Geometry', type: 'user' }],
+ embedding: [0.12, 0.08]
+ },
+ {
+ id: 'n2',
+ carnetId: '4',
+ title: 'Sustainable Materiality',
+ date: 'Oct 24, 2024',
+ content: 'Exploring cross-laminated timber (CLT) as a sustainable alternative to concrete. Material choice is key to carbon-neutral construction. The warmth of wood contrasts with the coldness of steel.\n\n- [ ] Contacter le fournisseur de CLT local\n- [ ] Estimer le ratio d\'émission de carbone évitée',
+ imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [
+ { id: 't3', label: 'Materials', type: 'user' },
+ { id: 't4', label: 'Sustainabilty', type: 'ai' }
+ ],
+ embedding: [0.8, 0.8]
+ },
+ {
+ id: 'n2-b',
+ carnetId: '7',
+ title: 'Solar Passive Design',
+ date: 'Oct 25, 2024',
+ content: 'Using orientation to maximize natural heat. Sustainable architecture must prioritize passive systems over active ones. Thermal mass and insulation are critical factors.\n\n- [x] Simuler l\'ensoleillement d\'hiver sur la façade sud\n- [ ] Calculer l\'épaisseur d\'isolation en fibre de bois',
+ imageUrl: 'https://images.unsplash.com/photo-1509391366360-fe5bb5843e0c?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [{ id: 't4', label: 'Sustainabilty', type: 'user' }],
+ embedding: [0.85, 0.75]
+ },
+ {
+ id: 'n3',
+ carnetId: '4',
+ title: 'Light & Minimalist Space',
+ date: 'Oct 22, 2024',
+ content: 'Minimalism is about the subtraction of the unnecessary. Light becomes a material in itself. Reflections on glass and white surfaces create depth without clutter.',
+ imageUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [
+ { id: 't5', label: 'Lighting', type: 'user' },
+ { id: 't6', label: 'Atmosphere', type: 'ai' }
+ ],
+ embedding: [0.2, 0.8]
+ },
+ {
+ id: 'n3-b',
+ carnetId: '6',
+ title: 'The Glass House Study',
+ date: 'Oct 23, 2024',
+ content: 'Analyzing the transparency of the Glass House. The boundary between interior and exterior is blurred. A pure expression of modernist ideals and minimal structure.',
+ imageUrl: 'https://images.unsplash.com/photo-1464938050520-ef2270bb8ce8?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [{ id: 't6', label: 'Modernism', type: 'user' }],
+ embedding: [0.25, 0.85]
+ },
+ {
+ id: 'bridge-1',
+ carnetId: '4',
+ title: 'Geometric Ecology',
+ date: 'Oct 28, 2024',
+ content: 'Can we use grid systems to optimize sustainable solar collection? This note bridges the gap between rigid geometry and ecological necessity. Structured sustainability.',
+ imageUrl: 'https://images.unsplash.com/photo-1464146072230-91cabc968276?auto=format&fit=crop&q=80&w=800&h=600',
+ tags: [{ id: 't1', label: 'Bridge', type: 'ai' }],
+ embedding: [0.45, 0.45] // Center point
+ }
+];
diff --git a/architectural-grid1/src/index.css b/architectural-grid1/src/index.css
new file mode 100644
index 0000000..59da734
--- /dev/null
+++ b/architectural-grid1/src/index.css
@@ -0,0 +1,98 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap');
+@import "tailwindcss";
+
+@theme {
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
+ --font-serif: "Playfair Display", serif;
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
+
+ /* Foundation */
+ --color-paper: #F2F0E9;
+ --color-ink: #1C1C1C;
+ --color-muted-ink: rgba(28, 28, 28, 0.6);
+ --color-border: rgba(28, 28, 28, 0.1);
+ --color-concrete: #8D8D8D;
+
+ /* Architectural Accents */
+ --color-accent: #A47148; /* Warm Earthy Brown */
+ --color-slate: #4A4E69;
+ --color-ochre: #D4A373;
+ --color-sage: #A3B18A;
+ --color-rust: #9B2226;
+ --color-glass: rgba(255, 255, 255, 0.4);
+
+ /* Dark Theme Aliases */
+ --color-dark-paper: #0D0D0D;
+ --color-dark-ink: #EAEAEA;
+ --color-dark-muted: rgba(234, 234, 234, 0.5);
+ --color-dark-border: rgba(234, 234, 234, 0.1);
+}
+
+@layer base {
+ body {
+ @apply bg-paper text-ink font-sans antialiased transition-colors duration-500;
+ }
+
+ .dark body {
+ @apply bg-dark-paper;
+ }
+
+ .dark {
+ --color-paper: #121212;
+ --color-ink: #EAEAEA;
+ --color-muted-ink: rgba(234, 234, 234, 0.6);
+ --color-border: rgba(255, 255, 255, 0.08);
+ --color-glass: rgba(0, 0, 0, 0.4);
+ --color-concrete: #555555;
+ }
+}
+
+.paper-texture {
+ background-color: var(--color-paper);
+ background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png");
+}
+
+/* Custom Scrollbar - Architectural Minimalist */
+.custom-scrollbar::-webkit-scrollbar {
+ width: 3px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb {
+ background: rgba(28, 28, 28, 0.08);
+ border-radius: 10px;
+}
+
+.custom-scrollbar:hover::-webkit-scrollbar-thumb {
+ background: rgba(28, 28, 28, 0.2);
+}
+
+.ai-glass {
+ background: rgba(255, 255, 255, 0.85);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+.dark .ai-glass {
+ background: rgba(30, 30, 30, 0.85);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.sidebar-shadow {
+ box-shadow: 1px 0 10px rgba(0, 0, 0, 0.05);
+}
+
+.active-nav-item {
+ background: linear-gradient(to right, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4));
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+.dark .active-nav-item {
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
diff --git a/architectural-grid1/src/main.tsx b/architectural-grid1/src/main.tsx
new file mode 100644
index 0000000..080dac3
--- /dev/null
+++ b/architectural-grid1/src/main.tsx
@@ -0,0 +1,10 @@
+import {StrictMode} from 'react';
+import {createRoot} from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/architectural-grid1/src/services/clusteringService.ts b/architectural-grid1/src/services/clusteringService.ts
new file mode 100644
index 0000000..ebce3a7
--- /dev/null
+++ b/architectural-grid1/src/services/clusteringService.ts
@@ -0,0 +1,242 @@
+
+import { Note, NoteCluster, BridgeNote } from '../types';
+import { cosineSimilarity } from './geminiService';
+
+export function dbscan(notes: Note[], eps: number, minPts: number): number[] {
+ const n = notes.length;
+ const labels = new Array(n).fill(-1); // -1 = noise, 0+ = cluster id
+ let clusterId = 0;
+
+ for (let i = 0; i < n; i++) {
+ if (labels[i] !== -1) continue;
+
+ const neighbors = getNeighbors(i, notes, eps);
+
+ if (neighbors.length < minPts) {
+ labels[i] = -1; // remains noise for now
+ continue;
+ }
+
+ labels[i] = clusterId;
+ const queue = neighbors.filter(idx => idx !== i);
+
+ for (let j = 0; j < queue.length; j++) {
+ const pIdx = queue[j];
+
+ if (labels[pIdx] === -1) {
+ labels[pIdx] = clusterId; // noisy point becomes border point
+ }
+
+ if (labels[pIdx] !== -1 && labels[pIdx] < clusterId) {
+ // This should not happen in standard DBSCAN unless we re-visit
+ }
+
+ if (labels[pIdx] === clusterId && labels[pIdx] !== -1) {
+ // Skip if already processed in this cluster
+ }
+
+ // If it was already labeled, skip re-neighboring
+ const pWasNoise = labels[pIdx] === -1;
+ if (labels[pIdx] === -1) labels[pIdx] = clusterId;
+
+ // If point was not processed
+ if (pWasNoise || labels[pIdx] === clusterId ) {
+ // This is a simplified queue processing
+ }
+ }
+
+ // Standard DBSCAN expansion
+ expandCluster(i, neighbors, labels, clusterId, notes, eps, minPts);
+ clusterId++;
+ }
+
+ return labels;
+}
+
+function expandCluster(pIdx: number, neighbors: number[], labels: number[], clusterId: number, notes: Note[], eps: number, minPts: number) {
+ let i = 0;
+ while (i < neighbors.length) {
+ const qIdx = neighbors[i];
+ if (labels[qIdx] === -1) {
+ labels[qIdx] = clusterId;
+ } else if (labels[qIdx] === undefined || labels[qIdx] === -1) {
+ // unreachable
+ }
+
+ if (labels[qIdx] === clusterId || labels[qIdx] === -1) {
+ const qNeighbors = getNeighbors(qIdx, notes, eps);
+ if (qNeighbors.length >= minPts) {
+ for(const qn of qNeighbors) {
+ if (labels[qn] === -1) {
+ labels[qn] = clusterId;
+ neighbors.push(qn);
+ } else if (!labels.hasOwnProperty(qn)) {
+ // logic error
+ }
+ }
+ }
+ }
+ i++;
+ }
+}
+
+// Clean DBSCAN implementation
+export function runClustering(notes: Note[], eps: number = 0.15, minPts: number = 2): { labels: number[], clusters: NoteCluster[] } {
+ const validNotes = notes.filter(n => n.embedding && n.embedding.length > 0);
+ if (validNotes.length === 0) return { labels: [], clusters: [] };
+
+ const n = validNotes.length;
+ const labels = new Array(n).fill(-1);
+ let cId = 0;
+
+ for (let i = 0; i < n; i++) {
+ if (labels[i] !== -1) continue;
+
+ const neighbors = findNeighbors(i, validNotes, eps);
+ if (neighbors.length < minPts) {
+ labels[i] = -1;
+ } else {
+ labels[i] = cId;
+ expand(i, neighbors, labels, cId, validNotes, eps, minPts);
+ cId++;
+ }
+ }
+
+ const clusters: NoteCluster[] = [];
+ const colorPalette = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF'];
+
+ for (let i = 0; i < cId; i++) {
+ const noteIds = validNotes.filter((_, idx) => labels[idx] === i).map(n => n.id);
+ clusters.push({
+ id: `cluster-${i}`,
+ name: `Cluster ${i + 1}`,
+ noteIds,
+ color: colorPalette[i % colorPalette.length]
+ });
+ }
+
+ return { labels, clusters };
+}
+
+function findNeighbors(idx: number, notes: Note[], eps: number): number[] {
+ const neighbors: number[] = [];
+ const targetEmbedding = notes[idx].embedding!;
+ for (let i = 0; i < notes.length; i++) {
+ const sim = cosineSimilarity(targetEmbedding, notes[i].embedding!);
+ const dist = 1 - sim;
+ if (dist <= eps) {
+ neighbors.push(i);
+ }
+ }
+ return neighbors;
+}
+
+function expand(rootIdx: number, neighbors: number[], labels: number[], cId: number, notes: Note[], eps: number, minPts: number) {
+ const queue = [...neighbors];
+ for (let i = 0; i < queue.length; i++) {
+ const qIdx = queue[i];
+ if (labels[qIdx] === -1) {
+ labels[qIdx] = cId;
+ }
+ if (labels[qIdx] !== -1 && labels[qIdx] !== cId) continue;
+ if (labels[qIdx] === cId) {
+ // already visited but let's check neighbors if we just added it
+ }
+
+ // If point was noise, it now belongs to cluster, but we don't necessarily expand from it unless it's a core point
+ // This is the standard DBSCAN: noise points can become border points
+ }
+
+ // Re-implementing correctly
+ let head = 0;
+ while(head < queue.length) {
+ const qIdx = queue[head];
+ if (labels[qIdx] === -1) labels[qIdx] = cId;
+ if (labels[qIdx] === cId) {
+ const qNeighbors = findNeighbors(qIdx, notes, eps);
+ if (qNeighbors.length >= minPts) {
+ for(const qn of qNeighbors) {
+ if (labels[qn] === -1) {
+ labels[qn] = cId;
+ queue.push(qn);
+ }
+ }
+ }
+ }
+ head++;
+ }
+}
+
+function getNeighbors(idx: number, notes: Note[], eps: number): number[] {
+ const neighbors: number[] = [];
+ const target = notes[idx].embedding!;
+ for (let i = 0; i < notes.length; i++) {
+ if (!notes[i].embedding) continue;
+ const dist = 1 - cosineSimilarity(target, notes[i].embedding!);
+ if (dist <= eps) neighbors.push(i);
+ }
+ return neighbors;
+}
+
+export function detectBridges(notes: Note[], clusters: NoteCluster[], threshold: number = 0.5): BridgeNote[] {
+ const bridges: BridgeNote[] = [];
+ const validNotes = notes.filter(n => n.embedding);
+
+ for (const note of validNotes) {
+ const connectedClusters = new Set();
+
+ for (const cluster of clusters) {
+ // Check if note has strong links to ANY note in this cluster
+ const clusterNotes = notes.filter(n => cluster.noteIds.includes(n.id) && n.embedding);
+ const hasStrongLink = clusterNotes.some(cn => cosineSimilarity(note.embedding!, cn.embedding!) > threshold);
+
+ if (hasStrongLink) {
+ connectedClusters.add(cluster.id);
+ }
+ }
+
+ if (connectedClusters.size >= 2) {
+ bridges.push({
+ noteId: note.id,
+ connectedClusterIds: Array.from(connectedClusters),
+ bridgeScore: connectedClusters.size / Math.max(clusters.length, 1)
+ });
+ }
+ }
+
+ return bridges.sort((a, b) => b.bridgeScore - a.bridgeScore);
+}
+
+export function calculateCentroid(noteIds: string[], allNotes: Note[]): number[] | undefined {
+ const clusterNotes = allNotes.filter(n => noteIds.includes(n.id) && n.embedding);
+ if (clusterNotes.length === 0) return undefined;
+
+ const embeddingDim = clusterNotes[0].embedding!.length;
+ const centroid = new Array(embeddingDim).fill(0);
+
+ for (const note of clusterNotes) {
+ for (let i = 0; i < embeddingDim; i++) {
+ centroid[i] += note.embedding![i];
+ }
+ }
+
+ for (let i = 0; i < embeddingDim; i++) {
+ centroid[i] /= clusterNotes.length;
+ }
+
+ return centroid;
+}
+
+export function getMostCentralNoteTitles(noteIds: string[], centroid: number[] | undefined, allNotes: Note[], count: number = 5): string[] {
+ const clusterNotes = allNotes.filter(n => noteIds.includes(n.id) && n.embedding);
+ if (clusterNotes.length === 0) return [];
+ if (!centroid) return clusterNotes.slice(0, count).map(n => n.title);
+
+ const scored = clusterNotes.map(n => ({
+ title: n.title,
+ similarity: cosineSimilarity(n.embedding!, centroid)
+ }));
+
+ scored.sort((a, b) => b.similarity - a.similarity);
+ return scored.slice(0, count).map(item => item.title);
+}
diff --git a/architectural-grid1/src/services/geminiService.ts b/architectural-grid1/src/services/geminiService.ts
new file mode 100644
index 0000000..01e2451
--- /dev/null
+++ b/architectural-grid1/src/services/geminiService.ts
@@ -0,0 +1,313 @@
+
+import { GoogleGenAI, Type } from "@google/genai";
+import { BrainstormIdea } from "../types";
+
+const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
+
+const BRAINSTORM_SCHEMA = {
+ type: Type.OBJECT,
+ properties: {
+ ideas: {
+ type: Type.ARRAY,
+ items: {
+ type: Type.OBJECT,
+ properties: {
+ title: { type: Type.STRING },
+ description: { type: Type.STRING },
+ connection_to_seed: { type: Type.STRING },
+ novelty_score: { type: Type.NUMBER }
+ },
+ required: ["title", "description", "connection_to_seed", "novelty_score"]
+ }
+ }
+ },
+ required: ["ideas"]
+};
+
+const SUGGESTIONS_SCHEMA = {
+ type: Type.OBJECT,
+ properties: {
+ suggestions: {
+ type: Type.ARRAY,
+ items: {
+ type: Type.OBJECT,
+ properties: {
+ title: { type: Type.STRING },
+ description: { type: Type.STRING },
+ reasoning: { type: Type.STRING }
+ },
+ required: ["title", "description", "reasoning"]
+ }
+ }
+ },
+ required: ["suggestions"]
+};
+
+export async function generateBrainstormWave(
+ seedIdea: string,
+ waveNumber: number,
+ contextSummaries: string = ""
+): Promise[]> {
+ const waveDescriptions = [
+ "", // index 0 unused
+ "VAGUE 1 (proximité directe) : Sous-aspects, reformulations, variations de l'idée. Reste dans le même domaine.",
+ "VAGUE 2 (analogies) : Trouve des parallèles dans d'autres domaines. Comment cette idée se manifeste-t-elle ailleurs ? Quelles techniques d'autres industries pourraient s'appliquer ?",
+ "VAGUE 3 (disruption) : Inverse l'idée. Pousse-la à l'extrême. Combine-la avec un domaine totalement non lié. Que se passe-t-il si l'opposé est vrai ?"
+ ];
+
+ const prompt = `
+ Idée seed : "${seedIdea}"
+ Contexte : ${contextSummaries}
+ Génère 5 idées pour la VAGUE ${waveNumber} : ${waveDescriptions[waveNumber]}
+ Format JSON selon le schéma.
+ `;
+
+ try {
+ const response = await ai.models.generateContent({
+ model: "gemini-3-flash-preview",
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
+ config: {
+ systemInstruction: "Tu es un expert en brainstorming. Réponds uniquement en JSON valide.",
+ responseMimeType: "application/json",
+ responseSchema: BRAINSTORM_SCHEMA,
+ temperature: 1.0
+ }
+ });
+
+ const resText = response.text;
+ if (!resText) return [];
+
+ const parsed = JSON.parse(resText.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
+ const ideas = Array.isArray(parsed.ideas) ? parsed.ideas : (Array.isArray(parsed) ? parsed : []);
+
+ return ideas.map((item: any) => ({
+ title: item.title,
+ description: item.description,
+ connectionToSeed: item.connection_to_seed,
+ noveltyScore: item.novelty_score,
+ waveNumber
+ }));
+ } catch (error) {
+ console.error(`Error generating brainstorm wave ${waveNumber}:`, error);
+ throw error;
+ }
+}
+
+export async function generateExpansion(parentIdeaTitle: string, parentIdeaDescription: string): Promise[]> {
+ const prompt = `
+ Idée source : "${parentIdeaTitle} - ${parentIdeaDescription}"
+ Génère 3 idées d'extension ou de sous-aspects.
+ Format JSON.
+ `;
+
+ try {
+ const response = await ai.models.generateContent({
+ model: "gemini-3-flash-preview",
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
+ config: {
+ systemInstruction: "Tu es un expert en brainstorming. Réponds uniquement en JSON valide.",
+ responseMimeType: "application/json",
+ responseSchema: BRAINSTORM_SCHEMA,
+ temperature: 1.0
+ }
+ });
+
+ const resText = response.text;
+ if (!resText) return [];
+
+ const parsed = JSON.parse(resText.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
+ const ideas = Array.isArray(parsed.ideas) ? parsed.ideas : (Array.isArray(parsed) ? parsed : []);
+
+ return ideas.map((item: any) => ({
+ title: item.title,
+ description: item.description,
+ connectionToSeed: item.connection_to_seed,
+ noveltyScore: item.novelty_score
+ }));
+ } catch (error) {
+ console.error("Error generating expansion:", error);
+ throw error;
+ }
+}
+
+export async function getEmbedding(text: string): Promise {
+ try {
+ const result = await ai.models.embedContent({
+ model: 'gemini-embedding-2-preview',
+ contents: [text],
+ });
+ return result.embeddings[0].values;
+ } catch (error) {
+ console.error("Error generating embedding:", error);
+ throw error;
+ }
+}
+
+export function cosineSimilarity(a: number[], b: number[]): number {
+ if (!a || !b || a.length !== b.length) return 0;
+ const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
+ const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
+ const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
+ if (magnitudeA === 0 || magnitudeB === 0) return 0;
+ return dotProduct / (magnitudeA * magnitudeB);
+}
+
+export async function nameCluster(noteSummaries: string[]): Promise {
+ const prompt = `Quel thème commun relie ces notes ? Donne un nom court (2-4 mots).\nNotes :\n${noteSummaries.join('\n- ')}`;
+ try {
+ const result = await ai.models.generateContent({
+ model: "gemini-3-flash-preview",
+ contents: prompt
+ });
+ return result.text.trim();
+ } catch (error) {
+ console.error("Error naming cluster:", error);
+ return "Thematic Cluster";
+ }
+}
+
+export async function suggestBridgeIdeas(
+ clusterAName: string,
+ clusterBName: string,
+ clusterASummaries: string,
+ clusterBSummaries: string
+): Promise {
+ const prompt = `
+ Cluster A (${clusterAName}) contient des notes sur : ${clusterASummaries}
+ Cluster B (${clusterBName}) contient des notes sur : ${clusterBSummaries}
+
+ Ces deux clusters ne sont pas connectés. Propose 3 idées
+ de "notes pont" qui pourraient créer un lien créatif entre eux.
+ Pour chaque idée : titre, description, pourquoi ça connecte les deux.
+
+ Format JSON.
+ `;
+
+ try {
+ const response = await ai.models.generateContent({
+ model: "gemini-3-flash-preview",
+ contents: prompt,
+ config: {
+ responseMimeType: "application/json",
+ responseSchema: SUGGESTIONS_SCHEMA
+ }
+ });
+ const parsed = JSON.parse(response.text);
+ return Array.isArray(parsed.suggestions) ? parsed.suggestions : [];
+ } catch (error) {
+ console.error("Error suggesting bridge ideas:", error);
+ return [];
+ }
+}
+export async function parseDocument(fileUrl: string, fileName: string): Promise {
+ const prompt = `Extraits et résume le texte de ce document nommé "${fileName}".
+ Si c'est un PDF, ignore les éléments purement graphiques et concentre-toi sur le contenu sémantique.
+ Fais une extraction structurée.`;
+
+ try {
+ // In a real scenario, we would use media upload.
+ // Here we simulate the extraction.
+ const response = await ai.models.generateContent({
+ model: "gemini-3-flash-preview",
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
+ config: {
+ systemInstruction: "Tu es un expert en extraction de texte et analyse de documents.",
+ temperature: 0.2
+ }
+ });
+
+ return response.text || "Échec de l'extraction du texte.";
+ } catch (error) {
+ console.error("Error parsing document:", error);
+ return "Erreur lors de l'analyse du document.";
+ }
+}
+
+export async function extractActionItems(notes: { title: string; content: string }[]): Promise {
+ const notesContext = notes.map(n => `TITLE: ${n.title}\nCONTENT: ${n.content}`).join('\n\n---\n\n');
+ const prompt = `
+ Analyse les notes suivantes et extrais la liste des actions à accomplir (TODOs).
+ Pour chaque tâche, identifie si possible l'assigné et la date limite.
+ Présente le résultat sous forme d'un tableau Markdown structuré ou d'une liste claire.
+ Si aucune tâche n'est trouvée, indique-le.
+
+ Notes:
+ ${notesContext}
+ `;
+
+ try {
+ const response = await ai.models.generateContent({
+ model: "gemini-3-flash-preview",
+ contents: prompt,
+ config: {
+ systemInstruction: "Tu es un agent spécialisé dans l'organisation et la gestion de tâches. Ton but est d'être précis et exhaustif.",
+ temperature: 0.1
+ }
+ });
+
+ return response.text;
+ } catch (error) {
+ console.error("Error extracting action items:", error);
+ return "Erreur lors de l'extraction des tâches.";
+ }
+}
+
+const FLASHCARDS_SCHEMA = {
+ type: Type.OBJECT,
+ properties: {
+ flashcards: {
+ type: Type.ARRAY,
+ items: {
+ type: Type.OBJECT,
+ properties: {
+ question: { type: Type.STRING },
+ answer: { type: Type.STRING }
+ },
+ required: ["question", "answer"]
+ }
+ }
+ },
+ required: ["flashcards"]
+};
+
+export async function generateFlashcardsForNote(
+ noteTitle: string,
+ noteContent: string
+): Promise<{ question: string; answer: string }[]> {
+ const prompt = `
+ Titre de la note : "${noteTitle}"
+ Contenu de la note :
+ ${noteContent}
+
+ Génère entre 4 et 8 flashcards (paires question/réponse) d'apprentissage basées sur le contenu ci-dessus.
+
+ Règles de style :
+ - Les questions doivent être claires et guider vers une révision active (ex: "Quelle est la particularité de... ?", "Pourquoi utilise-t-on... ?").
+ - Les réponses doivent être courtes et percutantes.
+ - Langue : Français.
+ - Format de retour : JSON correspondant au schéma.
+ `;
+
+ try {
+ const response = await ai.models.generateContent({
+ model: "gemini-3.5-flash",
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
+ config: {
+ systemInstruction: "Tu es un assistant de révision agile. Tu convertis le contenu d'un cours ou d'une note en de superbes flashcards mémo-techniques.",
+ responseMimeType: "application/json",
+ responseSchema: FLASHCARDS_SCHEMA,
+ temperature: 0.7
+ }
+ });
+
+ const resText = response.text;
+ if (!resText) return [];
+
+ const parsed = JSON.parse(resText.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
+ return Array.isArray(parsed.flashcards) ? parsed.flashcards : (Array.isArray(parsed) ? parsed : []);
+ } catch (error) {
+ console.error("Error generating flashcards with Gemini:", error);
+ return [];
+ }
+}
+
diff --git a/architectural-grid1/src/services/temporalService.ts b/architectural-grid1/src/services/temporalService.ts
new file mode 100644
index 0000000..9864727
--- /dev/null
+++ b/architectural-grid1/src/services/temporalService.ts
@@ -0,0 +1,76 @@
+import { Note, NoteAccessLog, NotePrediction } from '../types';
+
+/**
+ * Simulates finding the dominant frequency in access logs for a specific note
+ * returning the period in days.
+ */
+export function detectAccessCycle(logs: NoteAccessLog[]): number | null {
+ if (logs.length < 5) return null;
+
+ const accessDays = logs
+ .map(log => new Date(log.accessedAt).getTime())
+ .sort((a, b) => a - b);
+
+ const intervals: number[] = [];
+ for (let i = 1; i < accessDays.length; i++) {
+ intervals.push((accessDays[i] - accessDays[i - 1]) / (1000 * 60 * 60 * 24));
+ }
+
+ // Simple heuristic: if intervals are consistently around a value, that's our cycle
+ // We'll calculate the median interval
+ const sortedIntervals = [...intervals].sort((a, b) => a - b);
+ const median = sortedIntervals[Math.floor(sortedIntervals.length / 2)];
+
+ // Check if enough intervals are close to median
+ const withinThreshold = intervals.filter(v => Math.abs(v - median) < Math.max(2, median * 0.2));
+
+ if (withinThreshold.length >= intervals.length * 0.6) {
+ return median;
+ }
+
+ return null;
+}
+
+export function predictNextAccess(note: Note, logs: NoteAccessLog[]): NotePrediction | null {
+ const cycleDays = detectAccessCycle(logs);
+ if (!cycleDays) return null;
+
+ const lastAccess = new Date(logs[logs.length - 1].accessedAt);
+ const nextAccessDate = new Date(lastAccess.getTime() + cycleDays * 24 * 60 * 60 * 1000);
+
+ const now = new Date();
+ const daysUntilNext = (nextAccessDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
+
+ // Only predict if it's coming up in the next 2 weeks
+ if (daysUntilNext > 0 && daysUntilNext < 14) {
+ return {
+ noteId: note.id,
+ predictedRelevanceDate: nextAccessDate.toISOString(),
+ confidence: 0.7,
+ reason: `Historical access pattern suggests a ${Math.round(cycleDays)}-day cycle.`,
+ generatedAt: now.toISOString()
+ };
+ }
+
+ return null;
+}
+
+export function getCoaccessedNotes(baseNoteId: string, logs: NoteAccessLog[], allNotes: Note[]): Note[] {
+ const WINDOW_MS = 30 * 60 * 1000; // 30 minutes
+
+ const baseNoteLogs = logs.filter(l => l.noteId === baseNoteId);
+ const coaccessedIds = new Set();
+
+ baseNoteLogs.forEach(baseLog => {
+ const baseTime = new Date(baseLog.accessedAt).getTime();
+ logs.forEach(otherLog => {
+ if (otherLog.noteId === baseNoteId) return;
+ const otherTime = new Date(otherLog.accessedAt).getTime();
+ if (Math.abs(baseTime - otherTime) < WINDOW_MS) {
+ coaccessedIds.add(otherLog.noteId);
+ }
+ });
+ });
+
+ return allNotes.filter(n => coaccessedIds.has(n.id));
+}
diff --git a/architectural-grid1/src/types.ts b/architectural-grid1/src/types.ts
new file mode 100644
index 0000000..9a9558f
--- /dev/null
+++ b/architectural-grid1/src/types.ts
@@ -0,0 +1,153 @@
+export type NavigationView = 'notebooks' | 'agents' | 'settings' | 'shared' | 'reminders' | 'trash' | 'brainstorm' | 'insights' | 'temporal' | 'graph' | 'revision';
+export type AITone = 'Professional' | 'Creative' | 'Academic' | 'Casual';
+export type AITab = 'infos' | 'versions' | 'relations' | 'discussion' | 'actions' | 'resources' | 'explore';
+export type SettingsTab = 'general' | 'ai' | 'billing' | 'appearance' | 'profile' | 'data' | 'mcp' | 'about';
+
+export interface Tag {
+ id: string;
+ label: string;
+ type: 'ai' | 'user';
+}
+
+export interface Attachment {
+ id: string;
+ name: string;
+ type: 'pdf' | 'docx' | 'image' | 'other';
+ url: string;
+ content?: string; // Extracted text
+ isProcessed?: boolean;
+}
+
+export interface NoteVersion {
+ id: string;
+ title: string;
+ content: string;
+ timestamp: string;
+ size: number;
+}
+
+export interface Note {
+ id: string;
+ carnetId: string;
+ title: string;
+ content: string;
+ imageUrl: string;
+ date: string;
+ tags: Tag[];
+ attachments?: Attachment[];
+ isPinned?: boolean;
+ isDeleted?: boolean;
+ deletedAt?: string;
+ embedding?: number[];
+ clusterId?: string;
+ isClipped?: boolean;
+ clipSourceUrl?: string;
+ clipFavicon?: string;
+ clipDate?: string;
+ isVersioningEnabled?: boolean;
+ versionHistory?: NoteVersion[];
+}
+
+export interface NoteCluster {
+ id: string;
+ name: string;
+ noteIds: string[];
+ centroid? : number[];
+ color: string;
+}
+
+export interface BridgeNote {
+ noteId: string;
+ connectedClusterIds: string[];
+ bridgeScore: number;
+}
+
+export interface ConnectionSuggestion {
+ id: string;
+ title: string;
+ description: string;
+ reasoning: string;
+ clusterAId: string;
+ clusterBId: string;
+}
+
+export interface BrainstormSession {
+ id: string;
+ seedIdea: string;
+ sourceNoteId?: string;
+ contextNoteIds?: string[];
+ exportedNoteId?: string;
+ createdAt: string;
+ updatedAt: string;
+ userId: string;
+}
+
+export type BrainstormIdeaStatus = 'active' | 'dismissed' | 'converted';
+
+export interface BrainstormIdea {
+ id: string;
+ sessionId: string;
+ waveNumber: 1 | 2 | 3;
+ title: string;
+ description: string;
+ connectionToSeed: string;
+ noveltyScore: number; // 1-10
+ parentIdeaId?: string;
+ convertedToNoteId?: string;
+ relatedNoteIds?: string[];
+ status: BrainstormIdeaStatus;
+ position?: { x: number; y: number };
+}
+
+export interface Carnet {
+ id: string;
+ name: string;
+ initial: string;
+ type: 'Private' | 'Project' | 'Shared';
+ isPrivate?: boolean;
+ parentId?: string;
+ isDeleted?: boolean;
+ deletedAt?: string;
+}
+
+export interface NoteAccessLog {
+ noteId: string;
+ accessedAt: string;
+ action: 'view' | 'edit' | 'search_hit';
+}
+
+export interface NotePrediction {
+ noteId: string;
+ predictedRelevanceDate: string;
+ confidence: number;
+ reason: string;
+ generatedAt: string;
+}
+
+export type FlashcardEvaluation = 'fail' | 'hesitant' | 'sure';
+
+export interface Flashcard {
+ id: string;
+ noteId: string;
+ question: string;
+ answer: string;
+ intervalDays: number; // For spaced repetition
+ nextReviewDate: string; // ISO String
+ easeFactor: number;
+ mastered: boolean;
+ history?: {
+ reviewedAt: string;
+ evaluation: FlashcardEvaluation;
+ }[];
+}
+
+export interface FlashcardDeck {
+ noteId: string;
+ title: string;
+ cardsCount: number;
+ nextReviewDate: string; // Min nextReviewDate of all cards
+ masteryScore: number; // Proportion of cards evaluation === 'sure'
+ cards: Flashcard[];
+}
+
+
diff --git a/architectural-grid1/tsconfig.json b/architectural-grid1/tsconfig.json
new file mode 100644
index 0000000..d88f175
--- /dev/null
+++ b/architectural-grid1/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "module": "ESNext",
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ },
+ "allowImportingTsExtensions": true,
+ "noEmit": true
+ }
+}
diff --git a/architectural-grid1/vite.config.ts b/architectural-grid1/vite.config.ts
new file mode 100644
index 0000000..0506f1b
--- /dev/null
+++ b/architectural-grid1/vite.config.ts
@@ -0,0 +1,24 @@
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+import {defineConfig, loadEnv} from 'vite';
+
+export default defineConfig(({mode}) => {
+ const env = loadEnv(mode, '.', '');
+ return {
+ plugins: [react(), tailwindcss()],
+ define: {
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '.'),
+ },
+ },
+ server: {
+ // HMR is disabled in AI Studio via DISABLE_HMR env var.
+ // Do not modifyâfile watching is disabled to prevent flickering during agent edits.
+ hmr: process.env.DISABLE_HMR !== 'true',
+ },
+ };
+});
diff --git a/docs/sprint-status.yaml b/docs/sprint-status.yaml
index ad42508..a5bd9ff 100644
--- a/docs/sprint-status.yaml
+++ b/docs/sprint-status.yaml
@@ -58,3 +58,7 @@ development_status:
4-5-eu-data-residency: backlog
4-6-sso-saml-audit-logging: backlog
epic-4-retrospective: optional
+ epic-5: in-progress
+ 5-1-nextgen-editor: ready-for-dev
+
+
diff --git a/docs/story-nextgen-editor.md b/docs/story-nextgen-editor.md
new file mode 100644
index 0000000..0937b6c
--- /dev/null
+++ b/docs/story-nextgen-editor.md
@@ -0,0 +1,118 @@
+# Story: Éditeur Next-Gen — Poignée Gutter & Base de Données Inline
+
+> **Epic:** Éditeur de Notes et Saisie Intuitive (Next-Gen)
+> **Priority:** High
+> **Status:** ready-for-dev
+> **Depends on:** US-LIVING-BLOCKS (UniqueID et transclusion), US-STRUCTURED-VIEWS (NotebookSchema et propriétés)
+> **Blocks:** —
+
+---
+
+## Context
+
+L'éditeur de notes actuel de Momento est construit sur [rich-text-editor.tsx](file:///home/devparsa/dev/Momento/memento-note/components/rich-text-editor.tsx) avec Tiptap/ProseMirror. Bien qu'il supporte des fonctionnalités avancées (Blocs Vivants, Résonance Sémantique), l'interaction utilisateur de saisie reste celle d'un traitement de texte classique (document linéaire à curseur unique).
+
+Pour offrir une expérience de saisie supérieure à celle de Notion (plus performante, plus fluide à l'écriture) tout en conservant les avantages de la manipulation de blocs, nous implémentons une approche hybride :
+1. **Gutter & Drag Handle Flottant :** Au lieu d'avoir un composant React lourd pour chaque paragraphe (comme dans Notion), un unique bouton de poignée de glissement suit le curseur de la souris dans la marge de l'éditeur en ProseMirror pur, éliminant tout décalage à la saisie.
+2. **Transclusion au Collage :** Faciliter la création de Blocs Vivants en interceptant les liens de blocs copiés et en proposant de les coller en tant que transclusion synchrone.
+3. **Bloc Database Inline :** Porter le composant de base de données relationnelle du prototype [ModernBlockNoteEditor.tsx](file:///home/devparsa/dev/Momento/architectural-grid1/src/components/ModernBlockNoteEditor.tsx#L1711) pour permettre aux utilisateurs d'insérer des tableaux/fiches interactives avec Rollups dynamiques directement au sein de leurs notes.
+
+---
+
+## User Stories
+
+### US-1: Poignée de Glissement Gutter Unique (Hover Drag Handle)
+**En tant que** rédacteur,
+**Je veux** voir apparaître une poignée de glissement discrète dans la marge gauche du bloc que je survole avec ma souris,
+**Afin de** pouvoir réordonner mes blocs par glisser-déposer de manière fluide.
+
+#### Critères d'Acceptation :
+* **Étant donné** que j'ai une note ouverte dans l'éditeur de Momento
+* **Quand** je survole une ligne de texte (paragraphe, titre, liste, citation, etc.) avec mon curseur de souris
+* **Alors** une poignée de glissement (`::drag-handle` flottante) apparaît dans le gutter gauche à la hauteur exacte du bloc survolé
+* **Et** le bouton suit mes déplacements de souris d'un bloc à l'autre sans latence et sans dupliquer les nœuds DOM (une seule poignée réutilisée)
+* **Quand** je clique-glisse sur cette poignée
+* **Alors** le bloc ProseMirror entier sous-jacent est sélectionné visuellement et je peux le déplacer vers le haut ou vers le bas, avec un indicateur visuel de la ligne d'insertion
+* **Et** la poignée est masquée sur les terminaux mobiles / écrans tactiles pour éviter d'entraver l'ergonomie.
+
+---
+
+### US-2: Menu Action Rapide de Bloc
+**En tant que** créateur de contenu,
+**Je veux** pouvoir cliquer sur la poignée de bloc pour ouvrir un menu contextuel d'actions rapides,
+**Afin de** manipuler la structure de mes documents sans avoir à utiliser les sélections de texte complexes ou des raccourcis clavier obscurs.
+
+#### Critères d'Acceptation :
+* **Étant donné** que la poignée de bloc est visible à côté d'un bloc
+* **Quand** je clique sur cette poignée
+* **Alors** un menu contextuel (dropdown à base de glassmorphism fluide) apparaît
+* **Et** il propose les options suivantes :
+ * `Supprimer` -> Efface le bloc ProseMirror ciblé
+ * `Dupliquer` -> Duplique le bloc ProseMirror immédiatement en dessous
+ * `Transformer en` -> Sous-menu pour convertir en : Titre 1, Titre 2, Titre 3, Liste à puces, Liste numérotée, Liste de tâches, Citation, Bloc de code, ou Base de données
+ * `Copier la référence` -> Génère/copie le lien unique du bloc (utilisant son `UniqueID` Tiptap) dans le presse-papier.
+
+---
+
+### US-3: Transclusion intelligente au Collage (Smart Paste)
+**En tant que** chercheur,
+**Je veux** pouvoir coller un lien de bloc copié et le transformer instantanément en bloc connecté synchrone,
+**Afin de** lier et synchroniser des informations à travers différentes notes sans effort manuel.
+
+#### Critères d'Acceptation :
+* **Étant donné** que j'ai copié la référence d'un bloc (contenant l'ID de la note source et le `blockId` unique du paragraphe)
+* **Quand** je colle (Ctrl+V ou Cmd+V) ce lien dans un paragraphe vide d'une autre note
+* **Alors** un petit menu interactif en ligne s'affiche : *"Coller en tant que Bloc Connecté (Live)"* ou *"Coller en tant que texte / lien simple"*
+* **Quand** je sélectionne *"Bloc Connecté"*,
+* **Alors** le bloc ProseMirror est remplacé par un nœud de type `liveBlock` (provenant de notre [LiveBlockExtension](file:///home/devparsa/dev/Momento/memento-note/components/tiptap-live-block-extension.tsx#L159)) synchronisant le contenu en temps réel.
+
+---
+
+### US-4: Bloc de Base de Données Relationnelle Inline
+**En tant que** chef de projet ou gestionnaire de données,
+**Je veux** pouvoir insérer une base de données relationnelle interactive directement au milieu de mon texte, basculer ses vues et suivre des rollups,
+**Afin de** structurer mes informations complexes sans quitter le flux de ma note (comme simulé dans le prototype).
+
+#### Critères d'Acceptation :
+* **Étant donné** que j'utilise l'éditeur de Momento
+* **Quand** je saisis `/database` ou sélectionne "Base de données" dans le menu d'insertion
+* **Alors** un bloc `databaseBlock` est inséré sous forme d'un React NodeView
+* **Et** il propose par défaut un modèle relationnel "Auteurs & Œuvres" (avec des données simulées ou liées au carnet actuel)
+* **Quand** je clique sur le sélecteur de vue en haut à droite du bloc
+* **Alors** je peux basculer entre la vue **Tableau** (grille avec colonnes de propriétés) et la vue **Fiches** (cartes illustrées de couvertures, tags et auteurs)
+* **Et** la colonne **Rollup Count** de la vue Tableau recalcule dynamiquement le nombre de livres liés à chaque auteur au fur et à mesure que j'ajoute ou supprime des fiches
+* **Et** un formulaire d'insertion rapide en bas du bloc me permet d'ajouter des livres ou des auteurs à ce modèle relationnel local.
+
+---
+
+## Spécifications Techniques d'Implémentation
+
+### 1. Structure du Nœud de Base de Données Tiptap (`DatabaseBlock`)
+Créer une extension Tiptap personnalisée `DatabaseBlockExtension` dans `/components/tiptap-database-block-extension.tsx` qui :
+- Gère un groupe `block`, se comporte comme un atome (non modifiable directement en texte brut).
+- Conserve les attributs : `dbId` (string), `dbView` ('table' | 'card'), `dbAuthors` (array) et `dbBooks` (array) représentant le schéma et les valeurs.
+- Utilise un `ReactNodeViewRenderer` pour monter notre composant d'édition de base de données relationnelle en React pur.
+
+### 2. Le Plugin Drag Handle & Gutter en ProseMirror
+Développer un plugin custom dans `/components/tiptap-drag-handle-plugin.ts` :
+* Attacher un écouteur d'événements `mouseover` et `mousemove` sur l'éditeur ProseMirror.
+* Utiliser `view.posAtCoords` pour localiser le nœud ProseMirror survolé.
+* Récupérer son DOM parent et calculer sa hauteur et son décalage vertical.
+* Repositionner l'élément HTML de la poignée en modifiant ses styles CSS `top` et `left` (le bouton est monté au niveau de l'éditeur parent en absolute, évitant d'être injecté dans le texte éditable).
+* Gérer le glissement avec l'API HTML5 Drag and Drop couplée aux transactions de ProseMirror (`tr.replaceWith` ou `MoverBlock`).
+
+### 3. Fichiers à modifier / créer :
+* `[NEW]` `memento-note/components/tiptap-drag-handle-plugin.ts` — Plugin de poignée Gutter
+* `[NEW]` `memento-note/components/tiptap-database-block-extension.tsx` — Nœud et composant DatabaseBlockEditor
+* `[MODIFY]` `memento-note/components/rich-text-editor.tsx` — Intégration du Drag Handle, de la DatabaseBlockExtension et de l'interception du presse-papier dans `editorProps.handlePaste`
+* `[MODIFY]` `memento-note/app/globals.css` — Ajout des classes CSS pour l'alignement précis du gutter, de la poignée, et du bloc de base de données (glassmorphic dropdowns et fiches).
+
+---
+
+## Plan de Vérification
+
+### Tests Manuels :
+1. **Vérification Gutter :** Survoler des textes longs et vérifier que la poignée se positionne correctement à gauche. Glisser un paragraphe sur un autre et valider le réordonnancement.
+2. **Vérification Menu :** Cliquer sur la poignée, dupliquer le bloc, supprimer le bloc, et le transformer en d'autres types.
+3. **Vérification Paste :** Copier une référence de bloc, la coller, et vérifier que la transclusion est proposée et s'insère sous forme de `LiveBlock`.
+4. **Vérification Base de Données Inline :** Insérer le bloc base de données, ajouter un auteur, ajouter un livre avec cet auteur, vérifier le bon calcul du rollup, et basculer en vue Fiches pour vérifier l'affichage des couvertures.
diff --git a/docs/user-stories.md b/docs/user-stories.md
index b76254a..bbd9563 100644
--- a/docs/user-stories.md
+++ b/docs/user-stories.md
@@ -1,7 +1,7 @@
# User Stories — Momento Next Phase
> Basé sur l'analyse du prototype `architectural-grid/` et du code production `memento-note/`.
-> Dernière mise à jour : 2026-05-24
+> Dernière mise à jour : 2026-05-25 (US-NEXTGEN-EDITOR réorganisé, 4 nouvelles stories éditeur ajoutées)
---
@@ -16,25 +16,44 @@
| **US-INFO-RÉSEAU** | Panneau Info + Réseau Local | ✅ **LIVRÉ** | `note-network-tab.tsx`, `sync-note-links.ts`, migration `NoteLink`, picker `[[` |
| **US-CLIPPER** | Web Clipper | ✅ **LIVRÉ** | `extension/`, `/api/clip/*`, migration `sourceUrl`, badge panneau Info |
| **US-GRAPH** | Graphe de Connaissance Global enrichi | ✅ **LIVRÉ** | `note-graph-view.tsx` — filtres liens, seuil sémantique, focus voisinage, couleurs carnets, double-clic ouverture |
-| **US-INSIGHTS** | Clusters Sémantiques + Bridge Notes | 🚧 **EN COURS** | clusters en base mais page masquait les résultats périmés — correction affichage |
-| **US-TEMPORAL** | Prédictions d'accès temporelles | ⏳ À faire | — |
+| **US-INSIGHTS** | Clusters Sémantiques + Bridge Notes | ✅ **LIVRÉ** | `app/(main)/insights/page.tsx`, `network-graph.tsx`, `/api/clusters`, `/api/bridge-notes/*`, état dégradé si clusters périmés |
+| **US-TEMPORAL** | Prédictions d'accès temporelles | ⏸️ **REPORTÉ** | Remplacé par rappels + révision SM-2 + Memory Echo ; heuristique faible, migration NoteAccessLog non prioritaire |
| **US-FLASHCARDS** | Révision IA — Répétition espacée SM-2 | ✅ **LIVRÉ** | `/revision`, `/api/flashcards/*`, SM-2, génération IA depuis l'éditeur |
| **US-STRUCTURED-VIEWS** | Vues Structurées (Tableau/Kanban/Galerie) | ✅ **LIVRÉ** | `/api/notebooks/[id]/schema`, `/api/notes/[id]/properties`, vues structurées + panneau propriétés éditeur |
+| **US-NEXTGEN-EDITOR** | Éditeur Next-Gen : Drag Handle + Menu Bloc + DB Inline + Smart Paste | 🚧 **PLANIFIÉ** | Voir `docs/story-nextgen-editor.md` |
+| **US-EDITOR-PERF** | Performance de frappe TipTap (quick wins) | 🚧 **PLANIFIÉ** | — |
+| **US-EDITOR-UX** | Micro-interactions saisie (slash menu, sélection multi-blocs, paste étendu, placeholders) | ⏳ **À FAIRE** | — |
+| **US-EDITOR-MOBILE** | Expérience tactile & toolbar mobile adaptée | ⏳ **À FAIRE** | — |
+| **US-EDITOR-MARKDOWN** | Rendu WYSIWYG Markdown fidèle (round-trip byte-for-byte) | ⏳ **À FAIRE** | — |
---
## Ordre d'implémentation (dépendances)
```
-US-LIVING-BLOCKS ← dépend de : TipTap UniqueID (fondation)
-US-MEMORY-ECHO ← dépend de : pgvector existant (aucune migration)
-US-INFO-RÉSEAU ← dépend de : wikilinks + backlinks existants
-US-CLIPPER ← dépend de : migration Note.sourceUrl
-US-GRAPH ← dépend de : /api/graph existant
-US-INSIGHTS ← dépend de : Memory Echo + clusters
-US-TEMPORAL ← dépend de : migration NoteAccessLog
-US-FLASHCARDS ← dépend de : migration FlashcardDeck + Flashcard
-US-STRUCTURED-VIEWS ← dépend de : migration NotebookSchema + NotebookProperty
+Quick wins (en premier, ~2h) :
+ US-EDITOR-PERF <- depend de : rien (modifs config TipTap)
+
+Editeur Next-Gen (bloc principal) :
+ US-NEXTGEN-EDITOR <- depend de : US-LIVING-BLOCKS, US-STRUCTURED-VIEWS
+ US-EDITOR-UX <- depend de : US-NEXTGEN-EDITOR (drag handle + menu bloc en place)
+ US-EDITOR-MOBILE <- depend de : US-NEXTGEN-EDITOR (drag handle existant)
+
+Plus tard :
+ US-EDITOR-MARKDOWN <- depend de : rien (evaluation Milkdown, low priority)
+
+Stories livrees :
+ US-LIVING-BLOCKS <- depend de : TipTap UniqueID (fondation)
+ US-MEMORY-ECHO <- depend de : pgvector existant
+ US-INFO-RESEAU <- depend de : wikilinks + backlinks
+ US-CLIPPER <- depend de : migration Note.sourceUrl
+ US-GRAPH <- depend de : /api/graph existant
+ US-INSIGHTS <- depend de : Memory Echo + clusters
+ US-FLASHCARDS <- depend de : migration FlashcardDeck + Flashcard
+ US-STRUCTURED-VIEWS <- depend de : migration NotebookSchema + NotebookProperty
+
+Reportees :
+ US-TEMPORAL <- depend de : migration NoteAccessLog (REPORTE)
```
---
@@ -539,6 +558,8 @@ model FlashcardReview {
## US-TEMPORAL — Prédictions d'Accès Temporelles
+> **Statut : ⏸️ REPORTÉ (2026-05-24)** — Chevauche rappels, révision SM-2 et Memory Echo ; faible signal sans volume d'ouvertures ; pas prioritaire pour éviter la surcharge produit. Spec conservée pour réévaluation ultérieure éventuelle.
+
**Contexte :**
`TemporalView.tsx` (169L) prédit quelles notes l'utilisateur voudra relire, basé sur des patterns d'accès. Rien n'existe en prod. Feature légère à fort impact perçu.
@@ -569,3 +590,231 @@ model NoteAccessLog {
- Liste des 3 meilleures prédictions
- Badge `cyclique` / `tendance` selon le type
- Clic → ouvre la note
+
+---
+
+## US-NEXTGEN-EDITOR — Éditeur Next-Gen : Drag Handle + Menu Bloc + DB Inline + Smart Paste
+
+> **Status :** PLANIFIÉ
+> **Depends on :** US-LIVING-BLOCKS (UniqueID et transclusion), US-STRUCTURED-VIEWS (NotebookSchema et propriétés)
+> **Spec détaillée :** `docs/story-nextgen-editor.md` (4 sous-stories : US-1 Drag Handle, US-2 Menu Action Bloc, US-3 Smart Paste, US-4 Database Inline)
+
+**Contexte :**
+L'éditeur actuel est un document linéaire classique. Pour rivaliser avec Notion tout en étant plus performant, on implémente une approche hybride : un unique bouton de poignée en ProseMirror pur (pas de composant React lourd par paragraphe), un menu contextuel de bloc, une transclusion au collage, et un bloc database inline.
+
+**4 sous-stories** (détail dans `docs/story-nextgen-editor.md`) :
+
+### US-1 : Poignée de Glissement Gutter Unique (Hover Drag Handle)
+- Bouton flottant unique dans la marge gauche, suit le curseur de bloc en bloc
+- Un seul élément DOM repositionné (pas de duplication)
+- Drag & drop de blocs avec indicateur de ligne d'insertion
+- Masqué sur mobile/tactile
+
+### US-2 : Menu Action Rapide de Bloc
+- Clic sur la poignée -> dropdown glassmorphism
+- Actions : Supprimer, Dupliquer, Transformer en (H1/H2/H3/liste/todo/citation/code/database), Copier la référence
+- Utilise le `UniqueID` TipTap pour les références stables
+
+### US-3 : Transclusion intelligente au Collage (Smart Paste)
+- Collage d'un lien de bloc -> menu inline : "Bloc Connecté (Live)" ou "Texte simple"
+- Insère un nœud `liveBlock` synchronisé via Redis Pub/Sub
+
+### US-4 : Bloc de Base de Données Relationnelle Inline
+- Slash `/database` -> insère un React NodeView
+- Vues Tableau / Fiches avec Rollup dynamique
+- Modèle relationnel local (auteurs/livres par défaut, ou lié au carnet)
+
+### Fichiers
+- `[NEW]` `tiptap-drag-handle-plugin.ts` — Plugin ProseMirror pur
+- `[NEW]` `tiptap-database-block-extension.tsx` — NodeView React
+- `[MODIFY]` `rich-text-editor.tsx` — Intégration drag handle + DB + paste intercept
+- `[MODIFY]` `globals.css` — Gutter, poignée, glassmorphic dropdowns
+
+---
+
+## US-EDITOR-PERF — Performance de Frappe TipTap (Quick Wins)
+
+> **Status :** PLANIFIÉ
+> **Depends on :** rien (modifications config TipTap, ~2h)
+> **Priorité :** HAUTE — impact immédiat sur le ressenti de saisie
+> **Source recherche :** TipTap 2.5 (mai 2026), TipTap docs performance, PR #7828
+
+**Contexte :**
+Actuellement `rich-text-editor.tsx` utilise `immediatelyRender: false` mais pas `shouldRerenderOnTransaction` ni `useEditorState`. TipTap re-render le composant React à chaque transaction (frappe, déplacement curseur, sélection) — ce qui ajoute de la latence. Obsidian atteint <16ms de latence (local-first), Notion 50-150ms (cloud). Momento est local mais se comporte comme Notion à cause de ces re-renders inutiles.
+
+**En tant qu'utilisateur**, je veux que la frappe dans l'éditeur soit instantanée, sans aucun décalage perceptible, même sur des notes longues avec de nombreux blocs.
+
+### 1. shouldRerenderOnTransaction: false (1 ligne)
+```typescript
+// rich-text-editor.tsx — useEditor()
+const editor = useEditor({
+ extensions: [...],
+ immediatelyRender: false,
+ shouldRerenderOnTransaction: false, // <-- AJOUTER
+ // ...
+})
+```
+- Le composant React EditorContent ne se re-render plus sur chaque transaction
+- Seul le DOM ProseMirror est mis à jour (ultra-rapide, natif)
+- Gain mesurable : de ~50-100ms par frappe à <16ms
+
+### 2. useEditorState pour la toolbar et les panels
+```typescript
+import { useEditorState } from '@tiptap/react'
+
+// Au lieu de useEditor + editor.isActive() dans le render :
+const { isBold, isItalic, isHeading } = useEditorState({
+ editor,
+ selector: (ctx) => ({
+ isBold: ctx.editor.isActive('bold'),
+ isItalic: ctx.editor.isActive('italic'),
+ isHeading: ctx.editor.isActive('heading'),
+ }),
+})
+```
+- La toolbar et le panneau propriétés ne se re-rendent que quand leur slice d'état change
+- Actuellement, tout le composant editor re-render à chaque frappe -> la toolbar aussi
+
+### 3. Isoler l'éditeur dans un composant dédié
+- Créer `NoteEditorCore` (composant wrapper) qui ne reçoit que les props strictement nécessaires
+- Les re-renders du parent (note-panel, sidebar ouverture, etc.) ne doivent PAS propager dans l'éditeur
+- React.memo sur le wrapper si besoin
+
+### 4. NodeViews : trackNodeViewPosition: false par défaut
+- Les NodeViews React (LiveBlock, Chart, DatabaseBlock) ne doivent pas se re-re-render quand seul leur position dans le document change
+- TipTap PR #7828 (mai 2026) : shallow prop comparison + opt-in position tracking
+- Vérifier que chaque NodeView utilise `stopEvent()`, `ignoreMutation()`, et ne déclenche pas de setState inutile
+
+### 5. Vérification
+- `console.count('editor render')` dans le composant éditeur pour mesurer le nombre de re-renders
+- Objectif : 0 re-render React pendant la frappe pure (seul ProseMirror DOM bouge)
+
+---
+
+## US-EDITOR-UX — Micro-Interactions de Saisie
+
+> **Status :** À FAIRE
+> **Depends on :** US-NEXTGEN-EDITOR (drag handle et menu bloc doivent être en place)
+> **Source recherche :** Mintlify "22 UX improvements" (mai 2026), BlockNote v0.50, BlockNote v0.49
+
+**Contexte :**
+Après les quick wins performance (US-EDITOR-PERF) et le drag handle (US-NEXTGEN-EDITOR), il reste des micro-interactions qui font la différence entre un éditeur "correct" et un éditeur "agréable". Mintlify a listé 22 améliorations UX en mai 2026 — voici les plus pertinentes pour Momento.
+
+**En tant qu'utilisateur**, je veux que chaque interaction courante (insérer un bloc, déplacer du contenu, transformer un format) soit fluide et intuitive, sans recourir à des raccourcis clavier obscurs.
+
+### 1. Sélection globale de blocs (Haute priorité)
+- Shift+clic pour sélectionner plusieurs blocs contigus
+- Drag de sélection multi-blocs
+- Actions groupées : supprimer, déplacer, transformer en masse
+- Visuel : blocs sélectionnés avec fond accent/5, bordure accent/30
+- **Inspiration :** Mintlify "Global Block Selection" — nettoyage de pages longues sans actions répétées
+
+### 2. Slash menu redessiné (Haute priorité)
+- Type-to-search avec catégories visuelles (Texte, Média, Données, Intégré, IA)
+- Navigation clavier fluide (flèches + Entrée, Esc pour fermer)
+- Épinglage des 3-5 commandes les plus utilisées en haut
+- Description courte sous chaque item (ex. "Code" -> "Bloc de code avec coloration syntaxique")
+- Preview visuel au survol pour les blocs complexes (tableau, database, chart)
+- **Inspiration :** Mintlify slash menu redesign, BlockNote catégories
+
+### 3. Placeholders contextuels par type de bloc (Moyenne priorité)
+- Paragraphe vide : "Tapez / pour insérer un bloc..."
+- Heading H1 vide : "Titre principal"
+- Heading H2 vide : "Titre de section"
+- TaskItem vide : "Ajouter une tâche"
+- Bloc de code vide : "Code..."
+- Bullet list vide : "Liste"
+- **Inspiration :** BlockNote "Helpful placeholders"
+
+### 4. Collage intelligent étendu (Moyenne priorité)
+- Coller une URL HTTP(S) -> propose : lien hypertexte / intégration image / intégration vidéo
+- Coller du code (détecté par caractères spéciaux {}[];) -> propose : bloc de code avec auto-détection langage
+- Coller une image depuis le presse-papier -> upload direct (déjà en prod, vérifier la fluidité)
+- Coller du HTML riche -> nettoyage intelligent (conserver structure, supprimer styles inline superflus)
+- **Inspiration :** US-3 Smart Paste existe pour les blocs connectés — étendre au contenu générique
+
+### 5. "Turn into" instant (Moyenne priorité)
+- Raccourci clavier : sélectionner du texte + `Cmd+Shift+H` -> cycle H1 > H2 > H3 > paragraphe
+- Via le menu bloc (US-2) : transformation instantanée sans flash ni re-render visible
+- Conserver le contenu et les attributs (gras, liens, etc.) lors de la conversion
+- **Inspiration :** Mintlify "Turn Blocks Into Anything"
+
+### 6. Undo/redo visuel discret (Basse priorité)
+- Toast subtil (2s) : "Action annulée" / "Action rétablie"
+- Raccourci affiché dans le toast : "Cmd+Z pour annuler, Cmd+Shift+Z pour rétablir"
+- Ne pas utiliser toast pour les actions normales — seulement undo/redo pour confirmer le feedback
+
+---
+
+## US-EDITOR-MOBILE — Expérience Tactile & Toolbar Mobile
+
+> **Status :** À FAIRE
+> **Depends on :** US-NEXTGEN-EDITOR (drag handle existant)
+> **Source recherche :** Notion mobile app, Obsidian mobile, benchmark 2026
+
+**Contexte :**
+L'éditeur fonctionne sur mobile mais l'expérience est dégradée : la bubble menu est trop petite pour les doigts, le drag handle est masqué (prévu) mais il n'y a pas d'alternative tactile, et les sélections longues sont douloureuses en contenteditable.
+
+**En tant qu'utilisateur mobile**, je veux pouvoir éditer mes notes confortablement depuis mon téléphone ou tablette, avec des contrôles adaptés au tactile.
+
+### 1. Toolbar mobile adaptée
+- Remplacer la bubble menu desktop par une toolbar fixe en bas d'écran (viewport < 768px)
+- Boutons 44x44px minimum (Apple HIG)
+- 8 actions principales : Gras, Italique, Surligner, Lien, Liste, Titre, Code, Plus (menu étendu)
+- Scroll horizontal si plus d'actions
+- **Inspiration :** Notion mobile toolbar
+
+### 2. Menu bloc tactile
+- Pas de drag handle sur mobile (déjà prévu dans US-1)
+- Alternative : swipe gauche sur un bloc -> reveal actions (supprimer, dupliquer, transformer)
+- Ou : tap long sur un bloc -> menu contextuel mobile natif (action sheet iOS / bottom sheet Android)
+- Bouton "Déplacer" dans le menu -> mode réorganisation avec poignées tactiles
+
+### 3. Sélection de texte améliorée
+- Les sélections longues en contenteditable sont frustrantes sur mobile
+- Ajouter un bouton "Sélectionner tout le bloc" dans le menu bloc tactile
+- Double-tap sélectionne le mot, triple-tap sélectionne le paragraphe (comportement natif iOS/Android, vérifier que TipTap ne l'écrase pas)
+
+### 4. Performance mobile
+- Les NodeViews React lourds (Chart, DatabaseBlock) doivent avoir un fallback léger sur mobile
+- Désactiver les animations de la bubble menu sur mobile (prefers-reduced-motion)
+- `immediatelyRender: false` déjà en place — bon pour le premier rendu mobile
+
+---
+
+## US-EDITOR-MARKDOWN — Rendu WYSIWYG Markdown Fidèle
+
+> **Status :** À FAIRE (low priority)
+> **Depends on :** rien (évaluation Milkdown)
+> **Source recherche :** Milkdown v7.20, "Human Markdown" extension VSCode, round-trip byte-for-byte
+
+**Contexte :**
+Momento stocke les notes en HTML (TipTap). Mais les notes de type `markdown` existent aussi. Le problème classique : éditer en riche et voir le Markdown reformatté intégralement (indentations changées, lignes vides supprimées, `##` convertis en soulignements). Milkdown (11k+ stars, ProseMirror + remark) résout ce problème avec un round-trip byte-for-byte.
+
+**En tant qu'utilisateur**, je veux que mes fichiers Markdown restent intacts quand je les édite en mode visuel — pas de diff parasite sur chaque modification.
+
+### 1. Évaluation technique Milkdown
+- Installer `@milkdown/core` + `@milkdown/preset-commonmark` + `@milkdown/preset-gfm`
+- Benchmarker le round-trip : ouvrir un .md, éditer un paragraphe, sauvegarder, vérifier que seul ce paragraphe change dans le diff
+- Comparer avec la solution actuelle (TipTap HTML storage) pour les notes markdown
+- **Référence :** "Human Markdown" VSCode extension (Milkdown + byte-for-byte tests)
+
+### 2. Mode d'édition dual (si Milkdown adopté)
+- Toggle dans la toolbar : mode Visuel (WYSIWYG) / mode Brut (CodeMirror)
+- Transition en <100ms, même position de scroll
+- Le mode Visuel affiche le rendu live (titres, listes, tables, images, checkboxes cliquables)
+- Le mode Brut affiche le Markdown source avec coloration syntaxique
+- **Inspiration :** "Human Markdown" — Cmd+Shift+V pour basculer
+
+### 3. Intégration avec les notes existantes
+- Notes type `richtext` : inchangé (stockage HTML TipTap)
+- Notes type `markdown` : mode dual avec Milkdown
+- Détection automatique du type à l'ouverture
+- Conversion possible : `richtext` -> `markdown` (export) et `markdown` -> `richtext` (import)
+
+### 4. Support GFM complet
+- Tables Markdown rendues comme de vrais tableaux éditables
+- Task lists avec checkboxes cliquables
+- Footnotes rendues inline
+- Frontmatter YAML en carte collapsible en haut de document
+- **Inspiration :** "Human Markdown" — GFM complet, Shiki pour le code
diff --git a/mcp-server/node_modules/.package-lock.json b/mcp-server/node_modules/.package-lock.json
index 1c3a6c2..d78be4e 100644
--- a/mcp-server/node_modules/.package-lock.json
+++ b/mcp-server/node_modules/.package-lock.json
@@ -1,9 +1,26 @@
{
"name": "memento-mcp-server",
- "version": "3.1.0",
+ "version": "3.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
+ "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/@hono/node-server": {
"version": "1.19.7",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
@@ -16,6 +33,13 @@
"hono": "^4"
}
},
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
@@ -392,6 +416,27 @@
"@prisma/debug": "5.22.0"
}
},
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "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/node": {
"version": "20.19.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
@@ -402,6 +447,119 @@
"undici-types": "~6.21.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/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -454,6 +612,16 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "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/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -514,6 +682,16 @@
"node": ">= 0.8"
}
},
+ "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/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -543,6 +721,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -615,6 +820,16 @@
"ms": "2.0.0"
}
},
+ "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/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -681,6 +896,13 @@
"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/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -693,12 +915,61 @@
"node": ">= 0.4"
}
},
+ "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/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
+ "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/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -729,6 +1000,16 @@
"node": ">=18.0.0"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@@ -1024,6 +1305,23 @@
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
},
+ "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/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/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1099,6 +1397,25 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "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/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1174,6 +1491,30 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"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/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/pkce-challenge": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
@@ -1183,6 +1524,35 @@
"node": ">=16.20.0"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "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.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/prisma": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
@@ -1264,6 +1634,58 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rollup": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -1483,6 +1905,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -1492,6 +1938,57 @@
"node": ">= 0.8"
}
},
+ "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/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/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/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1548,6 +2045,205 @@
"node": ">= 0.8"
}
},
+ "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/vite-node/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/vite-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/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/vitest/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/vitest/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/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -1563,6 +2259,23 @@
"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/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/memento-note/app/(main)/home/page.tsx b/memento-note/app/(main)/home/page.tsx
index f0bfd0b..ce3ba09 100644
--- a/memento-note/app/(main)/home/page.tsx
+++ b/memento-note/app/(main)/home/page.tsx
@@ -2,12 +2,7 @@ import { cookies } from 'next/headers'
import { getAllNotes } from '@/app/actions/notes'
import { getAISettings } from '@/app/actions/ai-settings'
import { HomeClient } from '@/components/home-client'
-import {
- NOTES_LAYOUT_COOKIE,
- NOTES_VIEW_TYPE_COOKIE,
- parseNotesLayoutMode,
- parseNotesViewType,
-} from '@/lib/notes-view-preference'
+import { NOTES_LAYOUT_COOKIE, parseNotesLayoutMode } from '@/lib/notes-view-preference'
export default async function HomePage() {
const [allNotes, settings, cookieStore] = await Promise.all([
@@ -17,13 +12,11 @@ export default async function HomePage() {
])
const initialLayoutMode = parseNotesLayoutMode(cookieStore.get(NOTES_LAYOUT_COOKIE)?.value)
- const initialViewType = parseNotesViewType(cookieStore.get(NOTES_VIEW_TYPE_COOKIE)?.value)
return (
]*data-id="${blockId}"[^>]*>([\\s\\S]*?)<\\/(?:p|h[1-6]|blockquote)>`,
- 'i'
- )
- const match = regex.exec(html)
- if (!match) return null
- return match[1].replace(/<[^>]+>/g, '').trim()
-}
+import { extractBlockContentById } from '@/lib/blocks/extract-blocks'
// GET /api/blocks/[blockId]/status?sourceNoteId=xxx
export async function GET(
@@ -38,7 +29,7 @@ export async function GET(
return NextResponse.json({ exists: false, content: '', sourceNoteTitle: '' })
}
- const content = extractBlockContent(note.content, blockId)
+ const content = extractBlockContentById(note.content, blockId)
if (content === null) {
return NextResponse.json({ exists: false, content: '', sourceNoteTitle: note.title || '' })
diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css
index c8e43bf..1a1759e 100644
--- a/memento-note/app/globals.css
+++ b/memento-note/app/globals.css
@@ -1035,106 +1035,59 @@ html.font-system * {
/* --- Editor Wrapper --- */
.notion-editor-wrapper {
position: relative;
- padding-left: 36px; /* Espace gutter pour la poignée */
}
-/* --- Drag Handle Gutter --- */
-.notion-drag-handle {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 28px; /* Légèrement plus grand pour faciliter la sélection */
- border-radius: 6px;
- color: var(--muted-foreground);
- /* Animation de glissement fluide (top) et d'échelle au clic */
- transition: opacity 0.2s ease,
- top 0.12s cubic-bezier(0.25, 1, 0.5, 1),
- background-color 0.15s ease,
- color 0.15s ease,
- transform 0.15s ease;
- user-select: none;
-}
-
-.notion-drag-handle:hover {
- background-color: rgba(59, 130, 246, 0.15); /* blue-500 @ 15% */
- color: #3b82f6; /* blue-500 */
-}
-
-.dark .notion-drag-handle:hover {
- background-color: rgba(59, 130, 246, 0.25);
- color: #60a5fa; /* blue-400 */
-}
-
-.notion-drag-handle:active {
- transform: scale(0.9);
- cursor: grabbing;
-}
-
-.notion-drag-handle-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- background: none;
- border: none;
- padding: 0;
- margin: 0;
- cursor: pointer;
- color: inherit;
- outline: none;
-}
-
-/* --- Styles pour l'extension officielle Tiptap DragHandle --- */
+/* --- Drag Handle (Novel / global-drag-handle) --- */
.drag-handle {
- position: absolute;
- z-index: 50;
- display: flex;
- align-items: stretch;
- justify-content: center;
- user-select: none;
- cursor: grab;
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.15s ease, transform 0.12s ease;
- transform: translateX(0);
-}
-
-.drag-handle.visible {
- opacity: 1;
- pointer-events: auto;
-}
-
-.drag-handle[data-dragging="true"] {
- cursor: grabbing;
- opacity: 1;
-}
-
-/* Inner content wrapper - positioned in gutter */
-.drag-handle > .notion-drag-handle {
+ position: fixed;
display: flex;
align-items: center;
justify-content: center;
- width: 24px;
- min-height: 28px;
- border-radius: 6px;
+ width: 1.25rem;
+ height: 1.5rem;
+ border-radius: 4px;
color: var(--muted-foreground);
- background: transparent;
- transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
- flex-shrink: 0;
+ opacity: 1;
+ z-index: 50;
+ cursor: grab;
+ user-select: none;
+ transition: opacity 0.15s ease, background-color 0.15s ease;
}
-.drag-handle.visible > .notion-drag-handle:hover {
- background-color: rgba(59, 130, 246, 0.15);
+.drag-handle:hover {
+ background-color: rgba(59, 130, 246, 0.12);
color: #3b82f6;
}
-.dark .drag-handle.visible > .notion-drag-handle:hover {
- background-color: rgba(59, 130, 246, 0.25);
+.dark .drag-handle:hover {
+ background-color: rgba(59, 130, 246, 0.22);
color: #60a5fa;
}
-.drag-handle:active > .notion-drag-handle {
- transform: scale(0.9);
+.drag-handle:active {
+ cursor: grabbing;
+}
+
+.drag-handle.hide {
+ opacity: 0;
+ pointer-events: none;
+}
+
+@media (pointer: coarse) {
+ .drag-handle {
+ display: none !important;
+ }
+}
+
+/* Curseur pendant le drag de bloc */
+.notion-editor-wrapper .ProseMirror.dragging {
+ cursor: grabbing;
+}
+
+/* Pas de surbrillance node pendant le drag (Novel) */
+.notion-editor-wrapper .ProseMirror:not(.dragging) .ProseMirror-selectednode,
+.notion-editor-wrapper .ProseMirror:not(.dragging) .ProseMirror-selectednoderange {
+ outline: none !important;
}
/* --- Block Action Menu (glassmorphism dropdown) --- */
@@ -1197,6 +1150,30 @@ html.font-system * {
color: #f87171;
}
+.smart-paste-menu {
+ min-width: 240px;
+ padding: 8px 4px 4px;
+}
+
+.smart-paste-menu__hint {
+ margin: 0 12px 4px;
+ font-size: 11px;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+ opacity: 0.55;
+}
+
+.smart-paste-menu__source {
+ margin: 0 12px 8px;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.block-action-separator {
height: 1px;
margin: 4px 8px;
@@ -1233,6 +1210,296 @@ html.font-system * {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
+/* --- Inline Database Block (US-NEXTGEN-EDITOR) --- */
+.database-block-wrapper {
+ margin: 0.75rem 0;
+}
+
+.database-block__inner {
+ border: 1px solid #e8e6e3;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.6);
+ padding: 1rem 1.1rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+}
+
+.dark .database-block__inner {
+ border-color: rgba(255, 255, 255, 0.08);
+ background: rgba(24, 24, 27, 0.5);
+}
+
+.database-block__header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ margin-bottom: 0.35rem;
+}
+
+.database-block__title {
+ display: block;
+ font-size: 0.9rem;
+ font-weight: 700;
+ color: var(--color-ink, #1a1a1a);
+}
+
+.database-block__id {
+ display: block;
+ font-size: 0.65rem;
+ font-family: ui-monospace, monospace;
+ opacity: 0.45;
+ margin-top: 0.1rem;
+}
+
+.database-block__hint {
+ font-size: 0.72rem;
+ opacity: 0.55;
+ margin: 0 0 0.85rem;
+}
+
+.database-block__view-toggle {
+ display: inline-flex;
+ padding: 2px;
+ border-radius: 8px;
+ background: rgba(0, 0, 0, 0.04);
+ border: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.dark .database-block__view-toggle {
+ background: rgba(255, 255, 255, 0.04);
+ border-color: rgba(255, 255, 255, 0.08);
+}
+
+.database-block__view-toggle button {
+ border: none;
+ background: transparent;
+ font-size: 0.68rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 0.35rem 0.65rem;
+ border-radius: 6px;
+ cursor: pointer;
+ opacity: 0.65;
+ color: inherit;
+}
+
+.database-block__view-toggle button.is-active {
+ background: white;
+ opacity: 1;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
+}
+
+.dark .database-block__view-toggle button.is-active {
+ background: rgba(255, 255, 255, 0.12);
+}
+
+.database-block__table-wrap {
+ overflow-x: auto;
+ border-radius: 8px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.dark .database-block__table-wrap {
+ border-color: rgba(255, 255, 255, 0.08);
+}
+
+.database-block__table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.8rem;
+}
+
+.database-block__table th {
+ text-align: left;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.65rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ opacity: 0.55;
+ background: rgba(0, 0, 0, 0.02);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.database-block__table td {
+ padding: 0.55rem 0.75rem;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.04);
+ vertical-align: top;
+}
+
+.database-block__works-cell {
+ max-width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ opacity: 0.85;
+}
+
+.database-block__rollup {
+ text-align: right;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+}
+
+.database-block__delete-btn {
+ border: none;
+ background: transparent;
+ font-size: 0.65rem;
+ opacity: 0.45;
+ cursor: pointer;
+ color: #ef4444;
+}
+
+.database-block__delete-btn:hover {
+ opacity: 1;
+}
+
+.database-block__inline-form,
+.database-block__book-form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.75rem;
+ padding-top: 0.75rem;
+ border-top: 1px dashed rgba(0, 0, 0, 0.08);
+}
+
+.database-block__book-form {
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.database-block__form-label,
+.database-block__form-heading {
+ font-size: 0.68rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ opacity: 0.55;
+ width: 100%;
+}
+
+.database-block__input {
+ font-size: 0.8rem;
+ padding: 0.45rem 0.65rem;
+ border-radius: 8px;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ background: white;
+ color: inherit;
+}
+
+.dark .database-block__input {
+ background: rgba(0, 0, 0, 0.25);
+ border-color: rgba(255, 255, 255, 0.1);
+}
+
+.database-block__primary-btn,
+.database-block__submit {
+ border: none;
+ border-radius: 8px;
+ background: #3b82f6;
+ color: white;
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 0.45rem 0.85rem;
+ cursor: pointer;
+}
+
+.database-block__primary-btn:hover,
+.database-block__submit:hover {
+ background: #2563eb;
+}
+
+.database-block__card {
+ position: relative;
+ border-radius: 10px;
+ overflow: hidden;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ background: white;
+}
+
+.dark .database-block__card {
+ background: rgba(0, 0, 0, 0.2);
+ border-color: rgba(255, 255, 255, 0.08);
+}
+
+.database-block__card-delete {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ z-index: 2;
+ border: none;
+ border-radius: 6px;
+ padding: 4px;
+ background: rgba(0, 0, 0, 0.45);
+ color: white;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.database-block__card:hover .database-block__card-delete {
+ opacity: 1;
+}
+
+.database-block__card-cover {
+ aspect-ratio: 4 / 3;
+ overflow: hidden;
+ background: #f4f4f5;
+}
+
+.database-block__card-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.database-block__card-body {
+ padding: 0.55rem 0.65rem 0.65rem;
+}
+
+.database-block__card-title {
+ font-size: 0.78rem;
+ font-weight: 700;
+ line-height: 1.3;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.database-block__tag {
+ font-size: 0.62rem;
+ font-weight: 600;
+ padding: 0.15rem 0.4rem;
+ border-radius: 4px;
+}
+
+.database-block__tag--author {
+ background: rgba(59, 130, 246, 0.12);
+ color: #2563eb;
+}
+
+.database-block__tag--genre {
+ background: rgba(0, 0, 0, 0.06);
+ opacity: 0.75;
+}
+
+.database-block__card-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 140px;
+ border-radius: 10px;
+ border: 2px dashed rgba(0, 0, 0, 0.08);
+ opacity: 0.5;
+ text-align: center;
+ padding: 1rem;
+}
+
/* --- Drop Indicator Line --- */
.notion-drop-indicator {
position: absolute;
@@ -1254,17 +1521,30 @@ html.font-system * {
}
/* --- Selected Node Visual Highlight during Drag --- */
-.notion-editor-wrapper .ProseMirror-selectednode {
+.notion-editor-wrapper .ProseMirror-selectednode,
+.notion-editor-wrapper .ProseMirror-selectednoderange {
+ position: relative;
outline: none !important;
background-color: rgba(59, 130, 246, 0.08) !important;
border-radius: 6px;
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4) !important;
- transition: background-color 0.2s ease, box-shadow 0.2s ease;
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35) !important;
}
-.dark .notion-editor-wrapper .ProseMirror-selectednode {
- background-color: rgba(96, 165, 250, 0.15) !important;
- box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.5) !important;
+.notion-editor-wrapper .ProseMirror-selectednode::before,
+.notion-editor-wrapper .ProseMirror-selectednoderange::before {
+ content: '';
+ position: absolute;
+ inset: -0.25rem;
+ background-color: rgba(59, 130, 246, 0.08);
+ border-radius: 0.2rem;
+ pointer-events: none;
+ z-index: -1;
+}
+
+.dark .notion-editor-wrapper .ProseMirror-selectednode,
+.dark .notion-editor-wrapper .ProseMirror-selectednoderange {
+ background-color: rgba(96, 165, 250, 0.12) !important;
+ box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.45) !important;
}
/* --- Premium Drag Target Line (Dropcursor) --- */
@@ -1287,10 +1567,13 @@ html.font-system * {
.notion-editor-wrapper .ProseMirror {
outline: none;
min-height: 120px;
- padding: 4px 0;
+ /* Gutter à gauche pour la poignée globale (Novel) */
+ padding: 4px 0 4px 3rem;
font-size: 0.9375rem;
line-height: 1.7;
caret-color: var(--primary);
+ user-select: text;
+ -webkit-user-select: text;
}
.notion-editor-wrapper .ProseMirror>*:first-child {
@@ -1341,18 +1624,21 @@ html.font-system * {
/* --- Lists --- */
.notion-editor-wrapper .ProseMirror ul {
list-style-type: disc;
- padding-inline-start: 1.5rem;
+ list-style-position: inside;
+ padding-inline-start: 0;
margin: 0.25em 0;
}
.notion-editor-wrapper .ProseMirror ol {
list-style-type: decimal;
- padding-inline-start: 1.5rem;
+ list-style-position: inside;
+ padding-inline-start: 0;
margin: 0.25em 0;
}
.notion-editor-wrapper .ProseMirror li>p {
margin: 0.1em 0;
+ padding-inline-start: 0.25rem;
}
.notion-editor-wrapper .ProseMirror li>ul,
@@ -2262,6 +2548,7 @@ html.font-system * {
line-height: 1.875;
color: var(--foreground);
outline: none;
+ padding-inline-start: 0;
}
.fullpage-editor .ProseMirror p {
diff --git a/memento-note/components/block-action-menu.tsx b/memento-note/components/block-action-menu.tsx
new file mode 100644
index 0000000..31b1cfd
--- /dev/null
+++ b/memento-note/components/block-action-menu.tsx
@@ -0,0 +1,225 @@
+'use client'
+
+import { useEffect, useRef, useState, useCallback } from 'react'
+import { createPortal } from 'react-dom'
+import { useLanguage } from '@/lib/i18n'
+import type { Editor } from '@tiptap/core'
+import type { Node as PMNode } from '@tiptap/pm/model'
+import { copyTextToClipboard } from '@/lib/editor/copy-text-to-clipboard'
+import { ensureBlockReferenceId } from '@/lib/editor/block-reference-id'
+import { rememberBlockReference } from '@/lib/editor/parse-block-reference'
+import { toast } from 'sonner'
+import {
+ Trash2, Copy, Repeat, Link, ChevronRight,
+ Heading1, Heading2, Heading3, List, ListOrdered,
+ CheckSquare, Quote, CodeXml, Database,
+} from 'lucide-react'
+import { replaceBlockWithDatabase } from '@/components/tiptap-database-block-extension'
+
+interface BlockActionMenuProps {
+ editor: Editor
+ onClose: () => void
+ anchorRect: DOMRect
+ blockPos: number
+ blockNode: PMNode | null
+ noteId?: string
+ sourceNoteTitle?: string
+ onBlockReferenceCopied?: (html: string) => void
+}
+
+type TurnIntoType =
+ | 'heading1' | 'heading2' | 'heading3'
+ | 'bulletList' | 'orderedList' | 'taskList'
+ | 'blockquote' | 'codeBlock' | 'database'
+
+interface TurnIntoOption {
+ id: TurnIntoType
+ icon: typeof Heading1
+ command?: (editor: Editor) => void
+ isDatabase?: boolean
+}
+
+const TURN_INTO_OPTIONS: TurnIntoOption[] = [
+ { id: 'heading1', icon: Heading1, command: (e) => e.chain().focus().toggleHeading({ level: 1 }).run() },
+ { id: 'heading2', icon: Heading2, command: (e) => e.chain().focus().toggleHeading({ level: 2 }).run() },
+ { id: 'heading3', icon: Heading3, command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run() },
+ { id: 'bulletList', icon: List, command: (e) => e.chain().focus().toggleBulletList().run() },
+ { id: 'orderedList', icon: ListOrdered, command: (e) => e.chain().focus().toggleOrderedList().run() },
+ { id: 'taskList', icon: CheckSquare, command: (e) => e.chain().focus().toggleTaskList().run() },
+ { id: 'blockquote', icon: Quote, command: (e) => e.chain().focus().toggleBlockquote().run() },
+ { id: 'codeBlock', icon: CodeXml, command: (e) => e.chain().focus().toggleCodeBlock().run() },
+ { id: 'database', icon: Database, isDatabase: true },
+]
+
+function focusBlock(editor: Editor, blockPos: number) {
+ const docSize = editor.state.doc.content.size
+ const cursorPos = Math.min(blockPos + 1, docSize)
+ editor.chain().focus().setTextSelection(cursorPos).run()
+}
+
+function getBlockPlainContent(editor: Editor, blockPos: number, blockNode: PMNode | null): string {
+ const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null)
+ if (!node || blockPos < 0) return ''
+ const from = blockPos + 1
+ const to = blockPos + node.nodeSize - 1
+ if (to > from) {
+ return editor.state.doc.textBetween(from, to, '\n', '\0').trim()
+ }
+ return node.textContent?.trim() ?? ''
+}
+
+export function BlockActionMenu({
+ editor,
+ onClose,
+ anchorRect,
+ blockPos,
+ blockNode,
+ noteId,
+ sourceNoteTitle,
+ onBlockReferenceCopied,
+}: BlockActionMenuProps) {
+ const { t } = useLanguage()
+ const menuRef = useRef(null)
+ const [showTurnInto, setShowTurnInto] = useState(false)
+
+ const handleDelete = useCallback(() => {
+ if (blockNode && blockPos >= 0) {
+ editor.chain().focus().deleteRange({ from: blockPos, to: blockPos + blockNode.nodeSize }).run()
+ }
+ onClose()
+ }, [editor, blockNode, blockPos, onClose])
+
+ const handleDuplicate = useCallback(() => {
+ if (blockNode && blockPos >= 0) {
+ const insertPos = blockPos + blockNode.nodeSize
+ editor.view.dispatch(
+ editor.state.tr.insert(insertPos, blockNode.copy())
+ )
+ editor.commands.focus()
+ }
+ onClose()
+ }, [editor, blockNode, blockPos, onClose])
+
+ const handleCopyRef = useCallback(async () => {
+ if (!noteId?.trim()) {
+ toast.error(t('blockAction.copyRefNoNote'))
+ onClose()
+ return
+ }
+
+ const blockId = ensureBlockReferenceId(editor, blockPos, blockNode)
+ if (!blockId) {
+ toast.error(t('blockAction.copyRefUnsupported'))
+ onClose()
+ return
+ }
+
+ const html = editor.getHTML()
+ const blockContent = getBlockPlainContent(editor, blockPos, blockNode)
+ onBlockReferenceCopied?.(html)
+
+ const ref = `${window.location.origin}/home?openNote=${encodeURIComponent(noteId)}#block-${encodeURIComponent(blockId)}`
+ const copied = await copyTextToClipboard(ref)
+ if (copied) {
+ rememberBlockReference(ref, { blockContent, sourceNoteTitle })
+ toast.success(t('blockAction.copied'))
+ } else {
+ toast.error(t('blockAction.copyRefFailed'))
+ }
+ onClose()
+ }, [blockNode, blockPos, editor, noteId, onBlockReferenceCopied, onClose, sourceNoteTitle, t])
+
+ const handleTurnInto = useCallback((option: TurnIntoOption) => {
+ if (blockPos >= 0 && blockNode) {
+ if (option.isDatabase) {
+ replaceBlockWithDatabase(editor, blockPos, blockNode)
+ } else if (option.command) {
+ focusBlock(editor, blockPos)
+ option.command(editor)
+ }
+ }
+ setShowTurnInto(false)
+ onClose()
+ }, [editor, blockNode, blockPos, onClose])
+
+ useEffect(() => {
+ function handleClickOutside(e: MouseEvent) {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ onClose()
+ }
+ }
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Escape') onClose()
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ document.addEventListener('keydown', handleKeyDown)
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [onClose])
+
+ const menuStyle: React.CSSProperties = {
+ position: 'fixed',
+ left: anchorRect.right + 6,
+ top: anchorRect.top - 4,
+ zIndex: 9999,
+ }
+
+ if (Number(menuStyle.left) > window.innerWidth - 220) {
+ menuStyle.left = anchorRect.left - 210
+ }
+ if (Number(menuStyle.top) + 300 > window.innerHeight) {
+ menuStyle.top = window.innerHeight - 310
+ }
+
+ return createPortal(
+
+
+
+ {t('blockAction.delete')}
+
+
+
+ {t('blockAction.duplicate')}
+
+
+
+
+
setShowTurnInto(!showTurnInto)}
+ onMouseEnter={() => setShowTurnInto(true)}
+ >
+
+ {t('blockAction.turnInto')}
+
+
+
+ {showTurnInto && (
+
+ {TURN_INTO_OPTIONS.map((opt) => (
+ handleTurnInto(opt)}
+ >
+
+ {t(`blockAction.turnInto_${opt.id}`)}
+
+ ))}
+
+ )}
+
+
+
+
{ void handleCopyRef() }}>
+
+ {t('blockAction.copyRef')}
+
+
,
+ document.body
+ )
+}
diff --git a/memento-note/components/database-block-editor.tsx b/memento-note/components/database-block-editor.tsx
new file mode 100644
index 0000000..47b57ca
--- /dev/null
+++ b/memento-note/components/database-block-editor.tsx
@@ -0,0 +1,255 @@
+'use client'
+
+import { useState, useCallback } from 'react'
+import { Trash2 } from 'lucide-react'
+import { useLanguage } from '@/lib/i18n'
+import {
+ type DatabaseBlockData,
+ randomDefaultCover,
+} from '@/lib/editor/database-block-types'
+
+interface DatabaseBlockEditorProps {
+ data: DatabaseBlockData
+ readOnly?: boolean
+ onChange: (data: DatabaseBlockData) => void
+}
+
+export function DatabaseBlockEditor({ data, readOnly, onChange }: DatabaseBlockEditorProps) {
+ const { t } = useLanguage()
+ const { dbId, dbView, dbAuthors, dbBooks } = data
+
+ const [newBookTitle, setNewBookTitle] = useState('')
+ const [newBookAuthor, setNewBookAuthor] = useState('')
+ const [newBookTag, setNewBookTag] = useState('')
+ const [newBookCover, setNewBookCover] = useState('')
+ const [newAuthorName, setNewAuthorName] = useState('')
+
+ const patch = useCallback((partial: Partial) => {
+ onChange({ ...data, ...partial })
+ }, [data, onChange])
+
+ const handleAddAuthor = useCallback(() => {
+ const name = newAuthorName.trim()
+ if (!name) return
+ if (dbAuthors.some((a) => a.name.toLowerCase() === name.toLowerCase())) return
+ patch({
+ dbAuthors: [...dbAuthors, { id: `auth-${Date.now()}`, name }],
+ })
+ setNewAuthorName('')
+ }, [dbAuthors, newAuthorName, patch])
+
+ const handleAddBook = useCallback((e: React.FormEvent) => {
+ e.preventDefault()
+ const title = newBookTitle.trim()
+ let author = newBookAuthor.trim()
+ if (!title || !author) return
+
+ let nextAuthors = [...dbAuthors]
+ if (author === '__new__') return
+ if (!nextAuthors.some((a) => a.name.toLowerCase() === author.toLowerCase())) {
+ nextAuthors = [...nextAuthors, { id: `auth-${Date.now()}`, name: author }]
+ }
+
+ patch({
+ dbAuthors: nextAuthors,
+ dbBooks: [
+ ...dbBooks,
+ {
+ id: `bk-${Date.now()}`,
+ title,
+ author,
+ cover: newBookCover.trim() || randomDefaultCover(),
+ tag: newBookTag.trim() || t('databaseBlock.defaultTag'),
+ },
+ ],
+ })
+ setNewBookTitle('')
+ setNewBookAuthor('')
+ setNewBookTag('')
+ setNewBookCover('')
+ }, [dbAuthors, dbBooks, newBookAuthor, newBookCover, newBookTag, newBookTitle, patch, t])
+
+ const handleDeleteBook = useCallback((id: string) => {
+ patch({ dbBooks: dbBooks.filter((b) => b.id !== id) })
+ }, [dbBooks, patch])
+
+ const handleDeleteAuthor = useCallback((id: string) => {
+ patch({ dbAuthors: dbAuthors.filter((a) => a.id !== id) })
+ }, [dbAuthors, patch])
+
+ return (
+
+
+
+
+
📚
+
+ {t('databaseBlock.title')}
+ id: {dbId}
+
+
+ {!readOnly && (
+
+ patch({ dbView: 'table' })}
+ className={dbView === 'table' ? 'is-active' : ''}
+ >
+ {t('databaseBlock.viewTable')}
+
+ patch({ dbView: 'card' })}
+ className={dbView === 'card' ? 'is-active' : ''}
+ >
+ {t('databaseBlock.viewCards')}
+
+
+ )}
+
+
+
{t('databaseBlock.hint')}
+
+ {dbView === 'table' ? (
+
+
+
+
+
+ {t('databaseBlock.colAuthor')}
+ {t('databaseBlock.colWorks')}
+ {t('databaseBlock.colRollup')}
+ {!readOnly && }
+
+
+
+ {dbAuthors.map((auth) => {
+ const authorBooks = dbBooks.filter(
+ (b) => b.author.toLowerCase() === auth.name.toLowerCase(),
+ )
+ const worksStr = authorBooks.map((b) => b.title).join(', ')
+ || t('databaseBlock.noLinkedWorks')
+ return (
+
+ {auth.name}
+ {worksStr}
+ {authorBooks.length}
+ {!readOnly && (
+
+ handleDeleteAuthor(auth.id)}
+ className="database-block__delete-btn"
+ >
+ {t('databaseBlock.deleteShort')}
+
+
+ )}
+
+ )
+ })}
+
+
+
+
+ {!readOnly && (
+
+ {t('databaseBlock.addAuthor')}
+ setNewAuthorName(e.target.value)}
+ className="database-block__input flex-1"
+ />
+
+ {t('databaseBlock.createAuthor')}
+
+
+ )}
+
+ ) : (
+
+ {dbBooks.map((book) => (
+
+ {!readOnly && (
+
handleDeleteBook(book.id)}
+ className="database-block__card-delete"
+ title={t('databaseBlock.deleteCard')}
+ >
+
+
+ )}
+
+
+
+
+
{book.title}
+
+ {book.author}
+ {book.tag}
+
+
+
+ ))}
+
+ 📖
+ {t('databaseBlock.worksBase')}
+
+ {t('databaseBlock.storedCount', { count: dbBooks.length })}
+
+
+
+ )}
+
+ {!readOnly && (
+
+ {t('databaseBlock.addWork')}
+
+ setNewBookTitle(e.target.value)}
+ className="database-block__input"
+ required
+ />
+ setNewBookAuthor(e.target.value)}
+ className="database-block__input"
+ required
+ >
+ {t('databaseBlock.selectAuthor')}
+ {dbAuthors.map((auth) => (
+ {auth.name}
+ ))}
+
+
+
+ setNewBookTag(e.target.value)}
+ className="database-block__input"
+ />
+ setNewBookCover(e.target.value)}
+ className="database-block__input"
+ />
+
+
+ {t('databaseBlock.insertWork')}
+
+
+ )}
+
+
+ )
+}
diff --git a/memento-note/components/editor-block-drag-handle.tsx b/memento-note/components/editor-block-drag-handle.tsx
new file mode 100644
index 0000000..1421ede
--- /dev/null
+++ b/memento-note/components/editor-block-drag-handle.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+/**
+ * Poignée drag globale (Novel / tiptap-extension-global-drag-handle).
+ * L’extension TipTap positionne cet élément ; le clic ouvre le menu bloc.
+ */
+import { memo, useEffect, useRef } from 'react'
+import type { Editor } from '@tiptap/core'
+import { BLOCK_DRAG_HANDLE_ID, resolveBlockAtDragHandle } from '@/lib/editor/block-at-drag-handle'
+
+type EditorBlockDragHandleProps = {
+ editor: Editor | null
+ onOpenMenu: (anchorRect: DOMRect) => void
+}
+
+export const EditorBlockDragHandle = memo(function EditorBlockDragHandle({
+ editor,
+ onOpenMenu,
+}: EditorBlockDragHandleProps) {
+ const dragStartedRef = useRef(false)
+ const onOpenMenuRef = useRef(onOpenMenu)
+ onOpenMenuRef.current = onOpenMenu
+
+ useEffect(() => {
+ if (!editor || editor.isDestroyed) return
+ const el = document.getElementById(BLOCK_DRAG_HANDLE_ID)
+ if (!el) return
+
+ const onPointerDown = () => {
+ dragStartedRef.current = false
+ }
+
+ const onDragStart = () => {
+ dragStartedRef.current = true
+ }
+
+ const onClick = (e: MouseEvent) => {
+ if (dragStartedRef.current) {
+ dragStartedRef.current = false
+ return
+ }
+ e.preventDefault()
+ e.stopPropagation()
+ const block = resolveBlockAtDragHandle(editor)
+ if (!block) return
+ onOpenMenuRef.current(el.getBoundingClientRect())
+ }
+
+ el.addEventListener('pointerdown', onPointerDown)
+ el.addEventListener('dragstart', onDragStart)
+ el.addEventListener('click', onClick)
+
+ return () => {
+ el.removeEventListener('pointerdown', onPointerDown)
+ el.removeEventListener('dragstart', onDragStart)
+ el.removeEventListener('click', onClick)
+ }
+ }, [editor])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+})
diff --git a/memento-note/components/home-client.tsx b/memento-note/components/home-client.tsx
index 62abe56..129d094 100644
--- a/memento-note/components/home-client.tsx
+++ b/memento-note/components/home-client.tsx
@@ -5,14 +5,11 @@ import { useSearchParams, useRouter } from 'next/navigation'
import dynamic from 'next/dynamic'
import { Note } from '@/lib/types'
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote, deleteNote, togglePin, toggleArchive, updateNote, updateFullOrderWithoutRevalidation } from '@/app/actions/notes'
-import { NotesListViews, type NotesLayoutMode, type NotesClassicLayoutMode, type NotesViewType, isClassicLayoutMode } from '@/components/notes-list-views'
+import { NotesListViews, type NotesLayoutMode, type NotesClassicLayoutMode, isClassicLayoutMode } from '@/components/notes-list-views'
import {
NOTES_LAYOUT_STORAGE_KEY,
- NOTES_VIEW_TYPE_STORAGE_KEY,
parseNotesLayoutMode,
- parseNotesViewType,
setNotesLayoutPreference,
- setNotesViewTypePreference,
} from '@/lib/notes-view-preference'
import { useNotebookSchema } from '@/hooks/use-notebook-schema'
import {
@@ -87,14 +84,12 @@ interface HomeClientProps {
initialNotes: Note[]
initialSettings: InitialSettings
initialLayoutMode?: NotesLayoutMode
- initialViewType?: NotesViewType
}
export function HomeClient({
initialNotes,
initialSettings,
initialLayoutMode = 'list',
- initialViewType = 'notes',
}: HomeClientProps) {
const searchParams = useSearchParams()
const router = useRouter()
@@ -136,7 +131,6 @@ export function HomeClient({
const [selectedTagIds, setSelectedTagIds] = useState([])
const [isTagsExpanded, setIsTagsExpanded] = useState(false)
const [tagSearchQuery, setTagSearchQuery] = useState('')
- const [viewType, setViewType] = useState(initialViewType)
const [layoutMode, setLayoutMode] = useState(initialLayoutMode)
const [addPropertyOpen, setAddPropertyOpen] = useState(false)
const [isEnablingStructured, setIsEnablingStructured] = useState(false)
@@ -164,20 +158,11 @@ export function HomeClient({
useEffect(() => {
const storedLayout = parseNotesLayoutMode(localStorage.getItem(NOTES_LAYOUT_STORAGE_KEY))
- const storedViewType = parseNotesViewType(localStorage.getItem(NOTES_VIEW_TYPE_STORAGE_KEY))
if (storedLayout !== initialLayoutMode) {
setLayoutMode(storedLayout)
setNotesLayoutPreference(storedLayout)
}
- if (storedViewType !== initialViewType) {
- setViewType(storedViewType)
- setNotesViewTypePreference(storedViewType)
- }
- }, [initialLayoutMode, initialViewType])
-
- useEffect(() => {
- setNotesViewTypePreference(viewType)
- }, [viewType])
+ }, [initialLayoutMode])
useEffect(() => {
setNotesLayoutPreference(layoutMode)
@@ -768,17 +753,20 @@ export function HomeClient({
return (
{editingNote ? (
-
+
+
+
) : (
-
- setViewType('notes')}
- className={cn(
- 'px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all',
- viewType === 'notes'
- ? 'bg-foreground text-background shadow-sm'
- : 'text-muted-foreground hover:text-foreground',
- )}
- >
- {t('notes.viewNotes')}
-
- setViewType('tasks')}
- className={cn(
- 'px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all',
- viewType === 'tasks'
- ? 'bg-foreground text-background shadow-sm'
- : 'text-muted-foreground hover:text-foreground',
- )}
- >
- {t('notes.viewTasks')}
-
-
-
- {viewType === 'notes' && (
-
+
selectLayoutMode('grid')}
@@ -1043,9 +1003,8 @@ export function HomeClient({
>
)}
- )}
- {viewType === 'notes' && currentNotebook && structuredModeActive && (
+ {currentNotebook && structuredModeActive && (
setAddPropertyOpen(true)}
@@ -1207,7 +1166,6 @@ export function HomeClient({
handleOpenNoteFresh(note.id, readOnly ?? false)}
onOpenHistory={handleOpenHistory}
diff --git a/memento-note/components/note-editor/index.tsx b/memento-note/components/note-editor/index.tsx
index 2877d28..2e7b553 100644
--- a/memento-note/components/note-editor/index.tsx
+++ b/memento-note/components/note-editor/index.tsx
@@ -1,8 +1,9 @@
'use client'
-import { NoteEditorProvider, useNoteEditorContext } from './note-editor-context'
+import { NoteEditorProvider } from './note-editor-context'
import { NoteEditorFullPage } from './note-editor-full-page'
import { NoteEditorDialog } from './note-editor-dialog'
+import { NoteEditorPeekHost } from './note-editor-peek-host'
import { Note } from '@/lib/types'
interface NoteEditorProps {
@@ -16,11 +17,13 @@ interface NoteEditorProps {
export function NoteEditor({ note, readOnly, onClose, fullPage = false, onNoteSaved }: NoteEditorProps) {
return (
- {fullPage ? (
-
- ) : (
-
- )}
+
+ {fullPage ? (
+
+ ) : (
+
+ )}
+
)
}
@@ -35,4 +38,4 @@ export { NoteEditorProvider } from './note-editor-context'
export { NoteTitleBlock } from './note-title-block'
export { NoteContentArea } from './note-content-area'
export { NoteMetadataSection } from './note-metadata-section'
-export { NoteEditorToolbar } from './note-editor-toolbar'
\ No newline at end of file
+export { NoteEditorToolbar } from './note-editor-toolbar'
diff --git a/memento-note/components/note-editor/note-content-area.tsx b/memento-note/components/note-editor/note-content-area.tsx
index 46fd7db..c05ca96 100644
--- a/memento-note/components/note-editor/note-content-area.tsx
+++ b/memento-note/components/note-editor/note-content-area.tsx
@@ -107,6 +107,7 @@ export function NoteContentArea() {
className="min-h-[280px]"
onImageUpload={uploadImageFile}
noteId={note.id}
+ noteTitle={state.title || note.title}
sourceUrl={note.sourceUrl}
/>
@@ -122,6 +123,7 @@ export function NoteContentArea() {
className="min-h-[200px]"
onImageUpload={uploadImageFile}
noteId={note.id}
+ noteTitle={state.title || note.title}
sourceUrl={note.sourceUrl}
/>
void
}
export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
- const router = useRouter()
const { t, language } = useLanguage()
const { requestAiConsent } = useAiConsent()
const dateLocale = language === 'fr' ? fr : enUS
@@ -66,10 +64,9 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
return (
<>
{/* ── outer container ── */}
-
-
+
{/* ── main scrollable column ── */}
-
+
{/* TOOLBAR */}
setUploadTrigger(v => v + 1)} attachmentsCount={attachmentsCount} />
diff --git a/memento-note/components/note-editor/note-editor-peek-host.tsx b/memento-note/components/note-editor/note-editor-peek-host.tsx
new file mode 100644
index 0000000..f9e2647
--- /dev/null
+++ b/memento-note/components/note-editor/note-editor-peek-host.tsx
@@ -0,0 +1,90 @@
+'use client'
+
+import { useState, useEffect, useCallback, type ReactNode } from 'react'
+import { AnimatePresence } from 'framer-motion'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { toast } from 'sonner'
+import type { Note } from '@/lib/types'
+import { getNoteById } from '@/app/actions/notes'
+import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
+import {
+ NOTE_PEEK_OPEN_EVENT,
+ NOTE_PEEK_CLOSE_EVENT,
+ type NotePeekOpenDetail,
+} from '@/lib/note-peek-sync'
+import { NoteEditorSplitPeek } from './note-editor-split-peek'
+import { useLanguage } from '@/lib/i18n'
+
+interface NoteEditorPeekHostProps {
+ noteId: string
+ fullPage?: boolean
+ children: ReactNode
+}
+
+export function NoteEditorPeekHost({ noteId, fullPage, children }: NoteEditorPeekHostProps) {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const { t, language } = useLanguage()
+ const isRtl = language === 'fa' || language === 'ar'
+ const [peekState, setPeekState] = useState<{ note: Note; blockId?: string } | null>(null)
+
+ useEffect(() => {
+ const onOpenPeek = (event: Event) => {
+ const detail = (event as CustomEvent).detail
+ if (!detail?.noteId) return
+ if (detail.noteId === noteId) return
+
+ void getNoteById(detail.noteId).then((fetched) => {
+ if (fetched) {
+ setPeekState({ note: fetched, blockId: detail.blockId })
+ } else {
+ toast.error(t('notePeek.loadFailed'))
+ }
+ })
+ }
+ const onClosePeek = () => setPeekState(null)
+
+ window.addEventListener(NOTE_PEEK_OPEN_EVENT, onOpenPeek)
+ window.addEventListener(NOTE_PEEK_CLOSE_EVENT, onClosePeek)
+ return () => {
+ window.removeEventListener(NOTE_PEEK_OPEN_EVENT, onOpenPeek)
+ window.removeEventListener(NOTE_PEEK_CLOSE_EVENT, onClosePeek)
+ }
+ }, [noteId, t])
+
+ const handleClosePeek = useCallback(() => {
+ setPeekState(null)
+ }, [])
+
+ const handleOpenPeekFully = useCallback(() => {
+ if (!peekState) return
+ window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
+ detail: { noteId, reason: 'before-peek-full-open' },
+ }))
+ const params = new URLSearchParams(searchParams.toString())
+ params.set('openNote', peekState.note.id)
+ router.replace(params.toString() ? `/home?${params.toString()}` : '/home', { scroll: false })
+ setPeekState(null)
+ }, [noteId, peekState, router, searchParams])
+
+ const shellClass = fullPage
+ ? 'flex flex-1 min-h-0 h-full w-full items-stretch overflow-hidden'
+ : 'relative flex min-h-0 flex-1 flex-col overflow-hidden'
+
+ return (
+
+
{children}
+
+ {peekState && (
+
+ )}
+
+
+ )
+}
diff --git a/memento-note/components/note-editor/note-editor-split-peek.tsx b/memento-note/components/note-editor/note-editor-split-peek.tsx
new file mode 100644
index 0000000..6b13d25
--- /dev/null
+++ b/memento-note/components/note-editor/note-editor-split-peek.tsx
@@ -0,0 +1,104 @@
+'use client'
+
+import { useEffect, useRef } from 'react'
+import { motion } from 'framer-motion'
+import { X, Maximize2 } from 'lucide-react'
+import type { Note } from '@/lib/types'
+import { useLanguage } from '@/lib/i18n'
+import { NoteEditorProvider, useNoteEditorContext } from './note-editor-context'
+import { NoteTitleBlock } from './note-title-block'
+import { NoteContentArea } from './note-content-area'
+import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
+import { fr } from 'date-fns/locale/fr'
+import { enUS } from 'date-fns/locale/en-US'
+
+interface NoteEditorSplitPeekProps {
+ note: Note
+ blockId?: string
+ onClose: () => void
+ onOpenFully: () => void
+}
+
+function PeekEditorBody({ blockId }: { blockId?: string }) {
+ const { note } = useNoteEditorContext()
+ const { t, language } = useLanguage()
+ const dateLocale = language === 'fr' ? fr : enUS
+ const scrollRootRef = useRef(null)
+
+ useEffect(() => {
+ if (!blockId) return
+ const timer = window.setTimeout(() => {
+ const escaped = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(blockId) : blockId
+ const el = scrollRootRef.current?.querySelector(`[data-id="${escaped}"]`)
+ el?.scrollIntoView({ behavior: 'smooth', block: 'center' })
+ }, 450)
+ return () => window.clearTimeout(timer)
+ }, [blockId, note.id])
+
+ return (
+
+
+
+ {formatAbsoluteDateLocalized(new Date(note.contentUpdatedAt), language, 'MMM d, yyyy', dateLocale)}
+
+
+
+
+
+
{t('notePeek.readOnlyHint')}
+
+
+ )
+}
+
+export function NoteEditorSplitPeek({ note, blockId, onClose, onOpenFully }: NoteEditorSplitPeekProps) {
+ const { t, language } = useLanguage()
+ const isRtl = language === 'fa' || language === 'ar'
+
+ return (
+
+
+
+
+ {t('notePeek.label')}
+
+
+
+
+ {t('notePeek.openFully')}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/memento-note/components/notes-editorial-view.tsx b/memento-note/components/notes-editorial-view.tsx
index 7418efd..f6e32f1 100644
--- a/memento-note/components/notes-editorial-view.tsx
+++ b/memento-note/components/notes-editorial-view.tsx
@@ -27,6 +27,7 @@ import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
import { cn } from '@/lib/utils'
+import { useHydrated } from '@/lib/use-hydrated'
type NotesEditorialViewProps = {
notes: Note[]
@@ -364,6 +365,7 @@ export function NotesEditorialView({
const { t, language } = useLanguage()
const { data: session } = useSession()
const { data: allLabels } = useLabelsQuery()
+ const hydrated = useHydrated()
const [aiIllustrationEnabled, setAiIllustrationEnabled] = useState(false)
useEffect(() => {
@@ -388,9 +390,9 @@ export function NotesEditorialView({
return (
onOpen(note)}
>
diff --git a/memento-note/components/notes-list-views.tsx b/memento-note/components/notes-list-views.tsx
index 50cd5d6..ab4ba63 100644
--- a/memento-note/components/notes-list-views.tsx
+++ b/memento-note/components/notes-list-views.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState, useTransition, useEffect, useCallback } from 'react'
+import { useMemo, useState, useEffect, useCallback } from 'react'
import {
DndContext,
DragOverlay,
@@ -28,7 +28,6 @@ import { getNoteDisplayTitle, getNoteFeedImage, getNotePlainExcerpt, prepareNote
import { useLanguage } from '@/lib/i18n'
import { useNotebooks } from '@/context/notebooks-context'
import { useLabelsQuery } from '@/lib/query-hooks'
-import { updateNote } from '@/app/actions/notes'
import { getAISettings } from '@/app/actions/ai-settings'
import { generateNoteIllustrationSvg } from '@/app/actions/note-illustration'
import { LabelBadge } from '@/components/label-badge'
@@ -39,8 +38,6 @@ import { toast } from 'sonner'
import {
Pin,
FileText,
- Link2,
- CheckSquare,
ChevronUp,
ChevronDown,
Wind,
@@ -50,6 +47,7 @@ import {
Loader2,
} from 'lucide-react'
import { cn } from '@/lib/utils'
+import { useHydrated } from '@/lib/use-hydrated'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
@@ -60,52 +58,6 @@ export type NotesClassicLayoutMode = 'grid' | 'list' | 'table'
export function isClassicLayoutMode(mode: NotesLayoutMode): mode is NotesClassicLayoutMode {
return mode === 'grid' || mode === 'list' || mode === 'table'
}
-export type NotesViewType = 'notes' | 'tasks'
-
-type TaskItem = {
- id: string
- noteId: string
- noteTitle: string
- text: string
- completed: boolean
- lineIndex: number
-}
-
-function getNoteTasksStats(content: string) {
- const lines = (content || '').split('\n')
- let total = 0
- let completed = 0
- for (const line of lines) {
- const match = line.match(/^\s*[-*]?\s*\[([ xX])\]\s*(.*)$/)
- if (match) {
- total++
- if (match[1].toLowerCase() === 'x') completed++
- }
- }
- return { completed, total }
-}
-
-function extractTasksFromNotes(notes: Note[]): TaskItem[] {
- const tasks: TaskItem[] = []
- for (const note of notes) {
- const title = note.title?.trim() || 'Sans titre'
- const lines = (note.content || '').split('\n')
- lines.forEach((line, idx) => {
- const match = line.match(/^\s*[-*]?\s*\[([ xX])\]\s*(.*)$/)
- if (match) {
- tasks.push({
- id: `${note.id}-${idx}`,
- noteId: note.id,
- noteTitle: title,
- text: match[2].trim(),
- completed: match[1].toLowerCase() === 'x',
- lineIndex: idx,
- })
- }
- })
- }
- return tasks
-}
function getNotebookColor(notebookId: string | null | undefined, name?: string) {
const colors = [
@@ -247,7 +199,6 @@ export type { NoteCollectionActions } from '@/lib/note-change-sync'
type NotesListViewsProps = {
notes: Note[]
pinnedNotes?: Note[]
- viewType: NotesViewType
layoutMode: NotesLayoutMode
onOpen: (note: Note, readOnly?: boolean) => void
onOpenHistory?: (note: Note) => void
@@ -258,7 +209,6 @@ type NotesListViewsProps = {
export function NotesListViews({
notes,
pinnedNotes = [],
- viewType,
layoutMode,
onOpen,
onOpenHistory,
@@ -275,8 +225,7 @@ export function NotesListViews({
const { data: session } = useSession()
const { notebooks } = useNotebooks()
const { data: allLabels = [] } = useLabelsQuery()
- const [, startTransition] = useTransition()
- const [sortColumn, setSortColumn] = useState<'title' | 'notebook' | 'tasks' | 'modified' | null>(null)
+ const [sortColumn, setSortColumn] = useState<'title' | 'notebook' | 'modified' | null>(null)
const [sortDirection, setSortDirection] = useState<'asc' | 'desc' | null>(null)
const [aiIllustrationEnabled, setAiIllustrationEnabled] = useState(false)
@@ -298,24 +247,7 @@ export function NotesListViews({
return [...pinnedNotes, ...unpinned]
}, [notes, pinnedNotes])
- const extractTasks = useMemo(() => extractTasksFromNotes(allDisplayNotes), [allDisplayNotes])
- const completedTasksCount = extractTasks.filter((task) => task.completed).length
-
- const handleToggleTask = (task: TaskItem) => {
- const note = allDisplayNotes.find((n) => n.id === task.noteId)
- if (!note) return
- const lines = (note.content || '').split('\n')
- const line = lines[task.lineIndex]
- if (!line) return
- const nextChar = task.completed ? ' ' : 'x'
- lines[task.lineIndex] = line.replace(/\[([ xX])\]/, `[${nextChar}]`)
- startTransition(async () => {
- await updateNote(note.id, { content: lines.join('\n') }, { skipRevalidation: true })
- onNotePatch?.(note.id, { content: lines.join('\n') })
- })
- }
-
- const handleSort = (field: 'title' | 'notebook' | 'tasks' | 'modified') => {
+ const handleSort = (field: 'title' | 'notebook' | 'modified') => {
if (sortColumn !== field) {
setSortColumn(field)
setSortDirection('asc')
@@ -339,9 +271,6 @@ export function NotesListViews({
} else if (sortColumn === 'notebook') {
valA = notebooks.find((nb) => nb.id === a.notebookId)?.name?.toLowerCase() || ''
valB = notebooks.find((nb) => nb.id === b.notebookId)?.name?.toLowerCase() || ''
- } else if (sortColumn === 'tasks') {
- valA = getNoteTasksStats(a.content || '').completed
- valB = getNoteTasksStats(b.content || '').completed
} else {
valA = new Date(a.updatedAt).getTime()
valB = new Date(b.updatedAt).getTime()
@@ -357,86 +286,6 @@ export function NotesListViews({
sortDirection === 'asc' ? :
) : null
- if (viewType === 'tasks') {
- return (
-
-
-
-
-
- {t('notes.tasksHeader')}
-
-
-
- {t('notes.tasksSummary')
- .replace('{count}', String(extractTasks.length))
- .replace('{completed}', String(completedTasksCount))}
-
-
-
- {extractTasks.length > 0 ? (
-
-
- {extractTasks.map((task) => (
-
-
- handleToggleTask(task)}
- className={cn(
- 'w-5 h-5 rounded-md border flex items-center justify-center transition-all shrink-0',
- task.completed
- ? 'bg-brand-accent border-brand-accent text-white'
- : 'border-border hover:border-brand-accent/60 bg-transparent',
- )}
- >
- {task.completed && ✓ }
-
-
- {task.text}
-
-
-
-
- {t('notes.taskFromNote').replace('{title}', task.noteTitle)}
-
- {
- const note = allDisplayNotes.find((n) => n.id === task.noteId)
- if (note) onOpen(note)
- }}
- className="p-1.5 rounded-full hover:bg-foreground/5 text-muted-foreground hover:text-foreground transition-all"
- title={t('notes.openSourceNote')}
- >
-
-
-
-
- ))}
-
-
- ) : (
-
-
-
-
-
{t('notes.tasksEmptyTitle')}
-
{t('notes.tasksEmptyHint')}
-
- )}
-
- )
- }
-
if (layoutMode === 'grid') {
return (
handleSort('tasks')}
- >
-
- {t('notes.tableTasks')}
-
-
- handleSort('modified')}
>
@@ -506,7 +347,6 @@ export function NotesListViews({
const title = getNoteDisplayTitle(note, untitled)
const nb = notebooks.find((n) => n.id === note.notebookId)
const nbColor = getNotebookColor(note.notebookId, nb?.name)
- const stats = getNoteTasksStats(note.content || '')
return (
-
- {stats.total > 0 ? (
-
- {stats.completed}/{stats.total} ✓
-
- ) : (
- —
- )}
-
{formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}
@@ -834,9 +665,9 @@ function GridCard({
}: GridCardSharedProps) {
const router = useRouter()
const { t, language } = useLanguage()
+ const hydrated = useHydrated()
const title = getNoteDisplayTitle(note, untitled)
const excerpt = getNotePlainExcerpt(note, 110)
- const stats = getNoteTasksStats(note.content || '')
const formattedDate = formatGridCardDate(note.updatedAt, language)
const handlePinClick = (e: React.MouseEvent) => {
@@ -857,9 +688,9 @@ function GridCard({
return (
onOpen(note)}
className="bg-card/60 border border-border/40 rounded-2xl overflow-hidden hover:shadow-md hover:border-brand-accent/30 transition-all duration-300 group/card cursor-pointer flex flex-col relative h-full"
>
@@ -874,11 +705,6 @@ function GridCard({
)}
- {stats.total > 0 && (
-
- {stats.completed}/{stats.total} ✓
-
- )}
diff --git a/memento-note/components/rich-text-editor.tsx b/memento-note/components/rich-text-editor.tsx
index 90a6fbf..0ec3081 100644
--- a/memento-note/components/rich-text-editor.tsx
+++ b/memento-note/components/rich-text-editor.tsx
@@ -25,13 +25,25 @@ import { ChartExtension } from './tiptap-chart-extension'
import { ChartSuggestionsDialog } from './chart-suggestions-dialog'
import { UniqueIdExtension } from './tiptap-unique-id-extension'
import { LiveBlockExtension } from './tiptap-live-block-extension'
+import { DatabaseBlockExtension, insertDatabaseBlockAtSelection } from './tiptap-database-block-extension'
import { RtlPreserveExtension } from './tiptap-rtl-preserve-extension'
import { ClipArticleExtension } from './tiptap-clip-article-extension'
import { BlockPicker, type BlockSuggestion } from './block-picker'
+import { EditorBlockDragHandle } from './editor-block-drag-handle'
+import { BlockActionMenu } from './block-action-menu'
+import { SmartPasteMenu } from './smart-paste-menu'
+import { globalDragHandleExtensions } from '@/lib/editor/global-drag-handle-extension'
+import { resolveBlockAtDragHandle } from '@/lib/editor/block-at-drag-handle'
+import { parseBlockReferenceFromText, recallLastBlockReference, type ParsedBlockReference } from '@/lib/editor/parse-block-reference'
+import { getEmptyParagraphAtSelection } from '@/lib/editor/empty-paragraph-at-selection'
+import { SmartPasteExtension } from '@/lib/editor/smart-paste-extension'
+import type { Node as PMNode } from '@tiptap/pm/model'
import { detectTextDirection } from '@/lib/clip/rtl-content'
import { stripHtmlToPlainText } from '@/lib/text/plain-text'
import { NoteLinkPicker, type NoteLinkOption } from './note-link-picker'
import { applyClipRtlDirection } from '@/lib/editor/apply-clip-rtl-direction'
+import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
+import { openNotePeek } from '@/lib/note-peek-sync'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import type { Editor } from '@tiptap/core'
import type { EditorState } from '@tiptap/pm/state'
@@ -42,7 +54,7 @@ import {
Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink,
FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus,
- SpellCheck, Languages, BookOpen, Presentation, BarChart3
+ SpellCheck, Languages, BookOpen, Presentation, BarChart3, Database
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
@@ -64,6 +76,7 @@ interface RichTextEditorProps {
placeholder?: string
onImageUpload?: (file: File) => Promise
noteId?: string
+ noteTitle?: string
/** URL source du clip (BBC Persian, etc.) — pour RTL explicite des listes */
sourceUrl?: string | null
}
@@ -86,7 +99,7 @@ type SlashItem = {
type SlashCategoryId = 'basic' | 'media' | 'formatting' | 'ai'
-type SlashMenuItem = SlashItem & { categoryId: SlashCategoryId }
+type SlashMenuItem = SlashItem & { categoryId: SlashCategoryId; slashKeywords?: string[] }
const ORDERED_SLASH_CATEGORIES: SlashCategoryId[] = ['basic', 'media', 'formatting', 'ai']
@@ -176,6 +189,10 @@ const slashCommands: SlashItem[] = [
window.dispatchEvent(new CustomEvent('memento-open-block-picker'))
}
},
+ {
+ title: 'Database', description: 'Inline authors & works database', icon: Database, category: 'Basic blocks', shortcut: '/database',
+ command: (e) => { insertDatabaseBlockAtSelection(e) },
+ },
]
async function aiReformulate(text: string, option: string, language?: string): Promise {
@@ -233,11 +250,28 @@ function useImageInsert() {
}
export const RichTextEditor = forwardRef(
- function RichTextEditor({ content, onChange, className, placeholder, onImageUpload, noteId, sourceUrl }, ref) {
+ function RichTextEditor({ content, onChange, className, placeholder, onImageUpload, noteId, noteTitle, sourceUrl }, ref) {
const { t } = useLanguage()
const { requestAiConsent } = useAiConsent()
const imageInsert = useImageInsert()
const [blockPickerOpen, setBlockPickerOpen] = useState(false)
+ const [blockMenuState, setBlockMenuState] = useState<{
+ anchor: DOMRect
+ pos: number
+ node: PMNode | null
+ } | null>(null)
+ const dragBlockRef = useRef<{ node: PMNode | null; pos: number }>({ node: null, pos: -1 })
+ const smartPastePendingRef = useRef<{
+ reference: ParsedBlockReference
+ blockPos: number
+ blockNode: PMNode
+ blockStatus?: { exists: boolean; content: string; sourceNoteTitle: string }
+ } | null>(null)
+ const [smartPasteMenu, setSmartPasteMenu] = useState<{
+ anchor: { top: number; left: number }
+ reference: ParsedBlockReference
+ sourceNoteTitle?: string
+ } | null>(null)
const [noteLinkPickerOpen, setNoteLinkPickerOpen] = useState(false)
const [noteLinkQuery, setNoteLinkQuery] = useState('')
const noteLinkRangeRef = useRef<{ from: number; to: number } | null>(null)
@@ -355,16 +389,38 @@ export const RichTextEditor = forwardRef {
+ if (event.defaultPrevented) return false
+ if (event.key !== 'Enter' && event.key !== ' ') return false
+ const { from, empty } = view.state.selection
+ if (!empty) return false
+ const textBefore = view.state.doc.textBetween(Math.max(0, from - 32), from, '\n')
+ if (!/\/(database|db)$/i.test(textBefore)) return false
+ event.preventDefault()
+ const slashIdx = textBefore.lastIndexOf('/')
+ const deleteFrom = from - (textBefore.length - slashIdx)
+ const ed = editorInstanceRef.current
+ if (!ed) return false
+ ed.chain().focus().deleteRange({ from: deleteFrom, to: from }).run()
+ if (!insertDatabaseBlockAtSelection(ed)) {
+ toast.error(t('databaseBlock.insertFailed'))
+ }
+ return true
+ },
click: (_view, event) => {
const link = (event.target as HTMLElement).closest('a[href]')
if (!link) return false
@@ -373,11 +429,12 @@ export const RichTextEditor = forwardRef {
+ if (!editor) return
+ editor.storage.liveBlock.hostNoteId = noteId ?? null
+ }, [editor, noteId])
+
+ useEffect(() => {
+ if (!editor) return
+
+ editor.storage.smartPaste.onPaste = (view, event) => {
+ const clipboardText = event.clipboardData?.getData('text/plain') ?? ''
+ let blockRef = parseBlockReferenceFromText(clipboardText)
+ if (!blockRef) {
+ blockRef = recallLastBlockReference()
+ }
+ if (!blockRef) return false
+
+ const emptyParagraph = getEmptyParagraphAtSelection(view.state)
+ if (!emptyParagraph) return false
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ const coords = view.coordsAtPos(view.state.selection.from)
+ smartPastePendingRef.current = {
+ reference: blockRef,
+ blockPos: emptyParagraph.pos,
+ blockNode: emptyParagraph.node,
+ }
+
+ queueMicrotask(() => {
+ setSmartPasteMenu({
+ anchor: { top: coords.bottom, left: coords.left },
+ reference: blockRef,
+ })
+ })
+
+ void fetch(
+ `/api/blocks/${encodeURIComponent(blockRef.blockId)}/status?sourceNoteId=${encodeURIComponent(blockRef.sourceNoteId)}`,
+ )
+ .then((res) => (res.ok ? res.json() : null))
+ .then((data: { content?: string; sourceNoteTitle?: string; exists?: boolean } | null) => {
+ if (smartPastePendingRef.current?.reference.raw !== blockRef.raw) return
+ const recalled = recallLastBlockReference()
+ const sessionFallback =
+ recalled?.raw === blockRef.raw
+ ? {
+ content: recalled.blockContent?.trim() ?? '',
+ sourceNoteTitle: recalled.sourceNoteTitle?.trim() ?? '',
+ }
+ : { content: '', sourceNoteTitle: '' }
+ const exists = Boolean(data?.exists) || sessionFallback.content.length > 0
+ const content = data?.exists ? (data.content ?? '') : (sessionFallback.content || data?.content || '')
+ const sourceNoteTitle = data?.sourceNoteTitle || sessionFallback.sourceNoteTitle || ''
+ smartPastePendingRef.current!.blockStatus = {
+ exists,
+ content,
+ sourceNoteTitle,
+ }
+ setSmartPasteMenu((prev) =>
+ prev?.reference.raw === blockRef.raw
+ ? { ...prev, sourceNoteTitle: sourceNoteTitle || prev.sourceNoteTitle }
+ : prev,
+ )
+ })
+ .catch(() => {})
+
+ return true
+ }
+
+ return () => {
+ editor.storage.smartPaste.onPaste = null
+ }
+ }, [editor])
+
// Chart suggestions dialog state
const [chartSuggestionsOpen, setChartSuggestionsOpen] = useState(false)
const [currentNoteContent, setCurrentNoteContent] = useState(content || '')
@@ -620,6 +751,159 @@ export const RichTextEditor = forwardRef {
+ if (!editor) return
+ const block = resolveBlockAtDragHandle(editor)
+ if (!block) return
+ dragBlockRef.current = block
+ setBlockMenuState({ anchor: anchorRect, pos: block.pos, node: block.node })
+ }, [editor])
+
+ const closeBlockActionMenu = useCallback(() => {
+ setBlockMenuState(null)
+ }, [])
+
+ const closeSmartPasteMenu = useCallback(() => {
+ smartPastePendingRef.current = null
+ setSmartPasteMenu(null)
+ }, [])
+
+ const handleBlockReferenceCopied = useCallback((html: string) => {
+ emitContentChange(html)
+ if (noteId) {
+ window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
+ detail: { noteId, reason: 'block-reference-copy' },
+ }))
+ }
+ }, [emitContentChange, noteId])
+
+ const fetchBlockStatus = useCallback(async (reference: ParsedBlockReference) => {
+ const cached = smartPastePendingRef.current?.blockStatus
+ if (cached && smartPastePendingRef.current?.reference.raw === reference.raw) {
+ return cached
+ }
+
+ const recalled = recallLastBlockReference()
+ const sessionFallback =
+ recalled?.raw === reference.raw
+ ? {
+ content: recalled.blockContent?.trim() ?? '',
+ sourceNoteTitle: recalled.sourceNoteTitle?.trim() ?? '',
+ }
+ : { content: '', sourceNoteTitle: '' }
+
+ const res = await fetch(
+ `/api/blocks/${encodeURIComponent(reference.blockId)}/status?sourceNoteId=${encodeURIComponent(reference.sourceNoteId)}`,
+ )
+ if (!res.ok) {
+ return {
+ exists: sessionFallback.content.length > 0,
+ content: sessionFallback.content,
+ sourceNoteTitle: sessionFallback.sourceNoteTitle,
+ }
+ }
+ const data = await res.json()
+ if (data.exists) {
+ return {
+ exists: true,
+ content: data.content ?? '',
+ sourceNoteTitle: data.sourceNoteTitle ?? '',
+ }
+ }
+ return {
+ exists: sessionFallback.content.length > 0,
+ content: sessionFallback.content || data.content || '',
+ sourceNoteTitle: sessionFallback.sourceNoteTitle || data.sourceNoteTitle || '',
+ }
+ }, [])
+
+ const handleSmartPasteLive = useCallback(async () => {
+ const pending = smartPastePendingRef.current
+ const ed = editorInstanceRef.current
+ if (!pending || !ed) {
+ closeSmartPasteMenu()
+ return
+ }
+
+ const status = await fetchBlockStatus(pending.reference)
+
+ if (noteId) {
+ try {
+ await fetch('/api/blocks/embed', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sourceNoteId: pending.reference.sourceNoteId,
+ blockId: pending.reference.blockId,
+ targetNoteId: noteId,
+ }),
+ })
+ } catch {
+ // Non-fatal
+ }
+ }
+
+ const liveBlockType = ed.schema.nodes.liveBlock
+ if (liveBlockType) {
+ ed.chain()
+ .focus()
+ .command(({ tr, dispatch }) => {
+ const node = liveBlockType.create({
+ sourceNoteId: pending.reference.sourceNoteId,
+ blockId: pending.reference.blockId,
+ snapshotContent: status.content,
+ sourceNoteTitle: status.sourceNoteTitle,
+ })
+ tr.replaceWith(pending.blockPos, pending.blockPos + pending.blockNode.nodeSize, node)
+ if (dispatch) dispatch(tr)
+ return true
+ })
+ .run()
+ emitContentChange(ed.getHTML())
+ if (noteId) {
+ window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
+ detail: { noteId, reason: 'smart-paste-live-block' },
+ }))
+ }
+ }
+
+ closeSmartPasteMenu()
+ }, [closeSmartPasteMenu, emitContentChange, fetchBlockStatus, noteId])
+
+ const handleSmartPastePlain = useCallback(async () => {
+ const pending = smartPastePendingRef.current
+ const ed = editorInstanceRef.current
+ if (!pending || !ed) {
+ closeSmartPasteMenu()
+ return
+ }
+
+ const status = await fetchBlockStatus(pending.reference)
+ const linkHref = pending.reference.raw.match(/^https?:\/\//)
+ ? pending.reference.raw
+ : `${window.location.origin}/home?openNote=${encodeURIComponent(pending.reference.sourceNoteId)}#block-${pending.reference.blockId}`
+ const linkText = status.sourceNoteTitle?.trim() || pending.reference.raw
+
+ ed.chain()
+ .focus()
+ .insertContent({
+ type: 'text',
+ text: linkText,
+ marks: [{
+ type: 'link',
+ attrs: {
+ href: linkHref,
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ },
+ }],
+ })
+ .run()
+
+ emitContentChange(ed.getHTML())
+ closeSmartPasteMenu()
+ }, [closeSmartPasteMenu, emitContentChange, fetchBlockStatus])
+
return (
{editor && (
@@ -645,8 +929,34 @@ export const RichTextEditor = forwardRef
}
+
+
+ {editor && blockMenuState && (
+
+ )}
+
+ {smartPasteMenu && (
+
{ void handleSmartPasteLive() }}
+ onPlain={() => { void handleSmartPastePlain() }}
+ onClose={closeSmartPasteMenu}
+ />
+ )}
+
{imageInsert.open && (
)}
@@ -973,6 +1283,7 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
{ ...slashCommands[27], title: t('richTextEditor.slashSlides'), description: t('richTextEditor.slashSlidesDesc'), categoryId: 'ai' },
{ ...slashCommands[28], title: 'Suggest Charts', description: 'AI suggère des graphiques basés sur votre contenu', categoryId: 'ai' },
{ ...slashCommands[29], title: 'Living Block', description: 'Insérer un bloc vivant depuis une autre note', categoryId: 'basic' },
+ { ...slashCommands[30], title: t('richTextEditor.slashDatabase'), description: t('richTextEditor.slashDatabaseDesc'), categoryId: 'basic', slashKeywords: ['database', 'db', 'base', 'données', 'donnees'] },
{
title: t('richTextEditor.slashNoteLink'),
description: t('richTextEditor.slashNoteLinkDesc'),
@@ -1021,6 +1332,11 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
finally { setAiLoading(false) }
} else if (item.title === 'Suggest Charts') {
deleteSlashText(); closeMenu(); onSuggestCharts()
+ } else if (item.title === t('richTextEditor.slashDatabase')) {
+ deleteSlashText(); closeMenu()
+ if (!insertDatabaseBlockAtSelection(editor)) {
+ toast.error(t('databaseBlock.insertFailed'))
+ }
} else {
deleteSlashText(); item.command(editor); closeMenu()
}
@@ -1029,7 +1345,13 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
const presentCategoryIds = new Set(localCommands.map(c => c.categoryId))
const allCategories = ORDERED_SLASH_CATEGORIES.filter(id => presentCategoryIds.has(id))
- const textFiltered = localCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
+ const q = query.toLowerCase()
+ const textFiltered = localCommands.filter(c =>
+ c.title.toLowerCase().includes(q)
+ || c.description.toLowerCase().includes(q)
+ || (c.shortcut?.toLowerCase().includes(q) ?? false)
+ || (c.slashKeywords?.some((kw) => kw.includes(q) || q.includes(kw)) ?? false)
+ )
const filtered = activeCategory ? textFiltered.filter(c => c.categoryId === activeCategory) : textFiltered
const availableCategoriesInSearch = textFiltered.reduce((acc, item) => {
@@ -1070,7 +1392,15 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
if (e.key === 'Enter') {
e.preventDefault()
const item = filtered[selectedIndex]
- if (item) handleSelect(item)
+ if (item) {
+ handleSelect(item)
+ } else if (/^(database|db)$/i.test(query)) {
+ deleteSlashText()
+ closeMenu()
+ if (!insertDatabaseBlockAtSelection(editor)) {
+ toast.error(t('databaseBlock.insertFailed'))
+ }
+ }
return
}
if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return }
diff --git a/memento-note/components/sidebar.tsx b/memento-note/components/sidebar.tsx
index 05adc18..5f1d4f1 100644
--- a/memento-note/components/sidebar.tsx
+++ b/memento-note/components/sidebar.tsx
@@ -40,7 +40,7 @@ import { useSearchModal } from '@/context/search-modal-context'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { applyDocumentTheme } from '@/lib/apply-document-theme'
-import { getAllNotes, getTrashCount } from '@/app/actions/notes'
+import { getAllNotes, getTrashCount, getNotesWithReminders } from '@/app/actions/notes'
import { NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync'
import { useNotebooks } from '@/context/notebooks-context'
import { Notebook, Note } from '@/lib/types'
@@ -60,7 +60,7 @@ import { performSignOut } from '@/lib/auth-client'
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
import { UsageMeter } from './usage-meter'
-type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision'
+type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
function NoteLink({
@@ -175,6 +175,96 @@ function SidebarBrainstorms() {
)
}
+function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, notebookId: string | null) => void }) {
+ const { t } = useLanguage()
+ const [reminders, setReminders] = useState<
+ { id: string; title: string | null; reminder: Date | string | null; isReminderDone: boolean; notebookId: string | null }[]
+ >([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ let cancelled = false
+ setLoading(true)
+ getNotesWithReminders()
+ .then((rows) => {
+ if (!cancelled) setReminders(rows as typeof reminders)
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ if (loading) {
+ return (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ )
+ }
+
+ const now = new Date()
+ const active = reminders.filter((r) => !r.isReminderDone && r.reminder)
+ const overdue = active.filter((r) => new Date(r.reminder!) < now)
+ const upcoming = active.filter((r) => new Date(r.reminder!) >= now)
+
+ if (active.length === 0) {
+ return (
+
+
+
{t('reminders.emptyDescription')}
+
+ )
+ }
+
+ const renderItem = (note: (typeof active)[0], overdueItem?: boolean) => (
+ onOpenNote(note.id, note.notebookId)}
+ className="w-full text-start px-4 py-2.5 rounded-xl hover:bg-brand-accent/5 transition-colors group"
+ >
+
+ {note.title || t('notes.untitled')}
+
+
+ {note.reminder &&
+ new Date(note.reminder).toLocaleString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+ )
+
+ return (
+
+ {overdue.length > 0 && (
+
+
+ {t('reminders.overdue')}
+
+
{overdue.map((n) => renderItem(n, true))}
+
+ )}
+ {upcoming.length > 0 && (
+
+
+ {t('reminders.upcoming')}
+
+
{upcoming.map((n) => renderItem(n))}
+
+ )}
+
+ )
+}
+
function SidebarCarnetItem({
carnet,
isActive,
@@ -536,8 +626,19 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
useEffect(() => {
if (pathname.startsWith('/brainstorm')) setActiveView('brainstorms')
else if (pathname.startsWith('/agents') || pathname.startsWith('/lab')) setActiveView('agents')
- else setActiveView('notebooks')
- }, [pathname])
+ else if (pathname === '/insights') setActiveView('insights')
+ else if (pathname.startsWith('/revision')) setActiveView('revision')
+ else if (searchParams.get('reminders') === '1' && pathname === '/home') setActiveView('reminders')
+ else if (pathname === '/home' || pathname.startsWith('/notes')) setActiveView('notebooks')
+ }, [pathname, searchParams])
+
+ const isRemindersRoute = pathname === '/home' && searchParams.get('reminders') === '1'
+ const isSharedRoute = pathname === '/home' && searchParams.get('shared') === '1'
+ const isNotebooksRoute =
+ (pathname === '/home' || pathname.startsWith('/notes')) &&
+ !pathname.startsWith('/settings') &&
+ !isRemindersRoute &&
+ !isSharedRoute
const displayName = user?.name || user?.email || ''
const initial = displayName ? displayName.charAt(0).toUpperCase() : '?'
@@ -645,6 +746,19 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
router.push(`/home?${params.toString()}`)
}
+ const handleRemindersClick = () => {
+ setActiveView('reminders')
+ router.push('/home?reminders=1&forceList=1')
+ }
+
+ const handleReminderNoteClick = (noteId: string, notebookId: string | null) => {
+ const params = new URLSearchParams()
+ params.set('reminders', '1')
+ if (notebookId) params.set('notebook', notebookId)
+ params.set('openNote', noteId)
+ router.push(`/home?${params.toString()}`)
+ }
+
const handleInboxClick = () => {
router.push('/home?forceList=1')
}
@@ -972,14 +1086,59 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{/* Boutons de navigation principaux */}
{([
- { id: 'notebooks', icon: BookOpen, label: t('nav.notebooks'), onClick: () => { setActiveView('notebooks'); if (pathname !== '/home') router.push('/home') }, isActive: activeView === 'notebooks' && !pathname.startsWith('/settings') },
- { id: 'insights', icon: Sparkles, label: t('nav.insights'), onClick: () => router.push('/insights'), isActive: pathname === '/insights' },
- { id: 'revision', icon: GraduationCap, label: t('nav.revision'), onClick: () => router.push('/revision'), isActive: pathname === '/revision' },
- { id: 'agents', icon: Bot, label: t('agents.intelligenceOS') || 'Intelligence IA', onClick: () => { setActiveView('agents'); router.push('/agents') }, isActive: activeView === 'agents' || (pathname.startsWith('/agents') && activeView !== 'notebooks') },
- { id: 'reminders', icon: Bell, label: t('sidebar.reminders'), onClick: () => setActiveView('reminders'), isActive: activeView === 'reminders' },
+ {
+ id: 'notebooks',
+ icon: BookOpen,
+ label: t('nav.notebooks'),
+ onClick: () => {
+ setActiveView('notebooks')
+ router.push('/home?forceList=1')
+ },
+ isActive: isNotebooksRoute,
+ },
+ {
+ id: 'insights',
+ icon: Sparkles,
+ label: t('nav.insights'),
+ onClick: () => {
+ setActiveView('insights')
+ router.push('/insights')
+ },
+ isActive: pathname === '/insights',
+ },
+ {
+ id: 'revision',
+ icon: GraduationCap,
+ label: t('nav.revision'),
+ onClick: () => {
+ setActiveView('revision')
+ router.push('/revision')
+ },
+ isActive: pathname.startsWith('/revision'),
+ },
+ {
+ id: 'agents',
+ icon: Bot,
+ label: t('agents.intelligenceOS') || 'Intelligence IA',
+ onClick: () => {
+ setActiveView('agents')
+ router.push('/agents')
+ },
+ isActive: pathname.startsWith('/agents') || pathname.startsWith('/lab'),
+ },
+ {
+ id: 'reminders',
+ icon: Bell,
+ label: t('sidebar.reminders'),
+ onClick: handleRemindersClick,
+ isActive: isRemindersRoute,
+ },
] as { id: string; icon: React.FC<{ size?: number }>; label: string; onClick: () => void; isActive: boolean }[]).map(item => (
)}
-
+
{item.label}
@@ -1232,6 +1394,60 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
+ ) : activeView === 'insights' ? (
+
+
+
+
+ {t('nav.insights')}
+
+
+
+ {t('sidebar.insightsPanelBody')}
+
+ router.push('/home')}
+ className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
+ >
+
+ {t('sidebar.backToNotebooks')}
+
+
+ ) : activeView === 'revision' ? (
+
+
+
+
+ {t('nav.revision')}
+
+
+
+ {t('sidebar.revisionPanelBody')}
+
+ router.push('/home')}
+ className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
+ >
+
+ {t('sidebar.backToNotebooks')}
+
+
) : activeView === 'reminders' ? (
-
- {t('sidebar.reminders')}
-
-
-
-
{t('sidebar.noReminders')}
+
+
+
+ {t('sidebar.reminders')}
+
+
+
+
) : activeView === 'agents' ? (
diff --git a/memento-note/components/smart-paste-menu.tsx b/memento-note/components/smart-paste-menu.tsx
new file mode 100644
index 0000000..3f6eeff
--- /dev/null
+++ b/memento-note/components/smart-paste-menu.tsx
@@ -0,0 +1,80 @@
+'use client'
+
+import { useEffect, useRef } from 'react'
+import { createPortal } from 'react-dom'
+import { useLanguage } from '@/lib/i18n'
+import { Link2, Blocks } from 'lucide-react'
+import type { ParsedBlockReference } from '@/lib/editor/parse-block-reference'
+
+export type SmartPasteMenuProps = {
+ anchor: { top: number; left: number }
+ reference: ParsedBlockReference
+ sourceNoteTitle?: string
+ onLive: () => void
+ onPlain: () => void
+ onClose: () => void
+}
+
+export function SmartPasteMenu({
+ anchor,
+ reference,
+ sourceNoteTitle,
+ onLive,
+ onPlain,
+ onClose,
+}: SmartPasteMenuProps) {
+ const { t } = useLanguage()
+ const menuRef = useRef
(null)
+
+ useEffect(() => {
+ function handleClickOutside(e: MouseEvent) {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ onClose()
+ }
+ }
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Escape') onClose()
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ document.addEventListener('keydown', handleKeyDown)
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [onClose])
+
+ const menuStyle: React.CSSProperties = {
+ position: 'fixed',
+ left: anchor.left,
+ top: anchor.top + 8,
+ zIndex: 9999,
+ maxWidth: 320,
+ }
+
+ if (Number(menuStyle.left) + 320 > window.innerWidth) {
+ menuStyle.left = Math.max(8, window.innerWidth - 328)
+ }
+ if (Number(menuStyle.top) + 140 > window.innerHeight) {
+ menuStyle.top = anchor.top - 132
+ }
+
+ const previewTitle = sourceNoteTitle?.trim() || t('smartPaste.unknownNote')
+
+ return createPortal(
+
+
{t('smartPaste.prompt')}
+
+ {previewTitle}
+
+
+
+ {t('smartPaste.liveBlock')}
+
+
+
+ {t('smartPaste.plainLink')}
+
+
,
+ document.body,
+ )
+}
diff --git a/memento-note/components/tiptap-database-block-extension.tsx b/memento-note/components/tiptap-database-block-extension.tsx
new file mode 100644
index 0000000..bdfbf10
--- /dev/null
+++ b/memento-note/components/tiptap-database-block-extension.tsx
@@ -0,0 +1,117 @@
+'use client'
+
+import { Node, mergeAttributes } from '@tiptap/core'
+import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps } from '@tiptap/react'
+import { useCallback } from 'react'
+import type { Editor } from '@tiptap/core'
+import type { Node as PMNode } from '@tiptap/pm/model'
+import { DatabaseBlockEditor } from '@/components/database-block-editor'
+import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
+import {
+ createDefaultDatabaseBlockData,
+ parseDatabaseBlockAttrs,
+ serializeDatabaseBlockData,
+ type DatabaseBlockData,
+} from '@/lib/editor/database-block-types'
+
+function DatabaseBlockView({ node, updateAttributes, editor }: NodeViewProps) {
+ const data = parseDatabaseBlockAttrs(node.attrs)
+
+ const requestSave = useCallback(() => {
+ const hostNoteId = editor.storage.liveBlock?.hostNoteId
+ if (hostNoteId) {
+ window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
+ detail: { noteId: hostNoteId, reason: 'database-block-mutation' },
+ }))
+ }
+ }, [editor])
+
+ const handleChange = useCallback((next: DatabaseBlockData) => {
+ updateAttributes(serializeDatabaseBlockData(next))
+ requestSave()
+ }, [requestSave, updateAttributes])
+
+ return (
+
+
+
+ )
+}
+
+export const DatabaseBlockExtension = Node.create({
+ name: 'databaseBlock',
+ group: 'block',
+ atom: true,
+ draggable: true,
+ selectable: true,
+
+ addAttributes() {
+ return {
+ dbId: {
+ default: '',
+ parseHTML: (element) => element.getAttribute('data-db-id') || '',
+ renderHTML: (attributes) => (attributes.dbId ? { 'data-db-id': attributes.dbId } : {}),
+ },
+ dbView: {
+ default: 'table',
+ parseHTML: (element) => element.getAttribute('data-db-view') || 'table',
+ renderHTML: (attributes) => ({ 'data-db-view': attributes.dbView || 'table' }),
+ },
+ dbAuthorsJson: {
+ default: '[]',
+ parseHTML: (element) => element.getAttribute('data-db-authors') || '[]',
+ renderHTML: (attributes) => ({ 'data-db-authors': attributes.dbAuthorsJson || '[]' }),
+ },
+ dbBooksJson: {
+ default: '[]',
+ parseHTML: (element) => element.getAttribute('data-db-books') || '[]',
+ renderHTML: (attributes) => ({ 'data-db-books': attributes.dbBooksJson || '[]' }),
+ },
+ }
+ },
+
+ parseHTML() {
+ return [{ tag: 'div[data-database-block]' }]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['div', mergeAttributes(HTMLAttributes, { 'data-database-block': 'true' })]
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(DatabaseBlockView)
+ },
+})
+
+export function insertDatabaseBlockAtSelection(editor: Editor): boolean {
+ const type = editor.schema.nodes.databaseBlock
+ if (!type) return false
+
+ const attrs = serializeDatabaseBlockData(createDefaultDatabaseBlockData())
+ const { empty, $from } = editor.state.selection
+ const parent = $from.parent
+
+ if (empty && parent.type.name === 'paragraph' && parent.content.size === 0) {
+ const pos = $from.before()
+ return editor
+ .chain()
+ .focus()
+ .command(({ tr, dispatch }) => {
+ tr.replaceWith(pos, pos + parent.nodeSize, type.create(attrs))
+ if (dispatch) dispatch(tr)
+ return true
+ })
+ .run()
+ }
+
+ return editor.chain().focus().insertContent({ type: 'databaseBlock', attrs }).run()
+}
+
+export function replaceBlockWithDatabase(editor: Editor, blockPos: number, blockNode: PMNode): void {
+ const type = editor.schema.nodes.databaseBlock
+ if (!type || blockPos < 0 || !blockNode) return
+ const attrs = serializeDatabaseBlockData(createDefaultDatabaseBlockData())
+ const dbNode = type.create(attrs)
+ editor.view.dispatch(editor.state.tr.replaceWith(blockPos, blockPos + blockNode.nodeSize, dbNode))
+ editor.commands.focus()
+}
diff --git a/memento-note/components/tiptap-live-block-extension.tsx b/memento-note/components/tiptap-live-block-extension.tsx
index 2ff226a..acbb25b 100644
--- a/memento-note/components/tiptap-live-block-extension.tsx
+++ b/memento-note/components/tiptap-live-block-extension.tsx
@@ -1,28 +1,27 @@
'use client'
import { Node, mergeAttributes } from '@tiptap/core'
-import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
+import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps } from '@tiptap/react'
import { useEffect, useRef, useState, useCallback } from 'react'
-import { Zap, AlertCircle, Unlink, ArrowRight } from 'lucide-react'
+import { Zap, AlertCircle, Unlink, ArrowRight, Trash2 } from 'lucide-react'
+import { useLanguage } from '@/lib/i18n'
+import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
+import { openNotePeek } from '@/lib/note-peek-sync'
+
+declare module '@tiptap/core' {
+ interface Storage {
+ liveBlock: {
+ hostNoteId: string | null
+ }
+ }
+}
// ---------------------------------------------------------------------------
// LiveBlock Node View
// ---------------------------------------------------------------------------
-interface LiveBlockViewProps {
- node: {
- attrs: {
- sourceNoteId: string
- blockId: string
- snapshotContent: string
- sourceNoteTitle: string
- }
- }
- updateAttributes: (attrs: Record) => void
- deleteNode: () => void
-}
-
-function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProps) {
+function LiveBlockView({ node, updateAttributes, deleteNode, editor, getPos }: NodeViewProps) {
+ const { t } = useLanguage()
const { sourceNoteId, blockId, snapshotContent, sourceNoteTitle } = node.attrs
const [localContent, setLocalContent] = useState(snapshotContent || '')
const [isDeleted, setIsDeleted] = useState(false)
@@ -30,6 +29,15 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
const [pulse, setPulse] = useState(false)
const pulseTimerRef = useRef | null>(null)
+ const requestSave = useCallback(() => {
+ const hostNoteId = editor.storage.liveBlock?.hostNoteId
+ if (hostNoteId) {
+ window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
+ detail: { noteId: hostNoteId, reason: 'live-block-mutation' },
+ }))
+ }
+ }, [editor])
+
// Fetch current block status on mount
useEffect(() => {
if (!sourceNoteId || !blockId) return
@@ -37,6 +45,10 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
.then(r => r.json())
.then((data: { exists: boolean; content: string; sourceNoteTitle: string }) => {
if (!data.exists) {
+ if (snapshotContent?.trim()) {
+ setLocalContent(snapshotContent)
+ return
+ }
setIsDeleted(true)
} else {
setLocalContent(data.content)
@@ -44,7 +56,7 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
}
})
.catch(() => setIsOffline(true))
- }, [sourceNoteId, blockId])
+ }, [sourceNoteId, blockId, snapshotContent, updateAttributes])
// Listen for real-time block update events
useEffect(() => {
@@ -70,14 +82,33 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
}
}, [blockId, updateAttributes])
- const handleDetach = useCallback(async () => {
- // Convert this node to a plain paragraph with snapshot text
- deleteNode()
- }, [deleteNode])
+ /** Convertit le bloc live en paragraphe local (conserve le texte affiché). */
+ const handleDetach = useCallback(() => {
+ const pos = getPos()
+ if (typeof pos !== 'number') return
+ const currentNode = editor.state.doc.nodeAt(pos)
+ if (!currentNode) return
- const handleOpenSource = useCallback(() => {
- window.open(`/home?openNote=${encodeURIComponent(sourceNoteId)}`, '_blank', 'noopener,noreferrer')
- }, [sourceNoteId])
+ const text = localContent.trim()
+ const paragraph = editor.state.schema.nodes.paragraph.create(
+ null,
+ text ? editor.state.schema.text(text) : undefined,
+ )
+ editor.view.dispatch(editor.state.tr.replaceWith(pos, pos + currentNode.nodeSize, paragraph))
+ editor.commands.focus()
+ requestSave()
+ }, [editor, getPos, localContent, requestSave])
+
+ const handleRemove = useCallback(() => {
+ deleteNode()
+ requestSave()
+ }, [deleteNode, requestSave])
+
+ const handleOpenSource = useCallback((event: React.MouseEvent) => {
+ event.preventDefault()
+ event.stopPropagation()
+ openNotePeek({ noteId: sourceNoteId, blockId: blockId || undefined })
+ }, [sourceNoteId, blockId])
const borderClass = isDeleted
? 'border-l-rose-500 border-y-rose-200 border-r-rose-200 bg-rose-50/20 dark:border-l-red-700 dark:border-y-red-900/40 dark:border-r-red-900/40 dark:bg-red-950/5'
@@ -87,54 +118,78 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
? 'border-l-blue-500 border-y-blue-300 border-r-blue-300 bg-blue-50/20 shadow-md shadow-blue-500/15 dark:bg-blue-950/10'
: 'border-l-blue-500 border-y-[#E8E6E3] border-r-[#E8E6E3] bg-blue-50/5 dark:border-y-zinc-800 dark:border-r-zinc-800 dark:bg-blue-950/5'
+ const headerTitle = isDeleted
+ ? t('liveBlock.sourceDisconnected')
+ : (sourceNoteTitle || t('liveBlock.connectedNote'))
+
+ const actionButtonClass =
+ 'text-[9.5px] font-bold flex items-center gap-1 transition-all shrink-0'
+
return (
{/* Header */}
-
+
{isDeleted ? (
) : (
)}
- {isDeleted ? 'Source déconnectée' : (sourceNoteTitle || 'Note connectée')}
+ {headerTitle}
{isDeleted ? (
- DÉCONNECTÉ
+ {t('liveBlock.statusDisconnected')}
) : isOffline ? (
- HORS-LIGNE
+ {t('liveBlock.statusOffline')}
) : (
- LIVE
+ {t('liveBlock.statusLive')}
)}
-
- {isDeleted ? (
-
-
- Décharger le lien
-
- ) : (
+
+
+
+ {t('liveBlock.detachLink')}
+
+ {!isDeleted && (
- Ouvrir
+ {t('liveBlock.openSource')}
)}
+
+
+
{/* Content */}
@@ -143,7 +198,7 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
className="text-sm leading-relaxed text-[var(--color-ink)] opacity-80 dark:text-[var(--color-dark-ink)] font-sans whitespace-pre-wrap"
contentEditable={false}
>
- {localContent || '(bloc vide)'}
+ {localContent || t('liveBlock.emptyContent')}
@@ -163,6 +218,12 @@ export const LiveBlockExtension = Node.create({
draggable: true,
selectable: true,
+ addStorage() {
+ return {
+ hostNoteId: null as string | null,
+ }
+ },
+
addAttributes() {
return {
sourceNoteId: { default: '' },
diff --git a/memento-note/components/tiptap-unique-id-extension.ts b/memento-note/components/tiptap-unique-id-extension.ts
index b34bddf..2b056cd 100644
--- a/memento-note/components/tiptap-unique-id-extension.ts
+++ b/memento-note/components/tiptap-unique-id-extension.ts
@@ -1,8 +1,25 @@
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
+import type { Transaction } from '@tiptap/pm/state'
+import type { Node as PMNode } from '@tiptap/pm/model'
const BLOCK_TYPES = ['paragraph', 'heading', 'blockquote', 'bulletList', 'orderedList', 'taskList', 'codeBlock']
+function assignMissingBlockIds(tr: Transaction, doc: PMNode) {
+ let modified = false
+ doc.descendants((node, pos) => {
+ if (!BLOCK_TYPES.includes(node.type.name)) return
+ if (node.attrs['data-id']) return
+
+ tr.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ 'data-id': generateBlockId(),
+ })
+ modified = true
+ })
+ return modified
+}
+
function generateBlockId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
@@ -17,6 +34,18 @@ function generateBlockId(): string {
export const UniqueIdExtension = Extension.create({
name: 'uniqueId',
+ onCreate({ editor }) {
+ const assign = () => {
+ if (editor.isDestroyed) return
+ const { tr, doc } = editor.state
+ if (assignMissingBlockIds(tr, doc)) {
+ editor.view.dispatch(tr)
+ }
+ }
+ assign()
+ requestAnimationFrame(assign)
+ },
+
addProseMirrorPlugins() {
return [
new Plugin({
@@ -26,20 +55,7 @@ export const UniqueIdExtension = Extension.create({
if (!hasDocChanged) return null
const { tr, doc } = newState
- let modified = false
-
- doc.descendants((node, pos) => {
- if (!BLOCK_TYPES.includes(node.type.name)) return
- if (node.attrs['data-id']) return
-
- tr.setNodeMarkup(pos, undefined, {
- ...node.attrs,
- 'data-id': generateBlockId(),
- })
- modified = true
- })
-
- return modified ? tr : null
+ return assignMissingBlockIds(tr, doc) ? tr : null
},
}),
]
diff --git a/memento-note/data/uploads/attachments/cmol70y6j000n9fhwbdz1r7dc/c5eb1178-9a7b-4bc4-80d6-2f139dd97e57.pdf b/memento-note/data/uploads/attachments/cmol70y6j000n9fhwbdz1r7dc/c5eb1178-9a7b-4bc4-80d6-2f139dd97e57.pdf
new file mode 100644
index 0000000..a428d00
Binary files /dev/null and b/memento-note/data/uploads/attachments/cmol70y6j000n9fhwbdz1r7dc/c5eb1178-9a7b-4bc4-80d6-2f139dd97e57.pdf differ
diff --git a/memento-note/data/uploads/notes/2130dc3c-7576-47ec-8d67-b1a96561fd72.png b/memento-note/data/uploads/notes/2130dc3c-7576-47ec-8d67-b1a96561fd72.png
new file mode 100644
index 0000000..b93a4a6
Binary files /dev/null and b/memento-note/data/uploads/notes/2130dc3c-7576-47ec-8d67-b1a96561fd72.png differ
diff --git a/memento-note/lib/blocks/extract-blocks.ts b/memento-note/lib/blocks/extract-blocks.ts
index 6b77241..f2e1373 100644
--- a/memento-note/lib/blocks/extract-blocks.ts
+++ b/memento-note/lib/blocks/extract-blocks.ts
@@ -5,6 +5,23 @@ export interface ExtractedBlock {
content: string
}
+function escapeRegExp(value: string): string {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+/** Contenu texte d'un bloc identifié par data-id (sans filtre longueur min). */
+export function extractBlockContentById(html: string, blockId: string): string | null {
+ if (!html || !blockId) return null
+ const regex = new RegExp(
+ `<(?:p|h[1-6]|blockquote|li)[^>]*data-id="${escapeRegExp(blockId)}"[^>]*>([\\s\\S]*?)<\\/(?:p|h[1-6]|blockquote|li)>`,
+ 'i',
+ )
+ const match = regex.exec(html)
+ if (!match) return null
+ const content = stripHtmlToPlainText(match[1])
+ return content.length > 0 ? content : null
+}
+
export function extractBlocksFromHtml(html: string): ExtractedBlock[] {
const blocks: ExtractedBlock[] = []
const regex = /<(?:p|h[1-6]|blockquote|li)[^>]*data-id="([^"]+)"[^>]*>([\s\S]*?)<\/(?:p|h[1-6]|blockquote|li)>/gi
diff --git a/memento-note/lib/editor/block-at-drag-handle.ts b/memento-note/lib/editor/block-at-drag-handle.ts
new file mode 100644
index 0000000..73fe169
--- /dev/null
+++ b/memento-note/lib/editor/block-at-drag-handle.ts
@@ -0,0 +1,25 @@
+import type { Editor } from '@tiptap/core'
+import type { Node as PMNode } from '@tiptap/pm/model'
+
+export const BLOCK_DRAG_HANDLE_ID = 'notion-block-drag-handle'
+export const BLOCK_DRAG_HANDLE_WIDTH = 20
+
+/** Même logique de résolution de bloc que tiptap-extension-global-drag-handle */
+export function resolveBlockAtDragHandle(editor: Editor): { node: PMNode; pos: number } | null {
+ const handle = document.getElementById(BLOCK_DRAG_HANDLE_ID)
+ if (!handle || editor.isDestroyed) return null
+
+ const rect = handle.getBoundingClientRect()
+ const coords = editor.view.posAtCoords({
+ left: rect.right + 50 + BLOCK_DRAG_HANDLE_WIDTH,
+ top: rect.top + rect.height / 2,
+ })
+ if (coords?.pos == null) return null
+
+ const $pos = editor.state.doc.resolve(coords.pos)
+ const blockPos = $pos.depth > 1 ? $pos.before($pos.depth) : coords.pos
+ const node = editor.state.doc.nodeAt(blockPos)
+ if (!node) return null
+
+ return { node, pos: blockPos }
+}
diff --git a/memento-note/lib/editor/block-reference-id.ts b/memento-note/lib/editor/block-reference-id.ts
new file mode 100644
index 0000000..02e7fc3
--- /dev/null
+++ b/memento-note/lib/editor/block-reference-id.ts
@@ -0,0 +1,122 @@
+import type { Editor } from '@tiptap/core'
+import type { Node as PMNode } from '@tiptap/pm/model'
+
+const ID_ATTR = 'data-id'
+
+const BLOCK_TYPES_WITH_ID = new Set([
+ 'paragraph',
+ 'heading',
+ 'blockquote',
+ 'bulletList',
+ 'orderedList',
+ 'taskList',
+ 'codeBlock',
+])
+
+export function generateBlockId(): string {
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ return crypto.randomUUID()
+ }
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
+ return v.toString(16)
+ })
+}
+
+function readNodeId(node: PMNode | null | undefined): string | null {
+ const id = node?.attrs[ID_ATTR]
+ return typeof id === 'string' && id.length > 0 ? id : null
+}
+
+function findDataIdInSubtree(node: PMNode): string | null {
+ const direct = readNodeId(node)
+ if (direct) return direct
+
+ let found: string | null = null
+ node.descendants((child) => {
+ if (found) return false
+ found = readNodeId(child)
+ return !found
+ })
+ return found
+}
+
+function findDataIdFromDom(editor: Editor, blockPos: number): string | null {
+ const dom = editor.view.nodeDOM(blockPos)
+ if (!(dom instanceof HTMLElement)) return null
+
+ const direct = dom.getAttribute(ID_ATTR)
+ if (direct) return direct
+
+ const nested = dom.querySelector(`[${ID_ATTR}]`)
+ const nestedId = nested?.getAttribute(ID_ATTR)
+ return nestedId || null
+}
+
+function findTextBlockToAssign(
+ editor: Editor,
+ blockPos: number,
+ blockNode: PMNode,
+): { pos: number; node: PMNode } | null {
+ if (BLOCK_TYPES_WITH_ID.has(blockNode.type.name)) {
+ return { pos: blockPos, node: blockNode }
+ }
+
+ let target: { pos: number; node: PMNode } | null = null
+ editor.state.doc.nodesBetween(blockPos, blockPos + blockNode.nodeSize, (node, pos) => {
+ if (target) return false
+ if (node.isTextblock && BLOCK_TYPES_WITH_ID.has(node.type.name)) {
+ target = { pos, node }
+ return false
+ }
+ return true
+ })
+ return target
+}
+
+/** Cherche un data-id sur le bloc résolu (nœud, descendants, ancêtres, DOM). */
+export function findBlockReferenceId(
+ editor: Editor,
+ blockPos: number,
+ blockNode: PMNode | null,
+): string | null {
+ const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null)
+ if (!node) return null
+
+ const fromSubtree = findDataIdInSubtree(node)
+ if (fromSubtree) return fromSubtree
+
+ const $pos = editor.state.doc.resolve(Math.min(blockPos + 1, editor.state.doc.content.size))
+ for (let depth = $pos.depth; depth > 0; depth--) {
+ const id = readNodeId($pos.node(depth))
+ if (id) return id
+ }
+
+ return findDataIdFromDom(editor, blockPos)
+}
+
+/** Assigne un data-id si absent, retourne l'id utilisable pour la référence. */
+export function ensureBlockReferenceId(
+ editor: Editor,
+ blockPos: number,
+ blockNode: PMNode | null,
+): string | null {
+ const existing = findBlockReferenceId(editor, blockPos, blockNode)
+ if (existing) return existing
+
+ const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null)
+ if (!node || blockPos < 0) return null
+
+ const target = findTextBlockToAssign(editor, blockPos, node)
+ if (!target) return null
+
+ const newId = generateBlockId()
+ editor.view.dispatch(
+ editor.state.tr.setNodeMarkup(target.pos, undefined, {
+ ...target.node.attrs,
+ [ID_ATTR]: newId,
+ }),
+ )
+ return newId
+}
diff --git a/memento-note/lib/editor/copy-text-to-clipboard.ts b/memento-note/lib/editor/copy-text-to-clipboard.ts
new file mode 100644
index 0000000..31380c1
--- /dev/null
+++ b/memento-note/lib/editor/copy-text-to-clipboard.ts
@@ -0,0 +1,30 @@
+/** Copie texte — writeText async + repli synchrone execCommand. */
+export async function copyTextToClipboard(text: string): Promise
{
+ if (typeof window === 'undefined') return false
+
+ if (navigator.clipboard?.writeText) {
+ try {
+ await navigator.clipboard.writeText(text)
+ return true
+ } catch {
+ // repli ci-dessous
+ }
+ }
+
+ try {
+ const ta = document.createElement('textarea')
+ ta.value = text
+ ta.setAttribute('readonly', '')
+ ta.style.position = 'fixed'
+ ta.style.left = '-9999px'
+ ta.style.top = '0'
+ document.body.appendChild(ta)
+ ta.focus()
+ ta.select()
+ const ok = document.execCommand('copy')
+ document.body.removeChild(ta)
+ return ok
+ } catch {
+ return false
+ }
+}
diff --git a/memento-note/lib/editor/database-block-types.ts b/memento-note/lib/editor/database-block-types.ts
new file mode 100644
index 0000000..f5f65d1
--- /dev/null
+++ b/memento-note/lib/editor/database-block-types.ts
@@ -0,0 +1,100 @@
+export type DatabaseAuthor = { id: string; name: string }
+
+export type DatabaseBook = {
+ id: string
+ title: string
+ author: string
+ cover: string
+ tag: string
+}
+
+export type DatabaseView = 'table' | 'card'
+
+export type DatabaseBlockData = {
+ dbId: string
+ dbView: DatabaseView
+ dbAuthors: DatabaseAuthor[]
+ dbBooks: DatabaseBook[]
+}
+
+const DEFAULT_COVERS = [
+ 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400',
+ 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400',
+ 'https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?auto=format&fit=crop&q=80&w=400',
+ 'https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=400',
+]
+
+export function randomDefaultCover(): string {
+ return DEFAULT_COVERS[Math.floor(Math.random() * DEFAULT_COVERS.length)]
+}
+
+export function createDefaultDatabaseBlockData(): DatabaseBlockData {
+ return {
+ dbId: `authors-works-${typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : Date.now()}`,
+ dbView: 'table',
+ dbAuthors: [
+ { id: 'a1', name: 'Jules Verne' },
+ { id: 'a2', name: 'Liu Cixin' },
+ ],
+ dbBooks: [
+ {
+ id: 'bk1',
+ title: 'Twenty Thousand Leagues Under The Sea',
+ author: 'Jules Verne',
+ cover: DEFAULT_COVERS[0],
+ tag: 'Aventure',
+ },
+ {
+ id: 'bk2',
+ title: 'The Three-Body Problem',
+ author: 'Liu Cixin',
+ cover: DEFAULT_COVERS[1],
+ tag: 'Hard SF',
+ },
+ {
+ id: 'bk3',
+ title: 'The Wandering Earth',
+ author: 'Liu Cixin',
+ cover: DEFAULT_COVERS[2],
+ tag: 'SF Spatial',
+ },
+ ],
+ }
+}
+
+export function serializeDatabaseBlockData(data: DatabaseBlockData): {
+ dbId: string
+ dbView: DatabaseView
+ dbAuthorsJson: string
+ dbBooksJson: string
+} {
+ return {
+ dbId: data.dbId,
+ dbView: data.dbView,
+ dbAuthorsJson: JSON.stringify(data.dbAuthors),
+ dbBooksJson: JSON.stringify(data.dbBooks),
+ }
+}
+
+export function parseDatabaseBlockAttrs(attrs: {
+ dbId?: string
+ dbView?: string
+ dbAuthorsJson?: string
+ dbBooksJson?: string
+}): DatabaseBlockData {
+ const fallback = createDefaultDatabaseBlockData()
+ let authors = fallback.dbAuthors
+ let books = fallback.dbBooks
+ try {
+ if (attrs.dbAuthorsJson) authors = JSON.parse(attrs.dbAuthorsJson)
+ } catch { /* keep fallback */ }
+ try {
+ if (attrs.dbBooksJson) books = JSON.parse(attrs.dbBooksJson)
+ } catch { /* keep fallback */ }
+ return {
+ dbId: attrs.dbId || fallback.dbId,
+ dbView: attrs.dbView === 'card' ? 'card' : 'table',
+ dbAuthors: Array.isArray(authors) ? authors : fallback.dbAuthors,
+ dbBooks: Array.isArray(books) ? books : fallback.dbBooks,
+ }
+}
diff --git a/memento-note/lib/editor/empty-paragraph-at-selection.ts b/memento-note/lib/editor/empty-paragraph-at-selection.ts
new file mode 100644
index 0000000..6d26036
--- /dev/null
+++ b/memento-note/lib/editor/empty-paragraph-at-selection.ts
@@ -0,0 +1,19 @@
+import type { EditorState } from '@tiptap/pm/state'
+import type { Node as PMNode } from '@tiptap/pm/model'
+
+function isEmptyParagraph(node: PMNode): boolean {
+ if (node.type.name !== 'paragraph') return false
+ return node.textContent.trim() === '' && node.content.size <= 1
+}
+
+/** Paragraphe vide sous le curseur (critère US-3 Smart Paste). */
+export function getEmptyParagraphAtSelection(
+ state: EditorState,
+): { pos: number; node: PMNode } | null {
+ const { $from, empty } = state.selection
+ if (!empty) return null
+ if (!isEmptyParagraph($from.parent)) return null
+
+ const pos = $from.before($from.depth)
+ return { pos, node: $from.parent }
+}
diff --git a/memento-note/lib/editor/global-drag-handle-extension.ts b/memento-note/lib/editor/global-drag-handle-extension.ts
new file mode 100644
index 0000000..83211a4
--- /dev/null
+++ b/memento-note/lib/editor/global-drag-handle-extension.ts
@@ -0,0 +1,17 @@
+import GlobalDragHandle from 'tiptap-extension-global-drag-handle'
+import AutoJoiner from 'tiptap-extension-auto-joiner'
+import { BLOCK_DRAG_HANDLE_ID, BLOCK_DRAG_HANDLE_WIDTH } from './block-at-drag-handle'
+
+/** Extensions drag handle Novel / Notion — une poignée globale, position fixed */
+export const globalDragHandleExtensions = [
+ GlobalDragHandle.configure({
+ dragHandleWidth: BLOCK_DRAG_HANDLE_WIDTH,
+ scrollTreshold: 100,
+ dragHandleSelector: `#${BLOCK_DRAG_HANDLE_ID}`,
+ excludedTags: [],
+ customNodes: [],
+ }),
+ AutoJoiner.configure({
+ elementsToJoin: ['bulletList', 'orderedList'],
+ }),
+]
diff --git a/memento-note/lib/editor/parse-block-reference.ts b/memento-note/lib/editor/parse-block-reference.ts
new file mode 100644
index 0000000..8eac685
--- /dev/null
+++ b/memento-note/lib/editor/parse-block-reference.ts
@@ -0,0 +1,67 @@
+export type ParsedBlockReference = {
+ sourceNoteId: string
+ blockId: string
+ raw: string
+}
+
+export type StoredBlockReference = ParsedBlockReference & {
+ blockContent?: string
+ sourceNoteTitle?: string
+}
+
+export const LAST_BLOCK_REF_SESSION_KEY = 'memento:lastBlockRef'
+
+/** Référence bloc copiée via « Copier la référence » : /home?openNote=…#block-… */
+export function parseBlockReferenceFromText(text: string): ParsedBlockReference | null {
+ const trimmed = text.trim()
+ if (!trimmed || trimmed.includes('\n')) return null
+
+ const match = trimmed.match(/openNote=([^\s]+)(?:[^#]*?)#block-([a-zA-Z0-9_-]+)/i)
+ if (!match) return null
+
+ try {
+ return {
+ sourceNoteId: decodeURIComponent(match[1]),
+ blockId: match[2],
+ raw: trimmed,
+ }
+ } catch {
+ return null
+ }
+}
+
+export function rememberBlockReference(
+ raw: string,
+ extras?: { blockContent?: string; sourceNoteTitle?: string },
+) {
+ if (typeof sessionStorage === 'undefined') return
+ const parsed = parseBlockReferenceFromText(raw)
+ if (!parsed) return
+ const payload: StoredBlockReference = {
+ ...parsed,
+ ...(extras?.blockContent ? { blockContent: extras.blockContent } : {}),
+ ...(extras?.sourceNoteTitle ? { sourceNoteTitle: extras.sourceNoteTitle } : {}),
+ }
+ try {
+ sessionStorage.setItem(LAST_BLOCK_REF_SESSION_KEY, JSON.stringify(payload))
+ } catch {
+ // quota / private mode
+ }
+}
+
+export function recallLastBlockReference(): StoredBlockReference | null {
+ if (typeof sessionStorage === 'undefined') return null
+ try {
+ const stored = sessionStorage.getItem(LAST_BLOCK_REF_SESSION_KEY)
+ if (!stored) return null
+ if (stored.startsWith('{')) {
+ const payload = JSON.parse(stored) as StoredBlockReference
+ if (payload?.sourceNoteId && payload?.blockId && payload?.raw) return payload
+ return null
+ }
+ const parsed = parseBlockReferenceFromText(stored)
+ return parsed ?? null
+ } catch {
+ return null
+ }
+}
diff --git a/memento-note/lib/editor/smart-paste-extension.ts b/memento-note/lib/editor/smart-paste-extension.ts
new file mode 100644
index 0000000..4dd4006
--- /dev/null
+++ b/memento-note/lib/editor/smart-paste-extension.ts
@@ -0,0 +1,39 @@
+import { Extension } from '@tiptap/core'
+import type { EditorView } from '@tiptap/pm/view'
+import { Plugin, PluginKey } from '@tiptap/pm/state'
+
+export type SmartPasteHandler = (view: EditorView, event: ClipboardEvent) => boolean
+
+export const smartPastePluginKey = new PluginKey('smartPaste')
+
+declare module '@tiptap/core' {
+ interface Storage {
+ smartPaste: {
+ onPaste: SmartPasteHandler | null
+ }
+ }
+}
+
+export const SmartPasteExtension = Extension.create({
+ name: 'smartPaste',
+
+ addStorage() {
+ return {
+ onPaste: null as SmartPasteHandler | null,
+ }
+ },
+
+ addProseMirrorPlugins() {
+ const storage = this.storage
+ return [
+ new Plugin({
+ key: smartPastePluginKey,
+ props: {
+ handlePaste(view, event) {
+ return storage.onPaste?.(view, event) ?? false
+ },
+ },
+ }),
+ ]
+ },
+})
diff --git a/memento-note/lib/note-peek-sync.ts b/memento-note/lib/note-peek-sync.ts
new file mode 100644
index 0000000..98a6341
--- /dev/null
+++ b/memento-note/lib/note-peek-sync.ts
@@ -0,0 +1,18 @@
+/** Ouvre la note source en aperçu split (panneau gauche) sans changer d’onglet. */
+export const NOTE_PEEK_OPEN_EVENT = 'memento-note-peek-open'
+export const NOTE_PEEK_CLOSE_EVENT = 'memento-note-peek-close'
+
+export type NotePeekOpenDetail = {
+ noteId: string
+ blockId?: string
+}
+
+export function openNotePeek(detail: NotePeekOpenDetail) {
+ if (typeof window === 'undefined') return
+ window.dispatchEvent(new CustomEvent(NOTE_PEEK_OPEN_EVENT, { detail }))
+}
+
+export function closeNotePeek() {
+ if (typeof window === 'undefined') return
+ window.dispatchEvent(new CustomEvent(NOTE_PEEK_CLOSE_EVENT))
+}
diff --git a/memento-note/lib/note-preview.ts b/memento-note/lib/note-preview.ts
index bcf8383..e3c86ab 100644
--- a/memento-note/lib/note-preview.ts
+++ b/memento-note/lib/note-preview.ts
@@ -3,6 +3,25 @@ import type { Note } from '@/lib/types'
const MD_IMG = /!\[[^\]]*\]\(([^)\s]+)\)/g
const HTML_IMG = / ]+src=["']([^"']+)["'][^>]*>/gi
+/** SSR-safe HTML entity decode so excerpt text matches server and client. */
+function decodeHtmlEntities(text: string): string {
+ return text
+ .replace(/ /gi, ' ')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/(?:*39;|')/g, "'")
+ .replace(/(\d+);/g, (_, code: string) => {
+ const n = Number(code)
+ return n > 0 && n < 0x110000 ? String.fromCodePoint(n) : `${code};`
+ })
+ .replace(/([0-9a-f]+);/gi, (_, hex: string) => {
+ const n = parseInt(hex, 16)
+ return n > 0 && n < 0x110000 ? String.fromCodePoint(n) : `${hex};`
+ })
+}
+
/**
* Plain-text preview for list view (light markdown stripping).
*/
@@ -58,6 +77,7 @@ export function getNotePlainExcerpt(note: Note, maxLen = 240): string {
let raw = note.content || ''
raw = raw.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
raw = raw.replace(/<[^>]+>/g, ' ')
+ raw = decodeHtmlEntities(raw)
return stripMarkdownPreview(raw, maxLen)
}
diff --git a/memento-note/lib/notes-view-preference.ts b/memento-note/lib/notes-view-preference.ts
index 383a0e7..fb33ec8 100644
--- a/memento-note/lib/notes-view-preference.ts
+++ b/memento-note/lib/notes-view-preference.ts
@@ -1,12 +1,9 @@
-import type { NotesLayoutMode, NotesViewType } from '@/components/notes-list-views'
+import type { NotesLayoutMode } from '@/components/notes-list-views'
export const NOTES_LAYOUT_COOKIE = 'memento-notes-layout'
-export const NOTES_VIEW_TYPE_COOKIE = 'memento-notes-view-type'
export const NOTES_LAYOUT_STORAGE_KEY = 'memento-notes-layout'
-export const NOTES_VIEW_TYPE_STORAGE_KEY = 'memento-notes-view-type'
const LAYOUT_VALUES: NotesLayoutMode[] = ['grid', 'list', 'table', 'kanban', 'gallery']
-const VIEW_TYPE_VALUES: NotesViewType[] = ['notes', 'tasks']
export function parseNotesLayoutMode(value: string | undefined | null): NotesLayoutMode {
if (value && (LAYOUT_VALUES as string[]).includes(value)) {
@@ -17,19 +14,8 @@ export function parseNotesLayoutMode(value: string | undefined | null): NotesLay
return 'list'
}
-export function parseNotesViewType(value: string | undefined | null): NotesViewType {
- if (value && (VIEW_TYPE_VALUES as string[]).includes(value)) return value as NotesViewType
- return 'notes'
-}
-
export function setNotesLayoutPreference(layout: NotesLayoutMode) {
if (typeof window === 'undefined') return
localStorage.setItem(NOTES_LAYOUT_STORAGE_KEY, layout)
document.cookie = `${NOTES_LAYOUT_COOKIE}=${layout}; path=/; max-age=31536000; SameSite=Lax`
}
-
-export function setNotesViewTypePreference(viewType: NotesViewType) {
- if (typeof window === 'undefined') return
- localStorage.setItem(NOTES_VIEW_TYPE_STORAGE_KEY, viewType)
- document.cookie = `${NOTES_VIEW_TYPE_COOKIE}=${viewType}; path=/; max-age=31536000; SameSite=Lax`
-}
diff --git a/memento-note/lib/use-hydrated.ts b/memento-note/lib/use-hydrated.ts
new file mode 100644
index 0000000..4ce1261
--- /dev/null
+++ b/memento-note/lib/use-hydrated.ts
@@ -0,0 +1,10 @@
+import { useSyncExternalStore } from 'react'
+
+/** True only after the client has hydrated (false on server + first client paint). */
+export function useHydrated(): boolean {
+ return useSyncExternalStore(
+ () => () => {},
+ () => true,
+ () => false,
+ )
+}
diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json
index 8ac8fc8..d783865 100644
--- a/memento-note/locales/en.json
+++ b/memento-note/locales/en.json
@@ -74,7 +74,10 @@
"noReminders": "No active reminders.",
"documents": "Documents",
"searchNotebooksPlaceholder": "Search notebooks…",
- "clearSearch": "Clear search"
+ "clearSearch": "Clear search",
+ "insightsPanelBody": "Semantic map of your notes: thematic clusters, bridge notes, and connection suggestions.",
+ "revisionPanelBody": "Review flashcards with the SM-2 algorithm. Decks are generated from your notes.",
+ "backToNotebooks": "Back to notebooks"
},
"notes": {
"title": "Notes",
@@ -2347,6 +2350,8 @@
"slashDividerDesc": "Horizontal separator",
"slashTable": "Table",
"slashTableDesc": "Insert a simple grid",
+ "slashDatabase": "Database",
+ "slashDatabaseDesc": "Inline authors & works database",
"slashDiagram": "Diagram",
"slashDiagramDesc": "Generate a flow or mindmap",
"slashSlides": "Presentation",
@@ -3310,5 +3315,76 @@
"uploadError": "Upload error",
"uploadFailed": "Upload failed",
"uploading": "Uploading..."
+ },
+ "blockAction": {
+ "delete": "Delete",
+ "duplicate": "Duplicate",
+ "turnInto": "Turn into",
+ "turnInto_heading1": "Heading 1",
+ "turnInto_heading2": "Heading 2",
+ "turnInto_heading3": "Heading 3",
+ "turnInto_bulletList": "Bullet List",
+ "turnInto_orderedList": "Numbered List",
+ "turnInto_taskList": "Task List",
+ "turnInto_blockquote": "Quote",
+ "turnInto_codeBlock": "Code Block",
+ "turnInto_database": "Database",
+ "copyRef": "Copy block reference",
+ "copied": "Reference copied!",
+ "copyRefFailed": "Could not copy block reference",
+ "copyRefNoNote": "Save the note before copying a block reference",
+ "copyRefUnsupported": "This block type cannot be referenced yet"
+ },
+ "smartPaste": {
+ "prompt": "Paste this block reference as:",
+ "liveBlock": "Connected block (live)",
+ "plainLink": "Plain text / link",
+ "unknownNote": "Untitled note"
+ },
+ "liveBlock": {
+ "connectedNote": "Connected note",
+ "sourceDisconnected": "Source disconnected",
+ "statusLive": "LIVE",
+ "statusOffline": "OFFLINE",
+ "statusDisconnected": "DISCONNECTED",
+ "detachLink": "Unlink block",
+ "detachHelp": "Convert this block to normal text in this note",
+ "openSource": "Open",
+ "removeBlock": "Remove block",
+ "emptyContent": "(empty block)"
+ },
+ "notePeek": {
+ "label": "Linked note",
+ "panelLabel": "Source note preview",
+ "close": "Close preview",
+ "openFully": "Open full screen",
+ "openFullyHelp": "Replace the current note with this one",
+ "readOnlyHint": "Read-only preview — open full screen to edit.",
+ "loadFailed": "Could not open the linked note preview."
+ },
+ "databaseBlock": {
+ "title": "Authors & Works",
+ "hint": "Inline database — data is stored in this note.",
+ "viewTable": "Table",
+ "viewCards": "Cards",
+ "colAuthor": "Author",
+ "colWorks": "Works",
+ "colRollup": "Rollup",
+ "noLinkedWorks": "No linked works",
+ "deleteShort": "Del",
+ "deleteCard": "Remove work",
+ "addAuthor": "Add author",
+ "authorPlaceholder": "Author name…",
+ "createAuthor": "Create",
+ "addWork": "Add a work",
+ "bookTitlePlaceholder": "Work title…",
+ "selectAuthor": "Select author…",
+ "tagPlaceholder": "Genre / tag…",
+ "coverPlaceholder": "Cover image URL (optional)",
+ "insertWork": "Insert work",
+ "defaultTag": "General",
+ "worksBase": "Works database",
+ "storedCount": "{count} work(s) stored",
+ "insertFailed": "Could not insert the database block. Try again on an empty line."
}
}
\ No newline at end of file
diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json
index 5be3f93..8c60b47 100644
--- a/memento-note/locales/fr.json
+++ b/memento-note/locales/fr.json
@@ -74,7 +74,10 @@
"noReminders": "Aucun rappel actif.",
"documents": "Documents",
"searchNotebooksPlaceholder": "Rechercher un carnet…",
- "clearSearch": "Effacer la recherche"
+ "clearSearch": "Effacer la recherche",
+ "insightsPanelBody": "Cartographie sémantique de vos notes : clusters thématiques, notes-ponts et suggestions de connexion.",
+ "revisionPanelBody": "Révisez vos flashcards avec l'algorithme SM-2. Les decks sont générés depuis vos notes.",
+ "backToNotebooks": "Retour aux carnets"
},
"notes": {
"title": "Notes",
@@ -2351,6 +2354,8 @@
"slashDividerDesc": "Séparateur horizontal",
"slashTable": "Tableau",
"slashTableDesc": "Insérer un tableau simple",
+ "slashDatabase": "Base de données",
+ "slashDatabaseDesc": "Base inline auteurs et œuvres",
"slashDiagram": "Diagramme",
"slashDiagramDesc": "Générer un flux ou une carte mentale",
"slashSlides": "Présentation",
@@ -3314,5 +3319,76 @@
"uploadError": "Erreur de téléchargement",
"uploadFailed": "Échec du téléchargement",
"uploading": "Téléchargement..."
+ },
+ "blockAction": {
+ "delete": "Supprimer",
+ "duplicate": "Dupliquer",
+ "turnInto": "Transformer en",
+ "turnInto_heading1": "Titre 1",
+ "turnInto_heading2": "Titre 2",
+ "turnInto_heading3": "Titre 3",
+ "turnInto_bulletList": "Liste à puces",
+ "turnInto_orderedList": "Liste numérotée",
+ "turnInto_taskList": "Liste de tâches",
+ "turnInto_blockquote": "Citation",
+ "turnInto_codeBlock": "Bloc de code",
+ "turnInto_database": "Base de données",
+ "copyRef": "Copier la référence du bloc",
+ "copied": "Référence copiée !",
+ "copyRefFailed": "Impossible de copier la référence du bloc",
+ "copyRefNoNote": "Enregistrez la note avant de copier une référence de bloc",
+ "copyRefUnsupported": "Ce type de bloc ne peut pas encore être référencé"
+ },
+ "smartPaste": {
+ "prompt": "Coller cette référence de bloc en tant que :",
+ "liveBlock": "Bloc connecté (live)",
+ "plainLink": "Texte / lien simple",
+ "unknownNote": "Note sans titre"
+ },
+ "liveBlock": {
+ "connectedNote": "Note connectée",
+ "sourceDisconnected": "Source déconnectée",
+ "statusLive": "LIVE",
+ "statusOffline": "HORS-LIGNE",
+ "statusDisconnected": "DÉCONNECTÉ",
+ "detachLink": "Décharger le lien",
+ "detachHelp": "Transformer ce bloc en texte normal dans cette note",
+ "openSource": "Ouvrir",
+ "removeBlock": "Supprimer le bloc",
+ "emptyContent": "(bloc vide)"
+ },
+ "notePeek": {
+ "label": "Note liée",
+ "panelLabel": "Aperçu de la note source",
+ "close": "Fermer l'aperçu",
+ "openFully": "Ouvrir en plein écran",
+ "openFullyHelp": "Remplacer la note courante par cette note",
+ "readOnlyHint": "Aperçu en lecture seule — ouvrez en plein écran pour modifier.",
+ "loadFailed": "Impossible d'ouvrir l'aperçu de la note liée."
+ },
+ "databaseBlock": {
+ "title": "Auteurs & Œuvres",
+ "hint": "Base inline — les données sont enregistrées dans cette note.",
+ "viewTable": "Tableau",
+ "viewCards": "Fiches",
+ "colAuthor": "Auteur",
+ "colWorks": "Œuvres",
+ "colRollup": "Total",
+ "noLinkedWorks": "Aucune œuvre liée",
+ "deleteShort": "Suppr.",
+ "deleteCard": "Retirer l'œuvre",
+ "addAuthor": "Ajouter un auteur",
+ "authorPlaceholder": "Nom de l'auteur…",
+ "createAuthor": "Créer",
+ "addWork": "Ajouter une œuvre",
+ "bookTitlePlaceholder": "Titre de l'œuvre…",
+ "selectAuthor": "Choisir un auteur…",
+ "tagPlaceholder": "Genre / étiquette…",
+ "coverPlaceholder": "URL de couverture (optionnel)",
+ "insertWork": "Insérer l'œuvre",
+ "defaultTag": "Général",
+ "worksBase": "Base d'œuvres",
+ "storedCount": "{count} œuvre(s) enregistrée(s)",
+ "insertFailed": "Impossible d'insérer le bloc base de données. Réessayez sur une ligne vide."
}
}
\ No newline at end of file
diff --git a/memento-note/next.config.ts b/memento-note/next.config.ts
index a37d780..6add61a 100644
--- a/memento-note/next.config.ts
+++ b/memento-note/next.config.ts
@@ -63,7 +63,7 @@ const nextConfig: NextConfig = {
},
{
source: '/reminders',
- destination: '/?reminders=1',
+ destination: '/home?reminders=1',
permanent: false,
},
]
diff --git a/memento-note/package-lock.json b/memento-note/package-lock.json
index 771659c..9a06150 100644
--- a/memento-note/package-lock.json
+++ b/memento-note/package-lock.json
@@ -40,9 +40,13 @@
"@stripe/react-stripe-js": "^6.3.0",
"@stripe/stripe-js": "^9.5.0",
"@tanstack/react-query": "^5.100.9",
+ "@tiptap/extension-collaboration": "^3.23.6",
"@tiptap/extension-color": "^3.22.5",
+ "@tiptap/extension-drag-handle": "^3.23.6",
+ "@tiptap/extension-drag-handle-react": "^3.23.6",
"@tiptap/extension-highlight": "^3.22.5",
"@tiptap/extension-image": "^3.22.5",
+ "@tiptap/extension-node-range": "^3.23.6",
"@tiptap/extension-placeholder": "^3.22.5",
"@tiptap/extension-subscript": "^3.22.5",
"@tiptap/extension-superscript": "^3.22.5",
@@ -59,6 +63,7 @@
"@tiptap/react": "^3.22.5",
"@tiptap/starter-kit": "^3.22.5",
"@tiptap/suggestion": "^3.22.5",
+ "@tiptap/y-tiptap": "^3.0.3",
"@types/d3": "^7.4.3",
"@types/jsdom": "^28.0.1",
"ai": "^6.0.23",
@@ -111,7 +116,11 @@
"stripe": "^22.1.1",
"tailwind-merge": "^3.4.0",
"tinyld": "^1.3.4",
+ "tiptap-extension-auto-joiner": "^0.1.3",
+ "tiptap-extension-global-drag-handle": "^0.1.18",
"vazirmatn": "^33.0.3",
+ "y-protocols": "^1.0.7",
+ "yjs": "^13.6.30",
"zod": "^4.3.5"
},
"devDependencies": {
@@ -3765,350 +3774,11 @@
"url": "https://github.com/sponsors/panva"
}
},
- "node_modules/@parcel/watcher": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
- "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "detect-libc": "^2.0.3",
- "is-glob": "^4.0.3",
- "node-addon-api": "^7.0.0",
- "picomatch": "^4.0.3"
- },
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "@parcel/watcher-android-arm64": "2.5.6",
- "@parcel/watcher-darwin-arm64": "2.5.6",
- "@parcel/watcher-darwin-x64": "2.5.6",
- "@parcel/watcher-freebsd-x64": "2.5.6",
- "@parcel/watcher-linux-arm-glibc": "2.5.6",
- "@parcel/watcher-linux-arm-musl": "2.5.6",
- "@parcel/watcher-linux-arm64-glibc": "2.5.6",
- "@parcel/watcher-linux-arm64-musl": "2.5.6",
- "@parcel/watcher-linux-x64-glibc": "2.5.6",
- "@parcel/watcher-linux-x64-musl": "2.5.6",
- "@parcel/watcher-win32-arm64": "2.5.6",
- "@parcel/watcher-win32-ia32": "2.5.6",
- "@parcel/watcher-win32-x64": "2.5.6"
- }
- },
- "node_modules/@parcel/watcher-android-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
- "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
- "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
- "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-freebsd-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
- "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
- "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
- "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
- "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
- "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
- "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
- "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
- "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-ia32": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
- "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
- "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher/node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
@@ -4142,14 +3812,14 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
- "devOptional": true,
+ "dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4163,14 +3833,14 @@
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0",
@@ -4182,7 +3852,7 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0"
@@ -7946,6 +7616,22 @@
"@tiptap/pm": "3.22.5"
}
},
+ "node_modules/@tiptap/extension-collaboration": {
+ "version": "3.23.6",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.23.6.tgz",
+ "integrity": "sha512-OvPtPUWH3uormtQ8Ei/di6h/gv9ttv+IDy8zz1t4hrP2XLQHgpd1yv8/bnwz0jsZhsl+4Km2Wb0H5oS3Rh5+cA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.23.6",
+ "@tiptap/pm": "3.23.6",
+ "@tiptap/y-tiptap": "^3.0.2",
+ "yjs": "^13"
+ }
+ },
"node_modules/@tiptap/extension-color": {
"version": "3.22.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.22.5.tgz",
@@ -7972,6 +7658,43 @@
"@tiptap/core": "3.22.5"
}
},
+ "node_modules/@tiptap/extension-drag-handle": {
+ "version": "3.23.6",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-drag-handle/-/extension-drag-handle-3.23.6.tgz",
+ "integrity": "sha512-+IJgVq7GDb4yDft5Dr8fW61qqdyeO30QgaS12GlvX+mfaurPOUtMhnr9POX1Vb4QMogxkwXD6pA0RA+AYkI3qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.6.13"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.23.6",
+ "@tiptap/extension-collaboration": "3.23.6",
+ "@tiptap/extension-node-range": "3.23.6",
+ "@tiptap/pm": "3.23.6",
+ "@tiptap/y-tiptap": "^3.0.2"
+ }
+ },
+ "node_modules/@tiptap/extension-drag-handle-react": {
+ "version": "3.23.6",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-drag-handle-react/-/extension-drag-handle-react-3.23.6.tgz",
+ "integrity": "sha512-ZScNZMinmjFNRgPNjvz0+ZHZXbM/s0JgYv14u/JptiGK7q63pSFDACL4mpRvx7aYwkkLs6egOOux1DVvWqhNHg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-drag-handle": "3.23.6",
+ "@tiptap/pm": "3.23.6",
+ "@tiptap/react": "3.23.6",
+ "react": "^16.8 || ^17 || ^18 || ^19",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/@tiptap/extension-dropcursor": {
"version": "3.22.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.5.tgz",
@@ -8150,6 +7873,20 @@
"@tiptap/extension-list": "3.22.5"
}
},
+ "node_modules/@tiptap/extension-node-range": {
+ "version": "3.23.6",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.23.6.tgz",
+ "integrity": "sha512-S+EN8HDnXOFxtamtkAkDY++B6jnVfjNBBHtDbO6i4as7PUeRY/JJIPPwlyL/cZULfP+ePYyr4eIVxFSMQxFXtg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "3.23.6",
+ "@tiptap/pm": "3.23.6"
+ }
+ },
"node_modules/@tiptap/extension-ordered-list": {
"version": "3.22.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.5.tgz",
@@ -8489,6 +8226,26 @@
"@tiptap/pm": "3.22.5"
}
},
+ "node_modules/@tiptap/y-tiptap": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.3.tgz",
+ "integrity": "sha512-8UvuV4lTisCE9cMTc/X8kRyTn9edUO7Kball0I6wb17VwZSjNDfh/YKtP4O5vcPawEzFHQIvZGq/k1h37kAf0w==",
+ "license": "MIT",
+ "dependencies": {
+ "lib0": "^0.2.100"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.7.1",
+ "prosemirror-state": "^1.2.3",
+ "prosemirror-view": "^1.9.10",
+ "y-protocols": "^1.0.1",
+ "yjs": "^13.5.38"
+ }
+ },
"node_modules/@tweenjs/tween.js": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
@@ -8952,6 +8709,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -8961,6 +8719,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -14232,6 +13991,16 @@
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
}
},
+ "node_modules/isomorphic.js": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
+ "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
+ "license": "MIT",
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -14633,6 +14402,27 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lib0": {
+ "version": "0.2.117",
+ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
+ "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
+ "license": "MIT",
+ "dependencies": {
+ "isomorphic.js": "^0.2.4"
+ },
+ "bin": {
+ "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
+ "0gentesthtml": "bin/gentesthtml.js",
+ "0serve": "bin/0serve.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
@@ -16336,15 +16126,6 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/node-addon-api": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
- "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/node-exports-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -16853,7 +16634,7 @@
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
@@ -16872,7 +16653,7 @@
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -17052,7 +16833,7 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
- "devOptional": true,
+ "dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -19090,6 +18871,18 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tiptap-extension-auto-joiner": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/tiptap-extension-auto-joiner/-/tiptap-extension-auto-joiner-0.1.3.tgz",
+ "integrity": "sha512-nY3aKeCpVb2WjjVEZkLtEqxsK3KU1zGioyglMhK1sUFNjKDccOfRyz/YDKrHRAVsKJPGnk2A8VA1827iGEAXWQ==",
+ "license": "MIT"
+ },
+ "node_modules/tiptap-extension-global-drag-handle": {
+ "version": "0.1.18",
+ "resolved": "https://registry.npmjs.org/tiptap-extension-global-drag-handle/-/tiptap-extension-global-drag-handle-0.1.18.tgz",
+ "integrity": "sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==",
+ "license": "MIT"
+ },
"node_modules/tldts": {
"version": "7.0.28",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
@@ -19920,24 +19713,6 @@
}
}
},
- "node_modules/vitest/node_modules/chokidar": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
- "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "readdirp": "^5.0.0"
- },
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
"node_modules/vitest/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -19953,15 +19728,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/vitest/node_modules/immutable": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
- "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
@@ -19975,45 +19741,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/vitest/node_modules/readdirp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
- "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/vitest/node_modules/sass": {
- "version": "1.100.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
- "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "chokidar": "^5.0.0",
- "immutable": "^5.1.5",
- "source-map-js": ">=0.6.2 <2.0.0"
- },
- "bin": {
- "sass": "sass.js"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "optionalDependencies": {
- "@parcel/watcher": "^2.4.1"
- }
- },
"node_modules/vitest/node_modules/vite": {
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
@@ -20442,6 +20169,26 @@
"node": ">=0.4.0"
}
},
+ "node_modules/y-protocols": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz",
+ "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==",
+ "license": "MIT",
+ "dependencies": {
+ "lib0": "^0.2.85"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ },
+ "peerDependencies": {
+ "yjs": "^13.0.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -20488,6 +20235,23 @@
"node": ">=12"
}
},
+ "node_modules/yjs": {
+ "version": "13.6.30",
+ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz",
+ "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lib0": "^0.2.99"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/memento-note/package.json b/memento-note/package.json
index 16ac2d2..46dca82 100644
--- a/memento-note/package.json
+++ b/memento-note/package.json
@@ -61,9 +61,13 @@
"@stripe/react-stripe-js": "^6.3.0",
"@stripe/stripe-js": "^9.5.0",
"@tanstack/react-query": "^5.100.9",
+ "@tiptap/extension-collaboration": "^3.23.6",
"@tiptap/extension-color": "^3.22.5",
+ "@tiptap/extension-drag-handle": "^3.23.6",
+ "@tiptap/extension-drag-handle-react": "^3.23.6",
"@tiptap/extension-highlight": "^3.22.5",
"@tiptap/extension-image": "^3.22.5",
+ "@tiptap/extension-node-range": "^3.23.6",
"@tiptap/extension-placeholder": "^3.22.5",
"@tiptap/extension-subscript": "^3.22.5",
"@tiptap/extension-superscript": "^3.22.5",
@@ -80,6 +84,7 @@
"@tiptap/react": "^3.22.5",
"@tiptap/starter-kit": "^3.22.5",
"@tiptap/suggestion": "^3.22.5",
+ "@tiptap/y-tiptap": "^3.0.3",
"@types/d3": "^7.4.3",
"@types/jsdom": "^28.0.1",
"ai": "^6.0.23",
@@ -132,7 +137,11 @@
"stripe": "^22.1.1",
"tailwind-merge": "^3.4.0",
"tinyld": "^1.3.4",
+ "tiptap-extension-auto-joiner": "^0.1.3",
+ "tiptap-extension-global-drag-handle": "^0.1.18",
"vazirmatn": "^33.0.3",
+ "y-protocols": "^1.0.7",
+ "yjs": "^13.6.30",
"zod": "^4.3.5"
},
"devDependencies": {