Files
Keep/keep-notes/PLAN_NOVEL_EDITOR.md

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 :

  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

// 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.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

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.