feat: standardize UI theme, fix dark mode consistency, and implement editorial tags
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m24s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m24s
This commit is contained in:
@@ -10,7 +10,7 @@ import { NotesEditorialView } from '@/components/notes-editorial-view'
|
||||
import { MemoryEchoNotification } from '@/components/memory-echo-notification'
|
||||
import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight } from 'lucide-react'
|
||||
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, Tag as TagIcon, X } from 'lucide-react'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useRefresh } from '@/lib/use-refresh'
|
||||
import { useReminderCheck } from '@/hooks/use-reminder-check'
|
||||
@@ -88,11 +88,15 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
const [autoLabelOpen, setAutoLabelOpen] = useState(false)
|
||||
const [summaryDialogOpen, setSummaryDialogOpen] = useState(false)
|
||||
const [createSubNotebookOpen, setCreateSubNotebookOpen] = useState(false)
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([])
|
||||
const [isTagsExpanded, setIsTagsExpanded] = useState(false)
|
||||
const [tagSearchQuery, setTagSearchQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldSuggestLabels && suggestNotebookId) {
|
||||
setAutoLabelOpen(true)
|
||||
}
|
||||
// Auto-trigger disabled — user opens manually from AI panel
|
||||
// if (shouldSuggestLabels && suggestNotebookId) {
|
||||
// setAutoLabelOpen(true)
|
||||
// }
|
||||
}, [shouldSuggestLabels, suggestNotebookId])
|
||||
|
||||
// Sidebar carnet / inbox: fermer l'éditeur plein écran (comme la ref. architectural-grid)
|
||||
@@ -278,7 +282,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
|
||||
if (labelFilter.length > 0) {
|
||||
allNotes = allNotes.filter((note: any) =>
|
||||
note.labels?.some((label: string) => labelFilter.includes(label))
|
||||
labelFilter.every((label: string) => note.labels?.includes(label))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -341,6 +345,52 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
return trail
|
||||
}, [currentNotebook, notebooks])
|
||||
|
||||
const availableTags = useMemo(() => {
|
||||
const tagsMap = new Map<string, { id: string; name: string; type?: string }>()
|
||||
const carnetNotes = notes
|
||||
carnetNotes.forEach(note => {
|
||||
;(note.labels || []).forEach(labelName => {
|
||||
if (!tagsMap.has(labelName)) {
|
||||
const labelObj = labels.find((l: any) => l.name === labelName)
|
||||
tagsMap.set(labelName, {
|
||||
id: labelObj?.id || labelName,
|
||||
name: labelName,
|
||||
type: labelObj?.type,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return Array.from(tagsMap.values()).sort((a, b) => {
|
||||
if (a.type === 'ai' && b.type !== 'ai') return -1
|
||||
if (a.type !== 'ai' && b.type === 'ai') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
}, [notes, labels])
|
||||
|
||||
const visibleTags = useMemo(() => {
|
||||
let filtered = availableTags
|
||||
if (tagSearchQuery) {
|
||||
filtered = availableTags.filter(t =>
|
||||
t.name.toLowerCase().includes(tagSearchQuery.toLowerCase())
|
||||
)
|
||||
} else if (!isTagsExpanded) {
|
||||
filtered = availableTags.slice(0, 10)
|
||||
selectedTagIds.forEach(id => {
|
||||
if (!filtered.find(t => t.id === id)) {
|
||||
const tag = availableTags.find(t => t.id === id)
|
||||
if (tag) filtered.push(tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
return filtered
|
||||
}, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds])
|
||||
|
||||
const toggleTag = useCallback((tagId: string) => {
|
||||
setSelectedTagIds(prev =>
|
||||
prev.includes(tagId) ? prev.filter(id => id !== tagId) : [...prev, tagId]
|
||||
)
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setControls({
|
||||
@@ -351,12 +401,22 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
|
||||
// Apply sort order to notes
|
||||
const sortedNotes = useMemo(() => {
|
||||
const sorted = [...notes]
|
||||
let sorted = [...notes]
|
||||
if (sortOrder === 'newest') sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
if (sortOrder === 'oldest') sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
if (sortOrder === 'alpha') sorted.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
|
||||
|
||||
if (selectedTagIds.length > 0) {
|
||||
const selectedNames = selectedTagIds
|
||||
.map(id => availableTags.find(t => t.id === id)?.name)
|
||||
.filter(Boolean) as string[]
|
||||
sorted = sorted.filter(note =>
|
||||
selectedNames.every(name => (note.labels || []).includes(name))
|
||||
)
|
||||
}
|
||||
|
||||
return sorted
|
||||
}, [notes, sortOrder])
|
||||
}, [notes, sortOrder, selectedTagIds, availableTags])
|
||||
|
||||
const sortedPinnedNotes = useMemo(() => {
|
||||
return sortedNotes.filter(n => n.isPinned)
|
||||
@@ -404,21 +464,20 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
fullPage
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto min-h-0 bg-memento-paper flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto min-h-0 bg-memento-paper dark:bg-background flex flex-col">
|
||||
<div
|
||||
className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-memento-paper/90 backdrop-blur-md z-30"
|
||||
className="px-12 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">
|
||||
<div>
|
||||
{currentNotebook && notebookPath.length > 0 && (
|
||||
<div
|
||||
className="flex items-center gap-2 text-[12px] uppercase tracking-[.2em] font-bold mb-2"
|
||||
style={{ color: 'var(--color-ink)', opacity: 1 }}
|
||||
className="flex items-center gap-2 text-[12px] uppercase tracking-[.2em] font-bold mb-2 text-ink/60"
|
||||
>
|
||||
{notebookPath.map((nb: any, i: number) => (
|
||||
<React.Fragment key={nb.id}>
|
||||
{i > 0 && <ChevronRight size={10} className="shrink-0" style={{ color: 'var(--color-concrete)' }} />}
|
||||
<span style={{ color: i === notebookPath.length - 1 ? 'var(--color-ink)' : 'var(--color-concrete)' }}>
|
||||
{i > 0 && <ChevronRight size={10} className="shrink-0 text-concrete" />}
|
||||
<span className={i === notebookPath.length - 1 ? 'text-ink' : 'text-concrete'}>
|
||||
{nb.name}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
@@ -557,6 +616,84 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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') || 'Filter by Tags'}</span>
|
||||
{selectedTagIds.length > 0 && (
|
||||
<span className="bg-memento-blue/10 text-memento-blue px-2 py-0.5 rounded-full text-[9px] lowercase tracking-normal">
|
||||
{selectedTagIds.length} active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{availableTags.length > 10 && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('labels.searchTags') || 'Search tags...'}
|
||||
className="bg-transparent border-b border-foreground/10 text-[10px] outline-none focus:border-memento-blue/40 py-1 px-2 w-32 transition-all focus:w-48 placeholder:text-muted-foreground/40"
|
||||
value={tagSearchQuery}
|
||||
onChange={e => setTagSearchQuery(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center min-h-[32px]">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleTags.map(tag => {
|
||||
const isActive = selectedTagIds.includes(tag.id)
|
||||
return (
|
||||
<motion.button
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
key={tag.id}
|
||||
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',
|
||||
isActive
|
||||
? 'bg-foreground text-background border-foreground shadow-sm'
|
||||
: 'bg-card/40 border-border text-muted-foreground hover:border-foreground/30 hover:bg-card/60',
|
||||
)}
|
||||
>
|
||||
{tag.type === 'ai' && (
|
||||
<Sparkles
|
||||
size={10}
|
||||
className={isActive ? 'text-memento-blue' : 'text-memento-blue/60'}
|
||||
/>
|
||||
)}
|
||||
{tag.name}
|
||||
{isActive && <X size={10} />}
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{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') || 'Show less')
|
||||
: `+ ${availableTags.length - 10} more`}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedTagIds.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTagIds([])}
|
||||
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-red-500 hover:underline ml-auto"
|
||||
>
|
||||
{t('labels.clearAll') || 'Clear all'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-12 flex-1 pb-20">
|
||||
|
||||
Reference in New Issue
Block a user