feat(ui): panneau repliable, carnets plus lisibles et accueil plus clair
Harmonise la liste des notes et l’éditeur (titres, dates, retour), rend la saisie rapide et les raccourcis de l’accueil utilisables sur téléphone, et conserve le panneau latéral repliable.
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
|
||||
import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, ChevronDown, Tag as TagIcon, X, Menu, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload, Globe, Presentation } from 'lucide-react'
|
||||
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, ChevronDown, Tag as TagIcon, X, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload, Globe, Presentation } from 'lucide-react'
|
||||
import { emitNoteChange, NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync'
|
||||
import { useReminderCheck } from '@/hooks/use-reminder-check'
|
||||
import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion'
|
||||
@@ -152,6 +152,7 @@ export function HomeClient({
|
||||
const [organizeNotebookOpen, setOrganizeNotebookOpen] = useState(false) // kept for compat — old dialog unused
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([])
|
||||
const [isTagsExpanded, setIsTagsExpanded] = useState(false)
|
||||
const [tagFiltersOpen, setTagFiltersOpen] = useState(false)
|
||||
const [tagSearchQuery, setTagSearchQuery] = useState('')
|
||||
const [layoutMode, setLayoutMode] = useState<NotesLayoutMode>(initialLayoutMode)
|
||||
const [addPropertyOpen, setAddPropertyOpen] = useState(false)
|
||||
@@ -246,9 +247,10 @@ export function HomeClient({
|
||||
}, [])
|
||||
|
||||
// Sidebar carnet / inbox: fermer l'éditeur plein écran (comme la ref. architectural-grid)
|
||||
// On GARDE forceList dans l'URL pour distinguer "liste" du "dashboard"
|
||||
// On GARDE forceList dans l'URL pour distinguer "liste" du "dashboard".
|
||||
// Si openNote est présent, la note a été ouverte depuis cette liste : ne pas la fermer.
|
||||
useEffect(() => {
|
||||
if (searchParams.get('forceList') === '1') {
|
||||
if (searchParams.get('forceList') === '1' && !searchParams.get('openNote')) {
|
||||
setEditingNote(null)
|
||||
}
|
||||
}, [searchParams])
|
||||
@@ -374,9 +376,12 @@ export function HomeClient({
|
||||
// Always fetch fresh from server — avoids stale state after a save regardless of
|
||||
// whether the notes list has re-fetched yet.
|
||||
const handleOpenNoteFresh = useCallback(async (noteId: string, readOnly = false) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.set('openNote', noteId)
|
||||
router.replace(`/home?${params.toString()}`, { scroll: false })
|
||||
const note = await getNoteById(noteId)
|
||||
if (note) setEditingNote({ note, readOnly })
|
||||
}, [])
|
||||
}, [router, searchParams])
|
||||
|
||||
const handleAddNote = () => {
|
||||
startCreating(async () => {
|
||||
@@ -752,7 +757,10 @@ export function HomeClient({
|
||||
// user closes and re-opens the note quickly after saving.
|
||||
const note = await getNoteById(openNoteId)
|
||||
if (cancelled || !note) return
|
||||
setEditingNote({ note, readOnly: false })
|
||||
setEditingNote((prev) => {
|
||||
if (prev?.note.id === note.id) return { ...prev, note }
|
||||
return { note, readOnly: false }
|
||||
})
|
||||
}
|
||||
run()
|
||||
return () => {
|
||||
@@ -837,6 +845,7 @@ export function HomeClient({
|
||||
}, [notes, labels])
|
||||
|
||||
const visibleTags = useMemo(() => {
|
||||
if (!tagFiltersOpen) return availableTags.filter(tag => selectedTagIds.includes(tag.id))
|
||||
let filtered = availableTags
|
||||
if (tagSearchQuery) {
|
||||
filtered = availableTags.filter(t =>
|
||||
@@ -852,7 +861,7 @@ export function HomeClient({
|
||||
})
|
||||
}
|
||||
return filtered
|
||||
}, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds])
|
||||
}, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds, tagFiltersOpen])
|
||||
|
||||
const toggleTag = useCallback((tagId: string) => {
|
||||
setSelectedTagIds(prev =>
|
||||
@@ -928,8 +937,9 @@ export function HomeClient({
|
||||
|
||||
const handleEditorClose = useCallback(() => {
|
||||
setEditingNote(null)
|
||||
const from = searchParams.get('from')
|
||||
// Ouvert depuis le dashboard (Comparer / Lier / clic note) → revenir au dashboard, pas à la liste des carnets.
|
||||
if (searchParams.get('from') === 'dashboard' || searchParams.get('peekNote')) {
|
||||
if (from === 'dashboard' || searchParams.get('peekNote')) {
|
||||
router.replace('/home', { scroll: false })
|
||||
return
|
||||
}
|
||||
@@ -939,6 +949,11 @@ export function HomeClient({
|
||||
params.delete('from')
|
||||
const qs = params.toString()
|
||||
router.replace(qs ? `/home?${qs}` : '/home', { scroll: false })
|
||||
if (from === 'search') {
|
||||
void import('@/lib/search/restore-search').then(({ requestSearchRestore }) => {
|
||||
requestSearchRestore()
|
||||
})
|
||||
}
|
||||
}, [router, searchParams])
|
||||
|
||||
const handleNoteSaved = useCallback((savedNote: Note) => {
|
||||
@@ -989,30 +1004,35 @@ export function HomeClient({
|
||||
className="px-4 sm:px-8 md:px-12 pt-6 sm:pt-10 md:pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-memento-paper/90 dark:bg-background/90 backdrop-blur-md z-30"
|
||||
>
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
{/* Hamburger mobile — ouvre la sidebar */}
|
||||
<button
|
||||
className="md:hidden p-2 -ms-2 text-foreground hover:bg-foreground/5 rounded-lg transition-colors shrink-0 self-start mt-1"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('open-mobile-sidebar'))}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu size={22} />
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
{currentNotebook && notebookPath.length > 0 && (
|
||||
<div
|
||||
className="flex items-center gap-2 text-[12px] uppercase tracking-[.2em] font-bold mb-2 text-ink/60"
|
||||
{currentNotebook && notebookPath.length > 1 && (
|
||||
<nav
|
||||
className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] uppercase tracking-[.2em] font-bold mb-2 text-ink/60 min-w-0"
|
||||
>
|
||||
{notebookPath.map((nb: any, i: number) => (
|
||||
{notebookPath.slice(0, -1).map((nb: any, i: number) => (
|
||||
<React.Fragment key={nb.id}>
|
||||
{i > 0 && <ChevronRight size={10} className="shrink-0 text-concrete" />}
|
||||
<span className={i === notebookPath.length - 1 ? 'text-ink' : 'text-concrete'}>
|
||||
{i > 0 && <ChevronRight size={10} className="shrink-0 text-concrete rtl:rotate-180" />}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams()
|
||||
params.set('notebook', nb.id)
|
||||
params.set('forceList', '1')
|
||||
router.push(`/home?${params.toString()}`)
|
||||
}}
|
||||
className="min-w-0 max-w-full break-words [overflow-wrap:anywhere] text-start text-concrete hover:text-ink hover:underline underline-offset-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-accent rounded-sm"
|
||||
dir="auto"
|
||||
>
|
||||
{nb.name}
|
||||
</span>
|
||||
</button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
<h1 className="font-memento-serif text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight text-foreground leading-tight pe-4 sm:pe-12">
|
||||
<h1
|
||||
className="font-memento-serif text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight text-foreground leading-tight pe-4 sm:pe-12 min-w-0 break-words [overflow-wrap:anywhere]"
|
||||
dir="auto"
|
||||
>
|
||||
{currentNotebook
|
||||
? currentNotebook.name
|
||||
: searchParams.get('shared') === '1'
|
||||
@@ -1026,8 +1046,8 @@ export function HomeClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-b border-foreground/5 pb-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex min-w-0 flex-col gap-3 border-b border-foreground/5 pb-4 xl:flex-row xl:flex-wrap xl:items-center xl:justify-between">
|
||||
<div className="flex min-w-0 max-w-full flex-wrap items-center gap-x-4 gap-y-1 sm:gap-x-6 [&>button]:min-h-11 [&>button]:max-w-full [&>button>svg]:shrink-0">
|
||||
<button
|
||||
onClick={handleAddNote}
|
||||
disabled={isCreating}
|
||||
@@ -1049,7 +1069,7 @@ export function HomeClient({
|
||||
|
||||
{/* Inline search — toggles an input within the toolbar */}
|
||||
{showInlineSearch ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-h-11 min-w-0 max-w-full items-center gap-2">
|
||||
{isSearching ? (
|
||||
<div className="w-3.5 h-3.5 border border-muted-foreground/50 border-t-foreground rounded-full animate-spin shrink-0" />
|
||||
) : (
|
||||
@@ -1093,7 +1113,7 @@ export function HomeClient({
|
||||
}
|
||||
}}
|
||||
placeholder={t('search.placeholder')}
|
||||
className="w-36 sm:w-48 bg-transparent border-b border-foreground/20 focus:border-foreground outline-none text-[13px] text-foreground placeholder:text-muted-foreground/50 py-0.5 transition-colors"
|
||||
className="w-36 min-w-0 sm:w-48 bg-transparent border-b border-foreground/20 focus:border-foreground outline-none text-[13px] text-foreground placeholder:text-muted-foreground/50 py-0.5 transition-colors"
|
||||
/>
|
||||
{inlineSearchQuery && (
|
||||
<button
|
||||
@@ -1135,8 +1155,8 @@ export function HomeClient({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="bg-foreground/[0.03] dark:bg-white/[0.04] p-0.5 rounded-full flex border border-border/30 items-center">
|
||||
<div className="flex min-w-0 max-w-full flex-wrap items-center gap-x-3 gap-y-2 [&>button]:min-h-11">
|
||||
<div className="bg-foreground/[0.03] dark:bg-white/[0.04] p-0.5 rounded-full flex shrink-0 border border-border/30 items-center [&>button]:flex [&>button]:h-11 [&>button]:w-11 [&>button]:items-center [&>button]:justify-center sm:[&>button]:h-8 sm:[&>button]:w-8 [&>button>svg]:h-4 [&>button>svg]:w-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectLayoutMode('grid')}
|
||||
@@ -1331,28 +1351,36 @@ export function HomeClient({
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground">
|
||||
<TagIcon size={12} />
|
||||
<span>{t('labels.filterByTags')}</span>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={tagFiltersOpen}
|
||||
aria-controls="note-tag-filters"
|
||||
onClick={() => setTagFiltersOpen(open => !open)}
|
||||
className="flex min-h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-foreground hover:bg-brand-accent/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-accent"
|
||||
>
|
||||
<TagIcon size={16} />
|
||||
<span>{t('labels.filterButton')}</span>
|
||||
{selectedTagIds.length > 0 && (
|
||||
<span className="bg-brand-accent/10 text-brand-accent px-2 py-0.5 rounded-full text-[9px] lowercase tracking-normal">
|
||||
{selectedTagIds.length} active
|
||||
<span className="bg-brand-accent/10 text-brand-accent px-2 py-0.5 rounded-full text-xs tabular-nums">
|
||||
{selectedTagIds.length.toLocaleString(language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{availableTags.length > 10 && (
|
||||
<ChevronDown size={14} className={cn('transition-transform motion-reduce:transition-none', tagFiltersOpen && 'rotate-180')} />
|
||||
</button>
|
||||
{tagFiltersOpen && availableTags.length > 10 && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('labels.searchTags')}
|
||||
className="bg-transparent border-b border-foreground/10 text-[10px] outline-none focus:border-brand-accent/40 py-1 px-2 w-32 transition-all focus:w-48 placeholder:text-muted-foreground/40"
|
||||
aria-label={t('labels.searchTags')}
|
||||
className="max-w-full min-h-11 w-48 rounded-md border border-border bg-background text-sm outline-none focus:border-brand-accent px-3 placeholder:text-muted-foreground"
|
||||
value={tagSearchQuery}
|
||||
onChange={e => setTagSearchQuery(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center min-h-[32px]">
|
||||
<div id="note-tag-filters" hidden={!tagFiltersOpen && selectedTagIds.length === 0} className={cn('flex flex-wrap gap-2 items-center', !tagFiltersOpen && selectedTagIds.length === 0 && 'hidden')}>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleTags.map(tag => {
|
||||
const isActive = selectedTagIds.includes(tag.id)
|
||||
@@ -1363,11 +1391,13 @@ export function HomeClient({
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
key={tag.id}
|
||||
type="button"
|
||||
aria-pressed={isActive}
|
||||
onClick={() => toggleTag(tag.id)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all border flex items-center gap-2',
|
||||
'max-w-full px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-wider transition-colors border flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-accent',
|
||||
isActive
|
||||
? 'bg-foreground text-background border-foreground shadow-sm'
|
||||
? 'bg-brand-accent/10 text-brand-accent border-brand-accent/40'
|
||||
: 'bg-card/40 border-border text-muted-foreground hover:border-foreground/30 hover:bg-card/60',
|
||||
)}
|
||||
>
|
||||
@@ -1377,21 +1407,21 @@ export function HomeClient({
|
||||
className={isActive ? 'text-brand-accent' : 'text-brand-accent/60'}
|
||||
/>
|
||||
)}
|
||||
{tag.name}
|
||||
{isActive && <X size={10} />}
|
||||
<span className="break-words [overflow-wrap:anywhere]">{tag.name}</span>
|
||||
{isActive && <X size={12} className="shrink-0" />}
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{availableTags.length > 10 && !tagSearchQuery && (
|
||||
{tagFiltersOpen && availableTags.length > 10 && !tagSearchQuery && (
|
||||
<button
|
||||
onClick={() => setIsTagsExpanded(!isTagsExpanded)}
|
||||
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60 hover:text-foreground transition-colors border border-dashed border-border rounded-full"
|
||||
>
|
||||
{isTagsExpanded
|
||||
? t('labels.showLess')
|
||||
: `+ ${availableTags.length - 10} more`}
|
||||
: t('labels.showMore')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user