Files
Keep/keep-notes/PLAN_NOVEL_EDITOR.md

900 lines
24 KiB
Markdown

# 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 <MobileTextarea {...props} />;
}
return <NovelEditor {...props} />;
}
```
### 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 (
<div className="flex h-full flex-col overflow-hidden">
{/* Toolbar existante (image, link, markdown toggle, AI) */}
<div className="flex shrink-0 items-center justify-between border-b...">
{/* ... existing toolbar buttons ... */}
</div>
{/* Zone d'édition - NOUVEAU */}
<div className="flex flex-1 flex-col overflow-y-auto px-8 py-5">
{/* Titre */}
<input
type="text"
className="..."
value={title}
onChange={(e) => {
changeTitle(e.target.value);
scheduleSave();
}}
/>
{/* Contenu - REMPLACÉ PAR EditorContainer */}
<div className="mt-4 flex flex-1 flex-col">
{note.type === 'text' ? (
<EditorContainer
content={content}
onChange={(newContent) => {
changeContent(newContent);
scheduleSave();
}}
placeholder={t('notes.takeNote')}
enableAI={true}
/>
) : (
/* Checklist existante - Gardée telle quelle ou migrée vers Tiptap */
<ChecklistEditor ... />
)}
</div>
</div>
</div>
);
}
```
---
## 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.