From a7b16870a4e1aa5256ddbbb372867a06686b6699 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Tue, 1 Sep 2026 19:40:10 +0000 Subject: [PATCH] =?UTF-8?q?fix(ui):=20s=C3=A9lection=20group=C3=A9e=20dans?= =?UTF-8?q?=20la=20bo=C3=AEte=20de=20r=C3=A9ception?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permet de cocher plusieurs notes, les déplacer vers un carnet ou les envoyer à la corbeille, et rend la carte mentale plus lisible. Co-authored-by: Cursor --- AGENTS.md | 12 +- memento-note/app/actions/notes-trash.ts | 51 +++++++ memento-note/app/actions/notes.ts | 39 ++++++ .../components/confirm-delete-note-dialog.tsx | 8 +- .../components/dashboard-mind-orbit.tsx | 40 ++++-- memento-note/components/home-client.tsx | 130 +++++++++++++++++- memento-note/components/inbox-bulk-bar.tsx | 92 +++++++++++++ .../components/move-to-notebook-picker.tsx | 4 +- .../components/note-select-checkbox.tsx | 39 ++++++ .../components/notes-editorial-view.tsx | 15 +- memento-note/components/notes-list-views.tsx | 56 +++++++- .../components/public-site-chrome.tsx | 8 +- memento-note/lib/notes/trash-toast.ts | 30 +++- memento-note/locales/en.json | 11 ++ memento-note/locales/fr.json | 11 ++ 15 files changed, 509 insertions(+), 37 deletions(-) create mode 100644 memento-note/components/inbox-bulk-bar.tsx create mode 100644 memento-note/components/note-select-checkbox.tsx diff --git a/AGENTS.md b/AGENTS.md index b3bec365..b711432c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,17 +3,17 @@ ## Learned User Preferences - **Français obligatoire dans les réponses à l’utilisateur** : français adulte, phrases complètes, comme on parle à quelqu’un d’intelligent qui n’est pas informaticien. **Interdit** : anglais, mots anglais collés dans une phrase française, jargon de designer ou de développeur (ex. héros, widget, streak, CTA, dashboard, flashcards — dire plutôt « grand encadré », « carte », « jours d’affilée », « bouton », « page d’accueil », « cartes de révision »). Le français télégraphique, infantile ou incorrect est une **correction dure** (l’utilisateur a déjà dit qu’il ne comprenait rien). Expliquer simplement ce qui a changé à l’écran — nommer le bouton ou la barre visible, pas une métaphore (« chapeau de l’éditeur »). **Rester sur le sujet demandé** (ne pas digresser vers git/ops hors scope quand la demande porte sur une feature métier). **Nom produit** : **Memento** (jamais « Momento ») — libellés, metadata, pages publiques, docs. -- 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 ; **jamais de marque tierce** dans les libellés utilisateur (ex. référence design « Kimi » → UI/i18n = « Page interactive » / « Interactive page ») ; **insights Memory Echo** (texte généré/stocké) : langue utilisateur à la génération + fallback i18n à l'affichage (`memoryEcho.defaultInsight`) — jamais d'anglais en dur quand l'UI est FR. +- 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 », « scrape », 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 ; **jamais de marque tierce** dans les libellés utilisateur (ex. référence design « Kimi » → UI/i18n = « Page interactive » / « Interactive page ») ; **insights Memory Echo** (texte généré/stocké) : langue utilisateur à la génération + fallback i18n à l'affichage (`memoryEcho.defaultInsight`) — jamais d'anglais en dur quand l'UI est FR. - 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/Memento/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 ; **sidebar rail** (`sidebar.tsx`) : une seule icône active ; **largeur sidebar** (`components/sidebar.tsx`) — redimensionnement utilisateur par poignée sur le bord droit (desktop `md+`, 280–560px, persisté `localStorage` `memento-sidebar-width`) ; défaut responsive `w-80` puis plus large en `xl`/`2xl` si aucune valeur stockée ; **hauteur zone carnets** ajustable via poignée horizontale (`memento-sidebar-notebooks-height`, min 120px) ; fallback `Suspense` de `app/(main)/layout.tsx` **aligné** sur les largeurs par défaut (rail fixe 54px) ; **pas de bouton « Note du jour »** dans la sidebar (retiré volontairement) ; **dashboard Second Brain** (`/home`, `dashboard-view.tsx`) : vue d'ensemble **interactive** (pas dashboard statique/mort) — layout **v7** configurable (catalogue ~22 widgets, persisté `/api/dashboard/layout`) — **ne PAS alléger/cacher des widgets** ni bumper `DASHBOARD_LAYOUT_VERSION` dans `lib/dashboard/layout.ts` (réécrit les layouts de tous les utilisateurs) ; distill ≠ amputer le produit ; jamais reset d'un dashboard vivant sans confirmation explicite ; chantier carte par carte / Impeccable sur `/home` : **ne pas cacher** les cartes catalogue-only (revue du matin, boucles ouvertes, usage, Gmail) ni les **activer** sans demande ; **« Continuer »** : pas de notes vides sans titre (un extrait est OK si la note a du texte) ; compteurs inbox / à réviser / découvertes / thèmes **une seule rangée actionnable**, pas répétés dans le sous-titre ET le bandeau ET les raccourcis du panneau latéral ; « L'IA a trouvé » **ne doit pas** reciter les mêmes ponts que « Prochaines pistes » ; « Personnaliser » **pas** en barre fixe en bas qui recouvre les cartes ; widget **« Prochaines pistes »** (`next-paths`) = recommandations actionnables depuis la dernière note éditée (fast paths briefing puis enrichissement async `/api/briefing/paths`) ; widget **« Revue quotidienne »** = **checklist matinale interactive** (inbox, découvertes IA, connexion, flashcards) — **pas** une revue automatique de contenu ; cartes cliquables, section Memory Echo « L'IA a trouvé » exploitable (pas vide après 1ère visite), consentement/provider IA + **quotas** vérifiés sur chaque action IA (dashboard et agents) ; chaque widget : **aide contextuelle « ? » i18n** ; couleurs alignées sur le **thème settings** ; icônes **Lucide** (pas emojis système type 🎯) ; `MindMapCard` remplace l'ancien `MiniResonanceGraph` D3 (graphe décoratif qui débordait) ; navigation sidebar alignée sur `/home` ; `activeView` synchronisé avec pathname et query (`/insights`, `/revision`, `/home?reminders=1`) ; **`/home` sans filtre = dashboard**, liste notes via `?forceList=1` (`lib/dashboard/home-route.ts`) — **toujours un moyen de revenir au dashboard** depuis notes/liste (ne jamais coincer l’utilisateur dans la vue notes ; un chantier design/Impeccable ne doit pas casser routing/sidebar/`forceList`) ; 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 ; **ne pas inventer ni enchaîner** d’autres dessins sans accord explicite (rejetés : libellés de thèmes tronqués, zones colorées avec noms empilés, titres de notes sur le cluster, grille de bulles, liste comme vue d’arrivée à la place du graphe) ; **ne pas retirer** les noms notes/thèmes/connexions du graphe sans demande ; **Connexions (`network-graph.tsx`) : une fois les noms restaurés, ne plus y toucher** (aucun nouvel essai de dessin) ; si un dessin est refusé, **s’arrêter et attendre** — pas d’essais en série ; lorsque données clusters en retard ou partiellement périmées, **montrer l’état dégradé exploitable plutôt qu’une page vide** ; liens/ponts proposés : **pertinence sémantique réelle** (rejeter associations absurdes cross-domaine) ; **pas d'IDs techniques** type « cluster 15 » exposés à l'utilisateur ; bouton Menu immersif `/insights` : **cacher la sidebar ET agrandir le contenu principal** (pas de bandeau beige vide — attention `twMerge` qui peut faire perdre `fixed` au profit de `relative`) ; si l'utilisateur hésite entre variantes UX, **trancher pour le design prototype** plutôt que multiplier les toggles ; scores Impeccable **indicatifs** — ils ne primeront pas sur le prototype `architectural-grid` ni PRODUCT.md en cas de conflit ; **landing page** (`components/landing-page.tsx`) : style marketing sombre/premium type **x.ai** (pas de fond blanc plat) ; mettre en avant **Second Brain** ; textes publics **sans jargon produit anglais restant** (lock-in, Scraper, Slide Gen, SM-2, etc.) ; noms de modèles BYOK depuis le **catalogue serveur vivant** (`/api/public/byok-catalog`) — pas une liste figée ; **multilinguisme** avec sélecteur de langue (icône globe — ne pas confondre avec navigation back) ; harmonie dark mode (ex. zone notifications). +- 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 ; **largeur sidebar** (`components/sidebar.tsx`) — redimensionnement utilisateur par poignée sur le bord droit (desktop `md+`, 280–560px, persisté `localStorage` `memento-sidebar-width`) ; défaut responsive `w-80` puis plus large en `xl`/`2xl` si aucune valeur stockée ; **hauteur zone carnets** ajustable via poignée horizontale (`memento-sidebar-notebooks-height`, min 120px) ; fallback `Suspense` de `app/(main)/layout.tsx` **aligné** sur les largeurs par défaut (rail fixe 54px) ; **pas de bouton « Note du jour »** dans la sidebar (retiré volontairement) ; **dashboard Second Brain** (`/home`, `dashboard-view.tsx`) : vue d'ensemble **interactive** (pas dashboard statique/mort) — layout **v7** configurable (catalogue ~22 widgets, persisté `/api/dashboard/layout`) — **ne PAS alléger/cacher des widgets** ni bumper `DASHBOARD_LAYOUT_VERSION` dans `lib/dashboard/layout.ts` (réécrit les layouts de tous les utilisateurs) ; distill ≠ amputer le produit ; jamais reset d'un dashboard vivant sans confirmation explicite ; chantier carte par carte / Impeccable sur `/home` : **ne pas cacher** les cartes catalogue-only (revue du matin, boucles ouvertes, usage, Gmail) ni les **activer** sans demande ; **« Continuer »** : pas de notes vides sans titre (un extrait est OK si la note a du texte) ; compteurs inbox / à réviser / découvertes / thèmes **une seule rangée actionnable**, pas répétés dans le sous-titre ET le bandeau ET les raccourcis du panneau latéral ; « L'IA a trouvé » **ne doit pas** reciter les mêmes ponts que « Prochaines pistes » ; « Personnaliser » **pas** en barre fixe en bas qui recouvre les cartes ; widget **« Prochaines pistes »** (`next-paths`) = recommandations actionnables depuis la dernière note éditée (fast paths briefing puis enrichissement async `/api/briefing/paths`) ; widget **« Revue quotidienne »** = **checklist matinale interactive** (inbox, découvertes IA, connexion, flashcards) — **pas** une revue automatique de contenu ; cartes cliquables, section Memory Echo « L'IA a trouvé » exploitable (pas vide après 1ère visite), consentement/provider IA + **quotas** vérifiés sur chaque action IA (dashboard et agents) ; chaque widget : **aide contextuelle « ? » i18n** ; couleurs alignées sur la couleur d’**Apparence** (Paramètres → Apparence, `--color-brand-accent` — boutons d’action inclus, pas de noir/violet/beige en dur) ; icônes **Lucide** (pas emojis système type 🎯) ; `MindMapCard` remplace l'ancien `MiniResonanceGraph` D3 (graphe décoratif qui débordait) — bulles à contraste lisible (pas fonds pâles + petits chiffres blancs) ; navigation sidebar alignée sur `/home` ; `activeView` synchronisé avec pathname et query (`/insights`, `/revision`, `/home?reminders=1`) ; **`/home` sans filtre = dashboard**, liste notes via `?forceList=1` (`lib/dashboard/home-route.ts`) — **toujours un moyen de revenir au dashboard** depuis notes/liste (ne jamais coincer l’utilisateur dans la vue notes ; un chantier design/Impeccable ne doit pas casser routing/sidebar/`forceList`) ; 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`) ; **`/graph` supprimée — ne pas restaurer** (reste mort : « Seuil sémantique ») ; ne pas substituer par une UX « géométrique » décorative ou un regroupement par carnet hors spec prototype ; **ne pas inventer ni enchaîner** d’autres dessins sans accord explicite (rejetés : libellés de thèmes tronqués, zones colorées avec noms empilés, titres de notes sur le cluster, grille de bulles, liste comme vue d’arrivée à la place du graphe) ; **ne pas retirer** les noms notes/thèmes/connexions du graphe sans demande ; **Connexions (`network-graph.tsx`) : une fois les noms restaurés, ne plus y toucher** (aucun nouvel essai de dessin) ; file restante `/insights` = chrome (libellés, i18n, jargon), **pas** le graphe ; si un dessin est refusé, **s’arrêter et attendre** — pas d’essais en série ; lorsque données clusters en retard ou partiellement périmées, **montrer l’état dégradé exploitable plutôt qu’une page vide** ; liens/ponts proposés : **pertinence sémantique réelle** (rejeter associations absurdes cross-domaine) ; **pas d'IDs techniques** type « cluster 15 » exposés à l'utilisateur ; bouton Menu immersif `/insights` : **cacher la sidebar ET agrandir le contenu principal** (pas de bandeau beige vide — attention `twMerge` qui peut faire perdre `fixed` au profit de `relative`) ; si l'utilisateur hésite entre variantes UX, **trancher pour le design prototype** plutôt que multiplier les toggles ; scores Impeccable **indicatifs** — ils ne primeront pas sur le prototype `architectural-grid` ni PRODUCT.md en cas de conflit ; **landing page** (`components/landing-page.tsx`) : style marketing sombre/premium type **x.ai** (pas de fond blanc plat) ; **deux univers** — site public sombre (landing **et** `/pricing` : même barre, pied de page, langue — pas un troisième look) vs app connectée crème type cahier ; mettre en avant **Second Brain** ; textes publics **sans jargon produit anglais restant** (lock-in, Scraper, Slide Gen, SM-2, etc.) ; noms de modèles BYOK depuis le **catalogue serveur vivant** (`/api/public/byok-catalog`) — pas une liste figée ; **multilinguisme** avec sélecteur de langue (icône globe — ne pas confondre avec navigation back) ; harmonie dark mode (ex. zone notifications). - 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 ; **ne jamais hardcoder d'IP/URL** dans le repo ou les défauts CI — uniquement variables d'env (ex. `GRAFANA_BIND`, `GRAFANA_URL`) ; la config runtime reste sur le serveur dans `.env.docker`. **Machines** : agent/dev = **192.168.1.83** (devSanbox) ; prod = **192.168.1.190** (`/opt/memento`) — **ne jamais confondre** les deux au diagnostic services. **CI/CD Gitea spécifique** : (1) CI `ubuntu-24.04` = lint + tests + build (validation) ; deploy `docker-host` = build sur le serveur → `/tmp/web-artifact.tgz` → `deploy-prod.sh` — **sans** upload/download-artifact inter-runners (stockage Gitea instable ; si artifacts utilisés, **@v3** uniquement — @v4 → `GHESNotSupportedError`) ; (2) `Dockerfile.socket.prebuilt` doit utiliser `--legacy-peer-deps` dans `npm install` (conflit TipTap 3.22.5 vs 3.23.6) ; (3) le serveur de prod (192.168.1.190) **ne peut pas pull Docker Hub** (DNS cassé) — le build Docker complet échoue, seul le chemin prebuilt artifact fonctionne ; (4) `docker-entrypoint.sh` applique les migrations Prisma **avant** de démarrer le serveur Next.js (ordre correct) ; (5) rollback d'urgence : `docker tag memento-memento-note:rollback memento-memento-note:latest && docker compose up -d --force-recreate memento-note` ; (6) `scripts/deploy-prod.sh` charge `/opt/memento/.env.docker` via un **parseur robuste ligne-par-ligne** (jamais `source` — crash `unexpected EOF` sur guillemet non fermé) et teste Postgres avec `pg_isready` + credentials **du conteneur** (`$POSTGRES_USER` interne) ; (7) lint CI = `eslint --max-warnings 9999` : les **warnings ne bloquent pas**, seules les **erreurs** bloquent (ex. `no-html-link-for-pages` → `` obligatoire pour la navigation interne, pas ``) ; **TRAVAILLER SUR UNE BRANCHE** pendant le dev, ne push sur `main` que quand le code est testé et fonctionnel — **avant push `main`** : exécuter localement `npm run lint`, `npm run test:unit`, `npm run build` (même enchaînement que CI) ; chaque push sur `main` déclenche un déploiement automatique en production. -- Authentification : priorité à l'inscription/connexion via **Google OAuth** (plutôt qu'un compte email/mot de passe) ; bouton Google **absent en local = attendu** (visible seulement en prod) — ne pas « corriger » ça ; inscription **email/mdp** : **email de confirmation obligatoire** (`lib/auth/email-verification.ts`, envoi à l'inscription) — login bloqué tant que `emailVerified` est null ; OAuth Google marque l'email comme vérifié automatiquement ; exiger une vraie déconnexion (invalidation session/cookies — pas de reconnexion implicite, y compris en navigation privée) ; prod : `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` dans `/opt/memento/.env.docker` **et** variables/secrets Gitea Actions (workflow deploy n'écrit pas les valeurs vides) — absence → `invalid_client` ; redirect URI exacte `https://memento-note.com/api/auth/callback/google` ; config via **Google Auth Platform** (Branding → Audience **In production**, pas Testing → Clients Web) ; `ALLOW_REGISTRATION=true` requis pour inscription email (env ou `SystemConfig` en base — la DB peut primer sur l'env) ; **navigateur intégré Cursor peu fiable pour le login** (focus volé, cookies isolés, OAuth Google souvent bloqué) — préférer un vrai navigateur pour la connexion manuelle, et pour l'automation (démos/Playwright) une **session JWT/cookies stockée** plutôt que de rejouer le login dans le browser Cursor. +- Authentification : priorité à l'inscription/connexion via **Google OAuth** (plutôt qu'un compte email/mot de passe) ; bouton Google **absent en local = attendu** (visible seulement en prod) — ne pas « corriger » ça ; inscription **email/mdp** : **email de confirmation obligatoire** (`lib/auth/email-verification.ts`, envoi à l'inscription) — login bloqué tant que `emailVerified` est null ; OAuth Google marque l'email comme vérifié automatiquement ; exiger une vraie déconnexion (invalidation session/cookies — pas de reconnexion implicite, y compris en navigation privée) ; prod : `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` dans `/opt/memento/.env.docker` **et** variables/secrets Gitea Actions (workflow deploy n'écrit pas les valeurs vides) — absence → `invalid_client` ; redirect URI exacte `https://memento-note.com/api/auth/callback/google` ; config via **Google Auth Platform** (Branding → Audience **In production**, pas Testing → Clients Web) ; `ALLOW_REGISTRATION=true` requis pour inscription email (env ou `SystemConfig` en base — la DB peut primer sur l'env) ; **navigateur intégré Cursor peu fiable pour le login** (focus volé, cookies isolés, OAuth Google souvent bloqué) — mismatch d’hydratation `data-cursor-ref` = overlay Cursor, **pas un bug Memento** ; préférer un vrai navigateur pour la connexion manuelle, et pour l'automation (démos/Playwright) une **session JWT/cookies stockée** plutôt que de rejouer le login dans le browser Cursor. - Priorité absolue à la qualité UX, même si l'implémentation est complexe (« je m'en fous si c'est complexe ») ; préférer les **solutions long terme** aux rustines ; si l'utilisateur demande explicitement d'**analyser sans coder**, expliquer d'abord et **ne pas modifier le code** tant qu'il n'a pas validé ; pour les **tâches ops** (config OAuth prod, serveur), préfère un **guidage interactif étape par étape** plutôt qu'un bloc d'instructions monolithique ; **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` — éditeur courant à **gauche**, note liée en lecture seule à **droite** en LTR ; **inversé en RTL** `fa`/`ar`), **pas nouvel onglet** ; **Interactive Demo** (player TipTap) : sélecteur de vitesse **visible** (`0.5x` / `1x` / `2x` / `4x` — pas un bouton cycle cryptique) ; scènes **dignes du contenu** (pas de boîtes jouet 2 nœuds / texte plat) — labels et équations issus de la note (TipTap math `data-latex` / KaTeX) ; génération IA via le **modèle slides** (`getSlidesProvider`), **pas** la lane chat ; **Page interactive** (explainer publié, **≠** Interactive Demo TipTap) : pipeline note → PageSpec → `/p/{slug}` ; priorité **vitesse/fiabilité** de génération (chemin **déterministe** `buildPageFromNote` / démo déterministe plutôt que LLM lent qui timeoute ~150 s vs abort client) ; simulateurs (ex. cycle de Carnot) : flèches correctement orientées, têtes SVG non déformées, flux branchés sur la machine (pas de flèches flottantes) ; unités température commutables **K / °C / °F** ; distinguer **énergie (kJ)** et **puissance (W)** ; modes **frigo / PAC / moteur** ; **ne jamais annuler du code non commité** (`git checkout`, reset fichier) **sans demande explicite** (perte de travail documentée, ex. drag handle éditeur) ; **ne jamais remettre du code que l'utilisateur a explicitement retiré sans demander d'abord** (ex. `reserveUsageOrThrow` retiré intentionnellement de `organize-notebook.ts` — agent l'a remis sans demander → user mécontent) ; **correction i18n ou spec doc** : **ne pas refondre logique/UI** hors scope (ex. US-4 `structuredViewBlock` — garder le dual-mode base locale + lien carnet, pas de suppression du mode local) ; 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 ; **vidéos promo** : VO **anglaise** ; montrer de **vraies features authentifiées** (dashboard, Memory Echo, Insights, Revision, Agents, menu `/` éditeur…) — pas un slideshow landing seul ; démos note/éditeur : **masquer sidebar/carnets** pour focaliser la note ; style kinetic OK (coupes rapides + texte à l'écran) mais **pas de fade-to-black** entre plans (clignote) — **crossfades** ; après saisie, montrer le **sparkle** titre IA ; **interdit Ken Burns / `zoompan`** (surtout sur fonds pointillés → moiré / tremblement) — images fixes, fond uni stable à la capture. - 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é) ; « suivant » / « continue » / « ok vas-y » après un commit n'est **pas** un chèque en blanc pour changer la surface majeure suivante sans montrer le plan et obtenir validation ; « fais le reste » = uniquement les points **déjà listés**, **pas** la permission d’inventer ni de retoucher une surface déjà refusée (surtout le dessin `/insights`) ; suivi dans `docs/user-stories.md` ; briefs pour outil de design externe sur demande ; **avant de développer une story, vérifier d'abord dans le code si la feature existe déjà** — les docs de stories (`docs/story-nextgen-editor.md`, `docs/user-stories.md`) sont **souvent périmés** (features livrées mais marquées « à faire ») ; **dashboard Second Brain** : même rythme — **une feature à la fois**, validation explicite avant la suivante. -- **Facturation & quotas IA** : limites mensuelles, tiers (BASIC/PRO/BUSINESS/ENTERPRISE) et Price IDs Stripe via **Admin > Facturation & quotas** (`/admin/billing`) — pas via `.env` pour le métier ; secrets Stripe (`STRIPE_SECRET_KEY`, webhook) restent en env serveur ; doc `memento-note/docs/admin-billing-quotas-guide.md` ; **essai gratuit abonnement = 7 jours** (`SUBSCRIPTION_TRIAL_DAYS` dans `lib/billing/trial-constants.ts` — pas 14) : afficher clairement sur **landing** et UI facturation (**i18n partout**) ; **tier BASIC sans accès MCP** ; chaque usage IA (dashboard, agents, MCP si autorisé, etc.) doit **décompter le quota** — mécanisme = **réservation atomique upfront** (Redis/Postgres) **avant** l'appel IA, sans rollback si l'appel échoue ensuite ; BYOK peut contourner selon config — listes de modèles (réglages et pages publiques) via le **catalogue serveur** (`/api/user/api-keys/live-models`, `/api/public/byok-catalog`), pas de noms en dur ; ne pas affirmer un déploiement prod sans vérif réelle (ex. travail MCP/quotas encore local). +- **Facturation & quotas IA** : limites mensuelles, tiers (BASIC/PRO/BUSINESS/ENTERPRISE) et Price IDs Stripe via **Admin > Facturation & quotas** (`/admin/billing`) — pas via `.env` pour le métier ; secrets Stripe (`STRIPE_SECRET_KEY`, webhook) restent en env serveur ; doc `memento-note/docs/admin-billing-quotas-guide.md` ; **essai gratuit abonnement = 7 jours** (`SUBSCRIPTION_TRIAL_DAYS` dans `lib/billing/trial-constants.ts` — pas 14) : afficher clairement sur **landing** et UI facturation (**i18n partout**) ; toggle **Annuel** : le grand chiffre est l’équivalent mensuel (ex. 8,25 € /mois facturé à l’année) plus le total annuel en dessous — jamais 99 € comme chiffre principal à côté d’un badge « 17 % » ; **tier BASIC sans accès MCP** ; chaque usage IA (dashboard, agents, MCP si autorisé, etc.) doit **décompter le quota** — mécanisme = **réservation atomique upfront** (Redis/Postgres) **avant** l'appel IA, sans rollback si l'appel échoue ensuite ; BYOK peut contourner selon config — listes de modèles (réglages et pages publiques) via le **catalogue serveur** (`/api/user/api-keys/live-models`, `/api/public/byok-catalog`), pas de noms en dur ; ne pas affirmer un déploiement prod sans vérif réelle (ex. travail MCP/quotas encore local). ## Learned Workspace Facts @@ -22,10 +22,10 @@ - i18n : 15 fichiers sous `memento-note/locales/` (de, en, es, fr, it, pt, nl, pl, ru, zh, ja, ko, ar, fa, hi) ; logique sous `memento-note/lib/i18n/` ; référence `en.json` (~2218 clés) ; auditer les « non traduits » par flatten EN vs locale (souvent valeurs identiques à l'EN) ; pages auth (`/login`, `/register`) : `LanguageProvider` + `detectUserLanguage` comme les layouts public/main (pas d'anglais forcé). - Workflow BMad : stories sous `docs/` (ex. `3-4-host-pays-session-logic.md`), suivi sprint dans `docs/sprint-status.yaml` et stories courantes dans `docs/user-stories.md` ; skills sous `.claude/skills/bmad-*` ; `_bmad-output/planning-artifacts` souvent vide — planification de référence dans `docs/` ; préférer **une user story par feature** (pas de stories groupées). - PostgreSQL Docker (`memento-postgres`) port 5433 ; Redis (`memento-redis`) port 6379 ; règles Prisma/DB dans `CLAUDE.md`. -- **Admin facturation** : page `/admin/billing` (`billing-admin-client.tsx`, actions `admin-billing.ts`) — quotas par feature IA, config Stripe métier en base (effet ~60 s), santé/stats ops ; essai **7 jours** via `lib/billing/trial-constants.ts` ; guide `memento-note/docs/admin-billing-quotas-guide.md` ; settings user `/settings/billing` (`billing-plans.tsx`) ; catalogues modèles BYOK : `lib/ai/live-public-catalog.ts`, `/api/public/byok-catalog` (landing/tarifs), `/api/user/api-keys/live-models` (réglages clés). +- **Admin facturation** : page `/admin/billing` (`billing-admin-client.tsx`, actions `admin-billing.ts`) — quotas par feature IA, config Stripe métier en base (effet ~60 s), santé/stats ops ; essai **7 jours** via `lib/billing/trial-constants.ts` ; guide `memento-note/docs/admin-billing-quotas-guide.md` ; settings user `/settings/billing` (`billing-plans.tsx`) — annuel : équivalent mensuel + total annuel (pas le total comme chiffre principal) ; période d’abonnement calculée (`lib/billing/period.ts`, pas de dates figées) ; catalogues modèles BYOK : `lib/ai/live-public-catalog.ts`, `/api/public/byok-catalog` (landing/tarifs), `/api/user/api-keys/live-models` (réglages clés). - **Hosts** : machine agent/dev (**192.168.1.83**, Vibe/annotations locales) ≠ prod (**192.168.1.190**, dépôt `/opt/memento`) — ne jamais diagnostiquer l'un pour l'autre. Production : conteneur `memento-note` port **3000**, URL publique **https://memento-note.com** (nginx + Cloudflare ; ancien domaine note.parsanet.org) ; `NEXTAUTH_URL` aligné sur ce domaine ; auth prod requiert `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` dans `.env.docker` (sinon OAuth `invalid_client`) et `ALLOW_REGISTRATION=true` pour l'inscription email ; email sortant via **Resend** (`SMTP_FROM` ex. `noreply@memento-note.com`, domaine vérifié sur resend.com) ; monitoring Grafana via `GRAFANA_BIND` / `GRAFANA_URL` dans `.env.docker` (pas d'IP en dur dans le compose) ; 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` (lint + tests + build validation), deploy sur runner **`docker-host`** (build local `/tmp/web-artifact.tgz`, sans artifacts inter-runners) ; deploy manuel via `.gitea/workflows/deploy.yaml` ou `bash scripts/deploy-prod.sh`. - 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` ; **Interactive Demo** : JSON déclaratif allowlisté + player TipTap (`lib/interactive-demo/` — `validate.ts` / `resolve.ts` / schéma ; `components/interactive-demo/` ; extension `tiptap-interactive-demo-extension` ; API `/api/ai/interactive-demo` via `getSlidesProvider` + extraction formules type slides, labels KaTeX) — le player **consomme** `resolve.ts` (pas de logique de portée dans React) ; **Page interactive** (explainer) : `lib/interactive-page/` (PageSpecV1, schema/validate/normalize, fixtures thermo/Carnot) + `components/interactive-page/` (`page-view`, `sim-block`, publish dialog) + API `/api/ai/interactive-page` ; preview `/dev/interactive-page` ; publication publique `/p/{slug}` ; **simulateur Carnot** : `lib/simulators/carnot-cycle*.ts` + `components/simulators/carnot-cycle*.tsx` (modes frigo/PAC/moteur, unités K/°C/°F, énergie kJ vs puissance W) ; **conversion markdown → texte enrichi** : un **seul** convertisseur `lib/markdown-to-html.ts` (`markdownToHtml`, gfm+breaks) + une **action atomique** `convertToRichText(html)` dans `note-editor-context.tsx` (applique le HTML **immédiatement** via `setContentImmediate` — pas le `setContent` débouncé 800ms — et bascule `isMarkdown=false`) ; toolbar (`handleConvertToRichtext`) **et** chat IA contextuel (`contextual-ai-chat.tsx`, action `toRichText` → `/api/ai/convert-markdown`) passent par cette action unique (ne pas dupliquer la conversion ni oublier de basculer `isMarkdown`) ; **US-4 `structuredViewBlock`** (`tiptap-structured-view-block-extension.tsx`, `structured-view-block-embed.tsx`) : **dual-mode** — base locale autonome par défaut (`/database`, `/vue`, `isLocal: true`) + option « Lier à un carnet » (Structured Views) ; i18n `structuredViewBlock.*` ; **rejeté** : ancien `databaseBlock` « Auteurs & Œuvres » et spec embed-only `docs/story-nextgen-editor-us4-redesign.md` ; 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`) ; tracking quotas IA : événement `ai-usage-changed` (window) — doit être dispatché depuis **tous** les points d'appel IA (recherche sémantique, auto-tag, auto-title, chat, reformulation, analyse carnet, etc.) ; UI quotas : `usage-meter.tsx` (polling 5s + écoute événement). -- 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) ; **Éditeur Next-Gen livré** (doc `docs/story-nextgen-editor.md` **périmé** — vérifier le code) : US-1 poignée gutter unique (`editor-block-drag-handle.tsx` + `lib/editor/global-drag-handle-extension.ts`, extension Novel `tiptap-extension-global-drag-handle`), US-2 menu action bloc (`block-action-menu.tsx`), US-3 Smart Paste transclusion (`smart-paste-extension.ts` + `smart-paste-menu.tsx`), US-4 database inline (`structuredViewBlock`), plus `data-id` / `liveBlock` / peek split ; **`/insights` livré** : `app/(main)/insights/page.tsx` + `network-graph.tsx` (clusters sémantiques, bridge notes ; **≠ `/graph`**) ; **US-TEMPORAL reporté** ; encore en gap : transclusion bidirectionnelle complète, graphe knowledge enrichi (`GraphKnowledgeMap.tsx`) ; publication **Chrome Web Store** : icônes 16/48/128, privacy policy, `host_permissions` prod restreints vs build dev ; **Wizards** : `NotebookOrganizerDialog` (tags/doublons) branché via bouton "Tags IA" dans `home-client.tsx` — `StructuredViewsWizard` encore **orphelin** (pas de point d'entrée UI) ; **Publication web** (`note-editor-toolbar.tsx`) : deux modes — simple (copie directe) + IA (2-3 templates **visuellement distincts**, reformulation/mise en page **adaptée au contenu** — exercices, toggles, callouts — **images incluses**, KaTeX pour équations) — quota IA consommé uniquement sur publication IA ; l'utilisateur rejette les rendus « copie du texte » / mise en page nulle ; **champs DB publication** sur modèle `Note` : `publicSlug` (pas `publishedSlug`), `publishedContent` (pas `publishedHtml`) — `publishMode` est paramètre API uniquement (`mode: 'simple' | 'ai'`), **pas** un champ en base ; **Second Brain dashboard** (`/home`, `dashboard-view.tsx`) **livré** : layout v7 configurable (~22 widgets, `/api/dashboard/layout`) — bumper `DASHBOARD_LAYOUT_VERSION` (`lib/dashboard/layout.ts`) réécrit les layouts persistés de tous les utilisateurs ; route `isDashboardHomeRoute` (`lib/dashboard/home-route.ts`) — liste notes via `/home?forceList=1` ; briefing agrégé (`/api/briefing`), pistes fast+enrich (`/api/briefing/paths`, `paths-fast.ts`), suggestions agents, scan Gmail (`GmailScanHistory`), navigation sidebar `/home` ; bento interactif — `MindMapCard` (clusters), suggestions agents (`AgentSuggestion`, cron), Memory Echo, sentiment, inbox/révisions ; **Gmail OAuth** (`/api/integrations/gmail/*`, pattern Calendar) — scan vols/colis/abonnements → notes + rappels ; crons entrypoint : agents, agent-suggestions, clusters, reminders, gmail-scan, sync-usage ; **vidéos promo** : pipeline `promo-video/` (`make_promo.py`, `make_promo_features.py`, `make_promo_slash.py`, `make_promo_slash_kinetic.py`, `SCENARIO.md`) — captures app authentifiée (Playwright + session JWT), VO EN (edge-tts), montage stable sans zoom (crossfades si kinetic). +- 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) ; **Éditeur Next-Gen livré** (doc `docs/story-nextgen-editor.md` **périmé** — vérifier le code) : US-1 poignée gutter unique (`editor-block-drag-handle.tsx` + `lib/editor/global-drag-handle-extension.ts`, extension Novel `tiptap-extension-global-drag-handle`), US-2 menu action bloc (`block-action-menu.tsx`), US-3 Smart Paste transclusion (`smart-paste-extension.ts` + `smart-paste-menu.tsx`), US-4 database inline (`structuredViewBlock`), plus `data-id` / `liveBlock` / peek split ; **`/insights` livré** : `app/(main)/insights/page.tsx` + `network-graph.tsx` (clusters sémantiques, bridge notes) ; **`/graph` retirée — ne pas restaurer** ; **US-TEMPORAL reporté** ; encore en gap : transclusion bidirectionnelle complète, graphe knowledge enrichi (`GraphKnowledgeMap.tsx`) ; publication **Chrome Web Store** : icônes 16/48/128, privacy policy, `host_permissions` prod restreints vs build dev ; **Wizards** : `NotebookOrganizerDialog` (tags/doublons) branché via bouton "Tags IA" dans `home-client.tsx` — `StructuredViewsWizard` encore **orphelin** (pas de point d'entrée UI) ; **Publication web** (`note-editor-toolbar.tsx`) : deux modes — simple (copie directe) + IA (2-3 templates **visuellement distincts**, reformulation/mise en page **adaptée au contenu** — exercices, toggles, callouts — **images incluses**, KaTeX pour équations) — quota IA consommé uniquement sur publication IA ; l'utilisateur rejette les rendus « copie du texte » / mise en page nulle ; **champs DB publication** sur modèle `Note` : `publicSlug` (pas `publishedSlug`), `publishedContent` (pas `publishedHtml`) — `publishMode` est paramètre API uniquement (`mode: 'simple' | 'ai'`), **pas** un champ en base ; **Second Brain dashboard** (`/home`, `dashboard-view.tsx`) **livré** : layout v7 configurable (~22 widgets, `/api/dashboard/layout`) — bumper `DASHBOARD_LAYOUT_VERSION` (`lib/dashboard/layout.ts`) réécrit les layouts persistés de tous les utilisateurs ; route `isDashboardHomeRoute` (`lib/dashboard/home-route.ts`) — liste notes via `/home?forceList=1` ; briefing agrégé (`/api/briefing`), pistes fast+enrich (`/api/briefing/paths`, `paths-fast.ts`), suggestions agents, scan Gmail (`GmailScanHistory`), navigation sidebar `/home` ; bento interactif — `MindMapCard` (clusters), suggestions agents (`AgentSuggestion`, cron), Memory Echo, sentiment, inbox/révisions ; **Gmail OAuth** (`/api/integrations/gmail/*`, pattern Calendar) — scan vols/colis/abonnements → notes + rappels ; crons entrypoint : agents, agent-suggestions, clusters, reminders, gmail-scan, sync-usage ; **vidéos promo** : pipeline `promo-video/` (`make_promo.py`, `make_promo_features.py`, `make_promo_slash.py`, `make_promo_slash_kinetic.py`, `SCENARIO.md`) — captures app authentifiée (Playwright + session JWT), VO EN (edge-tts), montage stable sans zoom (crossfades si kinetic). diff --git a/memento-note/app/actions/notes-trash.ts b/memento-note/app/actions/notes-trash.ts index 281699ae..7d4a082c 100644 --- a/memento-note/app/actions/notes-trash.ts +++ b/memento-note/app/actions/notes-trash.ts @@ -64,6 +64,57 @@ export async function restoreNote(id: string) { } } +const MAX_BULK_NOTES = 200 + +function uniqueNoteIds(ids: string[]) { + return [...new Set(ids.filter((id) => typeof id === 'string' && id.length > 0))].slice(0, MAX_BULK_NOTES) +} + +export async function bulkTrashNotes(ids: string[], options?: { skipRevalidation?: boolean }) { + const session = await auth() + if (!session?.user?.id) throw new Error('Unauthorized') + + const unique = uniqueNoteIds(ids) + if (unique.length === 0) return { success: true, count: 0 } + + try { + const result = await prisma.note.updateMany({ + where: { id: { in: unique }, userId: session.user.id, trashedAt: null }, + data: { trashedAt: new Date() }, + }) + if (!options?.skipRevalidation) { + revalidatePath('/home') + } + return { success: true, count: result.count } + } catch (error) { + console.error('Error bulk-trashing notes:', error) + throw new Error('Failed to trash notes') + } +} + +export async function restoreNotes(ids: string[], options?: { skipRevalidation?: boolean }) { + const session = await auth() + if (!session?.user?.id) throw new Error('Unauthorized') + + const unique = uniqueNoteIds(ids) + if (unique.length === 0) return { success: true, count: 0 } + + try { + const result = await prisma.note.updateMany({ + where: { id: { in: unique }, userId: session.user.id, trashedAt: { not: null } }, + data: { trashedAt: null }, + }) + if (!options?.skipRevalidation) { + revalidatePath('/home') + revalidatePath('/trash') + } + return { success: true, count: result.count } + } catch (error) { + console.error('Error restoring notes:', error) + throw new Error('Failed to restore notes') + } +} + export async function getTrashedNotes() { const session = await auth() if (!session?.user?.id) return [] diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index 5e2d7434..ac59e004 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -721,6 +721,41 @@ export async function updateNote(id: string, data: { } } +const MAX_BULK_MOVE = 200 + +export async function bulkMoveNotes( + ids: string[], + notebookId: string, + options?: { skipRevalidation?: boolean }, +) { + const session = await auth() + if (!session?.user?.id) throw new Error('Unauthorized') + + const unique = [...new Set(ids.filter((id) => typeof id === 'string' && id.length > 0))].slice(0, MAX_BULK_MOVE) + if (unique.length === 0) return { success: true, count: 0 } + + const notebook = await prisma.notebook.findFirst({ + where: { id: notebookId, userId: session.user.id, trashedAt: null }, + select: { id: true }, + }) + if (!notebook) throw new Error('Notebook not found') + + try { + const result = await prisma.note.updateMany({ + where: { id: { in: unique }, userId: session.user.id, trashedAt: null }, + data: { notebookId }, + }) + if (!options?.skipRevalidation) { + revalidatePath('/home') + revalidatePath(`/notebook/${notebookId}`) + } + return { success: true, count: result.count } + } catch (error) { + console.error('Error bulk-moving notes:', error) + throw new Error('Failed to move notes') + } +} + // Toggle functions export async function togglePin( id: string, @@ -1169,6 +1204,8 @@ import { deleteNote as _deleteNote, trashNote as _trashNote, restoreNote as _restoreNote, + bulkTrashNotes as _bulkTrashNotes, + restoreNotes as _restoreNotes, getTrashedNotes as _getTrashedNotes, permanentDeleteNote as _permanentDeleteNote, emptyTrash as _emptyTrash, @@ -1202,6 +1239,8 @@ export async function enableNoteHistory(...args: Parameters) { return _deleteNote(...args) } export async function trashNote(...args: Parameters) { return _trashNote(...args) } export async function restoreNote(...args: Parameters) { return _restoreNote(...args) } +export async function bulkTrashNotes(...args: Parameters) { return _bulkTrashNotes(...args) } +export async function restoreNotes(...args: Parameters) { return _restoreNotes(...args) } export async function getTrashedNotes(...args: Parameters) { return _getTrashedNotes(...args) } export async function permanentDeleteNote(...args: Parameters) { return _permanentDeleteNote(...args) } export async function emptyTrash(...args: Parameters) { return _emptyTrash(...args) } diff --git a/memento-note/components/confirm-delete-note-dialog.tsx b/memento-note/components/confirm-delete-note-dialog.tsx index 53ef7213..cbf2ff64 100644 --- a/memento-note/components/confirm-delete-note-dialog.tsx +++ b/memento-note/components/confirm-delete-note-dialog.tsx @@ -16,10 +16,14 @@ export function ConfirmDeleteNoteDialog({ open, onOpenChange, onConfirm, + title, + description, }: { open: boolean onOpenChange: (open: boolean) => void onConfirm: () => void | Promise + title?: string + description?: string }) { const { t } = useLanguage() @@ -27,8 +31,8 @@ export function ConfirmDeleteNoteDialog({ - {t('notes.confirmDeleteTitle')} - {t('notes.confirmDelete')} + {title ?? t('notes.confirmDeleteTitle')} + {description ?? t('notes.confirmDelete')} {t('common.cancel')} diff --git a/memento-note/components/dashboard-mind-orbit.tsx b/memento-note/components/dashboard-mind-orbit.tsx index 39cf32f3..85773e1e 100644 --- a/memento-note/components/dashboard-mind-orbit.tsx +++ b/memento-note/components/dashboard-mind-orbit.tsx @@ -18,9 +18,18 @@ interface BridgeNote { note?: { id: string; title: string | null } } -const CLUSTER_COLORS = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF'] +const CLUSTER_COLORS = ['#B91C1C', '#1D4ED8', '#047857', '#B45309', '#6D28D9', '#BE185D', '#0F766E'] const EASE = [0.16, 1, 0.3, 1] as const +function clusterInk(hex: string): string { + const n = hex.replace('#', '') + const r = parseInt(n.slice(0, 2), 16) + const g = parseInt(n.slice(2, 4), 16) + const b = parseInt(n.slice(4, 6), 16) + const y = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 + return y > 0.45 ? '#1C1917' : '#FAFAF9' +} + function clusterPoint(index: number, count: number): { x: number; y: number } { if (count === 1) return { x: 50, y: 42 } const angle = -Math.PI / 2 + (index * 2 * Math.PI) / count @@ -69,8 +78,8 @@ export function DashboardMindOrbit({ className="w-full h-[180px] rounded-2xl border border-dashed border-border/35 bg-white/50 dark:bg-zinc-900/50 flex flex-col items-center justify-center gap-2 p-6 text-center hover:border-brand-accent/30 transition-all" > -

