24 KiB
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)
# 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)
# Déjà inclus avec shadcn
# - @radix-ui/react-popover
# - @radix-ui/react-toolbar
# - class-variance-authority
# - clsx / tailwind-merge
2.3 Types
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.
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 :
interface NovelEditorProps {
value: string; // Markdown initial
onChange: (markdown: string) => void;
placeholder?: string;
enableAI?: boolean;
extensions?: Extension[]; // Extensions Tiptap additionnelles
}
Configuration :
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)
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 :
-
Bubble Menu (texte sélectionné) :
- Bold, Italic, Strike, Underline
- Highlight
- Link
- AI Improve (si sélection > 10 mots)
-
Floating Menu (ligne vide) :
-
- pour ajouter bloc
- Raccourcis rapides
-
4. Configuration Novel
4.1 novel-config.ts
// 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).
// 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)
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)
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)
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
// 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.
// 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
// 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)
// 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 :
# 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
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
// __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
// 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.tsxavec 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.tsxpour 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.tsxavec toolbar - Créer
editor-container.tsxavec 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/improveet/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
Exemples
Articles
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.