900 lines
24 KiB
Markdown
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.
|