feat: icon-only toolbar, versioning fixes, history modal, PanelRight repositioning
- Toolbar: remove text labels from all icon buttons (AI, Save, Preview, Convert) all buttons now icon-only with title tooltip for accessibility - Toolbar: reposition PanelRight (info panel toggle) to far right after three-dot menu - Versioning: decouple getNoteHistory/restoreNoteVersion from global userAISettings.noteHistory now checks note.historyEnabled directly — unblocks manual per-note history - Versioning: add 'Sauvegarder cette version' button in Versions tab of info panel calls commitNoteHistory with visual feedback (spinner → success state) - note-document-info-panel: import commitNoteHistory, add isSavingVersion state - notes.ts: fix double guard that silently blocked all history operations
This commit is contained in:
@@ -1,45 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Plus, X, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
|
||||
const NOTEBOOK_ICONS = [
|
||||
{ icon: Folder, name: 'folder' },
|
||||
{ icon: Briefcase, name: 'briefcase' },
|
||||
{ icon: FileText, name: 'document' },
|
||||
{ icon: Zap, name: 'lightning' },
|
||||
{ icon: BarChart3, name: 'chart' },
|
||||
{ icon: Globe, name: 'globe' },
|
||||
{ icon: Sparkles, name: 'sparkle' },
|
||||
{ icon: Book, name: 'book' },
|
||||
{ icon: Heart, name: 'heart' },
|
||||
{ icon: Crown, name: 'crown' },
|
||||
{ icon: Music, name: 'music' },
|
||||
{ icon: Building2, name: 'building' },
|
||||
]
|
||||
|
||||
const NOTEBOOK_COLORS = [
|
||||
{ name: 'Slate', value: '#64748B', bg: 'bg-slate-500' },
|
||||
{ name: 'Purple', value: '#8B5CF6', bg: 'bg-purple-500' },
|
||||
{ name: 'Red', value: '#EF4444', bg: 'bg-red-500' },
|
||||
{ name: 'Orange', value: '#F59E0B', bg: 'bg-orange-500' },
|
||||
{ name: 'Green', value: '#10B981', bg: 'bg-green-500' },
|
||||
{ name: 'Teal', value: '#14B8A6', bg: 'bg-teal-500' },
|
||||
{ name: 'Gray', value: '#6B7280', bg: 'bg-gray-500' },
|
||||
]
|
||||
|
||||
interface CreateNotebookDialogProps {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
@@ -49,166 +14,93 @@ export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialo
|
||||
const { t } = useLanguage()
|
||||
const { createNotebookOptimistic } = useNotebooks()
|
||||
const [name, setName] = useState('')
|
||||
const [selectedIcon, setSelectedIcon] = useState('folder')
|
||||
const [selectedColor, setSelectedColor] = useState('#3B82F6')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!name.trim()) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await createNotebookOptimistic({
|
||||
name: name.trim(),
|
||||
icon: selectedIcon,
|
||||
color: selectedColor,
|
||||
})
|
||||
// Close dialog — context already updated sidebar state
|
||||
await createNotebookOptimistic({ name: name.trim(), icon: 'folder', color: '#64748B' })
|
||||
setName('')
|
||||
onOpenChange?.(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to create notebook:', error)
|
||||
} catch (err) {
|
||||
console.error('Failed to create notebook:', err)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
const handleClose = () => {
|
||||
setName('')
|
||||
setSelectedIcon('folder')
|
||||
setSelectedColor('#3B82F6')
|
||||
onOpenChange?.(false)
|
||||
}
|
||||
|
||||
const SelectedIconComponent = NOTEBOOK_ICONS.find(i => i.name === selectedIcon)?.icon || Folder
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(val) => {
|
||||
onOpenChange?.(val)
|
||||
if (!val) handleReset()
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[500px] p-0">
|
||||
<button
|
||||
onClick={() => onOpenChange?.(false)}
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors z-10"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
<DialogHeader className="px-8 pt-8 pb-4">
|
||||
<DialogTitle className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{t('notebook.createNew')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('notebook.createDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={handleClose}
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
<form onSubmit={handleSubmit} className="px-8 pb-8">
|
||||
<div className="space-y-6">
|
||||
{/* Notebook Name */}
|
||||
<div>
|
||||
<label className="text-[11px] font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2 block">
|
||||
{t('notebook.name')}
|
||||
</label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('notebook.namePlaceholder')}
|
||||
className="w-full"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{/* Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.92, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.92, y: 20 }}
|
||||
transition={{ type: 'spring', damping: 28, stiffness: 280 }}
|
||||
className="relative w-full max-w-md bg-[#F2F0E9] dark:bg-zinc-900 border border-black/10 dark:border-white/10 shadow-2xl rounded-2xl p-8"
|
||||
>
|
||||
<h3 className="text-2xl font-memento-serif font-medium text-foreground mb-2">
|
||||
{t('notebook.createNew') || 'Nouveau carnet'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-6 font-light">
|
||||
{t('notebook.createDescription') || 'Donnez un nom à votre nouveau carnet.'}
|
||||
</p>
|
||||
|
||||
{/* Icon Selection */}
|
||||
<div>
|
||||
<label className="text-[11px] font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 block">
|
||||
{t('notebook.selectIcon')}
|
||||
</label>
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{NOTEBOOK_ICONS.map((item) => {
|
||||
const IconComponent = item.icon
|
||||
const isSelected = selectedIcon === item.name
|
||||
return (
|
||||
<button
|
||||
key={item.name}
|
||||
type="button"
|
||||
onClick={() => setSelectedIcon(item.name)}
|
||||
className={cn(
|
||||
"h-14 w-full rounded-xl border-2 flex items-center justify-center transition-all duration-200",
|
||||
isSelected
|
||||
? 'border-indigo-600 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600'
|
||||
: 'border-gray-200 dark:border-gray-700 text-gray-400 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
)}
|
||||
>
|
||||
<IconComponent className="h-5 w-5" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-foreground mb-2">
|
||||
{t('notebook.name') || 'Nom du carnet'}
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('notebook.namePlaceholder') || 'Ex. : Projets, Recherche…'}
|
||||
className="w-full bg-white dark:bg-zinc-800 border border-black/12 dark:border-white/15 rounded-lg px-4 py-3 outline-none focus:border-foreground/40 dark:focus:border-white/40 transition-colors font-memento-serif italic text-lg text-foreground placeholder:text-muted-foreground/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Color Selection */}
|
||||
<div>
|
||||
<label className="text-[11px] font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 block">
|
||||
{t('notebook.selectColor')}
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
{NOTEBOOK_COLORS.map((color) => {
|
||||
const isSelected = selectedColor === color.value
|
||||
return (
|
||||
<button
|
||||
key={color.value}
|
||||
type="button"
|
||||
onClick={() => setSelectedColor(color.value)}
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full border-2 transition-all duration-200",
|
||||
isSelected
|
||||
? 'border-white scale-110 shadow-lg'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:scale-105'
|
||||
)}
|
||||
style={{ backgroundColor: color.value }}
|
||||
title={color.name}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{name.trim() && (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center text-white shadow-md"
|
||||
style={{ backgroundColor: selectedColor }}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex-1 py-3 border border-black/12 dark:border-white/12 rounded-xl text-sm font-medium text-muted-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<SelectedIconComponent className="h-5 w-5" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-white">{name.trim()}</span>
|
||||
{t('notebook.cancel') || 'Annuler'}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!name.trim() || isSubmitting}
|
||||
className="flex-1 py-3 bg-foreground text-background rounded-xl text-sm font-medium hover:opacity-90 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting
|
||||
? (t('notebook.creating') || 'Création…')
|
||||
: (t('notebook.create') || 'Créer')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange?.(false)}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
>
|
||||
{t('notebook.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!name.trim() || isSubmitting}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white px-6"
|
||||
>
|
||||
{isSubmitting ? t('notebook.creating') : t('notebook.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user