# Plan Technique - Intégration Novel Editor ## Vue d'ensemble Intégration d'un éditeur riche type Notion basé sur **Novel.sh** (Tiptap) pour remplacer le textarea actuel en mode desktop, tout en conservant un textarea amélioré pour le mobile. --- ## 1. Architecture Générale ### 1.1 Structure des fichiers ``` keep-notes/ ├── app/ │ └── api/ │ └── ai/ │ └── editor/ # Endpoints AI pour l'éditeur │ ├── improve/route.ts │ └── shorten/route.ts ├── components/ │ ├── editor/ │ │ ├── novel-editor.tsx # Composant Novel principal │ │ ├── mobile-textarea.tsx # Fallback textarea mobile │ │ ├── editor-container.tsx # Switch desktop/mobile │ │ ├── slash-commands.tsx # Commandes slash (/heading, /list) │ │ ├── ai-commands.tsx # Commandes AI (/ai improve) │ │ ├── editor-toolbar.tsx # Toolbar sticky contextuelle │ │ └── markdown-preview.tsx # Preview mode carte │ ├── note-inline-editor.tsx # MODIFIÉ - Intègre EditorContainer │ └── note-card.tsx # Inchangé - Affiche aperçu texte ├── lib/ │ ├── editor/ │ │ ├── novel-config.ts # Configuration Tiptap/Novel │ │ ├── markdown-converter.ts # MD ↔ JSON conversion │ │ ├── editor-utils.ts # Helpers (extract text, etc.) │ │ └── extensions/ # Extensions custom Tiptap │ │ ├── checklist-extension.ts │ │ └── ai-extension.ts │ └── ai/ │ └── editor-commands.ts # Intégration AI dans l'éditeur ├── hooks/ │ ├── use-novel-editor.ts # Hook gestion état Novel │ ├── use-editor-save.ts # Hook sauvegarde auto │ └── use-device-type.ts # Détection desktop/mobile └── types/ └── editor.types.ts # Types TypeScript pour l'éditeur ``` ### 1.2 Flux de données ``` ┌─────────────────────────────────────────────────────────────┐ │ FLUX DE DONNÉES │ └─────────────────────────────────────────────────────────────┘ [Mode Liste - Édition] │ ▼ ┌─────────────────┐ │ EditorContainer │◄──── Détection mobile/desktop └────────┬────────┘ │ ┌────┴────┐ │ │ ▼ ▼ ┌───────┐ ┌──────────┐ │ Novel │ │ Textarea │ │Editor │ │ (mobile) │ └───┬───┘ └────┬─────┘ │ │ └─────┬─────┘ ▼ ┌─────────────────┐ │ Markdown (JSON) │◄──── Format de stockage └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Sauvegarde │◄──── API /api/notes/[id] │ Auto (1.5s) │ └─────────────────┘ [Mode Carte - Affichage] │ ▼ ┌─────────────────┐ │ getNotePreview │◄──── Extrait texte brut du Markdown │ (existing) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Note Card │◄──── Affiche 2 lignes max │ (unchanged) │ └─────────────────┘ ``` --- ## 2. Dépendances ### 2.1 Core (obligatoires) ```bash # Novel et Tiptap npm install novel npm install @tiptap/core @tiptap/starter-kit # Extensions essentielles npm install @tiptap/extension-task-list npm install @tiptap/extension-task-item npm install @tiptap/extension-placeholder npm install @tiptap/extension-link npm install @tiptap/extension-underline npm install @tiptap/extension-highlight npm install @tiptap/extension-code-block npm install @tiptap/extension-blockquote npm install @tiptap/extension-horizontal-rule # Markdown npm install @tiptap/extension-markdown ``` ### 2.2 UI (shadcn/radix déjà présents) ```bash # Déjà inclus avec shadcn # - @radix-ui/react-popover # - @radix-ui/react-toolbar # - class-variance-authority # - clsx / tailwind-merge ``` ### 2.3 Types ```bash npm install -D @types/tiptap ``` --- ## 3. Composants détaillés ### 3.1 EditorContainer (`components/editor/editor-container.tsx`) **Responsabilité :** Point d'entrée unique, détecte le device et route vers le bon éditeur. ```typescript interface EditorContainerProps { content: string; // Markdown onChange: (markdown: string) => void; placeholder?: string; enableAI?: boolean; // Active les commandes AI readOnly?: boolean; } function EditorContainer(props: EditorContainerProps) { const isMobile = useMediaQuery('(max-width: 768px)'); if (isMobile) { return ; } return ; } ``` ### 3.2 NovelEditor (`components/editor/novel-editor.tsx`) **Responsabilité :** Éditeur riche avec toutes les fonctionnalités. **Props :** ```typescript interface NovelEditorProps { value: string; // Markdown initial onChange: (markdown: string) => void; placeholder?: string; enableAI?: boolean; extensions?: Extension[]; // Extensions Tiptap additionnelles } ``` **Configuration :** ```typescript const defaultExtensions = [ StarterKit.configure({ heading: { levels: [1, 2, 3] }, bulletList: {}, orderedList: {}, codeBlock: {}, blockquote: {}, horizontalRule: {}, }), TaskList, TaskItem.configure({ nested: true, HTMLAttributes: { class: 'flex items-start gap-2' } }), Link.configure({ openOnClick: false, HTMLAttributes: { class: 'text-primary underline' } }), Underline, Highlight.configure({ multicolor: true }), Placeholder.configure({ placeholder: 'Écris / pour voir les commandes...' }), Markdown.configure({ html: false, transformCopiedText: true, }), ]; ``` ### 3.3 MobileTextarea (`components/editor/mobile-textarea.tsx`) **Responsabilité :** Textarea amélioré pour mobile avec toolbar minimal. **Features :** - Toolbar sticky bottom (B, I, List, Checklist) - Markdown shortcuts (## pour H2, - pour liste) - Auto-grow height - Swipe gestures (optionnel) ```typescript interface MobileTextareaProps { value: string; onChange: (value: string) => void; placeholder?: string; } ``` ### 3.4 SlashCommands (`components/editor/slash-commands.tsx`) **Responsabilité :** Menu commandes déclenché par `/`. **Commandes implémentées :** | Commande | Action | Icône | |----------|--------|-------| | `/h1` | Titre niveau 1 | Heading1 | | `/h2` | Titre niveau 2 | Heading2 | | `/h3` | Titre niveau 3 | Heading3 | | `/list` | Liste à puces | List | | `/num` | Liste numérotée | ListOrdered | | `/check` | Checklist | CheckSquare | | `/quote` | Citation | Quote | | `/code` | Bloc de code | Code | | `/line` | Ligne horizontale | Minus | | `/ai` | Commandes AI | Sparkles | ### 3.5 AICommands (`components/editor/ai-commands.tsx`) **Responsabilité :** Intégration AI dans l'éditeur. **Commandes AI :** | Commande | Description | Raccourci | |----------|-------------|-----------| | `/ai improve` | Améliore la rédaction | Ctrl+Shift+I | | `/ai shorten` | Raccourcit le texte | Ctrl+Shift+S | | `/ai longer` | Développe le texte | Ctrl+Shift+L | | `/ai fix` | Corrige orthographe | - | | `/ai title` | Génère titre depuis contenu | - | ### 3.6 EditorToolbar (`components/editor/editor-toolbar.tsx`) **Responsabilité :** Toolbar contextuelle (bubble menu) et sticky. **Modes :** 1. **Bubble Menu** (texte sélectionné) : - Bold, Italic, Strike, Underline - Highlight - Link - AI Improve (si sélection > 10 mots) 2. **Floating Menu** (ligne vide) : - + pour ajouter bloc - Raccourcis rapides --- ## 4. Configuration Novel ### 4.1 novel-config.ts ```typescript // lib/editor/novel-config.ts import { Extension } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; import TaskList from '@tiptap/extension-task-list'; import TaskItem from '@tiptap/extension-task-item'; // ... autres imports export const novelConfig = { // Extensions de base extensions: [ StarterKit.configure({ heading: { levels: [1, 2, 3] }, }), TaskList, TaskItem.configure({ nested: true }), Link.configure({ openOnClick: false }), Placeholder.configure({ placeholder: ({ node }) => { if (node.type.name === 'heading') return 'Titre...'; return 'Écris / pour commencer...'; } }), Markdown.configure({ transformCopiedText: true, transformPastedText: true, }), ], // Options d'éditeur editorProps: { attributes: { class: 'prose prose-sm dark:prose-invert max-w-none focus:outline-none', }, handleDOMEvents: { keydown: (view, event) => { // Custom keyboard shortcuts if (event.key === 'Tab' && event.shiftKey) { // Outdent return true; } return false; } } }, // Thème theme: { color: 'inherit', backgroundColor: 'transparent', } }; // Commandes slash export const slashCommands = [ { title: 'Titre 1', description: 'Grand titre', searchTerms: ['h1', 'title', 'titre'], icon: Heading1, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).toggleHeading({ level: 1 }).run(); } }, // ... autres commandes ]; ``` --- ## 5. Conversion Markdown ### 5.1 markdown-converter.ts **Responsabilité :** Convertir entre Markdown (stockage) et JSON Novel (édition). ```typescript // lib/editor/markdown-converter.ts import { generateJSON } from '@tiptap/html'; import { generateHTML } from '@tiptap/core'; import { novelExtensions } from './novel-config'; /** * Convertit Markdown en JSON Novel * Utilisé au chargement d'une note */ export function markdownToNovelJSON(markdown: string): JSONContent { // Tiptap Markdown extension parse le MD const html = markdownToHtml(markdown); // Utilise marked ou similar return generateJSON(html, novelExtensions); } /** * Convertit JSON Novel en Markdown * Utilisé pour la sauvegarde */ export function novelJSONToMarkdown(json: JSONContent): string { const html = generateHTML(json, novelExtensions); return htmlToMarkdown(html); // Utilise turndown ou similar } /** * Extrait le texte brut pour les cartes * Utilisé dans note-card.tsx */ export function extractTextFromNovelJSON(json: JSONContent): string { // Récursion sur les nodes pour extraire le texte let text = ''; function traverse(node: any) { if (node.text) { text += node.text + ' '; } if (node.content) { node.content.forEach(traverse); } } traverse(json); return text.trim().slice(0, 200); // Limite pour les cartes } ``` --- ## 6. Hooks ### 6.1 useNovelEditor (`hooks/use-novel-editor.ts`) ```typescript import { useEditor } from '@tiptap/react'; import { markdownToNovelJSON, novelJSONToMarkdown } from '@/lib/editor/markdown-converter'; export function useNovelEditor(initialMarkdown: string) { const [markdown, setMarkdown] = useState(initialMarkdown); const editor = useEditor({ extensions: novelExtensions, content: markdownToNovelJSON(initialMarkdown), onUpdate: ({ editor }) => { const json = editor.getJSON(); const newMarkdown = novelJSONToMarkdown(json); setMarkdown(newMarkdown); }, }); const updateContent = useCallback((newMarkdown: string) => { if (editor && newMarkdown !== markdown) { editor.commands.setContent(markdownToNovelJSON(newMarkdown)); } }, [editor, markdown]); return { editor, markdown, updateContent, }; } ``` ### 6.2 useEditorSave (`hooks/use-editor-save.ts`) ```typescript import { useDebounce } from './use-debounce'; export function useEditorSave(noteId: string) { const [content, setContent] = useState(''); const [isDirty, setIsDirty] = useState(false); const debouncedContent = useDebounce(content, 1500); useEffect(() => { if (debouncedContent && isDirty) { saveInline(noteId, { content: debouncedContent }); setIsDirty(false); } }, [debouncedContent, noteId, isDirty]); const updateContent = (newContent: string) => { setContent(newContent); setIsDirty(true); }; return { content, updateContent, isDirty, isSaving: isDirty, }; } ``` ### 6.3 useDeviceType (`hooks/use-device-type.ts`) ```typescript import { useMediaQuery } from './use-media-query'; export function useDeviceType() { const isMobile = useMediaQuery('(max-width: 768px)'); const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); const isDesktop = useMediaQuery('(min-width: 1025px)'); return { isMobile, isTablet, isDesktop, isTouch: isMobile || isTablet, }; } ``` --- ## 7. Intégration avec NoteInlineEditor ### 7.1 Modification de note-inline-editor.tsx ```typescript // components/note-inline-editor.tsx import { EditorContainer } from './editor/editor-container'; export function NoteInlineEditor({ note, onChange, onDelete, onArchive }: NoteInlineEditorProps) { const { t } = useLanguage(); const { deviceType } = useDeviceType(); // ... autres states (title, isMarkdown, etc.) return (
{/* Toolbar existante (image, link, markdown toggle, AI) */}
{/* ... existing toolbar buttons ... */}
{/* Zone d'édition - NOUVEAU */}
{/* Titre */} { changeTitle(e.target.value); scheduleSave(); }} /> {/* Contenu - REMPLACÉ PAR EditorContainer */}
{note.type === 'text' ? ( { changeContent(newContent); scheduleSave(); }} placeholder={t('notes.takeNote')} enableAI={true} /> ) : ( /* Checklist existante - Gardée telle quelle ou migrée vers Tiptap */ )}
); } ``` --- ## 8. Mode Carte (NoteCard) ### 8.1 Impact sur note-card.tsx **Aucune modification majeure requise.** Le système actuel utilise `getNotePreview()` qui extrait le texte du Markdown. Comme on continue de stocker du Markdown, la compatibilité est assurée. ```typescript // Pas de changement nécessaire dans note-card.tsx // getNotePreview() continue de fonctionner avec le Markdown function getNotePreview(note: Note, maxLength = 150): string { const content = note.content || ''; // Supprime la syntaxe Markdown pour l'affichage const plainText = content .replace(/#+ /g, '') // Titres .replace(/\*\*/g, '') // Gras .replace(/\*/g, '') // Italique .replace(/- \[([ x])\] /g, '') // Checklists .replace(/- /g, '') // Listes .replace(/\n/g, ' '); // Sauts de ligne return plainText.slice(0, maxLength) + (plainText.length > maxLength ? '...' : ''); } ``` --- ## 9. API AI pour l'Éditeur ### 9.1 POST /api/ai/editor/improve ```typescript // app/api/ai/editor/improve/route.ts import { NextRequest, NextResponse } from 'next/server'; import { auth } from '@/auth'; import { getAIProvider } from '@/lib/ai/factory'; export async function POST(req: NextRequest) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { text, language } = await req.json(); if (!text || text.length < 10) { return NextResponse.json({ error: 'Text too short' }, { status: 400 }); } const provider = getAIProvider(await getSystemConfig()); const prompt = `Améliore ce texte en ${language || 'français'} pour le rendre plus clair et fluide. Garde le même sens mais améliore la formulation. Texte: ${text} Réponds uniquement avec le texte amélioré, sans explications.`; const improved = await provider.generateText(prompt); return NextResponse.json({ text: improved.trim() }); } ``` ### 9.2 POST /api/ai/editor/shorten Similaire avec prompt pour raccourcir. --- ## 10. Responsive Strategy ### 10.1 Breakpoints | Breakpoint | Device | Composant | Raison | |------------|--------|-----------|--------| | `< 640px` | Mobile | Textarea | Performance, touch friendly | | `640px - 768px` | Large Mobile | Textarea | Touche bientôt desktop | | `768px - 1024px` | Tablet | Novel (light) | Plus d'espace, mais UX tactile | | `> 1024px` | Desktop | Novel (full) | Toutes les fonctionnalités | ### 10.2 Détection côté serveur (optionnel) ```typescript // Pour éviter le flash au chargement import { headers } from 'next/headers'; import { userAgent } from 'next/server'; export function detectMobile() { const { device } = userAgent({ headers: headers() }); return device?.type === 'mobile'; } ``` --- ## 11. Checklists ### 11.1 Approche recommandée : Hybride **Stockage :** Markdown natif `- [ ]` et `- [x]` **Pourquoi ?** - ✅ Standard Markdown (portable) - ✅ Lisible en mode carte - ✅ Éditable partout - ✅ Compatible export/import **Implémentation :** ```markdown # Ma note - [ ] Tâche à faire - [x] Tâche complétée - [ ] Sous-tâche - [ ] Item 1 - [x] Item 2 ``` ### 11.2 Extensions Tiptap pour checklists ```typescript import TaskList from '@tiptap/extension-task-list'; import TaskItem from '@tiptap/extension-task-item'; TaskItem.configure({ nested: true, HTMLAttributes: { class: 'flex items-start gap-2 my-1', }, }); ``` --- ## 12. Tests ### 12.1 Tests unitaires ```typescript // __tests__/markdown-converter.test.ts describe('Markdown Converter', () => { test('converts checklist markdown to JSON', () => { const md = '- [ ] Task 1\n- [x] Task 2'; const json = markdownToNovelJSON(md); expect(json.content[0].type).toBe('taskList'); expect(json.content[0].content).toHaveLength(2); }); test('converts headings correctly', () => { const md = '# Title\n## Subtitle'; const json = markdownToNovelJSON(md); expect(json.content[0].type).toBe('heading'); expect(json.content[0].attrs.level).toBe(1); }); test('extracts plain text for cards', () => { const json = { type: 'doc', content: [ { type: 'heading', content: [{ text: 'Title' }] }, { type: 'paragraph', content: [{ text: 'Content' }] }, ] }; const text = extractTextFromNovelJSON(json); expect(text).toBe('Title Content'); }); }); ``` ### 12.2 Tests e2e ```typescript // tests/editor.spec.ts test('user can create a checklist in novel editor', async ({ page }) => { await page.goto('/'); await page.click('[data-testid="new-note"]'); await page.click('[data-testid="list-view-toggle"]'); // Type slash command await page.click('[data-testid="note-editor"]'); await page.keyboard.type('/check'); await page.click('text=Checklist'); // Type checklist item await page.keyboard.type('My task'); // Verify checkbox appears await expect(page.locator('input[type="checkbox"]')).toBeVisible(); }); ``` --- ## 13. Timeline Détaillée ### Jour 1 : Setup et Configuration (4-5h) **Tâches :** - [ ] Installer toutes les dépendances Novel/Tiptap - [ ] Créer `lib/editor/novel-config.ts` - [ ] Créer `components/editor/novel-editor.tsx` (basique) - [ ] Vérifier que Novel s'affiche correctement **Livrable :** Novel affiche un texte simple, sauvegarde en Markdown. ### Jour 2 : Fonctionnalités Essentielles (6-7h) **Tâches :** - [ ] Créer `slash-commands.tsx` avec toutes les commandes - [ ] Configurer extensions checklists - [ ] Créer `editor-toolbar.tsx` (bubble menu) - [ ] Intégrer Markdown converter - [ ] Tester import/export Markdown **Livrable :** Éditeur fonctionnel avec slash commands et checklists. ### Jour 3 : Intégration Système (6-7h) **Tâches :** - [ ] Modifier `note-inline-editor.tsx` pour utiliser EditorContainer - [ ] Créer `hooks/use-novel-editor.ts` - [ ] Créer `hooks/use-editor-save.ts` - [ ] Connecter sauvegarde auto existante - [ ] Vérifier compatibilité mode liste/carte **Livrable :** Novel fonctionne dans l'app, sauvegarde auto OK. ### Jour 4 : Mobile et Responsive (5-6h) **Tâches :** - [ ] Créer `mobile-textarea.tsx` avec toolbar - [ ] Créer `editor-container.tsx` avec switch desktop/mobile - [ ] Implémenter `use-device-type.ts` - [ ] Tester sur différentes tailles d'écran - [ ] Optimiser performance mobile **Livrable :** Textarea sur mobile, Novel sur desktop. ### Jour 5 : AI et Polish (5-6h) **Tâches :** - [ ] Créer `/api/ai/editor/improve` et `/shorten` - [ ] Créer `ai-commands.tsx` - [ ] Intégrer commandes AI dans Novel - [ ] Tests et correction de bugs - [ ] Documentation **Livrable :** Commandes AI fonctionnelles, tests passent. ### Jour 6 : Tests et Déploiement (4-5h) **Tâches :** - [ ] Tests cross-navigateurs - [ ] Test avec 50+ notes - [ ] Test utilisateurs (interne) - [ ] Corrections finales - [ ] Merge et déploiement **Total : 6 jours (30-36h de développement)** --- ## 14. Risques et Mitigations | Risque | Probabilité | Impact | Mitigation | |--------|-------------|--------|------------| | Bundle size trop grand | Moyenne | Moyen | Tree-shaking, lazy load Novel | | Perf sur vieux devices | Moyenne | Haut | Fallback textarea automatique | | Migration données | Faible | Haut | Tests exhaustifs MD ↔ JSON | | UX change trop radicale | Faible | Moyen | Feature flag, rollback possible | | Conflits avec checklists existantes | Moyenne | Haut | Garder format MD compatible | --- ## 15. Checklist de Validation Avant de merger : - [ ] Novel fonctionne sur Chrome, Firefox, Safari - [ ] Mobile textarea fonctionne sur iOS/Android - [ ] Sauvegarde auto fonctionne (pas de régression) - [ ] Mode liste ↔ Mode carte sans perte de données - [ ] Checklists s'affichent correctement dans les deux modes - [ ] Export Markdown identique à l'entrée - [ ] Tests unitaires passent - [ ] Tests e2e passent - [ ] Bundle size < +150KB gzipped - [ ] Performance OK avec 50 notes - [ ] Accessibilité (keyboard navigation, ARIA) --- ## 16. Ressources ### Documentation - [Novel.sh](https://novel.sh/) - [Tiptap Docs](https://tiptap.dev/) - [ProseMirror (base de Tiptap)](https://prosemirror.net/) ### Exemples - [Novel sur GitHub](https://github.com/steven-tey/novel) - [Tiptap examples](https://tiptap.dev/examples) ### Articles - [Building a Notion-like editor](https://tiptap.dev/blog/building-a-notion-like-editor) --- **Document créé le :** 2024 **Version :** 1.0 **Auteur :** Assistant Claude **Statut :** Prêt pour développement --- ## Questions/Réponses **Q : Puis-je encore utiliser Markdown brut ?** R : Oui, le stockage reste en Markdown. Vous pouvez éditer le MD directement si besoin. **Q : Et si Novel ne me plaît pas ?** R : Retour au textarea est instantané (même format MD). Pas de vendor lock-in. **Q : Les checklists existantes seront-elles conservées ?** R : Oui, format MD identique. Migration transparente. **Q : Performance sur mobile ?** R : Textarea dédié mobile, donc meilleure perf qu'actuellement sur mobile.