feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
@@ -28,14 +28,15 @@ import {
|
||||
Pin,
|
||||
PinOff,
|
||||
Sparkles,
|
||||
Home,
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { applyDocumentTheme } from '@/lib/apply-document-theme'
|
||||
import { getAllNotes, getTrashCount } from '@/app/actions/notes'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { Notebook, Note } from '@/lib/types'
|
||||
import { toast } from 'sonner'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { getNoteDisplayTitle } from '@/lib/note-preview'
|
||||
import { CreateNotebookDialog } from './create-notebook-dialog'
|
||||
@@ -50,9 +51,10 @@ import {
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
|
||||
import { UsageMeter } from './usage-meter'
|
||||
|
||||
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms'
|
||||
type SortOrder = 'newest' | 'oldest' | 'alpha'
|
||||
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
|
||||
|
||||
function NoteLink({
|
||||
title,
|
||||
@@ -103,11 +105,11 @@ function SidebarBrainstorms() {
|
||||
if (!sessions || sessions.length === 0) {
|
||||
return (
|
||||
<div className="px-4 py-6 text-center">
|
||||
<Sparkles size={20} className="mx-auto text-orange-400/40 mb-2" />
|
||||
<p className="text-[11px] text-muted-foreground">{t('brainstorm.noSessions')}</p>
|
||||
<Sparkles size={20} className="mx-auto text-brand-accent/40 mb-2" />
|
||||
<p className="text-[11px] text-concrete">{t('brainstorm.noSessions')}</p>
|
||||
<button
|
||||
onClick={() => router.push('/brainstorm')}
|
||||
className="mt-2 text-[11px] text-orange-500 hover:text-orange-400 font-medium"
|
||||
className="mt-2 text-[11px] text-brand-accent hover:text-brand-accent/80 font-medium"
|
||||
>
|
||||
{t('brainstorm.startOne')} →
|
||||
</button>
|
||||
@@ -121,21 +123,23 @@ function SidebarBrainstorms() {
|
||||
<div key={s.id} className="relative group/item">
|
||||
<button
|
||||
onClick={() => router.replace(`/brainstorm?session=${s.id}`)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all duration-200 text-start hover:bg-memento-blue/5 group ${
|
||||
(s as any)._owned === false ? 'border-s-2 border-memento-blue/30 dark:border-memento-blue/70' : ''
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all duration-200 text-start hover:bg-brand-accent/5 group ${
|
||||
(s as any)._owned === false ? 'border-s-2 border-brand-accent/30 dark:border-brand-accent/70' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 ${
|
||||
(s as any)._owned === false
|
||||
? 'border border-memento-blue/20 dark:border-memento-blue/80 bg-memento-blue/5 dark:bg-memento-blue/20'
|
||||
: 'border border-orange-200 dark:border-orange-800/40 bg-orange-50 dark:bg-orange-900/20'
|
||||
? 'border border-brand-accent/20 dark:border-brand-accent/80 bg-brand-accent/5 dark:bg-brand-accent/20'
|
||||
: 'border border-brand-accent/20 dark:border-brand-accent/40 bg-brand-accent/5 dark:bg-brand-accent/20'
|
||||
}`}>
|
||||
<Sparkles size={12} className={(s as any)._owned === false ? 'text-memento-blue' : 'text-orange-500'} />
|
||||
<Sparkles size={12} className="text-brand-accent" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[12px] font-medium truncate">
|
||||
{s.seedIdea}
|
||||
{(s as any)._owned === false && <span className="text-[9px] ms-1.5 text-memento-blue/70 font-normal">· partagé</span>}
|
||||
{(s as any)._owned === false && (
|
||||
<span className="text-[9px] ms-1.5 text-brand-accent/70 font-normal">{t('sidebar.sharedNotebookBadge')}</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{s.activeIdeas} {t('brainstorm.ideas')} · {new Date(s.createdAt).toLocaleDateString()}
|
||||
@@ -177,6 +181,7 @@ function SidebarCarnetItem({
|
||||
level,
|
||||
isExpanded,
|
||||
toggleExpand,
|
||||
hasChildNotebooks,
|
||||
}: {
|
||||
carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
|
||||
isActive: boolean
|
||||
@@ -195,10 +200,11 @@ function SidebarCarnetItem({
|
||||
level: number
|
||||
isExpanded: boolean
|
||||
toggleExpand: () => void
|
||||
hasChildNotebooks?: boolean
|
||||
}) {
|
||||
const { t, language } = useLanguage()
|
||||
const isRtl = language === 'fa' || language === 'ar'
|
||||
const hasChildren = React.Children.count(children) > 0
|
||||
const hasChildren = hasChildNotebooks || React.Children.count(children) > 0
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
|
||||
|
||||
// Close context menu on outside click
|
||||
@@ -213,7 +219,7 @@ function SidebarCarnetItem({
|
||||
<div className={cn('transition-opacity', isDragging && 'opacity-40')}>
|
||||
<div
|
||||
className="flex items-center group relative h-10"
|
||||
style={{ paddingInlineStart: `${level * 16}px` }}
|
||||
style={{ paddingInlineStart: `${level * 24 + 8}px` }}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -260,14 +266,14 @@ function SidebarCarnetItem({
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="active-indicator"
|
||||
className="absolute -start-1 w-1 h-4 bg-blueprint rounded-full"
|
||||
className="absolute -start-1 w-1 h-4 bg-brand-accent rounded-full"
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<div className={cn(
|
||||
'w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-bold border shrink-0 transition-all',
|
||||
isActive
|
||||
? 'bg-blueprint text-white border-blueprint'
|
||||
? 'bg-brand-accent text-white border-brand-accent'
|
||||
: 'bg-white/60 dark:bg-white/5 text-ink dark:text-foreground border-border'
|
||||
)}>
|
||||
{carnet.initial}
|
||||
@@ -285,7 +291,7 @@ function SidebarCarnetItem({
|
||||
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
|
||||
{isPinned && (
|
||||
<span className="text-blueprint" title={t('notebook.pinnedFrozenTooltip')}>
|
||||
<span className="text-brand-accent" title={t('notebook.pinnedFrozenTooltip')}>
|
||||
<Pin size={9} className="opacity-70" />
|
||||
</span>
|
||||
)}
|
||||
@@ -338,8 +344,8 @@ function SidebarCarnetItem({
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
|
||||
>
|
||||
{isPinned
|
||||
? <><PinOff size={13} className="text-blueprint" /><span>{t('sidebar.unfreezePinnedNotebook')}</span></>
|
||||
: <><Pin size={13} className="text-blueprint" /><span>{t('sidebar.freezePinnedNotebook')}</span></>
|
||||
? <><PinOff size={13} className="text-brand-accent" /><span>{t('sidebar.unfreezePinnedNotebook')}</span></>
|
||||
: <><Pin size={13} className="text-brand-accent" /><span>{t('sidebar.freezePinnedNotebook')}</span></>
|
||||
}
|
||||
</button>
|
||||
<div className="mx-3 my-1 border-t border-border/50" />
|
||||
@@ -411,7 +417,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
const router = useRouter()
|
||||
const { t, language } = useLanguage()
|
||||
const isRtl = language === 'fa' || language === 'ar'
|
||||
const { notebooks, trashNotebook, updateNotebookOrderOptimistic } = useNotebooks()
|
||||
const { notebooks, trashNotebook, updateNotebookOrderOptimistic, moveNotebookToParent } = useNotebooks()
|
||||
const { refreshKey } = useNoteRefresh()
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [createParentId, setCreateParentId] = useState<string | null>(null)
|
||||
@@ -470,9 +476,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
!searchParams.get('trashed')
|
||||
|
||||
useEffect(() => {
|
||||
setActiveView(
|
||||
pathname.startsWith('/agents') || pathname.startsWith('/lab') ? 'agents' : 'notebooks'
|
||||
)
|
||||
if (pathname.startsWith('/brainstorm')) setActiveView('brainstorms')
|
||||
else if (pathname.startsWith('/agents') || pathname.startsWith('/lab')) setActiveView('agents')
|
||||
else setActiveView('notebooks')
|
||||
}, [pathname])
|
||||
|
||||
const displayName = user?.name || user?.email || ''
|
||||
@@ -481,6 +487,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
// Sorted list for the sort dropdown (not used directly when dragging)
|
||||
const sortedNotebooks = useMemo(() => {
|
||||
const arr = [...notebooks]
|
||||
if (sortOrder === 'manual') return arr.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
if (sortOrder === 'alpha') return arr.sort((a, b) => a.name.localeCompare(b.name))
|
||||
if (sortOrder === 'newest') return arr.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
if (sortOrder === 'oldest') return arr.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
@@ -542,60 +549,60 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
router.push(`/?${params.toString()}`)
|
||||
}
|
||||
|
||||
// ── Drag handlers ──
|
||||
// ── Drag state ──
|
||||
const [dropTarget, setDropTarget] = useState<string | null>(null)
|
||||
const [dropAction, setDropAction] = useState<'into' | null>(null)
|
||||
|
||||
const handleDragStart = (e: React.DragEvent, notebookId: string) => {
|
||||
setDraggedId(notebookId)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, notebookId: string) => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
if (dragOverId.current === notebookId) return
|
||||
dragOverId.current = notebookId
|
||||
|
||||
if (!draggedId || draggedId === notebookId) return
|
||||
setOrderedNotebooks(prev => {
|
||||
const fromIdx = prev.findIndex(n => n.id === draggedId)
|
||||
const toIdx = prev.findIndex(n => n.id === notebookId)
|
||||
if (fromIdx === -1 || toIdx === -1) return prev
|
||||
const next = [...prev]
|
||||
const [item] = next.splice(fromIdx, 1)
|
||||
next.splice(toIdx, 0, item)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
if (!draggedId) return
|
||||
const savedOrder = [...orderedNotebooks]
|
||||
setDraggedId(null)
|
||||
dragOverId.current = null
|
||||
// Block the sync effect so the server reload doesn't overwrite local order
|
||||
isSavingRef.current = true
|
||||
try {
|
||||
await updateNotebookOrderOptimistic(savedOrder.map(n => n.id))
|
||||
// Keep local order — server will return them in the right order next load
|
||||
setOrderedNotebooks(savedOrder)
|
||||
} catch {
|
||||
// On failure, revert to original server order
|
||||
isSavingRef.current = false
|
||||
setOrderedNotebooks(sortedNotebooks)
|
||||
} finally {
|
||||
// Allow sync again after 2 s (time for the server reload to settle)
|
||||
setTimeout(() => {
|
||||
isSavingRef.current = false
|
||||
}, 2000)
|
||||
}
|
||||
e.dataTransfer.setData('text/plain', notebookId)
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
if (draggedId) {
|
||||
// Drag cancelled without drop — restore
|
||||
setDraggedId(null)
|
||||
dragOverId.current = null
|
||||
isSavingRef.current = false
|
||||
setDropTarget(null)
|
||||
setDropAction(null)
|
||||
setOrderedNotebooks(sortedNotebooks)
|
||||
}
|
||||
|
||||
const handleDropOnNotebook = async (e: React.DragEvent, targetId: string) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDropTarget(null)
|
||||
setDropAction(null)
|
||||
const dragId = draggedId || e.dataTransfer.getData('text/plain')
|
||||
if (!dragId || dragId === targetId) {
|
||||
setDraggedId(null)
|
||||
dragOverId.current = null
|
||||
isSavingRef.current = false
|
||||
return
|
||||
}
|
||||
setDraggedId(null)
|
||||
dragOverId.current = null
|
||||
try {
|
||||
await moveNotebookToParent(dragId, targetId)
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : t('sidebar.moveFailed') || 'Failed to move notebook')
|
||||
setOrderedNotebooks(sortedNotebooks)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDropToRoot = async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDropTarget(null)
|
||||
setDropAction(null)
|
||||
const dragId = draggedId || e.dataTransfer.getData('text/plain')
|
||||
if (!dragId) {
|
||||
setDraggedId(null)
|
||||
return
|
||||
}
|
||||
setDraggedId(null)
|
||||
dragOverId.current = null
|
||||
try {
|
||||
await moveNotebookToParent(dragId, null)
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : t('sidebar.moveFailed') || 'Failed to move notebook')
|
||||
setOrderedNotebooks(sortedNotebooks)
|
||||
}
|
||||
}
|
||||
@@ -604,6 +611,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
newest: t('sidebar.sortNewest'),
|
||||
oldest: t('sidebar.sortOldest'),
|
||||
alpha: t('sidebar.sortAlpha'),
|
||||
manual: t('sidebar.sortManual'),
|
||||
}
|
||||
|
||||
const toggleExpand = useCallback((id: string) => {
|
||||
@@ -623,6 +631,22 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
} catch {}
|
||||
}, [])
|
||||
|
||||
// Auto-expand all parent notebooks on first load
|
||||
useEffect(() => {
|
||||
if (orderedNotebooks.length === 0) return
|
||||
const parentIds = new Set<string>()
|
||||
for (const nb of orderedNotebooks) {
|
||||
if (nb.parentId) parentIds.add(nb.parentId)
|
||||
}
|
||||
if (parentIds.size > 0) {
|
||||
setExpandedIds(prev => {
|
||||
const next = new Set(prev)
|
||||
for (const id of parentIds) next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}, [orderedNotebooks])
|
||||
|
||||
const togglePin = useCallback((id: string) => {
|
||||
setPinnedIds(prev => {
|
||||
const next = new Set(prev)
|
||||
@@ -701,21 +725,49 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
const notes = notebookNotes[notebook.id] || []
|
||||
const isDragging = draggedId === notebook.id
|
||||
const children = childNotebooks.get(notebook.id) || []
|
||||
const hasActiveDescendant = children.some(c =>
|
||||
currentNotebookId === c.id || (childNotebooks.get(c.id) || []).some(gc => currentNotebookId === gc.id)
|
||||
)
|
||||
const hasDescendant = (nid: string): boolean => {
|
||||
const desc = childNotebooks.get(nid) || []
|
||||
return desc.some(c => currentNotebookId === c.id || hasDescendant(c.id))
|
||||
}
|
||||
const hasActiveDescendant = hasDescendant(notebook.id)
|
||||
// A notebook stays expanded if: manually expanded, has active descendant, OR is pinned
|
||||
const isExpanded = expandedIds.has(notebook.id) || hasActiveDescendant || pinnedIds.has(notebook.id)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={notebook.id}
|
||||
>
|
||||
<motion.div key={notebook.id}>
|
||||
<div
|
||||
draggable={level === 0}
|
||||
onDragStart={level === 0 ? (e) => handleDragStart(e, notebook.id) : undefined}
|
||||
onDragOver={level === 0 ? (e) => handleDragOver(e, notebook.id) : undefined}
|
||||
onDragEnd={level === 0 ? handleDragEnd : undefined}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDragStart(e, notebook.id)
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!draggedId || draggedId === notebook.id) return
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
setDropTarget(notebook.id)
|
||||
setDropAction('into')
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
if (e.currentTarget.contains(e.relatedTarget as Node)) return
|
||||
if (dropTarget === notebook.id) {
|
||||
setDropTarget(null)
|
||||
setDropAction(null)
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleDropOnNotebook(e, notebook.id)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-lg transition-colors',
|
||||
dropTarget === notebook.id && dropAction === 'into' && draggedId && draggedId !== notebook.id
|
||||
&& 'bg-brand-accent/10 ring-1 ring-brand-accent/30',
|
||||
isDragging && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
<SidebarCarnetItem
|
||||
carnet={{
|
||||
@@ -749,117 +801,122 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
level={level}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpand={() => toggleExpand(notebook.id)}
|
||||
>
|
||||
{renderCarnetTree(notebook.id, level + 1)}
|
||||
</SidebarCarnetItem>
|
||||
hasChildNotebooks={children.length > 0}
|
||||
/>
|
||||
</div>
|
||||
{isExpanded && renderCarnetTree(notebook.id, level + 1)}
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
}, [rootNotebooks, childNotebooks, currentNotebookId, currentNoteId, notebookNotes, draggedId, expandedIds, toggleExpand, handleCarnetClick, handleNoteClick, handleDragStart, handleDragOver, handleDragEnd, handleStartRename])
|
||||
}, [rootNotebooks, childNotebooks, currentNotebookId, currentNoteId, notebookNotes, draggedId, dropTarget, dropAction, expandedIds, toggleExpand, handleCarnetClick, handleNoteClick, handleDragStart, handleDragEnd, handleDropOnNotebook, handleStartRename])
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
className={cn(
|
||||
'hidden h-full min-h-0 w-72 shrink-0 flex-col lg:w-80 md:flex',
|
||||
'border-e border-border/40 bg-memento-sidebar backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#252525] dark:backdrop-blur-none',
|
||||
'border-e border-border/40 bg-white/30 backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#151515] dark:backdrop-blur-none',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* ── Top: Avatar + View Toggle ── */}
|
||||
<div className="p-6 flex items-center justify-between mb-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded-full outline-none ring-offset-background transition-shadow hover:ring-2 hover:ring-primary/30 focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label={t('sidebar.accountMenu')}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-secondary border border-black/10 flex items-center justify-center text-foreground font-memento-serif text-lg shadow-sm">
|
||||
{user?.image ? (
|
||||
<Avatar className="size-10 ring-1 ring-border/60">
|
||||
<AvatarImage src={user.image} alt="" />
|
||||
<AvatarFallback className="bg-secondary text-sm font-semibold text-muted-ink">{initial}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<span>{initial}</span>
|
||||
)}
|
||||
{/* ── Top: Logo + Icons + View Toggle ── */}
|
||||
<div className="p-6 mb-8 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex items-center gap-2 group/logo cursor-pointer">
|
||||
<div className="w-10 h-10 bg-brand-accent flex items-center justify-center rounded-xl shadow-lg shadow-brand-accent/10 rotate-3 group-hover/logo:rotate-0 transition-all duration-500">
|
||||
<span className="text-white font-serif text-xl font-bold">M</span>
|
||||
</div>
|
||||
<span className="text-lg font-serif font-bold tracking-tight text-ink dark:text-paper">Memento</span>
|
||||
</div>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-52 bg-popover border-border">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/profile" className="flex items-center gap-2 cursor-pointer">
|
||||
<User className="h-4 w-4" />
|
||||
{t('sidebar.profile') || 'Profil'}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings" className="flex items-center gap-2 cursor-pointer">
|
||||
<Settings className="h-4 w-4" />
|
||||
{t('nav.settings') || 'Paramètres'}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{(user as { role?: string } | undefined)?.role === 'ADMIN' && (
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-52 bg-popover border-border">
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/admin" className="flex items-center gap-2 cursor-pointer">
|
||||
<Shield className="h-4 w-4" />
|
||||
{t('nav.adminDashboard') || 'Administration'}
|
||||
</a>
|
||||
<Link href="/settings/profile" className="flex items-center gap-2 cursor-pointer">
|
||||
<User className="h-4 w-4" />
|
||||
{t('sidebar.profile') || 'Profil'}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
>
|
||||
<LogOut className="h-4 w-4 me-2" />
|
||||
{t('sidebar.signOut') || 'Se déconnecter'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{(user as { role?: string } | undefined)?.role === 'ADMIN' && (
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/admin" className="flex items-center gap-2 cursor-pointer">
|
||||
<Shield className="h-4 w-4" />
|
||||
{t('nav.adminDashboard') || 'Administration'}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
>
|
||||
<LogOut className="h-4 w-4 me-2" />
|
||||
{t('sidebar.signOut') || 'Se déconnecter'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 text-muted-foreground hover:text-foreground transition-all bg-white/50 dark:bg-white/10 rounded-full border border-border dark:border-white/10"
|
||||
>
|
||||
{isDark ? <Sun size={14} /> : <Moon size={14} />}
|
||||
</button>
|
||||
|
||||
<NotificationPanel />
|
||||
<div className="flex bg-white/50 dark:bg-white/5 p-1 rounded-full border border-border dark:border-white/10 transition-all">
|
||||
<button
|
||||
onClick={() => { setActiveView('notebooks'); if (pathname !== '/') router.push('/') }}
|
||||
className={cn('p-1.5 rounded-full transition-all', activeView === 'notebooks' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink')}
|
||||
title={t('nav.notebooks')}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Link
|
||||
href="/settings"
|
||||
className={cn(
|
||||
'p-1.5 transition-all rounded-lg border flex items-center justify-center',
|
||||
pathname.startsWith('/settings')
|
||||
? 'bg-brand-accent text-white border-brand-accent shadow-lg shadow-brand-accent/20'
|
||||
: 'text-muted-ink hover:text-ink hover:bg-white/50 dark:hover:bg-white/10 border-transparent hover:border-border'
|
||||
)}
|
||||
title={t('nav.settings')}
|
||||
>
|
||||
<BookOpen size={14} />
|
||||
<Settings size={14} />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => { router.push('/') }}
|
||||
className="p-1.5 text-muted-ink hover:text-ink transition-all hover:bg-white/50 dark:hover:bg-white/10 rounded-lg border border-transparent hover:border-border"
|
||||
title={t('nav.home') || 'Accueil'}
|
||||
>
|
||||
<Home size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('reminders')}
|
||||
className={cn('p-1.5 rounded-full transition-all', activeView === 'reminders' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink')}
|
||||
title={t('sidebar.reminders')}
|
||||
onClick={toggleTheme}
|
||||
className="p-1.5 text-muted-ink hover:text-ink transition-all hover:bg-white/50 dark:hover:bg-white/10 rounded-lg border border-transparent hover:border-border"
|
||||
>
|
||||
<Clock size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveView('agents'); router.push('/agents') }}
|
||||
className={cn('p-1.5 rounded-full transition-all', activeView === 'agents' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink')}
|
||||
title={t('nav.agents')}
|
||||
>
|
||||
<Bot size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveView('brainstorms'); router.push('/brainstorm') }}
|
||||
className={cn('p-1.5 rounded-full transition-all', activeView === 'brainstorms' ? 'bg-orange-500 text-white shadow-sm' : 'text-muted-ink hover:text-ink')}
|
||||
title={t('brainstorm.sessions')}
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
{isDark ? <Sun size={14} /> : <Moon size={14} />}
|
||||
</button>
|
||||
<NotificationPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex bg-white/50 dark:bg-white/10 p-1 rounded-xl border border-border dark:border-white/10">
|
||||
<button
|
||||
onClick={() => { setActiveView('notebooks'); if (pathname !== '/') router.push('/') }}
|
||||
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'notebooks' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
|
||||
title={t('nav.notebooks')}
|
||||
>
|
||||
<BookOpen size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('reminders')}
|
||||
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'reminders' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
|
||||
title={t('sidebar.reminders')}
|
||||
>
|
||||
<Clock size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveView('agents'); router.push('/agents') }}
|
||||
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'agents' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
|
||||
title={t('nav.agents')}
|
||||
>
|
||||
<Bot size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveView('brainstorms'); router.push('/brainstorm') }}
|
||||
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'brainstorms' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
|
||||
title={t('brainstorm.sessions')}
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Scrollable content ── */}
|
||||
@@ -903,7 +960,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
exit={{ opacity: 0, scale: 0.9, y: -4 }}
|
||||
className="absolute end-0 top-full mt-1 bg-card border border-border rounded-xl shadow-lg z-50 py-1 min-w-[140px]"
|
||||
>
|
||||
{(['newest', 'oldest', 'alpha'] as SortOrder[]).map(order => (
|
||||
{(['newest', 'oldest', 'alpha', 'manual'] as SortOrder[]).map(order => (
|
||||
<button
|
||||
key={order}
|
||||
onClick={() => { setSortOrder(order); setShowSortMenu(false) }}
|
||||
@@ -950,11 +1007,18 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
{/* Notebooks list — draggable */}
|
||||
<div
|
||||
className="space-y-0.5"
|
||||
onDrop={handleDrop}
|
||||
className="space-y-0.5 min-h-[60px]"
|
||||
onDrop={handleDropToRoot}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
{renderCarnetTree(undefined, 0)}
|
||||
{draggedId && (
|
||||
<div
|
||||
className="h-10 rounded-lg border-2 border-dashed border-brand-accent/20 flex items-center justify-center text-[11px] text-brand-accent/50"
|
||||
>
|
||||
{t('sidebar.dropToRoot') || 'Drop here to move to root'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : activeView === 'reminders' ? (
|
||||
@@ -1028,7 +1092,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push('/brainstorm')}
|
||||
className="p-1 text-muted-foreground hover:text-orange-500 transition-colors rounded"
|
||||
className="p-1 text-muted-foreground hover:text-brand-accent transition-colors rounded"
|
||||
title={t('brainstorm.newBrainstorm')}
|
||||
>
|
||||
<Plus size={12} />
|
||||
@@ -1041,24 +1105,25 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</div>
|
||||
|
||||
{/* ── Footer ── */}
|
||||
<div className="pt-4 border-t border-border/40 mt-auto pb-4">
|
||||
<div className="pt-4 border-t border-border/40 mt-auto pb-4 space-y-4">
|
||||
<UsageMeter />
|
||||
<div className="px-2 space-y-0.5">
|
||||
<Link
|
||||
href="/?shared=1&forceList=1"
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl',
|
||||
searchParams.get('shared') === '1' && pathname === '/'
|
||||
? 'bg-blueprint/5 text-blueprint'
|
||||
: 'text-muted-ink hover:text-ink hover:bg-foreground/5'
|
||||
? 'bg-accent/5 text-accent'
|
||||
: 'text-muted-ink hover:text-ink hover:bg-black/5'
|
||||
)}
|
||||
>
|
||||
<Users size={14} className={searchParams.get('shared') === '1' && pathname === '/' ? 'text-blueprint' : 'text-muted-ink group-hover:text-ink'} />
|
||||
<Users size={14} className={searchParams.get('shared') === '1' && pathname === '/' ? 'text-accent' : 'text-muted-ink group-hover:text-ink'} />
|
||||
<span>{t('sidebar.sharedWithMe') || 'Shared'}</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/archive"
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-[12px] text-muted-ink hover:text-ink hover:bg-foreground/5 transition-all font-medium group rounded-xl"
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-[12px] text-muted-ink hover:text-ink hover:bg-black/5 transition-all font-medium group rounded-xl"
|
||||
>
|
||||
<Archive size={14} className="text-muted-ink group-hover:text-ink" />
|
||||
<span>{t('sidebar.archive')}</span>
|
||||
@@ -1081,19 +1146,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</Link>
|
||||
|
||||
<div className="my-2 h-px bg-border/20 mx-2" />
|
||||
|
||||
<Link
|
||||
href="/settings"
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl',
|
||||
pathname.startsWith('/settings')
|
||||
? 'bg-ink text-paper shadow-sm'
|
||||
: 'text-muted-ink hover:text-ink hover:bg-foreground/5'
|
||||
)}
|
||||
>
|
||||
<Settings size={14} className={pathname.startsWith('/settings') ? 'text-paper' : 'text-muted-ink group-hover:text-ink'} />
|
||||
<span>{t('nav.settings')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user