- sidebar-resize.tsx: hook useSidebarResize (localStorage persistence, 280-560px)
- sidebar-resize-client.tsx: wrapper layout qui applique la largeur
- layout.tsx: remplace div statique par SidebarResizeClient
- Resize handle: barre 1px→3px au hover, brand-accent pendant drag
- pointer events (pas mouse) pour support tactile
- cursor: col-resize + userSelect:none pendant le drag
L'utilisateur peut maintenant réduire ou élargir la zone de carnet
en glissant la barre entre le sidebar et le contenu principal.
Quand une idée est déjà convertie en note:
- Badge 'Note Created →' cliquable dans le header de l'idée (ouvre le peek)
- Bouton 'Open Note →' remplace 'Create Note' (bordure emerald, fond vert)
- peek.open(convertedToNoteId) ouvre le drawer latéral avec la note
- L'utilisateur peut relire la note sans quitter le brainstorm
Vrai fix UX: quand l'utilisateur clique 'Create Note' dans le canvas brainstorm,
il voit maintenant un dialog (ConvertNotebookDialog) qui demande dans
quel carnet sauvegarder la note — au lieu d'aller silencieusement dans
le carnet source ou un carnet auto-créé.
- convert-notebook-dialog.tsx (162 lignes): dialog de sélection carnet
- design system respecté: emerald, memento-paper, font-memento-serif
- preview de l'idée en card
- dropdown carnet avec animation d'expansion
- bouton 'Create note' avec loader
- role=dialog, aria-modal, aria-label
- brainstorm-page.tsx: convertConfirm state + bouton 'Create Note' ouvre le dialog
- 3 nouvelles clés i18n (EN/FR): convertTitle, convertSubtitle, ideaLabel
- brainstorm-page.tsx: lit urlNotebookId depuis URL params
- useConvertIdea accepte targetNotebookId (hook mis à jour)
- handleConvert prend le carnet ciblé en paramètre
- URL notebookId propage via router.push depuis le modal de confirmation
- toast inclut le nom du carnet et un bouton 'Open notebook' qui redirige vers /home?notebook=X
- peek panel affiche un lien vers le carnet source en haut du titre
- useNotebooks pour le lookup du nom depuis notebooks
Vrai fix: les notes vont maintenant là où l'utilisateur a choisi,
pas dans un carnet auto-créé sans qu'il s'en aperçoive.
Mobile fixes (CRITIQUE):
- theme.ts: C.surface + emerald/blue/amber/purple ajoutés (crash Révision fix)
- note/[id].tsx: édition préserve le HTML (titre seulement, contenu read-only)
+ message explicatif 'utilisez la version web'
- store.ts: logout appelle POST /api/mobile/auth/logout avant clearToken
Mobile auth (SÉCURITÉ):
- Nouvelle route /api/mobile/auth/logout — blacklist Redis du token (TTL 90j)
- verifyMobileToken devient async + check Redis blacklist
- getMobileUserId devient async
Note: Publication IA mode + templates déjà dans note-editor-toolbar.tsx (pas manquant)
Note: Structured Views Wizard déjà branché via StructuredViewsIntro (pas orphelin)
Vrai problème (hauteur, pas largeur) : le conteneur scrollable externe
était flex-1 + overflow-y-auto mais PAS flex flex-col. Du coup le
flex-1 de la vue carnets ne s'appliquait pas → la liste prenait la
hauteur de son contenu et s'arrêtait au milieu (trait flottant + vide
en dessous) quand il y avait peu de carnets.
Fix : ajoute `flex flex-col min-h-0` au conteneur externe pour que la
chaîne flex fonctionne — la liste des carnets remplit désormais toute
la hauteur jusqu'au pied de page (Pack Pro), quel que soit le nombre
de carnets.
Co-authored-by: Cursor <cursoragent@cursor.com>
L'écart précédent (320->352, +32px) était imperceptible. La sidebar
étant ancrée dès md (768px), on applique une largeur franche à ce
breakpoint pour un résultat indiscutable :
- < 768px (mobile overlay) : w-80 (320px)
- >= 768px : w-[26rem] (416px) [+96px]
- >= 1536px : w-[30rem] (480px)
Co-authored-by: Cursor <cursoragent@cursor.com>
La sidebar restait à 320px en production car l'élargissement ne se
déclenchait qu'à partir du breakpoint xl (1280px). Abaisse le seuil à
md (768px) pour couvrir tous les écrans desktop en production.
- < 768px (mobile, overlay) : w-80 (320px)
- >= 768px : w-[22rem] (352px)
- >= 1536px : w-[26rem] (416px)
Le fallback Suspense du layout est aligné pour éviter tout décalage.
Co-authored-by: Cursor <cursoragent@cursor.com>
h-full dépendait du parent pour la hauteur — ne cascadaît pas en prod.
h-screen (100vh) + self-stretch force la hauteur pleine indépendamment.
Suspense fallback aussi mis à h-screen pour éviter le flash.
Global (globals.css):
- prefers-reduced-motion: désactive toutes les animations/transitions
- focus-visible:ring global sur tous les éléments interactifs
Search modal:
- role="dialog" + aria-modal="true" + aria-label
- onClick backdrop ferme la modale
Insights peek:
- DOMPurify.sanitize() sur dangerouslySetInnerHTML (XSS fix)
Login form:
- autoComplete="email" / "current-password"
- htmlFor sur tous les labels
Register form:
- autoComplete="name" / "email" / "new-password"
- htmlFor sur tous les labels
GridCard (notes-list-views):
- Actions visibles sur mobile (opacity-100 sm:opacity-0)
- aria-sort sur colonne triable
- role="button" + tabIndex + onKeyDown sur lignes table
Editorial view:
- role="button" + tabIndex + onKeyDown sur articles
- Menu trigger aria-label + visible mobile
Toolbar:
- aria-label voice i18n
Root cause: le mode grille utilise GridCard dans notes-list-views.tsx,
PAS NoteCard de note-card.tsx. Toutes les fixes précédentes étaient
sur le mauvais composant.
Fix:
- onOpenHistory ajouté à GridCardSharedProps
- Badge History en haut-droite si historyEnabled=true
- Bouton History dans la barre d'actions (vert si activé, gris sinon)
- Prop threadé: NotesListViews → NotesMasonryGrid → GridCard
Avant: History caché dans le menu '...' (MoreVertical) — invisible
Maintenant: bouton History standalone comme Pin/Reminder/Color
- Vert emerald si versioning activé
- Gris discret si versioning désactivé
- Cliquer ouvre l'historique ou l'activation
Root cause: home-client.tsx n'écoutait PAS NOTE_CHANGE_EVENT.
Quand versioning était activé depuis l'éditeur, emitNoteChange était dispatché
mais personne ne mettait à jour la liste des notes → historyEnabled restait false
sur les cartes/liste.
Fix: useEffect qui écoute NOTE_CHANGE_EVENT et patch les notes + pinnedNotes
avec les champs mis à jour (historyEnabled, etc.).
Card view:
- Badge plus visible: bg-emerald-500/15 + border + backdrop-blur
- Position top-2.5 end-2.5 (top-right corner, bien visible)
Table/list view (notes-list-views.tsx):
- Icône History 11px text-emerald-500 à côté du titre (comme Pin)
- Present sur toutes les lignes du tableau
Editorial view (notes-editorial-view.tsx):
- Icône History 14px text-emerald-500 à côté du titre + Pin
- Visible sans hover (contrairement au menu dropdown)
Sidebar (UX: User Freedom):
- Visible par défaut sur /insights (pas caché par le système)
- Toggle via bouton hamburger: toggle-insights-sidebar event
- Préférence persistée en localStorage (insights-sidebar-collapsed)
- User décide: voir ou cacher le sidebar
Cluster chips:
- Triées par taille (plus grand cluster en premier)
- Compteur de notes par cluster (badge arrondi)
- max-w-[120px] sur le nom pour éviter overflow
- padding/gap plus compacts (px-2.5 py-1 gap-1.5)
AGENTS.md: 'pas la liste carnets sur /insights'
- sidebar.tsx: isImmersiveRoute sur /insights → fixed overlay au lieu de relative
- Toutes tailles d'écran: sidebar caché par défaut, s'ouvre en drawer
- Bouton hamburger visible sur desktop + mobile (pas lg:hidden)
- Insights prend 100% de la largeur → graphe + dashboard plus spacious
- handleNoteClick fetch la note + ouvre peek panel au lieu de naviguer
- Panel fixed right (left en RTL), spring animation (stiffness 340 damping 34)
- Header avec titre + boutons Maximize2 (ouvrir note complète) et X (fermer)
- Content rendu en read-only via dangerouslySetInnerHTML
- prefers-reduced-motion: animation désactivée
- responsive: w-full sur mobile, 50vw/640px max sur desktop
enableNoteHistory() mettait à jour la DB mais ne notifiait pas la home page.
L'éditeur fetch la note à jour (historyEnabled=true) → icône visible.
La home gardait les données en cache (historyEnabled=false) → pas d'icône.
Fix: emitNoteChange({ type: 'updated', note: { ...note, historyEnabled: true } })
dispatché après les 2 points d'appel dans note-document-info-panel.tsx.
L'icône était à bottom-3 end-3, masquée par la barre d'actions (bottom-0 full-width).
Déplacée en top-3 end-3 avec badge vert (bg-emerald-500/10 + text-emerald-600)
pour être visible d'un coup d'œil.
Note card:
- Icône History en overlay bottom-right (visible si historyEnabled=true)
- Discrète: text-muted-foreground/50, ne pollue pas l'UI
- Tooltip 'Historique des versions activé'
Toolbar éditeur:
- Icône History à côté du statut Saved/Dirty
- Visible seulement en sm+ (desktop)
- cursor-help + tooltip
i18n:
- notes.historyEnabledTooltip ajouté aux 15 locales (FR/EN traduits, 13 EN placeholder)
- zoomRef stocke le behavior d3.zoom pour accès externe au useEffect
- handleFitView: d3.zoomIdentity reset (600ms transition) + clear selectedClusterId
- Bouton Maximize2 en haut à droite du graphe avec aria-label
- cursor-pointer + focus-visible:ring pour a11y
Accessibility (CRITIQUE per UI/UX Pro Max skill):
- NetworkGraph Accessibility Grade D → added accessible List view alternative
(toggle Graph/List with cluster→notes table, keyboard navigable)
- aria-label text summary on graph container for screen readers
- role=button + tabIndex + onKeyDown on bridge note cards (keyboard accessible)
- focus-visible:ring on all interactive cards (isolated clusters, bridges, list items)
UX (HIGH):
- prefers-reduced-motion: whileHover disabled when user prefers reduced motion
- cursor-pointer verified + focus-visible:ring-ochre on all clickable cards
- Mobile sidebar: hamburger Menu button in header (dispatches open-mobile-sidebar)
Performance (MEDIUM):
- NetworkGraph lazy-loaded via next/dynamic (D3 ~200KB deferred, ssr:false)
- Loading spinner shown while D3 chunk loads
i18n:
- listView, graphAriaLabel, listAriaLabel added to 15 locales
Quand la similarité whole-note ne passe pas le seuil, vérifie les chunks.
Si une section spécifique de la note A résonne avec une section de la note B,
la connexion est créée avec le snippet précis qui a matché.
SQL: cross-join LATERAL sur NoteEmbeddingChunk avec pgvector <=>.
Fallback gracieux si la table chunks est vide ou erreur.
1. Recherche: fetchChunkSnippets() — après le classement RRF existant,
récupère les passages précis qui matchent depuis NoteEmbeddingChunk.
Pur affichage, AUCUN changement de classement.
2. Script migration: scripts/migrate-chunk-embeddings.ts
Indexe toutes les notes existantes en fragments.
Batch de 10, barre de progression.
Usage: npx tsx scripts/migrate-chunk-embeddings.ts
3. Memory Echo chunk-level: à faire (US restante)