UX Pro Max appliqué selon les best practices 2026 (Heptabase, Obsidian, Tana):
- confirmation-dialogs (HIGH): modal de confirmation avant brainstorm
- success-feedback (MEDIUM): toast avec destination du carnet
- nav-state-active (MEDIUM): indication claire de la session active
3 fixes:
1. Modal de confirmation (brainstorm-confirm-dialog.tsx, 181 lignes):
- Design system respecté: brand-accent, memento-paper, font-memento-serif
- Aperçu seed en card paper
- Sélecteur de carnet avec animation d'expansion
- Bouton 'Annuler' + 'Lancer le brainstorm' avec loader
- role=dialog, aria-modal, aria-label
- focus-visible, keyboard nav
- backdrop-blur-sm + click-to-close
- Lazy loaded via next/dynamic
2. Race condition fix (brainstorm-page.tsx L111):
- Auto-select session désactivé si urlSeed présent
- urlSeed + urlInviteToken court-circuitent le useEffect
3. Toast avec destination (handleConvert + noteCreatedIn key):
- message: 'Note créée dans [Nom du carnet]'
- useConvertIdea accepte notebookId optionnel
- API convert accepte notebookId via zod schema
- useConvertIdea(mutationFn) accepte string | {ideaId, notebookId}
i18n: 9 nouvelles clés brainstorm.* en EN/FR
182 lines
8.5 KiB
TypeScript
182 lines
8.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState, memo } from 'react'
|
|
import { motion, AnimatePresence } from 'motion/react'
|
|
import { Wind, FolderOpen, X, Loader2, ChevronDown } from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { useNotebooks } from '@/context/notebooks-context'
|
|
import type { Note } from '@/lib/types'
|
|
|
|
interface BrainstormConfirmDialogProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
onConfirm: (notebookId: string | null) => void
|
|
note: Note | null
|
|
}
|
|
|
|
function BrainstormConfirmDialogInner({ open, onClose, onConfirm, note }: BrainstormConfirmDialogProps) {
|
|
const { t } = useLanguage()
|
|
const { notebooks } = useNotebooks()
|
|
const [showNotebookSelect, setShowNotebookSelect] = useState(false)
|
|
|
|
const noteNotebook = notebooks.find(nb => nb.id === note?.notebookId)
|
|
const defaultNotebookName = noteNotebook?.name || t('brainstorm.defaultNotebook') || 'Brainstorm'
|
|
const defaultNotebookId = note?.notebookId ?? null
|
|
|
|
const [selectedNotebookId, setSelectedNotebookId] = useState<string | null>(defaultNotebookId)
|
|
const [selectedNotebookName, setSelectedNotebookName] = useState(defaultNotebookName)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleConfirm = () => {
|
|
setLoading(true)
|
|
onConfirm(selectedNotebookId)
|
|
}
|
|
|
|
const handleClose = () => {
|
|
setLoading(false)
|
|
setSelectedNotebookId(defaultNotebookId)
|
|
setSelectedNotebookName(defaultNotebookName)
|
|
setShowNotebookSelect(false)
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && note && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
|
|
onClick={(e) => { if (e.target === e.currentTarget) handleClose() }}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t('brainstorm.confirmTitle') || 'Brainstorm this idea'}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.92, y: 20 }}
|
|
animate={{ scale: 1, y: 0 }}
|
|
exit={{ scale: 0.92, y: 20 }}
|
|
transition={{ type: 'spring', stiffness: 340, damping: 30 }}
|
|
className="bg-memento-paper dark:bg-zinc-900 rounded-3xl shadow-2xl border border-border/40 w-full max-w-md overflow-hidden"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="px-6 py-5 border-b border-border/20 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-xl bg-brand-accent/10 flex items-center justify-center">
|
|
<Wind size={16} className="text-brand-accent" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-memento-serif text-base font-medium text-ink dark:text-dark-ink">
|
|
{t('brainstorm.confirmTitle') || 'Brainstorm this idea'}
|
|
</h3>
|
|
<p className="text-[10px] text-concrete uppercase tracking-widest font-bold">
|
|
{t('brainstorm.confirmSubtitle') || 'AI will expand your idea'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleClose}
|
|
className="p-1.5 rounded-lg text-concrete hover:text-ink hover:bg-black/5 dark:hover:bg-white/5 transition-colors cursor-pointer"
|
|
aria-label={t('common.close') || 'Close'}
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="px-6 py-5 space-y-4">
|
|
{/* Seed preview */}
|
|
<div className="p-4 rounded-2xl bg-white/60 dark:bg-white/5 border border-border/30">
|
|
<p className="text-[10px] uppercase tracking-widest font-bold text-concrete mb-2">
|
|
{t('brainstorm.seedLabel') || 'Seed idea'}
|
|
</p>
|
|
<p className="text-sm font-memento-serif text-ink dark:text-dark-ink leading-relaxed line-clamp-3">
|
|
{note.title || t('notes.untitled')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Destination notebook */}
|
|
<div>
|
|
<p className="text-[10px] uppercase tracking-widest font-bold text-concrete mb-2">
|
|
{t('brainstorm.destinationLabel') || 'Notes will be saved to'}
|
|
</p>
|
|
<button
|
|
onClick={() => setShowNotebookSelect(!showNotebookSelect)}
|
|
className="w-full flex items-center justify-between p-3 rounded-xl bg-white/60 dark:bg-white/5 border border-border/30 hover:border-brand-accent/40 transition-colors cursor-pointer"
|
|
>
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-7 h-7 rounded-lg bg-brand-accent/10 flex items-center justify-center shrink-0">
|
|
<FolderOpen size={13} className="text-brand-accent" />
|
|
</div>
|
|
<span className="text-sm font-medium text-ink dark:text-dark-ink truncate">
|
|
{selectedNotebookName}
|
|
</span>
|
|
</div>
|
|
<ChevronDown size={14} className={`text-concrete transition-transform ${showNotebookSelect ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
<AnimatePresence>
|
|
{showNotebookSelect && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="mt-1 max-h-48 overflow-y-auto rounded-xl border border-border/30 bg-white dark:bg-zinc-800 shadow-lg">
|
|
<button
|
|
onClick={() => { setSelectedNotebookId(null); setSelectedNotebookName(t('brainstorm.noNotebook') || 'No notebook'); setShowNotebookSelect(false) }}
|
|
className="w-full text-left px-3 py-2.5 hover:bg-brand-accent/5 transition-colors text-sm text-concrete cursor-pointer"
|
|
>
|
|
{t('brainstorm.noNotebook') || 'No notebook'}
|
|
</button>
|
|
{notebooks.map(nb => (
|
|
<button
|
|
key={nb.id}
|
|
onClick={() => { setSelectedNotebookId(nb.id); setSelectedNotebookName(nb.name); setShowNotebookSelect(false) }}
|
|
className={`w-full text-left px-3 py-2.5 hover:bg-brand-accent/5 transition-colors text-sm cursor-pointer flex items-center gap-2 ${selectedNotebookId === nb.id ? 'text-brand-accent font-medium' : 'text-ink dark:text-dark-ink'}`}
|
|
>
|
|
<FolderOpen size={12} className="shrink-0 opacity-50" />
|
|
<span className="truncate">{nb.name}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-4 border-t border-border/20 flex items-center justify-end gap-3">
|
|
<button
|
|
onClick={handleClose}
|
|
disabled={loading}
|
|
className="px-4 py-2.5 rounded-xl text-sm font-medium text-concrete hover:text-ink hover:bg-black/5 dark:hover:bg-white/5 transition-colors cursor-pointer"
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
onClick={handleConfirm}
|
|
disabled={loading}
|
|
className="px-5 py-2.5 rounded-xl bg-brand-accent text-white text-sm font-bold uppercase tracking-wider hover:bg-brand-accent/90 transition-colors cursor-pointer disabled:opacity-50 flex items-center gap-2"
|
|
>
|
|
{loading ? (
|
|
<><Loader2 size={14} className="animate-spin" /> {t('brainstorm.launching') || 'Launching…'}</>
|
|
) : (
|
|
<><Wind size={14} /> {t('brainstorm.launch') || 'Brainstorm'}</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
export const BrainstormConfirmDialog = memo(BrainstormConfirmDialogInner)
|