{t('homeDashboard.mindMapEmpty')}

- {t('homeDashboard.mindMapOpen')} +

{t('homeDashboard.mindMapEmpty')}

+ {t('homeDashboard.mindMapOpen')} ) } @@ -85,7 +94,7 @@ export function DashboardMindOrbit({ + + {selectedCount === 1 + ? t('notes.selectedCountOne') + : t('notes.selectedCount', { count: selectedCount })} + +
+ {hasSelection ? ( + { + if (notebookId) onMove(notebookId) + }} + > + + + ) : ( + + )} + +
+ + ) +} diff --git a/memento-note/components/move-to-notebook-picker.tsx b/memento-note/components/move-to-notebook-picker.tsx index 45c84e89..ae4964f8 100644 --- a/memento-note/components/move-to-notebook-picker.tsx +++ b/memento-note/components/move-to-notebook-picker.tsx @@ -17,6 +17,7 @@ type MoveToNotebookPickerProps = { children: React.ReactElement align?: 'start' | 'end' preferDropUp?: boolean + showGeneralNotes?: boolean } export function MoveToNotebookPicker({ @@ -26,6 +27,7 @@ export function MoveToNotebookPicker({ children, align = 'end', preferDropUp = false, + showGeneralNotes = true, }: MoveToNotebookPickerProps) { const { t } = useLanguage() const [open, setOpen] = useState(false) @@ -91,7 +93,7 @@ export function MoveToNotebookPicker({ selectedId={currentNotebookId} onSelect={(id) => handleSelect(id)} onClose={close} - showGeneralNotes + showGeneralNotes={showGeneralNotes} generalNotesLabel={t('notebookSuggestion.generalNotes') || 'Notes générales'} onSelectGeneralNotes={() => handleSelect(null)} searchPlaceholder={t('notebookSuggestion.filterNotebooks') || 'Filtrer les carnets…'} diff --git a/memento-note/components/note-select-checkbox.tsx b/memento-note/components/note-select-checkbox.tsx new file mode 100644 index 00000000..4d57097d --- /dev/null +++ b/memento-note/components/note-select-checkbox.tsx @@ -0,0 +1,39 @@ +'use client' + +import { Check } from 'lucide-react' +import { cn } from '@/lib/utils' + +export function NoteSelectCheckbox({ + checked, + onToggle, + label, + className, +}: { + checked: boolean + onToggle: () => void + label: string + className?: string +}) { + return ( + + ) +} diff --git a/memento-note/components/notes-editorial-view.tsx b/memento-note/components/notes-editorial-view.tsx index e5ca5f41..854b3090 100644 --- a/memento-note/components/notes-editorial-view.tsx +++ b/memento-note/components/notes-editorial-view.tsx @@ -25,6 +25,7 @@ import { useNotebooks } from '@/context/notebooks-context' import { toast } from 'sonner' import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog' import { showNoteTrashedToast } from '@/lib/notes/trash-toast' +import { NoteSelectCheckbox } from '@/components/note-select-checkbox' import { fr } from 'date-fns/locale/fr' import { enUS } from 'date-fns/locale/en-US' import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date' @@ -37,6 +38,10 @@ type NotesEditorialViewProps = { onOpen: (note: Note, readOnly?: boolean) => void notebookName?: string onOpenHistory?: (note: Note) => void + selection?: { + selectedIds: ReadonlySet + onToggle: (id: string) => void + } } & NoteCollectionActions function formatNoteDate(date: Date | string, language: string): string { @@ -376,6 +381,7 @@ export function NotesEditorialView({ onMoveToNotebook, onNotePatch, onNoteIllustrationGenerated, + selection, }: NotesEditorialViewProps) { const { t, language } = useLanguage() const { data: session } = useSession() @@ -454,7 +460,14 @@ export function NotesEditorialView({

- + + {selection && ( + selection.onToggle(note.id)} + label={t('notes.selectNote')} + /> + )} {note.isPinned && } {note.historyEnabled && } {title} diff --git a/memento-note/components/notes-list-views.tsx b/memento-note/components/notes-list-views.tsx index eccf03fc..2013621e 100644 --- a/memento-note/components/notes-list-views.tsx +++ b/memento-note/components/notes-list-views.tsx @@ -53,6 +53,7 @@ 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' +import { NoteSelectCheckbox } from '@/components/note-select-checkbox' export type NotesLayoutMode = 'grid' | 'list' | 'table' | 'kanban' | 'gallery' export type NotesClassicLayoutMode = 'grid' | 'list' | 'table' @@ -253,6 +254,12 @@ type NotesListViewsProps = { onOpenHistory?: (note: Note) => void notebookName?: string onGridReorder?: (orderedIds: string[]) => void | Promise + selection?: { + selectedIds: ReadonlySet + onToggle: (id: string) => void + onToggleAll?: () => void + allSelected?: boolean + } } & Partial export function NotesListViews({ @@ -269,6 +276,7 @@ export function NotesListViews({ onNotePatch, onNoteIllustrationGenerated, onGridReorder, + selection, }: NotesListViewsProps) { const { t, language } = useLanguage() const { data: session } = useSession() @@ -352,6 +360,7 @@ export function NotesListViews({ onOpenHistory={onOpenHistory} onGridReorder={onGridReorder} pinnedLabel={t('notes.pinned')} + selection={selection} /> ) } @@ -363,6 +372,15 @@ export function NotesListViews({ + {selection && ( + + )} + )}
+ selection.onToggleAll?.()} + label={selection.allSelected ? t('notes.deselectAll') : t('notes.selectAll')} + /> + handleSort('title')} @@ -407,6 +425,15 @@ export function NotesListViews({ role="button" className="h-11 hover:bg-foreground/[0.02] cursor-pointer transition-colors group focus-visible:bg-foreground/[0.04]" > + {selection && ( + + selection.onToggle(note.id)} + label={t('notes.selectNote')} + /> + {note.isPinned && } @@ -464,6 +491,7 @@ export function NotesListViews({ onMoveToNotebook={onMoveToNotebook} onNotePatch={onNotePatch} onNoteIllustrationGenerated={onNoteIllustrationGenerated} + selection={selection} /> )} @@ -479,6 +507,7 @@ export function NotesListViews({ onMoveToNotebook={onMoveToNotebook} onNotePatch={onNotePatch} onNoteIllustrationGenerated={onNoteIllustrationGenerated} + selection={selection} /> )} @@ -497,6 +526,13 @@ function formatGridCardDate(date: Date | string, language: string): string { return `${month.toUpperCase()} ${day}, ${year}` } +type NotesSelection = { + selectedIds: ReadonlySet + onToggle: (id: string) => void + onToggleAll?: () => void + allSelected?: boolean +} + type GridCardSharedProps = { note: Note index: number @@ -512,6 +548,7 @@ type GridCardSharedProps = { onNoteIllustrationDeleted?: (noteId: string) => void | Promise onOpenHistory?: (note: Note) => void isOverlay?: boolean + selection?: NotesSelection } function NotesMasonryGrid({ @@ -624,6 +661,7 @@ function NotesGridSection({ onDeleteNote, onMoveToNotebook, onNoteIllustrationGenerated, + selection, className, }: Omit & { notes: Note[] @@ -650,6 +688,7 @@ function NotesGridSection({ onDeleteNote={onDeleteNote} onMoveToNotebook={onMoveToNotebook} onNoteIllustrationGenerated={onNoteIllustrationGenerated} + selection={selection} /> ) : ( ), )} @@ -721,6 +761,7 @@ const GridCard = memo(function GridCard({ onNoteIllustrationDeleted, onOpenHistory, isOverlay = false, + selection, }: GridCardSharedProps) { const router = useRouter() const { t, language } = useLanguage() @@ -760,8 +801,21 @@ const GridCard = memo(function GridCard({ onNoteIllustrationGenerated={onNoteIllustrationGenerated} onNoteIllustrationDeleted={onNoteIllustrationDeleted} /> + {selection && !isOverlay && ( +
+ selection.onToggle(note.id)} + label={t('notes.selectNote')} + className="shadow-sm" + /> +
+ )} {note.isPinned && ( -
+
)} diff --git a/memento-note/components/public-site-chrome.tsx b/memento-note/components/public-site-chrome.tsx index 07970f6f..4024dc8a 100644 --- a/memento-note/components/public-site-chrome.tsx +++ b/memento-note/components/public-site-chrome.tsx @@ -229,15 +229,15 @@ export function PublicSiteChrome({
Memento -

{t('landing.footer.desc')}

+

{t('landing.footer.desc')}

{(['product', 'community', 'legal'] as const).map((section) => (
-

+

{t(`landing.footer.${section}.title`)}

-
    +
      {[0, 1, 2].map((j) => { const label = t(`landing.footer.${section}.link${j}`) const href = t(`landing.footer.${section}.link${j}Href`) @@ -258,7 +258,7 @@ export function PublicSiteChrome({ ))}
-

+

© 2026 Memento. {t('landing.footer.rights')}

diff --git a/memento-note/lib/notes/trash-toast.ts b/memento-note/lib/notes/trash-toast.ts index 4839dccf..724f19e5 100644 --- a/memento-note/lib/notes/trash-toast.ts +++ b/memento-note/lib/notes/trash-toast.ts @@ -1,7 +1,7 @@ 'use client' import { toast } from 'sonner' -import { restoreNote } from '@/app/actions/notes' +import { restoreNote, restoreNotes } from '@/app/actions/notes' import { emitNoteChange } from '@/lib/note-change-sync' import type { Note } from '@/lib/types' @@ -27,3 +27,31 @@ export function showNoteTrashedToast( }, }) } + +export function showNotesTrashedToast( + notes: Note[], + t: (key: string, params?: Record) => string, + onRestored?: () => void, +) { + if (notes.length === 1) { + showNoteTrashedToast(notes[0], t, onRestored) + return + } + toast.success(t('notes.bulkTrashedToast', { count: notes.length }), { + action: { + label: t('notes.undoDelete'), + onClick: async () => { + try { + await restoreNotes(notes.map((note) => note.id), { skipRevalidation: true }) + for (const note of notes) { + emitNoteChange({ type: 'created', note: { ...note, trashedAt: null } }) + } + onRestored?.() + toast.success(t('trash.noteRestored')) + } catch { + toast.error(t('general.error')) + } + }, + }, + }) +} diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index 83ae1a5d..ff4b56b3 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -114,6 +114,17 @@ "title": "Notes", "newNote": "New note", "reorganize": "Reorganize notes", + "selectAll": "Select all", + "deselectAll": "Deselect all", + "selectedCount": "{count} notes selected", + "selectedCountOne": "1 note selected", + "bulkMove": "Move", + "bulkTrash": "Trash", + "selectNote": "Select this note", + "confirmBulkDeleteTitle": "Send to trash", + "confirmBulkDelete": "These notes will go to the trash. You can restore them later.", + "bulkTrashedToast": "{count} notes sent to trash.", + "bulkMovedToast": "{count} notes moved.", "untitled": "Untitled", "placeholder": "Take a note...", "markdownPlaceholder": "Take a note... (Markdown supported)", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index 735ddc8f..f53847bb 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -114,6 +114,17 @@ "title": "Notes", "newNote": "Nouvelle note", "reorganize": "Réorganiser les notes", + "selectAll": "Tout sélectionner", + "deselectAll": "Tout désélectionner", + "selectedCount": "{count} notes sélectionnées", + "selectedCountOne": "1 note sélectionnée", + "bulkMove": "Déplacer", + "bulkTrash": "Corbeille", + "selectNote": "Sélectionner cette note", + "confirmBulkDeleteTitle": "Envoyer à la corbeille", + "confirmBulkDelete": "Ces notes iront dans la corbeille. Vous pourrez les récupérer ensuite.", + "bulkTrashedToast": "{count} notes envoyées à la corbeille.", + "bulkMovedToast": "{count} notes déplacées.", "untitled": "Sans titre", "placeholder": "Prenez une note...", "markdownPlaceholder": "Prenez une note... (Markdown supporté)",