- mobile-action-sheet.tsx: 15 chaînes → t() - wikilinks-backlinks-panel.tsx: 3 chaînes → t() + import useLanguage - note-content-area.tsx: 'Cliquez pour éditer' → t() - undo-redo-feedback-extension.ts: strings → options configurables - i18n FR/EN: 11 nouvelles clés (mobile.*, editor.*) - SlashPreview et SlashCommand: déjà OK (i18n via localCommands)
191 lines
7.7 KiB
TypeScript
191 lines
7.7 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect, useRef } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import type { Editor } from '@tiptap/core'
|
|
import {
|
|
Trash2, Copy, FileText, Sparkles, Lightbulb, Scissors,
|
|
Wand2, Expand, X, CheckSquare, Quote, Heading1, Heading2, Heading3
|
|
} from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { toast } from 'sonner'
|
|
|
|
export type MobileActionSheetProps = {
|
|
editor: Editor | null
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
export function MobileActionSheet({
|
|
editor,
|
|
isOpen,
|
|
onClose,
|
|
}: MobileActionSheetProps) {
|
|
const { t } = useLanguage()
|
|
const sheetRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (sheetRef.current && !sheetRef.current.contains(e.target as Node)) {
|
|
onClose()
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
}, [isOpen, onClose])
|
|
|
|
if (!isOpen || !editor) return null
|
|
|
|
const handleSelectAllBlock = () => {
|
|
const { from } = editor.state.selection
|
|
const $pos = editor.state.doc.resolve(from)
|
|
const depth = $pos.depth
|
|
if (depth === 0) return
|
|
|
|
const start = $pos.start(1)
|
|
const end = $pos.end(1)
|
|
|
|
editor.chain().focus().setTextSelection({ from: start, to: end }).run()
|
|
toast.success(t('richTextEditor.blockSelected') || 'Bloc sélectionné en entier')
|
|
onClose()
|
|
}
|
|
|
|
const handleDuplicateBlock = () => {
|
|
const { from } = editor.state.selection
|
|
const $pos = editor.state.doc.resolve(from)
|
|
const depth = $pos.depth
|
|
if (depth === 0) return
|
|
|
|
const start = $pos.before(1)
|
|
const end = $pos.after(1)
|
|
const nodeText = editor.state.doc.slice(start, end)
|
|
|
|
editor.chain().focus().insertContentAt(end, nodeText.content.toJSON()).run()
|
|
toast.success(t('richTextEditor.blockDuplicated') || 'Bloc dupliqué')
|
|
onClose()
|
|
}
|
|
|
|
const handleDeleteBlock = () => {
|
|
const { from } = editor.state.selection
|
|
const $pos = editor.state.doc.resolve(from)
|
|
const depth = $pos.depth
|
|
if (depth === 0) return
|
|
|
|
const start = $pos.before(1)
|
|
const end = $pos.after(1)
|
|
editor.chain().focus().deleteRange({ from: start, to: end }).run()
|
|
toast.success(t('richTextEditor.blockDeleted') || 'Bloc supprimé')
|
|
onClose()
|
|
}
|
|
|
|
const handleAiAction = (action: 'clarify' | 'shorten' | 'improve' | 'expand') => {
|
|
// Déclenche l'appel IA en émettant un événement personnalisé ou en modifiant le contenu
|
|
// Réutilisons l'API IA existante ou ouvrons le panneau IA d'actions
|
|
onClose()
|
|
const tab = action === 'improve' ? 'actions' : 'chat'
|
|
window.dispatchEvent(new CustomEvent('memento-open-ai', { detail: { tab, scroll: action } }))
|
|
toast.info(t('richTextEditor.aiActionStarted') || 'IA Note sollicitée...')
|
|
}
|
|
|
|
return createPortal(
|
|
<div className="mobile-action-sheet-overlay">
|
|
<div ref={sheetRef} className="mobile-action-sheet-content">
|
|
<div className="mobile-action-sheet-header">
|
|
<div className="drag-indicator"></div>
|
|
<button type="button" className="close-btn" onClick={onClose} aria-label="Close">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mobile-action-sheet-body">
|
|
{/* Section 1 : Actions de bloc */}
|
|
<div className="mobile-action-sheet-section">
|
|
<h4 className="section-title">{t('mobile.blockActions') || 'Actions sur le bloc'}</h4>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<button type="button" className="action-tile-btn" onClick={handleSelectAllBlock}>
|
|
<FileText size={20} className="text-blue-500" />
|
|
<span>{t('mobile.selectAll') || 'Sélectionner tout'}</span>
|
|
</button>
|
|
<button type="button" className="action-tile-btn" onClick={handleDuplicateBlock}>
|
|
<Copy size={20} className="text-emerald-500" />
|
|
<span>{t('mobile.duplicate') || 'Dupliquer'}</span>
|
|
</button>
|
|
<button type="button" className="action-tile-btn text-rose-500" onClick={handleDeleteBlock}>
|
|
<Trash2 size={20} className="text-rose-500" />
|
|
<span>{t('mobile.delete') || 'Supprimer'}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 2 : IA Note */}
|
|
<div className="mobile-action-sheet-section">
|
|
<h4 className="section-title">{t('mobile.aiNote') || 'IA Note'}</h4>
|
|
<div className="grid grid-cols-4 gap-2">
|
|
<button type="button" className="action-tile-btn" onClick={() => handleAiAction('improve')}>
|
|
<Wand2 size={18} className="text-purple-500 animate-pulse" />
|
|
<span className="text-[10px]">{t('richTextEditor.slashImprove') || 'Améliorer'}</span>
|
|
</button>
|
|
<button type="button" className="action-tile-btn" onClick={() => handleAiAction('clarify')}>
|
|
<Lightbulb size={18} className="text-amber-500" />
|
|
<span className="text-[10px]">{t('richTextEditor.slashClarify') || 'Clarifier'}</span>
|
|
</button>
|
|
<button type="button" className="action-tile-btn" onClick={() => handleAiAction('shorten')}>
|
|
<Scissors size={18} className="text-indigo-500" />
|
|
<span className="text-[10px]">{t('richTextEditor.slashShorten') || 'Raccourcir'}</span>
|
|
</button>
|
|
<button type="button" className="action-tile-btn" onClick={() => handleAiAction('expand')}>
|
|
<Expand size={18} className="text-teal-500" />
|
|
<span className="text-[10px]">{t('richTextEditor.slashExpand') || 'Développer'}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 3 : Format de bloc */}
|
|
<div className="mobile-action-sheet-section">
|
|
<h4 className="section-title">{t('mobile.convertFormat') || 'Convertir le format'}</h4>
|
|
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-none">
|
|
<button
|
|
type="button"
|
|
className="format-pill-btn"
|
|
onClick={() => { editor.chain().focus().setParagraph().run(); onClose() }}
|
|
>
|
|
{t('mobile.paragraph') || 'Paragraphe'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="format-pill-btn"
|
|
onClick={() => { editor.chain().focus().toggleHeading({ level: 1 }).run(); onClose() }}
|
|
>
|
|
<Heading1 size={14} className="inline mr-1" /> {t('notes.heading1') || 'Titre 1'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="format-pill-btn"
|
|
onClick={() => { editor.chain().focus().toggleHeading({ level: 2 }).run(); onClose() }}
|
|
>
|
|
<Heading2 size={14} className="inline mr-1" /> {t('notes.heading2') || 'Titre 2'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="format-pill-btn"
|
|
onClick={() => { editor.chain().focus().toggleHeading({ level: 3 }).run(); onClose() }}
|
|
>
|
|
<Heading3 size={14} className="inline mr-1" /> {t('notes.heading3') || 'Titre 3'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="format-pill-btn"
|
|
onClick={() => { editor.chain().focus().toggleBlockquote().run(); onClose() }}
|
|
>
|
|
<Quote size={14} className="inline mr-1" /> {t('mobile.quote') || 'Citation'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|