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:
Antigravity
2026-05-09 07:28:03 +00:00
parent 574c8b3166
commit 97b08e5d0b
65 changed files with 2991 additions and 2296 deletions

View File

@@ -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>
)
}